diff --git a/EVENTS.txt b/EVENTS.txt index 90242fa133..bb4936b354 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -729,3 +729,66 @@ StartGetProfileUri: When determining the canonical URI for a given profile EndGetProfileUri: After determining the canonical URI for a given profile - $profile: the current profile - &$uri: the URI + +StartFavorNotice: Saving a notice as a favorite +- $profile: profile of the person faving (can be remote!) +- $notice: notice being faved +- &$fave: Favor object; null to start off with, but feel free to override. + +EndFavorNotice: After saving a notice as a favorite +- $profile: profile of the person faving (can be remote!) +- $notice: notice being faved + +StartDisfavorNotice: Saving a notice as a favorite +- $profile: profile of the person faving (can be remote!) +- $notice: notice being faved +- &$result: result of the disfavoring (if you override) + +EndDisfavorNotice: After saving a notice as a favorite +- $profile: profile of the person faving (can be remote!) +- $notice: notice being faved + +StartFindMentions: start finding mentions in a block of text +- $sender: sender profile +- $text: plain text version of the notice +- &$mentions: mentions found so far. Array of arrays; each array + has 'mentioned' (array of mentioned profiles), 'url' (url to link as), + 'title' (title of the link), 'position' (position of the text to + replace), 'text' (text to replace) + +EndFindMentions: end finding mentions in a block of text +- $sender: sender profile +- $text: plain text version of the notice +- &$mentions: mentions found so far. Array of arrays; each array + has 'mentioned' (array of mentioned profiles), 'url' (url to link as), + 'title' (title of the link), 'position' (position of the text to + replace), 'text' (text to replace) + +StartShowSubscriptionsContent: before showing the subscriptions content +- $action: the current action + +EndShowSubscriptionsContent: after showing the subscriptions content +- $action: the current action + +StartShowAllContent: before showing the all (you and friends) content +- $action: the current action + +EndShowAllContent: after showing the all (you and friends) content +- $action: the current action + +StartDeleteUserForm: starting the data in the form for deleting a user +- $action: action being shown +- $user: user being deleted + +EndDeleteUserForm: Ending the data in the form for deleting a user +- $action: action being shown +- $user: user being deleted + +StartDeleteUser: handling the post for deleting a user +- $action: action being shown +- $user: user being deleted + +EndDeleteUser: handling the post for deleting a user +- $action: action being shown +- $user: user being deleted + diff --git a/actions/accessadminpanel.php b/actions/accessadminpanel.php index 4768e2faf9..73354e97a7 100644 --- a/actions/accessadminpanel.php +++ b/actions/accessadminpanel.php @@ -51,6 +51,7 @@ class AccessadminpanelAction extends AdminPanelAction function title() { + // TRANS: Page title return _('Access'); } @@ -62,6 +63,7 @@ class AccessadminpanelAction extends AdminPanelAction function getInstructions() { + // TRANS: Page notice return _('Site access settings'); } @@ -155,24 +157,34 @@ class AccessAdminPanelForm extends AdminForm function formData() { $this->out->elementStart('fieldset', array('id' => 'settings_admin_access')); + // TRANS: Form legend for registration form. $this->out->element('legend', null, _('Registration')); $this->out->elementStart('ul', 'form_data'); $this->li(); - $this->out->checkbox('private', _('Private'), + // TRANS: Checkbox instructions for admin setting "Private" + $instructions = _('Prohibit anonymous users (not logged in) from viewing site?'); + // TRANS: Checkbox label for prohibiting anonymous users from viewing site. + $this->out->checkbox('private', _m('LABEL', 'Private'), (bool) $this->value('private'), - _('Prohibit anonymous users (not logged in) from viewing site?')); + $instructions); $this->unli(); $this->li(); + // TRANS: Checkbox instructions for admin setting "Invite only" + $instructions = _('Make registration invitation only.'); + // TRANS: Checkbox label for configuring site as invite only. $this->out->checkbox('inviteonly', _('Invite only'), (bool) $this->value('inviteonly'), - _('Make registration invitation only.')); + $instructions); $this->unli(); $this->li(); + // TRANS: Checkbox instructions for admin setting "Closed" (no new registrations) + $instructions = _('Disable new registrations.'); + // TRANS: Checkbox label for disabling new user registrations. $this->out->checkbox('closed', _('Closed'), (bool) $this->value('closed'), - _('Disable new registrations.')); + $instructions); $this->unli(); $this->out->elementEnd('ul'); $this->out->elementEnd('fieldset'); @@ -186,7 +198,9 @@ class AccessAdminPanelForm extends AdminForm function formActions() { - $this->out->submit('submit', _('Save'), 'submit', null, _('Save access settings')); + // TRANS: Title / tooltip for button to save access settings in site admin panel + $title = _('Save access settings'); + $this->out->submit('submit', _m('BUTTON', 'Save'), 'submit', null, $title); } } diff --git a/actions/all.php b/actions/all.php index 3eb1852147..8c22e6f5f0 100644 --- a/actions/all.php +++ b/actions/all.php @@ -60,6 +60,7 @@ class AllAction extends ProfileAction } if ($this->page > 1 && $this->notice->N == 0) { + // TRANS: Server error when page not found (404) $this->serverError(_('No such page'), $code = 404); } @@ -81,8 +82,10 @@ class AllAction extends ProfileAction function title() { if ($this->page > 1) { + // TRANS: Page title. %1$s is user nickname, %2$d is page number return sprintf(_('%1$s and friends, page %2$d'), $this->user->nickname, $this->page); } else { + // TRANS: Page title. %1$s is user nickname return sprintf(_("%s and friends"), $this->user->nickname); } } @@ -96,6 +99,7 @@ class AllAction extends ProfileAction 'nickname' => $this->user->nickname) ), + // TRANS: %1$s is user nickname sprintf(_('Feed for friends of %s (RSS 1.0)'), $this->user->nickname)), new Feed(Feed::RSS2, common_local_url( @@ -104,6 +108,7 @@ class AllAction extends ProfileAction 'id' => $this->user->nickname ) ), + // TRANS: %1$s is user nickname sprintf(_('Feed for friends of %s (RSS 2.0)'), $this->user->nickname)), new Feed(Feed::ATOM, common_local_url( @@ -112,6 +117,7 @@ class AllAction extends ProfileAction 'id' => $this->user->nickname ) ), + // TRANS: %1$s is user nickname sprintf(_('Feed for friends of %s (Atom)'), $this->user->nickname)) ); } @@ -124,6 +130,7 @@ class AllAction extends ProfileAction function showEmptyListMessage() { + // TRANS: %1$s is user nickname $message = sprintf(_('This is the timeline for %s and friends but no one has posted anything yet.'), $this->user->nickname) . ' '; if (common_logged_in()) { @@ -131,6 +138,7 @@ class AllAction extends ProfileAction if ($this->user->id === $current_user->id) { $message .= _('Try subscribing to more people, [join a group](%%action.groups%%) or post something yourself.'); } else { + // TRANS: %1$s is user nickname, %2$s is user nickname, %2$s is user nickname prefixed with "@" $message .= sprintf(_('You can try to [nudge %1$s](../%2$s) from his profile or [post something to his or her attention](%%%%action.newnotice%%%%?status_textarea=%3$s).'), $this->user->nickname, $this->user->nickname, '@' . $this->user->nickname); } } else { @@ -144,26 +152,32 @@ class AllAction extends ProfileAction function showContent() { - $nl = new NoticeList($this->notice, $this); + if (Event::handle('StartShowAllContent', array($this))) { + $nl = new NoticeList($this->notice, $this); - $cnt = $nl->show(); + $cnt = $nl->show(); - if (0 == $cnt) { - $this->showEmptyListMessage(); + if (0 == $cnt) { + $this->showEmptyListMessage(); + } + + $this->pagination( + $this->page > 1, $cnt > NOTICES_PER_PAGE, + $this->page, 'all', array('nickname' => $this->user->nickname) + ); + + Event::handle('EndShowAllContent', array($this)); } - - $this->pagination( - $this->page > 1, $cnt > NOTICES_PER_PAGE, - $this->page, 'all', array('nickname' => $this->user->nickname) - ); } function showPageTitle() { $user = common_current_user(); if ($user && ($user->id == $this->user->id)) { + // TRANS: H1 text $this->element('h1', null, _("You and friends")); } else { + // TRANS: H1 text. %1$s is user nickname $this->element('h1', null, sprintf(_('%s and friends'), $this->user->nickname)); } } diff --git a/actions/apidirectmessage.php b/actions/apidirectmessage.php index 5fbc46518b..53da9e0c68 100644 --- a/actions/apidirectmessage.php +++ b/actions/apidirectmessage.php @@ -79,7 +79,7 @@ class ApiDirectMessageAction extends ApiAuthAction } $server = common_root_url(); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); if ($this->arg('sent')) { @@ -182,11 +182,6 @@ class ApiDirectMessageAction extends ApiAuthAction $message->whereAdd('id > ' . $this->since_id); } - if (!empty($since)) { - $d = date('Y-m-d H:i:s', $this->since); - $message->whereAdd("created > '$d'"); - } - $message->orderBy('created DESC, id DESC'); $message->limit((($this->page - 1) * $this->count), $this->count); $message->find(); diff --git a/actions/apifriendshipsdestroy.php b/actions/apifriendshipsdestroy.php index 91c6fd0324..d48a577562 100644 --- a/actions/apifriendshipsdestroy.php +++ b/actions/apifriendshipsdestroy.php @@ -124,12 +124,9 @@ class ApiFriendshipsDestroyAction extends ApiAuthAction return; } - $result = subs_unsubscribe_user($this->user, $this->other->nickname); - - if (is_string($result)) { - $this->clientError($result, 403, $this->format); - return; - } + // throws an exception on error + Subscription::cancel($this->user->getProfile(), + $this->other->getProfile()); $this->initDocument($this->format); $this->showProfile($this->other, $this->format); diff --git a/actions/apigroupcreate.php b/actions/apigroupcreate.php index 028d76a782..145806356c 100644 --- a/actions/apigroupcreate.php +++ b/actions/apigroupcreate.php @@ -123,7 +123,9 @@ class ApiGroupCreateAction extends ApiAuthAction 'description' => $this->description, 'location' => $this->location, 'aliases' => $this->aliases, - 'userid' => $this->user->id)); + 'userid' => $this->user->id, + 'local' => true)); + switch($this->format) { case 'xml': $this->showSingleXmlGroup($group); @@ -306,9 +308,9 @@ class ApiGroupCreateAction extends ApiAuthAction function groupNicknameExists($nickname) { - $group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); - if (!empty($group)) { + if (!empty($local)) { return true; } diff --git a/actions/apigrouplist.php b/actions/apigrouplist.php index 66b67a030e..98fdb0497a 100644 --- a/actions/apigrouplist.php +++ b/actions/apigrouplist.php @@ -93,7 +93,7 @@ class ApiGroupListAction extends ApiBareAuthAction $sitename = common_config('site', 'name'); $title = sprintf(_("%s's groups"), $this->user->nickname); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $id = "tag:$taguribase:Groups"; $link = common_local_url( 'usergroups', @@ -152,8 +152,7 @@ class ApiGroupListAction extends ApiBareAuthAction ($this->page - 1) * $this->count, $this->count, $this->since_id, - $this->max_id, - $this->since + $this->max_id ); while ($group->fetch()) { diff --git a/actions/apigrouplistall.php b/actions/apigrouplistall.php index 1921c1f193..e1b54a8322 100644 --- a/actions/apigrouplistall.php +++ b/actions/apigrouplistall.php @@ -88,7 +88,7 @@ class ApiGroupListAllAction extends ApiPrivateAuthAction $sitename = common_config('site', 'name'); $title = sprintf(_("%s groups"), $sitename); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $id = "tag:$taguribase:Groups"; $link = common_local_url('groups'); $subtitle = sprintf(_("groups on %s"), $sitename); @@ -134,13 +134,13 @@ class ApiGroupListAllAction extends ApiPrivateAuthAction function getGroups() { - $groups = array(); - - // XXX: Use the $page, $count, $max_id, $since_id, and $since parameters + $qry = 'SELECT user_group.* '. + 'from user_group join local_group on user_group.id = local_group.group_id '. + 'order by created desc '; $group = new User_group(); - $group->orderBy('created DESC'); - $group->find(); + + $group->query($qry); while ($group->fetch()) { $groups[] = clone($group); diff --git a/actions/apigroupmembership.php b/actions/apigroupmembership.php index 3c7c8e8835..9f72b527cf 100644 --- a/actions/apigroupmembership.php +++ b/actions/apigroupmembership.php @@ -125,8 +125,7 @@ class ApiGroupMembershipAction extends ApiPrivateAuthAction ($this->page - 1) * $this->count, $this->count, $this->since_id, - $this->max_id, - $this->since + $this->max_id ); while ($profile->fetch()) { diff --git a/actions/apistatusnetconfig.php b/actions/apistatusnetconfig.php index 0345a9bc07..bff8313b5c 100644 --- a/actions/apistatusnetconfig.php +++ b/actions/apistatusnetconfig.php @@ -32,8 +32,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/api.php'; - /** * Gives a full dump of configuration variables for this instance * of StatusNet, minus variables that may be security-sensitive (like diff --git a/actions/apitimelinefavorites.php b/actions/apitimelinefavorites.php index f7f900ddfb..c89d02247a 100644 --- a/actions/apitimelinefavorites.php +++ b/actions/apitimelinefavorites.php @@ -110,7 +110,7 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction $this->user->nickname ); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $id = "tag:$taguribase:Favorites:" . $this->user->id; $subtitle = sprintf( diff --git a/actions/apitimelinefriends.php b/actions/apitimelinefriends.php index 0af04fe4fb..9ef3ace607 100644 --- a/actions/apitimelinefriends.php +++ b/actions/apitimelinefriends.php @@ -112,7 +112,7 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); $sitename = common_config('site', 'name'); $title = sprintf(_("%s and friends"), $this->user->nickname); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $id = "tag:$taguribase:FriendsTimeline:" . $this->user->id; $subtitle = sprintf( @@ -202,11 +202,11 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction if (!empty($this->auth_user) && $this->auth_user->id == $this->user->id) { $notice = $this->user->ownFriendsTimeline(($this->page-1) * $this->count, $this->count, $this->since_id, - $this->max_id, $this->since); + $this->max_id); } else { $notice = $this->user->friendsTimeline(($this->page-1) * $this->count, $this->count, $this->since_id, - $this->max_id, $this->since); + $this->max_id); } while ($notice->fetch()) { diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index 3c74e36b56..d0af49844c 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -107,8 +107,6 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction $sitename = common_config('site', 'name'); $avatar = $this->group->homepage_logo; $title = sprintf(_("%s timeline"), $this->group->nickname); - $taguribase = common_config('integration', 'taguri'); - $id = "tag:$taguribase:GroupTimeline:" . $this->group->id; $subtitle = sprintf( _('Updates from %1$s on %2$s!'), @@ -138,19 +136,9 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction try { - // If this was called using an integer ID, i.e.: using the canonical - // URL for this group's feed, then pass the Group object into the feed, - // so the OStatus plugin, and possibly other plugins, can access it. - // Feels sorta hacky. -- Z + $atom = new AtomGroupNoticeFeed($this->group); - $atom = null; - $id = $this->arg('id'); - - if (strval(intval($id)) === strval($id)) { - $atom = new AtomGroupNoticeFeed($this->group); - } else { - $atom = new AtomGroupNoticeFeed(); - } + // @todo set all this Atom junk up inside the feed class $atom->setId($id); $atom->setTitle($title); @@ -169,6 +157,8 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction $aargs['id'] = $id; } + $atom->setId($this->getSelfUri('ApiTimelineGroup', $aargs)); + $atom->addLink( $this->getSelfUri('ApiTimelineGroup', $aargs), array('rel' => 'self', 'type' => 'application/atom+xml') @@ -176,7 +166,8 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction $atom->addEntryFromNotices($this->notices); - $this->raw($atom->getString()); + //$this->raw($atom->getString()); + print $atom->getString(); // temp hack until PuSH feeds are redone cleanly } catch (Atom10FeedException $e) { $this->serverError( @@ -213,8 +204,7 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction ($this->page-1) * $this->count, $this->count, $this->since_id, - $this->max_id, - $this->since + $this->max_id ); while ($notice->fetch()) { diff --git a/actions/apitimelinehome.php b/actions/apitimelinehome.php index ae41680702..abd3877860 100644 --- a/actions/apitimelinehome.php +++ b/actions/apitimelinehome.php @@ -113,7 +113,7 @@ class ApiTimelineHomeAction extends ApiBareAuthAction $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); $sitename = common_config('site', 'name'); $title = sprintf(_("%s and friends"), $this->user->nickname); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $id = "tag:$taguribase:HomeTimeline:" . $this->user->id; $subtitle = sprintf( @@ -200,13 +200,13 @@ class ApiTimelineHomeAction extends ApiBareAuthAction $notice = $this->user->noticeInbox( ($this->page-1) * $this->count, $this->count, $this->since_id, - $this->max_id, $this->since + $this->max_id ); } else { $notice = $this->user->noticesWithFriends( ($this->page-1) * $this->count, $this->count, $this->since_id, - $this->max_id, $this->since + $this->max_id ); } diff --git a/actions/apitimelinementions.php b/actions/apitimelinementions.php index d2e31d0bdd..31627ab7bf 100644 --- a/actions/apitimelinementions.php +++ b/actions/apitimelinementions.php @@ -117,7 +117,7 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction _('%1$s / Updates mentioning %2$s'), $sitename, $this->user->nickname ); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $id = "tag:$taguribase:Mentions:" . $this->user->id; $link = common_local_url( 'replies', @@ -189,7 +189,7 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction $notice = $this->user->getReplies( ($this->page - 1) * $this->count, $this->count, - $this->since_id, $this->max_id, $this->since + $this->since_id, $this->max_id ); while ($notice->fetch()) { diff --git a/actions/apitimelinepublic.php b/actions/apitimelinepublic.php index c1fa72a3ee..3e4dad690e 100644 --- a/actions/apitimelinepublic.php +++ b/actions/apitimelinepublic.php @@ -75,10 +75,6 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction $this->notices = $this->getNotices(); - if ($this->since) { - throw new ServerException("since parameter is disabled for performance; use since_id", 403); - } - return true; } @@ -109,7 +105,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction $sitename = common_config('site', 'name'); $sitelogo = (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'); $title = sprintf(_("%s public timeline"), $sitename); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $id = "tag:$taguribase:PublicTimeline"; $link = common_root_url(); $subtitle = sprintf(_("%s updates from everyone!"), $sitename); diff --git a/actions/apitimelineretweetedtome.php b/actions/apitimelineretweetedtome.php index e47bc30b85..73e35c86bf 100644 --- a/actions/apitimelineretweetedtome.php +++ b/actions/apitimelineretweetedtome.php @@ -109,7 +109,7 @@ class ApiTimelineRetweetedToMeAction extends ApiAuthAction $profile = $this->auth_user->getProfile(); $title = sprintf(_("Repeated to %s"), $this->auth_user->nickname); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $id = "tag:$taguribase:RepeatedToMe:" . $this->auth_user->id; $link = common_local_url('all', array('nickname' => $this->auth_user->nickname)); diff --git a/actions/apitimelineretweetsofme.php b/actions/apitimelineretweetsofme.php index 26706a75e7..c77912fd0f 100644 --- a/actions/apitimelineretweetsofme.php +++ b/actions/apitimelineretweetsofme.php @@ -112,7 +112,7 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction $profile = $this->auth_user->getProfile(); $title = sprintf(_("Repeats of %s"), $this->auth_user->nickname); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $id = "tag:$taguribase:RepeatsOfMe:" . $this->auth_user->id; header('Content-Type: application/atom+xml; charset=utf-8'); diff --git a/actions/apitimelinetag.php b/actions/apitimelinetag.php index 5b6ded4c04..a29061fccf 100644 --- a/actions/apitimelinetag.php +++ b/actions/apitimelinetag.php @@ -105,7 +105,7 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction $this->tag, $sitename ); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $id = "tag:$taguribase:TagTimeline:".$tag; switch($this->format) { diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 9f7ec4c236..94491946c2 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -116,8 +116,6 @@ class ApiTimelineUserAction extends ApiBareAuthAction $sitename = common_config('site', 'name'); $title = sprintf(_("%s timeline"), $this->user->nickname); - $taguribase = common_config('integration', 'taguri'); - $id = "tag:$taguribase:UserTimeline:" . $this->user->id; $link = common_local_url( 'showstream', array('nickname' => $this->user->nickname) @@ -148,21 +146,10 @@ class ApiTimelineUserAction extends ApiBareAuthAction header('Content-Type: application/atom+xml; charset=utf-8'); - // If this was called using an integer ID, i.e.: using the canonical - // URL for this user's feed, then pass the User object into the feed, - // so the OStatus plugin, and possibly other plugins, can access it. - // Feels sorta hacky. -- Z + // @todo set all this Atom junk up inside the feed class - $atom = null; - $id = $this->arg('id'); + $atom = new AtomUserNoticeFeed($this->user); - if (strval(intval($id)) === strval($id)) { - $atom = new AtomUserNoticeFeed($this->user); - } else { - $atom = new AtomUserNoticeFeed(); - } - - $atom->setId($id); $atom->setTitle($title); $atom->setSubtitle($subtitle); $atom->setLogo($logo); @@ -181,6 +168,8 @@ class ApiTimelineUserAction extends ApiBareAuthAction $aargs['id'] = $id; } + $atom->setId($this->getSelfUri('ApiTimelineUser', $aargs)); + $atom->addLink( $this->getSelfUri('ApiTimelineUser', $aargs), array('rel' => 'self', 'type' => 'application/atom+xml') @@ -222,7 +211,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction $notice = $this->user->getNotices( ($this->page-1) * $this->count, $this->count, - $this->since_id, $this->max_id, $this->since + $this->since_id, $this->max_id ); while ($notice->fetch()) { diff --git a/actions/blockedfromgroup.php b/actions/blockedfromgroup.php index 0b4caf5bf3..a0598db270 100644 --- a/actions/blockedfromgroup.php +++ b/actions/blockedfromgroup.php @@ -74,7 +74,14 @@ class BlockedfromgroupAction extends GroupDesignAction return false; } - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { $this->clientError(_('No such group.'), 404); diff --git a/actions/deleteuser.php b/actions/deleteuser.php index 32b703aa7f..c4f84fad2d 100644 --- a/actions/deleteuser.php +++ b/actions/deleteuser.php @@ -131,18 +131,21 @@ class DeleteuserAction extends ProfileFormAction $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); $this->element('legend', _('Delete user')); - $this->element('p', null, - _('Are you sure you want to delete this user? '. - 'This will clear all data about the user from the '. - 'database, without a backup.')); - $this->element('input', array('id' => 'deleteuserto-' . $id, - 'name' => 'profileid', - 'type' => 'hidden', - 'value' => $id)); - foreach ($this->args as $k => $v) { - if (substr($k, 0, 9) == 'returnto-') { - $this->hidden($k, $v); + if (Event::handle('StartDeleteUserForm', array($this, $this->user))) { + $this->element('p', null, + _('Are you sure you want to delete this user? '. + 'This will clear all data about the user from the '. + 'database, without a backup.')); + $this->element('input', array('id' => 'deleteuserto-' . $id, + 'name' => 'profileid', + 'type' => 'hidden', + 'value' => $id)); + foreach ($this->args as $k => $v) { + if (substr($k, 0, 9) == 'returnto-') { + $this->hidden($k, $v); + } } + Event::handle('EndDeleteUserForm', array($this, $this->user)); } $this->submit('form_action-no', _('No'), 'submit form_action-primary', 'no', _("Do not block this user")); $this->submit('form_action-yes', _('Yes'), 'submit form_action-secondary', 'yes', _('Delete this user')); @@ -158,7 +161,9 @@ class DeleteuserAction extends ProfileFormAction function handlePost() { - $this->user->delete(); + if (Event::handle('StartDeleteUser', array($this, $this->user))) { + $this->user->delete(); + Event::handle('EndDeleteUser', array($this, $this->user)); + } } } - diff --git a/actions/doc.php b/actions/doc.php index eaf4b7df2d..459f5f0968 100644 --- a/actions/doc.php +++ b/actions/doc.php @@ -173,6 +173,10 @@ class DocAction extends Action } $local = glob(INSTALLDIR.'/local/doc-src/'.$this->title.'.*'); + if ($local === false) { + // Some systems return false, others array(), if dir didn't exist. + $local = array(); + } if (count($local) || isset($localDef)) { return $this->negotiateLanguage($local, $localDef); @@ -183,6 +187,9 @@ class DocAction extends Action } $dist = glob(INSTALLDIR.'/doc-src/'.$this->title.'.*'); + if ($dist === false) { + $dist = array(); + } if (count($dist) || isset($distDef)) { return $this->negotiateLanguage($dist, $distDef); diff --git a/actions/editgroup.php b/actions/editgroup.php index ad0b6e185d..4b596cade9 100644 --- a/actions/editgroup.php +++ b/actions/editgroup.php @@ -86,10 +86,14 @@ class EditgroupAction extends GroupDesignAction } $groupid = $this->trimmed('groupid'); + if ($groupid) { $this->group = User_group::staticGet('id', $groupid); } else { - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + if ($local) { + $this->group = User_group::staticGet('id', $local->group_id); + } } if (!$this->group) { @@ -245,6 +249,7 @@ class EditgroupAction extends GroupDesignAction $this->group->homepage = $homepage; $this->group->description = $description; $this->group->location = $location; + $this->group->mainpage = common_local_url('showgroup', array('nickname' => $nickname)); $result = $this->group->update($orig); @@ -259,6 +264,12 @@ class EditgroupAction extends GroupDesignAction $this->serverError(_('Could not create aliases.')); } + if ($nickname != $orig->nickname) { + common_log(LOG_INFO, "Saving local group info."); + $local = Local_group::staticGet('group_id', $this->group->id); + $local->setNickname($nickname); + } + $this->group->query('COMMIT'); if ($this->group->nickname != $orig->nickname) { @@ -272,10 +283,10 @@ class EditgroupAction extends GroupDesignAction function nicknameExists($nickname) { - $group = User_group::staticGet('nickname', $nickname); + $group = Local_group::staticGet('nickname', $nickname); if (!empty($group) && - $group->id != $this->group->id) { + $group->group_id != $this->group->id) { return true; } diff --git a/actions/favor.php b/actions/favor.php index 2aeb1da613..afca9768ae 100644 --- a/actions/favor.php +++ b/actions/favor.php @@ -79,7 +79,7 @@ class FavorAction extends Action $this->clientError(_('This notice is already a favorite!')); return; } - $fave = Fave::addNew($user, $notice); + $fave = Fave::addNew($user->getProfile(), $notice); if (!$fave) { $this->serverError(_('Could not create favorite.')); return; diff --git a/actions/foafgroup.php b/actions/foafgroup.php index f5fd7fe885..ebdf1cee25 100644 --- a/actions/foafgroup.php +++ b/actions/foafgroup.php @@ -56,7 +56,14 @@ class FoafGroupAction extends Action return false; } - $this->group = User_group::staticGet('nickname', $this->nickname); + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { $this->clientError(_('No such group.'), 404); @@ -113,7 +120,7 @@ class FoafGroupAction extends Action if ($this->group->homepage_logo) { $this->element('depiction', array('rdf:resource' => $this->group->homepage_logo)); } - + $members = $this->group->getMembers(); $member_details = array(); while ($members->fetch()) { @@ -123,7 +130,7 @@ class FoafGroupAction extends Action ); $this->element('member', array('rdf:resource' => $member_uri)); } - + $admins = $this->group->getAdmins(); while ($admins->fetch()) { $admin_uri = common_local_url('userbyid', array('id'=>$admins->id)); @@ -132,7 +139,7 @@ class FoafGroupAction extends Action } $this->elementEnd('Group'); - + ksort($member_details); foreach ($member_details as $uri => $details) { if ($details['is_admin']) @@ -158,7 +165,7 @@ class FoafGroupAction extends Action )); } } - + $this->elementEnd('rdf:RDF'); $this->endXML(); } diff --git a/actions/groupdesignsettings.php b/actions/groupdesignsettings.php index e290ba5141..526226a285 100644 --- a/actions/groupdesignsettings.php +++ b/actions/groupdesignsettings.php @@ -90,7 +90,10 @@ class GroupDesignSettingsAction extends DesignSettingsAction if ($groupid) { $this->group = User_group::staticGet('id', $groupid); } else { - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + if ($local) { + $this->group = User_group::staticGet('id', $local->group_id); + } } if (!$this->group) { diff --git a/actions/grouplogo.php b/actions/grouplogo.php index 3c9b562962..f414a23cc3 100644 --- a/actions/grouplogo.php +++ b/actions/grouplogo.php @@ -92,7 +92,10 @@ class GrouplogoAction extends GroupDesignAction if ($groupid) { $this->group = User_group::staticGet('id', $groupid); } else { - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + if ($local) { + $this->group = User_group::staticGet('id', $local->group_id); + } } if (!$this->group) { diff --git a/actions/groupmembers.php b/actions/groupmembers.php index f16e972a41..a16debd7b0 100644 --- a/actions/groupmembers.php +++ b/actions/groupmembers.php @@ -77,7 +77,14 @@ class GroupmembersAction extends GroupDesignAction return false; } - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { $this->clientError(_('No such group.'), 404); diff --git a/actions/grouprss.php b/actions/grouprss.php index 866fc66eb1..490f6f945c 100644 --- a/actions/grouprss.php +++ b/actions/grouprss.php @@ -92,7 +92,14 @@ class groupRssAction extends Rss10Action return false; } - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { $this->clientError(_('No such group.'), 404); diff --git a/actions/groups.php b/actions/groups.php index 10a1d5964d..8aacff8b0e 100644 --- a/actions/groups.php +++ b/actions/groups.php @@ -109,17 +109,21 @@ class GroupsAction extends Action } $offset = ($this->page-1) * GROUPS_PER_PAGE; - $limit = GROUPS_PER_PAGE + 1; + $limit = GROUPS_PER_PAGE + 1; + + $qry = 'SELECT user_group.* '. + 'from user_group join local_group on user_group.id = local_group.group_id '. + 'order by user_group.created desc '. + 'limit ' . $limit . ' offset ' . $offset; $groups = new User_group(); - $groups->orderBy('created DESC'); - $groups->limit($offset, $limit); $cnt = 0; - if ($groups->find()) { - $gl = new GroupList($groups, null, $this); - $cnt = $gl->show(); - } + + $groups->query($qry); + + $gl = new GroupList($groups, null, $this); + $cnt = $gl->show(); $this->pagination($this->page > 1, $cnt > GROUPS_PER_PAGE, $this->page, 'groups'); diff --git a/actions/hcard.php b/actions/hcard.php new file mode 100644 index 0000000000..55d0f65c8f --- /dev/null +++ b/actions/hcard.php @@ -0,0 +1,120 @@ +. + * + * @category Personal + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * User profile page + * + * @category Personal + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class HcardAction extends Action +{ + var $user; + var $profile; + + function prepare($args) + { + parent::prepare($args); + + $nickname_arg = $this->arg('nickname'); + $nickname = common_canonical_nickname($nickname_arg); + + // Permanent redirect on non-canonical nickname + + if ($nickname_arg != $nickname) { + $args = array('nickname' => $nickname); + common_redirect(common_local_url('hcard', $args), 301); + return false; + } + + $this->user = User::staticGet('nickname', $nickname); + + if (!$this->user) { + $this->clientError(_('No such user.'), 404); + return false; + } + + $this->profile = $this->user->getProfile(); + + if (!$this->profile) { + $this->serverError(_('User has no profile.')); + return false; + } + + return true; + } + + function handle($args) + { + parent::handle($args); + $this->showPage(); + } + + function title() + { + return $this->profile->getBestName(); + } + + function showContent() + { + $up = new ShortUserProfile($this, $this->user, $this->profile); + $up->show(); + } + + function showHeader() + { + return; + } + + function showAside() + { + return; + } + + function showSecondaryNav() + { + return; + } +} + +class ShortUserProfile extends UserProfile +{ + function showEntityActions() + { + return; + } +} \ No newline at end of file diff --git a/actions/invite.php b/actions/invite.php index d0ed64ec90..54b2de62ac 100644 --- a/actions/invite.php +++ b/actions/invite.php @@ -194,7 +194,8 @@ class InviteAction extends CurrentUserDesignAction _('Optionally add a personal message to the invitation.')); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('send', _('Send')); + // TRANS: Send button for inviting friends + $this->submit('send', _m('BUTTON', 'Send')); $this->elementEnd('fieldset'); $this->elementEnd('form'); } diff --git a/actions/joingroup.php b/actions/joingroup.php index 235e5ab4c2..f87e5dae29 100644 --- a/actions/joingroup.php +++ b/actions/joingroup.php @@ -62,23 +62,33 @@ class JoingroupAction extends Action } $nickname_arg = $this->trimmed('nickname'); - $nickname = common_canonical_nickname($nickname_arg); + $id = intval($this->arg('id')); + if ($id) { + $this->group = User_group::staticGet('id', $id); + } else if ($nickname_arg) { + $nickname = common_canonical_nickname($nickname_arg); - // Permanent redirect on non-canonical nickname + // Permanent redirect on non-canonical nickname - if ($nickname_arg != $nickname) { - $args = array('nickname' => $nickname); - common_redirect(common_local_url('joingroup', $args), 301); + if ($nickname_arg != $nickname) { + $args = array('nickname' => $nickname); + common_redirect(common_local_url('leavegroup', $args), 301); + return false; + } + + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); + } else { + $this->clientError(_('No nickname or ID.'), 404); return false; } - if (!$nickname) { - $this->clientError(_('No nickname.'), 404); - return false; - } - - $this->group = User_group::staticGet('nickname', $nickname); - if (!$this->group) { $this->clientError(_('No such group.'), 404); return false; diff --git a/actions/leavegroup.php b/actions/leavegroup.php index 9b9d83b6ca..329b5aafe1 100644 --- a/actions/leavegroup.php +++ b/actions/leavegroup.php @@ -62,23 +62,33 @@ class LeavegroupAction extends Action } $nickname_arg = $this->trimmed('nickname'); - $nickname = common_canonical_nickname($nickname_arg); + $id = intval($this->arg('id')); + if ($id) { + $this->group = User_group::staticGet('id', $id); + } else if ($nickname_arg) { + $nickname = common_canonical_nickname($nickname_arg); - // Permanent redirect on non-canonical nickname + // Permanent redirect on non-canonical nickname - if ($nickname_arg != $nickname) { - $args = array('nickname' => $nickname); - common_redirect(common_local_url('leavegroup', $args), 301); + if ($nickname_arg != $nickname) { + $args = array('nickname' => $nickname); + common_redirect(common_local_url('leavegroup', $args), 301); + return false; + } + + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); + } else { + $this->clientError(_('No nickname or ID.'), 404); return false; } - if (!$nickname) { - $this->clientError(_('No nickname.'), 404); - return false; - } - - $this->group = User_group::staticGet('nickname', $nickname); - if (!$this->group) { $this->clientError(_('No such group.'), 404); return false; diff --git a/actions/newgroup.php b/actions/newgroup.php index 25da7f8fc7..75bc293ec6 100644 --- a/actions/newgroup.php +++ b/actions/newgroup.php @@ -180,6 +180,8 @@ class NewgroupAction extends Action } } + $mainpage = common_local_url('showgroup', array('nickname' => $nickname)); + $cur = common_current_user(); // Checked in prepare() above @@ -192,16 +194,18 @@ class NewgroupAction extends Action 'description' => $description, 'location' => $location, 'aliases' => $aliases, - 'userid' => $cur->id)); + 'userid' => $cur->id, + 'mainpage' => $mainpage, + 'local' => true)); common_redirect($group->homeUrl(), 303); } function nicknameExists($nickname) { - $group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); - if (!empty($group)) { + if (!empty($local)) { return true; } diff --git a/actions/newnotice.php b/actions/newnotice.php index 78480ababb..ed0fa1b2b5 100644 --- a/actions/newnotice.php +++ b/actions/newnotice.php @@ -294,6 +294,9 @@ class NewnoticeAction extends Action if ($profile) { $content = '@' . $profile->nickname . ' '; } + } else { + // @fixme most of these bits above aren't being passed on above + $inreplyto = null; } $notice_form = new NoticeForm($this, '', $content, null, $inreplyto); diff --git a/actions/postnotice.php b/actions/postnotice.php index fb06703766..b2f6f1bb95 100644 --- a/actions/postnotice.php +++ b/actions/postnotice.php @@ -54,7 +54,10 @@ class PostnoticeAction extends Action */ function prepare($argarray) { + StatusNet::setApi(true); // Send smaller error pages + parent::prepare($argarray); + try { $this->checkNotice(); } catch (Exception $e) { @@ -71,6 +74,14 @@ class PostnoticeAction extends Action $srv = new OMB_Service_Provider(null, omb_oauth_datastore(), omb_oauth_server()); $srv->handlePostNotice(); + } catch (OMB_RemoteServiceException $rse) { + $msg = $rse->getMessage(); + if (preg_match('/Revoked accesstoken/', $msg) || + preg_match('/No subscriber/', $msg)) { + $this->clientError($msg, 403); + } else { + $this->clientError($msg); + } } catch (Exception $e) { $this->serverError($e->getMessage()); return; diff --git a/actions/profilesettings.php b/actions/profilesettings.php index 0d67778791..161e35b111 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -285,6 +285,10 @@ class ProfilesettingsAction extends AccountSettingsAction } else { // Re-initialize language environment if it changed common_init_language(); + // Clear the site owner, in case nickname changed + if ($user->hasRole(Profile_role::OWNER)) { + User::blow('user:site_owner'); + } } } diff --git a/actions/public.php b/actions/public.php index 50278bfced..0b3b5fde84 100644 --- a/actions/public.php +++ b/actions/public.php @@ -94,6 +94,7 @@ class PublicAction extends Action } if($this->page > 1 && $this->notice->N == 0){ + // TRANS: Server error when page not found (404) $this->serverError(_('No such page'),$code=404); } diff --git a/actions/replies.php b/actions/replies.php index 164c328db3..4ff1b7a8d2 100644 --- a/actions/replies.php +++ b/actions/replies.php @@ -89,6 +89,7 @@ class RepliesAction extends OwnerDesignAction NOTICES_PER_PAGE + 1); if($this->page > 1 && $this->notice->N == 0){ + // TRANS: Server error when page not found (404) $this->serverError(_('No such page'),$code=404); } diff --git a/actions/showfavorites.php b/actions/showfavorites.php index f2d0822936..5b85de6835 100644 --- a/actions/showfavorites.php +++ b/actions/showfavorites.php @@ -134,6 +134,7 @@ class ShowfavoritesAction extends OwnerDesignAction } if($this->page > 1 && $this->notice->N == 0){ + // TRANS: Server error when page not found (404) $this->serverError(_('No such page'),$code=404); } diff --git a/actions/showgroup.php b/actions/showgroup.php index eb12389029..4e1fcb6c7b 100644 --- a/actions/showgroup.php +++ b/actions/showgroup.php @@ -122,9 +122,9 @@ class ShowgroupAction extends GroupDesignAction return false; } - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); - if (!$this->group) { + if (!$local) { $alias = Group_alias::staticGet('alias', $nickname); if ($alias) { $args = array('id' => $alias->group_id); @@ -134,11 +134,19 @@ class ShowgroupAction extends GroupDesignAction common_redirect(common_local_url('groupbyid', $args), 301); return false; } else { + common_log(LOG_NOTICE, "Couldn't find local group for nickname '$nickname'"); $this->clientError(_('No such group.'), 404); return false; } } + $this->group = User_group::staticGet('id', $local->group_id); + + if (!$this->group) { + $this->clientError(_('No such group.'), 404); + return false; + } + common_set_returnto($this->selfUrl()); return true; diff --git a/actions/subscribe.php b/actions/subscribe.php index a90d7facdf..b1243f3933 100644 --- a/actions/subscribe.php +++ b/actions/subscribe.php @@ -1,7 +1,9 @@ . + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ */ -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Subscription action + * + * Subscribing to a profile. Does not work for OMB 0.1 remote subscriptions, + * but may work for other remote subscription protocols, like OStatus. + * + * Takes parameters: + * + * - subscribeto: a profile ID + * - token: session token to prevent CSRF attacks + * - ajax: boolean; whether to return Ajax or full-browser results + * + * Only works if the current user is logged in. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ class SubscribeAction extends Action { + var $user; + var $other; - function handle($args) + /** + * Check pre-requisites and instantiate attributes + * + * @param Array $args array of arguments (URL, GET, POST) + * + * @return boolean success flag + */ + + function prepare($args) { - parent::handle($args); + parent::prepare($args); - if (!common_logged_in()) { - $this->clientError(_('Not logged in.')); - return; - } - - $user = common_current_user(); + // Only allow POST requests if ($_SERVER['REQUEST_METHOD'] != 'POST') { - common_redirect(common_local_url('subscriptions', array('nickname' => $user->nickname))); - return; + $this->clientError(_('This action only accepts POST requests.')); + return false; } - # CSRF protection + // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { - $this->clientError(_('There was a problem with your session token. Try again, please.')); - return; + $this->clientError(_('There was a problem with your session token.'. + ' Try again, please.')); + return false; } + // Only for logged-in users + + $this->user = common_current_user(); + + if (empty($this->user)) { + $this->clientError(_('Not logged in.')); + return false; + } + + // Profile to subscribe to + $other_id = $this->arg('subscribeto'); - $other = User::staticGet('id', $other_id); + $this->other = Profile::staticGet('id', $other_id); - if (!$other) { - $this->clientError(_('Not a local user.')); - return; + if (empty($this->other)) { + $this->clientError(_('No such profile.')); + return false; } - $result = subs_subscribe_to($user, $other); + // OMB 0.1 doesn't have a mechanism for local-server- + // originated subscription. - if (is_string($result)) { - $this->clientError($result); - return; + $omb01 = Remote_profile::staticGet('id', $other_id); + + if (!empty($omb01)) { + $this->clientError(_('You cannot subscribe to an OMB 0.1'. + ' remote profile with this action.')); + return false; } + return true; + } + + /** + * Handle request + * + * Does the subscription and returns results. + * + * @param Array $args unused. + * + * @return void + */ + + function handle($args) + { + // Throws exception on error + + Subscription::start($this->user->getProfile(), + $this->other); + if ($this->boolean('ajax')) { $this->startHTML('text/xml;charset=utf-8'); $this->elementStart('head'); $this->element('title', null, _('Subscribed')); $this->elementEnd('head'); $this->elementStart('body'); - $unsubscribe = new UnsubscribeForm($this, $other->getProfile()); + $unsubscribe = new UnsubscribeForm($this, $this->other); $unsubscribe->show(); $this->elementEnd('body'); $this->elementEnd('html'); } else { - common_redirect(common_local_url('subscriptions', array('nickname' => - $user->nickname)), - 303); + $url = common_local_url('subscriptions', + array('nickname' => $this->user->nickname)); + common_redirect($url, 303); } } } diff --git a/actions/subscriptions.php b/actions/subscriptions.php index 0ef31aa9f1..ba6171ef4c 100644 --- a/actions/subscriptions.php +++ b/actions/subscriptions.php @@ -79,32 +79,37 @@ class SubscriptionsAction extends GalleryAction function showContent() { - parent::showContent(); + if (Event::handle('StartShowSubscriptionsContent', array($this))) { + parent::showContent(); - $offset = ($this->page-1) * PROFILES_PER_PAGE; - $limit = PROFILES_PER_PAGE + 1; + $offset = ($this->page-1) * PROFILES_PER_PAGE; + $limit = PROFILES_PER_PAGE + 1; - $cnt = 0; + $cnt = 0; - if ($this->tag) { - $subscriptions = $this->user->getTaggedSubscriptions($this->tag, $offset, $limit); - } else { - $subscriptions = $this->user->getSubscriptions($offset, $limit); - } - - if ($subscriptions) { - $subscriptions_list = new SubscriptionsList($subscriptions, $this->user, $this); - $cnt = $subscriptions_list->show(); - if (0 == $cnt) { - $this->showEmptyListMessage(); + if ($this->tag) { + $subscriptions = $this->user->getTaggedSubscriptions($this->tag, $offset, $limit); + } else { + $subscriptions = $this->user->getSubscriptions($offset, $limit); } + + if ($subscriptions) { + $subscriptions_list = new SubscriptionsList($subscriptions, $this->user, $this); + $cnt = $subscriptions_list->show(); + if (0 == $cnt) { + $this->showEmptyListMessage(); + } + } + + $subscriptions->free(); + + $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, + $this->page, 'subscriptions', + array('nickname' => $this->user->nickname)); + + + Event::handle('EndShowSubscriptionsContent', array($this)); } - - $subscriptions->free(); - - $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, - $this->page, 'subscriptions', - array('nickname' => $this->user->nickname)); } function showScripts() diff --git a/actions/sup.php b/actions/sup.php index 5daf0a1c1d..4e428dfa58 100644 --- a/actions/sup.php +++ b/actions/sup.php @@ -66,10 +66,12 @@ class SupAction extends Action $divider = common_sql_date(time() - $seconds); $notice->query('SELECT profile_id, max(id) AS max_id ' . - 'FROM notice ' . + 'FROM ( ' . + 'SELECT profile_id, id FROM notice ' . ((common_config('db','type') == 'pgsql') ? 'WHERE extract(epoch from created) > (extract(epoch from now()) - ' . $seconds . ') ' : 'WHERE created > "'.$divider.'" ' ) . + ') AS latest ' . 'GROUP BY profile_id'); $updates = array(); diff --git a/actions/tag.php b/actions/tag.php index e91df6ea97..ee9617b662 100644 --- a/actions/tag.php +++ b/actions/tag.php @@ -48,6 +48,7 @@ class TagAction extends Action $this->notice = Notice_tag::getStream($this->tag, (($this->page-1)*NOTICES_PER_PAGE), NOTICES_PER_PAGE + 1); if($this->page > 1 && $this->notice->N == 0){ + // TRANS: Server error when page not found (404) $this->serverError(_('No such page'),$code=404); } diff --git a/actions/twitapisearchatom.php b/actions/twitapisearchatom.php index baed2a0c7c..24aa619bd7 100644 --- a/actions/twitapisearchatom.php +++ b/actions/twitapisearchatom.php @@ -31,8 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/lib/api.php'; - /** * Action for outputting search results in Twitter compatible Atom * format. @@ -245,7 +243,7 @@ class TwitapisearchatomAction extends ApiAction 'xmlns:twitter' => 'http://api.twitter.com/', 'xml:lang' => 'en-US')); // XXX Other locales ? - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $this->element('id', null, "tag:$taguribase:search/$server"); $site_uri = common_path(false); @@ -329,7 +327,7 @@ class TwitapisearchatomAction extends ApiAction $this->elementStart('entry'); - $taguribase = common_config('integration', 'taguri'); + $taguribase = TagURI::base(); $this->element('id', null, "tag:$taguribase:$notice->id"); $this->element('published', null, common_date_w3dtf($notice->created)); diff --git a/actions/twitapisearchjson.php b/actions/twitapisearchjson.php index 741ed78d63..b5c006aa7b 100644 --- a/actions/twitapisearchjson.php +++ b/actions/twitapisearchjson.php @@ -31,7 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/lib/api.php'; require_once INSTALLDIR.'/lib/jsonsearchresultslist.php'; /** diff --git a/actions/twitapitrends.php b/actions/twitapitrends.php index 779405e6d6..5a04569a22 100644 --- a/actions/twitapitrends.php +++ b/actions/twitapitrends.php @@ -31,8 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/lib/api.php'; - /** * Returns the top ten queries that are currently trending * diff --git a/actions/updateprofile.php b/actions/updateprofile.php index e416a6fa93..bae6108cce 100644 --- a/actions/updateprofile.php +++ b/actions/updateprofile.php @@ -55,6 +55,8 @@ class UpdateprofileAction extends Action */ function prepare($argarray) { + StatusNet::setApi(true); // Send smaller error pages + parent::prepare($argarray); $license = $_POST['omb_listenee_license']; $site_license = common_config('license', 'url'); @@ -75,6 +77,14 @@ class UpdateprofileAction extends Action $srv = new OMB_Service_Provider(null, omb_oauth_datastore(), omb_oauth_server()); $srv->handleUpdateProfile(); + } catch (OMB_RemoteServiceException $rse) { + $msg = $rse->getMessage(); + if (preg_match('/Revoked accesstoken/', $msg) || + preg_match('/No subscriber/', $msg)) { + $this->clientError($msg, 403); + } else { + $this->clientError($msg); + } } catch (Exception $e) { $this->serverError($e->getMessage()); return; diff --git a/actions/useradminpanel.php b/actions/useradminpanel.php index 6813222f5f..ee9c230769 100644 --- a/actions/useradminpanel.php +++ b/actions/useradminpanel.php @@ -55,7 +55,8 @@ class UseradminpanelAction extends AdminPanelAction function title() { - return _('User'); + // TRANS: User admin panel title + return _m('TITLE', 'User'); } /** diff --git a/classes/Fave.php b/classes/Fave.php index 8113c8e166..a04f15e9c4 100644 --- a/classes/Fave.php +++ b/classes/Fave.php @@ -21,17 +21,47 @@ class Fave extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - static function addNew($user, $notice) { - $fave = new Fave(); - $fave->user_id = $user->id; - $fave->notice_id = $notice->id; - if (!$fave->insert()) { - common_log_db_error($fave, 'INSERT', __FILE__); - return false; + static function addNew($profile, $notice) { + + $fave = null; + + if (Event::handle('StartFavorNotice', array($profile, $notice, &$fave))) { + + $fave = new Fave(); + + $fave->user_id = $profile->id; + $fave->notice_id = $notice->id; + + if (!$fave->insert()) { + common_log_db_error($fave, 'INSERT', __FILE__); + return false; + } + + Event::handle('EndFavorNotice', array($profile, $notice)); } + return $fave; } + function delete() + { + $profile = Profile::staticGet('id', $this->user_id); + $notice = Notice::staticGet('id', $this->notice_id); + + $result = null; + + if (Event::handle('StartDisfavorNotice', array($profile, $notice, &$result))) { + + $result = parent::delete(); + + if ($result) { + Event::handle('EndDisfavorNotice', array($profile, $notice)); + } + } + + return $result; + } + function pkeyGet($kv) { return Memcached_DataObject::pkeyGet('Fave', $kv); @@ -47,7 +77,7 @@ class Fave extends Memcached_DataObject return $ids; } - function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id, $since) + function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id) { $fav = new Fave(); $qry = null; @@ -70,10 +100,6 @@ class Fave extends Memcached_DataObject $qry .= 'AND notice_id <= ' . $max_id . ' '; } - if (!is_null($since)) { - $qry .= 'AND modified > \'' . date('Y-m-d H:i:s', $since) . '\' '; - } - // NOTE: we sort by fave time, not by notice time! $qry .= 'ORDER BY modified DESC '; diff --git a/classes/File.php b/classes/File.php index 189e04ce02..79a7d66813 100644 --- a/classes/File.php +++ b/classes/File.php @@ -169,7 +169,11 @@ class File extends Memcached_DataObject { require_once 'MIME/Type/Extension.php'; $mte = new MIME_Type_Extension(); - $ext = $mte->getExtension($mimetype); + try { + $ext = $mte->getExtension($mimetype); + } catch ( Exception $e) { + $ext = strtolower(preg_replace('/\W/', '', $mimetype)); + } $nickname = $profile->nickname; $datestamp = strftime('%Y%m%dT%H%M%S', time()); $random = strtolower(common_confirmation_code(32)); diff --git a/classes/Inbox.php b/classes/Inbox.php index be62611a16..014ba3d829 100644 --- a/classes/Inbox.php +++ b/classes/Inbox.php @@ -137,7 +137,7 @@ class Inbox extends Memcached_DataObject } } - function stream($user_id, $offset, $limit, $since_id, $max_id, $since, $own=false) + function stream($user_id, $offset, $limit, $since_id, $max_id, $own=false) { $inbox = Inbox::staticGet('user_id', $user_id); @@ -195,15 +195,15 @@ class Inbox extends Memcached_DataObject * @param int $limit * @param mixed $since_id return only notices after but not including this id * @param mixed $max_id return only notices up to and including this id - * @param mixed $since obsolete/ignored * @param mixed $own ignored? * @return array of Notice objects * * @todo consider repacking the inbox when this happens? + * @fixme reimplement $own if we need it? */ - function streamNotices($user_id, $offset, $limit, $since_id, $max_id, $since, $own=false) + function streamNotices($user_id, $offset, $limit, $since_id, $max_id, $own=false) { - $ids = self::stream($user_id, $offset, self::MAX_NOTICES, $since_id, $max_id, $since, $own); + $ids = self::stream($user_id, $offset, self::MAX_NOTICES, $since_id, $max_id, $own); // Do a bulk lookup for the first $limit items // Fast path when nothing's deleted. diff --git a/classes/Local_group.php b/classes/Local_group.php new file mode 100644 index 0000000000..42312ec63c --- /dev/null +++ b/classes/Local_group.php @@ -0,0 +1,46 @@ +decache(); + $qry = 'UPDATE local_group set nickname = "'.$nickname.'" where group_id = ' . $this->group_id; + + $result = $this->query($qry); + + if ($result) { + $this->nickname = $nickname; + $this->fixupTimestamps(); + $this->encache(); + } else { + common_log_db_error($local, 'UPDATE', __FILE__); + throw new ServerException(_('Could not update local group.')); + } + + return $result; + } +} diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index 40576dc717..bc4c3a000c 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -501,7 +501,11 @@ class Memcached_DataObject extends Safe_DataObject function raiseError($message, $type = null, $behaviour = null) { - throw new ServerException("DB_DataObject error [$type]: $message"); + $id = get_class($this); + if ($this->id) { + $id .= ':' . $this->id; + } + throw new ServerException("[$id] DB_DataObject error [$type]: $message"); } static function cacheGet($keyPart) diff --git a/classes/Notice.php b/classes/Notice.php index b0edb6de60..63dc968972 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -121,6 +121,9 @@ class Notice extends Memcached_DataObject $result = parent::delete(); } + /** + * Extract #hashtags from this notice's content and save them to the database. + */ function saveTags() { /* extract all #hastags */ @@ -129,14 +132,22 @@ class Notice extends Memcached_DataObject return true; } + /* Add them to the database */ + return $this->saveKnownTags($match[1]); + } + + /** + * Record the given set of hash tags in the db for this notice. + * Given tag strings will be normalized and checked for dupes. + */ + function saveKnownTags($hashtags) + { //turn each into their canonical tag //this is needed to remove dupes before saving e.g. #hash.tag = #hashtag - $hashtags = array(); - for($i=0; $isaveTag($hashtag); @@ -145,6 +156,10 @@ class Notice extends Memcached_DataObject return true; } + /** + * Record a single hash tag as associated with this notice. + * Tag format and uniqueness must be validated by caller. + */ function saveTag($hashtag) { $tag = new Notice_tag(); @@ -187,13 +202,23 @@ class Notice extends Memcached_DataObject * int 'location_ns' geoname namespace to interpret location_id * int 'reply_to'; notice ID this is a reply to * int 'repeat_of'; notice ID this is a repeat of - * string 'uri' permalink to notice; defaults to local notice URL + * string 'uri' unique ID for notice; defaults to local notice URL + * string 'url' permalink to notice; defaults to local notice URL + * string 'rendered' rendered HTML version of content + * array 'replies' list of profile URIs for reply delivery in + * place of extracting @-replies from content. + * array 'groups' list of group IDs to deliver to, in place of + * extracting ! tags from content + * array 'tags' list of hashtag strings to save with the notice + * in place of extracting # tags from content + * @fixme tag override * * @return Notice * @throws ClientException */ static function saveNew($profile_id, $content, $source, $options=null) { $defaults = array('uri' => null, + 'url' => null, 'reply_to' => null, 'repeat_of' => null); @@ -256,9 +281,10 @@ class Notice extends Memcached_DataObject } $notice->content = $final; - $notice->rendered = common_render_content($final, $notice); + $notice->source = $source; $notice->uri = $uri; + $notice->url = $url; // Handle repeat case @@ -283,6 +309,12 @@ class Notice extends Memcached_DataObject $notice->location_ns = $location_ns; } + if (!empty($rendered)) { + $notice->rendered = $rendered; + } else { + $notice->rendered = common_render_content($final, $notice); + } + if (Event::handle('StartNoticeSave', array(&$notice))) { // XXX: some of these functions write to the DB @@ -325,8 +357,33 @@ class Notice extends Memcached_DataObject # Clear the cache for subscribed users, so they'll update at next request # XXX: someone clever could prepend instead of clearing the cache + $notice->blowOnInsert(); + // Save per-notice metadata... + + if (isset($replies)) { + $notice->saveKnownReplies($replies); + } else { + $notice->saveReplies(); + } + + if (isset($groups)) { + $notice->saveKnownGroups($groups); + } else { + $notice->saveGroups(); + } + + if (isset($tags)) { + $notice->saveKnownTags($tags); + } else { + $notice->saveTags(); + } + + // @fixme pass in data for URLs too? + $notice->saveUrls(); + + // Prepare inbox delivery, may be queued to background. $notice->distribute(); return $notice; @@ -502,17 +559,17 @@ class Notice extends Memcached_DataObject } } - function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) + function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0) { $ids = Notice::stream(array('Notice', '_publicStreamDirect'), array(), 'public', - $offset, $limit, $since_id, $max_id, $since); + $offset, $limit, $since_id, $max_id); return Notice::getStreamByIds($ids); } - function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) + function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0) { $notice = new Notice(); @@ -541,10 +598,6 @@ class Notice extends Memcached_DataObject $notice->whereAdd('id <= ' . $max_id); } - if (!is_null($since)) { - $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); - } - $ids = array(); if ($notice->find()) { @@ -559,17 +612,17 @@ class Notice extends Memcached_DataObject return $ids; } - function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) + function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0) { $ids = Notice::stream(array('Notice', '_conversationStreamDirect'), array($id), 'notice:conversation_ids:'.$id, - $offset, $limit, $since_id, $max_id, $since); + $offset, $limit, $since_id, $max_id); return Notice::getStreamByIds($ids); } - function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) + function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0) { $notice = new Notice(); @@ -592,10 +645,6 @@ class Notice extends Memcached_DataObject $notice->whereAdd('id <= ' . $max_id); } - if (!is_null($since)) { - $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); - } - $ids = array(); if ($notice->find()) { @@ -677,11 +726,39 @@ class Notice extends Memcached_DataObject return $ni; } - function addToInboxes($groups, $recipients) + /** + * Adds this notice to the inboxes of each local user who should receive + * it, based on author subscriptions, group memberships, and @-replies. + * + * Warning: running a second time currently will make items appear + * multiple times in users' inboxes. + * + * @fixme make more robust against errors + * @fixme break up massive deliveries to smaller background tasks + * + * @param array $groups optional list of Group objects; + * if left empty, will be loaded from group_inbox records + * @param array $recipient optional list of reply profile ids + * if left empty, will be loaded from reply records + */ + function addToInboxes($groups=null, $recipients=null) { $ni = $this->whoGets($groups, $recipients); - Inbox::bulkInsert($this->id, array_keys($ni)); + $ids = array_keys($ni); + + // We remove the author (if they're a local user), + // since we'll have already done this in distribute() + + $i = array_search($this->profile_id, $ids); + + if ($i !== false) { + unset($ids[$i]); + } + + // Bulk insert + + Inbox::bulkInsert($this->id, $ids); return; } @@ -714,6 +791,42 @@ class Notice extends Memcached_DataObject } /** + * Record this notice to the given group inboxes for delivery. + * Overrides the regular parsing of !group markup. + * + * @param string $group_ids + * @fixme might prefer URIs as identifiers, as for replies? + * best with generalizations on user_group to support + * remote groups better. + */ + function saveKnownGroups($group_ids) + { + if (!is_array($group_ids)) { + throw new ServerException("Bad type provided to saveKnownGroups"); + } + + $groups = array(); + foreach ($group_ids as $id) { + $group = User_group::staticGet('id', $id); + if ($group) { + common_log(LOG_ERR, "Local delivery to group id $id, $group->nickname"); + $result = $this->addToGroupInbox($group); + if (!$result) { + common_log_db_error($gi, 'INSERT', __FILE__); + } + + // @fixme should we save the tags here or not? + $groups[] = clone($group); + } else { + common_log(LOG_ERR, "Local delivery to group id $id skipped, doesn't exist"); + } + } + + return $groups; + } + + /** + * Parse !group delivery and record targets into group_inbox. * @return array of Group objects */ function saveGroups() @@ -797,8 +910,51 @@ class Notice extends Memcached_DataObject } /** + * Save reply records indicating that this notice needs to be + * delivered to the local users with the given URIs. + * + * Since this is expected to be used when saving foreign-sourced + * messages, we won't deliver to any remote targets as that's the + * source service's responsibility. + * + * @fixme Unlike saveReplies() there's no mail notification here. + * Move that to distrib queue handler? + * + * @param array of unique identifier URIs for recipients + */ + function saveKnownReplies($uris) + { + foreach ($uris as $uri) { + + $user = User::staticGet('uri', $uri); + + if (!empty($user)) { + + $reply = new Reply(); + + $reply->notice_id = $this->id; + $reply->profile_id = $user->id; + + $id = $reply->insert(); + + self::blow('reply:stream:%d', $user->id); + } + } + + return; + } + + /** + * Pull @-replies from this message's content in StatusNet markup format + * and save reply records indicating that this message needs to be + * delivered to those users. + * + * Side effect: local recipients get e-mail notifications here. + * @fixme move mail notifications to distrib? + * * @return array of integer profile IDs */ + function saveReplies() { // Don't save reply data for repeats @@ -807,76 +963,47 @@ class Notice extends Memcached_DataObject return array(); } - // Alternative reply format - $tname = false; - if (preg_match('/^T ([A-Z0-9]{1,64}) /', $this->content, $match)) { - $tname = $match[1]; - } - // extract all @messages - $cnt = preg_match_all('/(?:^|\s)@([a-z0-9]{1,64})/', $this->content, $match); - - $names = array(); - - if ($cnt || $tname) { - // XXX: is there another way to make an array copy? - $names = ($tname) ? array_unique(array_merge(array(strtolower($tname)), $match[1])) : array_unique($match[1]); - } - $sender = Profile::staticGet($this->profile_id); + // @todo ideally this parser information would only + // be calculated once. + + $mentions = common_find_mentions($this->content, $this); + $replied = array(); // store replied only for first @ (what user/notice what the reply directed, // we assume first @ is it) - for ($i=0; $icreated); - if (empty($recipient)) { - continue; - } - // Don't save replies from blocked profile to local user - $recipient_user = User::staticGet('id', $recipient->id); - if (!empty($recipient_user) && $recipient_user->hasBlocked($sender)) { - continue; - } - $reply = new Reply(); - $reply->notice_id = $this->id; - $reply->profile_id = $recipient->id; - $id = $reply->insert(); - if (!$id) { - $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError'); - common_log(LOG_ERR, 'DB error inserting reply: ' . $last_error->message); - common_server_error(sprintf(_('DB error inserting reply: %s'), $last_error->message)); - return array(); - } else { - $replied[$recipient->id] = 1; - } - } + foreach ($mentions as $mention) { - // Hash format replies, too - $cnt = preg_match_all('/(?:^|\s)@#([a-z0-9]{1,64})/', $this->content, $match); - if ($cnt) { - foreach ($match[1] as $tag) { - $tagged = Profile_tag::getTagged($sender->id, $tag); - foreach ($tagged as $t) { - if (!$replied[$t->id]) { - // Don't save replies from blocked profile to local user - $t_user = User::staticGet('id', $t->id); - if ($t_user && $t_user->hasBlocked($sender)) { - continue; - } - $reply = new Reply(); - $reply->notice_id = $this->id; - $reply->profile_id = $t->id; - $id = $reply->insert(); - if (!$id) { - common_log_db_error($reply, 'INSERT', __FILE__); - return array(); - } else { - $replied[$recipient->id] = 1; - } - } + foreach ($mention['mentioned'] as $mentioned) { + + // skip if they're already covered + + if (!empty($replied[$mentioned->id])) { + continue; + } + + // Don't save replies from blocked profile to local user + + $mentioned_user = User::staticGet('id', $mentioned->id); + if (!empty($mentioned_user) && $mentioned_user->hasBlocked($sender)) { + continue; + } + + $reply = new Reply(); + + $reply->notice_id = $this->id; + $reply->profile_id = $mentioned->id; + + $id = $reply->insert(); + + if (!$id) { + common_log_db_error($reply, 'INSERT', __FILE__); + throw new ServerException("Couldn't save reply for {$this->id}, {$mentioned->id}"); + } else { + $replied[$mentioned->id] = 1; } } } @@ -917,9 +1044,10 @@ class Notice extends Memcached_DataObject } /** - * Same calculation as saveGroups but without the saving - * @fixme merge the functions - * @return array of Group_inbox objects + * Pull list of groups this notice needs to be delivered to, + * as previously recorded by saveGroups() or saveKnownGroups(). + * + * @return array of Group objects */ function getGroups() { @@ -942,7 +1070,10 @@ class Notice extends Memcached_DataObject if ($gi->find()) { while ($gi->fetch()) { - $groups[] = clone($gi); + $group = User_group::staticGet('id', $gi->group_id); + if ($group) { + $groups[] = $group; + } } } @@ -962,6 +1093,8 @@ class Notice extends Memcached_DataObject 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', 'xmlns:georss' => 'http://www.georss.org/georss', 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', + 'xmlns:media' => 'http://purl.org/syndication/atommedia', + 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0', 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0'); } else { $attrs = array(); @@ -993,12 +1126,12 @@ class Notice extends Memcached_DataObject } $xs->element('title', null, $this->content); - $xs->element('summary', null, $this->content); $xs->raw($profile->asAtomAuthor()); $xs->raw($profile->asActivityActor()); $xs->element('link', array('rel' => 'alternate', + 'type' => 'text/html', 'href' => $this->bestUrl())); $xs->element('id', null, $this->uri); @@ -1045,6 +1178,17 @@ class Notice extends Memcached_DataObject } } + $groups = $this->getGroups(); + + foreach ($groups as $group) { + $xs->element( + 'link', array( + 'rel' => 'ostatus:attention', + 'href' => $group->permalink() + ) + ); + } + if (!empty($this->repeat_of)) { $repeat = Notice::staticGet('id', $this->repeat_of); if (!empty($repeat)) { @@ -1090,6 +1234,21 @@ class Notice extends Memcached_DataObject return $xs->getString(); } + /** + * Returns an XML string fragment with a reference to a notice as an + * Activity Streams noun object with the given element type. + * + * Assumes that 'activity' namespace has been previously defined. + * + * @param string $element one of 'subject', 'object', 'target' + * @return string + */ + function asActivityNoun($element) + { + $noun = ActivityObject::fromNotice($this); + return $noun->asString('activity:' . $element); + } + function bestUrl() { if (!empty($this->url)) { @@ -1102,16 +1261,16 @@ class Notice extends Memcached_DataObject } } - function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null) + function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0) { $cache = common_memcache(); if (empty($cache) || - $since_id != 0 || $max_id != 0 || (!is_null($since) && $since > 0) || + $since_id != 0 || $max_id != 0 || is_null($limit) || ($offset + $limit) > NOTICE_CACHE_WINDOW) { return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id, - $max_id, $since))); + $max_id))); } $idkey = common_cache_key($cachekey); @@ -1487,6 +1646,14 @@ class Notice extends Memcached_DataObject function distribute() { + // We always insert for the author so they don't + // have to wait + + $user = User::staticGet('id', $this->profile_id); + if (!empty($user)) { + Inbox::insertNotice($user->id, $this->id); + } + if (common_config('queue', 'inboxes')) { // If there's a failure, we want to _force_ // distribution at this point. diff --git a/classes/Notice_inbox.php b/classes/Notice_inbox.php index c27dcdfd61..47ed6b22db 100644 --- a/classes/Notice_inbox.php +++ b/classes/Notice_inbox.php @@ -49,12 +49,12 @@ class Notice_inbox extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - function stream($user_id, $offset, $limit, $since_id, $max_id, $since, $own=false) + function stream($user_id, $offset, $limit, $since_id, $max_id, $own=false) { throw new Exception('Notice_inbox no longer used; use Inbox'); } - function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id, $since) + function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id) { throw new Exception('Notice_inbox no longer used; use Inbox'); } diff --git a/classes/Notice_tag.php b/classes/Notice_tag.php index 4fd76e8ea8..a5d0716a71 100644 --- a/classes/Notice_tag.php +++ b/classes/Notice_tag.php @@ -46,7 +46,7 @@ class Notice_tag extends Memcached_DataObject return Notice::getStreamByIds($ids); } - function _streamDirect($tag, $offset, $limit, $since_id, $max_id, $since) + function _streamDirect($tag, $offset, $limit, $since_id, $max_id) { $nt = new Notice_tag(); @@ -63,10 +63,6 @@ class Notice_tag extends Memcached_DataObject $nt->whereAdd('notice_id < ' . $max_id); } - if (!is_null($since)) { - $nt->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); - } - $nt->orderBy('notice_id DESC'); if (!is_null($offset)) { diff --git a/classes/Profile.php b/classes/Profile.php index 494c697e42..470ef33207 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -163,27 +163,27 @@ class Profile extends Memcached_DataObject return null; } - function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0, $since=null) + function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { $ids = Notice::stream(array($this, '_streamTaggedDirect'), array($tag), 'profile:notice_ids_tagged:' . $this->id . ':' . $tag, - $offset, $limit, $since_id, $max_id, $since); + $offset, $limit, $since_id, $max_id); return Notice::getStreamByIds($ids); } - function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0, $since=null) + function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { // XXX: I'm not sure this is going to be any faster. It probably isn't. $ids = Notice::stream(array($this, '_streamDirect'), array(), 'profile:notice_ids:' . $this->id, - $offset, $limit, $since_id, $max_id, $since); + $offset, $limit, $since_id, $max_id); return Notice::getStreamByIds($ids); } - function _streamTaggedDirect($tag, $offset, $limit, $since_id, $max_id, $since) + function _streamTaggedDirect($tag, $offset, $limit, $since_id, $max_id) { // XXX It would be nice to do this without a join @@ -202,10 +202,6 @@ class Profile extends Memcached_DataObject $query .= " and id < $max_id"; } - if (!is_null($since)) { - $query .= " and created > '" . date('Y-m-d H:i:s', $since) . "'"; - } - $query .= ' order by id DESC'; if (!is_null($offset)) { @@ -223,7 +219,7 @@ class Profile extends Memcached_DataObject return $ids; } - function _streamDirect($offset, $limit, $since_id, $max_id, $since = null) + function _streamDirect($offset, $limit, $since_id, $max_id) { $notice = new Notice(); @@ -240,10 +236,6 @@ class Profile extends Memcached_DataObject $notice->whereAdd('id <= ' . $max_id); } - if (!is_null($since)) { - $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); - } - $notice->orderBy('id DESC'); if (!is_null($offset)) { @@ -792,44 +784,17 @@ class Profile extends Memcached_DataObject * Returns an XML string fragment with profile information as an * Activity Streams noun object with the given element type. * - * Assumes that 'activity' namespace has been previously defined. + * Assumes that 'activity', 'georss', and 'poco' namespace has been + * previously defined. * * @param string $element one of 'actor', 'subject', 'object', 'target' + * * @return string */ function asActivityNoun($element) { - $xs = new XMLStringer(true); - - $xs->elementStart('activity:' . $element); - $xs->element( - 'activity:object-type', - null, - 'http://activitystrea.ms/schema/1.0/person' - ); - $xs->element( - 'id', - null, - $this->getUri() - ); - $xs->element('title', null, $this->getBestName()); - - $avatar = $this->getAvatar(AVATAR_PROFILE_SIZE); - - $xs->element( - 'link', array( - 'type' => empty($avatar) ? 'image/png' : $avatar->mediatype, - 'rel' => 'avatar', - 'href' => empty($avatar) - ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) - : $avatar->displayUrl() - ), - '' - ); - - $xs->elementEnd('activity:' . $element); - - return $xs->getString(); + $noun = ActivityObject::fromProfile($this); + return $noun->asString('activity:' . $element); } /** @@ -841,31 +806,37 @@ class Profile extends Memcached_DataObject { $uri = null; - // check for a local user first - $user = User::staticGet('id', $this->id); + // give plugins a chance to set the URI + if (Event::handle('StartGetProfileUri', array($this, &$uri))) { - if (!empty($user)) { - $uri = common_local_url( - 'userbyid', - array('id' => $user->id) - ); - } else { - - // give plugins a chance to set the URI - if (Event::handle('StartGetProfileUri', array($this, &$uri))) { + // check for a local user first + $user = User::staticGet('id', $this->id); + if (!empty($user)) { + $uri = $user->uri; + } else { // return OMB profile if any $remote = Remote_profile::staticGet('id', $this->id); - if (!empty($remote)) { $uri = $remote->uri; } - - Event::handle('EndGetProfileUri', array($this, &$uri)); } + Event::handle('EndGetProfileUri', array($this, &$uri)); } return $uri; } + function hasBlocked($other) + { + $block = Profile_block::get($this->id, $other->id); + + if (empty($block)) { + $result = false; + } else { + $result = true; + } + + return $result; + } } diff --git a/classes/Reply.php b/classes/Reply.php index 49b1e05e51..659e04c925 100644 --- a/classes/Reply.php +++ b/classes/Reply.php @@ -22,16 +22,16 @@ class Reply extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0, $since=null) + function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { $ids = Notice::stream(array('Reply', '_streamDirect'), array($user_id), 'reply:stream:' . $user_id, - $offset, $limit, $since_id, $max_id, $since); + $offset, $limit, $since_id, $max_id); return $ids; } - function _streamDirect($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0, $since=null) + function _streamDirect($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { $reply = new Reply(); $reply->profile_id = $user_id; @@ -44,10 +44,6 @@ class Reply extends Memcached_DataObject $reply->whereAdd('notice_id < ' . $max_id); } - if (!is_null($since)) { - $reply->whereAdd('modified > \'' . date('Y-m-d H:i:s', $since) . '\''); - } - $reply->orderBy('notice_id DESC'); if (!is_null($offset)) { diff --git a/classes/Subscription.php b/classes/Subscription.php index faf1331cda..9cef2df1ad 100644 --- a/classes/Subscription.php +++ b/classes/Subscription.php @@ -24,7 +24,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } */ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; -class Subscription extends Memcached_DataObject +class Subscription extends Memcached_DataObject { ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -34,8 +34,8 @@ class Subscription extends Memcached_DataObject public $subscribed; // int(4) primary_key not_null public $jabber; // tinyint(1) default_1 public $sms; // tinyint(1) default_1 - public $token; // varchar(255) - public $secret; // varchar(255) + public $token; // varchar(255) + public $secret; // varchar(255) public $created; // datetime() not_null public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP @@ -45,9 +45,177 @@ class Subscription extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - + function pkeyGet($kv) { return Memcached_DataObject::pkeyGet('Subscription', $kv); } + + /** + * Make a new subscription + * + * @param Profile $subscriber party to receive new notices + * @param Profile $other party sending notices; publisher + * + * @return Subscription new subscription + */ + + static function start($subscriber, $other) + { + if (!$subscriber->hasRight(Right::SUBSCRIBE)) { + throw new Exception(_('You have been banned from subscribing.')); + } + + if (self::exists($subscriber, $other)) { + throw new Exception(_('Already subscribed!')); + } + + if ($other->hasBlocked($subscriber)) { + throw new Exception(_('User has blocked you.')); + } + + if (Event::handle('StartSubscribe', array($subscriber, $other))) { + + $sub = new Subscription(); + + $sub->subscriber = $subscriber->id; + $sub->subscribed = $other->id; + $sub->created = common_sql_now(); + + $result = $sub->insert(); + + if (!$result) { + common_log_db_error($sub, 'INSERT', __FILE__); + throw new Exception(_('Could not save subscription.')); + } + + $sub->notify(); + + self::blow('user:notices_with_friends:%d', $subscriber->id); + + $subscriber->blowSubscriptionsCount(); + $other->blowSubscribersCount(); + + $otherUser = User::staticGet('id', $other->id); + + if (!empty($otherUser) && + $otherUser->autosubscribe && + !self::exists($other, $subscriber) && + !$subscriber->hasBlocked($other)) { + + $auto = new Subscription(); + + $auto->subscriber = $subscriber->id; + $auto->subscribed = $other->id; + $auto->created = common_sql_now(); + + $result = $auto->insert(); + + if (!$result) { + common_log_db_error($auto, 'INSERT', __FILE__); + throw new Exception(_('Could not save subscription.')); + } + + $auto->notify(); + } + + Event::handle('EndSubscribe', array($subscriber, $other)); + } + + return true; + } + + function notify() + { + # XXX: add other notifications (Jabber, SMS) here + # XXX: queue this and handle it offline + # XXX: Whatever happens, do it in Twitter-like API, too + + $this->notifyEmail(); + } + + function notifyEmail() + { + $subscribedUser = User::staticGet('id', $this->subscribed); + + if (!empty($subscribedUser)) { + + $subscriber = Profile::staticGet('id', $this->subscriber); + + mail_subscribe_notify_profile($subscribedUser, $subscriber); + } + } + + /** + * Cancel a subscription + * + */ + + function cancel($subscriber, $other) + { + if (!self::exists($subscriber, $other)) { + throw new Exception(_('Not subscribed!')); + } + + // Don't allow deleting self subs + + if ($subscriber->id == $other->id) { + throw new Exception(_('Couldn\'t delete self-subscription.')); + } + + if (Event::handle('StartUnsubscribe', array($subscriber, $other))) { + + $sub = Subscription::pkeyGet(array('subscriber' => $subscriber->id, + 'subscribed' => $other->id)); + + // note we checked for existence above + + assert(!empty($sub)); + + // @todo: move this block to EndSubscribe handler for + // OMB plugin when it exists. + + if (!empty($sub->token)) { + + $token = new Token(); + + $token->tok = $sub->token; + + if ($token->find(true)) { + + $result = $token->delete(); + + if (!$result) { + common_log_db_error($token, 'DELETE', __FILE__); + throw new Exception(_('Couldn\'t delete subscription OMB token.')); + } + } else { + common_log(LOG_ERR, "Couldn't find credentials with token {$token->tok}"); + } + } + + $result = $sub->delete(); + + if (!$result) { + common_log_db_error($sub, 'DELETE', __FILE__); + throw new Exception(_('Couldn\'t delete subscription.')); + } + + self::blow('user:notices_with_friends:%d', $subscriber->id); + + $subscriber->blowSubscriptionsCount(); + $other->blowSubscribersCount(); + + Event::handle('EndUnsubscribe', array($subscriber, $other)); + } + + return; + } + + function exists($subscriber, $other) + { + $sub = Subscription::pkeyGet(array('subscriber' => $subscriber->id, + 'subscribed' => $other->id)); + return (empty($sub)) ? false : true; + } } diff --git a/classes/User.php b/classes/User.php index 72c3f39e94..357c1e5013 100644 --- a/classes/User.php +++ b/classes/User.php @@ -80,11 +80,7 @@ class User extends Memcached_DataObject function isSubscribed($other) { - assert(!is_null($other)); - // XXX: cache results of this query - $sub = Subscription::pkeyGet(array('subscriber' => $this->id, - 'subscribed' => $other->id)); - return (is_null($sub)) ? false : true; + return Subscription::exists($this->getProfile(), $other); } // 'update' won't write key columns, so we have to do it ourselves. @@ -167,17 +163,8 @@ class User extends Memcached_DataObject function hasBlocked($other) { - - $block = Profile_block::get($this->id, $other->id); - - if (is_null($block)) { - $result = false; - } else { - $result = true; - $block->free(); - } - - return $result; + $profile = $this->getProfile(); + return $profile->hasBlocked($other); } /** @@ -219,6 +206,7 @@ class User extends Memcached_DataObject if(! User::allowed_nickname($nickname)){ common_log(LOG_WARNING, sprintf("Attempted to register a nickname that is not allowed: %s", $profile->nickname), __FILE__); + return false; } $profile->profileurl = common_profile_url($nickname); @@ -469,28 +457,28 @@ class User extends Memcached_DataObject return $user; } - function getReplies($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) + function getReplies($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { - $ids = Reply::stream($this->id, $offset, $limit, $since_id, $before_id, $since); + $ids = Reply::stream($this->id, $offset, $limit, $since_id, $before_id); return Notice::getStreamByIds($ids); } - function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) { + function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { $profile = $this->getProfile(); if (!$profile) { return null; } else { - return $profile->getTaggedNotices($tag, $offset, $limit, $since_id, $before_id, $since); + return $profile->getTaggedNotices($tag, $offset, $limit, $since_id, $before_id); } } - function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) + function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { $profile = $this->getProfile(); if (!$profile) { return null; } else { - return $profile->getNotices($offset, $limit, $since_id, $before_id, $since); + return $profile->getNotices($offset, $limit, $since_id, $before_id); } } @@ -500,24 +488,24 @@ class User extends Memcached_DataObject return Notice::getStreamByIds($ids); } - function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) + function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { - return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, $since, false); + return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, false); } - function noticeInbox($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) + function noticeInbox($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { - return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, $since, true); + return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, true); } - function friendsTimeline($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) + function friendsTimeline($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { - return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, $since, false); + return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, false); } - function ownFriendsTimeline($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) + function ownFriendsTimeline($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { - return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, $since, true); + return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, true); } function blowFavesCache() @@ -802,7 +790,7 @@ class User extends Memcached_DataObject return Notice::getStreamByIds($ids); } - function _repeatedByMeDirect($offset, $limit, $since_id, $max_id, $since) + function _repeatedByMeDirect($offset, $limit, $since_id, $max_id) { $notice = new Notice(); @@ -826,10 +814,6 @@ class User extends Memcached_DataObject $notice->whereAdd('id <= ' . $max_id); } - if (!is_null($since)) { - $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); - } - $ids = array(); if ($notice->find()) { @@ -849,12 +833,12 @@ class User extends Memcached_DataObject $ids = Notice::stream(array($this, '_repeatsOfMeDirect'), array(), 'user:repeats_of_me:'.$this->id, - $offset, $limit, $since_id, $max_id, null); + $offset, $limit, $since_id, $max_id); return Notice::getStreamByIds($ids); } - function _repeatsOfMeDirect($offset, $limit, $since_id, $max_id, $since) + function _repeatsOfMeDirect($offset, $limit, $since_id, $max_id) { $qry = 'SELECT DISTINCT original.id AS id ' . @@ -869,10 +853,6 @@ class User extends Memcached_DataObject $qry .= 'AND original.id <= ' . $max_id . ' '; } - if (!is_null($since)) { - $qry .= 'AND original.modified > \'' . date('Y-m-d H:i:s', $since) . '\' '; - } - // NOTE: we sort by fave time, not by notice time! $qry .= 'ORDER BY original.id DESC '; diff --git a/classes/User_group.php b/classes/User_group.php index 379e6b7219..e928874746 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -10,21 +10,23 @@ class User_group extends Memcached_DataObject public $__table = 'user_group'; // table name public $id; // int(4) primary_key not_null - public $nickname; // varchar(64) unique_key + public $nickname; // varchar(64) public $fullname; // varchar(255) public $homepage; // varchar(255) - public $description; // text() + public $description; // text public $location; // varchar(255) public $original_logo; // varchar(255) public $homepage_logo; // varchar(255) public $stream_logo; // varchar(255) public $mini_logo; // varchar(255) public $design_id; // int(4) - public $created; // datetime() not_null - public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP + public $created; // datetime not_null default_0000-00-00%2000%3A00%3A00 + public $modified; // timestamp not_null default_CURRENT_TIMESTAMP + public $uri; // varchar(255) unique_key + public $mainpage; // varchar(255) /* Static get */ - function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('User_group',$k,$v); } + function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('User_group',$k,$v); } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE @@ -39,14 +41,44 @@ class User_group extends Memcached_DataObject function homeUrl() { - return common_local_url('showgroup', - array('nickname' => $this->nickname)); + $url = null; + if (Event::handle('StartUserGroupHomeUrl', array($this, &$url))) { + // normally stored in mainpage, but older ones may be null + if (!empty($this->mainpage)) { + $url = $this->mainpage; + } else { + $url = common_local_url('showgroup', + array('nickname' => $this->nickname)); + } + } + Event::handle('EndUserGroupHomeUrl', array($this, &$url)); + return $url; + } + + function getUri() + { + $uri = null; + if (Event::handle('StartUserGroupGetUri', array($this, &$uri))) { + if (!empty($this->uri)) { + $uri = $this->uri; + } else { + $uri = common_local_url('groupbyid', + array('id' => $this->id)); + } + } + Event::handle('EndUserGroupGetUri', array($this, &$uri)); + return $uri; } function permalink() { - return common_local_url('groupbyid', - array('id' => $this->id)); + $url = null; + if (Event::handle('StartUserGroupPermalink', array($this, &$url))) { + $url = common_local_url('groupbyid', + array('id' => $this->id)); + } + Event::handle('EndUserGroupPermalink', array($this, &$url)); + return $url; } function getNotices($offset, $limit, $since_id=null, $max_id=null) @@ -59,7 +91,7 @@ class User_group extends Memcached_DataObject return Notice::getStreamByIds($ids); } - function _streamDirect($offset, $limit, $since_id, $max_id, $since) + function _streamDirect($offset, $limit, $since_id, $max_id) { $inbox = new Group_inbox(); @@ -76,10 +108,6 @@ class User_group extends Memcached_DataObject $inbox->whereAdd('notice_id <= ' . $max_id); } - if (!is_null($since)) { - $inbox->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); - } - $inbox->orderBy('notice_id DESC'); if (!is_null($offset)) { @@ -367,25 +395,41 @@ class User_group extends Memcached_DataObject return $xs->getString(); } + /** + * Returns an XML string fragment with group information as an + * Activity Streams element. + * + * Assumes that 'activity' namespace has been previously defined. + * + * @return string + */ function asActivitySubject() { - $xs = new XMLStringer(true); + return $this->asActivityNoun('subject'); + } - $xs->elementStart('activity:subject'); - $xs->element('activity:object', null, 'http://activitystrea.ms/schema/1.0/group'); - $xs->element('id', null, $this->permalink()); - $xs->element('title', null, $this->getBestName()); - $xs->element( - 'link', array( - 'rel' => 'avatar', - 'href' => empty($this->homepage_logo) - ? User_group::defaultLogo(AVATAR_PROFILE_SIZE) - : $this->homepage_logo - ) - ); - $xs->elementEnd('activity:subject'); + /** + * Returns an XML string fragment with group information as an + * Activity Streams noun object with the given element type. + * + * Assumes that 'activity', 'georss', and 'poco' namespace has been + * previously defined. + * + * @param string $element one of 'actor', 'subject', 'object', 'target' + * + * @return string + */ + function asActivityNoun($element) + { + $noun = ActivityObject::fromGroup($this); + return $noun->asString('activity:' . $element); + } - return $xs->getString(); + function getAvatar() + { + return empty($this->homepage_logo) + ? User_group::defaultLogo(AVATAR_PROFILE_SIZE) + : $this->homepage_logo; } static function register($fields) { @@ -403,28 +447,31 @@ class User_group extends Memcached_DataObject $group->homepage = $homepage; $group->description = $description; $group->location = $location; + $group->uri = $uri; + $group->mainpage = $mainpage; $group->created = common_sql_now(); $result = $group->insert(); if (!$result) { common_log_db_error($group, 'INSERT', __FILE__); - $this->serverError( - _('Could not create group.'), - 500, - $this->format - ); - return; + throw new ServerException(_('Could not create group.')); } + + if (!isset($uri) || empty($uri)) { + $orig = clone($group); + $group->uri = common_local_url('groupbyid', array('id' => $group->id)); + $result = $group->update($orig); + if (!$result) { + common_log_db_error($group, 'UPDATE', __FILE__); + throw new ServerException(_('Could not set group URI.')); + } + } + $result = $group->setAliases($aliases); if (!$result) { - $this->serverError( - _('Could not create aliases.'), - 500, - $this->format - ); - return; + throw new ServerException(_('Could not create aliases.')); } $member = new Group_member(); @@ -438,12 +485,22 @@ class User_group extends Memcached_DataObject if (!$result) { common_log_db_error($member, 'INSERT', __FILE__); - $this->serverError( - _('Could not set group membership.'), - 500, - $this->format - ); - return; + throw new ServerException(_('Could not set group membership.')); + } + + if ($local) { + $local_group = new Local_group(); + + $local_group->group_id = $group->id; + $local_group->nickname = $nickname; + $local_group->created = common_sql_now(); + + $result = $local_group->insert(); + + if (!$result) { + common_log_db_error($local_group, 'INSERT', __FILE__); + throw new ServerException(_('Could not save local group info.')); + } } $group->query('COMMIT'); diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 81c1b68b23..3fb8ee208b 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -245,13 +245,6 @@ modified = 384 group_id = K profile_id = K -[invitation] -code = 130 -user_id = 129 -address = 130 -address_type = 130 -created = 142 - [inbox] user_id = 129 notice_ids = 66 @@ -259,9 +252,26 @@ notice_ids = 66 [inbox__keys] user_id = K +[invitation] +code = 130 +user_id = 129 +address = 130 +address_type = 130 +created = 142 + [invitation__keys] code = K +[local_group] +group_id = 129 +nickname = 2 +created = 142 +modified = 384 + +[local_group__keys] +group_id = K +nickname = U + [location_namespace] id = 129 description = 2 @@ -369,7 +379,7 @@ icon = 130 source_url = 2 organization = 2 homepage = 2 -callback_url = 130 +callback_url = 2 type = 17 access_type = 17 created = 142 @@ -440,13 +450,13 @@ tag = K [queue_item] id = 129 -frame = 66 +frame = 194 transport = 130 created = 142 claimed = 14 [queue_item__keys] -id = K +id = N [related_group] group_id = 129 @@ -593,10 +603,11 @@ mini_logo = 2 design_id = 1 created = 142 modified = 384 +uri = 2 +mainpage = 2 [user_group__keys] id = N -nickname = U [user_openid] canonical = 130 @@ -627,4 +638,3 @@ modified = 384 [user_location_prefs__keys] user_id = K - diff --git a/config.php.sample b/config.php.sample index b8852dc672..5c5fb5b539 100644 --- a/config.php.sample +++ b/config.php.sample @@ -275,6 +275,8 @@ $config['sphinx']['port'] = 3312; // Support for file uploads (attachments), // select supported mimetypes and quotas (in bytes) // $config['attachments']['supported'] = array('image/png', 'application/ogg'); +// $config['attachments']['supported'] = true; //allow all file types to be uploaded + // $config['attachments']['file_quota'] = 5000000; // $config['attachments']['user_quota'] = 50000000; // $config['attachments']['monthly_quota'] = 15000000; diff --git a/db/08to09.sql b/db/08to09.sql index d5f30a26b9..c6c5d7af61 100644 --- a/db/08to09.sql +++ b/db/08to09.sql @@ -140,4 +140,8 @@ create table oauth_application_user ( constraint primary key (profile_id, application_id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; +alter table file_to_post + add index post_id_idx (post_id); +alter table group_inbox + add index group_inbox_notice_id_idx (notice_id); diff --git a/db/beta5tobeta6.sql b/db/beta5tobeta6.sql new file mode 100644 index 0000000000..e9dff17efe --- /dev/null +++ b/db/beta5tobeta6.sql @@ -0,0 +1,28 @@ +alter table oauth_application + modify column name varchar(255) not null unique key comment 'name of the application', + modify column access_type tinyint default 0 comment 'access type, bit 1 = read, bit 2 = write'; + +alter table user_group +add column uri varchar(255) unique key comment 'universal identifier', +add column mainpage varchar(255) comment 'page for group info to link to', +drop index nickname; + +create table conversation ( + id integer auto_increment primary key comment 'unique identifier', + uri varchar(225) unique comment 'URI of the conversation', + created datetime not null comment 'date this record was created', + modified timestamp comment 'date this record was modified' +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + +create table local_group ( + group_id integer primary key comment 'group represented' references user_group (id), + nickname varchar(64) unique key comment 'group represented', + + created datetime not null comment 'date this record was created', + modified timestamp comment 'date this record was modified' + +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + +insert into local_group (group_id, nickname, created) +select id, nickname, created from user_group; + diff --git a/db/statusnet.sql b/db/statusnet.sql index 97117c80aa..3f95948e1e 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -406,7 +406,7 @@ create table profile_block ( create table user_group ( id integer auto_increment primary key comment 'unique identifier', - nickname varchar(64) unique key comment 'nickname for addressing', + nickname varchar(64) comment 'nickname for addressing', fullname varchar(255) comment 'display name', homepage varchar(255) comment 'URL, cached so we dont regenerate', description text comment 'group description', @@ -421,6 +421,9 @@ create table user_group ( created datetime not null comment 'date this record was created', modified timestamp comment 'date this record was modified', + uri varchar(255) unique key comment 'universal identifier', + mainpage varchar(255) comment 'page for group info to link to', + index user_group_nickname_idx (nickname) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci; @@ -455,7 +458,8 @@ create table group_inbox ( created datetime not null comment 'date the notice was created', constraint primary key (group_id, notice_id), - index group_inbox_created_idx (created) + index group_inbox_created_idx (created), + index group_inbox_notice_id_idx (notice_id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; @@ -520,7 +524,8 @@ create table file_to_post ( post_id integer comment 'id of the notice it belongs to' references notice (id), modified timestamp comment 'date this record was modified', - constraint primary key (file_id, post_id) + constraint primary key (file_id, post_id), + index post_id_idx (post_id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; @@ -641,3 +646,13 @@ create table conversation ( modified timestamp comment 'date this record was modified' ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; +create table local_group ( + + group_id integer primary key comment 'group represented' references user_group (id), + nickname varchar(64) unique key comment 'group represented', + + created datetime not null comment 'date this record was created', + modified timestamp comment 'date this record was modified' + +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + diff --git a/index.php b/index.php index 06ff9900fd..215d1f676e 100644 --- a/index.php +++ b/index.php @@ -37,8 +37,6 @@ define('INSTALLDIR', dirname(__FILE__)); define('STATUSNET', true); define('LACONICA', true); // compatibility -require_once INSTALLDIR . '/lib/common.php'; - $user = null; $action = null; @@ -68,52 +66,69 @@ function getPath($req) */ function handleError($error) { - if ($error->getCode() == DB_DATAOBJECT_ERROR_NODATA) { - return; - } + try { - $logmsg = "PEAR error: " . $error->getMessage(); - if (common_config('site', 'logdebug')) { - $logmsg .= " : ". $error->getDebugInfo(); - } - // DB queries often end up with a lot of newlines; merge to a single line - // for easier grepability... - $logmsg = str_replace("\n", " ", $logmsg); - common_log(LOG_ERR, $logmsg); - - // @fixme backtrace output should be consistent with exception handling - if (common_config('site', 'logdebug')) { - $bt = $error->getBacktrace(); - foreach ($bt as $n => $line) { - common_log(LOG_ERR, formatBacktraceLine($n, $line)); + if ($error->getCode() == DB_DATAOBJECT_ERROR_NODATA) { + return; } - } - if ($error instanceof DB_DataObject_Error - || $error instanceof DB_Error - ) { - $msg = sprintf( - _( - 'The database for %s isn\'t responding correctly, '. - 'so the site won\'t work properly. '. - 'The site admins probably know about the problem, '. - 'but you can contact them at %s to make sure. '. - 'Otherwise, wait a few minutes and try again.' - ), - common_config('site', 'name'), - common_config('site', 'email') - ); - } else { - $msg = _( - 'An important error occured, probably related to email setup. '. - 'Check logfiles for more info..' - ); - } - $dac = new DBErrorAction($msg, 500); - $dac->showPage(); + $logmsg = "PEAR error: " . $error->getMessage(); + if ($error instanceof PEAR_Exception && common_config('site', 'logdebug')) { + $logmsg .= " : ". $error->toText(); + } + // DB queries often end up with a lot of newlines; merge to a single line + // for easier grepability... + $logmsg = str_replace("\n", " ", $logmsg); + common_log(LOG_ERR, $logmsg); + + // @fixme backtrace output should be consistent with exception handling + if (common_config('site', 'logdebug')) { + $bt = $error->getTrace(); + foreach ($bt as $n => $line) { + common_log(LOG_ERR, formatBacktraceLine($n, $line)); + } + } + if ($error instanceof DB_DataObject_Error + || $error instanceof DB_Error + || ($error instanceof PEAR_Exception && $error->getCode() == -24) + ) { + //If we run into a DB error, assume we can't connect to the DB at all + //so set the current user to null, so we don't try to access the DB + //while rendering the error page. + global $_cur; + $_cur = null; + + $msg = sprintf( + _( + 'The database for %s isn\'t responding correctly, '. + 'so the site won\'t work properly. '. + 'The site admins probably know about the problem, '. + 'but you can contact them at %s to make sure. '. + 'Otherwise, wait a few minutes and try again.' + ), + common_config('site', 'name'), + common_config('site', 'email') + ); + } else { + $msg = _( + 'An important error occured, probably related to email setup. '. + 'Check logfiles for more info..' + ); + } + + $dac = new DBErrorAction($msg, 500); + $dac->showPage(); + + } catch (Exception $e) { + echo _('An error occurred.'); + } exit(-1); } +set_exception_handler('handleError'); + +require_once INSTALLDIR . '/lib/common.php'; + /** * Format a backtrace line for debug output roughly like debug_print_backtrace() does. * Exceptions already have this built in, but PEAR error objects just give us the array. @@ -238,10 +253,6 @@ function main() return; } - // For database errors - - PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError'); - // Make sure RW database is setup setupRW(); diff --git a/js/jquery.form.js b/js/jquery.form.js index dde394270f..936b847abe 100644 --- a/js/jquery.form.js +++ b/js/jquery.form.js @@ -1,660 +1,632 @@ -/* - * jQuery Form Plugin - * version: 2.36 (07-NOV-2009) - * @requires jQuery v1.2.6 or later - * - * Examples and documentation at: http://malsup.com/jquery/form/ - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - */ -;(function($) { - -/* - Usage Note: - ----------- - Do not use both ajaxSubmit and ajaxForm on the same form. These - functions are intended to be exclusive. Use ajaxSubmit if you want - to bind your own submit handler to the form. For example, - - $(document).ready(function() { - $('#myForm').bind('submit', function() { - $(this).ajaxSubmit({ - target: '#output' - }); - return false; // <-- important! - }); - }); - - Use ajaxForm when you want the plugin to manage all the event binding - for you. For example, - - $(document).ready(function() { - $('#myForm').ajaxForm({ - target: '#output' - }); - }); - - When using ajaxForm, the ajaxSubmit function will be invoked for you - at the appropriate time. -*/ - -/** - * ajaxSubmit() provides a mechanism for immediately submitting - * an HTML form using AJAX. - */ -$.fn.ajaxSubmit = function(options) { - // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) - if (!this.length) { - log('ajaxSubmit: skipping submit process - no element selected'); - return this; - } - - if (typeof options == 'function') - options = { success: options }; - - var url = $.trim(this.attr('action')); - if (url) { - // clean url (don't include hash vaue) - url = (url.match(/^([^#]+)/)||[])[1]; - } - url = url || window.location.href || ''; - - options = $.extend({ - url: url, - type: this.attr('method') || 'GET', - iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank' - }, options || {}); - - // hook for manipulating the form data before it is extracted; - // convenient for use with rich editors like tinyMCE or FCKEditor - var veto = {}; - this.trigger('form-pre-serialize', [this, options, veto]); - if (veto.veto) { - log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); - return this; - } - - // provide opportunity to alter form data before it is serialized - if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { - log('ajaxSubmit: submit aborted via beforeSerialize callback'); - return this; - } - - var a = this.formToArray(options.semantic); - if (options.data) { - options.extraData = options.data; - for (var n in options.data) { - if(options.data[n] instanceof Array) { - for (var k in options.data[n]) - a.push( { name: n, value: options.data[n][k] } ); - } - else - a.push( { name: n, value: options.data[n] } ); - } - } - - // give pre-submit callback an opportunity to abort the submit - if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { - log('ajaxSubmit: submit aborted via beforeSubmit callback'); - return this; - } - - // fire vetoable 'validate' event - this.trigger('form-submit-validate', [a, this, options, veto]); - if (veto.veto) { - log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); - return this; - } - - var q = $.param(a); - - if (options.type.toUpperCase() == 'GET') { - options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; - options.data = null; // data is null for 'get' - } - else - options.data = q; // data is the query string for 'post' - - var $form = this, callbacks = []; - if (options.resetForm) callbacks.push(function() { $form.resetForm(); }); - if (options.clearForm) callbacks.push(function() { $form.clearForm(); }); - - // perform a load on the target only if dataType is not provided - if (!options.dataType && options.target) { - var oldSuccess = options.success || function(){}; - callbacks.push(function(data) { - $(options.target).html(data).each(oldSuccess, arguments); - }); - } - else if (options.success) - callbacks.push(options.success); - - options.success = function(data, status) { - for (var i=0, max=callbacks.length; i < max; i++) - callbacks[i].apply(options, [data, status, $form]); - }; - - // are there files to upload? - var files = $('input:file', this).fieldValue(); - var found = false; - for (var j=0; j < files.length; j++) - if (files[j]) - found = true; - - var multipart = false; -// var mp = 'multipart/form-data'; -// multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); - - // options.iframe allows user to force iframe mode - // 06-NOV-09: now defaulting to iframe mode if file input is detected - if ((files.length && options.iframe !== false) || options.iframe || found || multipart) { - // hack to fix Safari hang (thanks to Tim Molendijk for this) - // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d - if (options.closeKeepAlive) - $.get(options.closeKeepAlive, fileUpload); - else - fileUpload(); - } - else - $.ajax(options); - - // fire 'notify' event - this.trigger('form-submit-notify', [this, options]); - return this; - - - // private function for handling file uploads (hat tip to YAHOO!) - function fileUpload() { - var form = $form[0]; - - if ($(':input[name=submit]', form).length) { - alert('Error: Form elements must not be named "submit".'); - return; - } - - var opts = $.extend({}, $.ajaxSettings, options); - var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts); - - var id = 'jqFormIO' + (new Date().getTime()); - var $io = $('