diff --git a/EVENTS.txt b/EVENTS.txt index 1494a9c890..0a9759c246 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -1089,13 +1089,13 @@ EndGroupSave: After saving a group, aliases, and first member - $group: group that was saved StartInterpretCommand: Before running a command -- $cmd: First word in the string, 'foo' in 'foo argument' +- $cmd: First word in the string, 'foo' in 'foo argument' - $arg: Argument, if any, like 'argument' in 'foo argument' - $user: User who issued the command - &$result: Resulting command; you can set this! EndInterpretCommand: Before running a command -- $cmd: First word in the string, 'foo' in 'foo argument' +- $cmd: First word in the string, 'foo' in 'foo argument' - $arg: Argument, if any, like 'argument' in 'foo argument' - $user: User who issued the command - $result: Resulting command @@ -1111,11 +1111,161 @@ EndGroupActionsList: End the list of actions on a group profile page (before action for output + +EndPeopletagGroupNav: after showing the people tag nav menu +- $menu: the menu widget; use $menu->action for output + +StartShowPeopletagItem: when showing a people tag +- $widget: PeopletagListItem widget + +EndShowPeopletagItem: after showing a people tag +- $widget: PeopletagListItem widget + +StartSubscribePeopletagForm: when showing people tag subscription form +- $action: action being executed (for output and params) +- $peopletag: people tag being subscribed to + +EndSubscribePeopletagForm: after showing the people tag subscription form +- $action: action being executed (for output and params) +- $peopletag: people tag being subscribed to + +StartShowPeopletags: when showing a textual list of people tags +- $widget: PeopletagsWidget; use $widget->out for output +- $tagger: profile of the tagger +- $tagged: profile tagged + +EndShowPeopletags: after showing a textual list of people tags +- $widget: PeopletagsWidget; use $widget->out for output +- $tagger: profile of the tagger +- $tagged: profile tagged + +StartProfileListItemTags: when showing people tags in a profile list item widget +- $widget: ProfileListItem widget + +EndProfileListItemTags: after showing people tags in a profile list item widget +- $widget: ProfileListItem widget + StartActivityObjectOutputAtom: Called at start of Atom XML output generation for ActivityObject chunks, just inside the . Cancel the event to take over its output completely (you're responsible for calling the matching End event if so) - $obj: ActivityObject - $out: XMLOutputter to append custom output @@ -1139,3 +1289,132 @@ StartNoticeWhoGets: Called at start of inbox delivery prep; plugins can schedule EndNoticeWhoGets: Called at end of inbox delivery prep; plugins can filter out profiles from receiving inbox delivery here. Be aware that output can be cached or used several times, so should remain idempotent. - $notice Notice - &$ni: in/out array mapping profile IDs to constants: NOTICE_INBOX_SOURCE_SUB etc + +StartDefaultLocalNav: When showing the default local nav +- $menu: the menu +- $user: current user + +EndDefaultLocalNav: When showing the default local nav +- $menu: the menu +- $user: current user + +StartShowAccountProfileBlock: When showing the profile block for an account +- $out: XMLOutputter to append custom output +- $profile: the profile being shown + +EndShowAccountProfileBlock: After showing the profile block for an account +- $out: XMLOutputter to append custom output +- $profile: the profile being shown + +StartShowGroupProfileBlock: When showing the profile block for a group +- $out: XMLOutputter to append custom output +- $profile: the profile being shown + +EndShowGroupProfileBlock: After showing the profile block for a group +- $out: XMLOutputter to append custom output +- $group: the group being shown + +StartShowThreadedNoticeTail: when showing the replies etc. to a notice +- $nli: parent noticelistitem +- $notice: parent notice +- &$children: list of children + +EndShowThreadedNoticeTail: when showing the replies etc. to a notice +- $nli: parent noticelistitem +- $notice: parent notice +- $children: list of children + +StartShowThreadedNoticeSub: when showing a reply to a notice +- $nli: parent noticelistitem +- $parent: parent notice +- $child: child notice + +EndShowThreadedNoticeSub: when showing a reply to a notice +- $nli: parent noticelistitem +- $parent: parent notice +- $child: child notice + +StartAddEmailAddress: when adding an email address through the Web UI +- $user: user getting the new address +- $email: email being added + +EndAddEmailAddress: done adding an email address through the Web UI +- $user: user getting the new address +- $email: email being added + +StartValidateEmailInvite: when validating an email address for invitations +- $user: user doing the invite +- $email: email address +- &$valid: flag for if it's valid; can be modified + +EndValidateEmailInvite: after validating an email address for invitations +- $user: user doing the invite +- $email: email address +- &$valid: flag for if it's valid; can be modified + +StartLocalURL: before resolving a local url for an action +- &$action: action to find a path for +- &$paramsi: parameters to pass to the action +- &$fragment: any url fragement +- &$addSession: whether to add session variable +- &$url: resulting URL to local resource + +EndLocalURL: before resolving a local url for an action +- &$action: action to find a path for +- &$paramsi: parameters to pass to the action +- &$fragment: any url fragement +- &$addSession: whether to add session variable +- &$url: resulting URL to local resource + +StartProfileGetAvatar: When getting an avatar for a profile +- $profile: profile +- $size: size of the avatar +- &$avatar: avatar + +EndProfileGetAvatar: After getting an avatar for a profile +- $profile: profile +- $size: size of the avatar +- &$avatar: avatar + +StartRegisterSuccess: Before showing the registration success message +- $action: the registration action + +StartRegisterSuccess: After showing the registration success message +- $action: the registration action + +StartDocFileForTitle: Before searching for a doc or mail template +- $title: Title we're looking for +- &$paths: Paths we're searching +- &$filename: Filename so far (set this if you want) + +EndDocFileForTitle: After searching for a doc or mail template +- $title: Title we looked for +- $paths: Paths we searched +- &$filename: Filename so far (set this if you want) + +StartReadWriteTables: when noting which tables must be read-write, even on read-only actions +- &$tables: list of table names +- &$rwdb: read-write database URI + +EndReadWriteTables: after noting which tables must be read-write, even on read-only actions +- $tables: list of table names +- $rwdb: read-write database URI + +StartShowInviteForm: Right before displaying the invitations form +- $action: invitation action + +EndShowInviteForm: After displaying the invitations form +- $action: invitation action + +StartSendInvitations: Right before sending invitations +- $action: invitation action + +EndSendInvitations: Right after sending invitations +- $action: invitation action + +StartShowInvitationSuccess: Right before showing invitations success msg +- $action: invitation action + +EndShowInvitationSuccess: After showing invitations success msg +- $action: invitation action + diff --git a/README b/README index 0dcbeea239..74ef138a2a 100644 --- a/README +++ b/README @@ -2,8 +2,8 @@ README ------ -StatusNet 0.9.6 "Man on the Moon" -29 October 2010 +StatusNet 0.9.7 "World Leader Pretend" +17 March 2011 This is the README file for StatusNet, the Open Source microblogging platform. It includes installation instructions, descriptions of @@ -96,43 +96,47 @@ for additional terms. New this version ================ -This is a security, bug and feature release since version 0.9.5 released on -10 September 2010. +This is a security, bug and feature release since version 0.9.6 released on +23 October 2010. -For best compatibility with client software and site federation, and a lot of -bug fixes, it is highly recommended that all public sites upgrade to the new -version. +For best compatibility with client software and site federation, and a +lot of bug fixes, it is highly recommended that all public sites +upgrade to the new version. Upgrades require new database indexes for +best performance; see Upgrade below. Notable changes this version: -- Site moderators can now delete groups. -- New themes: clean, shiny, mnml, victorian -- New YammerImport plugin allows site admins to import non-private profiles and - message from an authenticated Yammer site. -- New experimental plugins: AnonFavorites, SlicedFavorites, GroupFavorited, - ForceGroup, ShareNotice -- OAuth upgraded to 1.0a -- Localization updates now include plugins, thanks to translatewiki.net! -- SSL link generation should be more consistent; alternate SSL URLs can be - set in the admin UI for more parts of the system. -- Experimental backupuser.php, restoreuser.php command-line scripts to - dump/restore a user's complete activity stream. Can be used to transfer - accounts manually between sites, or to save a backup before deleting. -- Unicode fixes for OStatus notices -- Header metadata on notice pages to aid in manual reposting on Facebook -- Lots of little fixes... +- GroupPrivateMessage plugin lets users send private messages + to a group. (Similar to "private groups" on Yammer.) +- Support for Twitter streaming API in Twitter bridge plugin +- Support for a new Activity Streams-based API using AtomPub, allowing + richer API data. See http://status.net/wiki/AtomPub for details. +- Unified Facebook plugin, replacing previous Facebook application + and Facebook Connect plugin. +- A plugin to send out a daily summary email to network users. +- In-line thumbnails of some attachments (video, images) and oEmbed objects. +- Local copies of remote profiles to let moderators manage OStatus users. +- Upgrade upstream JS, minify everything. +- Allow pushing plugin JS, CSS, and static files to a CDN. +- Configurable nickname rules. +- Better support for bit.ly URL shortener. +- InProcessCache plugin for additional caching on top of memcached. +- Support for Activity Streams JSON feeds on many streams. +- User-initiated backup and restore of account data in Activity Streams + format. +- Bookmark plugin for making del.icio.us-like social bookmarking sites, + including del.icio.us backup file import. Supports OStatus. +- SQLProfile plugin to tune SQL queries. +- Better sorting on timelines to support restored or imported data. +- Hundreds of translations from http://translatewiki.net/ +- Hundreds of performance tunings, bug fixes, and UI improvements. +- Remove deprecated data from Activity Streams Atom output, to the + extent possible. +- NewMenu plugin for new layout of menu items. +- Experimental support for moving an account from one server to + another, using new AtomPub API. -Changes from 0.9.6 release candidate 1: -- fix for broken group pages when logged out -- fix for stuck ping queue entries when bad profile -- fix for bogus single-user nickname config entry error -- i18n updates -- nofollow updates -- SSL-only mode secure cookie fix -- experimental ApiLogger plugin for usage data gathering -- experimental follow-everyone plugin - -A full changelog is available at http://status.net/wiki/StatusNet_0.9.6. +A full changelog is available at http://status.net/wiki/StatusNet_0.9.7. Prerequisites ============= @@ -243,9 +247,9 @@ especially if you've previously installed PHP/MySQL packages. 1. Unpack the tarball you downloaded on your Web server. Usually a command like this will work: - tar zxf statusnet-0.9.6.tar.gz + tar zxf statusnet-0.9.7.tar.gz - ...which will make a statusnet-0.9.6 subdirectory in your current + ...which will make a statusnet-0.9.7 subdirectory in your current directory. (If you don't have shell access on your Web server, you may have to unpack the tarball on your local computer and FTP the files to the server.) @@ -253,7 +257,7 @@ especially if you've previously installed PHP/MySQL packages. 2. Move the tarball to a directory of your choosing in your Web root directory. Usually something like this will work: - mv statusnet-0.9.6 /var/www/statusnet + mv statusnet-0.9.7 /var/www/statusnet This will make your StatusNet instance available in the statusnet path of your server, like "http://example.net/statusnet". "microblog" or @@ -668,7 +672,7 @@ with this situation. If you've been using StatusNet 0.7, 0.6, 0.5 or lower, or if you've been tracking the "git" version of the software, you will probably want to upgrade and keep your existing data. There is no automated -upgrade procedure in StatusNet 0.9.6. Try these step-by-step +upgrade procedure in StatusNet 0.9.7. Try these step-by-step instructions; read to the end first before trying them. 0. Download StatusNet and set up all the prerequisites as if you were @@ -689,25 +693,30 @@ instructions; read to the end first before trying them. 5. Once all writing processes to your site are turned off, make a final backup of the Web directory and database. 6. Move your StatusNet directory to a backup spot, like "statusnet.bak". -7. Unpack your StatusNet 0.9.6 tarball and move it to "statusnet" or +7. Unpack your StatusNet 0.9.7 tarball and move it to "statusnet" or wherever your code used to be. 8. Copy the config.php file and the contents of the avatar/, background/, file/, and local/ subdirectories from your old directory to your new directory. 9. Copy htaccess.sample to .htaccess in the new directory. Change the RewriteBase to use the correct path. -10. Rebuild the database. (You can safely skip this step and go to #12 - if you're upgrading from another 0.9.x version). +10. Rebuild the database. NOTE: this step is destructive and cannot be reversed. YOU CAN EASILY DESTROY YOUR SITE WITH THIS STEP. Don't do it without a known-good backup! - If your database is at version 0.8.0 or above, you can run a + If your database is at version 0.8.0 or higher in the 0.8.x line, you can run a special upgrade script: mysql -u -p db/08to09.sql + If you are upgrading from any 0.9.x version like 0.9.6, run this script: + + mysql -u -p db/096to097.sql + + Despite the name, it should work for any 0.9.x branch. + Otherwise, go to your StatusNet directory and AFTER YOU MAKE A BACKUP run the rebuilddb.sh script like this: @@ -1143,6 +1152,9 @@ ssl: Whether to use SSL for JavaScript files. Default is null, which means sslserver: SSL server to use when page is HTTPS-encrypted. If unspecified, site ssl server and so on will be used. sslpath: If sslserver if defined, path to use when page is HTTPS-encrypted. +bustframes: If true, all web pages will break out of framesets. If false, + can comfortably live in a frame or iframe... probably. Default + to true. xmpp ---- @@ -1392,13 +1404,25 @@ maxaliases: maximum number of aliases a group can have. Default 3. Set desclimit: maximum number of characters to allow in group descriptions. null (default) means to use the site-wide text limits. 0 means no limit. +addtag: Whether to add a tag for the group nickname for every group post + (pre-1.0.x behaviour). Defaults to false. -oohembed +oembed -------- -oEmbed endpoint for multimedia attachments (links in posts). +oEmbed endpoint for multimedia attachments (links in posts). Will also +work as 'oohembed' for backwards compatibility. -endpoint: oohembed endpoint using http://oohembed.com/ software. +endpoint: oohembed endpoint using http://oohembed.com/ software. Defaults to + 'http://oohembed.com/oohembed/'. +order: Array of methods to check for OEmbed data. Methods include 'built-in' + (use a built-in function to simulate oEmbed for some sites), + 'well-known' (use well-known public oEmbed endpoints), + 'discovery' (discover using headers in HTML), 'service' (use + a third-party service, like oohembed or embed.ly. Default is + array('built-in', 'well-known', 'service', 'discovery'). Note that very + few sites implement oEmbed; 'discovery' is going to fail 99% of the + time. search ------ @@ -1472,6 +1496,8 @@ Configuration options specific to notices. contentlimit: max length of the plain-text content of a notice. Default is null, meaning to use the site-wide text limit. 0 means no limit. +defaultscope: default scope for notices. Defaults to 0; set to + 1 to keep notices private to this site by default. message ------- diff --git a/actions/addpeopletag.php b/actions/addpeopletag.php new file mode 100644 index 0000000000..1e130e27e8 --- /dev/null +++ b/actions/addpeopletag.php @@ -0,0 +1,178 @@ +. + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/togglepeopletag.php'; + +/** + * + * Action to tag a profile with a single tag. + * + * Takes parameters: + * + * - tagged: the ID of the profile being tagged + * - token: session token to prevent CSRF attacks + * - ajax: boolean; whether to return Ajax or full-browser results + * - peopletag_id: the ID of the tag being used + * + * Only works if the current user is logged in. + * + * @category Action + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ +class AddpeopletagAction extends Action +{ + var $user; + var $tagged; + var $peopletag; + + /** + * Check pre-requisites and instantiate attributes + * + * @param Array $args array of arguments (URL, GET, POST) + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + // CSRF protection + + $token = $this->trimmed('token'); + + if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. + $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)) { + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. + $this->clientError(_('Not logged in.')); + return false; + } + + // Profile to subscribe to + + $tagged_id = $this->arg('tagged'); + + $this->tagged = Profile::staticGet('id', $tagged_id); + + if (empty($this->tagged)) { + // TRANS: Client error displayed trying to perform an action related to a non-existing profile. + $this->clientError(_('No such profile.')); + return false; + } + + $id = $this->arg('peopletag_id'); + $this->peopletag = Profile_list::staticGet('id', $id); + + if (empty($this->peopletag)) { + // TRANS: Client error displayed trying to reference a non-existing list. + $this->clientError(_('No such list.')); + return false; + } + + // OMB 0.1 doesn't have a mechanism for local-server- + // originated tag. + + $omb01 = Remote_profile::staticGet('id', $tagged_id); + + if (!empty($omb01)) { + // TRANS: Client error displayed when trying to add an OMB 0.1 remote profile to a list. + $this->clientError(_('You cannot list an OMB 0.1 '. + 'remote profile with this action.')); + return false; + } + + return true; + } + + /** + * Handle request + * + * Does the tagging and returns results. + * + * @param Array $args unused. + * + * @return void + */ + function handle($args) + { + // Throws exception on error + $ptag = Profile_tag::setTag($this->user->id, $this->tagged->id, + $this->peopletag->tag); + + if (!$ptag) { + $user = User::staticGet('id', $id); + if ($user) { + $this->clientError( + // TRANS: Client error displayed when an unknown error occurs when adding a user to a list. + // TRANS: %s is a username. + sprintf(_('There was an unexpected error while listing %s.'), + $user->nickname)); + } else { + // TRANS: Client error displayed when an unknown error occurs when adding a user to a list. + // TRANS: %s is a profile URL. + $this->clientError(sprintf(_('There was a problem listing %s. ' . + 'The remote server is probably not responding correctly. ' . + 'Please try retrying later.'), $this->profile->profileurl)); + } + return false; + } + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + // TRANS: Title after adding a user to a list. + $this->element('title', null, _m('TITLE','Listed')); + $this->elementEnd('head'); + $this->elementStart('body'); + $unsubscribe = new UntagButton($this, $this->tagged, $this->peopletag); + $unsubscribe->show(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + $url = common_local_url('subscriptions', + array('nickname' => $this->user->nickname)); + common_redirect($url, 303); + } + } +} diff --git a/actions/all.php b/actions/all.php index 2826319c0d..2e53691636 100644 --- a/actions/all.php +++ b/actions/all.php @@ -55,17 +55,15 @@ class AllAction extends ProfileAction function prepare($args) { parent::prepare($args); - $cur = common_current_user(); - if (!empty($cur) && $cur->id == $this->user->id) { - $this->notice = $this->user->noticeInboxThreaded(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1); - } else { - $this->notice = $this->user->noticesWithFriendsThreaded(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1); - } + $stream = new ThreadingInboxNoticeStream($this->user, Profile::current()); + + $this->notice = $stream->getNotices(($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); + $this->serverError(_('No such page.'), 404); } return true; @@ -86,12 +84,15 @@ 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); + $user = common_current_user(); + if ($user->id == $this->user->id) { + // TRANS: Title of a user's own start page. + return _('Home timeline'); } else { - // TRANS: Page title. %s is user nickname - return sprintf(_("%s and friends"), $this->user->nickname); + $profile = $this->user->getProfile(); + // TRANS: Title of another user's start page. + // TRANS: %s is the other user's name. + return sprintf(_("%s's home timeline"), $profile->getBestName()); } } @@ -157,7 +158,16 @@ class AllAction extends ProfileAction function showContent() { if (Event::handle('StartShowAllContent', array($this))) { - $nl = new ThreadedNoticeList($this->notice, $this); + + $profile = null; + + $current_user = common_current_user(); + + if (!empty($current_user)) { + $profile = $current_user->getProfile(); + } + + $nl = new ThreadedNoticeList($this->notice, $this, $profile); $cnt = $nl->show(); @@ -174,15 +184,21 @@ class AllAction extends ProfileAction } } - function showPageTitle() + function showSections() { - $user = common_current_user(); - if ($user && ($user->id == $this->user->id)) { - // TRANS: H1 text for page when viewing a list for self. - $this->element('h1', null, _("You and friends")); - } else { - // TRANS: H1 text for page. %s is a user nickname. - $this->element('h1', null, sprintf(_('%s and friends'), $this->user->nickname)); - } + $ibs = new InviteButtonSection($this); + $ibs->show(); + $pop = new PopularNoticeSection($this); + $pop->show(); + // $pop = new InboxTagCloudSection($this, $this->user); + // $pop->show(); + } +} + +class ThreadingInboxNoticeStream extends ThreadingNoticeStream +{ + function __construct($user, $profile) + { + parent::__construct(new InboxNoticeStream($user, $profile)); } } diff --git a/actions/allrss.php b/actions/allrss.php index 573bb4eb2f..6d82e551e7 100644 --- a/actions/allrss.php +++ b/actions/allrss.php @@ -83,16 +83,9 @@ class AllrssAction extends Rss10Action */ function getNotices($limit=0) { - $cur = common_current_user(); - $user = $this->user; + $stream = new InboxNoticeStream($this->user); + $notice = $stream->getNotices(0, $limit, null, null); - if (!empty($cur) && $cur->id == $user->id) { - $notice = $this->user->noticeInbox(0, $limit); - } else { - $notice = $this->user->noticesWithFriends(0, $limit); - } - - $notice = $user->noticesWithFriends(0, $limit); $notices = array(); while ($notice->fetch()) { diff --git a/actions/apiaccountratelimitstatus.php b/actions/apiaccountratelimitstatus.php index 8d7f89eadc..8490e2965c 100644 --- a/actions/apiaccountratelimitstatus.php +++ b/actions/apiaccountratelimitstatus.php @@ -66,6 +66,7 @@ class ApiAccountRateLimitStatusAction extends ApiBareAuthAction if (!in_array($this->format, array('xml', 'json'))) { $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apiaccountupdatedeliverydevice.php b/actions/apiaccountupdatedeliverydevice.php index a36806b216..57e4fbfa00 100644 --- a/actions/apiaccountupdatedeliverydevice.php +++ b/actions/apiaccountupdatedeliverydevice.php @@ -88,7 +88,7 @@ class ApiAccountUpdateDeliveryDeviceAction extends ApiAuthAction if (!in_array($this->format, array('xml', 'json'))) { $this->clientError( - // TRANS: Client error displayed handling a non-existing API method. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apiaccountupdateprofile.php b/actions/apiaccountupdateprofile.php index d0b9abe9b7..15b13c0f20 100644 --- a/actions/apiaccountupdateprofile.php +++ b/actions/apiaccountupdateprofile.php @@ -90,7 +90,7 @@ class ApiAccountUpdateProfileAction extends ApiAuthAction if (!in_array($this->format, array('xml', 'json'))) { $this->clientError( - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format @@ -107,7 +107,7 @@ class ApiAccountUpdateProfileAction extends ApiAuthAction $profile = $this->user->getProfile(); if (empty($profile)) { - // TRANS: Client error displayed if a user profile could not be found. + // TRANS: Error message displayed when referring to a user without a profile. $this->clientError(_('User has no profile.')); return; } diff --git a/actions/apiaccountupdateprofilebackgroundimage.php b/actions/apiaccountupdateprofilebackgroundimage.php index f26c30198d..bb0cef52a4 100644 --- a/actions/apiaccountupdateprofilebackgroundimage.php +++ b/actions/apiaccountupdateprofilebackgroundimage.php @@ -88,7 +88,7 @@ class ApiAccountUpdateProfileBackgroundImageAction extends ApiAuthAction if (!in_array($this->format, array('xml', 'json'))) { $this->clientError( - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format @@ -195,7 +195,7 @@ class ApiAccountUpdateProfileBackgroundImageAction extends ApiAuthAction $profile = $this->user->getProfile(); if (empty($profile)) { - // TRANS: Client error displayed when a user has no profile. + // TRANS: Error message displayed when referring to a user without a profile. $this->clientError(_('User has no profile.')); return; } diff --git a/actions/apiaccountupdateprofilecolors.php b/actions/apiaccountupdateprofilecolors.php index 4c102c4090..4fa85c6877 100644 --- a/actions/apiaccountupdateprofilecolors.php +++ b/actions/apiaccountupdateprofilecolors.php @@ -111,7 +111,7 @@ class ApiAccountUpdateProfileColorsAction extends ApiAuthAction if (!in_array($this->format, array('xml', 'json'))) { $this->clientError( - // TRANS: Client error displayed trying to execute an unknown API method updating profile colours. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format @@ -179,7 +179,7 @@ class ApiAccountUpdateProfileColorsAction extends ApiAuthAction $profile = $this->user->getProfile(); if (empty($profile)) { - // TRANS: Client error displayed a user has no profile updating profile colours. + // TRANS: Error message displayed when referring to a user without a profile. $this->clientError(_('User has no profile.')); return; } diff --git a/actions/apiaccountupdateprofileimage.php b/actions/apiaccountupdateprofileimage.php index 986a8f3f1e..3bf064ea89 100644 --- a/actions/apiaccountupdateprofileimage.php +++ b/actions/apiaccountupdateprofileimage.php @@ -127,7 +127,7 @@ class ApiAccountUpdateProfileImageAction extends ApiAuthAction $profile = $this->user->getProfile(); if (empty($profile)) { - // TRANS: Client error displayed if a user profile could not be found updating a profile image. + // TRANS: Error message displayed when referring to a user without a profile. $this->clientError(_('User has no profile.')); return; } diff --git a/actions/apiaccountverifycredentials.php b/actions/apiaccountverifycredentials.php index 26d4e2fc5c..359939b0cc 100644 --- a/actions/apiaccountverifycredentials.php +++ b/actions/apiaccountverifycredentials.php @@ -64,7 +64,7 @@ class ApiAccountVerifyCredentialsAction extends ApiAuthAction parent::handle($args); if (!in_array($this->format, array('xml', 'json'))) { - // TRANS: Client error displayed trying to execute an unknown API method verifying user credentials. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); return; } diff --git a/actions/apiblockcreate.php b/actions/apiblockcreate.php index 6942a53bb8..766d91bd41 100644 --- a/actions/apiblockcreate.php +++ b/actions/apiblockcreate.php @@ -92,7 +92,7 @@ class ApiBlockCreateAction extends ApiAuthAction } if (empty($this->user) || empty($this->other)) { - // TRANS: Client error displayed when trying to block a non-existing user or a user from another site. + // TRANS: Client error displayed when trying to block a non-existing user or a user from another site. $this->clientError(_('No such user.'), 404, $this->format); return; } diff --git a/actions/apidirectmessage.php b/actions/apidirectmessage.php index e072e27b83..584decc747 100644 --- a/actions/apidirectmessage.php +++ b/actions/apidirectmessage.php @@ -153,7 +153,7 @@ class ApiDirectMessageAction extends ApiAuthAction $this->showJsonDirectMessages(); break; default: - // TRANS: Client error given when an API method was not found (404). + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); break; } diff --git a/actions/apifavoritecreate.php b/actions/apifavoritecreate.php index b2f6266ebf..b890d4af69 100644 --- a/actions/apifavoritecreate.php +++ b/actions/apifavoritecreate.php @@ -94,7 +94,7 @@ class ApiFavoriteCreateAction extends ApiAuthAction if (!in_array($this->format, array('xml', 'json'))) { $this->clientError( - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apifavoritedestroy.php b/actions/apifavoritedestroy.php index f86c985dc0..db121ac882 100644 --- a/actions/apifavoritedestroy.php +++ b/actions/apifavoritedestroy.php @@ -94,7 +94,7 @@ class ApiFavoriteDestroyAction extends ApiAuthAction if (!in_array($this->format, array('xml', 'json'))) { $this->clientError( - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apifriendshipscreate.php b/actions/apifriendshipscreate.php index 89557f8392..9932809818 100644 --- a/actions/apifriendshipscreate.php +++ b/actions/apifriendshipscreate.php @@ -95,7 +95,7 @@ class ApiFriendshipsCreateAction extends ApiAuthAction if (!in_array($this->format, array('xml', 'json'))) { $this->clientError( - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apifriendshipsdestroy.php b/actions/apifriendshipsdestroy.php index a5dff08bab..1534aa799f 100644 --- a/actions/apifriendshipsdestroy.php +++ b/actions/apifriendshipsdestroy.php @@ -95,7 +95,7 @@ class ApiFriendshipsDestroyAction extends ApiAuthAction if (!in_array($this->format, array('xml', 'json'))) { $this->clientError( - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apifriendshipsshow.php b/actions/apifriendshipsshow.php index 6b069c4fcf..1eaca49f0e 100644 --- a/actions/apifriendshipsshow.php +++ b/actions/apifriendshipsshow.php @@ -120,7 +120,7 @@ class ApiFriendshipsShowAction extends ApiBareAuthAction parent::handle($args); if (!in_array($this->format, array('xml', 'json'))) { - // TRANS: Client error displayed trying to execute an unknown API method showing friendship. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), 404); return; } diff --git a/actions/apigroupcreate.php b/actions/apigroupcreate.php index d01504bc80..8615bcff7e 100644 --- a/actions/apigroupcreate.php +++ b/actions/apigroupcreate.php @@ -134,7 +134,7 @@ class ApiGroupCreateAction extends ApiAuthAction break; default: $this->clientError( - // TRANS: Client error given when an API method was not found (404). + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apigroupismember.php b/actions/apigroupismember.php index 8d31c65ddb..13ed9e1fbf 100644 --- a/actions/apigroupismember.php +++ b/actions/apigroupismember.php @@ -111,7 +111,7 @@ class ApiGroupIsMemberAction extends ApiBareAuthAction break; default: $this->clientError( - // TRANS: Client error displayed trying to execute an unknown API method showing group membership. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 400, $this->format diff --git a/actions/apigroupjoin.php b/actions/apigroupjoin.php index 7124a4b0f0..6f3df0d8cd 100644 --- a/actions/apigroupjoin.php +++ b/actions/apigroupjoin.php @@ -144,7 +144,7 @@ class ApiGroupJoinAction extends ApiAuthAction break; default: $this->clientError( - // TRANS: Client error displayed trying to execute an unknown API method joining a group. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apigroupleave.php b/actions/apigroupleave.php index 35a4e04d78..9d2825b00e 100644 --- a/actions/apigroupleave.php +++ b/actions/apigroupleave.php @@ -134,7 +134,7 @@ class ApiGroupLeaveAction extends ApiAuthAction break; default: $this->clientError( - // TRANS: Client error displayed trying to execute an unknown API method leaving a group. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apigrouplist.php b/actions/apigrouplist.php index f80fbce932..c7518ca129 100644 --- a/actions/apigrouplist.php +++ b/actions/apigrouplist.php @@ -67,6 +67,7 @@ class ApiGroupListAction extends ApiBareAuthAction $this->user = $this->getTargetUser(null); if (empty($this->user)) { + // TRANS: Client error displayed when user not found for an action. $this->clientError(_('No such user.'), 404, $this->format); return false; } @@ -130,7 +131,7 @@ class ApiGroupListAction extends ApiBareAuthAction break; default: $this->clientError( - // TRANS: Client error displayed trying to execute an unknown API method checking group membership. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apigrouplistall.php b/actions/apigrouplistall.php index d05baa0992..51c3df1b2f 100644 --- a/actions/apigrouplistall.php +++ b/actions/apigrouplistall.php @@ -89,7 +89,7 @@ class ApiGroupListAllAction extends ApiPrivateAuthAction $taguribase = TagURI::base(); $id = "tag:$taguribase:Groups"; $link = common_local_url('groups'); - // TRANS: Message is used as a subtitle when listing the lastest 20 groups. %s is a site name. + // TRANS: Message is used as a subtitle when listing the latest 20 groups. %s is a site name. $subtitle = sprintf(_("groups on %s"), $sitename); switch($this->format) { @@ -116,7 +116,7 @@ class ApiGroupListAllAction extends ApiPrivateAuthAction break; default: $this->clientError( - // TRANS: Client error displayed trying to execute an unknown API method listing the latest 20 groups. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apigroupmembership.php b/actions/apigroupmembership.php index 939d22d757..7ad8fb767e 100644 --- a/actions/apigroupmembership.php +++ b/actions/apigroupmembership.php @@ -101,7 +101,7 @@ class ApiGroupMembershipAction extends ApiPrivateAuthAction break; default: $this->clientError( - // TRANS: Client error displayed trying to execute an unknown API method showing group membership. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apigroupprofileupdate.php b/actions/apigroupprofileupdate.php index 9a629a47d7..379e01a428 100644 --- a/actions/apigroupprofileupdate.php +++ b/actions/apigroupprofileupdate.php @@ -85,6 +85,7 @@ class ApiGroupProfileUpdateAction extends ApiAuthAction if ($_SERVER['REQUEST_METHOD'] != 'POST') { $this->clientError( + // TRANS: Client error message. POST is a HTTP command. It should not be translated. _('This method requires a POST.'), 400, $this->format ); @@ -93,7 +94,7 @@ class ApiGroupProfileUpdateAction extends ApiAuthAction if (!in_array($this->format, array('xml', 'json'))) { $this->clientError( - // TRANS: Client error displayed when using an unsupported API format. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format @@ -211,7 +212,7 @@ class ApiGroupProfileUpdateAction extends ApiAuthAction $this->showSingleJsonGroup($this->group); break; default: - // TRANS: Client error displayed when using an unsupported API format. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), 404, $this->format); break; } diff --git a/actions/apigroupshow.php b/actions/apigroupshow.php index 471aa141f9..a7385ffaaf 100644 --- a/actions/apigroupshow.php +++ b/actions/apigroupshow.php @@ -110,7 +110,7 @@ class ApiGroupShowAction extends ApiPrivateAuthAction $this->showSingleJsonGroup($this->group); break; default: - // TRANS: Client error displayed trying to execute an unknown API method showing a group. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), 404, $this->format); break; } diff --git a/actions/apihelptest.php b/actions/apihelptest.php index fbe5f12784..1bbbe572bf 100644 --- a/actions/apihelptest.php +++ b/actions/apihelptest.php @@ -80,7 +80,7 @@ class ApiHelpTestAction extends ApiPrivateAuthAction $this->endDocument('json'); } else { $this->clientError( - // TRANS: Client error displayed trying to execute an unknown API method testing API connectivity. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apilist.php b/actions/apilist.php new file mode 100644 index 0000000000..90cbfddb69 --- /dev/null +++ b/actions/apilist.php @@ -0,0 +1,270 @@ +. + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apibareauth.php'; + +class ApiListAction extends ApiBareAuthAction +{ + /** + * The list in question in the current request + */ + var $list = null; + + /** + * Is this an update request? + */ + var $update = false; + + /** + * Is this a delete request? + */ + var $delete = false; + + /** + * Set the flags for handling the request. Show list if this is a GET + * request, update it if it is POST, delete list if method is DELETE + * or if method is POST and an argument _method is set to DELETE. Act + * like we don't know if the current user has no access to the list. + * + * Takes parameters: + * - user: the user id or nickname + * - id: the id of the tag or the tag itself + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->delete = ($_SERVER['REQUEST_METHOD'] == 'DELETE' || + ($this->trimmed('_method') == 'DELETE' && + $_SERVER['REQUEST_METHOD'] == 'POST')); + + // update list if method is POST or PUT and $this->delete is not true + $this->update = (!$this->delete && + in_array($_SERVER['REQUEST_METHOD'], array('POST', 'PUT'))); + + $this->user = $this->getTargetUser($this->arg('user')); + $this->list = $this->getTargetList($this->arg('user'), $this->arg('id')); + + if (empty($this->list)) { + // TRANS: Client error displayed when referring to a non-existing list. + $this->clientError(_('List not found.'), 404, $this->format); + return false; + } + + return true; + } + + /** + * Handle the request + * + * @return boolean success flag + */ + function handle($args) + { + parent::handle($args); + + if($this->delete) { + $this->handleDelete(); + return true; + } + + if($this->update) { + $this->handlePut(); + return true; + } + + switch($this->format) { + case 'xml': + $this->showSingleXmlList($this->list); + break; + case 'json': + $this->showSingleJsonList($this->list); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + break; + } + } + + /** + * require authentication if it is a write action or user is ambiguous + * + */ + function requiresAuth() + { + return parent::requiresAuth() || + $this->create || $this->delete; + } + + /** + * Update a list + * + * @return boolean success + */ + function handlePut() + { + if($this->auth_user->id != $this->list->tagger) { + $this->clientError( + // TRANS: Client error displayed when trying to update another user's list. + _('You cannot update lists that do not belong to you.'), + 401, + $this->format + ); + } + + $new_list = clone($this->list); + $new_list->tag = common_canonical_tag($this->arg('name')); + $new_list->description = common_canonical_tag($this->arg('description')); + $new_list->private = ($this->arg('mode') === 'private') ? true : false; + + $result = $new_list->update($this->list); + + if(!$result) { + $this->clientError( + // TRANS: Client error displayed when an unknown error occurs updating a list. + _('An error occured.'), + 503, + $this->format + ); + } + + switch($this->format) { + case 'xml': + $this->showSingleXmlList($new_list); + break; + case 'json': + $this->showSingleJsonList($new_list); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + break; + } + } + + /** + * Delete a list + * + * @return boolean success + */ + function handleDelete() + { + if($this->auth_user->id != $this->list->tagger) { + $this->clientError( + // TRANS: Client error displayed when trying to delete another user's list. + _('You cannot delete lists that do not belong to you.'), + 401, + $this->format + ); + } + + $record = clone($this->list); + $this->list->delete(); + + switch($this->format) { + case 'xml': + $this->showSingleXmlList($record); + break; + case 'json': + $this->showSingleJsonList($record); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + break; + } + } + + /** + * Indicate that this resource is not read-only. + * + * @return boolean is_read-only=false + */ + function isReadOnly($args) + { + return false; + } + + /** + * When was the list (people tag) last updated? + * + * @return String time_last_modified + */ + function lastModified() + { + if(!empty($this->list)) { + return strtotime($this->list->modified); + } + return null; + } + + /** + * An entity tag for this list + * + * Returns an Etag based on the action name, language, user ID and + * timestamps of the first and last list the user has joined + * + * @return string etag + */ + function etag() + { + if (!empty($this->list)) { + + return '"' . implode( + ':', + array($this->arg('action'), + common_language(), + $this->user->id, + strtotime($this->list->created), + strtotime($this->list->modified)) + ) + . '"'; + } + + return null; + } +} diff --git a/actions/apilistmember.php b/actions/apilistmember.php new file mode 100644 index 0000000000..8e6a84c96d --- /dev/null +++ b/actions/apilistmember.php @@ -0,0 +1,125 @@ +. + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apibareauth.php'; + +/** + * Action handler for Twitter list_memeber methods + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * @see ApiBareAuthAction + */ +class ApiListMemberAction extends ApiBareAuthAction +{ + /** + * Set the flags for handling the request. Show the profile if this + * is a GET request AND the profile is a member of the list, add a member + * if it is a POST, remove the profile from the list if method is DELETE + * or if method is POST and an argument _method is set to DELETE. Act + * like we don't know if the current user has no access to the list. + * + * Takes parameters: + * - user: the user id or nickname + * - list_id: the id of the tag or the tag itself + * - id: the id of the member being looked for/added/removed + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->user = $this->getTargetUser($this->arg('id')); + $this->list = $this->getTargetList($this->arg('user'), $this->arg('list_id')); + + if (empty($this->list)) { + // TRANS: Client error displayed when referring to a non-existing list. + $this->clientError(_('List not found.'), 404, $this->format); + return false; + } + + if (empty($this->user)) { + // TRANS: Client error displayed when referring to a non-existing user. + $this->clientError(_('No such user.'), 404, $this->format); + return false; + } + return true; + } + + /** + * Handle the request + * + * @return boolean success flag + */ + function handle($args) + { + parent::handle($args); + + $arr = array('tagger' => $this->list->tagger, + 'tag' => $this->list->tag, + 'tagged' => $this->user->id); + $ptag = Profile_tag::pkeyGet($arr); + + if(empty($ptag)) { + $this->clientError( + // TRANS: Client error displayed when referring to a non-list member. + _('The specified user is not a member of this list.'), + 400, + $this->format + ); + } + + $user = $this->twitterUserArray($this->user->getProfile(), true); + + switch($this->format) { + case 'xml': + $this->showTwitterXmlUser($user, 'user', true); + break; + case 'json': + $this->showSingleJsonUser($user); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + break; + } + return true; + } +} diff --git a/actions/apilistmembers.php b/actions/apilistmembers.php new file mode 100644 index 0000000000..0c003fd6dc --- /dev/null +++ b/actions/apilistmembers.php @@ -0,0 +1,179 @@ +. + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apilistusers.php'; + +class ApiListMembersAction extends ApiListUsersAction +{ + /** + * Add a user to a list (tag someone) + * + * @return boolean success + */ + function handlePost() + { + if($this->auth_user->id != $this->list->tagger) { + $this->clientError( + // TRANS: Client error displayed when trying to add members to a list without having the right to do so. + _('You are not allowed to add members to this list.'), + 401, + $this->format + ); + return false; + } + + if($this->user === false) { + $this->clientError( + // TRANS: Client error displayed when trying to modify list members without specifying them. + _('You must specify a member.'), + 400, + $this->format + ); + return false; + } + + $result = Profile_tag::setTag($this->auth_user->id, + $this->user->id, $this->list->tag); + + if(empty($result)) { + $this->clientError( + // TRANS: Client error displayed when an unknown error occurs viewing list members. + _('An error occured.'), + 500, + $this->format + ); + return false; + } + + switch($this->format) { + case 'xml': + $this->showSingleXmlList($this->list); + break; + case 'json': + $this->showSingleJsonList($this->list); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + return false; + break; + } + } + + /** + * Remove a user from a list (untag someone) + * + * @return boolean success + */ + function handleDelete() + { + if($this->auth_user->id != $this->list->tagger) { + $this->clientError( + // TRANS: Client error displayed when trying to remove members from a list without having the right to do so. + _('You are not allowed to remove members from this list.'), + 401, + $this->format + ); + return false; + } + + if($this->user === false) { + $this->clientError( + // TRANS: Client error displayed when trying to modify list members without specifying them. + _('You must specify a member.'), + 400, + $this->format + ); + return false; + } + + $args = array('tagger' => $this->auth_user->id, + 'tagged' => $this->user->id, + 'tag' => $this->list->tag); + $ptag = Profile_tag::pkeyGet($args); + + if(empty($ptag)) { + $this->clientError( + // TRANS: Client error displayed when trying to remove a list member that is not part of a list. + _('The user you are trying to remove from the list is not a member.'), + 400, + $this->format + ); + return false; + } + + $result = $ptag->delete(); + + if(empty($result)) { + $this->clientError( + // TRANS: Client error displayed when an unknown error occurs viewing list members. + _('An error occured.'), + 500, + $this->format + ); + return false; + } + + switch($this->format) { + case 'xml': + $this->showSingleXmlList($this->list); + break; + case 'json': + $this->showSingleJsonList($this->list); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + return false; + break; + } + return true; + } + + /** + * List the members of a list (people tagged) + */ + function getUsers() + { + $fn = array($this->list, 'getTagged'); + list($this->users, $this->next_cursor, $this->prev_cursor) = + Profile_list::getAtCursor($fn, array(), $this->cursor, 20); + } +} diff --git a/actions/apilistmemberships.php b/actions/apilistmemberships.php new file mode 100644 index 0000000000..3d235c5d93 --- /dev/null +++ b/actions/apilistmemberships.php @@ -0,0 +1,134 @@ +. + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apibareauth.php'; + +/** + * Action handler for API method to list lists a user belongs to. + * (people tags for a user) + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * @see ApiBareAuthAction + */ +class ApiListMembershipsAction extends ApiBareAuthAction +{ + var $lists = array(); + var $cursor = -1; + var $next_cursor = 0; + var $prev_cursor = 0; + + /** + * Prepare for running the action + * Take arguments for running:s + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + function prepare($args) + { + parent::prepare($args); + + $this->cursor = (int) $this->arg('cursor', -1); + $this->user = $this->getTargetUser($this->arg('user')); + + if (empty($this->user)) { + // TRANS: Client error displayed trying to perform an action related to a non-existing user. + $this->clientError(_('No such user.'), 404, $this->format); + return; + } + + $this->getLists(); + + return true; + } + + /** + * Handle the request + * + * Show the lists + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + switch($this->format) { + case 'xml': + $this->showXmlLists($this->lists, $this->next_cursor, $this->prev_cursor); + break; + case 'json': + $this->showJsonLists($this->lists, $this->next_cursor, $this->prev_cursor); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 400, + $this->format + ); + break; + } + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return true; + } + + function getLists() + { + $profile = $this->user->getProfile(); + $fn = array($profile, 'getOtherTags'); + + # 20 lists + list($this->lists, $this->next_cursor, $this->prev_cursor) = + Profile_list::getAtCursor($fn, array($this->auth_user), $this->cursor, 20); + } +} diff --git a/actions/apilists.php b/actions/apilists.php new file mode 100644 index 0000000000..5eab3e6a1f --- /dev/null +++ b/actions/apilists.php @@ -0,0 +1,240 @@ +. + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apibareauth.php'; + +/** + * Action handler for Twitter list_memeber methods + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * @see ApiBareAuthAction + */ +class ApiListsAction extends ApiBareAuthAction +{ + var $lists = null; + var $cursor = 0; + var $next_cursor = 0; + var $prev_cursor = 0; + var $create = false; + + /** + * Set the flags for handling the request. List lists created by user if this + * is a GET request, create a new list if it is a POST request. + * + * Takes parameters: + * - user: the user id or nickname + * Parameters for POST request + * - name: name of the new list (the people tag itself) + * - mode: (optional) mode for the new list private/public + * - description: (optional) description for the list + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->create = ($_SERVER['REQUEST_METHOD'] == 'POST'); + + if (!$this->create) { + + $this->user = $this->getTargetUser($this->arg('user')); + + if (empty($this->user)) { + // TRANS: Client error displayed trying to perform an action related to a non-existing user. + $this->clientError(_('No such user.'), 404, $this->format); + return false; + } + $this->getLists(); + } + + return true; + } + + /** + * require authentication if it is a write action or user is ambiguous + * + */ + function requiresAuth() + { + return parent::requiresAuth() || + $this->create || $this->delete; + } + + /** + * Handle request: + * Show the lists the user has created if the request method is GET + * Create a new list by diferring to handlePost() if it is POST. + */ + function handle($args) + { + parent::handle($args); + + if($this->create) { + return $this->handlePost(); + } + + switch($this->format) { + case 'xml': + $this->showXmlLists($this->lists, $this->next_cursor, $this->prev_cursor); + break; + case 'json': + $this->showJsonLists($this->lists, $this->next_cursor, $this->prev_cursor); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + break; + } + } + + /** + * Create a new list + * + * @return boolean success + */ + function handlePost() + { + $name=$this->arg('name'); + if(empty($name)) { + // mimick twitter + // TRANS: Client error displayed when trying to create a list without a name. + print _("A list must have a name."); + exit(1); + } + + // twitter creates a new list by appending a number to the end + // if the list by the given name already exists + // it makes more sense to return the existing list instead + + $private = null; + if ($this->arg('mode') === 'public') { + $private = false; + } else if ($this->arg('mode') === 'private') { + $private = true; + } + + $list = Profile_list::ensureTag($this->auth_user->id, + $this->arg('name'), + $this->arg('description'), + $private); + if (empty($list)) { + return false; + } + + switch($this->format) { + case 'xml': + $this->showSingleXmlList($list); + break; + case 'json': + $this->showSingleJsonList($list); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + break; + } + return true; + } + + /** + * Get lists + */ + function getLists() + { + $cursor = (int) $this->arg('cursor', -1); + + // twitter fixes count at 20 + // there is no argument named count + $count = 20; + $profile = $this->user->getProfile(); + $fn = array($profile, 'getLists'); + + list($this->lists, + $this->next_cursor, + $this->prev_cursor) = Profile_list::getAtCursor($fn, array($this->auth_user), $cursor, $count); + } + + function isReadOnly($args) + { + return false; + } + + function lastModified() + { + if (!$this->create && !empty($this->lists) && (count($this->lists) > 0)) { + return strtotime($this->lists[0]->created); + } + + return null; + } + + /** + * An entity tag for this list of lists + * + * Returns an Etag based on the action name, language, user ID and + * timestamps of the first and last list the user has joined + * + * @return string etag + */ + function etag() + { + if (!$this->create && !empty($this->lists) && (count($this->lists) > 0)) { + + $last = count($this->lists) - 1; + + return '"' . implode( + ':', + array($this->arg('action'), + common_language(), + $this->user->id, + strtotime($this->lists[0]->created), + strtotime($this->lists[$last]->created)) + ) + . '"'; + } + + return null; + } +} diff --git a/actions/apilistsubscriber.php b/actions/apilistsubscriber.php new file mode 100644 index 0000000000..cd10568e1b --- /dev/null +++ b/actions/apilistsubscriber.php @@ -0,0 +1,95 @@ +. + * + * @category API + * @package StatusNet + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +class ApiListSubscriberAction extends ApiBareAuthAction +{ + var $list = null; + + function prepare($args) + { + parent::prepare($args); + + $this->user = $this->getTargetUser($this->arg('id')); + $this->list = $this->getTargetList($this->arg('user'), $this->arg('list_id')); + + if (empty($this->list)) { + // TRANS: Client error displayed trying to perform an action related to a non-existing list. + $this->clientError(_('List not found.'), 404, $this->format); + return false; + } + + if (empty($this->user)) { + // TRANS: Client error displayed trying to perform an action related to a non-existing user. + $this->clientError(_('No such user.'), 404, $this->format); + return false; + } + return true; + } + + function handle($args) + { + parent::handle($args); + + $arr = array('profile_tag_id' => $this->list->id, + 'profile_id' => $this->user->id); + $sub = Profile_tag_subscription::pkeyGet($arr); + + if(empty($sub)) { + $this->clientError( + // TRANS: Client error displayed when a membership check for a user is nagative. + _('The specified user is not a subscriber of this list.'), + 400, + $this->format + ); + } + + $user = $this->twitterUserArray($this->user->getProfile(), true); + + switch($this->format) { + case 'xml': + $this->showTwitterXmlUser($user, 'user', true); + break; + case 'json': + $this->showSingleJsonUser($user); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + break; + } + } +} diff --git a/actions/apilistsubscribers.php b/actions/apilistsubscribers.php new file mode 100644 index 0000000000..480f9b4a5f --- /dev/null +++ b/actions/apilistsubscribers.php @@ -0,0 +1,129 @@ +. + * + * @category API + * @package StatusNet + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apilistusers.php'; + +class ApiListSubscribersAction extends ApiListUsersAction +{ + /** + * Subscribe to list + * + * @return boolean success + */ + function handlePost() + { + $result = Profile_tag_subscription::add($this->list, + $this->auth_user); + + if(empty($result)) { + $this->clientError( + // TRANS: Client error displayed when an unknown error occurs in the list subscribers action. + _('An error occured.'), + 500, + $this->format + ); + return false; + } + + switch($this->format) { + case 'xml': + $this->showSingleXmlList($this->list); + break; + case 'json': + $this->showSingleJsonList($this->list); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + return false; + break; + } + } + + function handleDelete() + { + $args = array('profile_tag_id' => $this->list->id, + 'profile_id' => $this->auth_user->id); + $ptag = Profile_tag_subscription::pkeyGet($args); + + if(empty($ptag)) { + $this->clientError( + // TRANS: Client error displayed when trying to unsubscribe from a non-subscribed list. + _('You are not subscribed to this list.'), + 400, + $this->format + ); + return false; + } + + Profile_tag_subscription::remove($this->list, $this->auth_user); + + if(empty($result)) { + $this->clientError( + // TRANS: Client error displayed when an unknown error occurs unsubscribing from a list. + _('An error occured.'), + 500, + $this->format + ); + return false; + } + + switch($this->format) { + case 'xml': + $this->showSingleXmlList($this->list); + break; + case 'json': + $this->showSingleJsonList($this->list); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + return false; + break; + } + return true; + } + + function getUsers() + { + $fn = array($this->list, 'getSubscribers'); + list($this->users, $this->next_cursor, $this->prev_cursor) = + Profile_list::getAtCursor($fn, array(), $this->cursor, 20); + } +} diff --git a/actions/apilistsubscriptions.php b/actions/apilistsubscriptions.php new file mode 100644 index 0000000000..480523033b --- /dev/null +++ b/actions/apilistsubscriptions.php @@ -0,0 +1,125 @@ +. + * + * @category API + * @package StatusNet + * @copyright 2009 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apibareauth.php'; + +class ApiListSubscriptionsAction extends ApiBareAuthAction +{ + var $lists = array(); + var $cursor = -1; + var $next_cursor = 0; + var $prev_cursor = 0; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + function prepare($args) + { + parent::prepare($args); + + $this->cursor = (int) $this->arg('cursor', -1); + $this->user = $this->getTargetUser($this->arg('user')); + $this->getLists(); + + return true; + } + + /** + * Handle the request + * + * Show the lists + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + if (empty($this->user)) { + // TRANS: Client error displayed trying to perform an action related to a non-existing user. + $this->clientError(_('No such user.'), 404, $this->format); + return; + } + + switch($this->format) { + case 'xml': + $this->showXmlLists($this->lists, $this->next_cursor, $this->prev_cursor); + break; + case 'json': + $this->showJsonLists($this->lists, $this->next_cursor, $this->prev_cursor); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 400, + $this->format + ); + break; + } + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return true; + } + + function getLists() + { + if(empty($this->user)) { + return; + } + + $profile = $this->user->getProfile(); + $fn = array($profile, 'getTagSubscriptions'); + # 20 lists + list($this->lists, $this->next_cursor, $this->prev_cursor) = + Profile_list::getAtCursor($fn, array(), $this->cursor, 20); + } +} diff --git a/actions/apistatusesdestroy.php b/actions/apistatusesdestroy.php index d73e574b3c..b4a8870faa 100644 --- a/actions/apistatusesdestroy.php +++ b/actions/apistatusesdestroy.php @@ -97,7 +97,7 @@ class ApiStatusesDestroyAction extends ApiAuthAction if (!in_array($this->format, array('xml', 'json'))) { $this->clientError( - // TRANS: Client error displayed trying to execute an unknown API method deleting a status. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404 ); diff --git a/actions/apistatusesretweet.php b/actions/apistatusesretweet.php index ecc4a3f033..4832da1823 100644 --- a/actions/apistatusesretweet.php +++ b/actions/apistatusesretweet.php @@ -78,22 +78,6 @@ class ApiStatusesRetweetAction extends ApiAuthAction $this->user = $this->auth_user; - if ($this->user->id == $this->original->profile_id) { - // TRANS: Client error displayed trying to repeat an own notice through the API. - $this->clientError(_('Cannot repeat your own notice.'), - 400, $this->format); - return false; - } - - $profile = $this->user->getProfile(); - - if ($profile->hasRepeated($id)) { - // TRANS: Client error displayed trying to re-repeat a notice through the API. - $this->clientError(_('Already repeated that notice.'), - 400, $this->format); - return false; - } - return true; } diff --git a/actions/apistatusesretweets.php b/actions/apistatusesretweets.php index cc7caee19d..7220196836 100644 --- a/actions/apistatusesretweets.php +++ b/actions/apistatusesretweets.php @@ -106,7 +106,7 @@ class ApiStatusesRetweetsAction extends ApiAuthAction $this->showJsonTimeline($strm); break; default: - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); break; } diff --git a/actions/apistatusesshow.php b/actions/apistatusesshow.php index de4c4065c1..13cc88c2c7 100644 --- a/actions/apistatusesshow.php +++ b/actions/apistatusesshow.php @@ -101,7 +101,7 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction parent::handle($args); if (!in_array($this->format, array('xml', 'json', 'atom'))) { - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), 404); return; } diff --git a/actions/apistatusesupdate.php b/actions/apistatusesupdate.php index 5773bdc2e8..b0f3527160 100644 --- a/actions/apistatusesupdate.php +++ b/actions/apistatusesupdate.php @@ -239,8 +239,8 @@ class ApiStatusesUpdateAction extends ApiAuthAction $this->clientError( sprintf( - // TRANS: Client error displayed when the parameter "status" is missing. - // TRANS: %d is the maximum number of character for a notice. + // TRANS: Client error displayed exceeding the maximum notice length. + // TRANS: %d is the maximum length for a notice. _m('That\'s too long. Maximum notice size is %d character.', 'That\'s too long. Maximum notice size is %d characters.', Notice::maxContent()), diff --git a/actions/apistatusnetconfig.php b/actions/apistatusnetconfig.php index b34c6cc544..7cd7c5ed6c 100644 --- a/actions/apistatusnetconfig.php +++ b/actions/apistatusnetconfig.php @@ -135,7 +135,7 @@ class ApiStatusnetConfigAction extends ApiAction break; default: $this->clientError( - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apistatusnetversion.php b/actions/apistatusnetversion.php index bc2babc3f2..3a7b150cab 100644 --- a/actions/apistatusnetversion.php +++ b/actions/apistatusnetversion.php @@ -87,7 +87,7 @@ class ApiStatusnetVersionAction extends ApiPrivateAuthAction break; default: $this->clientError( - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. _('API method not found.'), 404, $this->format diff --git a/actions/apisubscriptions.php b/actions/apisubscriptions.php index fc0a2638b6..84731ac00f 100644 --- a/actions/apisubscriptions.php +++ b/actions/apisubscriptions.php @@ -105,7 +105,7 @@ class ApiSubscriptionsAction extends ApiBareAuthAction parent::handle($args); if (!in_array($this->format, array('xml', 'json'))) { - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); return; } diff --git a/actions/apitimelinefavorites.php b/actions/apitimelinefavorites.php index 36fc3089f5..2c962b450a 100644 --- a/actions/apitimelinefavorites.php +++ b/actions/apitimelinefavorites.php @@ -178,7 +178,7 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction $this->raw($doc->asString()); break; default: - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); break; } diff --git a/actions/apitimelinefriends.php b/actions/apitimelinefriends.php index 0e356bb18b..279265a30e 100644 --- a/actions/apitimelinefriends.php +++ b/actions/apitimelinefriends.php @@ -204,6 +204,8 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction $profile = $this->user->getProfile(); $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); $sitename = common_config('site', 'name'); + // TRANS: Title of API timeline for a user and friends. + // TRANS: %s is a username. $title = sprintf(_("%s and friends"), $this->user->nickname); $taguribase = TagURI::base(); $id = "tag:$taguribase:FriendsTimeline:" . $this->user->id; @@ -272,7 +274,7 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction $this->raw($doc->asString()); break; default: - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); break; } @@ -287,15 +289,12 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction { $notices = array(); - 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); - } else { - $notice = $this->user->friendsTimeline(($this->page-1) * $this->count, - $this->count, $this->since_id, - $this->max_id); - } + $stream = new InboxNoticeStream($this->user); + + $notice = $stream->getNotices(($this->page-1) * $this->count, + $this->count, + $this->since_id, + $this->max_id); while ($notice->fetch()) { $notices[] = clone($notice); diff --git a/actions/apitimelinehome.php b/actions/apitimelinehome.php index 023c9698a1..3c18a5b0bf 100644 --- a/actions/apitimelinehome.php +++ b/actions/apitimelinehome.php @@ -177,7 +177,7 @@ class ApiTimelineHomeAction extends ApiBareAuthAction $this->raw($doc->asString()); break; default: - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); break; } @@ -192,19 +192,12 @@ class ApiTimelineHomeAction extends ApiBareAuthAction { $notices = array(); - if (!empty($this->auth_user) && $this->auth_user->id == $this->user->id) { - $notice = $this->user->noticeInbox( - ($this->page-1) * $this->count, - $this->count, $this->since_id, - $this->max_id - ); - } else { - $notice = $this->user->noticesWithFriends( - ($this->page-1) * $this->count, - $this->count, $this->since_id, - $this->max_id - ); - } + $stream = new InboxNoticeStream($this->user); + + $notice = $stream->getNotices(($this->page-1) * $this->count, + $this->count, + $this->since_id, + $this->max_id); while ($notice->fetch()) { $notices[] = clone($notice); diff --git a/actions/apitimelinelist.php b/actions/apitimelinelist.php new file mode 100644 index 0000000000..c2339f9c35 --- /dev/null +++ b/actions/apitimelinelist.php @@ -0,0 +1,258 @@ +. + * + * @category API + * @package StatusNet + * @author Craig Andrews + * @author Evan Prodromou + * @author Jeffery To + * @author Zach Copley + * @copyright 2009 StatusNet, Inc. + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiprivateauth.php'; +require_once INSTALLDIR . '/lib/atomlistnoticefeed.php'; + +/** + * Returns the most recent notices (default 20) posted to the list specified by ID + * + * @category API + * @package StatusNet + * @author Craig Andrews + * @author Evan Prodromou + * @author Jeffery To + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class ApiTimelineListAction extends ApiPrivateAuthAction +{ + + var $list = null; + var $notices = array(); + var $next_cursor = 0; + var $prev_cursor = 0; + var $cursor = -1; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + function prepare($args) + { + parent::prepare($args); + + $this->cursor = (int) $this->arg('cursor', -1); + $this->list = $this->getTargetList($this->arg('user'), $this->arg('id')); + + return true; + } + + /** + * Handle the request + * + * Just show the notices + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + if (empty($this->list)) { + // TRANS: Client error displayed trying to perform an action related to a non-existing list. + $this->clientError(_('List not found.'), 404, $this->format); + return false; + } + + $this->getNotices(); + $this->showTimeline(); + } + + /** + * Show the timeline of notices + * + * @return void + */ + function showTimeline() + { + // We'll pull common formatting out of this for other formats + $atom = new AtomListNoticeFeed($this->list, $this->auth_user); + + $self = $this->getSelfUri(); + + switch($this->format) { + case 'xml': + $this->initDocument('xml'); + $this->elementStart('statuses_list', + array('xmlns:statusnet' => 'http://status.net/schema/api/1/')); + $this->elementStart('statuses', array('type' => 'array')); + + foreach ($this->notices as $n) { + $twitter_status = $this->twitterStatusArray($n); + $this->showTwitterXmlStatus($twitter_status); + } + + $this->elementEnd('statuses'); + $this->element('next_cursor', null, $this->next_cursor); + $this->element('previous_cursor', null, $this->prev_cursor); + $this->elementEnd('statuses_list'); + $this->endDocument('xml'); + break; + case 'rss': + $this->showRssTimeline( + $this->notices, + $atom->title, + $this->list->getUri(), + $atom->subtitle, + null, + $atom->logo, + $self + ); + break; + case 'atom': + header('Content-Type: application/atom+xml; charset=utf-8'); + + try { + $atom->setId($self); + $atom->setSelfLink($self); + $atom->addEntryFromNotices($this->notices); + $this->raw($atom->getString()); + } catch (Atom10FeedException $e) { + // TRANS: Server error displayed whe trying to get a timeline fails. + // TRANS: %s is the error message. + $this->serverError( sprintf(_('Could not generate feed for list - %s'),$e->getMessage())); + return; + } + + break; + case 'json': + $this->initDocument('json'); + + $statuses = array(); + foreach ($this->notices as $n) { + $twitter_status = $this->twitterStatusArray($n); + array_push($statuses, $twitter_status); + } + + $statuses_list = array('statuses' => $statuses, + 'next_cursor' => $this->next_cusror, + 'next_cursor_str' => strval($this->next_cusror), + 'previous_cursor' => $this->prev_cusror, + 'previous_cursor_str' => strval($this->prev_cusror) + ); + $this->showJsonObjects($statuses_list); + + $this->initDocument('json'); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + break; + } + } + + /** + * Get notices + * + * @return array notices + */ + function getNotices() + { + $fn = array($this->list, 'getNotices'); + list($this->notices, $this->next_cursor, $this->prev_cursor) = + Profile_list::getAtCursor($fn, array(), $this->cursor, 20); + if (!$this->notices) { + $this->notices = array(); + } + } + + /** + * Is this action read only? + * + * @param array $args other arguments + * + * @return boolean true + */ + function isReadOnly($args) + { + return true; + } + + /** + * When was this feed last modified? + * + * @return string datestamp of the latest notice in the stream + */ + function lastModified() + { + if (!empty($this->notices) && (count($this->notices) > 0)) { + return strtotime($this->notices[0]->created); + } + + return null; + } + + /** + * An entity tag for this stream + * + * Returns an Etag based on the action name, language, list ID and + * timestamps of the first and last notice in the timeline + * + * @return string etag + */ + function etag() + { + if (!empty($this->notices) && (count($this->notices) > 0)) { + + $last = count($this->notices) - 1; + + return '"' . implode( + ':', + array($this->arg('action'), + common_language(), + $this->list->id, + strtotime($this->notices[0]->created), + strtotime($this->notices[$last]->created)) + ) + . '"'; + } + + return null; + } +} diff --git a/actions/apitimelinementions.php b/actions/apitimelinementions.php index 2857bd41ea..ecd2a1a0e6 100644 --- a/actions/apitimelinementions.php +++ b/actions/apitimelinementions.php @@ -178,7 +178,7 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction $this->raw($doc->asString()); break; default: - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); break; } diff --git a/actions/apitimelinepublic.php b/actions/apitimelinepublic.php index 353973b653..a786aa15cc 100644 --- a/actions/apitimelinepublic.php +++ b/actions/apitimelinepublic.php @@ -243,7 +243,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction $this->raw($doc->asString()); break; default: - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); break; } diff --git a/actions/apitimelineretweetedtome.php b/actions/apitimelineretweetedtome.php index 628dc40247..ef943f4f88 100644 --- a/actions/apitimelineretweetedtome.php +++ b/actions/apitimelineretweetedtome.php @@ -146,7 +146,7 @@ class ApiTimelineRetweetedToMeAction extends ApiAuthAction $this->raw($doc->asString()); break; default: - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); break; } diff --git a/actions/apitimelineretweetsofme.php b/actions/apitimelineretweetsofme.php index aec6877f15..d38e730ac6 100644 --- a/actions/apitimelineretweetsofme.php +++ b/actions/apitimelineretweetsofme.php @@ -101,6 +101,8 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction $profile = $this->auth_user->getProfile(); $subtitle = sprintf( + // TRANS: Subtitle of API time with retweets of me. + // TRANS: %1$s is the StatusNet sitename, %2$s is the user nickname, %3$s is the user profile name. _('%1$s notices that %2$s / %3$s has repeated.'), $sitename, $this->auth_user->nickname, $profile->getBestName() ); @@ -143,7 +145,7 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction $this->raw($doc->asString()); break; default: - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), 404); break; } diff --git a/actions/apitimelinetag.php b/actions/apitimelinetag.php index 5fa76d0cd0..6c3b135ed9 100644 --- a/actions/apitimelinetag.php +++ b/actions/apitimelinetag.php @@ -161,7 +161,7 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction $this->raw($doc->asString()); break; default: - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); break; } diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 3fe73c691c..b0681c191a 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -213,7 +213,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction $this->raw($doc->asString()); break; default: - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); break; } diff --git a/actions/apiuserprofileimage.php b/actions/apiuserprofileimage.php index d2cf9a3e57..2d26e5c4ea 100644 --- a/actions/apiuserprofileimage.php +++ b/actions/apiuserprofileimage.php @@ -84,7 +84,7 @@ class ApiUserProfileImageAction extends ApiPrivateAuthAction $profile = $this->user->getProfile(); if (empty($profile)) { - // TRANS: Client error displayed when requesting user information for a user without a profile. + // TRANS: Error message displayed when referring to a user without a profile. $this->clientError(_('User has no profile.')); return; } diff --git a/actions/apiusershow.php b/actions/apiusershow.php index fbd4d60598..11638954db 100644 --- a/actions/apiusershow.php +++ b/actions/apiusershow.php @@ -96,7 +96,7 @@ class ApiUserShowAction extends ApiPrivateAuthAction } if (!in_array($this->format, array('xml', 'json'))) { - // TRANS: Client error displayed when trying to handle an unknown API method. + // TRANS: Client error displayed when coming across a non-supported API method. $this->clientError(_('API method not found.'), $code = 404); return; } @@ -104,7 +104,7 @@ class ApiUserShowAction extends ApiPrivateAuthAction $profile = $this->user->getProfile(); if (empty($profile)) { - // TRANS: Client error displayed when requesting user information for a user without a profile. + // TRANS: Error message displayed when referring to a user without a profile. $this->clientError(_('User has no profile.')); return; } diff --git a/actions/approvegroup.php b/actions/approvegroup.php index 5039cfae6b..d46a4451c4 100644 --- a/actions/approvegroup.php +++ b/actions/approvegroup.php @@ -121,6 +121,7 @@ class ApprovegroupAction extends Action if (empty($this->request)) { // TRANS: Client error displayed trying to approve group membership for a non-existing request. + // TRANS: %s is a nickname. $this->clientError(sprintf(_('%s is not in the moderation queue for this group.'), $this->profile->nickname), 403); } @@ -152,12 +153,12 @@ class ApprovegroupAction extends Action try { if ($this->approve) { - $this->profile->completeJoinGroup($this->group); + $this->request->complete(); } elseif ($this->cancel) { - $this->profile->cancelJoinGroup($this->group); + $this->request->abort(); } } catch (Exception $e) { - common_log(LOG_ERROR, "Exception canceling group sub: " . $e->getMessage()); + common_log(LOG_ERR, "Exception canceling group sub: " . $e->getMessage()); // TRANS: Server error displayed when cancelling a queued group join request fails. // TRANS: %1$s is the leaving user's nickname, $2$s is the group nickname for which the leave failed. $this->serverError(sprintf(_('Could not cancel request for user %1$s to join group %2$s.'), @@ -176,10 +177,10 @@ class ApprovegroupAction extends Action $this->elementEnd('head'); $this->elementStart('body'); if ($this->approve) { - // TRANS: Message on page for group admin after approving a join request. + // TRANS: Message on page for group admin after approving a join request. $this->element('p', 'success', _('Join request approved.')); } elseif ($this->cancel) { - // TRANS: Message on page for group admin after rejecting a join request. + // TRANS: Message on page for group admin after rejecting a join request. $this->element('p', 'success', _('Join request canceled.')); } $this->elementEnd('body'); diff --git a/actions/approvesub.php b/actions/approvesub.php new file mode 100644 index 0000000000..be07b6e877 --- /dev/null +++ b/actions/approvesub.php @@ -0,0 +1,145 @@ +. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Leave a group + * + * This is the action for leaving a group. It works more or less like the subscribe action + * for users. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class ApprovesubAction extends Action +{ + var $profile = null; + + /** + * Prepare to run + */ + function prepare($args) + { + parent::prepare($args); + + $cur = common_current_user(); + if (empty($cur)) { + // TRANS: Client error displayed trying to approve group membership while not logged in. + $this->clientError(_('Must be logged in.'), 403); + return false; + } + if ($this->arg('profile_id')) { + $this->profile = Profile::staticGet('id', $this->arg('profile_id')); + } else { + // TRANS: Client error displayed trying to approve subscriptionswithout specifying a profile to approve. + $this->clientError(_('Must specify a profile.')); + return false; + } + + $this->request = Subscription_queue::pkeyGet(array('subscriber' => $this->profile->id, + 'subscribed' => $cur->id)); + + if (empty($this->request)) { + // TRANS: Client error displayed trying to approve subscription for a non-existing request. + $this->clientError(sprintf(_('%s is not in the moderation queue for your subscriptions.'), $this->profile->nickname), 403); + } + + $this->approve = (bool)$this->arg('approve'); + $this->cancel = (bool)$this->arg('cancel'); + if (!$this->approve && !$this->cancel) { + // TRANS: Client error displayed trying to approve/deny subscription. + $this->clientError(_('Internal error: received neither cancel nor abort.')); + } + if ($this->approve && $this->cancel) { + // TRANS: Client error displayed trying to approve/deny subscription + $this->clientError(_('Internal error: received both cancel and abort.')); + } + return true; + } + + /** + * Handle the request + * + * On POST, add the current user to the group + * + * @param array $args unused + * + * @return void + */ + function handle($args) + { + parent::handle($args); + $cur = common_current_user(); + + try { + if ($this->approve) { + $this->request->complete(); + } elseif ($this->cancel) { + $this->request->abort(); + } + } catch (Exception $e) { + common_log(LOG_ERR, "Exception canceling sub: " . $e->getMessage()); + // TRANS: Server error displayed when cancelling a queued subscription request fails. + // TRANS: %1$s is the leaving user's nickname, $2$s is the nickname for which the leave failed. + $this->serverError(sprintf(_('Could not cancel or approve request for user %1$s to join group %2$s.'), + $this->profile->nickname, $cur->nickname)); + return; + } + + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + // TRANS: Title for subscription approval ajax return + // TRANS: %1$s is the approved user's nickname + $this->element('title', null, sprintf(_m('TITLE','%1$s\'s request'), + $this->profile->nickname)); + $this->elementEnd('head'); + $this->elementStart('body'); + if ($this->approve) { + // TRANS: Message on page for user after approving a subscription request. + $this->element('p', 'success', _('Subscription approved.')); + } elseif ($this->cancel) { + // TRANS: Message on page for user after rejecting a subscription request. + $this->element('p', 'success', _('Subscription canceled.')); + } + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect(common_local_url('subqueue', array('nickname' => + $cur->nickname)), + 303); + } + } +} diff --git a/actions/attachment.php b/actions/attachment.php index 45aa78728a..22018ab043 100644 --- a/actions/attachment.php +++ b/actions/attachment.php @@ -143,15 +143,6 @@ class AttachmentAction extends Action } } - /** - * Don't show local navigation - * - * @return void - */ - function showLocalNavBlock() - { - } - /** * Fill the content area of the page * diff --git a/actions/avatarbynickname.php b/actions/avatarbynickname.php index fa97a86ebf..a581d5ae35 100644 --- a/actions/avatarbynickname.php +++ b/actions/avatarbynickname.php @@ -81,7 +81,7 @@ class AvatarbynicknameAction extends Action } $profile = $user->getProfile(); if (!$profile) { - // TRANS: Client error displayed trying to get an avatar for a user without a profile. + // TRANS: Error message displayed when referring to a user without a profile. $this->clientError(_('User has no profile.')); return; } diff --git a/actions/avatarsettings.php b/actions/avatarsettings.php index d9542a39c8..9ac7115e93 100644 --- a/actions/avatarsettings.php +++ b/actions/avatarsettings.php @@ -104,8 +104,8 @@ class AvatarsettingsAction extends SettingsAction if (!$profile) { common_log_db_error($user, 'SELECT', __FILE__); - // TRANS: Server error displayed in avatar upload page when no matching profile can be found for a user. - $this->serverError(_('User without matching profile.')); + // TRANS: Error message displayed when referring to a user without a profile. + $this->serverError(_('User has no profile.')); return; } @@ -146,13 +146,15 @@ class AvatarsettingsAction extends SettingsAction // TRANS: Header on avatar upload page for thumbnail of to be used rendition of uploaded avatar (h2). $this->element('h2', null, _("Preview")); $this->elementStart('div', array('id'=>'avatar_preview_view')); - $this->element('img', array('src' => $original->url, + $this->element('img', array('src' => $avatar->url, 'width' => AVATAR_PROFILE_SIZE, 'height' => AVATAR_PROFILE_SIZE, 'alt' => $user->nickname)); $this->elementEnd('div'); - // TRANS: Button on avatar upload page to delete current avatar. - $this->submit('delete', _m('BUTTON','Delete')); + if (!empty($avatar->filename)) { + // TRANS: Button on avatar upload page to delete current avatar. + $this->submit('delete', _m('BUTTON','Delete')); + } $this->elementEnd('li'); } @@ -188,8 +190,8 @@ class AvatarsettingsAction extends SettingsAction if (!$profile) { common_log_db_error($user, 'SELECT', __FILE__); - // TRANS: Server error displayed in avatar upload page when no matching profile can be found for a user. - $this->serverError(_('User without matching profile.')); + // TRANS: Error message displayed when referring to a user without a profile. + $this->serverError(_('User has no profile.')); return; } @@ -277,6 +279,7 @@ class AvatarsettingsAction extends SettingsAction $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->showForm(_('There was a problem with your session token. '. 'Try again, please.')); return; diff --git a/actions/cancelgroup.php b/actions/cancelgroup.php index 57df1a10a7..3074e3ffa3 100644 --- a/actions/cancelgroup.php +++ b/actions/cancelgroup.php @@ -139,9 +139,9 @@ class CancelgroupAction extends Action parent::handle($args); try { - $this->profile->cancelJoinGroup($this->group); + $this->request->abort(); } catch (Exception $e) { - common_log(LOG_ERROR, "Exception canceling group sub: " . $e->getMessage()); + common_log(LOG_ERR, "Exception canceling group sub: " . $e->getMessage()); // TRANS: Server error displayed when cancelling a queued group join request fails. // TRANS: %1$s is the leaving user's nickname, $2$s is the group nickname for which the leave failed. $this->serverError(sprintf(_('Could not cancel request for user %1$s to join group %2$s.'), diff --git a/actions/cancelsubscription.php b/actions/cancelsubscription.php new file mode 100644 index 0000000000..226fd0822e --- /dev/null +++ b/actions/cancelsubscription.php @@ -0,0 +1,124 @@ +. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Leave a group + * + * This is the action for leaving a group. It works more or less like the subscribe action + * for users. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class CancelsubscriptionAction extends Action +{ + + function handle($args) + { + parent::handle($args); + if ($this->boolean('ajax')) { + StatusNet::setApi(true); + } + if (!common_logged_in()) { + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. + $this->clientError(_('Not logged in.')); + return; + } + + $user = common_current_user(); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + common_redirect(common_local_url('subscriptions', + array('nickname' => $user->nickname))); + return; + } + + /* Use a session token for CSRF protection. */ + + $token = $this->trimmed('token'); + + if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. + $this->clientError(_('There was a problem with your session token. ' . + 'Try again, please.')); + return; + } + + $other_id = $this->arg('unsubscribeto'); + + if (!$other_id) { + // TRANS: Client error displayed when trying to leave a group without specifying an ID. + $this->clientError(_('No profile ID in request.')); + return; + } + + $other = Profile::staticGet('id', $other_id); + + if (!$other) { + // TRANS: Client error displayed when trying to leave a non-existing group. + $this->clientError(_('No profile with that ID.')); + return; + } + + $this->request = Subscription_queue::pkeyGet(array('subscriber' => $user->id, + 'subscribed' => $other->id)); + + if (empty($this->request)) { + // TRANS: Client error displayed when trying to approve a non-existing group join request. + // TRANS: %s is a user nickname. + $this->clientError(sprintf(_('%s is not in the moderation queue for this group.'), $this->profile->nickname), 403); + } + + $this->request->abort(); + + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + // TRANS: Title after unsubscribing from a group. + $this->element('title', null, _m('TITLE','Unsubscribed')); + $this->elementEnd('head'); + $this->elementStart('body'); + $subscribe = new SubscribeForm($this, $other); + $subscribe->show(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect(common_local_url('subscriptions', + array('nickname' => $user->nickname)), + 303); + } + } +} diff --git a/actions/conversation.php b/actions/conversation.php index 8d11df37bc..f33d267d35 100644 --- a/actions/conversation.php +++ b/actions/conversation.php @@ -47,8 +47,12 @@ require_once INSTALLDIR.'/lib/noticelist.php'; */ class ConversationAction extends Action { - var $id = null; - var $page = null; + var $id = null; + var $page = null; + var $notices = null; + var $userProfile = null; + + const MAX_NOTICES = 500; /** * Initialization. @@ -69,6 +73,19 @@ class ConversationAction extends Action if (empty($this->page)) { $this->page = 1; } + + $cur = common_current_user(); + + if (empty($cur)) { + $this->userProfile = null; + } else { + $this->userProfile = $cur->getProfile(); + } + + $stream = new ConversationNoticeStream($this->id, $this->userProfile); + + $this->notices = $stream->getNotices(0, self::MAX_NOTICES); + return true; } @@ -106,11 +123,9 @@ class ConversationAction extends Action */ function showContent() { - $notices = Notice::conversationStream($this->id, null, null); + $tnl = new FullThreadedNoticeList($this->notices, $this, $this->userProfile); - $ct = new ConversationTree($notices, $this); - - $cnt = $ct->show(); + $cnt = $tnl->show(); } function isReadOnly() @@ -118,173 +133,3 @@ class ConversationAction extends Action return true; } } - -/** - * Conversation tree - * - * The widget class for displaying a hierarchical list of notices. - * - * @category Widget - * @package StatusNet - * @author Evan Prodromou - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ - */ -class ConversationTree extends NoticeList -{ - var $tree = null; - var $table = null; - - /** - * Show the tree of notices - * - * @return void - */ - function show() - { - $cnt = $this->_buildTree(); - - $this->out->elementStart('div', array('id' =>'notices_primary')); - // TRANS: Header on conversation page. Hidden by default (h2). - $this->out->element('h2', null, _('Notices')); - $this->out->elementStart('ol', array('class' => 'notices xoxo')); - - if (array_key_exists('root', $this->tree)) { - $rootid = $this->tree['root'][0]; - $this->showNoticePlus($rootid); - } - - $this->out->elementEnd('ol'); - $this->out->elementEnd('div'); - - return $cnt; - } - - function _buildTree() - { - $cnt = 0; - - $this->tree = array(); - $this->table = array(); - - while ($this->notice->fetch()) { - - $cnt++; - - $id = $this->notice->id; - $notice = clone($this->notice); - - $this->table[$id] = $notice; - - if (is_null($notice->reply_to)) { - $this->tree['root'] = array($notice->id); - } else if (array_key_exists($notice->reply_to, $this->tree)) { - $this->tree[$notice->reply_to][] = $notice->id; - } else { - $this->tree[$notice->reply_to] = array($notice->id); - } - } - - return $cnt; - } - - /** - * Shows a notice plus its list of children. - * - * @param integer $id ID of the notice to show - * - * @return void - */ - function showNoticePlus($id) - { - $notice = $this->table[$id]; - - // We take responsibility for doing the li - - $this->out->elementStart('li', array('class' => 'hentry notice', - 'id' => 'notice-' . $id)); - - $item = $this->newListItem($notice); - $item->show(); - - if (array_key_exists($id, $this->tree)) { - $children = $this->tree[$id]; - - $this->out->elementStart('ol', array('class' => 'notices')); - - sort($children); - - foreach ($children as $child) { - $this->showNoticePlus($child); - } - - $this->out->elementEnd('ol'); - } - - $this->out->elementEnd('li'); - } - - /** - * Override parent class to return our preferred item. - * - * @param Notice $notice Notice to display - * - * @return NoticeListItem a list item to show - */ - function newListItem($notice) - { - return new ConversationTreeItem($notice, $this->out); - } -} - -/** - * Conversation tree list item - * - * Special class of NoticeListItem for use inside conversation trees. - * - * @category Widget - * @package StatusNet - * @author Evan Prodromou - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ - */ -class ConversationTreeItem extends NoticeListItem -{ - /** - * start a single notice. - * - * The default creates the
  • ; we skip, since the ConversationTree - * takes care of that. - * - * @return void - */ - function showStart() - { - return; - } - - /** - * finish the notice - * - * The default closes the
  • ; we skip, since the ConversationTree - * takes care of that. - * - * @return void - */ - function showEnd() - { - return; - } - - /** - * show link to notice conversation page - * - * Since we're only used on the conversation page, we skip this - * - * @return void - */ - function showContext() - { - return; - } -} diff --git a/actions/conversationreplies.php b/actions/conversationreplies.php index 21780fdf1d..55c3efa9e0 100644 --- a/actions/conversationreplies.php +++ b/actions/conversationreplies.php @@ -66,9 +66,7 @@ class ConversationRepliesAction extends ConversationAction */ function showContent() { - $notices = Notice::conversationStream($this->id, null, null); - - $ct = new FullThreadedNoticeList($notices, $this); + $ct = new FullThreadedNoticeList($this->notices, $this, $this->userProfile); $cnt = $ct->show(); } @@ -88,19 +86,3 @@ class ConversationRepliesAction extends ConversationAction $this->elementEnd('html'); } } - -class FullThreadedNoticeList extends ThreadedNoticeList -{ - function newListItem($notice) - { - return new FullThreadedNoticeListItem($notice, $this->out); - } -} - -class FullThreadedNoticeListItem extends ThreadedNoticeListItem -{ - function initialItems() - { - return 1000; // @fixme - } -} diff --git a/actions/deleteapplication.php b/actions/deleteapplication.php index 9f9ac18971..8c3b8e0ba7 100644 --- a/actions/deleteapplication.php +++ b/actions/deleteapplication.php @@ -99,6 +99,7 @@ class DeleteapplicationAction extends Action // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token.')); return; } diff --git a/actions/deletenotice.php b/actions/deletenotice.php index c997bb756a..e3690c51d4 100644 --- a/actions/deletenotice.php +++ b/actions/deletenotice.php @@ -48,7 +48,7 @@ class DeletenoticeAction extends Action $this->user = common_current_user(); if (!$this->user) { - // TRANS: Error message displayed trying to delete a notice while not logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. common_user_error(_('Not logged in.')); exit; } @@ -174,6 +174,7 @@ class DeletenoticeAction extends Action $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->showForm(_('There was a problem with your session token. ' . 'Try again, please.')); return; diff --git a/actions/disfavor.php b/actions/disfavor.php index 39598d60bf..e9fc17c5b7 100644 --- a/actions/disfavor.php +++ b/actions/disfavor.php @@ -57,7 +57,7 @@ class DisfavorAction extends Action { parent::handle($args); if (!common_logged_in()) { - // TRANS: Client error displayed when trying to remove a favorite while not logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.')); return; } diff --git a/actions/doc.php b/actions/doc.php index 20cf9e2810..95f76fb9dd 100644 --- a/actions/doc.php +++ b/actions/doc.php @@ -147,80 +147,19 @@ class DocAction extends Action { if (Event::handle('StartLoadDoc', array(&$this->title, &$this->output))) { - $this->filename = $this->getFilename(); + $paths = DocFile::defaultPaths(); - if (empty($this->filename)) { + $docfile = DocFile::forTitle($this->title, $paths); + + if (empty($docfile)) { // TRANS: Client exception thrown when requesting a document from the documentation that does not exist. // TRANS: %s is the non-existing document. throw new ClientException(sprintf(_('No such document "%s".'), $this->title), 404); } - $c = file_get_contents($this->filename); - - $this->output = common_markup_to_html($c); + $this->output = $docfile->toHTML(); Event::handle('EndLoadDoc', array($this->title, &$this->output)); } } - - function getFilename() - { - $localDef = null; - $local = null; - - $site = StatusNet::currentSite(); - - if (!empty($site) && file_exists(INSTALLDIR.'/local/doc-src/'.$site.'/'.$this->title)) { - $localDef = INSTALLDIR.'/local/doc-src/'.$site.'/'.$this->title; - - $local = glob(INSTALLDIR.'/local/doc-src/'.$site.'/'.$this->title.'.*'); - if ($local === false) { - // Some systems return false, others array(), if dir didn't exist. - $local = array(); - } - } else { - if (file_exists(INSTALLDIR.'/local/doc-src/'.$this->title)) { - $localDef = INSTALLDIR.'/local/doc-src/'.$this->title; - } - - $local = glob(INSTALLDIR.'/local/doc-src/'.$this->title.'.*'); - if ($local === false) { - $local = array(); - } - } - - if (count($local) || isset($localDef)) { - return $this->negotiateLanguage($local, $localDef); - } - - if (file_exists(INSTALLDIR.'/doc-src/'.$this->title)) { - $distDef = INSTALLDIR.'/doc-src/'.$this->title; - } - - $dist = glob(INSTALLDIR.'/doc-src/'.$this->title.'.*'); - if ($dist === false) { - $dist = array(); - } - - if (count($dist) || isset($distDef)) { - return $this->negotiateLanguage($dist, $distDef); - } - - return null; - } - - function negotiateLanguage($filenames, $defaultFilename=null) - { - // XXX: do this better - - $langcode = common_language(); - - foreach ($filenames as $filename) { - if (preg_match('/\.'.$langcode.'$/', $filename)) { - return $filename; - } - } - - return $defaultFilename; - } } diff --git a/actions/editapplication.php b/actions/editapplication.php index 4e67d9e57b..02fae3eb49 100644 --- a/actions/editapplication.php +++ b/actions/editapplication.php @@ -128,6 +128,7 @@ class EditApplicationAction extends OwnerDesignAction // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token.')); return; } diff --git a/actions/editgroup.php b/actions/editgroup.php index f5bada04fc..e46b1481d0 100644 --- a/actions/editgroup.php +++ b/actions/editgroup.php @@ -185,7 +185,15 @@ class EditgroupAction extends GroupDesignAction $description = $this->trimmed('description'); $location = $this->trimmed('location'); $aliasstring = $this->trimmed('aliases'); - $join_policy = intval($this->arg('join_policy')); + $private = $this->boolean('private'); + + if ($private) { + $force_scope = 1; + $join_policy = User_group::JOIN_POLICY_MODERATE; + } else { + $force_scope = 0; + $join_policy = User_group::JOIN_POLICY_OPEN; + } if ($this->nicknameExists($nickname)) { // TRANS: Group edit form validation error. @@ -267,6 +275,7 @@ class EditgroupAction extends GroupDesignAction $this->group->location = $location; $this->group->mainpage = common_local_url('showgroup', array('nickname' => $nickname)); $this->group->join_policy = $join_policy; + $this->group->force_scope = $force_scope; $result = $this->group->update($orig); diff --git a/actions/editpeopletag.php b/actions/editpeopletag.php new file mode 100644 index 0000000000..115049f7ab --- /dev/null +++ b/actions/editpeopletag.php @@ -0,0 +1,333 @@ +. + * + * @category Group + * @package StatusNet + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Add a new group + * + * This is the form for adding a new group + * + * @category Group + * @package StatusNet + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class EditpeopletagAction extends OwnerDesignAction +{ + var $msg, $confirm, $confirm_args=array(); + + function title() + { + if ($_SERVER['REQUEST_METHOD'] == 'POST' && $this->boolean('delete')) { + // TRANS: Title for edit list page after deleting a tag. + // TRANS: %s is a list. + return sprintf(_('Delete %s list'), $this->peopletag->tag); + } + // TRANS: Title for edit list page. + // TRANS: %s is a list. + return sprintf(_('Edit list %s'), $this->peopletag->tag); + } + + /** + * Prepare to run + */ + + function prepare($args) + { + parent::prepare($args); + + if (!common_logged_in()) { + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. + $this->clientError(_('Not logged in.')); + return false; + } + + $id = $this->arg('id'); + $tagger_arg = $this->arg('tagger'); + $tag_arg = $this->arg('tag'); + + $tagger = common_canonical_nickname($tagger_arg); + $tag = common_canonical_tag($tag_arg); + + $current = common_current_user(); + + // Permanent redirect on non-canonical tag + + if ($tagger_arg != $tagger || $tag_arg != $tag) { + $args = array('tagger' => $tagger, 'tag' => $tag); + common_redirect(common_local_url('editpeopletag', $args), 301); + return false; + } + + $user = null; + if ($id) { + $this->peopletag = Profile_list::staticGet('id', $id); + if (!empty($this->peopletag)) { + $user = User::staticGet('id', $this->peopletag->tagger); + } + } else { + if (!$tagger) { + // TRANS: Error message displayed when trying to perform an action that requires a tagging user or ID. + $this->clientError(_('No tagger or ID.'), 404); + return false; + } + + $user = User::staticGet('nickname', $tagger); + $this->peopletag = Profile_list::pkeyGet(array('tagger' => $user->id, 'tag' => $tag)); + } + + if (!$this->peopletag) { + // TRANS: Client error displayed when referring to a non-existing list. + $this->clientError(_('No such list.'), 404); + return false; + } + + if (!$user) { + // This should not be happening + // TRANS: Client error displayed when referring to non-local user. + $this->clientError(_('Not a local user.'), 404); + return false; + } + + if ($current->id != $user->id) { + // TRANS: Client error displayed when reting to edit a tag that was not self-created. + $this->clientError(_('You must be the creator of the tag to edit it.'), 404); + return false; + } + + $this->tagger = $user->getProfile(); + + return true; + } + + /** + * Handle the request + * + * On GET, show the form. On POST, try to save the group. + * + * @param array $args unused + * + * @return void + */ + function handle($args) + { + parent::handle($args); + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->trySave(); + } else { + $this->showForm(); + } + } + + function showConfirm($msg=null, $fwd=null) + { + $this->confirm = $msg; + $this->confirm_args = $fwd; + $this->showPage(); + } + + function showConfirmForm() + { + $this->elementStart('form', array('id' => 'form_peopletag_edit_confirm', + 'class' => 'form_settings', + 'method' => 'post', + 'action' => common_local_url('editpeopletag', + array('tagger' => $this->tagger->nickname, + 'tag' => $this->peopletag->tag)))); + $this->elementStart('fieldset'); + $this->hidden('token', common_session_token()); + $this->hidden('id', $this->arg('id')); + + foreach ($this->confirm_args as $key => $val) { + $this->hidden($key, $val); + } + + $this->submit('form_action-no', + _m('BUTTON','No'), + 'submit form_action-primary', + 'cancel'); + $this->submit('form_action-yes', + _m('BUTTON','Yes'), + 'submit form_action-secondary', + 'confirm'); + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + } + + function showForm($msg=null) + { + $this->msg = $msg; + $this->showPage(); + } + + function showObjectNav() + { + $nav = new PeopletagGroupNav($this, $this->peopletag); + $nav->show(); + } + + function showContent() + { + if ($this->confirm) { + $this->showConfirmForm(); + return; + } + + $form = new PeopletagEditForm($this, $this->peopletag); + $form->show(); + + $form->showProfileList(); + } + + function showPageNotice() + { + if ($this->msg) { + $this->element('p', 'error', $this->msg); + } else if ($this->confirm) { + $this->element('p', 'instructions', $this->confirm); + } else { + $this->element('p', 'instructions', + // TRANS: Form instruction for edit list form. + _('Use this form to edit the list.')); + } + } + + function showScripts() + { + parent::showScripts(); + $this->autofocus('tag'); + } + + function trySave() + { + $tag = common_canonical_tag($this->trimmed('tag')); + $description = $this->trimmed('description'); + $private = $this->boolean('private'); + $delete = $this->arg('delete'); + $confirm = $this->arg('confirm'); + $cancel = $this->arg('cancel'); + + if ($delete && $cancel) { + // TRANS: Form validation error displayed if the form data for deleting a tag was incorrect. + $this->showForm(_('Delete aborted.')); + return; + } + + $set_private = $private && $this->peopletag->private != $private; + + if ($delete && !$confirm) { + // TRANS: Text in confirmation dialog for deleting a tag. + $this->showConfirm(_('Deleting this tag will permanantly remove ' . + 'all its subscription and membership records. ' . + 'Do you still want to continue?'), array('delete' => 1)); + return; + } else if (common_valid_tag($tag)) { + // TRANS: Form validation error displayed if a given tag is invalid. + $this->showForm(_('Invalid tag.')); + return; + } else if ($tag != $this->peopletag->tag && $this->tagExists($tag)) { + // TRANS: Form validation error displayed if a given tag is already present. + // TRANS: %s is the already present tag. + $this->showForm(sprintf(_('You already have a tag named %s.'), $tag)); + return; + } else if (Profile_list::descriptionTooLong($description)) { + $this->showForm(sprintf( + // TRANS: Client error shown when providing too long a description when editing a list. + // TRANS: %d is the maximum number of allowed characters. + _m('Description is too long (maximum %d character).', + 'Description is too long (maximum %d characters).', + Profile_list::maxDescription()), + Profile_list::maxDescription())); + return; + } else if ($set_private && !$confirm && !$cancel) { + $fwd = array('tag' => $tag, + 'description' => $description, + 'private' => (int) $private); + + // TRANS: Text in confirmation dialog for setting a tag from public to private. + $this->showConfirm(_('Setting a public tag as private will ' . + 'permanently remove all the existing ' . + 'subscriptions to it. Do you still want to continue?'), $fwd); + return; + } + + $this->peopletag->query('BEGIN'); + + $orig = clone($this->peopletag); + + $this->peopletag->tag = $tag; + $this->peopletag->description = $description; + if (!$set_private || $confirm) { + $this->peopletag->private = $private; + } + + $result = $this->peopletag->update($orig); + + if (!$result) { + common_log_db_error($this->group, 'UPDATE', __FILE__); + // TRANS: TRANS: Server error displayed when updating a list fails. + $this->serverError(_('Could not update list.')); + } + + $this->peopletag->query('COMMIT'); + + if ($set_private && $confirm) { + Profile_tag_subscription::cleanup($this->peopletag); + } + + if ($delete) { + // This might take quite a bit of time. + $this->peopletag->delete(); + // send home. + common_redirect(common_local_url('all', + array('nickname' => $this->tagger->nickname)), + 303); + } + + if ($tag != $orig->tag) { + common_redirect(common_local_url('editpeopletag', + array('tagger' => $this->tagger->nickname, + 'tag' => $tag)), + 303); + } else { + // TRANS: Edit list form success message. + $this->showForm(_('Options saved.')); + } + } + + function tagExists($tag) + { + $args = array('tagger' => $this->tagger->id, 'tag' => $tag); + $ptag = Profile_list::pkeyGet($args); + + return !empty($ptag); + } +} diff --git a/actions/emailsettings.php b/actions/emailsettings.php index 6780928157..9493be80aa 100644 --- a/actions/emailsettings.php +++ b/actions/emailsettings.php @@ -207,45 +207,45 @@ class EmailsettingsAction extends SettingsAction $this->elementStart('ul', 'form_data'); if (Event::handle('StartEmailFormData', array($this))) { - $this->elementStart('li'); - $this->checkbox('emailnotifysub', - // TRANS: Checkbox label in e-mail preferences form. - _('Send me notices of new subscriptions through email.'), - $user->emailnotifysub); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('emailnotifyfav', - // TRANS: Checkbox label in e-mail preferences form. - _('Send me email when someone '. - 'adds my notice as a favorite.'), - $user->emailnotifyfav); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('emailnotifymsg', - // TRANS: Checkbox label in e-mail preferences form. - _('Send me email when someone sends me a private message.'), - $user->emailnotifymsg); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('emailnotifyattn', - // TRANS: Checkbox label in e-mail preferences form. - _('Send me email when someone sends me an "@-reply".'), - $user->emailnotifyattn); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('emailnotifynudge', - // TRANS: Checkbox label in e-mail preferences form. - _('Allow friends to nudge me and send me an email.'), - $user->emailnotifynudge); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('emailmicroid', - // TRANS: Checkbox label in e-mail preferences form. - _('Publish a MicroID for my email address.'), - $user->emailmicroid); - $this->elementEnd('li'); - Event::handle('EndEmailFormData', array($this)); - } + $this->elementStart('li'); + $this->checkbox('emailnotifysub', + // TRANS: Checkbox label in e-mail preferences form. + _('Send me notices of new subscriptions through email.'), + $user->emailnotifysub); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->checkbox('emailnotifyfav', + // TRANS: Checkbox label in e-mail preferences form. + _('Send me email when someone '. + 'adds my notice as a favorite.'), + $user->emailnotifyfav); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->checkbox('emailnotifymsg', + // TRANS: Checkbox label in e-mail preferences form. + _('Send me email when someone sends me a private message.'), + $user->emailnotifymsg); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->checkbox('emailnotifyattn', + // TRANS: Checkbox label in e-mail preferences form. + _('Send me email when someone sends me an "@-reply".'), + $user->emailnotifyattn); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->checkbox('emailnotifynudge', + // TRANS: Checkbox label in e-mail preferences form. + _('Allow friends to nudge me and send me an email.'), + $user->emailnotifynudge); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->checkbox('emailmicroid', + // TRANS: Checkbox label in e-mail preferences form. + _('Publish a MicroID for my email address.'), + $user->emailmicroid); + $this->elementEnd('li'); + Event::handle('EndEmailFormData', array($this)); + } $this->elementEnd('ul'); // TRANS: Button label to save e-mail preferences. $this->submit('save', _m('BUTTON','Save')); @@ -289,6 +289,7 @@ class EmailsettingsAction extends SettingsAction // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->show_form(_('There was a problem with your session token. '. 'Try again, please.')); return; @@ -319,48 +320,47 @@ class EmailsettingsAction extends SettingsAction */ function savePreferences() { - $user = common_current_user(); + $user = common_current_user(); - if (Event::handle('StartEmailSaveForm', array($this, &$user))) { + if (Event::handle('StartEmailSaveForm', array($this, &$user))) { + $emailnotifysub = $this->boolean('emailnotifysub'); + $emailnotifyfav = $this->boolean('emailnotifyfav'); + $emailnotifymsg = $this->boolean('emailnotifymsg'); + $emailnotifynudge = $this->boolean('emailnotifynudge'); + $emailnotifyattn = $this->boolean('emailnotifyattn'); + $emailmicroid = $this->boolean('emailmicroid'); + $emailpost = $this->boolean('emailpost'); - $emailnotifysub = $this->boolean('emailnotifysub'); - $emailnotifyfav = $this->boolean('emailnotifyfav'); - $emailnotifymsg = $this->boolean('emailnotifymsg'); - $emailnotifynudge = $this->boolean('emailnotifynudge'); - $emailnotifyattn = $this->boolean('emailnotifyattn'); - $emailmicroid = $this->boolean('emailmicroid'); - $emailpost = $this->boolean('emailpost'); + assert(!is_null($user)); // should already be checked - assert(!is_null($user)); // should already be checked + $user->query('BEGIN'); - $user->query('BEGIN'); + $original = clone($user); - $original = clone($user); + $user->emailnotifysub = $emailnotifysub; + $user->emailnotifyfav = $emailnotifyfav; + $user->emailnotifymsg = $emailnotifymsg; + $user->emailnotifynudge = $emailnotifynudge; + $user->emailnotifyattn = $emailnotifyattn; + $user->emailmicroid = $emailmicroid; + $user->emailpost = $emailpost; - $user->emailnotifysub = $emailnotifysub; - $user->emailnotifyfav = $emailnotifyfav; - $user->emailnotifymsg = $emailnotifymsg; - $user->emailnotifynudge = $emailnotifynudge; - $user->emailnotifyattn = $emailnotifyattn; - $user->emailmicroid = $emailmicroid; - $user->emailpost = $emailpost; + $result = $user->update($original); - $result = $user->update($original); + if ($result === false) { + common_log_db_error($user, 'UPDATE', __FILE__); + // TRANS: Server error thrown on database error updating e-mail preferences. + $this->serverError(_('Could not update user.')); + return; + } - if ($result === false) { - common_log_db_error($user, 'UPDATE', __FILE__); - // TRANS: Server error thrown on database error updating e-mail preferences. - $this->serverError(_('Could not update user.')); - return; - } + $user->query('COMMIT'); - $user->query('COMMIT'); + Event::handle('EndEmailSaveForm', array($this)); - Event::handle('EndEmailSaveForm', array($this)); - - // TRANS: Confirmation message for successful e-mail preferences save. - $this->showForm(_('Email preferences saved.'), true); - } + // TRANS: Confirmation message for successful e-mail preferences save. + $this->showForm(_('Email preferences saved.'), true); + } } /** @@ -404,24 +404,29 @@ class EmailsettingsAction extends SettingsAction return; } - $confirm = new Confirm_address(); + if (Event::handle('StartAddEmailAddress', array($user, $email))) { - $confirm->address = $email; - $confirm->address_type = 'email'; - $confirm->user_id = $user->id; - $confirm->code = common_confirmation_code(64); + $confirm = new Confirm_address(); - $result = $confirm->insert(); + $confirm->address = $email; + $confirm->address_type = 'email'; + $confirm->user_id = $user->id; + $confirm->code = common_confirmation_code(64); - if ($result === false) { - common_log_db_error($confirm, 'INSERT', __FILE__); - // TRANS: Server error thrown on database error adding e-mail confirmation code. - $this->serverError(_('Could not insert confirmation code.')); - return; + $result = $confirm->insert(); + + if ($result === false) { + common_log_db_error($confirm, 'INSERT', __FILE__); + // TRANS: Server error thrown on database error adding e-mail confirmation code. + $this->serverError(_('Could not insert confirmation code.')); + return; + } + + mail_confirm_address($user, $confirm->code, $user->nickname, $email); + + Event::handle('EndAddEmailAddress', array($user, $email)); } - mail_confirm_address($user, $confirm->code, $user->nickname, $email); - // TRANS: Message given saving valid e-mail address that is to be confirmed. $msg = _('A confirmation code was sent to the email address you added. '. 'Check your inbox (and spam box!) for the code and instructions '. diff --git a/actions/favor.php b/actions/favor.php index 61db235738..39d7735d00 100644 --- a/actions/favor.php +++ b/actions/favor.php @@ -58,7 +58,7 @@ class FavorAction extends Action { parent::handle($args); if (!common_logged_in()) { - // TRANS: Client error displayed when trying to mark a notice as favorite without being logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.')); return; } @@ -72,6 +72,7 @@ class FavorAction extends Action $notice = Notice::staticGet($id); $token = $this->trimmed('token-'.$notice->id); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token. Try again, please.')); return; } diff --git a/actions/foaf.php b/actions/foaf.php index ceb575c736..fa6efde6e4 100644 --- a/actions/foaf.php +++ b/actions/foaf.php @@ -65,7 +65,7 @@ class FoafAction extends Action $this->profile = $this->user->getProfile(); if (!$this->profile) { - // TRANS: Server error displayed when requesting Friends of a Friend feed for a user for which the profile could not be found. + // TRANS: Error message displayed when referring to a user without a profile. $this->serverError(_('User has no profile.'), 500); return false; } diff --git a/actions/geocode.php b/actions/geocode.php index 123a839f56..9e208914c1 100644 --- a/actions/geocode.php +++ b/actions/geocode.php @@ -52,6 +52,7 @@ class GeocodeAction extends Action parent::prepare($args); $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token. '. 'Try again, please.')); } diff --git a/actions/groupblock.php b/actions/groupblock.php index d426563d8c..a597d47c29 100644 --- a/actions/groupblock.php +++ b/actions/groupblock.php @@ -56,12 +56,13 @@ class GroupblockAction extends RedirectingAction { parent::prepare($args); if (!common_logged_in()) { - // TRANS: Client error displayed trying to block a user from a group while not logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.')); return false; } $token = $this->trimmed('token'); if (empty($token) || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token. Try again, please.')); return; } diff --git a/actions/grouplogo.php b/actions/grouplogo.php index db0d40ff0c..a6694cd837 100644 --- a/actions/grouplogo.php +++ b/actions/grouplogo.php @@ -180,8 +180,8 @@ class GrouplogoAction extends GroupDesignAction if (!$profile) { common_log_db_error($user, 'SELECT', __FILE__); - // TRANS: Server error displayed coming across a request from a user without a profile. - $this->serverError(_('User without matching profile.')); + // TRANS: Error message displayed when referring to a user without a profile. + $this->serverError(_('User has no profile.')); return; } diff --git a/actions/groupqueue.php b/actions/groupqueue.php index dca0ff7bd5..7cc32a9c69 100644 --- a/actions/groupqueue.php +++ b/actions/groupqueue.php @@ -156,11 +156,12 @@ class GroupqueueAction extends GroupDesignAction $members->free(); $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, - $this->page, 'groupmembers', + $this->page, 'groupqueue', array('nickname' => $this->group->nickname)); } } +// @todo FIXME: documentation missing. class GroupQueueList extends GroupMemberList { function newListItem($profile) @@ -169,6 +170,7 @@ class GroupQueueList extends GroupMemberList } } +// @todo FIXME: documentation missing. class GroupQueueListItem extends GroupMemberListItem { function showActions() diff --git a/actions/grouprss.php b/actions/grouprss.php index 39dcff83d9..0f54fc435b 100644 --- a/actions/grouprss.php +++ b/actions/grouprss.php @@ -137,7 +137,7 @@ class groupRssAction extends Rss10Action // TRANS: Message is used as link title. %s is a user nickname. 'title' => sprintf(_('%s timeline'), $group->nickname), 'link' => common_local_url('showgroup', array('nickname' => $group->nickname)), - // TRANS: Message is used as link description. %1$s is a username, %2$s is a site name. + // TRANS: Message is used as link description. %1$s is a group name, %2$s is a site name. 'description' => sprintf(_('Updates from members of %1$s on %2$s!'), $group->nickname, common_config('site', 'name'))); return $c; diff --git a/actions/groupunblock.php b/actions/groupunblock.php index de0af59821..c14ec04adc 100644 --- a/actions/groupunblock.php +++ b/actions/groupunblock.php @@ -56,12 +56,13 @@ class GroupunblockAction extends Action { parent::prepare($args); if (!common_logged_in()) { - // TRANS: Client error displayed when trying to unblock a user from a group while not logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.')); return false; } $token = $this->trimmed('token'); if (empty($token) || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token. Try again, please.')); return; } diff --git a/actions/hcard.php b/actions/hcard.php index 8781f6f882..6db2972b9d 100644 --- a/actions/hcard.php +++ b/actions/hcard.php @@ -71,7 +71,7 @@ class HcardAction extends Action $this->profile = $this->user->getProfile(); if (!$this->profile) { - // TRANS: Server error displayed when trying to get a user hCard for a user without a profile. + // TRANS: Error message displayed when referring to a user without a profile. $this->serverError(_('User has no profile.')); return false; } diff --git a/actions/imsettings.php b/actions/imsettings.php index b887fdb231..e809981a3f 100644 --- a/actions/imsettings.php +++ b/actions/imsettings.php @@ -240,6 +240,7 @@ class ImsettingsAction extends SettingsAction // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->showForm(_('There was a problem with your session token. '. 'Try again, please.')); return; diff --git a/actions/invite.php b/actions/invite.php index bbb6b26c11..1bfc9f76d3 100644 --- a/actions/invite.php +++ b/actions/invite.php @@ -1,7 +1,7 @@ trimmed('token'); - if (!$token || $token != common_session_token()) { - $this->showForm(_('There was a problem with your session token. Try again, please.')); - return; - } - - $user = common_current_user(); - $profile = $user->getProfile(); - - $bestname = $profile->getBestName(); - $sitename = common_config('site', 'name'); - $personal = $this->trimmed('personal'); - - $addresses = explode("\n", $this->trimmed('addresses')); - - foreach ($addresses as $email) { - $email = trim($email); - if (!Validate::email($email, common_config('email', 'check_domain'))) { - // TRANS: Form validation message when providing an e-mail address that does not validate. - // TRANS: %s is an invalid e-mail address. - $this->showForm(sprintf(_('Invalid email address: %s.'), $email)); + if (Event::handle('StartSendInvitations', array(&$this))) { + // CSRF protection + $token = $this->trimmed('token'); + if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. + $this->showForm(_('There was a problem with your session token. Try again, please.')); return; } - } - $this->already = array(); - $this->subbed = array(); + $user = common_current_user(); + $profile = $user->getProfile(); - foreach ($addresses as $email) { - $email = common_canonical_email($email); - $other = User::staticGet('email', $email); - if ($other) { - if ($user->isSubscribed($other)) { - $this->already[] = $other; - } else { - subs_subscribe_to($user, $other); - $this->subbed[] = $other; + $bestname = $profile->getBestName(); + $sitename = common_config('site', 'name'); + $personal = $this->trimmed('personal'); + + $addresses = explode("\n", $this->trimmed('addresses')); + foreach ($addresses as $email) { + $email = trim($email); + $valid = null; + + try { + + if (Event::handle('StartValidateUserEmail', array(null, $email, &$valid))) { + $valid = Validate::email($email, common_config('email', 'check_domain')); + Event::handle('EndValidateUserEmail', array(null, $email, &$valid)); + } + + if ($valid) { + if (Event::handle('StartValidateEmailInvite', array($user, $email, &$valid))) { + $valid = true; + Event::handle('EndValidateEmailInvite', array($user, $email, &$valid)); + } + } + + if (!$valid) { + // TRANS: Form validation message when providing an e-mail address that does not validate. + // TRANS: %s is an invalid e-mail address. + $this->showForm(sprintf(_('Invalid email address: %s.'), $email)); + return; + } + } catch (ClientException $e) { + $this->showForm($e->getMessage()); + return; } - } else { - $this->sent[] = $email; - $this->sendInvitation($email, $user, $personal); } + + $this->already = array(); + $this->subbed = array(); + + foreach ($addresses as $email) { + $email = common_canonical_email($email); + $other = User::staticGet('email', $email); + if ($other) { + if ($user->isSubscribed($other)) { + $this->already[] = $other; + } else { + subs_subscribe_to($user, $other); + $this->subbed[] = $other; + } + } else { + $this->sent[] = $email; + $this->sendInvitation($email, $user, $personal); + } + } + + $this->mode = 'sent'; + + $this->showPage(); + Event::handle('EndSendInvitations', array($this)); } - - $this->mode = 'sent'; - - $this->showPage(); } function showScripts() @@ -132,50 +160,53 @@ class InviteAction extends CurrentUserDesignAction function showInvitationSuccess() { - if ($this->already) { - // TRANS: Message displayed inviting users to use a StatusNet site while the inviting user - // TRANS: is already subscribed to one or more users with the given e-mail address(es). - // TRANS: Plural form is based on the number of reported already subscribed e-mail addresses. - // TRANS: Followed by a bullet list. - $this->element('p', null, _m('You are already subscribed to this user:', - 'You are already subscribed to these users:', - count($this->already))); - $this->elementStart('ul'); - foreach ($this->already as $other) { - // TRANS: Used as list item for already subscribed users (%1$s is nickname, %2$s is e-mail address). - $this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email)); + if (Event::handle('StartShowInvitationSuccess', array($this))) { + if ($this->already) { + // TRANS: Message displayed inviting users to use a StatusNet site while the inviting user + // TRANS: is already subscribed to one or more users with the given e-mail address(es). + // TRANS: Plural form is based on the number of reported already subscribed e-mail addresses. + // TRANS: Followed by a bullet list. + $this->element('p', null, _m('You are already subscribed to this user:', + 'You are already subscribed to these users:', + count($this->already))); + $this->elementStart('ul'); + foreach ($this->already as $other) { + // TRANS: Used as list item for already subscribed users (%1$s is nickname, %2$s is e-mail address). + $this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email)); + } + $this->elementEnd('ul'); } - $this->elementEnd('ul'); - } - if ($this->subbed) { - // TRANS: Message displayed inviting users to use a StatusNet site while the invited user - // TRANS: already uses a this StatusNet site. Plural form is based on the number of - // TRANS: reported already present people. Followed by a bullet list. - $this->element('p', null, _m('This person is already a user and you were automatically subscribed:', - 'These people are already users and you were automatically subscribed to them:', - count($this->subbed))); - $this->elementStart('ul'); - foreach ($this->subbed as $other) { - // TRANS: Used as list item for already registered people (%1$s is nickname, %2$s is e-mail address). - $this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email)); + if ($this->subbed) { + // TRANS: Message displayed inviting users to use a StatusNet site while the invited user + // TRANS: already uses a this StatusNet site. Plural form is based on the number of + // TRANS: reported already present people. Followed by a bullet list. + $this->element('p', null, _m('This person is already a user and you were automatically subscribed:', + 'These people are already users and you were automatically subscribed to them:', + count($this->subbed))); + $this->elementStart('ul'); + foreach ($this->subbed as $other) { + // TRANS: Used as list item for already registered people (%1$s is nickname, %2$s is e-mail address). + $this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email)); + } + $this->elementEnd('ul'); } - $this->elementEnd('ul'); - } - if ($this->sent) { - // TRANS: Message displayed inviting users to use a StatusNet site. Plural form is - // TRANS: based on the number of invitations sent. Followed by a bullet list of - // TRANS: e-mail addresses to which invitations were sent. - $this->element('p', null, _m('Invitation sent to the following person:', - 'Invitations sent to the following people:', - count($this->sent))); - $this->elementStart('ul'); - foreach ($this->sent as $other) { - $this->element('li', null, $other); + if ($this->sent) { + // TRANS: Message displayed inviting users to use a StatusNet site. Plural form is + // TRANS: based on the number of invitations sent. Followed by a bullet list of + // TRANS: e-mail addresses to which invitations were sent. + $this->element('p', null, _m('Invitation sent to the following person:', + 'Invitations sent to the following people:', + count($this->sent))); + $this->elementStart('ul'); + foreach ($this->sent as $other) { + $this->element('li', null, $other); + } + $this->elementEnd('ul'); + // TRANS: Generic message displayed after sending out one or more invitations to + // TRANS: people to join a StatusNet site. + $this->element('p', null, _('You will be notified when your invitees accept the invitation and register on the site. Thanks for growing the community!')); } - $this->elementEnd('ul'); - // TRANS: Generic message displayed after sending out one or more invitations to - // TRANS: people to join a StatusNet site. - $this->element('p', null, _('You will be notified when your invitees accept the invitation and register on the site. Thanks for growing the community!')); + Event::handle('EndShowInvitationSuccess', array($this)); } } @@ -203,35 +234,11 @@ class InviteAction extends CurrentUserDesignAction function showInviteForm() { - $this->elementStart('form', array('method' => 'post', - 'id' => 'form_invite', - 'class' => 'form_settings', - 'action' => common_local_url('invite'))); - $this->elementStart('fieldset'); - // TRANS: Form legend. - $this->element('legend', null, 'Send an invitation'); - $this->hidden('token', common_session_token()); - - $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - // TRANS: Field label for a list of e-mail addresses. - $this->textarea('addresses', _('Email addresses'), - $this->trimmed('addresses'), - // TRANS: Tooltip for field label for a list of e-mail addresses. - _('Addresses of friends to invite (one per line).')); - $this->elementEnd('li'); - $this->elementStart('li'); - // TRANS: Field label for a personal message to send to invitees. - $this->textarea('personal', _('Personal message'), - $this->trimmed('personal'), - // TRANS: Tooltip for field label for a personal message to send to invitees. - _('Optionally add a personal message to the invitation.')); - $this->elementEnd('li'); - $this->elementEnd('ul'); - // TRANS: Send button for inviting friends - $this->submit('send', _m('BUTTON', 'Send')); - $this->elementEnd('fieldset'); - $this->elementEnd('form'); + if (Event::handle('StartShowInviteForm', array($this))) { + $form = new InviteForm($this); + $form->show(); + Event::handle('EndShowInviteForm', array($this)); + } } function sendInvitation($email, $user, $personal) @@ -254,44 +261,31 @@ class InviteAction extends CurrentUserDesignAction return false; } + $confirmUrl = common_local_url('register', array('code' => $invite->code)); + $recipients = array($email); $headers['From'] = mail_notify_from(); $headers['To'] = trim($email); + $headers['Content-Type'] = 'text/html; charset=UTF-8'; + // TRANS: Subject for invitation email. Note that 'them' is correct as a gender-neutral // TRANS: singular 3rd-person pronoun in English. %1$s is the inviting user, $2$s is // TRANS: the StatusNet sitename. $headers['Subject'] = sprintf(_('%1$s has invited you to join them on %2$s'), $bestname, $sitename); - // TRANS: Body text for invitation email. Note that 'them' is correct as a gender-neutral - // TRANS: singular 3rd-person pronoun in English. %1$s is the inviting user, %2$s is the - // TRANS: StatusNet sitename, %3$s is the site URL, %4$s is the personal message from the - // TRANS: inviting user, %s%5 a link to the timeline for the inviting user, %s$6 is a link - // TRANS: to register with the StatusNet site. - $body = sprintf(_("%1\$s has invited you to join them on %2\$s (%3\$s).\n\n". - "%2\$s is a micro-blogging service that lets you keep up-to-date with people you know and people who interest you.\n\n". - "You can also share news about yourself, your thoughts, or your life online with people who know about you. ". - "It's also great for meeting new people who share your interests.\n\n". - "%1\$s said:\n\n%4\$s\n\n". - "You can see %1\$s's profile page on %2\$s here:\n\n". - "%5\$s\n\n". - "If you'd like to try the service, click on the link below to accept the invitation.\n\n". - "%6\$s\n\n". - "If not, you can ignore this message. Thanks for your patience and your time.\n\n". - "Sincerely, %2\$s\n"), - $bestname, - $sitename, - common_root_url(), - $personal, - common_local_url('showstream', array('nickname' => $user->nickname)), - common_local_url('register', array('code' => $invite->code))); + $title = (empty($personal)) ? 'invite' : 'invitepersonal'; + + // @todo FIXME: i18n issue. + $inviteTemplate = DocFile::forTitle($title, DocFile::mailPaths()); + + $body = $inviteTemplate->toHTML(array('inviter' => $bestname, + 'inviterurl' => $profile->profileurl, + 'confirmurl' => $confirmUrl, + 'personal' => $personal)); + + common_debug('Confirm URL is ' . common_local_url('register', array('code' => $invite->code))); mail_send($recipients, $headers, $body); } - - function showObjectNav() - { - $nav = new SubGroupNav($this, common_current_user()); - $nav->show(); - } } diff --git a/actions/login.php b/actions/login.php index 7ec9c32139..f774a0ed80 100644 --- a/actions/login.php +++ b/actions/login.php @@ -228,7 +228,7 @@ class LoginAction extends Action $this->elementStart('ul', 'form_data'); $this->elementStart('li'); // TRANS: Field label on login page. - $this->input('nickname', _('Nickname')); + $this->input('nickname', _('Username or email address')); $this->elementEnd('li'); $this->elementStart('li'); // TRANS: Field label on login page. diff --git a/actions/logout.php b/actions/logout.php index 8e903db221..567d808cd1 100644 --- a/actions/logout.php +++ b/actions/logout.php @@ -65,7 +65,7 @@ class LogoutAction extends Action { parent::handle($args); if (!common_logged_in()) { - // TRANS: Client error displayed trying to log out when not logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.')); } else { if (Event::handle('StartLogout', array($this))) { diff --git a/actions/makeadmin.php b/actions/makeadmin.php index 3613e1eb70..8ec8a7ce0c 100644 --- a/actions/makeadmin.php +++ b/actions/makeadmin.php @@ -58,12 +58,13 @@ class MakeadminAction extends RedirectingAction { parent::prepare($args); if (!common_logged_in()) { - // TRANS: Client error displayed when trying to access the "make admin" page while not logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.')); return false; } $token = $this->trimmed('token'); if (empty($token) || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token. Try again, please.')); return; } diff --git a/actions/newapplication.php b/actions/newapplication.php index a2c4f58b8d..93ef417f63 100644 --- a/actions/newapplication.php +++ b/actions/newapplication.php @@ -109,6 +109,7 @@ class NewApplicationAction extends OwnerDesignAction // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token.')); return; } @@ -294,9 +295,8 @@ class NewApplicationAction extends OwnerDesignAction $app->uploadLogo(); } catch (Exception $e) { $app->query('ROLLBACK'); - // TRANS: Form validation error on New application page when providing an invalid image upload. $this->showForm(_('Invalid image.')); - return; + return; } $app->query('COMMIT'); diff --git a/actions/newgroup.php b/actions/newgroup.php index 540a42b9ba..c54e24ed95 100644 --- a/actions/newgroup.php +++ b/actions/newgroup.php @@ -130,8 +130,8 @@ class NewgroupAction extends Action $homepage = $this->trimmed('homepage'); $description = $this->trimmed('description'); $location = $this->trimmed('location'); + $private = $this->boolean('private'); $aliasstring = $this->trimmed('aliases'); - $join_policy = intval($this->arg('join_policy')); if ($this->nicknameExists($nickname)) { // TRANS: Group create form validation error. @@ -203,6 +203,14 @@ class NewgroupAction extends Action } } + if ($private) { + $force_scope = 1; + $join_policy = User_group::JOIN_POLICY_MODERATE; + } else { + $force_scope = 0; + $join_policy = User_group::JOIN_POLICY_OPEN; + } + $cur = common_current_user(); // Checked in prepare() above @@ -217,6 +225,7 @@ class NewgroupAction extends Action 'aliases' => $aliases, 'userid' => $cur->id, 'join_policy' => $join_policy, + 'force_scope' => $force_scope, 'local' => true)); $this->group = $group; diff --git a/actions/newmessage.php b/actions/newmessage.php index 8a03aebfac..dd7c5357ba 100644 --- a/actions/newmessage.php +++ b/actions/newmessage.php @@ -85,7 +85,7 @@ class NewmessageAction extends Action parent::handle($args); if (!common_logged_in()) { - // TRANS: Client error displayed trying to create a new direct message while not logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.'), 403); } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { $this->saveNewMessage(); @@ -137,6 +137,7 @@ class NewmessageAction extends Action $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->showForm(_('There was a problem with your session token. ' . 'Try again, please.')); return; diff --git a/actions/newnotice.php b/actions/newnotice.php index 7f697e23f3..0aafcd41c6 100644 --- a/actions/newnotice.php +++ b/actions/newnotice.php @@ -64,7 +64,7 @@ class NewnoticeAction extends Action function title() { // TRANS: Page title for sending a new notice. - return _('New notice'); + return _m('TITLE','New notice'); } /** @@ -83,7 +83,7 @@ class NewnoticeAction extends Action function handle($args) { if (!common_logged_in()) { - // TRANS: Client error displayed trying to send a notice while not logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.')); } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { // check for this before token since all POST and FILES data @@ -101,6 +101,7 @@ class NewnoticeAction extends Action // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token. '. 'Try again, please.')); } @@ -180,6 +181,8 @@ class NewnoticeAction extends Action if (Notice::contentTooLong($content_shortened)) { $upload->delete(); + // TRANS: Client error displayed exceeding the maximum notice length. + // TRANS: %d is the maximum length for a notice. $this->clientError(sprintf(_m('Maximum notice size is %d character, including attachment URL.', 'Maximum notice size is %d characters, including attachment URL.', Notice::maxContent()), @@ -209,6 +212,10 @@ class NewnoticeAction extends Action $author_id = $user->id; $text = $content_shortened; + // Does the heavy-lifting for getting "To:" information + + ToSelector::fillOptions($this, $options); + if (Event::handle('StartNoticeSaveWeb', array($this, &$author_id, &$text, &$options))) { $notice = Notice::saveNew($user->id, $content_shortened, 'web', $options); @@ -282,7 +289,8 @@ class NewnoticeAction extends Action { $this->startHTML('text/xml;charset=utf-8', true); $this->elementStart('head'); - $this->element('title', null, _('New notice')); + // TRANS: Title for form to send a new notice. + $this->element('title', null, _m('TITLE','New notice')); $this->elementEnd('head'); $this->elementStart('body'); @@ -323,6 +331,8 @@ class NewnoticeAction extends Action } /** + * // XXX: Should we be showing the notice form with microapps here? + * * Overload for replies or bad results * * We show content in the notice form if there were replies or results. @@ -344,10 +354,27 @@ class NewnoticeAction extends Action $inreplyto = null; } - $notice_form = new NoticeForm($this, array('content' => $content, - 'inreplyto' => $inreplyto)); + $this->elementStart('div', 'input_forms'); + $this->elementStart( + 'div', + array( + 'id' => 'input_form_status', + 'class' => 'input_form current nonav' + ) + ); + + $notice_form = new NoticeForm( + $this, + array( + 'content' => $content, + 'inreplyto' => $inreplyto + ) + ); $notice_form->show(); + + $this->elementEnd('div'); + $this->elementEnd('div'); } /** diff --git a/actions/noticesearch.php b/actions/noticesearch.php index 1f43af800d..d4fc884f4b 100644 --- a/actions/noticesearch.php +++ b/actions/noticesearch.php @@ -48,10 +48,36 @@ require_once INSTALLDIR.'/lib/searchaction.php'; */ class NoticesearchAction extends SearchAction { + protected $q = null; + function prepare($args) { parent::prepare($args); + $this->q = $this->trimmed('q'); + + // FIXME: very dependent on tag format + if (preg_match('/^#([\pL\pN_\-\.]{1,64})/ue', $this->q)) { + common_redirect(common_local_url('tag', + array('tag' => common_canonical_tag(substr($this->q, 1)))), + 303); + } + + if (!empty($this->q)) { + + $profile = Profile::current(); + $stream = new SearchNoticeStream($this->q, $profile); + $page = $this->trimmed('page'); + + if (empty($page)) { + $page = 1; + } else { + $page = (int)$page; + } + + $this->notice = $stream->getNotices((($page-1)*NOTICES_PER_PAGE), NOTICES_PER_PAGE + 1); + } + common_set_returnto($this->selfUrl()); return true; @@ -106,18 +132,25 @@ class NoticesearchAction extends SearchAction */ function showResults($q, $page) { - $notice = new Notice(); - - $search_engine = $notice->getSearchEngine('notice'); - $search_engine->set_sort_mode('chron'); - // Ask for an extra to see if there's more. - $search_engine->limit((($page-1)*NOTICES_PER_PAGE), NOTICES_PER_PAGE + 1); - if (false === $search_engine->query($q)) { - $cnt = 0; - } else { - $cnt = $notice->find(); + if (Event::handle('StartNoticeSearchShowResults', array($this, $q, $this->notice))) { + if ($this->notice->N === 0) { + $this->showEmptyResults($q, $page); + } else { + $terms = preg_split('/[\s,]+/', $q); + $nl = new SearchNoticeList($this->notice, $this, $terms); + $cnt = $nl->show(); + $this->pagination($page > 1, + $cnt > NOTICES_PER_PAGE, + $page, + 'noticesearch', + array('q' => $q)); + } + Event::handle('EndNoticeSearchShowResults', array($this, $q, $this->notice)); } - if ($cnt === 0) { + } + + function showEmptyResults($q, $page) + { // TRANS: Text for notice search results is the query had no results. $this->element('p', 'error', _('No results.')); @@ -137,15 +170,6 @@ class NoticesearchAction extends SearchAction $this->raw(common_markup_to_html($message)); $this->elementEnd('div'); return; - } - if (Event::handle('StartNoticeSearchShowResults', array($this, $q, $notice))) { - $terms = preg_split('/[\s,]+/', $q); - $nl = new SearchNoticeList($notice, $this, $terms); - $cnt = $nl->show(); - $this->pagination($page > 1, $cnt > NOTICES_PER_PAGE, - $page, 'noticesearch', array('q' => $q)); - Event::handle('EndNoticeSearchShowResults', array($this, $q, $notice)); - } } function showScripts() diff --git a/actions/nudge.php b/actions/nudge.php index 61874c7505..a44915a2d6 100644 --- a/actions/nudge.php +++ b/actions/nudge.php @@ -60,7 +60,7 @@ class NudgeAction extends Action parent::handle($args); if (!common_logged_in()) { - // TRANS: Client error displayed trying to nudge a user without being logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.')); return; } @@ -78,6 +78,7 @@ class NudgeAction extends Action $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token. Try again, please.')); return; } diff --git a/actions/oauthconnectionssettings.php b/actions/oauthconnectionssettings.php index bcef773ac6..5c66730931 100644 --- a/actions/oauthconnectionssettings.php +++ b/actions/oauthconnectionssettings.php @@ -132,6 +132,7 @@ class OauthconnectionssettingsAction extends SettingsAction $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->showForm(_('There was a problem with your session token. '. 'Try again, please.')); return; diff --git a/actions/passwordsettings.php b/actions/passwordsettings.php index d9a6d32ae0..3ac1a3f946 100644 --- a/actions/passwordsettings.php +++ b/actions/passwordsettings.php @@ -143,6 +143,7 @@ class PasswordsettingsAction extends SettingsAction $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->showForm(_('There was a problem with your session token. '. 'Try again, please.')); return; diff --git a/actions/peopletag.php b/actions/peopletag.php index 2adc24ea66..dbdcf5fbc7 100644 --- a/actions/peopletag.php +++ b/actions/peopletag.php @@ -2,7 +2,7 @@ /** * StatusNet, the distributed open-source microblogging tool * - * Action for showing profiles self-tagged with a given tag + * Lists by a user * * PHP version 5 * @@ -16,13 +16,16 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * + * PHP version 5 + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * - * @category Action + * @category Personal * @package StatusNet * @author Evan Prodromou * @author Zach Copley + * @author Shashi Gowda * @copyright 2009 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ @@ -32,150 +35,131 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -/** - * This class outputs a paginated list of profiles self-tagged with a given tag - * - * @category Output - * @package StatusNet - * @author Evan Prodromou - * @author Zach Copley - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ - * - * @see Action - */ +require_once INSTALLDIR.'/lib/peopletaglist.php'; +// cache 3 pages +define('PEOPLETAG_CACHE_WINDOW', PEOPLETAGS_PER_PAGE*3 + 1); + class PeopletagAction extends Action { - - var $tag = null; var $page = null; + var $tag = null; - /** - * For initializing members of the class. - * - * @param array $argarray misc. arguments - * - * @return boolean true - */ - function prepare($argarray) + function isReadOnly($args) { - parent::prepare($argarray); + return true; + } - $this->tag = $this->trimmed('tag'); - - if (!common_valid_profile_tag($this->tag)) { - // TRANS: Client error displayed when trying to tag a profile with an invalid tag. - // TRANS: %s is the invalid tag. - $this->clientError(sprintf(_('Not a valid people tag: %s.'), - $this->tag)); - return; + function title() + { + if ($this->page == 1) { + // TRANS: Title for list page. + // TRANS: %s is a list. + return sprintf(_('Public list %s'), $this->tag); + } else { + // TRANS: Title for list page. + // TRANS: %1$s is a list, %2$d is a page number. + return sprintf(_('Public list %1$s, page %2$d'), $this->tag, $this->page); } + } - $this->page = ($this->arg('page')) ? $this->arg('page') : 1; + function prepare($args) + { + parent::prepare($args); + $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; - common_set_returnto($this->selfUrl()); + $tag_arg = $this->arg('tag'); + $tag = common_canonical_tag($tag_arg); + + // Permanent redirect on non-canonical nickname + + if ($tag_arg != $tag) { + $args = array('tag' => $nickname); + if ($this->page && $this->page != 1) { + $args['page'] = $this->page; + } + common_redirect(common_local_url('peopletag', $args), 301); + return false; + } + $this->tag = $tag; return true; } - /** - * Handler method - * - * @param array $argarray is ignored since it's now passed in in prepare() - * - * @return boolean is read only action? - */ - function handle($argarray) + function handle($args) { - parent::handle($argarray); + parent::handle($args); $this->showPage(); } - /** - * Whips up a query to get a list of profiles based on the provided - * people tag and page, initalizes a ProfileList widget, and displays - * it to the user. - * - * @return nothing - */ + function showLocalNav() + { + $nav = new PublicGroupNav($this); + $nav->show(); + } + + function showAnonymousMessage() + { + $notice = + // TRANS: Message for anonymous users on list page. + // TRANS: This message contains Markdown links in the form [description](link). + _('Lists are how you sort similar ' . + 'people on %%site.name%%, a [micro-blogging]' . + '(http://en.wikipedia.org/wiki/Micro-blogging) service ' . + 'based on the Free Software [StatusNet](http://status.net/) tool. ' . + 'You can then easily keep track of what they ' . + 'are doing by subscribing to the list\'s timeline.' ); + $this->elementStart('div', array('id' => 'anon_notice')); + $this->raw(common_markup_to_html($notice)); + $this->elementEnd('div'); + } + function showContent() { + $offset = ($this->page-1) * PEOPLETAGS_PER_PAGE; + $limit = PEOPLETAGS_PER_PAGE + 1; - $profile = new Profile(); + $ptags = new Profile_list(); + $ptags->tag = $this->tag; - $offset = ($this->page - 1) * PROFILES_PER_PAGE; - $limit = PROFILES_PER_PAGE + 1; + $user = common_current_user(); - if (common_config('db', 'type') == 'pgsql') { - $lim = ' LIMIT ' . $limit . ' OFFSET ' . $offset; + if (empty($user)) { + $ckey = sprintf('profile_list:tag:%s', $this->tag); + $ptags->private = false; + $ptags->orderBy('profile_list.modified DESC'); + + $c = Cache::instance(); + if ($offset+$limit <= PEOPLETAG_CACHE_WINDOW && !empty($c)) { + $cached_ptags = Profile_list::getCached($ckey, $offset, $limit); + if ($cached_ptags === false) { + $ptags->limit(0, PEOPLETAG_CACHE_WINDOW); + $ptags->find(); + + Profile_list::setCache($ckey, $ptags, $offset, $limit); + } else { + $ptags = clone($cached_ptags); + } + } else { + $ptags->limit($offset, $limit); + $ptags->find(); + } } else { - $lim = ' LIMIT ' . $offset . ', ' . $limit; + $ptags->whereAdd('(profile_list.private = false OR (' . + ' profile_list.tagger =' . $user->id . + ' AND profile_list.private = true) )'); + + $ptags->orderBy('profile_list.modified DESC'); + $ptags->find(); } - // XXX: memcached this + $pl = new PeopletagList($ptags, $this); + $cnt = $pl->show(); - $qry = 'SELECT profile.* ' . - 'FROM profile JOIN profile_tag ' . - 'ON profile.id = profile_tag.tagger ' . - 'WHERE profile_tag.tagger = profile_tag.tagged ' . - "AND tag = '%s' " . - 'ORDER BY profile_tag.modified DESC%s'; - - $profile->query(sprintf($qry, $this->tag, $lim)); - - $ptl = new PeopleTagList($profile, $this); // pass the ammunition - $cnt = $ptl->show(); - - $this->pagination($this->page > 1, - $cnt > PROFILES_PER_PAGE, - $this->page, - 'peopletag', - array('tag' => $this->tag)); + $this->pagination($this->page > 1, $cnt > PEOPLETAGS_PER_PAGE, + $this->page, 'peopletag', array('tag' => $this->tag)); } - /** - * Returns the page title - * - * @return string page title - */ - function title() + function showSections() { - // TRANS: Page title for users with a certain self-tag. - // TRANS: %1$s is the tag, %2$s is the page number. - return sprintf(_('Users self-tagged with %1$s - page %2$d'), - $this->tag, $this->page); - } -} - -class PeopleTagList extends ProfileList -{ - function newListItem($profile) - { - return new PeopleTagListItem($profile, $this->action); - } -} - -class PeopleTagListItem extends ProfileListItem -{ - function linkAttributes() - { - $aAttrs = parent::linkAttributes(); - - if (common_config('nofollow', 'peopletag')) { - $aAttrs['rel'] .= ' nofollow'; - } - - return $aAttrs; - } - - function homepageAttributes() - { - $aAttrs = parent::linkAttributes(); - - if (common_config('nofollow', 'peopletag')) { - $aAttrs['rel'] = 'nofollow'; - } - - return $aAttrs; } } diff --git a/actions/peopletagautocomplete.php b/actions/peopletagautocomplete.php new file mode 100644 index 0000000000..5b6ae57a53 --- /dev/null +++ b/actions/peopletagautocomplete.php @@ -0,0 +1,126 @@ +. + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class PeopletagautocompleteAction extends Action +{ + var $user; + var $tags; + var $last_mod; + + /** + * Check pre-requisites and instantiate attributes + * + * @param Array $args array of arguments (URL, GET, POST) + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + // Only for logged-in users + + $this->user = common_current_user(); + + if (empty($this->user)) { + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. + $this->clientError(_('Not logged in.')); + return false; + } + + // CSRF protection + + $token = $this->trimmed('token'); + + if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. + $this->clientError(_('There was a problem with your session token.'. + ' Try again, please.')); + return false; + } + + $profile = $this->user->getProfile(); + $tags = $profile->getLists(common_current_user()); + + $this->tags = array(); + while ($tags->fetch()) { + + if (empty($this->last_mod)) { + $this->last_mod = $tags->modified; + } + + $arr = array(); + $arr['tag'] = $tags->tag; + $arr['mode'] = $tags->private ? 'private' : 'public'; + // $arr['url'] = $tags->homeUrl(); + $arr['freq'] = $tags->taggedCount(); + + $this->tags[] = $arr; + } + + $tags = NULL; + + return true; + } + + /** + * Last modified time + * + * Helps in browser-caching + * + * @return String time + */ + function lastModified() + { + return strtotime($this->last_mod); + } + + /** + * Handle request + * + * Print the JSON autocomplete data + * + * @param Array $args unused. + * + * @return void + */ + function handle($args) + { + //common_log(LOG_DEBUG, 'Autocomplete data: ' . json_encode($this->tags)); + if ($this->tags) { + print(json_encode($this->tags)); + exit(0); + } + return false; + } +} diff --git a/actions/peopletagged.php b/actions/peopletagged.php new file mode 100644 index 0000000000..ea25c8675b --- /dev/null +++ b/actions/peopletagged.php @@ -0,0 +1,230 @@ +. + * + * @category Group + * @package StatusNet + * @author Shashi Gowda + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once(INSTALLDIR.'/lib/profilelist.php'); + +/** + * List of people tagged by the user with a tag + * + * @category Peopletag + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class PeopletaggedAction extends OwnerDesignAction +{ + var $page = null; + var $peopletag = null; + var $tagger = null; + + function isReadOnly($args) + { + return true; + } + + function prepare($args) + { + parent::prepare($args); + $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; + + $tagger_arg = $this->arg('tagger'); + $tag_arg = $this->arg('tag'); + $tagger = common_canonical_nickname($tagger_arg); + $tag = common_canonical_tag($tag_arg); + + // Permanent redirect on non-canonical nickname + + if ($tagger_arg != $tagger || $tag_arg != $tag) { + $args = array('tagger' => $nickname, 'tag' => $tag); + if ($this->page != 1) { + $args['page'] = $this->page; + } + common_redirect(common_local_url('peopletagged', $args), 301); + return false; + } + + if (!$tagger) { + // TRANS: Client error displayed when a tagger is expected but not provided. + $this->clientError(_('No tagger.'), 404); + return false; + } + + $user = User::staticGet('nickname', $tagger); + + if (!$user) { + // TRANS: Client error displayed when referring to non-existing user. + $this->clientError(_('No such user.'), 404); + return false; + } + + $this->tagger = $user->getProfile(); + $this->peopletag = Profile_list::pkeyGet(array('tagger' => $user->id, 'tag' => $tag)); + + if (!$this->peopletag) { + // TRANS: Client error displayed when referring to a non-existing list. + $this->clientError(_('No such list.'), 404); + return false; + } + + return true; + } + + function title() + { + if ($this->page == 1) { + // TRANS: Title for list of people listed by the user. + // TRANS: %1$s is a list, %2$s is a username. + return sprintf(_('People listed in %1$s by %2$s'), + $this->peopletag->tag, $this->tagger->nickname); + } else { + // TRANS: Title for list of people listed by the user. + // TRANS: %1$s is a list, %2$s is a username, %2$s is a page number. + return sprintf(_('People listed in %1$s by %2$s, page %3$d'), + $this->peopletag->tag, $this->user->nickname, + $this->page); + } + } + + function handle($args) + { + parent::handle($args); + $this->showPage(); + } + + function showPageNotice() + { + } + + function showLocalNav() + { + $nav = new PeopletagGroupNav($this, $this->peopletag); + $nav->show(); + } + + function showContent() + { + $offset = ($this->page-1) * PROFILES_PER_PAGE; + $limit = PROFILES_PER_PAGE + 1; + + $cnt = 0; + + $subs = $this->peopletag->getTagged($offset, $limit); + + if ($subs) { + $subscriber_list = new PeopletagMemberList($subs, $this->peopletag, $this); + $cnt = $subscriber_list->show(); + } + + $subs->free(); + + $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, + $this->page, 'peopletagged', + array('tagger' => $this->tagger->nickname, + 'tag' => $this->peopletag->tag)); + } +} + +class PeopletagMemberList extends ProfileList +{ + var $peopletag = null; + + function __construct($profile, $peopletag, $action) + { + parent::__construct($profile, $action); + + $this->peopletag = $peopletag; + } + + function newListItem($profile) + { + return new PeopletagMemberListItem($profile, $this->peopletag, $this->action); + } +} + +class PeopletagMemberListItem extends ProfileListItem +{ + var $peopletag = null; + + function __construct($profile, $peopletag, $action) + { + parent::__construct($profile, $action); + + $this->peopletag = $peopletag; + } + + function showFullName() + { + parent::showFullName(); + if ($this->profile->id == $this->peopletag->tagger) { + $this->out->text(' '); + // TRANS: Addition in tag membership list for creator of a tag. + $this->out->element('span', 'role', _('Creator')); + } + } + + function showActions() + { + $this->startActions(); + if (Event::handle('StartProfileListItemActionElements', array($this))) { + $this->showSubscribeButton(); + // TODO: Untag button + Event::handle('EndProfileListItemActionElements', array($this)); + } + $this->endActions(); + } + + function linkAttributes() + { + // tagging people is healthy page-rank flow. + return parent::linkAttributes(); + } + + /** + * Fetch necessary return-to arguments for the profile forms + * to return to this list when they're done. + * + * @return array + */ + protected function returnToArgs() + { + $args = array('action' => 'peopletagged', + 'tag' => $this->peopletag->tag, + 'tagger' => $this->profile->nickname); + $page = $this->out->arg('page'); + if ($page) { + $args['param-page'] = $page; + } + return $args; + } +} diff --git a/actions/peopletagsbyuser.php b/actions/peopletagsbyuser.php new file mode 100644 index 0000000000..d348585c90 --- /dev/null +++ b/actions/peopletagsbyuser.php @@ -0,0 +1,290 @@ +. + * + * @category Personal + * @package StatusNet + * @author Shashi Gowda + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/peopletaglist.php'; + +class PeopletagsbyuserAction extends OwnerDesignAction +{ + var $page = null; + var $tagger = null; + var $tags = null; + + function isReadOnly($args) + { + return true; + } + + function title() + { + if ($this->page == 1) { + if ($this->isOwner()) { + if ($this->arg('private')) { + // TRANS: Title for lists by a user page for a private tag. + return _('Private lists by you'); + } else if ($this->arg('public')) { + // TRANS: Title for lists by a user page for a public tag. + return _('Public lists by you'); + } + // TRANS: Title for lists by a user page. + return _('Lists by you'); + } + // TRANS: Title for lists by a user page. + // TRANS: %s is a user nickname. + return sprintf(_('Lists by %s'), $this->tagger->nickname); + } else { + // TRANS: Title for lists by a user page. + // TRANS: %1$s is a user nickname, %2$d is a page number. + return sprintf(_('Lists by %1$s, page %2$d'), $this->tagger->nickname, $this->page); + } + } + + function prepare($args) + { + parent::prepare($args); + + if ($this->arg('public') && $this->arg('private')) { + $this->args['public'] = $this->args['private'] = false; + } + + $nickname_arg = $this->arg('nickname'); + $nickname = common_canonical_nickname($nickname_arg); + + // Permanent redirect on non-canonical nickname + + if ($nickname_arg != $nickname) { + $args = $this->getSelfUrlArgs(); + if ($this->arg('page') && $this->arg('page') != 1) { + $args['page'] = $this->arg['page']; + } + common_redirect(common_local_url('peopletagsbyuser', $args), 301); + return false; + } + + $this->user = User::staticGet('nickname', $nickname); + + if (!$this->user) { + // TRANS: Client error displayed trying to perform an action related to a non-existing user. + $this->clientError(_('No such user.'), 404); + return false; + } + + $this->tagger = $this->user->getProfile(); + + if (!$this->tagger) { + // TRANS: Error message displayed when referring to a user without a profile. + $this->serverError(_('User has no profile.')); + return false; + } + + $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; + + + $offset = ($this->page-1) * PEOPLETAGS_PER_PAGE; + $limit = PEOPLETAGS_PER_PAGE + 1; + + $user = common_current_user(); + if ($this->arg('public')) { + $this->tags = $this->tagger->getLists(false, $offset, $limit); + } else if ($this->arg('private')) { + if (empty($user)) { + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. + $this->clientError(_('Not logged in.'), 403); + } + + if ($this->isOwner()) { + $this->tags = $this->tagger->getPrivateTags($offset, $limit); + } else { + // TRANS: Client error displayed when trying view another user's private lists. + $this->clientError(_('You cannot view others\' private lists'), 403); + } + } else { + $this->tags = $this->tagger->getLists(common_current_user(), $offset, $limit); + } + return true; + } + + function handle($args) + { + parent::handle($args); + + # Post from the tag dropdown; redirect to a GET + + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + common_redirect(common_local_url('peopletagsbyuser', $this->getSelfUrlArgs()), 303); + return; + } + + $this->showPage(); + } + + function showModeSelector() + { + $this->elementStart('dl', array('id'=>'filter_tags')); + // TRANS: Mode selector label. + $this->element('dt', null, _('Mode')); + $this->elementStart('dd'); + $this->elementStart('ul'); + $this->elementStart('li', array('id' => 'filter_tags_for', + 'class' => 'child_1')); + $this->element('a', + array('href' => + common_local_url('peopletagsforuser', + array('nickname' => $this->user->nickname))), + // TRANS: Link text to show lists for user %s. + sprintf(_('Lists for %s'), $this->tagger->nickname)); + $this->elementEnd('li'); + + if ($this->isOwner()) { + $this->elementStart('li', array('id'=>'filter_tags_item')); + $this->elementStart('form', array('name' => 'modeselector', + 'id' => 'form_filter_bymode', + 'action' => common_local_url('peopletagsbyuser', + array('nickname' => $this->tagger->nickname)), + 'method' => 'post')); + $this->elementStart('fieldset'); + // TRANS: Fieldset legend. + $this->element('legend', null, _('Select tag to filter')); + + $priv = $this->arg('private'); + $pub = $this->arg('public'); + + if (!$priv && !$pub) { + $priv = $pub = true; + } + // TRANS: Checkbox label to show private tags. + $this->checkbox('private', _m('LABEL','Private'), $priv, + // TRANS: Checkbox title. + _('Show private tags.')); + // TRANS: Checkbox label to show public tags. + $this->checkbox('public', _m('LABEL','Public'), $pub, + // TRANS: Checkbox title. + _('Show public tags.')); + $this->hidden('nickname', $this->user->nickname); + // TRANS: Submit button text for tag filter form. + $this->submit('submit', _m('BUTTON','Go')); + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + $this->elementEnd('li'); + } + $this->elementEnd('ul'); + $this->elementEnd('dd'); + $this->elementEnd('dl'); + } + + function showAnonymousMessage() + { + $notice = + // TRANS: Message displayed for anonymous users on page that displays lists by a user. + // TRANS: This message contains Markdown links in the form [description](links). + // TRANS: %s is a tagger nickname. + sprintf(_('These are lists created by **%s**. ' . + 'Lists are how you sort similar ' . + 'people on %%%%site.name%%%%, a [micro-blogging]' . + '(http://en.wikipedia.org/wiki/Micro-blogging) service ' . + 'based on the Free Software [StatusNet](http://status.net/) tool. ' . + 'You can easily keep track of what they ' . + 'are doing by subscribing to the tag\'s timeline.' ), $this->tagger->nickname); + $this->elementStart('div', array('id' => 'anon_notice')); + $this->raw(common_markup_to_html($notice)); + $this->elementEnd('div'); + } + + function showPageNotice() + { + $this->elementStart('div', 'instructions'); + $this->showModeSelector(); + $this->elementEnd('div'); + } + + function showContent() + { + #TODO: controls here. + + $pl = new PeopletagList($this->tags, $this); + $cnt = $pl->show(); + + if ($cnt == 0) { + $this->showEmptyListMessage(); + } + $this->pagination($this->page > 1, $cnt > PEOPLETAGS_PER_PAGE, + $this->page, 'peopletagsbyuser', $this->getSelfUrlArgs()); + } + + function getSelfUrlArgs() + { + $args = array(); + if ($this->arg('private')) { + $args['private'] = 1; + } else if ($this->arg('public')) { + $args['public'] = 1; + } + $args['nickname'] = $this->trimmed('nickname'); + + return $args; + } + + function isOwner() + { + $user = common_current_user(); + return !empty($user) && $user->id == $this->tagger->id; + } + + function showObjectNav() + { + $nav = new PeopletagNav($this, $this->tagger); + $nav->show(); + } + + function showEmptyListMessage() + { + // TRANS: Message displayed on page that displays lists by a user when there are none. + // TRANS: This message contains Markdown links in the form [description](links). + // TRANS: %s is a tagger nickname. + $message = sprintf(_('%s has not created any [lists](%%%%doc.lists%%%%) yet.'), $this->tagger->nickname); + $this->elementStart('div', 'guide'); + $this->raw(common_markup_to_html($message)); + $this->elementEnd('div'); + } + + function showProfileBlock() + { + $block = new AccountProfileBlock($this, $this->tagger); + $block->show(); + } + + function showSections() + { + #TODO: tags with most subscribers + #TODO: tags with most "members" + } +} diff --git a/actions/peopletagsforuser.php b/actions/peopletagsforuser.php new file mode 100644 index 0000000000..9883bd3657 --- /dev/null +++ b/actions/peopletagsforuser.php @@ -0,0 +1,167 @@ +. + * + * @category Personal + * @package StatusNet + * @author Shashi Gowda + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/peopletaglist.php'; + +class PeopletagsforuserAction extends OwnerDesignAction +{ + var $page = null; + var $tagged = null; + + function isReadOnly($args) + { + return true; + } + + function title() + { + if ($this->page == 1) { + // TRANS: Page title. %s is a tagged user's nickname. + return sprintf(_('Lists with %s in them'), $this->tagged->nickname); + } else { + // TRANS: Page title. %1$s is a tagged user's nickname, %2$s is a page number. + return sprintf(_('Lists with %1$s, page %2$d'), $this->tagged->nickname, $this->page); + } + } + + 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); + if ($this->arg('page') && $this->arg('page') != 1) { + $args['page'] = $this->arg['page']; + } + common_redirect(common_local_url('peopletagsforuser', $args), 301); + return false; + } + + $this->user = User::staticGet('nickname', $nickname); + + if (!$this->user) { + // TRANS: Client error displayed trying to perform an action related to a non-existing user. + $this->clientError(_('No such user.'), 404); + return false; + } + + $this->tagged = $this->user->getProfile(); + + if (!$this->tagged) { + // TRANS: Error message displayed when referring to a user without a profile. + $this->serverError(_('User has no profile.')); + return false; + } + + $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; + + return true; + } + + function handle($args) + { + parent::handle($args); + $this->showPage(); + } + + function showAnonymousMessage() + { + $notice = + // TRANS: Message displayed for anonymous users on page that displays lists for a user. + // TRANS: This message contains Markdown links in the form [description](links). + // TRANS: %s is a tagger nickname. + sprintf(_('These are lists for **%s**. ' . + 'lists are how you sort similar ' . + 'people on %%%%site.name%%%%, a [micro-blogging]' . + '(http://en.wikipedia.org/wiki/Micro-blogging) service ' . + 'based on the Free Software [StatusNet](http://status.net/) tool. ' . + 'You can easily keep track of what they ' . + 'are doing by subscribing to the tag\'s timeline.' ), $this->tagged->nickname); + $this->elementStart('div', array('id' => 'anon_notice')); + $this->raw(common_markup_to_html($notice)); + $this->elementEnd('div'); + } + + function showContent() + { + #TODO: controls here. + + $offset = ($this->page-1) * PEOPLETAGS_PER_PAGE; + $limit = PEOPLETAGS_PER_PAGE + 1; + + $ptags = $this->tagged->getOtherTags(common_current_user(), $offset, $limit); + + $pl = new PeopletagList($ptags, $this); + $cnt = $pl->show(); + + if ($cnt == 0) { + $this->showEmptyListMessage(); + } + $this->pagination($this->page > 1, $cnt > PEOPLETAGS_PER_PAGE, + $this->page, 'peopletagsforuser', array('nickname' => $this->tagged->id)); + } + + function showEmptyListMessage() + { + // TRANS: Message displayed on page that displays lists a user was added to when there are none. + // TRANS: This message contains Markdown links in the form [description](links). + // TRANS: %s is a user nickname. + $message = sprintf(_('%s has not been [listed](%%%%doc.lists%%%%) by anyone yet.'), $this->tagged->nickname); + $this->elementStart('div', 'guide'); + $this->raw(common_markup_to_html($message)); + $this->elementEnd('div'); + } + + function showObjectNav() + { + $nav = new PeopletagNav($this, $this->tagged); + $nav->show(); + } + + function showProfileBlock() + { + $block = new AccountProfileBlock($this, $this->tagged); + $block->show(); + } + + function showSections() + { + #TODO: tags with most subscribers + #TODO: tags with most "members" + } +} diff --git a/actions/peopletagsubscribers.php b/actions/peopletagsubscribers.php new file mode 100644 index 0000000000..ebc3a9f494 --- /dev/null +++ b/actions/peopletagsubscribers.php @@ -0,0 +1,245 @@ +. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once(INSTALLDIR.'/lib/profilelist.php'); + +/** + * List of peopletag subscribers + * + * @category Peopletag + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class PeopletagsubscribersAction extends OwnerDesignAction +{ + var $page = null; + var $peopletag = null; + var $tagger = null; + + function isReadOnly($args) + { + return true; + } + + function prepare($args) + { + parent::prepare($args); + $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; + + $tagger_arg = $this->arg('tagger'); + $tag_arg = $this->arg('tag'); + $tagger = common_canonical_nickname($tagger_arg); + $tag = common_canonical_tag($tag_arg); + + // Permanent redirect on non-canonical nickname + + if ($tagger_arg != $tagger || $tag_arg != $tag) { + $args = array('tagger' => $nickname, 'tag' => $tag); + if ($this->page != 1) { + $args['page'] = $this->page; + } + common_redirect(common_local_url('peopletagged', $args), 301); + return false; + } + + if (!$tagger) { + // TRANS: Client error displayed when a tagger is expected but not provided. + $this->clientError(_('No tagger.'), 404); + return false; + } + + $user = User::staticGet('nickname', $tagger); + + if (!$user) { + // TRANS: Client error displayed trying to perform an action related to a non-existing user. + $this->clientError(_('No such user.'), 404); + return false; + } + + $this->tagger = $user->getProfile(); + $this->peopletag = Profile_list::pkeyGet(array('tagger' => $user->id, 'tag' => $tag)); + + if (!$this->peopletag) { + // TRANS: Client error displayed trying to reference a non-existing list. + $this->clientError(_('No such list.'), 404); + return false; + } + + return true; + } + + function title() + { + if ($this->page == 1) { + // TRANS: Page title for list of list subscribers. + // TRANS: %1$s is a list, %2$s is a user nickname. + return sprintf(_('Subscribers to list %1$s by %2$s'), + $this->peopletag->tag, $this->tagger->nickname); + } else { + // TRANS: Page title for list of list subscribers. + // TRANS: %1$s is a list, %2$s is a user nickname, %3$d is a page number. + return sprintf(_('Subscribers to list %1$s by %2$s, page %3$d'), + $this->peopletag->tag, $this->tagger->nickname, + $this->page); + } + } + + function handle($args) + { + parent::handle($args); + $this->showPage(); + } + + function showPageNotice() + { + } + + function showLocalNav() + { + $nav = new PeopletagGroupNav($this); + $nav->show(); + } + + function showContent() + { + $offset = ($this->page-1) * PROFILES_PER_PAGE; + $limit = PROFILES_PER_PAGE + 1; + + $cnt = 0; + + $subs = $this->peopletag->getSubscribers($offset, $limit); + + if ($subs) { + $subscriber_list = new PeopletagSubscriberList($subs, $this->peopletag, $this); + $cnt = $subscriber_list->show(); + } + + $subs->free(); + + $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, + $this->page, 'peopletagsubscribers', + array('tagger' => $this->tagger->nickname, + 'tag' => $this->peopletag->tag)); + } +} + +class PeopletagSubscriberList extends ProfileList +{ + var $peopletag = null; + + function __construct($profile, $peopletag, $action) + { + parent::__construct($profile, $action); + + $this->peopletag = $peopletag; + } + + function newListItem($profile) + { + return new PeopletagSubscriberListItem($profile, $this->peopletag, $this->action); + } +} + +class PeopletagSubscriberListItem extends ProfileListItem +{ + var $peopletag = null; + + function __construct($profile, $peopletag, $action) + { + parent::__construct($profile, $action); + + $this->peopletag = $peopletag; + } + + function showFullName() + { + parent::showFullName(); + if ($this->profile->id == $this->peopletag->tagger) { + $this->out->text(' '); + // TRANS: Addition in tag subscribers list for creator of a tag. + $this->out->element('span', 'role', _('Creator')); + } + } + + function showActions() + { + $this->startActions(); + if (Event::handle('StartProfileListItemActionElements', array($this))) { + $this->showSubscribeButton(); + Event::handle('EndProfileListItemActionElements', array($this)); + } + $this->endActions(); + } + + function linkAttributes() + { + $aAttrs = parent::linkAttributes(); + + if (common_config('nofollow', 'members')) { + $aAttrs['rel'] .= ' nofollow'; + } + + return $aAttrs; + } + + function homepageAttributes() + { + $aAttrs = parent::linkAttributes(); + + if (common_config('nofollow', 'members')) { + $aAttrs['rel'] = 'nofollow'; + } + + return $aAttrs; + } + + /** + * Fetch necessary return-to arguments for the profile forms + * to return to this list when they're done. + * + * @return array + */ + protected function returnToArgs() + { + $args = array('action' => 'peopletagsubscribers', + 'tag' => $this->peopletag->tag, + 'tagger' => $this->profile->nickname); + $page = $this->out->arg('page'); + if ($page) { + $args['param-page'] = $page; + } + return $args; + } +} diff --git a/actions/peopletagsubscriptions.php b/actions/peopletagsubscriptions.php new file mode 100644 index 0000000000..b65542e074 --- /dev/null +++ b/actions/peopletagsubscriptions.php @@ -0,0 +1,153 @@ +. + * + * @category Personal + * @package StatusNet + * @author Shashi Gowda + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/peopletaglist.php'; + +class PeopletagsubscriptionsAction extends OwnerDesignAction +{ + var $page = null; + var $profile = null; + + function isReadOnly($args) + { + return true; + } + + function title() + { + if ($this->page == 1) { + // TRANS: Title for page that displays lists subscribed to by a user. + // TRANS: %s is a profile nickname. + return sprintf(_('Lists subscribed to by %s'), $this->profile->nickname); + } else { + // TRANS: Title for page that displays lists subscribed to by a user. + // TRANS: %1$s is a profile nickname, %2$d is a page number. + return sprintf(_('Lists subscribed to by %1$s, page %2$d'), $this->profile->nickname, $this->page); + } + } + + 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); + if ($this->arg('page') && $this->arg('page') != 1) { + $args['page'] = $this->arg['page']; + } + common_redirect(common_local_url('peopletagsbyuser', $args), 301); + return false; + } + + $user = User::staticGet('nickname', $nickname); + + if (!$user) { + // TRANS: Client error displayed trying to perform an action related to a non-existing user. + $this->clientError(_('No such user.'), 404); + return false; + } + + $this->profile = $user->getProfile(); + + if (!$this->profile) { + // TRANS: Error message displayed when referring to a user without a profile. + $this->serverError(_('User has no profile.')); + return false; + } + + $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; + + return true; + } + + function handle($args) + { + parent::handle($args); + $this->showPage(); + } + + function showAnonymousMessage() + { + $notice = + // TRANS: Message displayed for anonymous users on page that displays lists subscribed to by a user. + // TRANS: This message contains Markdown links in the form [description](links). + // TRANS: %s is a profile nickname. + sprintf(_('These are lists subscribed to by **%s**. ' . + 'Lists are how you sort similar ' . + 'people on %%%%site.name%%%%, a [micro-blogging]' . + '(http://en.wikipedia.org/wiki/Micro-blogging) service ' . + 'based on the Free Software [StatusNet](http://status.net/) tool. ' . + 'You can easily keep track of what they ' . + 'are doing by subscribing to the list\'s timeline.' ), $this->profile->nickname); + $this->elementStart('div', array('id' => 'anon_notice')); + $this->raw(common_markup_to_html($notice)); + $this->elementEnd('div'); + } + + function showContent() + { + $offset = ($this->page-1) * PEOPLETAGS_PER_PAGE; + $limit = PEOPLETAGS_PER_PAGE + 1; + + $ptags = $this->profile->getTagSubscriptions($offset, $limit); + + $pl = new PeopletagList($ptags, $this); + $cnt = $pl->show(); + + $this->pagination($this->page > 1, $cnt > PEOPLETAGS_PER_PAGE, + $this->page, 'peopletagsubscriptions', array('nickname' => $this->profile->id)); + } + + function showObjectNav() + { + $nav = new PeopletagNav($this, $this->profile); + $nav->show(); + } + + function showProfileBlock() + { + $block = new AccountProfileBlock($this, $this->profile); + $block->show(); + } + + function showSections() + { + #TODO: tags with most subscribers + #TODO: tags with most "members" + } +} diff --git a/actions/pluginenable.php b/actions/pluginenable.php index 0f2b4ba670..156a604cf0 100644 --- a/actions/pluginenable.php +++ b/actions/pluginenable.php @@ -84,6 +84,7 @@ class PluginEnableAction extends Action $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token.'. ' Try again, please.')); return false; @@ -94,7 +95,7 @@ class PluginEnableAction extends Action $this->user = common_current_user(); if (empty($this->user)) { - // TRANS: Client error displayed when trying to enable or disable a plugin while not logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.')); return false; } diff --git a/actions/profilecompletion.php b/actions/profilecompletion.php new file mode 100644 index 0000000000..045bf68a7f --- /dev/null +++ b/actions/profilecompletion.php @@ -0,0 +1,222 @@ +. + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/peopletageditform.php'; + +/** + * 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 Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class ProfilecompletionAction extends Action +{ + var $user; + var $peopletag; + var $field; + var $msg; + + /** + * Check pre-requisites and instantiate attributes + * + * @param Array $args array of arguments (URL, GET, POST) + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + // CSRF protection + + $token = $this->trimmed('token'); + + if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. + $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)) { + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. + $this->clientError(_('Not logged in.')); + return false; + } + + $id = $this->arg('peopletag_id'); + $this->peopletag = Profile_list::staticGet('id', $id); + + if (empty($this->peopletag)) { + // TRANS: Client error displayed trying to reference a non-existing list. + $this->clientError(_('No such list.')); + return false; + } + + $field = $this->arg('field'); + if (!in_array($field, array('fulltext', 'nickname', 'fullname', 'description', 'location', 'uri'))) { + // TRANS: Client error displayed when trying to add an unindentified field to profile. + // TRANS: %s is a field name. + $this->clientError(sprintf(_('Unidentified field %s.'), htmlspecialchars($field)), 404); + return false; + } + $this->field = $field; + + return true; + } + + /** + * Handle request + * + * Does the subscription and returns results. + * + * @param Array $args unused. + * + * @return void + */ + + function handle($args) + { + $this->msg = null; + + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + // TRANS: Page title. + $this->element('title', null, _m('TITLE','Search results')); + $this->elementEnd('head'); + $this->elementStart('body'); + $profiles = $this->getResults(); + + if ($this->msg !== null) { + $this->element('p', 'error', $this->msg); + } else { + if (count($profiles) > 0) { + $this->elementStart('ul', array('id' => 'profile_search_results', 'class' => 'profile-lister')); + foreach ($profiles as $profile) { + $this->showProfileItem($profile); + } + $this->elementEnd('ul'); + } else { + // TRANS: Output when there are no results for a search. + $this->element('p', 'error', _('No results.')); + } + } + $this->elementEnd('body'); + $this->elementEnd('html'); + } + + function getResults() + { + $profiles = array(); + $q = $this->arg('q'); + $q = strtolower($q); + if (strlen($q) < 3) { + // TRANS: Error message in case a search is shorter than three characters. + $this->msg = _('The search string must be at least 3 characters long.'); + } + $page = $this->arg('page'); + $page = (int) (empty($page) ? 1 : $page); + + $profile = new Profile(); + $search_engine = $profile->getSearchEngine('profile'); + + if (Event::handle('StartProfileCompletionSearch', array($this, &$profile, $search_engine))) { + $search_engine->set_sort_mode('chron'); + $search_engine->limit((($page-1)*PROFILES_PER_PAGE), PROFILES_PER_PAGE + 1); + + if (false === $search_engine->query($q)) { + $cnt = 0; + } + else { + $cnt = $profile->find(); + } + // @todo FIXME: Call-time pass-by-reference has been deprecated. + Event::handle('EndProfileCompletionSearch', $this, &$profile, $search_engine); + } + + while ($profile->fetch()) { + $profiles[] = clone($profile); + } + return $this->filter($profiles); + } + + function filter($profiles) + { + $current = $this->user->getProfile(); + $filtered_profiles = array(); + foreach ($profiles as $profile) { + if ($current->canTag($profile)) { + $filtered_profiles[] = $profile; + } + } + return $filtered_profiles; + } + + function showProfileItem($profile) + { + $this->elementStart('li', 'entity_removable_profile'); + $item = new TaggedProfileItem($this, $profile); + $item->show(); + $this->elementStart('span', 'entity_actions'); + + if ($profile->isTagged($this->peopletag)) { + $untag = new UntagButton($this, $profile, $this->peopletag); + $untag->show(); + } else { + $tag = new TagButton($this, $profile, $this->peopletag); + $tag->show(); + } + + $this->elementEnd('span'); + $this->elementEnd('li'); + } +} diff --git a/actions/profilesettings.php b/actions/profilesettings.php index e1d686ca29..667fea2cf4 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -33,8 +33,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - - /** * Change profile settings * @@ -46,7 +44,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class ProfilesettingsAction extends SettingsAction { /** @@ -127,15 +124,15 @@ class ProfilesettingsAction extends SettingsAction // TRANS: Tooltip for field label in form for profile settings. Plural // TRANS: is decided by the number of characters available for the // TRANS: biography (%d). - $bioInstr = sprintf(_m('Describe yourself and your interests in %d character', - 'Describe yourself and your interests in %d characters', + $bioInstr = sprintf(_m('Describe yourself and your interests in %d character.', + 'Describe yourself and your interests in %d characters.', $maxBio), $maxBio); } else { // TRANS: Tooltip for field label in form for profile settings. - $bioInstr = _('Describe yourself and your interests'); + $bioInstr = _('Describe yourself and your interests.'); } - // TRANS: Text area label in form for profile settings where users can provide. + // TRANS: Text area label in form for profile settings where users can provide // TRANS: their biography. $this->textarea('bio', _('Bio'), ($this->arg('bio')) ? $this->arg('bio') : $profile->bio, @@ -146,7 +143,7 @@ class ProfilesettingsAction extends SettingsAction $this->input('location', _('Location'), ($this->arg('location')) ? $this->arg('location') : $profile->location, // TRANS: Tooltip for field label in form for profile settings. - _('Where you are, like "City, State (or Region), Country"')); + _('Where you are, like "City, State (or Region), Country".')); $this->elementEnd('li'); if (common_config('location', 'share') == 'user') { $this->elementStart('li'); @@ -192,7 +189,27 @@ class ProfilesettingsAction extends SettingsAction ($this->arg('autosubscribe')) ? $this->boolean('autosubscribe') : $user->autosubscribe); $this->elementEnd('li'); + $this->elementStart('li'); + $this->dropdown('subscribe_policy', + // TRANS: Dropdown field label on profile settings, for what policies to apply when someone else tries to subscribe to your updates. + _('Subscription policy'), + // TRANS: Dropdown field option for following policy. + array(User::SUBSCRIBE_POLICY_OPEN => _('Let anyone follow me'), + // TRANS: Dropdown field option for following policy. + User::SUBSCRIBE_POLICY_MODERATE => _('Ask me first')), + // TRANS: Dropdown field title on group edit form. + _('Whether other users need your permission to follow your updates.'), + false, + (empty($user->subscribe_policy)) ? User::SUBSCRIBE_POLICY_OPEN : $user->subscribe_policy); + $this->elementEnd('li'); } + $this->elementStart('li'); + $this->checkbox('private_stream', + // TRANS: Checkbox label in profile settings. + _('Make updates visible only to my followers'), + ($this->arg('private_stream')) ? + $this->boolean('private_stream') : $user->private_stream); + $this->elementEnd('li'); $this->elementEnd('ul'); // TRANS: Button to save input in profile settings. $this->submit('save', _m('BUTTON','Save')); @@ -234,6 +251,8 @@ class ProfilesettingsAction extends SettingsAction $bio = $this->trimmed('bio'); $location = $this->trimmed('location'); $autosubscribe = $this->boolean('autosubscribe'); + $subscribe_policy = $this->trimmed('subscribe_policy'); + $private_stream = $this->boolean('private_stream'); $language = $this->trimmed('language'); $timezone = $this->trimmed('timezone'); $tagstring = $this->trimmed('tags'); @@ -279,18 +298,24 @@ class ProfilesettingsAction extends SettingsAction return; } - if ($tagstring) { - $tags = array_map('common_canonical_tag', preg_split('/[\s,]+/', $tagstring)); - } else { - $tags = array(); - } + $tags = array(); + $tag_priv = array(); + if (is_string($tagstring) && strlen($tagstring) > 0) { - foreach ($tags as $tag) { - if (!common_valid_profile_tag($tag)) { - // TRANS: Validation error in form for profile settings. - // TRANS: %s is an invalid tag. - $this->showForm(sprintf(_('Invalid tag: "%s".'), $tag)); - return; + $tags = preg_split('/[\s,]+/', $tagstring); + + foreach ($tags as &$tag) { + $private = @$tag[0] === '.'; + + $tag = common_canonical_tag($tag); + if (!common_valid_profile_tag($tag)) { + // TRANS: Validation error in form for profile settings. + // TRANS: %s is an invalid tag. + $this->showForm(sprintf(_('Invalid tag: "%s".'), $tag)); + return; + } + + $tag_priv[$tag] = $private; } } @@ -333,11 +358,15 @@ class ProfilesettingsAction extends SettingsAction } // XXX: XOR - if ($user->autosubscribe ^ $autosubscribe) { + if (($user->autosubscribe ^ $autosubscribe) || + ($user->private_stream ^ $private_stream) || + ($user->subscribe_policy != $subscribe_policy)) { $original = clone($user); - $user->autosubscribe = $autosubscribe; + $user->autosubscribe = $autosubscribe; + $user->private_stream = $private_stream; + $user->subscribe_policy = $subscribe_policy; $result = $user->update($original); @@ -345,7 +374,7 @@ class ProfilesettingsAction extends SettingsAction common_log_db_error($user, 'UPDATE', __FILE__); // TRANS: Server error thrown when user profile settings could not be updated to // TRANS: automatically subscribe to any subscriber. - $this->serverError(_('Could not update user for autosubscribe.')); + $this->serverError(_('Could not update user for autosubscribe or subscribe_policy.')); return; } } @@ -421,7 +450,7 @@ class ProfilesettingsAction extends SettingsAction } // Set the user tags - $result = $user->setSelfTags($tags); + $result = $user->setSelfTags($tags, $tag_priv); if (!$result) { // TRANS: Server error thrown when user profile settings tags could not be saved. diff --git a/actions/profiletagbyid.php b/actions/profiletagbyid.php new file mode 100644 index 0000000000..396e634aba --- /dev/null +++ b/actions/profiletagbyid.php @@ -0,0 +1,92 @@ +. + * + * @category Peopletag + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +class ProfiletagbyidAction extends Action +{ + /** peopletag we're viewing. */ + var $peopletag = null; + + /** + * Is this page read-only? + * + * @return boolean true + */ + function isReadOnly($args) + { + return true; + } + + function prepare($args) + { + parent::prepare($args); + + $id = $this->arg('id'); + $tagger_id = $this->arg('tagger_id'); + + if (!$id) { + // TRANS: Client error displayed trying to perform an action without providing an ID. + $this->clientError(_('No ID.')); + return false; + } + + common_debug("Peopletag id $id by user id $tagger_id"); + + $this->peopletag = Profile_list::staticGet('id', $id); + + if (!$this->peopletag) { + // TRANS: Client error displayed trying to reference a non-existing list. + $this->clientError(_('No such list.'), 404); + return false; + } + + $user = User::staticGet('id', $tagger_id); + if (!$user) { + // remote peopletag, permanently redirect + common_redirect($this->peopletag->permalink(), 301); + } + + return true; + } + + /** + * Handle the request + * + * Shows a profile for the group, some controls, and a list of + * group notices. + * + * @return void + */ + function handle($args) + { + common_redirect($this->peopletag->homeUrl(), 303); + } +} diff --git a/actions/public.php b/actions/public.php index b029407250..90e0e6e259 100644 --- a/actions/public.php +++ b/actions/public.php @@ -59,6 +59,7 @@ class PublicAction extends Action var $page = null; var $notice; + var $userProfile = null; function isReadOnly($args) { @@ -85,8 +86,12 @@ class PublicAction extends Action common_set_returnto($this->selfUrl()); - $this->notice = Notice::publicStream(($this->page-1)*NOTICES_PER_PAGE, - NOTICES_PER_PAGE + 1); + $this->userProfile = Profile::current(); + + $stream = new ThreadingPublicNoticeStream($this->userProfile); + + $this->notice = $stream->getNotices(($this->page-1)*NOTICES_PER_PAGE, + NOTICES_PER_PAGE + 1); if (!$this->notice) { // TRANS: Server error displayed when a public timeline cannot be retrieved. @@ -203,7 +208,7 @@ class PublicAction extends Action */ function showContent() { - $nl = new ThreadedNoticeList($this->notice, $this); + $nl = new ThreadedNoticeList($this->notice, $this, $this->userProfile); $cnt = $nl->show(); @@ -217,12 +222,12 @@ class PublicAction extends Action function showSections() { - // $top = new TopPostersSection($this); - // $top->show(); + $ibs = new InviteButtonSection($this); + $ibs->show(); $pop = new PopularNoticeSection($this); $pop->show(); - $gbp = new GroupsByMembersSection($this); - $gbp->show(); + $cloud = new PublicTagCloudSection($this); + $cloud->show(); $feat = new FeaturedUsersSection($this); $feat->show(); } @@ -247,3 +252,11 @@ class PublicAction extends Action $this->elementEnd('div'); } } + +class ThreadingPublicNoticeStream extends ThreadingNoticeStream +{ + function __construct($profile) + { + parent::__construct(new PublicNoticeStream($profile)); + } +} diff --git a/actions/publicpeopletagcloud.php b/actions/publicpeopletagcloud.php new file mode 100644 index 0000000000..cb65bbb163 --- /dev/null +++ b/actions/publicpeopletagcloud.php @@ -0,0 +1,182 @@ +. + * + * @category Public + * @package StatusNet + * @author Mike Cochrane + * @author Evan Prodromou + * @copyright 2008 Mike Cochrane + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +define('TAGS_PER_PAGE', 100); + +/** + * Public tag cloud for notices + * + * @category Personal + * @package StatusNet + * @author Mike Cochrane + * @author Evan Prodromou + * @copyright 2008 Mike Cochrane + * @copyright 2008-2009 StatusNet, Inc. + * @link http://status.net/ + */ +class PublicpeopletagcloudAction extends Action +{ + function isReadOnly($args) + { + return true; + } + + function title() + { + // TRANS: Title for page with public list cloud. + return _('Public list cloud'); + } + + function showPageNotice() + { + $this->element('p', 'instructions', + // TRANS: Page notice for page with public list cloud. + // TRANS: %s is a StatusNet sitename. + sprintf(_('These are largest lists on %s'), + common_config('site', 'name'))); + } + + function showEmptyList() + { + // TRANS: Empty list message on page with public list cloud. + // TRANS: This message contains Markdown links in the form [description](link). + $message = _('No one has [listed](%%doc.tags%%) anyone yet.') . ' '; + + if (common_logged_in()) { + // TRANS: Additional empty list message on page with public list cloud for logged in users. + $message .= _('Be the first to list someone!'); + } + else { + // TRANS: Additional empty list message on page with public list cloud for anonymous users. + // TRANS: This message contains Markdown links in the form [description](link). + $message .= _('Why not [register an account](%%action.register%%) and be the first to list someone!'); + } + + $this->elementStart('div', 'guide'); + $this->raw(common_markup_to_html($message)); + $this->elementEnd('div'); + } + + function showLocalNav() + { + $nav = new PublicGroupNav($this); + $nav->show(); + } + + function handle($args) + { + parent::handle($args); + $this->showPage(); + } + + function showContent() + { + // XXX: cache this + + $tags = new Profile_tag(); + $plist = new Profile_list(); + $plist->private = false; + + $tags->joinAdd($plist); + $tags->selectAdd(); + $tags->selectAdd('profile_tag.tag'); + $tags->selectAdd('count(profile_tag.tag) as weight'); + $tags->groupBy('profile_tag.tag'); + $tags->orderBy('weight DESC'); + + $tags->limit(TAGS_PER_PAGE); + + $cnt = $tags->find(); + + if ($cnt > 0) { + $this->elementStart('div', array('id' => 'tagcloud', + 'class' => 'section')); + + $tw = array(); + $sum = 0; + while ($tags->fetch()) { + $tw[$tags->tag] = $tags->weight; + $sum += $tags->weight; + } + + ksort($tw); + + $this->elementStart('dl'); + // TRANS: DT element on on page with public list cloud. + $this->element('dt', null, _('List cloud')); + $this->elementStart('dd'); + $this->elementStart('ul', 'tags xoxo tag-cloud'); + foreach ($tw as $tag => $weight) { + if ($sum) { + $weightedSum = $weight/$sum; + } else { + $weightedSum = 0.5; + } + $this->showTag($tag, $weight, $weightedSum); + } + $this->elementEnd('ul'); + $this->elementEnd('dd'); + $this->elementEnd('dl'); + $this->elementEnd('div'); + } else { + $this->showEmptyList(); + } + } + + function showTag($tag, $weight, $relative) + { + if ($relative > 0.1) { + $rel = 'tag-cloud-7'; + } else if ($relative > 0.05) { + $rel = 'tag-cloud-6'; + } else if ($relative > 0.02) { + $rel = 'tag-cloud-5'; + } else if ($relative > 0.01) { + $rel = 'tag-cloud-4'; + } else if ($relative > 0.005) { + $rel = 'tag-cloud-3'; + } else if ($relative > 0.002) { + $rel = 'tag-cloud-2'; + } else { + $rel = 'tag-cloud-1'; + } + + $this->elementStart('li', $rel); + + // TRANS: Link title for number of listed people. %d is the number of listed people. + $title = sprintf(_m('1 person listed','%d people listed',$weight),$weight); + $this->element('a', array('href' => common_local_url('peopletag', array('tag' => $tag)), + 'title' => $title), $tag); + $this->elementEnd('li'); + } +} diff --git a/actions/recoverpassword.php b/actions/recoverpassword.php index 71f673bd3b..47a947dc0c 100644 --- a/actions/recoverpassword.php +++ b/actions/recoverpassword.php @@ -440,4 +440,18 @@ class RecoverpasswordAction extends Action $this->success = true; $this->showPage(); } + + /** + * A local menu + * + * Shows different login/register actions. + * + * @return void + */ + + function showLocalNav() + { + $nav = new LoginGroupNav($this); + $nav->show(); + } } diff --git a/actions/register.php b/actions/register.php index 7b3c075156..3fe5e20a13 100644 --- a/actions/register.php +++ b/actions/register.php @@ -160,6 +160,7 @@ class RegisterAction extends Action if (Event::handle('StartRegistrationTry', array($this))) { $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->showForm(_('There was a problem with your session token. '. 'Try again, please.')); return; @@ -211,6 +212,7 @@ class RegisterAction extends Action // TRANS: Form validation error displayed when trying to register with an invalid nickname. $this->showForm(_('Not a valid nickname.')); } else if ($this->emailExists($email)) { + // TRANS: Form validation error displayed when trying to register with an already registered e-mail address. $this->showForm(_('Email address already exists.')); } else if (!is_null($homepage) && (strlen($homepage) > 0) && !Validate::uri($homepage, @@ -268,11 +270,11 @@ class RegisterAction extends Action common_rememberme($user); } - Event::handle('EndRegistrationTry', array($this)); - // Re-init language env in case it changed (not yet, but soon) common_init_language(); + Event::handle('EndRegistrationTry', array($this)); + $this->showSuccess(); } else { // TRANS: Form validation error displayed when trying to register with an invalid username or password. @@ -531,7 +533,7 @@ class RegisterAction extends Action $this->elementEnd('li'); } $this->elementEnd('ul'); - // TRANS: Field label on account registration page. + // TRANS: Button text to register a user on account registration page. $this->submit('submit', _m('BUTTON','Register')); $this->elementEnd('fieldset'); $this->elementEnd('form'); @@ -603,48 +605,52 @@ class RegisterAction extends Action */ function showSuccessContent() { - $nickname = $this->arg('nickname'); + if (Event::handle('StartRegisterSuccess', array($this))) { + $nickname = $this->arg('nickname'); - $profileurl = common_local_url('showstream', - array('nickname' => $nickname)); + $profileurl = common_local_url('showstream', + array('nickname' => $nickname)); - $this->elementStart('div', 'success'); - // TRANS: Text displayed after successful account registration. - // TRANS: %1$s is the registered nickname, %2$s is the profile URL. - // TRANS: This message contains Markdown links in the form [link text](link) - // TRANS: and variables in the form %%%%variable%%%%. Please mind the syntax. - $instr = sprintf(_('Congratulations, %1$s! And welcome to %%%%site.name%%%%. '. - 'From here, you may want to...'. "\n\n" . - '* Go to [your profile](%2$s) '. - 'and post your first message.' . "\n" . - '* Add a [Jabber/GTalk address]'. - '(%%%%action.imsettings%%%%) '. - 'so you can send notices '. - 'through instant messages.' . "\n" . - '* [Search for people](%%%%action.peoplesearch%%%%) '. - 'that you may know or '. - 'that share your interests. ' . "\n" . - '* Update your [profile settings]'. - '(%%%%action.profilesettings%%%%)'. - ' to tell others more about you. ' . "\n" . - '* Read over the [online docs](%%%%doc.help%%%%)'. - ' for features you may have missed. ' . "\n\n" . - 'Thanks for signing up and we hope '. - 'you enjoy using this service.'), - $nickname, $profileurl); + $this->elementStart('div', 'success'); + // TRANS: Text displayed after successful account registration. + // TRANS: %1$s is the registered nickname, %2$s is the profile URL. + // TRANS: This message contains Markdown links in the form [link text](link) + // TRANS: and variables in the form %%%%variable%%%%. Please mind the syntax. + $instr = sprintf(_('Congratulations, %1$s! And welcome to %%%%site.name%%%%. '. + 'From here, you may want to...'. "\n\n" . + '* Go to [your profile](%2$s) '. + 'and post your first message.' . "\n" . + '* Add a [Jabber/GTalk address]'. + '(%%%%action.imsettings%%%%) '. + 'so you can send notices '. + 'through instant messages.' . "\n" . + '* [Search for people](%%%%action.peoplesearch%%%%) '. + 'that you may know or '. + 'that share your interests. ' . "\n" . + '* Update your [profile settings]'. + '(%%%%action.profilesettings%%%%)'. + ' to tell others more about you. ' . "\n" . + '* Read over the [online docs](%%%%doc.help%%%%)'. + ' for features you may have missed. ' . "\n\n" . + 'Thanks for signing up and we hope '. + 'you enjoy using this service.'), + $nickname, $profileurl); - $this->raw(common_markup_to_html($instr)); + $this->raw(common_markup_to_html($instr)); - $have_email = $this->trimmed('email'); - if ($have_email) { - // TRANS: Instruction text on how to deal with the e-mail address confirmation e-mail. - $emailinstr = _('(You should receive a message by email '. - 'momentarily, with ' . - 'instructions on how to confirm '. - 'your email address.)'); - $this->raw(common_markup_to_html($emailinstr)); + $have_email = $this->trimmed('email'); + if ($have_email) { + // TRANS: Instruction text on how to deal with the e-mail address confirmation e-mail. + $emailinstr = _('(You should receive a message by email '. + 'momentarily, with ' . + 'instructions on how to confirm '. + 'your email address.)'); + $this->raw(common_markup_to_html($emailinstr)); + } + $this->elementEnd('div'); + + Event::handle('EndRegisterSuccess', array($this)); } - $this->elementEnd('div'); } /** @@ -654,15 +660,23 @@ class RegisterAction extends Action */ function showLocalNav() { - $nav = new LoginGroupNav($this); - $nav->show(); - } - - function showNoticeForm() - { + if (common_logged_in()) { + parent::showLocalNav(); + } else { + $nav = new LoginGroupNav($this); + $nav->show(); + } } + /** + * Show a bit of login context + * + * @return nothing + */ function showProfileBlock() { + if (common_logged_in()) { + parent::showProfileBlock(); + } } } diff --git a/actions/remotesubscribe.php b/actions/remotesubscribe.php index 66cee65cab..16945122f4 100644 --- a/actions/remotesubscribe.php +++ b/actions/remotesubscribe.php @@ -74,6 +74,7 @@ class RemotesubscribeAction extends Action /* Use a session token for CSRF protection. */ $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->showForm(_('There was a problem with your session token. '. 'Try again, please.')); return; @@ -204,8 +205,8 @@ class RemotesubscribeAction extends Action $profile = $user->getProfile(); if (!$profile) { common_log_db_error($user, 'SELECT', __FILE__); - // TRANS: Server error displayed on page for remote subscribe when user does not have a matching profile. - $this->serverError(_('User without matching profile.')); + // TRANS: Error message displayed when referring to a user without a profile. + $this->serverError(_('User has no profile.')); return; } diff --git a/actions/removepeopletag.php b/actions/removepeopletag.php new file mode 100644 index 0000000000..6943f35ed1 --- /dev/null +++ b/actions/removepeopletag.php @@ -0,0 +1,180 @@ +. + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/togglepeopletag.php'; + +/** + * 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 Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ +class RemovepeopletagAction extends Action +{ + var $user; + var $tagged; + var $peopletag; + + /** + * Check pre-requisites and instantiate attributes + * + * @param Array $args array of arguments (URL, GET, POST) + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + // CSRF protection + + $token = $this->trimmed('token'); + + if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. + $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)) { + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. + $this->clientError(_('Not logged in.')); + return false; + } + + // Profile to subscribe to + + $tagged_id = $this->arg('tagged'); + + $this->tagged = Profile::staticGet('id', $tagged_id); + + if (empty($this->tagged)) { + // TRANS: Client error displayed when referring to a non-existing profile. + $this->clientError(_('No such profile.')); + return false; + } + + $id = $this->arg('peopletag_id'); + $this->peopletag = Profile_list::staticGet('id', $id); + + if (empty($this->peopletag)) { + // TRANS: Client error displayed trying to reference a non-existing list. + $this->clientError(_('No such list.')); + return false; + } + + // OMB 0.1 doesn't have a mechanism for local-server- + // originated tag. + + $omb01 = Remote_profile::staticGet('id', $tagged_id); + + if (!empty($omb01)) { + // TRANS: Client error displayed when trying to (un)list an OMB 0.1 remote profile. + $this->clientError(_('You cannot (un)list 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 + + $ptag = Profile_tag::unTag($this->user->id, $this->tagged->id, + $this->peopletag->tag); + + if (!$ptag) { + $user = User::staticGet('id', $this->tagged->id); + if ($user) { + $this->clientError( + // TRANS: Client error displayed when an unknown error occurs while delisting a user. + // TRANS: %s is a username. + sprintf(_('There was an unexpected error while delisting %s.'), + $user->nickname)); + } else { + // TRANS: Client error displayed when an unknown error occurs while listing a user. + // TRANS: %s is a profile URL. + $this->clientError(sprintf(_('There was a problem listing %s. ' . + 'The remote server is probably not responding correctly, ' . + 'please try retrying later.'), $this->profile->profileurl)); + } + return false; + } + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + // TRANS: Title after removing a user from a list. + $this->element('title', null, _('Unlisted')); + $this->elementEnd('head'); + $this->elementStart('body'); + $unsubscribe = new TagButton($this, $this->tagged, $this->peopletag); + $unsubscribe->show(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + $url = common_local_url('subscriptions', + array('nickname' => $this->user->nickname)); + common_redirect($url, 303); + } + } +} diff --git a/actions/repeat.php b/actions/repeat.php index 869c2ddd4e..333e1cd02e 100644 --- a/actions/repeat.php +++ b/actions/repeat.php @@ -73,27 +73,14 @@ class RepeatAction extends Action return false; } - if ($this->user->id == $this->notice->profile_id) { - // TRANS: Client error displayed when trying to repeat an own notice. - $this->clientError(_('You cannot repeat your own notice.')); - return false; - } - $token = $this->trimmed('token-'.$id); if (empty($token) || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token. Try again, please.')); return false; } - $profile = $this->user->getProfile(); - - if ($profile->hasRepeated($id)) { - // TRANS: Client error displayed when trying to repeat an already repeated notice. - $this->clientError(_('You already repeated that notice.')); - return false; - } - return true; } diff --git a/actions/replies.php b/actions/replies.php index 54109b7b9f..7ae2d0eb76 100644 --- a/actions/replies.php +++ b/actions/replies.php @@ -76,7 +76,7 @@ class RepliesAction extends OwnerDesignAction $profile = $this->user->getProfile(); if (!$profile) { - // TRANS: Server error displayed when trying to reply to a user without a profile. + // TRANS: Error message displayed when referring to a user without a profile. $this->serverError(_('User has no profile.')); return false; } diff --git a/actions/selftag.php b/actions/selftag.php new file mode 100644 index 0000000000..b886c3d9f7 --- /dev/null +++ b/actions/selftag.php @@ -0,0 +1,204 @@ +. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @author Zach Copley + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * This class outputs a paginated list of profiles self-tagged with a given tag + * + * @category Output + * @package StatusNet + * @author Evan Prodromou + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see Action + */ +class SelftagAction extends Action +{ + var $tag = null; + var $page = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + + $this->tag = $this->trimmed('tag'); + + if (!common_valid_profile_tag($this->tag)) { + // TRANS: Client error displayed when trying to list a profile with an invalid list. + // TRANS: %s is the invalid list name. + $this->clientError(sprintf(_('Not a valid list: %s.'), + $this->tag)); + return; + } + + $this->page = ($this->arg('page')) ? $this->arg('page') : 1; + + common_set_returnto($this->selfUrl()); + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return boolean is read only action? + */ + function handle($argarray) + { + parent::handle($argarray); + $this->showPage(); + } + + /** + * Whips up a query to get a list of profiles based on the provided + * people tag and page, initalizes a ProfileList widget, and displays + * it to the user. + * + * @return nothing + */ + function showContent() + { + $profile = new Profile(); + + $offset = ($this->page - 1) * PROFILES_PER_PAGE; + $limit = PROFILES_PER_PAGE + 1; + + if (common_config('db', 'type') == 'pgsql') { + $lim = ' LIMIT ' . $limit . ' OFFSET ' . $offset; + } else { + $lim = ' LIMIT ' . $offset . ', ' . $limit; + } + + // XXX: memcached this + + $qry = 'SELECT profile.* ' . + 'FROM profile JOIN ( profile_tag, profile_list ) ' . + 'ON profile.id = profile_tag.tagger ' . + 'AND profile_tag.tagger = profile_list.tagger ' . + 'AND profile_list.tag = profile_tag.tag ' . + 'WHERE profile_tag.tagger = profile_tag.tagged ' . + "AND profile_tag.tag = '%s' "; + + $user = common_current_user(); + if (empty($user)) { + $qry .= 'AND profile_list.private = false '; + } else { + $qry .= 'AND (profile_list.tagger = ' . $user->id . + ' OR profile_list.private = false) '; + } + + $qry .= 'ORDER BY profile_tag.modified DESC%s'; + + $profile->query(sprintf($qry, $this->tag, $lim)); + + $ptl = new SelfTagProfileList($profile, $this); // pass the ammunition + $cnt = $ptl->show(); + + $this->pagination($this->page > 1, + $cnt > PROFILES_PER_PAGE, + $this->page, + 'selftag', + array('tag' => $this->tag)); + } + + /** + * Returns the page title + * + * @return string page title + */ + function title() + { + // TRANS: Page title for page showing self tags. + // TRANS: %1$s is a tag, %2$d is a page number. + return sprintf(_('Users self-tagged with %1$s, page %2$d'), + $this->tag, $this->page); + } +} + +class SelfTagProfileList extends ProfileList +{ + function newListItem($profile) + { + return new SelfTagProfileListItem($profile, $this->action); + } +} + +class SelfTagProfileListItem extends ProfileListItem +{ + function linkAttributes() + { + $aAttrs = parent::linkAttributes(); + + if (common_config('nofollow', 'selftag')) { + $aAttrs['rel'] .= ' nofollow'; + } + + return $aAttrs; + } + + function homepageAttributes() + { + $aAttrs = parent::linkAttributes(); + + if (common_config('nofollow', 'selftag')) { + $aAttrs['rel'] = 'nofollow'; + } + + return $aAttrs; + } + + function showTags() + { + $selftags = new SelfTagsWidget($this->out, $this->profile, $this->profile); + $selftags->show(); + + $user = common_current_user(); + + if (!empty($user) && $user->id != $this->profile->id && + $user->getProfile()->canTag($this->profile)) { + $yourtags = new PeopleTagsWidget($this->out, $user, $this->profile); + $yourtags->show(); + } + } +} diff --git a/actions/showapplication.php b/actions/showapplication.php index 38e6f1953e..c9cdbf1848 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -113,6 +113,7 @@ class ShowApplicationAction extends OwnerDesignAction // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token.')); return; } diff --git a/actions/showgroup.php b/actions/showgroup.php index d77fbeed71..5bb90e86dd 100644 --- a/actions/showgroup.php +++ b/actions/showgroup.php @@ -50,6 +50,8 @@ class ShowgroupAction extends GroupDesignAction { /** page we're viewing. */ var $page = null; + var $userProfile = null; + var $notice = null; /** * Is this page read-only? @@ -144,6 +146,13 @@ class ShowgroupAction extends GroupDesignAction return false; } + $this->userProfile = Profile::current(); + + $stream = new ThreadingGroupNoticeStream($this->group, $this->userProfile); + + $this->notice = $stream->getNotices(($this->page-1)*NOTICES_PER_PAGE, + NOTICES_PER_PAGE + 1); + common_set_returnto($this->selfUrl()); return true; @@ -190,10 +199,7 @@ class ShowgroupAction extends GroupDesignAction */ function showGroupNotices() { - $notice = $this->group->getNotices(($this->page-1)*NOTICES_PER_PAGE, - NOTICES_PER_PAGE + 1); - - $nl = new ThreadedNoticeList($notice, $this); + $nl = new ThreadedNoticeList($this->notice, $this, $this->userProfile); $cnt = $nl->show(); $this->pagination($this->page > 1, @@ -365,6 +371,18 @@ class ShowgroupAction extends GroupDesignAction $this->raw(common_markup_to_html($m)); $this->elementEnd('div'); } + + function noticeFormOptions() + { + $options = parent::noticeFormOptions(); + $cur = common_current_user(); + + if (!empty($cur) && $cur->isMember($this->group)) { + $options['to_group'] = $this->group; + } + + return $options; + } } class GroupAdminSection extends ProfileSection @@ -420,3 +438,11 @@ class GroupMembersMiniListItem extends ProfileMiniListItem return $aAttrs; } } + +class ThreadingGroupNoticeStream extends ThreadingNoticeStream +{ + function __construct($group, $profile) + { + parent::__construct(new GroupNoticeStream($group, $profile)); + } +} \ No newline at end of file diff --git a/actions/shownotice.php b/actions/shownotice.php index f6074faddc..ea9041efb5 100644 --- a/actions/shownotice.php +++ b/actions/shownotice.php @@ -77,21 +77,19 @@ class ShownoticeAction extends OwnerDesignAction StatusNet::setApi(true); } - $id = $this->arg('notice'); + $this->notice = $this->getNotice(); - $this->notice = Notice::staticGet($id); + $cur = common_current_user(); - if (empty($this->notice)) { - // Did we used to have it, and it got deleted? - $deleted = Deleted_notice::staticGet($id); - if (!empty($deleted)) { - // TRANS: Client error displayed trying to show a deleted notice. - $this->clientError(_('Notice deleted.'), 410); - } else { - // TRANS: Client error displayed trying to show a non-existing notice. - $this->clientError(_('No such notice.'), 404); - } - return false; + if (!empty($cur)) { + $curProfile = $cur->getProfile(); + } else { + $curProfile = null; + } + + if (!$this->notice->inScope($curProfile)) { + // TRANS: Client exception thrown when trying a view a notice the user has no access to. + throw new ClientException(_('Not available.'), 403); } $this->profile = $this->notice->getProfile(); @@ -109,6 +107,33 @@ class ShownoticeAction extends OwnerDesignAction return true; } + /** + * Fetch the notice to show. This may be overridden by child classes to + * customize what we fetch without duplicating all of the prepare() method. + * + * @return Notice + */ + function getNotice() + { + $id = $this->arg('notice'); + + $notice = Notice::staticGet('id', $id); + + if (empty($notice)) { + // Did we used to have it, and it got deleted? + $deleted = Deleted_notice::staticGet($id); + if (!empty($deleted)) { + // TRANS: Client error displayed trying to show a deleted notice. + $this->clientError(_('Notice deleted.'), 410); + } else { + // TRANS: Client error displayed trying to show a non-existing notice. + $this->clientError(_('No such notice.'), 404); + } + return false; + } + return $notice; + } + /** * Is this action read-only? * @@ -208,15 +233,6 @@ class ShownoticeAction extends OwnerDesignAction } } - /** - * Don't show local navigation - * - * @return void - */ - function showLocalNavBlock() - { - } - /** * Fill the content area of the page * @@ -317,58 +333,8 @@ class ShownoticeAction extends OwnerDesignAction // @todo FIXME: Class documentation missing. class SingleNoticeItem extends DoFollowListItem { - /** - * Recipe function for displaying a single notice. - * - * We overload to show attachments. - * - * @return void - */ - function show() + function avatarSize() { - $this->showStart(); - if (Event::handle('StartShowNoticeItem', array($this))) { - $this->showNotice(); - $this->showNoticeAttachments(); - $this->showNoticeInfo(); - $this->showNoticeOptions(); - Event::handle('EndShowNoticeItem', array($this)); - } - - $this->showEnd(); - } - - /** - * For our zoomed-in special case we'll use a fuller list - * for the attachment info. - */ - function showNoticeAttachments() { - $al = new AttachmentList($this->notice, $this->out); - $al->show(); - } - - /** - * show the avatar of the notice's author - * - * We use the larger size for single notice page. - * - * @return void - */ - function showAvatar() - { - $avatar_size = AVATAR_PROFILE_SIZE; - - $avatar = $this->profile->getAvatar($avatar_size); - - $this->out->element('img', array('src' => ($avatar) ? - $avatar->displayUrl() : - Avatar::defaultImage($avatar_size), - 'class' => 'avatar photo', - 'width' => $avatar_size, - 'height' => $avatar_size, - 'alt' => - ($this->profile->fullname) ? - $this->profile->fullname : - $this->profile->nickname)); + return AVATAR_STREAM_SIZE; } } diff --git a/actions/showprofiletag.php b/actions/showprofiletag.php new file mode 100644 index 0000000000..d1b0768d23 --- /dev/null +++ b/actions/showprofiletag.php @@ -0,0 +1,378 @@ +. + * + * @category Actions + * @package Actions + * @license GNU Affero General Public License http://www.gnu.org/licenses/ + * @link http://status.net + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/profileminilist.php'; +require_once INSTALLDIR.'/lib/peopletaglist.php'; +require_once INSTALLDIR.'/lib/noticelist.php'; +require_once INSTALLDIR.'/lib/feedlist.php'; + +class ShowprofiletagAction extends Action +{ + var $notice, $tagger, $peopletag, $userProfile; + + function isReadOnly($args) + { + return true; + } + + function prepare($args) + { + parent::prepare($args); + + $tagger_arg = $this->arg('tagger'); + $tag_arg = $this->arg('tag'); + $tagger = common_canonical_nickname($tagger_arg); + $tag = common_canonical_tag($tag_arg); + + // Permanent redirect on non-canonical nickname + + if ($tagger_arg != $tagger || $tag_arg != $tag) { + $args = array('tagger' => $nickname, 'tag' => $tag); + if ($this->page != 1) { + $args['page'] = $this->page; + } + common_redirect(common_local_url('showprofiletag', $args), 301); + return false; + } + + if (!$tagger) { + // TRANS: Client error displayed when a tagger is expected but not provided. + $this->clientError(_('No tagger.'), 404); + return false; + } + + $user = User::staticGet('nickname', $tagger); + + if (!$user) { + // TRANS: Client error displayed trying to perform an action related to a non-existing user. + $this->clientError(_('No such user.'), 404); + return false; + } + + $this->tagger = $user->getProfile(); + $this->peopletag = Profile_list::pkeyGet(array('tagger' => $user->id, 'tag' => $tag)); + + $current = common_current_user(); + $can_see = !empty($this->peopletag) && (!$this->peopletag->private || + ($this->peopletag->private && $this->peopletag->tagger === $current->id)); + + if (!$can_see) { + // TRANS: Client error displayed trying to reference a non-existing list. + $this->clientError(_('No such list.'), 404); + return false; + } + + $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; + $this->userProfile = Profile::current(); + + $stream = new PeopletagNoticeStream($this->peopletag, $this->userProfile); + + $this->notice = $stream->getNotices(($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); + } + + return true; + } + + function handle($args) + { + parent::handle($args); + + if (!$this->peopletag) { + // TRANS: Client error displayed trying to perform an action related to a non-existing user. + $this->clientError(_('No such user.')); + return; + } + + $this->showPage(); + } + + function title() + { + if ($this->page > 1) { + if($this->peopletag->private) { + // TRANS: Title for private list timeline. + // TRANS: %1$s is a list, %2$s is a page number. + return sprintf(_('Private timeline for %1$s list by you, page %2$d'), + $this->peopletag->tag, $this->page); + } + + $current = common_current_user(); + if (!empty($current) && $current->id == $this->peopletag->tagger) { + // TRANS: Title for public list timeline where the viewer is the tagger. + // TRANS: %1$s is a list, %2$s is a page number. + return sprintf(_('Timeline for %1$s list by you, page %2$d'), + $this->peopletag->tag, $this->page); + } + + // TRANS: Title for private list timeline. + // TRANS: %1$s is a list, %2$s is the tagger's nickname, %3$d is a page number. + return sprintf(_('Timeline for %1$s list by %2$s, page %3$d'), + $this->peopletag->tag, + $this->tagger->nickname, + $this->page + ); + } else { + if($this->peopletag->private) { + // TRANS: Title for private list timeline. + // TRANS: %s is a list. + return sprintf(_('Private timeline of %s list by you'), + $this->peopletag->tag); + } + + $current = common_current_user(); + if (!empty($current) && $current->id == $this->peopletag->tagger) { + // TRANS: Title for public list timeline where the viewer is the tagger. + // TRANS: %s is a list. + return sprintf(_('Timeline for %s list by you'), + $this->peopletag->tag); + } + + // TRANS: Title for private list timeline. + // TRANS: %1$s is a list, %2$s is the tagger's nickname. + return sprintf(_('Timeline for %1$s list by %2$s'), + $this->peopletag->tag, + $this->tagger->nickname + ); + } + } + + function getFeeds() + { + #XXX: make these actually work + return array(new Feed(Feed::RSS2, + common_local_url( + 'ApiTimelineList', array( + 'user' => $this->tagger->id, + 'id' => $this->peopletag->id, + 'format' => 'rss' + ) + ), + // TRANS: Feed title. + // TRANS: %s is tagger's nickname. + sprintf(_('Feed for friends of %s (RSS 2.0)'), $this->tagger->nickname)), + new Feed(Feed::ATOM, + common_local_url( + 'ApiTimelineList', array( + 'user' => $this->tagger->id, + 'id' => $this->peopletag->id, + 'format' => 'atom' + ) + ), + // TRANS: Feed title. + // TRANS: %1$s is a list, %2$s is tagger's nickname. + sprintf(_('Feed for %1$s list by %2$s (Atom)'), + $this->peopletag->tag, $this->tagger->nickname + ) + ) + ); + } + + function showObjectNav() + { + $nav = new PeopletagGroupNav($this); + $nav->show(); + } + + function showEmptyListMessage() + { + // TRANS: Empty list message for list timeline. + // TRANS: %1$s is a list, %2$s is a tagger's nickname. + $message = sprintf(_('This is the timeline for %1$s list by %2$s but no one has posted anything yet.'), + $this->peopletag->tag, + $this->tagger->nickname) . ' '; + + if (common_logged_in()) { + $current_user = common_current_user(); + if ($this->tagger->id == $current_user->id) { + // TRANS: Additional empty list message for list timeline for currently logged in user tagged tags. + $message .= _('Try tagging more people.'); + } + } else { + // TRANS: Additional empty list message for list timeline. + // TRANS: This message contains Markdown links in the form [description](link). + $message .= _('Why not [register an account](%%%%action.register%%%%) and start following this timeline!'); + } + + $this->elementStart('div', 'guide'); + $this->raw(common_markup_to_html($message)); + $this->elementEnd('div'); + } + + function showContent() + { + $this->showPeopletag(); + $this->showNotices(); + } + + function showPeopletag() + { + $cur = common_current_user(); + $tag = new Peopletag($this->peopletag, $cur, $this); + $tag->show(); + } + + function showNotices() + { + if (Event::handle('StartShowProfileTagContent', array($this))) { + $nl = new NoticeList($this->notice, $this); + + $cnt = $nl->show(); + + if (0 == $cnt) { + $this->showEmptyListMessage(); + } + + $this->pagination($this->page > 1, + $cnt > NOTICES_PER_PAGE, + $this->page, + 'showprofiletag', + array('tag' => $this->peopletag->tag, + 'tagger' => $this->tagger->nickname) + ); + + Event::handle('EndShowProfileTagContent', array($this)); + } + } + + function showSections() + { + $this->showTagged(); + if (!$this->peopletag->private) { + $this->showSubscribers(); + } + # $this->showStatistics(); + } + + function showPageTitle() + { + $this->element('h1', null, $this->title()); + } + + function showTagged() + { + $profile = $this->peopletag->getTagged(0, PROFILES_PER_MINILIST + 1); + + $this->elementStart('div', array('id' => 'entity_tagged', + 'class' => 'section')); + if (Event::handle('StartShowTaggedProfilesMiniList', array($this))) { + $title = ''; + + // TRANS: Header on show list page. + $this->element('h2', null, _('Listed')); + + $cnt = 0; + + if (!empty($profile)) { + $pml = new ProfileMiniList($profile, $this); + $cnt = $pml->show(); + if ($cnt == 0) { + // TRANS: Content of "Listed" page if there are no listed users. + $this->element('p', null, _('(None)')); + } + } + + if ($cnt > PROFILES_PER_MINILIST) { + $this->elementStart('p'); + $this->element('a', array('href' => common_local_url('taggedprofiles', + array('nickname' => $this->tagger->nickname, + 'profiletag' => $this->peopletag->tag)), + 'class' => 'more'), + // TRANS: Link for more "People in list x by a user" + // TRANS: if there are more than the mini list's maximum. + _('Show all')); + $this->elementEnd('p'); + } + + Event::handle('EndShowTaggedProfilesMiniList', array($this)); + } + $this->elementEnd('div'); + } + + function showSubscribers() + { + $profile = $this->peopletag->getSubscribers(0, PROFILES_PER_MINILIST + 1); + + $this->elementStart('div', array('id' => 'entity_subscribers', + 'class' => 'section')); + if (Event::handle('StartShowProfileTagSubscribersMiniList', array($this))) { + // TRANS: Header for tag subscribers. + $this->element('h2', null, _('Subscribers')); + + $cnt = 0; + + if (!empty($profile)) { + $pml = new ProfileMiniList($profile, $this); + $cnt = $pml->show(); + if ($cnt == 0) { + // TRANS: Content of "People following tag x" if there are no subscribed users. + $this->element('p', null, _('(None)')); + } + } + + if ($cnt > PROFILES_PER_MINILIST) { + $this->elementStart('p'); + $this->element('a', array('href' => common_local_url('profiletagsubscribers', + array('nickname' => $this->tagger->nickname, + 'profiletag' => $this->peopletag->tag)), + 'class' => 'more'), + // TRANS: Link for more "People following tag x" + // TRANS: if there are more than the mini list's maximum. + _('All subscribers')); + $this->elementEnd('p'); + } + + Event::handle('EndShowProfileTagSubscribersMiniList', array($this)); + } + $this->elementEnd('div'); + } +} + +class Peopletag extends PeopletagListItem +{ + function showStart() + { + $mode = $this->peopletag->private ? 'private' : 'public'; + $this->out->elementStart('div', array('class' => 'hentry peopletag peopletag-profile mode-'.$mode, + 'id' => 'peopletag-' . $this->peopletag->id)); + } + + function showEnd() + { + $this->out->elementEnd('div'); + } + + function showAvatar() + { + parent::showAvatar(AVATAR_PROFILE_SIZE); + } +} diff --git a/actions/showstream.php b/actions/showstream.php index 1a01812ec5..fe819d30cf 100644 --- a/actions/showstream.php +++ b/actions/showstream.php @@ -67,11 +67,11 @@ class ShowstreamAction extends ProfileAction if ($this->page == 1) { // TRANS: Page title showing tagged notices in one user's stream. // TRANS: %1$s is the username, %2$s is the hash tag. - return sprintf(_('%1$s tagged %2$s'), $base, $this->tag); + return sprintf(_('Notices by %1$s tagged %2$s'), $base, $this->tag); } else { // TRANS: Page title showing tagged notices in one user's stream. // TRANS: %1$s is the username, %2$s is the hash tag, %3$d is the page number. - return sprintf(_('%1$s tagged %2$s, page %3$d'), $base, $this->tag, $this->page); + return sprintf(_('Notices by %1$s tagged %2$s, page %3$d'), $base, $this->tag, $this->page); } } else { if ($this->page == 1) { @@ -79,7 +79,7 @@ class ShowstreamAction extends ProfileAction } else { // TRANS: Extended page title showing tagged notices in one user's stream. // TRANS: %1$s is the username, %2$d is the page number. - return sprintf(_('%1$s, page %2$d'), + return sprintf(_('Notices by %1$s, page %2$d'), $base, $this->page); } @@ -103,12 +103,6 @@ class ShowstreamAction extends ProfileAction $this->showNotices(); } - function showObjectNav() - { - $nav = new SubGroupNav($this, $this->user); - $nav->show(); - } - function showProfileBlock() { $block = new AccountProfileBlock($this, $this->profile); @@ -278,6 +272,18 @@ class ShowstreamAction extends ProfileAction $cloud = new PersonalTagCloudSection($this, $this->user); $cloud->show(); } + + function noticeFormOptions() + { + $options = parent::noticeFormOptions(); + $cur = common_current_user(); + + if (empty($cur) || $cur->id != $this->profile->id) { + $options['to_profile'] = $this->profile; + } + + return $options; + } } // We don't show the author for a profile, since we already know who it is! diff --git a/actions/smssettings.php b/actions/smssettings.php index 1545679c17..d151ff45d6 100644 --- a/actions/smssettings.php +++ b/actions/smssettings.php @@ -246,6 +246,7 @@ class SmssettingsAction extends SettingsAction $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->showForm(_('There was a problem with your session token. '. 'Try again, please.')); return; diff --git a/actions/snapshotadminpanel.php b/actions/snapshotadminpanel.php index be0a793e51..9790947071 100644 --- a/actions/snapshotadminpanel.php +++ b/actions/snapshotadminpanel.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class SnapshotadminpanelAction extends AdminPanelAction { /** @@ -48,10 +47,10 @@ class SnapshotadminpanelAction extends AdminPanelAction * * @return string page title */ - function title() { - return _('Snapshots'); + // TRANS: Title for admin panel to configure snapshots. + return _m('TITLE','Snapshots'); } /** @@ -59,9 +58,9 @@ class SnapshotadminpanelAction extends AdminPanelAction * * @return string instructions */ - function getInstructions() { + // TRANS: Instructions for admin panel to configure snapshots. return _('Manage snapshot configuration'); } @@ -70,7 +69,6 @@ class SnapshotadminpanelAction extends AdminPanelAction * * @return void */ - function showForm() { $form = new SnapshotAdminPanelForm($this); @@ -83,7 +81,6 @@ class SnapshotadminpanelAction extends AdminPanelAction * * @return void */ - function saveSettings() { static $settings = array( @@ -124,12 +121,14 @@ class SnapshotadminpanelAction extends AdminPanelAction // Validate snapshot run value if (!in_array($values['snapshot']['run'], array('web', 'cron', 'never'))) { + // TRANS: Client error displayed on admin panel for snapshots when providing an invalid run value. $this->clientError(_('Invalid snapshot run value.')); } // Validate snapshot frequency value if (!Validate::number($values['snapshot']['frequency'])) { + // TRANS: Client error displayed on admin panel for snapshots when providing an invalid value for frequency. $this->clientError(_('Snapshot frequency must be a number.')); } @@ -141,11 +140,13 @@ class SnapshotadminpanelAction extends AdminPanelAction array('allowed_schemes' => array('http', 'https') ) )) { + // TRANS: Client error displayed on admin panel for snapshots when providing an invalid report URL. $this->clientError(_('Invalid snapshot report URL.')); } } } +// @todo FIXME: add documentation class SnapshotAdminPanelForm extends AdminForm { /** @@ -153,7 +154,6 @@ class SnapshotAdminPanelForm extends AdminForm * * @return int ID of the form */ - function id() { return 'form_snapshot_admin_panel'; @@ -164,7 +164,6 @@ class SnapshotAdminPanelForm extends AdminForm * * @return string class of the form */ - function formClass() { return 'form_settings'; @@ -175,7 +174,6 @@ class SnapshotAdminPanelForm extends AdminForm * * @return string URL of the action */ - function action() { return common_local_url('snapshotadminpanel'); @@ -186,26 +184,31 @@ class SnapshotAdminPanelForm extends AdminForm * * @return void */ - function formData() { $this->out->elementStart( 'fieldset', array('id' => 'settings_admin_snapshots') ); - $this->out->element('legend', null, _('Snapshots')); + // TRANS: Fieldset legend on admin panel for snapshots. + $this->out->element('legend', null, _m('LEGEND','Snapshots')); $this->out->elementStart('ul', 'form_data'); $this->li(); $snapshot = array( + // TRANS: Option in dropdown for snapshot method in admin panel for snapshots. 'web' => _('Randomly during web hit'), + // TRANS: Option in dropdown for snapshot method in admin panel for snapshots. 'cron' => _('In a scheduled job'), + // TRANS: Option in dropdown for snapshot method in admin panel for snapshots. 'never' => _('Never') ); $this->out->dropdown( 'run', + // TRANS: Dropdown label for snapshot method in admin panel for snapshots. _('Data snapshots'), $snapshot, - _('When to send statistical data to status.net servers'), + // TRANS: Dropdown title for snapshot method in admin panel for snapshots. + _('When to send statistical data to status.net servers.'), false, $this->value('run', 'snapshot') ); @@ -214,8 +217,10 @@ class SnapshotAdminPanelForm extends AdminForm $this->li(); $this->input( 'frequency', + // TRANS: Input field label for snapshot frequency in admin panel for snapshots. _('Frequency'), - _('Snapshots will be sent once every N web hits'), + // TRANS: Input field title for snapshot frequency in admin panel for snapshots. + _('Snapshots will be sent once every N web hits.'), 'snapshot' ); $this->unli(); @@ -223,8 +228,10 @@ class SnapshotAdminPanelForm extends AdminForm $this->li(); $this->input( 'reporturl', + // TRANS: Input field label for snapshot report URL in admin panel for snapshots. _('Report URL'), - _('Snapshots will be sent to this URL'), + // TRANS: Input field title for snapshot report URL in admin panel for snapshots. + _('Snapshots will be sent to this URL.'), 'snapshot' ); $this->unli(); @@ -237,15 +244,16 @@ class SnapshotAdminPanelForm extends AdminForm * * @return void */ - function formActions() { $this->out->submit( 'submit', - _('Save'), + // TRANS: Button text to save snapshot settings. + _m('BUTTON','Save'), 'submit', null, - _('Save snapshot settings') + // TRANS: Title for button to save snapshot settings. + _('Save snapshot settings.') ); } } diff --git a/actions/subedit.php b/actions/subedit.php index 3b77aff584..7439904af0 100644 --- a/actions/subedit.php +++ b/actions/subedit.php @@ -29,7 +29,7 @@ class SubeditAction extends Action parent::prepare($args); if (!common_logged_in()) { - // TRANS: Client error displayed trying a change a subscription while not logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.')); return false; } @@ -37,6 +37,7 @@ class SubeditAction extends Action $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token. '. 'Try again, please.')); return false; diff --git a/actions/subqueue.php b/actions/subqueue.php new file mode 100644 index 0000000000..38bc16e562 --- /dev/null +++ b/actions/subqueue.php @@ -0,0 +1,142 @@ +. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once(INSTALLDIR.'/lib/profilelist.php'); + +/** + * List of group members + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class SubqueueAction extends GalleryAction +{ + var $page = null; + + function isReadOnly($args) + { + return true; + } + + // @todo FIXME: most of this belongs in a base class, sounds common to most group actions? + function prepare($args) + { + parent::prepare($args); + + $cur = common_current_user(); + if (!$cur || $cur->id != $this->profile->id) { + // TRANS: Client error displayed when trying to approve group applicants without being a group administrator. + $this->clientError(_('You may only approve your own pending subscriptions.')); + return false; + } + return true; + } + + function title() + { + if ($this->page == 1) { + // TRANS: Title of the first page showing pending subscribers still awaiting approval. + // TRANS: %s is the name of the user. + return sprintf(_('%s subscribers awaiting approval'), + $this->profile->nickname); + } else { + // TRANS: Title of all but the first page showing pending subscribersmembers still awaiting approval. + // TRANS: %1$s is the name of the user, %2$d is the page number of the members list. + return sprintf(_('%1$s subscribers awaiting approval, page %2$d'), + $this->profile->nickname, + $this->page); + } + } + + function showPageNotice() + { + $this->element('p', 'instructions', + // TRANS: Page notice for group members page. + _('A list of users awaiting approval to subscribe to you.')); + } + + + function showContent() + { + $offset = ($this->page-1) * PROFILES_PER_PAGE; + $limit = PROFILES_PER_PAGE + 1; + + $cnt = 0; + + $members = $this->profile->getRequests($offset, $limit); + + if ($members) { + // @fixme change! + $member_list = new SubQueueList($members, $this); + $cnt = $member_list->show(); + } + + $members->free(); + + $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, + $this->page, 'subqueue', + array('nickname' => $this->profile->nickname)); // urgh + } +} + +class SubQueueList extends ProfileList +{ + function newListItem($profile) + { + return new SubQueueListItem($profile, $this->action); + } +} + +class SubQueueListItem extends ProfileListItem +{ + function showActions() + { + $this->startActions(); + if (Event::handle('StartProfileListItemActionElements', array($this))) { + $this->showApproveButtons(); + Event::handle('EndProfileListItemActionElements', array($this)); + } + $this->endActions(); + } + + function showApproveButtons() + { + $this->out->elementStart('li', 'entity_approval'); + $form = new ApproveSubForm($this->out, $this->profile); + $form->show(); + $this->out->elementEnd('li'); + } +} diff --git a/actions/subscribe.php b/actions/subscribe.php index 3837915d53..b8c1cdd8f7 100644 --- a/actions/subscribe.php +++ b/actions/subscribe.php @@ -94,7 +94,7 @@ class SubscribeAction extends Action $this->user = common_current_user(); if (empty($this->user)) { - // TRANS: Client error displayed trying to subscribe when not logged in. + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.')); return false; } @@ -139,8 +139,8 @@ class SubscribeAction extends Action { // Throws exception on error - Subscription::start($this->user->getProfile(), - $this->other); + $sub = Subscription::start($this->user->getProfile(), + $this->other); if ($this->boolean('ajax')) { $this->startHTML('text/xml;charset=utf-8'); @@ -149,8 +149,12 @@ class SubscribeAction extends Action $this->element('title', null, _('Subscribed')); $this->elementEnd('head'); $this->elementStart('body'); - $unsubscribe = new UnsubscribeForm($this, $this->other); - $unsubscribe->show(); + if ($sub instanceof Subscription) { + $form = new UnsubscribeForm($this, $this->other); + } else { + $form = new CancelSubscriptionForm($this, $this->other); + } + $form->show(); $this->elementEnd('body'); $this->elementEnd('html'); } else { diff --git a/actions/subscribepeopletag.php b/actions/subscribepeopletag.php new file mode 100644 index 0000000000..dee0384c5d --- /dev/null +++ b/actions/subscribepeopletag.php @@ -0,0 +1,151 @@ +. + * + * @category Peopletag + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Subscribe to a peopletag + * + * This is the action for subscribing to a peopletag. It works more or less like the join action + * for groups. + * + * @category Peopletag + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class SubscribepeopletagAction extends Action +{ + var $peopletag = null; + var $tagger = null; + + /** + * Prepare to run + */ + function prepare($args) + { + parent::prepare($args); + + if (!common_logged_in()) { + // TRANS: Client error displayed when trying to perform an action while not logged in. + $this->clientError(_('You must be logged in to unsubscribe from a list.')); + return false; + } + // Only allow POST requests + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + // TRANS: Client error displayed when trying to use another method than POST. + $this->clientError(_('This action only accepts POST requests.')); + return false; + } + + // CSRF protection + + $token = $this->trimmed('token'); + + if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. + $this->clientError(_('There was a problem with your session token.'. + ' Try again, please.')); + return false; + } + + $tagger_arg = $this->trimmed('tagger'); + $tag_arg = $this->trimmed('tag'); + + $id = intval($this->arg('id')); + if ($id) { + $this->peopletag = Profile_list::staticGet('id', $id); + } else { + // TRANS: Client error displayed when trying to perform an action without providing an ID. + $this->clientError(_('No ID given.'), 404); + return false; + } + + if (!$this->peopletag || $this->peopletag->private) { + // TRANS: Client error displayed trying to reference a non-existing list. + $this->clientError(_('No such list.'), 404); + return false; + } + + $this->tagger = Profile::staticGet('id', $this->peopletag->tagger); + + return true; + } + + /** + * Handle the request + * + * On POST, add the current user to the group + * + * @param array $args unused + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + $cur = common_current_user(); + + try { + Profile_tag_subscription::add($this->peopletag, $cur); + } catch (Exception $e) { + // TRANS: Server error displayed subscribing to a list fails. + // TRANS: %1$s is a user nickname, %2$s is a list, %3$s is the error message (no period). + $this->serverError(sprintf(_('Could not subscribe user %1$s to list %2$s: %3$s'), + $cur->nickname, $this->peopletag->tag), $e->getMessage()); + } + + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + // TRANS: Title of form to subscribe to a list. + // TRANS: %1%s is a user nickname, %2$s is a list, %3$s is a tagger nickname. + $this->element('title', null, sprintf(_('%1$s subscribed to list %2$s by %3$s'), + $cur->nickname, + $this->peopletag->tag, + $this->tagger->nickname)); + $this->elementEnd('head'); + $this->elementStart('body'); + $lf = new UnsubscribePeopletagForm($this, $this->peopletag); + $lf->show(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect(common_local_url('peopletagsubscribers', + array('tagger' => $this->tagger->nickname, + 'tag' =>$this->peopletag->tag)), + 303); + } + } +} diff --git a/actions/subscribers.php b/actions/subscribers.php index ad522a4bae..9e1fb4cf73 100644 --- a/actions/subscribers.php +++ b/actions/subscribers.php @@ -135,11 +135,6 @@ class SubscribersAction extends GalleryAction function showSections() { parent::showSections(); - $cloud = new SubscribersPeopleTagCloudSection($this); - $cloud->show(); - - $cloud2 = new SubscribersPeopleSelfTagCloudSection($this); - $cloud2->show(); } } diff --git a/actions/subscriptions.php b/actions/subscriptions.php index cfe2b5683a..b4575565b9 100644 --- a/actions/subscriptions.php +++ b/actions/subscriptions.php @@ -152,16 +152,6 @@ class SubscriptionsAction extends GalleryAction $this->elementEnd('div'); } - function showSections() - { - parent::showSections(); - $cloud = new SubscriptionsPeopleTagCloudSection($this); - $cloud->show(); - - $cloud2 = new SubscriptionsPeopleSelfTagCloudSection($this); - $cloud2->show(); - } - /** * Link to feeds of subscriptions * @@ -237,8 +227,8 @@ class SubscriptionsListItem extends SubscriptionListItem } $this->out->element('input', $attrs); - // TRANS: Checkbox label for enabling Jabber messages for a profile in a subscriptions list. - $this->out->element('label', array('for' => 'jabber-'.$this->profile->id), _('IM')); + // TRANS: Checkbox label for enabling IM messages for a profile in a subscriptions list. + $this->out->element('label', array('for' => 'jabber-'.$this->profile->id), _m('LABEL','IM')); } else { $this->out->hidden('jabber', $sub->jabber); } diff --git a/actions/tag.php b/actions/tag.php index 944cda1f4a..f3514bef55 100644 --- a/actions/tag.php +++ b/actions/tag.php @@ -19,9 +19,9 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +// @todo FIXME: documentation missing. class TagAction extends Action { - var $notice; function prepare($args) @@ -48,19 +48,13 @@ 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) + // TRANS: Server error when page not found (404). $this->serverError(_('No such page.'),$code=404); } return true; } - function showSections() - { - $pop = new PopularNoticeSection($this); - $pop->show(); - } - function title() { if ($this->page == 1) { @@ -88,18 +82,24 @@ class TagAction extends Action return array(new Feed(Feed::RSS1, common_local_url('tagrss', array('tag' => $this->tag)), + // TRANS: Link label for feed on "notices with tag" page. + // TRANS: %s is the tag the feed is for. sprintf(_('Notice feed for tag %s (RSS 1.0)'), $this->tag)), new Feed(Feed::RSS2, common_local_url('ApiTimelineTag', array('format' => 'rss', 'tag' => $this->tag)), + // TRANS: Link label for feed on "notices with tag" page. + // TRANS: %s is the tag the feed is for. sprintf(_('Notice feed for tag %s (RSS 2.0)'), $this->tag)), new Feed(Feed::ATOM, common_local_url('ApiTimelineTag', array('format' => 'atom', 'tag' => $this->tag)), + // TRANS: Link label for feed on "notices with tag" page. + // TRANS: %s is the tag the feed is for. sprintf(_('Notice feed for tag %s (Atom)'), $this->tag))); } @@ -107,7 +107,7 @@ class TagAction extends Action function showContent() { if(Event::handle('StartTagShowContent', array($this))) { - + $nl = new NoticeList($this->notice, $this); $cnt = $nl->show(); diff --git a/actions/tagother.php b/actions/tagother.php deleted file mode 100644 index c3bf219f67..0000000000 --- a/actions/tagother.php +++ /dev/null @@ -1,222 +0,0 @@ -. - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - -require_once(INSTALLDIR.'/lib/settingsaction.php'); - -// @todo FIXME: documentation missing. -class TagotherAction extends Action -{ - var $profile = null; - var $error = null; - - function prepare($args) - { - parent::prepare($args); - if (!common_logged_in()) { - $this->clientError(_('Not logged in.'), 403); - return false; - } - - $id = $this->trimmed('id'); - if (!$id) { - $this->clientError(_('No ID argument.')); - return false; - } - - $this->profile = Profile::staticGet('id', $id); - - if (!$this->profile) { - $this->clientError(_('No profile with that ID.')); - return false; - } - - return true; - } - - function handle($args) - { - parent::handle($args); - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $this->saveTags(); - } else { - $this->showForm($profile); - } - } - - function title() - { - return sprintf(_('Tag %s'), $this->profile->nickname); - } - - function showForm($error=null) - { - $this->error = $error; - $this->showPage(); - } - - function showContent() - { - $this->elementStart('div', 'entity_profile vcard author'); - $this->element('h2', null, _('User profile')); - - $avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE); - $this->element('img', array('src' => ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE), - 'class' => 'photo avatar entity_depiction', - 'width' => AVATAR_PROFILE_SIZE, - 'height' => AVATAR_PROFILE_SIZE, - 'alt' => - ($this->profile->fullname) ? $this->profile->fullname : - $this->profile->nickname)); - - $this->element('a', array('href' => $this->profile->profileurl, - 'class' => 'entity_nickname nickname'), - $this->profile->nickname); - - if ($this->profile->fullname) { - $this->element('div', 'fn entity_fn', $this->profile->fullname); - } - - if ($this->profile->location) { - $this->element('div', 'label entity_location', $this->profile->location); - } - - if ($this->profile->homepage) { - $this->element('a', array('href' => $this->profile->homepage, - 'rel' => 'me', - 'class' => 'url entity_url'), - $this->profile->homepage); - } - - if ($this->profile->bio) { - $this->element('div', 'note entity_note', $this->profile->bio); - } - - $this->elementEnd('div'); - - $this->elementStart('form', array('method' => 'post', - 'id' => 'form_tag_user', - 'class' => 'form_settings', - 'name' => 'tagother', - 'action' => common_local_url('tagother', array('id' => $this->profile->id)))); - - $this->elementStart('fieldset'); - $this->element('legend', null, _('Tag user')); - $this->hidden('token', common_session_token()); - $this->hidden('id', $this->profile->id); - - $user = common_current_user(); - - $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - $this->input('tags', _('Tags'), - ($this->arg('tags')) ? $this->arg('tags') : implode(' ', Profile_tag::getTags($user->id, $this->profile->id)), - _('Tags for this user (letters, numbers, -, ., and _), separated by commas or spaces.')); - $this->elementEnd('li'); - $this->elementEnd('ul'); - $this->submit('save', _m('BUTTON','Save')); - $this->elementEnd('fieldset'); - $this->elementEnd('form'); - } - - function saveTags() - { - $id = $this->trimmed('id'); - $tagstring = $this->trimmed('tags'); - $token = $this->trimmed('token'); - - if (!$token || $token != common_session_token()) { - $this->showForm(_('There was a problem with your session token. '. - 'Try again, please.')); - return; - } - - if (is_string($tagstring) && strlen($tagstring) > 0) { - - $tags = array_map('common_canonical_tag', - preg_split('/[\s,]+/', $tagstring)); - - foreach ($tags as $tag) { - if (!common_valid_profile_tag($tag)) { - // TRANS: Form validation error when entering an invalid tag. - // TRANS: %s is the invalid tag. - $this->showForm(sprintf(_('Invalid tag: "%s".'), $tag)); - return; - } - } - } else { - $tags = array(); - } - - $user = common_current_user(); - - if (!Subscription::pkeyGet(array('subscriber' => $user->id, - 'subscribed' => $this->profile->id)) && - !Subscription::pkeyGet(array('subscriber' => $this->profile->id, - 'subscribed' => $user->id))) - { - $this->clientError(_('You can only tag people you are subscribed to or who are subscribed to you.')); - return; - } - - $result = Profile_tag::setTags($user->id, $this->profile->id, $tags); - - if (!$result) { - $this->clientError(_('Could not save tags.')); - return; - } - - $action = $user->isSubscribed($this->profile) ? 'subscriptions' : 'subscribers'; - - if ($this->boolean('ajax')) { - $this->startHTML('text/xml;charset=utf-8'); - $this->elementStart('head'); - $this->element('title', null, _('Tags')); - $this->elementEnd('head'); - $this->elementStart('body'); - $this->elementStart('p', 'subtags'); - foreach ($tags as $tag) { - $this->element('a', array('href' => common_local_url($action, - array('nickname' => $user->nickname, - 'tag' => $tag))), - $tag); - } - $this->elementEnd('p'); - $this->elementEnd('body'); - $this->elementEnd('html'); - } else { - common_redirect(common_local_url($action, array('nickname' => - $user->nickname)), - 303); - } - } - - function showPageNotice() - { - if ($this->error) { - $this->element('p', 'error', $this->error); - } else { - $this->elementStart('div', 'instructions'); - $this->element('p', null, - _('Use this form to add tags to your subscribers or subscriptions.')); - $this->elementEnd('div'); - } - } -} diff --git a/actions/tagprofile.php b/actions/tagprofile.php new file mode 100644 index 0000000000..4305f4bc88 --- /dev/null +++ b/actions/tagprofile.php @@ -0,0 +1,264 @@ +. + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +require_once INSTALLDIR . '/lib/settingsaction.php'; +require_once INSTALLDIR . '/lib/peopletags.php'; + +class TagprofileAction extends Action +{ + var $profile = null; + var $error = null; + + function prepare($args) + { + parent::prepare($args); + if (!common_logged_in()) { + common_set_returnto($_SERVER['REQUEST_URI']); + if (Event::handle('RedirectToLogin', array($this, null))) { + common_redirect(common_local_url('login'), 303); + } + } + + $id = $this->trimmed('id'); + if (!$id) { + $this->profile = false; + } else { + $this->profile = Profile::staticGet('id', $id); + + if (!$this->profile) { + // TRANS: Client error displayed when referring to non-existing profile ID. + $this->clientError(_('No profile with that ID.')); + return false; + } + } + + $current = common_current_user()->getProfile(); + if ($this->profile && !$current->canTag($this->profile)) { + // TRANS: Client error displayed when trying to tag a user that cannot be tagged. + $this->clientError(_('You cannot tag this user.')); + } + return true; + } + + function handle($args) + { + parent::handle($args); + if (Event::handle('StartTagProfileAction', array($this, $this->profile))) { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->saveTags(); + } else { + $this->showForm(); + } + Event::handle('EndTagProfileAction', array($this, $this->profile)); + } + } + + function title() + { + if (!$this->profile) { + // TRANS: Title for list form when not on a profile page. + return _('List a profile'); + } + // TRANS: Title for list form when on a profile page. + // TRANS: %s is a profile nickname. + return sprintf(_m('ADDTOLIST','List %s'), $this->profile->nickname); + } + + function showForm($error=null) + { + $this->error = $error; + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + // TRANS: Title for list form when an error has occurred. + $this->element('title', null, _m('TITLE','Error')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->element('p', 'error', $error); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + $this->showPage(); + } + } + + function showContent() + { + if (Event::handle('StartShowTagProfileForm', array($this, $this->profile)) && $this->profile) { + $this->elementStart('div', 'entity_profile vcard author'); + // TRANS: Header in list form. + $this->element('h2', null, _('User profile')); + + $avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE); + $this->element('img', array('src' => ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE), + 'class' => 'photo avatar entity_depiction', + 'width' => AVATAR_PROFILE_SIZE, + 'height' => AVATAR_PROFILE_SIZE, + 'alt' => + ($this->profile->fullname) ? $this->profile->fullname : + $this->profile->nickname)); + + $this->element('a', array('href' => $this->profile->profileurl, + 'class' => 'entity_nickname nickname'), + $this->profile->nickname); + if ($this->profile->fullname) { + $this->element('div', 'fn entity_fn', $this->profile->fullname); + } + + if ($this->profile->location) { + $this->element('div', 'label entity_location', $this->profile->location); + } + + if ($this->profile->homepage) { + $this->element('a', array('href' => $this->profile->homepage, + 'rel' => 'me', + 'class' => 'url entity_url'), + $this->profile->homepage); + } + + if ($this->profile->bio) { + $this->element('div', 'note entity_note', $this->profile->bio); + } + + $this->elementEnd('div'); + + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_tag_user', + 'class' => 'form_settings', + 'name' => 'tagprofile', + 'action' => common_local_url('tagprofile', array('id' => $this->profile->id)))); + + $this->elementStart('fieldset'); + // TRANS: Fieldset legend for list form. + $this->element('legend', null, _('List user')); + $this->hidden('token', common_session_token()); + $this->hidden('id', $this->profile->id); + + $user = common_current_user(); + + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + + $tags = Profile_tag::getTagsArray($user->id, $this->profile->id, $user->id); + // TRANS: Field label on list form. + $this->input('tags', _m('LABEL','Lists'), + ($this->arg('tags')) ? $this->arg('tags') : implode(' ', $tags), + // TRANS: Field title on list form. + _('Lists for this user (letters, numbers, -, ., and _), comma- or space- separated.')); + $this->elementEnd('li'); + $this->elementEnd('ul'); + // TRANS: Button text to save lists. + $this->submit('save', _m('BUTTON','Save')); + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + + Event::handle('EndShowTagProfileForm', array($this, $this->profile)); + } + } + + function saveTags() + { + $id = $this->trimmed('id'); + $tagstring = $this->trimmed('tags'); + $token = $this->trimmed('token'); + + if (Event::handle('StartSavePeopletags', array($this, $tagstring))) { + if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. + $this->showForm(_('There was a problem with your session token. '. + 'Try again, please.')); + return; + } + + $tags = array(); + $tag_priv = array(); + + if (is_string($tagstring) && strlen($tagstring) > 0) { + + $tags = preg_split('/[\s,]+/', $tagstring); + + foreach ($tags as &$tag) { + $private = @$tag[0] === '.'; + + $tag = common_canonical_tag($tag); + if (!common_valid_profile_tag($tag)) { + // TRANS: Form validation error displayed if a given tag is invalid. + // TRANS: %s is the invalid tag. + $this->showForm(sprintf(_('Invalid tag: "%s".'), $tag)); + return; + } + + $tag_priv[$tag] = $private; + } + } + + $user = common_current_user(); + + try { + $result = Profile_tag::setTags($user->id, $this->profile->id, $tags, $tag_priv); + if (!$result) { + throw new Exception('The tags could not be saved.'); + } + } catch (Exception $e) { + $this->showForm($e->getMessage()); + return false; + } + + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + $this->element('title', null, _m('TITLE','Tags')); + $this->elementEnd('head'); + $this->elementStart('body'); + + if ($user->id == $this->profile->id) { + $widget = new SelftagsWidget($this, $user, $this->profile); + $widget->show(); + } else { + $widget = new PeopletagsWidget($this, $user, $this->profile); + $widget->show(); + } + + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + // TRANS: Success message if lists are saved. + $this->error = _('Lists saved.'); + $this->showForm(); + } + + Event::handle('EndSavePeopletags', array($this, $tagstring)); + } + } + + function showPageNotice() + { + if ($this->error) { + $this->element('p', 'error', $this->error); + } else { + $this->elementStart('div', 'instructions'); + $this->element('p', null, + // TRANS: Page notice. + _('Use this form to add your subscribers or subscriptions to lists.')); + $this->elementEnd('div'); + } + } +} diff --git a/actions/tagrss.php b/actions/tagrss.php index 467a64abed..5f4831d221 100644 --- a/actions/tagrss.php +++ b/actions/tagrss.php @@ -22,7 +22,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } require_once(INSTALLDIR.'/lib/rssaction.php'); // Formatting of RSS handled by Rss10Action - class TagrssAction extends Rss10Action { var $tag; @@ -32,6 +31,7 @@ class TagrssAction extends Rss10Action $tag = common_canonical_tag($this->trimmed('tag')); $this->tag = Notice_tag::staticGet('tag', $tag); if (!$this->tag) { + // TRANS: Client error when requesting a tag feed for a non-existing tag. $this->clientError(_('No such tag.')); return false; } else { @@ -62,6 +62,8 @@ class TagrssAction extends Rss10Action $c = array('url' => common_local_url('tagrss', array('tag' => $tagname)), 'title' => $tagname, 'link' => common_local_url('tagrss', array('tag' => $tagname)), + // TRANS: Tag feed description. + // TRANS: %1$s is the tag name, %2$s is the StatusNet sitename. 'description' => sprintf(_('Updates tagged with %1$s on %2$s!'), $tagname, common_config('site', 'name'))); return $c; diff --git a/actions/unsandbox.php b/actions/unsandbox.php index d50b5072ee..98bd2d4f54 100644 --- a/actions/unsandbox.php +++ b/actions/unsandbox.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class UnsandboxAction extends ProfileFormAction { /** @@ -50,7 +49,6 @@ class UnsandboxAction extends ProfileFormAction * * @return boolean success flag */ - function prepare($args) { if (!parent::prepare($args)) { @@ -62,6 +60,7 @@ class UnsandboxAction extends ProfileFormAction assert(!empty($cur)); // checked by parent if (!$cur->hasRight(Right::SANDBOXUSER)) { + // TRANS: Client error on page to unsandbox a user when the feature is not enabled. $this->clientError(_('You cannot sandbox users on this site.')); return false; } @@ -69,6 +68,7 @@ class UnsandboxAction extends ProfileFormAction assert(!empty($this->profile)); // checked by parent if (!$this->profile->isSandboxed()) { + // TRANS: Client error on page to unsilence a user when the to be unsandboxed user has not been sandboxed. $this->clientError(_('User is not sandboxed.')); return false; } @@ -81,7 +81,6 @@ class UnsandboxAction extends ProfileFormAction * * @return void */ - function handlePost() { $this->profile->unsandbox(); diff --git a/actions/unsilence.php b/actions/unsilence.php index 7d282c3661..723bf1bed5 100644 --- a/actions/unsilence.php +++ b/actions/unsilence.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class UnsilenceAction extends ProfileFormAction { /** @@ -50,7 +49,6 @@ class UnsilenceAction extends ProfileFormAction * * @return boolean success flag */ - function prepare($args) { if (!parent::prepare($args)) { @@ -62,6 +60,7 @@ class UnsilenceAction extends ProfileFormAction assert(!empty($cur)); // checked by parent if (!$cur->hasRight(Right::SILENCEUSER)) { + // TRANS: Client error on page to unsilence a user when the feature is not enabled. $this->clientError(_('You cannot silence users on this site.')); return false; } @@ -69,6 +68,7 @@ class UnsilenceAction extends ProfileFormAction assert(!empty($this->profile)); // checked by parent if (!$this->profile->isSilenced()) { + // TRANS: Client error on page to unsilence a user when the to be unsilenced user has not been silenced. $this->clientError(_('User is not silenced.')); return false; } @@ -81,7 +81,6 @@ class UnsilenceAction extends ProfileFormAction * * @return void */ - function handlePost() { $this->profile->unsilence(); diff --git a/actions/unsubscribe.php b/actions/unsubscribe.php index 57ca15d687..ba9ecd8f00 100644 --- a/actions/unsubscribe.php +++ b/actions/unsubscribe.php @@ -44,11 +44,11 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { */ class UnsubscribeAction extends Action { - function handle($args) { parent::handle($args); if (!common_logged_in()) { + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. $this->clientError(_('Not logged in.')); return; } @@ -66,6 +66,7 @@ class UnsubscribeAction extends Action $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->clientError(_('There was a problem with your session token. ' . 'Try again, please.')); return; @@ -74,6 +75,7 @@ class UnsubscribeAction extends Action $other_id = $this->arg('unsubscribeto'); if (!$other_id) { + // TRANS: Client error displayed when trying to unsubscribe without providing a profile ID. $this->clientError(_('No profile ID in request.')); return; } @@ -81,6 +83,7 @@ class UnsubscribeAction extends Action $other = Profile::staticGet('id', $other_id); if (!$other) { + // TRANS: Client error displayed when trying to unsubscribe while providing a non-existing profile ID. $this->clientError(_('No profile with that ID.')); return; } @@ -95,6 +98,7 @@ class UnsubscribeAction extends Action if ($this->boolean('ajax')) { $this->startHTML('text/xml;charset=utf-8'); $this->elementStart('head'); + // TRANS: Page title for page to unsubscribe. $this->element('title', null, _('Unsubscribed')); $this->elementEnd('head'); $this->elementStart('body'); diff --git a/actions/unsubscribepeopletag.php b/actions/unsubscribepeopletag.php new file mode 100644 index 0000000000..bb53766cc1 --- /dev/null +++ b/actions/unsubscribepeopletag.php @@ -0,0 +1,147 @@ +. + * + * @category Peopletag + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Unsubscribe to a peopletag + * + * This is the action for subscribing to a peopletag. It works more or less like the join action + * for groups. + * + * @category Peopletag + * @package StatusNet + * @author Shashi Gowda + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class UnsubscribepeopletagAction extends Action +{ + var $peopletag = null; + var $tagger = null; + + /** + * Prepare to run + */ + + function prepare($args) + { + parent::prepare($args); + + if (!common_logged_in()) { + // TRANS: Client error displayed when trying to perform an action while not logged in. + $this->clientError(_('You must be logged in to unsubscribe from a list.')); + return false; + } + // Only allow POST requests + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + // TRANS: Client error displayed when trying to use another method than POST. + $this->clientError(_('This action only accepts POST requests.')); + return false; + } + + // CSRF protection + + $token = $this->trimmed('token'); + + if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. + $this->clientError(_('There was a problem with your session token.'. + ' Try again, please.')); + return false; + } + + $tagger_arg = $this->trimmed('tagger'); + $tag_arg = $this->trimmed('tag'); + + $id = intval($this->arg('id')); + if ($id) { + $this->peopletag = Profile_list::staticGet('id', $id); + } else { + // TRANS: Client error displayed when trying to perform an action without providing an ID. + $this->clientError(_('No ID given.'), 404); + return false; + } + + if (!$this->peopletag || $this->peopletag->private) { + // TRANS: Client error displayed trying to reference a non-existing list. + $this->clientError(_('No such list.'), 404); + return false; + } + + $this->tagger = Profile::staticGet('id', $this->peopletag->tagger); + + return true; + } + + /** + * Handle the request + * + * On POST, add the current user to the group + * + * @param array $args unused + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + $cur = common_current_user(); + + Profile_tag_subscription::remove($this->peopletag, $cur); + + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + // TRANS: Page title for form that allows unsubscribing from a list. + // TRANS: %1$s is a nickname, %2$s is a list, %3$s is a tagger nickname. + $this->element('title', null, sprintf(_('%1$s unsubscribed from list %2$s by %3$s'), + $cur->nickname, + $this->peopletag->tag, + $this->tagger->nickname)); + $this->elementEnd('head'); + $this->elementStart('body'); + $lf = new SubscribePeopletagForm($this, $this->peopletag); + $lf->show(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + if (common_get_returnto()) { + common_redirect(common_get_returnto(), 303); + return true; + } + common_redirect(common_local_url('peopletagsbyuser', + array('nickname' => $this->tagger->nickname)), + 303); + } + } +} diff --git a/actions/updateprofile.php b/actions/updateprofile.php index bae6108cce..e5c0803495 100644 --- a/actions/updateprofile.php +++ b/actions/updateprofile.php @@ -45,7 +45,6 @@ require_once INSTALLDIR.'/extlib/libomb/service_provider.php'; */ class UpdateprofileAction extends Action { - /** * For initializing members of the class. * @@ -61,8 +60,10 @@ class UpdateprofileAction extends Action $license = $_POST['omb_listenee_license']; $site_license = common_config('license', 'url'); if (!common_compatible_license($license, $site_license)) { - $this->clientError(sprintf(_('Listenee stream license ‘%1$s’ is not '. - 'compatible with site license ‘%2$s’.'), + // TRANS: Client error displayed when trying to update profile with an incompatible license. + // TRANS: %1$s is the license incompatible with site license %2$s. + $this->clientError(sprintf(_('Listenee stream license "%1$s" is not '. + 'compatible with site license "%2$s".'), $license, $site_license)); return false; } diff --git a/actions/urlsettings.php b/actions/urlsettings.php index 7661086aae..02a895955d 100644 --- a/actions/urlsettings.php +++ b/actions/urlsettings.php @@ -32,8 +32,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - - /** * Miscellaneous settings actions * @@ -46,7 +44,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class UrlsettingsAction extends SettingsAction { /** @@ -54,9 +51,9 @@ class UrlsettingsAction extends SettingsAction * * @return string Title of the page */ - function title() { + // TRANS: Title of URL settings tab in profile settings. return _('URL settings'); } @@ -65,7 +62,6 @@ class UrlsettingsAction extends SettingsAction * * @return instructions for use */ - function getInstructions() { // TRANS: Instructions for tab "Other" in user profile settings. @@ -85,7 +81,6 @@ class UrlsettingsAction extends SettingsAction * * @return void */ - function showContent() { $user = common_current_user(); @@ -118,11 +113,12 @@ class UrlsettingsAction extends SettingsAction // Include default values + // TRANS: Default value for URL shortening settings. $services['none'] = _('[none]'); + // TRANS: Default value for URL shortening settings. $services['internal'] = _('[internal]'); if ($services) { - asort($services); $this->elementStart('li'); @@ -135,16 +131,20 @@ class UrlsettingsAction extends SettingsAction } $this->elementStart('li'); $this->input('maxurllength', + // TRANS: Field label in URL settings in profile. _('URL longer than'), (!is_null($this->arg('maxurllength'))) ? $this->arg('maxurllength') : User_urlshortener_prefs::maxUrlLength($user), + // TRANS: Field title in URL settings in profile. _('URLs longer than this will be shortened, 0 means always shorten.')); $this->elementEnd('li'); $this->elementStart('li'); $this->input('maxnoticelength', + // TRANS: Field label in URL settings in profile. _('Text longer than'), (!is_null($this->arg('maxnoticelength'))) ? $this->arg('maxnoticelength') : User_urlshortener_prefs::maxNoticeLength($user), + // TRANS: Field title in URL settings in profile. _('URLs in notices longer than this will be shortened, 0 means always shorten.')); $this->elementEnd('li'); $this->elementEnd('ul'); @@ -162,12 +162,12 @@ class UrlsettingsAction extends SettingsAction * * @return void */ - function handlePost() { // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. $this->showForm(_('There was a problem with your session token. '. 'Try again, please.')); return; @@ -184,13 +184,15 @@ class UrlsettingsAction extends SettingsAction $maxurllength = $this->trimmed('maxurllength'); if (!Validate::number($maxurllength, array('min' => 0))) { - throw new ClientException(_('Invalid number for max url length.')); + // TRANS: Client exception thrown when the maximum URL settings value is invalid in profile URL settings. + throw new ClientException(_('Invalid number for maximum URL length.')); } $maxnoticelength = $this->trimmed('maxnoticelength'); if (!Validate::number($maxnoticelength, array('min' => 0))) { - throw new ClientException(_('Invalid number for max notice length.')); + // TRANS: Client exception thrown when the maximum notice length settings value is invalid in profile URL settings. + throw new ClientException(_('Invalid number for maximum notice length.')); } $user = common_current_user(); @@ -235,6 +237,7 @@ class UrlsettingsAction extends SettingsAction } if (!$result) { + // TRANS: Server exception thrown in profile URL settings when preferences could not be saved. throw new ServerException(_('Error saving user URL shortening preferences.')); } diff --git a/actions/useradminpanel.php b/actions/useradminpanel.php index c8861bd834..19673189f5 100644 --- a/actions/useradminpanel.php +++ b/actions/useradminpanel.php @@ -54,7 +54,7 @@ class UseradminpanelAction extends AdminPanelAction */ function title() { - // TRANS: User admin panel title + // TRANS: User admin panel title. return _m('TITLE', 'User'); } @@ -172,6 +172,7 @@ class UseradminpanelAction extends AdminPanelAction } } +// @todo FIXME: Class documentation missing. class UserAdminPanelForm extends AdminForm { /** @@ -212,7 +213,8 @@ class UserAdminPanelForm extends AdminForm function formData() { $this->out->elementStart('fieldset', array('id' => 'settings_user-profile')); - $this->out->element('legend', null, _('Profile')); + // TRANS: Fieldset legend in user administration panel. + $this->out->element('legend', null, _m('LEGEND','Profile')); $this->out->elementStart('ul', 'form_data'); $this->li(); diff --git a/actions/userauthorization.php b/actions/userauthorization.php index d9cdc660fd..0cb55715f9 100644 --- a/actions/userauthorization.php +++ b/actions/userauthorization.php @@ -50,6 +50,7 @@ class UserauthorizationAction extends Action $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { $srv = $this->getStoredParams(); + // TRANS: Client error displayed when the session token does not match or is not given. $this->showForm($srv->getRemoteUser(), _('There was a problem ' . 'with your session token. Try again, ' . 'please.')); @@ -70,9 +71,8 @@ class UserauthorizationAction extends Action $profile = $user->getProfile(); if (!$profile) { common_log_db_error($user, 'SELECT', __FILE__); - // TRANS: Server error displayed when trying to authorise a remote subscription request - // TRANS: while the user has no profile. - $this->serverError(_('User without matching profile.')); + // TRANS: Error message displayed when referring to a user without a profile. + $this->serverError(_('User has no profile.')); return; } @@ -111,7 +111,7 @@ class UserauthorizationAction extends Action function showPageNotice() { - // TRANS: Page notice on "Auhtorize subscription" page. + // TRANS: Page notice on "Authorize subscription" page. $this->element('p', null, _('Please check these details to make sure '. 'that you want to subscribe to this ' . 'user’s notices. If you didn’t just ask ' . @@ -243,10 +243,10 @@ class UserauthorizationAction extends Action { // TRANS: Accept message header from Authorise subscription page. common_show_header(_('Subscription authorized')); - // TRANS: Accept message text from Authorise subscription page. $this->element('p', null, + // TRANS: Accept message text from Authorise subscription page. _('The subscription has been authorized, but no '. - 'callback URL was passed. Check with the site’s ' . + 'callback URL was passed. Check with the site\'s ' . 'instructions for details on how to authorize the ' . 'subscription. Your subscription token is:')); $this->element('blockquote', 'token', $tok); @@ -257,10 +257,10 @@ class UserauthorizationAction extends Action { // TRANS: Reject message header from Authorise subscription page. common_show_header(_('Subscription rejected')); - // TRANS: Reject message from Authorise subscription page. $this->element('p', null, + // TRANS: Reject message from Authorise subscription page. _('The subscription has been rejected, but no '. - 'callback URL was passed. Check with the site’s ' . + 'callback URL was passed. Check with the site\'s ' . 'instructions for details on how to fully reject ' . 'the subscription.')); common_show_footer(); diff --git a/actions/usergroups.php b/actions/usergroups.php index f9063d8867..178a3586fd 100644 --- a/actions/usergroups.php +++ b/actions/usergroups.php @@ -45,7 +45,7 @@ require_once INSTALLDIR.'/lib/grouplist.php'; * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ -class UsergroupsAction extends OwnerDesignAction +class UsergroupsAction extends ProfileAction { var $page = null; var $profile = null; @@ -99,7 +99,7 @@ class UsergroupsAction extends OwnerDesignAction $this->profile = $this->user->getProfile(); if (!$this->profile) { - // TRANS: Server error displayed requesting groups for a user without a profile. + // TRANS: Error message displayed when referring to a user without a profile. $this->serverError(_('User has no profile.')); return false; } @@ -115,12 +115,6 @@ class UsergroupsAction extends OwnerDesignAction $this->showPage(); } - function showObjectNav() - { - $nav = new SubGroupNav($this, $this->user); - $nav->show(); - } - function showContent() { $this->elementStart('p', array('id' => 'new_group')); diff --git a/actions/userrss.php b/actions/userrss.php index ba9f64f8ac..85ea2fd7fa 100644 --- a/actions/userrss.php +++ b/actions/userrss.php @@ -37,6 +37,7 @@ class UserrssAction extends Rss10Action $this->tag = $this->trimmed('tag'); if (!$this->user) { + // TRANS: Client error displayed when user not found for an action. $this->clientError(_('No such user.')); return false; } else { @@ -105,7 +106,8 @@ class UserrssAction extends Rss10Action $profile = $user->getProfile(); if (!$profile) { common_log_db_error($user, 'SELECT', __FILE__); - $this->serverError(_('User without matching profile.')); + // TRANS: Error message displayed when referring to a user without a profile. + $this->serverError(_('User has no profile.')); return null; } $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); diff --git a/actions/userxrd.php b/actions/userxrd.php index 7691ff155b..1d888064d6 100644 --- a/actions/userxrd.php +++ b/actions/userxrd.php @@ -56,6 +56,7 @@ class UserxrdAction extends XrdAction } if (!$this->user) { + // TRANS: Client error displayed when user not found for an action. $this->clientError(_('No such user.'), 404); return false; } diff --git a/classes/Avatar.php b/classes/Avatar.php index 34ec4a3caf..0b5141ba53 100644 --- a/classes/Avatar.php +++ b/classes/Avatar.php @@ -102,7 +102,7 @@ class Avatar extends Memcached_DataObject function displayUrl() { $server = common_config('avatar', 'server'); - if ($server) { + if ($server && !empty($this->filename)) { return Avatar::url($this->filename); } else { return $this->url; diff --git a/classes/Confirm_address.php b/classes/Confirm_address.php index ed3875d223..4b9bec64c6 100644 --- a/classes/Confirm_address.php +++ b/classes/Confirm_address.php @@ -28,4 +28,36 @@ class Confirm_address extends Memcached_DataObject function sequenceKey() { return array(false, false); } + + static function getAddress($address, $addressType) + { + $ca = new Confirm_address(); + + $ca->address = $address; + $ca->address_type = $addressType; + + if ($ca->find(true)) { + return $ca; + } + + return null; + } + + static function saveNew($user, $address, $addressType, $extra=null) + { + $ca = new Confirm_address(); + + if (!empty($user)) { + $ca->user_id = $user->id; + } + + $ca->address = $address; + $ca->address_type = $addressType; + $ca->address_extra = $extra; + $ca->code = common_confirmation_code(64); + + $ca->insert(); + + return $ca; + } } diff --git a/classes/Conversation.php b/classes/Conversation.php index aab55723f6..e029c20ba0 100755 --- a/classes/Conversation.php +++ b/classes/Conversation.php @@ -74,4 +74,23 @@ class Conversation extends Memcached_DataObject return $conv; } + + static function noticeCount($id) + { + $keypart = sprintf('conversation:notice_count:%d', $id); + + $cnt = self::cacheGet($keypart); + + if ($cnt !== false) { + return $cnt; + } + + $notice = new Notice(); + $notice->conversation = $id; + $cnt = $notice->count(); + + self::cacheSet($keypart, $cnt); + + return $cnt; + } } diff --git a/classes/File_redirection.php b/classes/File_redirection.php index 1096f500bf..74e89db5f2 100644 --- a/classes/File_redirection.php +++ b/classes/File_redirection.php @@ -23,8 +23,6 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; require_once INSTALLDIR.'/classes/File.php'; require_once INSTALLDIR.'/classes/File_oembed.php'; -define('USER_AGENT', 'StatusNet user agent / file probe'); - /** * Table Definition for file_redirection */ diff --git a/classes/Group_block.php b/classes/Group_block.php index ffc57a496e..68feaef4de 100644 --- a/classes/Group_block.php +++ b/classes/Group_block.php @@ -35,7 +35,7 @@ class Group_block extends Memcached_DataObject public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP /* Static get */ - function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('Group_block',$k,$v); } + function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Group_block',$k,$v); } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE diff --git a/classes/Group_join_queue.php b/classes/Group_join_queue.php index 48b36cae2d..7df71ed042 100644 --- a/classes/Group_join_queue.php +++ b/classes/Group_join_queue.php @@ -56,6 +56,71 @@ class Group_join_queue extends Managed_DataObject return $rq; } + function getMember() + { + $member = Profile::staticGet('id', $this->profile_id); + + if (empty($member)) { + // TRANS: Exception thrown providing an invalid profile ID. + // TRANS: %s is the invalid profile ID. + throw new Exception(sprintf(_('Profile ID %s is invalid.'),$this->profile_id)); + } + + return $member; + } + + function getGroup() + { + $group = User_group::staticGet('id', $this->group_id); + + if (empty($group)) { + // TRANS: Exception thrown providing an invalid group ID. + // TRANS: %s is the invalid group ID. + throw new Exception(sprintf(_('Group ID %s is invalid.'),$this->group_id)); + } + + return $group; + } + + /** + * Abort the pending group join... + * + * @param User_group $group + */ + function abort() + { + $profile = $this->getMember(); + $group = $this->getGroup(); + if ($request) { + if (Event::handle('StartCancelJoinGroup', array($profile, $group))) { + $this->delete(); + Event::handle('EndCancelJoinGroup', array($profile, $group)); + } + } + } + + /** + * Complete a pending group join... + * + * @return Group_member object on success + */ + function complete() + { + $join = null; + $profile = $this->getMember(); + $group = $this->getGroup(); + if (Event::handle('StartJoinGroup', array($profile, $group))) { + $join = Group_member::join($group->id, $profile->id); + $this->delete(); + Event::handle('EndJoinGroup', array($profile, $group)); + } + if (!$join) { + throw new Exception('Internal error: group join failed.'); + } + $join->notify(); + return $join; + } + /** * Send notifications via email etc to group administrators about * this exciting new pending moderation queue item! diff --git a/classes/Inbox.php b/classes/Inbox.php index feaead249b..336bba048c 100644 --- a/classes/Inbox.php +++ b/classes/Inbox.php @@ -157,168 +157,11 @@ class Inbox extends Memcached_DataObject } } - function stream($user_id, $offset, $limit, $since_id, $max_id, $own=false) - { - $inbox = Inbox::staticGet('user_id', $user_id); - - if (empty($inbox)) { - $inbox = Inbox::fromNoticeInbox($user_id); - if (empty($inbox)) { - return array(); - } else { - $inbox->encache(); - } - } - - $ids = $inbox->unpack(); - - if (!empty($since_id)) { - $newids = array(); - foreach ($ids as $id) { - if ($id > $since_id) { - $newids[] = $id; - } - } - $ids = $newids; - } - - if (!empty($max_id)) { - $newids = array(); - foreach ($ids as $id) { - if ($id <= $max_id) { - $newids[] = $id; - } - } - $ids = $newids; - } - - $ids = array_slice($ids, $offset, $limit); - - return $ids; - } - - /** - * Wrapper for Inbox::stream() and Notice::getStreamByIds() returning - * additional items up to the limit if we were short due to deleted - * notices still being listed in the inbox. - * - * This is meant to assist threaded views, and optimizes paging for - * threadness. Not ideal for very late pages, as we have to bump about - * through all previous items. - * - * Should avoid duplicates in paging, though. - * - * @param int $user_id - * @param int $offset skip past the most recent N notices (after since_id checks) - * @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 $own ignored? - * @return array of Notice objects - * - * @todo consider repacking the inbox when this happens? - * @fixme reimplement $own if we need it? - */ - function streamNoticesThreaded($user_id, $offset, $limit, $since_id, $max_id, $own=false) - { - // So what we want is: - // * slurp in the beginning of the notice list - // * filter out deleted notices - // * replace any reply notices with their conversation roots - // * filter out any duplicate conversations - // * return $limit notices after skipping $offset from the most recent - - $ids = self::stream($user_id, 0, self::MAX_NOTICES, $since_id, $max_id, $own); - - // Do a bulk lookup for the first $limit items - // Fast path when nothing's deleted. - $firstChunk = array_slice($ids, 0, $offset + $limit); - $notices = NoticeStream::getStreamByIds($firstChunk); - - assert($notices instanceof ArrayWrapper); - $items = $notices->_items; - - // Extract the latest non-deleted item in each convo - $noticeByConvo = array(); - foreach ($items as $notice) { - if (empty($noticeByConvo[$notice->conversation])) { - $noticeByConvo[$notice->conversation] = $notice; - } - } - - $wanted = count($firstChunk); // raw entry count in the inbox up to our $limit - // There were deleted notices, we'll need to look for more. - $remainder = array_slice($ids, $limit); - - for ($i = $offset + $limit; count($noticeByConvo) < $wanted && $i < count($ids); $i++) { - $notice = Notice::staticGet($ids[$i]); - if ($notice && empty($noticeByConvo[$notice->conversation])) { - $noticeByConvo[$notice->conversation] = $notice; - } - } - - $slice = array_slice($noticeByConvo, $offset, $limit, false); - return new ArrayWrapper($slice); - } - - /** - * Wrapper for Inbox::stream() and Notice::getStreamByIds() returning - * additional items up to the limit if we were short due to deleted - * notices still being listed in the inbox. - * - * The fast path (when no items are deleted) should be just as fast; the - * offset parameter is applied *before* lookups for maximum efficiency. - * - * This means offset-based paging may show duplicates, but similar behavior - * already exists when new notices are posted between page views, so we - * think people will be ok with this until id-based paging is introduced - * to the user interface. - * - * @param int $user_id - * @param int $offset skip past the most recent N notices (after since_id checks) - * @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 $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, $own=false) - { - $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. - $firstChunk = array_slice($ids, 0, $limit); - $notices = NoticeStream::getStreamByIds($firstChunk); - - $wanted = count($firstChunk); // raw entry count in the inbox up to our $limit - if ($notices->N >= $wanted) { - return $notices; - } - - // There were deleted notices, we'll need to look for more. - assert($notices instanceof ArrayWrapper); - $items = $notices->_items; - $remainder = array_slice($ids, $limit); - - while (count($items) < $wanted && count($remainder) > 0) { - $notice = Notice::staticGet(array_shift($remainder)); - if ($notice) { - $items[] = $notice; - } else { - } - } - return new ArrayWrapper($items); - } - /** * Saves a list of integer notice_ids into a packed blob in this object. * @param array $ids list of integer notice_ids */ - protected function pack(array $ids) + function pack(array $ids) { $this->notice_ids = call_user_func_array('pack', array_merge(array('N*'), $ids)); } @@ -326,7 +169,7 @@ class Inbox extends Memcached_DataObject /** * @return array of integer notice_ids */ - protected function unpack() + function unpack() { return unpack('N*', $this->notice_ids); } diff --git a/classes/Invitation.php b/classes/Invitation.php index 0e87c1629c..27ff400883 100644 --- a/classes/Invitation.php +++ b/classes/Invitation.php @@ -14,6 +14,7 @@ class Invitation extends Memcached_DataObject public $user_id; // int(4) not_null public $address; // varchar(255) multiple_key not_null public $address_type; // varchar(8) multiple_key not_null + public $registered_user_id; // int(4) not_null public $created; // datetime() not_null /* Static get */ @@ -22,4 +23,11 @@ class Invitation extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + + function convert($user) + { + $orig = clone($this); + $this->registered_user_id = $user->id; + return $this->update($orig); + } } diff --git a/classes/Login_token.php b/classes/Login_token.php index 20d5d9dbce..7a9388c947 100644 --- a/classes/Login_token.php +++ b/classes/Login_token.php @@ -35,7 +35,7 @@ class Login_token extends Memcached_DataObject public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP /* Static get */ - function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('Login_token',$k,$v); } + function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Login_token',$k,$v); } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index 59809dc8f7..c361ba8783 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -273,24 +273,23 @@ class Memcached_DataObject extends Safe_DataObject function getSearchEngine($table) { require_once INSTALLDIR.'/lib/search_engines.php'; - static $search_engine; - if (!isset($search_engine)) { - if (Event::handle('GetSearchEngine', array($this, $table, &$search_engine))) { - if ('mysql' === common_config('db', 'type')) { - $type = common_config('search', 'type'); - if ($type == 'like') { - $search_engine = new MySQLLikeSearch($this, $table); - } else if ($type == 'fulltext') { - $search_engine = new MySQLSearch($this, $table); - } else { - // Low level exception. No need for i18n as discussed with Brion. - throw new ServerException('Unknown search type: ' . $type); - } + + if (Event::handle('GetSearchEngine', array($this, $table, &$search_engine))) { + if ('mysql' === common_config('db', 'type')) { + $type = common_config('search', 'type'); + if ($type == 'like') { + $search_engine = new MySQLLikeSearch($this, $table); + } else if ($type == 'fulltext') { + $search_engine = new MySQLSearch($this, $table); } else { - $search_engine = new PGSearch($this, $table); + // Low level exception. No need for i18n as discussed with Brion. + throw new ServerException('Unknown search type: ' . $type); } + } else { + $search_engine = new PGSearch($this, $table); } } + return $search_engine; } diff --git a/classes/Notice.php b/classes/Notice.php index 114119bfc9..87363158dd 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -73,6 +73,7 @@ class Notice extends Memcached_DataObject public $location_ns; // int(4) public $repeat_of; // int(4) public $object_type; // varchar(255) + public $scope; // int(4) /* Static get */ function staticGet($k,$v=NULL) @@ -89,17 +90,27 @@ class Notice extends Memcached_DataObject const LOCAL_NONPUBLIC = -1; const GATEWAY = -2; + const PUBLIC_SCOPE = 0; // Useful fake constant + const SITE_SCOPE = 1; + const ADDRESSEE_SCOPE = 2; + const GROUP_SCOPE = 4; + const FOLLOWER_SCOPE = 8; + + protected $_profile = -1; + function getProfile() { - $profile = Profile::staticGet('id', $this->profile_id); + if (is_int($this->_profile) && $this->_profile == -1) { + $this->_profile = Profile::staticGet('id', $this->profile_id); - if (empty($profile)) { - // TRANS: Server exception thrown when a user profile for a notice cannot be found. - // TRANS: %1$d is a profile ID (number), %2$d is a notice ID (number). - throw new ServerException(sprintf(_('No such profile (%1$d) for notice (%2$d).'), $this->profile_id, $this->id)); + if (empty($this->_profile)) { + // TRANS: Server exception thrown when a user profile for a notice cannot be found. + // TRANS: %1$d is a profile ID (number), %2$d is a notice ID (number). + throw new ServerException(sprintf(_('No such profile (%1$d) for notice (%2$d).'), $this->profile_id, $this->id)); + } } - return $profile; + return $this->_profile; } function delete() @@ -197,7 +208,7 @@ class Notice extends Memcached_DataObject if (!$id) { // TRANS: Server exception. %s are the error details. - throw new ServerException(sprintf(_('Database error inserting hashtag: %s'), + throw new ServerException(sprintf(_('Database error inserting hashtag: %s.'), $last_error->message)); return; } @@ -243,6 +254,7 @@ class Notice extends Memcached_DataObject * notice in place of extracting links from content * boolean 'distribute' whether to distribute the notice, default true * string 'object_type' URL of the associated object type (default ActivityObject::NOTE) + * int 'scope' Scope bitmask; default to SITE_SCOPE on private sites, 0 otherwise * * @fixme tag override * @@ -254,6 +266,7 @@ class Notice extends Memcached_DataObject 'url' => null, 'reply_to' => null, 'repeat_of' => null, + 'scope' => null, 'distribute' => true); if (!empty($options)) { @@ -333,17 +346,80 @@ class Notice extends Memcached_DataObject $notice->uri = $uri; $notice->url = $url; + // Get the groups here so we can figure out replies and such + + if (!isset($groups)) { + $groups = self::groupsFromText($notice->content, $profile); + } + + $reply = null; + // Handle repeat case if (isset($repeat_of)) { + + // Check for a private one + + $repeat = Notice::staticGet('id', $repeat_of); + + if (empty($repeat)) { + // TRANS: Client exception thrown in notice when trying to repeat a missing or deleted notice. + throw new ClientException(_('Cannot repeat; original notice is missing or deleted.')); + } + + if ($profile->id == $repeat->profile_id) { + // TRANS: Client error displayed when trying to repeat an own notice. + throw new ClientException(_('You cannot repeat your own notice.')); + } + + if ($repeat->scope != Notice::SITE_SCOPE && + $repeat->scope != Notice::PUBLIC_SCOPE) { + // TRANS: Client error displayed when trying to repeat a non-public notice. + throw new ClientException(_('Cannot repeat a private notice.'), 403); + } + + if (!$repeat->inScope($profile)) { + // The generic checks above should cover this, but let's be sure! + // TRANS: Client error displayed when trying to repeat a notice you cannot access. + throw new ClientException(_('Cannot repeat a notice you cannot read.'), 403); + } + + if ($profile->hasRepeated($repeat->id)) { + // TRANS: Client error displayed when trying to repeat an already repeated notice. + throw new ClientException(_('You already repeated that notice.')); + } + $notice->repeat_of = $repeat_of; } else { - $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final); - } + $reply = self::getReplyTo($reply_to, $profile_id, $source, $final); - if (!empty($notice->reply_to)) { - $reply = Notice::staticGet('id', $notice->reply_to); - $notice->conversation = $reply->conversation; + if (!empty($reply)) { + + if (!$reply->inScope($profile)) { + // TRANS: Client error displayed when trying to reply to a notice a the target has no access to. + // TRANS: %1$s is a user nickname, %2$d is a notice ID (number). + throw new ClientException(sprintf(_('%1$s has no access to notice %2$d.'), + $profile->nickname, $reply->id), 403); + } + + $notice->reply_to = $reply->id; + $notice->conversation = $reply->conversation; + + // If the original is private to a group, and notice has no group specified, + // make it to the same group(s) + + if (empty($groups) && ($reply->scope | Notice::GROUP_SCOPE)) { + $groups = array(); + $replyGroups = $reply->getGroups(); + foreach ($replyGroups as $group) { + if ($profile->isMember($group)) { + $groups[] = $group->id; + } + } + } + + // Scope set below + } } if (!empty($lat) && !empty($lon)) { @@ -368,6 +444,40 @@ class Notice extends Memcached_DataObject $notice->object_type = $object_type; } + if (is_null($scope)) { // 0 is a valid value + if (!empty($reply)) { + $notice->scope = $reply->scope; + } else { + $notice->scope = common_config('notice', 'defaultscope'); + } + } else { + $notice->scope = $scope; + } + + // For private streams + + $user = $profile->getUser(); + + if (!empty($user)) { + if ($user->private_stream && + ($notice->scope == Notice::PUBLIC_SCOPE || + $notice->scope == Notice::SITE_SCOPE)) { + $notice->scope |= Notice::FOLLOWER_SCOPE; + } + } + + // Force the scope for private groups + + foreach ($groups as $groupId) { + $group = User_group::staticGet('id', $groupId); + if (!empty($group)) { + if ($group->force_scope) { + $notice->scope |= Notice::GROUP_SCOPE; + break; + } + } + } + if (Event::handle('StartNoticeSave', array(&$notice))) { // XXX: some of these functions write to the DB @@ -431,11 +541,9 @@ class Notice extends Memcached_DataObject // Note: groups may save tags, so must be run after tags are saved // to avoid errors on duplicates. - if (isset($groups)) { - $notice->saveKnownGroups($groups); - } else { - $notice->saveGroups(); - } + // Note: groups should always be set. + + $notice->saveKnownGroups($groups); if (isset($urls)) { $notice->saveKnownUrls($urls); @@ -453,19 +561,20 @@ class Notice extends Memcached_DataObject function blowOnInsert($conversation = false) { - self::blow('profile:notice_ids:%d', $this->profile_id); + $this->blowStream('profile:notice_ids:%d', $this->profile_id); if ($this->isPublic()) { - self::blow('public'); + $this->blowStream('public'); } // XXX: Before we were blowing the casche only if the notice id // was not the root of the conversation. What to do now? self::blow('notice:conversation_ids:%d', $this->conversation); + self::blow('conversation::notice_count:%d', $this->conversation); if (!empty($this->repeat_of)) { - self::blow('notice:repeats:%d', $this->repeat_of); + $this->blowStream('notice:repeats:%d', $this->repeat_of); } $original = Notice::staticGet('id', $this->repeat_of); @@ -473,14 +582,20 @@ class Notice extends Memcached_DataObject if (!empty($original)) { $originalUser = User::staticGet('id', $original->profile_id); if (!empty($originalUser)) { - self::blow('user:repeats_of_me:%d', $originalUser->id); + $this->blowStream('user:repeats_of_me:%d', $originalUser->id); } } $profile = Profile::staticGet($this->profile_id); + if (!empty($profile)) { $profile->blowNoticeCount(); } + + $ptags = $this->getProfileTags(); + foreach ($ptags as $ptag) { + $ptag->blowNoticeStreamCache(); + } } /** @@ -503,6 +618,47 @@ class Notice extends Memcached_DataObject // In case we're the first, will need to calc a new root. self::blow('notice:conversation_root:%d', $this->conversation); } + + $ptags = $this->getProfileTags(); + foreach ($ptags as $ptag) { + $ptag->blowNoticeStreamCache(true); + } + } + + function blowStream() + { + $c = self::memcache(); + + if (empty($c)) { + return false; + } + + $args = func_get_args(); + + $format = array_shift($args); + + $keyPart = vsprintf($format, $args); + + $cacheKey = Cache::key($keyPart); + + $c->delete($cacheKey); + + // delete the "last" stream, too, if this notice is + // older than the top of that stream + + $lastKey = $cacheKey.';last'; + + $lastStr = $c->get($lastKey); + + if ($lastStr !== false) { + $window = explode(',', $lastStr); + $lastID = $window[0]; + $lastNotice = Notice::staticGet('id', $lastID); + if (empty($lastNotice) // just weird + || strtotime($lastNotice->created) >= strtotime($this->created)) { + $c->delete($lastKey); + } + } } /** save all urls in the notice to the db @@ -614,18 +770,34 @@ class Notice extends Memcached_DataObject } function attachments() { - // XXX: cache this - $att = array(); - $f2p = new File_to_post; - $f2p->post_id = $this->id; - if ($f2p->find()) { - while ($f2p->fetch()) { - $f = File::staticGet($f2p->file_id); - if ($f) { - $att[] = clone($f); + + $keypart = sprintf('notice:file_ids:%d', $this->id); + + $idstr = self::cacheGet($keypart); + + if ($idstr !== false) { + $ids = explode(',', $idstr); + } else { + $ids = array(); + $f2p = new File_to_post; + $f2p->post_id = $this->id; + if ($f2p->find()) { + while ($f2p->fetch()) { + $ids[] = $f2p->file_id; } } + self::cacheSet($keypart, implode(',', $ids)); } + + $att = array(); + + foreach ($ids as $id) { + $f = File::staticGet('id', $id); + if (!empty($f)) { + $att[] = clone($f); + } + } + return $att; } @@ -671,30 +843,65 @@ class Notice extends Memcached_DataObject * * @return Notice or null */ - function conversationRoot() + function conversationRoot($profile=-1) { - if (!empty($this->conversation)) { - $c = self::memcache(); + // XXX: can this happen? - $key = Cache::key('notice:conversation_root:' . $this->conversation); - $notice = $c->get($key); - if ($notice) { - return $notice; - } - - $notice = new Notice(); - $notice->conversation = $this->conversation; - $notice->orderBy('CREATED'); - $notice->limit(1); - $notice->find(true); - - if ($notice->N) { - $c->set($key, $notice); - return $notice; - } + if (empty($this->conversation)) { + return null; } - return null; + + // Get the current profile if not specified + + if (is_int($profile) && $profile == -1) { + $profile = Profile::current(); + } + + // If this notice is out of scope, no root for you! + + if (!$this->inScope($profile)) { + return null; + } + + // If this isn't a reply to anything, then it's its own + // root. + + if (empty($this->reply_to)) { + return $this; + } + + if (is_null($profile)) { + $keypart = sprintf('notice:conversation_root:%d:null', $this->id); + } else { + $keypart = sprintf('notice:conversation_root:%d:%d', + $this->id, + $profile->id); + } + + $root = self::cacheGet($keypart); + + if ($root !== false && $root->inScope($profile)) { + return $root; + } else { + $last = $this; + + do { + $parent = $last->getOriginal(); + if (!empty($parent) && $parent->inScope($profile)) { + $last = $parent; + continue; + } else { + $root = $last; + break; + } + } while (!empty($parent)); + + self::cacheSet($keypart, $root); + } + + return $root; } + /** * Pull up a full list of local recipients who will be getting * this notice in their inbox. Results will be cached, so don't @@ -726,6 +933,7 @@ class Notice extends Memcached_DataObject } $users = $this->getSubscribedUsers(); + $ptags = $this->getProfileTags(); // FIXME: kind of ignoring 'transitional'... // we'll probably stop supporting inboxless mode @@ -749,27 +957,39 @@ class Notice extends Memcached_DataObject } } + foreach ($ptags as $ptag) { + $users = $ptag->getUserSubscribers(); + foreach ($users as $id) { + if (!array_key_exists($id, $ni)) { + $user = User::staticGet('id', $id); + if (!$user->hasBlocked($profile)) { + $ni[$id] = NOTICE_INBOX_SOURCE_PROFILE_TAG; + } + } + } + } + foreach ($recipients as $recipient) { if (!array_key_exists($recipient, $ni)) { $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY; } - } - // Exclude any deleted, non-local, or blocking recipients. - $profile = $this->getProfile(); - $originalProfile = null; - if ($this->repeat_of) { - // Check blocks against the original notice's poster as well. - $original = Notice::staticGet('id', $this->repeat_of); - if ($original) { - $originalProfile = $original->getProfile(); + // Exclude any deleted, non-local, or blocking recipients. + $profile = $this->getProfile(); + $originalProfile = null; + if ($this->repeat_of) { + // Check blocks against the original notice's poster as well. + $original = Notice::staticGet('id', $this->repeat_of); + if ($original) { + $originalProfile = $original->getProfile(); + } } - } - foreach ($ni as $id => $source) { - $user = User::staticGet('id', $id); - if (empty($user) || $user->hasBlocked($profile) || - ($originalProfile && $user->hasBlocked($originalProfile))) { - unset($ni[$id]); + foreach ($ni as $id => $source) { + $user = User::staticGet('id', $id); + if (empty($user) || $user->hasBlocked($profile) || + ($originalProfile && $user->hasBlocked($originalProfile))) { + unset($ni[$id]); + } } } @@ -849,6 +1069,19 @@ class Notice extends Memcached_DataObject return $ids; } + function getProfileTags() + { + $profile = $this->getProfile(); + $list = $profile->getOtherTags($profile); + $ptags = array(); + + while($list->fetch()) { + $ptags[] = clone($list); + } + + return $ptags; + } + /** * Record this notice to the given group inboxes for delivery. * Overrides the regular parsing of !group markup. @@ -875,7 +1108,17 @@ class Notice extends Memcached_DataObject common_log_db_error($gi, 'INSERT', __FILE__); } - // @fixme should we save the tags here or not? + if (common_config('group', 'addtag')) { + // we automatically add a tag for every group name, too + + $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($group->nickname), + 'notice_id' => $this->id)); + + if (is_null($tag)) { + $this->saveTag($group->nickname); + } + } + $groups[] = clone($group); } else { common_log(LOG_ERR, "Local delivery to group id $id skipped, doesn't exist"); @@ -897,36 +1140,19 @@ class Notice extends Memcached_DataObject return array(); } - $groups = array(); - - /* extract all !group */ - $count = preg_match_all('/(?:^|\s)!(' . Nickname::DISPLAY_FMT . ')/', - strtolower($this->content), - $match); - if (!$count) { - return $groups; - } - $profile = $this->getProfile(); + $groups = self::groupsFromText($this->content, $profile); + /* Add them to the database */ - foreach (array_unique($match[1]) as $nickname) { + foreach ($groups as $group) { /* XXX: remote groups. */ - $group = User_group::getForNickname($nickname, $profile); if (empty($group)) { continue; } - // we automatically add a tag for every group name, too - - $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($nickname), - 'notice_id' => $this->id)); - - if (is_null($tag)) { - $this->saveTag($nickname); - } if ($profile->isMember($group)) { @@ -1004,15 +1230,8 @@ class Notice extends Memcached_DataObject continue; } - $reply = new Reply(); - - $reply->notice_id = $this->id; - $reply->profile_id = $profile->id; - $reply->modified = $this->created; - - common_log(LOG_INFO, __METHOD__ . ": saving reply: notice $this->id to profile $profile->id"); - - $id = $reply->insert(); + $this->saveReply($profile->id); + self::blow('reply:stream:%d', $profile->id); } return; @@ -1038,13 +1257,27 @@ class Notice extends Memcached_DataObject $sender = Profile::staticGet($this->profile_id); + $replied = array(); + + // If it's a reply, save for the replied-to author + + if (!empty($this->reply_to)) { + $original = $this->getOriginal(); + if (!empty($original)) { // that'd be weird + $author = $original->getProfile(); + if (!empty($author)) { + $this->saveReply($author->id); + $replied[$author->id] = 1; + self::blow('reply:stream:%d', $author->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) @@ -1065,23 +1298,9 @@ class Notice extends Memcached_DataObject continue; } - $reply = new Reply(); - - $reply->notice_id = $this->id; - $reply->profile_id = $mentioned->id; - $reply->modified = $this->created; - - $id = $reply->insert(); - - if (!$id) { - common_log_db_error($reply, 'INSERT', __FILE__); - // TRANS: Server exception thrown when a reply cannot be saved. - // TRANS: %1$d is a notice ID, %2$d is the ID of the mentioned user. - throw new ServerException(sprintf(_('Could not save reply for %1$d, %2$d.'), $this->id, $mentioned->id)); - } else { - $replied[$mentioned->id] = 1; - self::blow('reply:stream:%d', $mentioned->id); - } + $this->saveReply($mentioned->id); + $replied[$mentioned->id] = 1; + self::blow('reply:stream:%d', $mentioned->id); } } @@ -1090,6 +1309,19 @@ class Notice extends Memcached_DataObject return $recipientIds; } + function saveReply($profileId) + { + $reply = new Reply(); + + $reply->notice_id = $this->id; + $reply->profile_id = $profileId; + $reply->modified = $this->created; + + $reply->insert(); + + return $reply; + } + /** * Pull the complete list of @-reply targets for this notice. * @@ -1097,26 +1329,51 @@ class Notice extends Memcached_DataObject */ function getReplies() { - // XXX: cache me + $keypart = sprintf('notice:reply_ids:%d', $this->id); - $ids = array(); + $idstr = self::cacheGet($keypart); - $reply = new Reply(); - $reply->selectAdd(); - $reply->selectAdd('profile_id'); - $reply->notice_id = $this->id; + if ($idstr !== false) { + $ids = explode(',', $idstr); + } else { + $ids = array(); - if ($reply->find()) { - while($reply->fetch()) { - $ids[] = $reply->profile_id; + $reply = new Reply(); + $reply->selectAdd(); + $reply->selectAdd('profile_id'); + $reply->notice_id = $this->id; + + if ($reply->find()) { + while($reply->fetch()) { + $ids[] = $reply->profile_id; + } } + self::cacheSet($keypart, implode(',', $ids)); } - $reply->free(); - return $ids; } + /** + * Pull the complete list of @-reply targets for this notice. + * + * @return array of Profiles + */ + function getReplyProfiles() + { + $ids = $this->getReplies(); + $profiles = array(); + + foreach ($ids as $id) { + $profile = Profile::staticGet('id', $id); + if (!empty($profile)) { + $profiles[] = $profile; + } + } + + return $profiles; + } + /** * Send e-mail notifications to local @-reply targets. * @@ -1155,28 +1412,40 @@ class Notice extends Memcached_DataObject return array(); } - // XXX: cache me + $ids = array(); + + $keypart = sprintf('notice:groups:%d', $this->id); + + $idstr = self::cacheGet($keypart); + + if ($idstr !== false) { + $ids = explode(',', $idstr); + } else { + $gi = new Group_inbox(); + + $gi->selectAdd(); + $gi->selectAdd('group_id'); + + $gi->notice_id = $this->id; + + if ($gi->find()) { + while ($gi->fetch()) { + $ids[] = $gi->group_id; + } + } + + self::cacheSet($keypart, implode(',', $ids)); + } $groups = array(); - $gi = new Group_inbox(); - - $gi->selectAdd(); - $gi->selectAdd('group_id'); - - $gi->notice_id = $this->id; - - if ($gi->find()) { - while ($gi->fetch()) { - $group = User_group::staticGet('id', $gi->group_id); - if ($group) { - $groups[] = $group; - } + foreach ($ids as $id) { + $group = User_group::staticGet('id', $id); + if ($group) { + $groups[] = $group; } } - $gi->free(); - return $groups; } @@ -1262,9 +1531,9 @@ class Notice extends Memcached_DataObject $reply_ids = $this->getReplies(); foreach ($reply_ids as $id) { - $profile = Profile::staticGet('id', $id); - if (!empty($profile)) { - $ctx->attention[] = $profile->getUri(); + $rprofile = Profile::staticGet('id', $id); + if (!empty($rprofile)) { + $ctx->attention[] = $rprofile->getUri(); } } @@ -1459,7 +1728,7 @@ class Notice extends Memcached_DataObject if (!empty($reply_to)) { $reply_notice = Notice::staticGet('id', $reply_to); if (!empty($reply_notice)) { - return $reply_to; + return $reply_notice; } } @@ -1498,8 +1767,10 @@ class Notice extends Memcached_DataObject $last = $recipient->getCurrentNotice(); if (!empty($last)) { - return $last->id; + return $last; } + + return null; } static function maxContent() @@ -1535,6 +1806,15 @@ class Notice extends Memcached_DataObject return $location; } + /** + * Convenience function for posting a repeat of an existing message. + * + * @param int $repeater_id: profile ID of user doing the repeat + * @param string $source: posting source key, eg 'web', 'api', etc + * @return Notice + * + * @throws Exception on failure or permission problems + */ function repeat($repeater_id, $source) { $author = Profile::staticGet('id', $this->profile_id); @@ -1556,8 +1836,13 @@ class Notice extends Memcached_DataObject $content = mb_substr($content, 0, $maxlen - 4) . ' ...'; } - return self::saveNew($repeater_id, $content, $source, - array('repeat_of' => $this->id)); + // Scope is same as this one's + + return self::saveNew($repeater_id, + $content, + $source, + array('repeat_of' => $this->id, + 'scope' => $this->scope)); } // These are supposed to be in chron order! @@ -1880,14 +2165,24 @@ class Notice extends Memcached_DataObject public function getTags() { $tags = array(); - $tag = new Notice_tag(); - $tag->notice_id = $this->id; - if ($tag->find()) { - while ($tag->fetch()) { - $tags[] = $tag->tag; + + $keypart = sprintf('notice:tags:%d', $this->id); + + $tagstr = self::cacheGet($keypart); + + if ($tagstr !== false) { + $tags = explode(',', $tagstr); + } else { + $tag = new Notice_tag(); + $tag->notice_id = $this->id; + if ($tag->find()) { + while ($tag->fetch()) { + $tags[] = $tag->tag; + } } + self::cacheSet($keypart, implode(',', $tags)); } - $tag->free(); + return $tags; } @@ -2011,4 +2306,168 @@ class Notice extends Memcached_DataObject ($this->is_local != Notice::GATEWAY)); } } + + /** + * Check that the given profile is allowed to read, respond to, or otherwise + * act on this notice. + * + * The $scope member is a bitmask of scopes, representing a logical AND of the + * scope requirement. So, 0x03 (Notice::ADDRESSEE_SCOPE | Notice::SITE_SCOPE) means + * "only visible to people who are mentioned in the notice AND are users on this site." + * Users on the site who are not mentioned in the notice will not be able to see the + * notice. + * + * @param Profile $profile The profile to check; pass null to check for public/unauthenticated users. + * + * @return boolean whether the profile is in the notice's scope + */ + function inScope($profile) + { + if (is_null($profile)) { + $keypart = sprintf('notice:in-scope-for:%d:null', $this->id); + } else { + $keypart = sprintf('notice:in-scope-for:%d:%d', $this->id, $profile->id); + } + + $result = self::cacheGet($keypart); + + if ($result === false) { + $bResult = $this->_inScope($profile); + $result = ($bResult) ? 1 : 0; + self::cacheSet($keypart, $result, 0, 300); + } + + return ($result == 1) ? true : false; + } + + protected function _inScope($profile) + { + // If there's no scope, anyone (even anon) is in scope. + + if ($this->scope == 0) { + return true; + } + + // If there's scope, anon cannot be in scope + + if (empty($profile)) { + return false; + } + + // Author is always in scope + + if ($this->profile_id == $profile->id) { + return true; + } + + // Only for users on this site + + if ($this->scope & Notice::SITE_SCOPE) { + $user = $profile->getUser(); + if (empty($user)) { + return false; + } + } + + // Only for users mentioned in the notice + + if ($this->scope & Notice::ADDRESSEE_SCOPE) { + + // XXX: just query for the single reply + + $replies = $this->getReplies(); + + if (!in_array($profile->id, $replies)) { + return false; + } + } + + // Only for members of the given group + + if ($this->scope & Notice::GROUP_SCOPE) { + + // XXX: just query for the single membership + + $groups = $this->getGroups(); + + $foundOne = false; + + foreach ($groups as $group) { + if ($profile->isMember($group)) { + $foundOne = true; + break; + } + } + + if (!$foundOne) { + return false; + } + } + + // Only for followers of the author + + if ($this->scope & Notice::FOLLOWER_SCOPE) { + $author = $this->getProfile(); + if (!Subscription::exists($profile, $author)) { + return false; + } + } + + return true; + } + + static function groupsFromText($text, $profile) + { + $groups = array(); + + /* extract all !group */ + $count = preg_match_all('/(?:^|\s)!(' . Nickname::DISPLAY_FMT . ')/', + strtolower($text), + $match); + + if (!$count) { + return $groups; + } + + foreach (array_unique($match[1]) as $nickname) { + $group = User_group::getForNickname($nickname, $profile); + if (!empty($group) && $profile->isMember($group)) { + $groups[] = $group->id; + } + } + + return $groups; + } + + protected $_original = -1; + + function getOriginal() + { + if (is_int($this->_original) && $this->_original == -1) { + if (empty($this->reply_to)) { + $this->_original = null; + } else { + $this->_original = Notice::staticGet('id', $this->reply_to); + } + } + return $this->_original; + } + + /** + * Magic function called at serialize() time. + * + * We use this to drop a couple process-specific references + * from DB_DataObject which can cause trouble in future + * processes. + * + * @return array of variable names to include in serialization. + */ + + function __sleep() + { + $vars = parent::__sleep(); + $skip = array('_original', '_profile'); + return array_diff($vars, $skip); + } + } diff --git a/classes/Profile.php b/classes/Profile.php index b582451350..2638f745f7 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -52,9 +52,15 @@ class Profile extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + protected $_user = -1; // Uninitialized value distinct from null + function getUser() { - return User::staticGet('id', $this->id); + if (is_int($this->_user) && $this->_user == -1) { + $this->_user = User::staticGet('id', $this->id); + } + + return $this->_user; } function getAvatar($width, $height=null) @@ -62,9 +68,17 @@ class Profile extends Memcached_DataObject if (is_null($height)) { $height = $width; } - return Avatar::pkeyGet(array('profile_id' => $this->id, - 'width' => $width, - 'height' => $height)); + + $avatar = null; + + if (Event::handle('StartProfileGetAvatar', array($this, $width, &$avatar))) { + $avatar = Avatar::pkeyGet(array('profile_id' => $this->id, + 'width' => $width, + 'height' => $height)); + Event::handle('EndProfileGetAvatar', array($this, $width, &$avatar)); + } + + return $avatar; } function getOriginalAvatar() @@ -168,7 +182,7 @@ class Profile extends Memcached_DataObject function getFancyName() { if ($this->fullname) { - // TRANS: Full name of a profile or group followed by nickname in parens + // TRANS: Full name of a profile or group (%1$s) followed by nickname (%2$s) in parentheses. return sprintf(_m('FANCYNAME','%1$s (%2$s)'), $this->fullname, $this->nickname); } else { return $this->nickname; @@ -180,7 +194,6 @@ class Profile extends Memcached_DataObject * * @return mixed Notice or null */ - function getCurrentNotice() { $notice = $this->getNotices(0, 1); @@ -212,31 +225,16 @@ class Profile extends Memcached_DataObject function isMember($group) { - $mem = new Group_member(); - - $mem->group_id = $group->id; - $mem->profile_id = $this->id; - - if ($mem->find()) { - return true; - } else { - return false; - } + $gm = Group_member::pkeyGet(array('profile_id' => $this->id, + 'group_id' => $group->id)); + return (!empty($gm)); } function isAdmin($group) { - $mem = new Group_member(); - - $mem->group_id = $group->id; - $mem->profile_id = $this->id; - $mem->is_admin = 1; - - if ($mem->find()) { - return true; - } else { - return false; - } + $gm = Group_member::pkeyGet(array('profile_id' => $this->id, + 'group_id' => $group->id)); + return (!empty($gm) && $gm->is_admin); } function isPendingMember($group) @@ -246,30 +244,247 @@ class Profile extends Memcached_DataObject return !empty($request); } - function getGroups($offset=0, $limit=null) + function getGroups($offset=0, $limit=PROFILES_PER_PAGE) { - $qry = - 'SELECT user_group.* ' . - 'FROM user_group JOIN group_member '. - 'ON user_group.id = group_member.group_id ' . - 'WHERE group_member.profile_id = %d ' . - 'ORDER BY group_member.created DESC '; + $ids = array(); - if ($offset>0 && !is_null($limit)) { - if ($offset) { - if (common_config('db','type') == 'pgsql') { - $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; - } else { - $qry .= ' LIMIT ' . $offset . ', ' . $limit; + $keypart = sprintf('profile:groups:%d', $this->id); + + $idstring = self::cacheGet($keypart); + + if ($idstring !== false) { + $ids = explode(',', $idstring); + } else { + $gm = new Group_member(); + + $gm->profile_id = $this->id; + + if ($gm->find()) { + while ($gm->fetch()) { + $ids[] = $gm->group_id; } } + + self::cacheSet($keypart, implode(',', $ids)); + } + + $groups = array(); + + foreach ($ids as $id) { + $group = User_group::staticGet('id', $id); + if (!empty($group)) { + $groups[] = $group; + } } - $groups = new User_group(); + return new ArrayWrapper($groups); + } - $cnt = $groups->query(sprintf($qry, $this->id)); + function isTagged($peopletag) + { + $tag = Profile_tag::pkeyGet(array('tagger' => $peopletag->tagger, + 'tagged' => $this->id, + 'tag' => $peopletag->tag)); + return !empty($tag); + } - return $groups; + function canTag($tagged) + { + if (empty($tagged)) { + return false; + } + + if ($tagged->id == $this->id) { + return true; + } + + $all = common_config('peopletag', 'allow_tagging', 'all'); + $local = common_config('peopletag', 'allow_tagging', 'local'); + $remote = common_config('peopletag', 'allow_tagging', 'remote'); + $subs = common_config('peopletag', 'allow_tagging', 'subs'); + + if ($all) { + return true; + } + + $tagged_user = $tagged->getUser(); + if (!empty($tagged_user)) { + if ($local) { + return true; + } + } else if ($subs) { + return (Subscription::exists($this, $tagged) || + Subscription::exists($tagged, $this)); + } else if ($remote) { + return true; + } + return false; + } + + function getLists($auth_user, $offset=0, $limit=null, $since_id=0, $max_id=0) + { + $ids = array(); + + $keypart = sprintf('profile:lists:%d', $this->id); + + $idstr = self::cacheGet($keypart); + + if ($idstr !== false) { + $ids = explode(',', $idstr); + } else { + $list = new Profile_list(); + $list->selectAdd(); + $list->selectAdd('id'); + $list->tagger = $this->id; + $list->selectAdd('id as "cursor"'); + + if ($since_id>0) { + $list->whereAdd('id > '.$since_id); + } + + if ($max_id>0) { + $list->whereAdd('id <= '.$max_id); + } + + if($offset>=0 && !is_null($limit)) { + $list->limit($offset, $limit); + } + + $list->orderBy('id DESC'); + + if ($list->find()) { + while ($list->fetch()) { + $ids[] = $list->id; + } + } + + self::cacheSet($keypart, implode(',', $ids)); + } + + $showPrivate = (($auth_user instanceof User || + $auth_user instanceof Profile) && + $auth_user->id === $this->id); + + $lists = array(); + + foreach ($ids as $id) { + $list = Profile_list::staticGet('id', $id); + if (!empty($list) && + ($showPrivate || !$list->private)) { + + if (!isset($list->cursor)) { + $list->cursor = $list->id; + } + + $lists[] = $list; + } + } + + return new ArrayWrapper($lists); + } + + function getOtherTags($auth_user=null, $offset=0, $limit=null, $since_id=0, $max_id=0) + { + $lists = new Profile_list(); + + $tags = new Profile_tag(); + $tags->tagged = $this->id; + + $lists->joinAdd($tags); + #@fixme: postgres (round(date_part('epoch', my_date))) + $lists->selectAdd('unix_timestamp(profile_tag.modified) as "cursor"'); + + if ($auth_user instanceof User || $auth_user instanceof Profile) { + $lists->whereAdd('( ( profile_list.private = false ) ' . + 'OR ( profile_list.tagger = ' . $auth_user->id . ' AND ' . + 'profile_list.private = true ) )'); + } else { + $lists->private = false; + } + + if ($since_id>0) { + $lists->whereAdd('cursor > '.$since_id); + } + + if ($max_id>0) { + $lists->whereAdd('cursor <= '.$max_id); + } + + if($offset>=0 && !is_null($limit)) { + $lists->limit($offset, $limit); + } + + $lists->orderBy('profile_tag.modified DESC'); + $lists->find(); + + return $lists; + } + + function getPrivateTags($offset=0, $limit=null, $since_id=0, $max_id=0) + { + $tags = new Profile_list(); + $tags->private = true; + $tags->tagger = $this->id; + + if ($since_id>0) { + $tags->whereAdd('id > '.$since_id); + } + + if ($max_id>0) { + $tags->whereAdd('id <= '.$max_id); + } + + if($offset>=0 && !is_null($limit)) { + $tags->limit($offset, $limit); + } + + $tags->orderBy('id DESC'); + $tags->find(); + + return $tags; + } + + function hasLocalTags() + { + $tags = new Profile_tag(); + + $tags->joinAdd(array('tagger', 'user:id')); + $tags->whereAdd('tagged = '.$this->id); + $tags->whereAdd('tagger != '.$this->id); + + $tags->limit(0, 1); + $tags->fetch(); + + return ($tags->N == 0) ? false : true; + } + + function getTagSubscriptions($offset=0, $limit=null, $since_id=0, $max_id=0) + { + $lists = new Profile_list(); + $subs = new Profile_tag_subscription(); + + $lists->joinAdd($subs); + #@fixme: postgres (round(date_part('epoch', my_date))) + $lists->selectAdd('unix_timestamp(profile_tag_subscription.created) as "cursor"'); + + $lists->whereAdd('profile_tag_subscription.profile_id = '.$this->id); + + if ($since_id>0) { + $lists->whereAdd('cursor > '.$since_id); + } + + if ($max_id>0) { + $lists->whereAdd('cursor <= '.$max_id); + } + + if($offset>=0 && !is_null($limit)) { + $lists->limit($offset, $limit); + } + + $lists->orderBy('"cursor" DESC'); + $lists->find(); + + return $lists; } /** @@ -287,6 +502,7 @@ class Profile extends Memcached_DataObject } else { if (Event::handle('StartJoinGroup', array($group, $this))) { $join = Group_member::join($group->id, $this->id); + self::blow('profile:groups:%d', $this->id); Event::handle('EndJoinGroup', array($group, $this)); } } @@ -297,49 +513,6 @@ class Profile extends Memcached_DataObject return $join; } - /** - * Cancel a pending group join... - * - * @param User_group $group - */ - function cancelJoinGroup(User_group $group) - { - $request = Group_join_queue::pkeyGet(array('profile_id' => $this->id, - 'group_id' => $group->id)); - if ($request) { - if (Event::handle('StartCancelJoinGroup', array($group, $this))) { - $request->delete(); - Event::handle('EndCancelJoinGroup', array($group, $this)); - } - } - } - - /** - * Complete a pending group join on our end... - * - * @param User_group $group - */ - function completeJoinGroup(User_group $group) - { - $join = null; - $request = Group_join_queue::pkeyGet(array('profile_id' => $this->id, - 'group_id' => $group->id)); - if ($request) { - if (Event::handle('StartJoinGroup', array($group, $this))) { - $join = Group_member::join($group->id, $this->id); - $request->delete(); - Event::handle('EndJoinGroup', array($group, $this)); - } - } else { - // TRANS: Exception thrown trying to approve a non-existing group join request. - throw new Exception(_('Invalid group join approval: not pending.')); - } - if ($join) { - $join->notify(); - } - return $join; - } - /** * Leave a group that this profile is a member of. * @@ -349,6 +522,7 @@ class Profile extends Memcached_DataObject { if (Event::handle('StartLeaveGroup', array($group, $this))) { Group_member::leave($group->id, $this->id); + self::blow('profile:groups:%d', $this->id); Event::handle('EndLeaveGroup', array($group, $this)); } } @@ -399,6 +573,61 @@ class Profile extends Memcached_DataObject return new ArrayWrapper($profiles); } + function getTaggedSubscribers($tag) + { + $qry = + 'SELECT profile.* ' . + 'FROM profile JOIN (subscription, profile_tag, profile_list) ' . + 'ON profile.id = subscription.subscriber ' . + 'AND profile.id = profile_tag.tagged ' . + 'AND profile_tag.tagger = profile_list.tagger AND profile_tag.tag = profile_list.tag ' . + 'WHERE subscription.subscribed = %d ' . + 'AND subscription.subscribed != subscription.subscriber ' . + 'AND profile_tag.tagger = %d AND profile_tag.tag = "%s" ' . + 'AND profile_list.private = false ' . + 'ORDER BY subscription.created DESC'; + + $profile = new Profile(); + $tagged = array(); + + $cnt = $profile->query(sprintf($qry, $this->id, $this->id, $tag)); + + while ($profile->fetch()) { + $tagged[] = clone($profile); + } + return $tagged; + } + + /** + * Get pending subscribers, who have not yet been approved. + * + * @param int $offset + * @param int $limit + * @return Profile + */ + function getRequests($offset=0, $limit=null) + { + $qry = + 'SELECT profile.* ' . + 'FROM profile JOIN subscription_queue '. + 'ON profile.id = subscription_queue.subscriber ' . + 'WHERE subscription_queue.subscribed = %d ' . + 'ORDER BY subscription_queue.created DESC '; + + if ($limit != null) { + if (common_config('db','type') == 'pgsql') { + $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; + } else { + $qry .= ' LIMIT ' . $offset . ', ' . $limit; + } + } + + $members = new Profile(); + + $members->query(sprintf($qry, $this->id)); + return $members; + } + function subscriptionCount() { $c = Cache::instance(); @@ -457,6 +686,17 @@ class Profile extends Memcached_DataObject return Subscription::exists($this, $other); } + /** + * Check if a pending subscription request is outstanding for this... + * + * @param Profile $other + * @return boolean + */ + function hasPendingSubscription($other) + { + return Subscription_queue::exists($this, $other); + } + /** * Are these two profiles subscribed to each other? * @@ -471,34 +711,6 @@ class Profile extends Memcached_DataObject function hasFave($notice) { - $cache = Cache::instance(); - - // XXX: Kind of a hack. - - if (!empty($cache)) { - // This is the stream of favorite notices, in rev chron - // order. This forces it into cache. - - $ids = Fave::idStream($this->id, 0, CachingNoticeStream::CACHE_WINDOW); - - // If it's in the list, then it's a fave - - if (in_array($notice->id, $ids)) { - return true; - } - - // If we're not past the end of the cache window, - // then the cache has all available faves, so this one - // is not a fave. - - if (count($ids) < CachingNoticeStream::CACHE_WINDOW) { - return false; - } - - // Otherwise, cache doesn't have all faves; - // fall through to the default - } - $fave = Fave::pkeyGet(array('user_id' => $this->id, 'notice_id' => $notice->id)); return ((is_null($fave)) ? false : true); @@ -1090,4 +1302,72 @@ class Profile extends Memcached_DataObject return $profile; } + + function canRead(Notice $notice) + { + if ($notice->scope & Notice::SITE_SCOPE) { + $user = $this->getUser(); + if (empty($user)) { + return false; + } + } + + if ($notice->scope & Notice::ADDRESSEE_SCOPE) { + $replies = $notice->getReplies(); + + if (!in_array($this->id, $replies)) { + $groups = $notice->getGroups(); + + $foundOne = false; + + foreach ($groups as $group) { + if ($this->isMember($group)) { + $foundOne = true; + break; + } + } + + if (!$foundOne) { + return false; + } + } + } + + if ($notice->scope & Notice::FOLLOWER_SCOPE) { + $author = $notice->getProfile(); + if (!Subscription::exists($this, $author)) { + return false; + } + } + + return true; + } + + static function current() + { + $user = common_current_user(); + if (empty($user)) { + $profile = null; + } else { + $profile = $user->getProfile(); + } + return $profile; + } + + /** + * Magic function called at serialize() time. + * + * We use this to drop a couple process-specific references + * from DB_DataObject which can cause trouble in future + * processes. + * + * @return array of variable names to include in serialization. + */ + + function __sleep() + { + $vars = parent::__sleep(); + $skip = array('_user'); + return array_diff($vars, $skip); + } } diff --git a/classes/Profile_list.php b/classes/Profile_list.php new file mode 100644 index 0000000000..17c2ffd4f4 --- /dev/null +++ b/classes/Profile_list.php @@ -0,0 +1,924 @@ +. + * + * @category Notices + * @package StatusNet + * @author Shashi Gowda + * @license GNU Affero General Public License http://www.gnu.org/licenses/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Table Definition for profile_list + */ +require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; + +class Profile_list extends Memcached_DataObject +{ + ###START_AUTOCODE + /* the code below is auto generated do not remove the above tag */ + + public $__table = 'profile_list'; // table name + public $id; // int(4) primary_key not_null + public $tagger; // int(4) + public $tag; // varchar(64) + public $description; // text + public $private; // tinyint(1) + 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) + public $tagged_count; // smallint + public $subscriber_count; // smallint + + /* Static get */ + function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('Profile_list',$k,$v); } + + /* the code above is auto generated do not remove the tag below */ + ###END_AUTOCODE + + /** + * return a profile_list record, given its tag and tagger. + * + * @param array $kv ideally array('tag' => $tag, 'tagger' => $tagger) + * + * @return Profile_list a Profile_list object with the given tag and tagger. + */ + + function pkeyGet($kv) + { + return Memcached_DataObject::pkeyGet('Profile_list', $kv); + } + + /** + * get the tagger of this profile_list object + * + * @return Profile the tagger + */ + + function getTagger() + { + return Profile::staticGet('id', $this->tagger); + } + + /** + * return a string to identify this + * profile_list in the user interface etc. + * + * @return String + */ + + function getBestName() + { + return $this->tag; + } + + /** + * return a uri string for this profile_list + * + * @return String uri + */ + + function getUri() + { + $uri = null; + if (Event::handle('StartProfiletagGetUri', array($this, &$uri))) { + if (!empty($this->uri)) { + $uri = $this->uri; + } else { + $uri = common_local_url('profiletagbyid', + array('id' => $this->id, 'tagger_id' => $this->tagger)); + } + } + Event::handle('EndProfiletagGetUri', array($this, &$uri)); + return $uri; + } + + /** + * return a url to the homepage of this item + * + * @return String home url + */ + + function homeUrl() + { + $url = null; + if (Event::handle('StartUserPeopletagHomeUrl', 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('showprofiletag', + array('tagger' => $this->getTagger()->nickname, + 'tag' => $this->tag)); + } + } + Event::handle('EndUserPeopletagHomeUrl', array($this, &$url)); + return $url; + } + + /** + * return an immutable url for this object + * + * @return String permalink + */ + + function permalink() + { + $url = null; + if (Event::handle('StartProfiletagPermalink', array($this, &$url))) { + $url = common_local_url('profiletagbyid', + array('id' => $this->id)); + } + Event::handle('EndProfiletagPermalink', array($this, &$url)); + return $url; + } + + /** + * Query notices by users associated with this tag, + * but first check the cache before hitting the DB. + * + * @param integer $offset offset + * @param integer $limit maximum no of results + * @param integer $since_id=null since this id + * @param integer $max_id=null maximum id in result + * + * @return Notice the query + */ + + function getNotices($offset, $limit, $since_id=null, $max_id=null) + { + $stream = new PeopletagNoticeStream($this); + + return $stream->getNotices($offset, $limit, $since_id, $max_id); + } + + /** + * Get subscribers (local and remote) to this people tag + * Order by reverse chronology + * + * @param integer $offset offset + * @param integer $limit maximum no of results + * @param integer $since_id=null since unix timestamp + * @param integer $upto=null maximum unix timestamp when subscription was made + * + * @return Profile results + */ + + function getSubscribers($offset=0, $limit=null, $since=0, $upto=0) + { + $subs = new Profile(); + $sub = new Profile_tag_subscription(); + $sub->profile_tag_id = $this->id; + + $subs->joinAdd($sub); + $subs->selectAdd('unix_timestamp(profile_tag_subscription.' . + 'created) as "cursor"'); + + if ($since != 0) { + $subs->whereAdd('cursor > ' . $since); + } + + if ($upto != 0) { + $subs->whereAdd('cursor <= ' . $upto); + } + + if ($limit != null) { + $subs->limit($offset, $limit); + } + + $subs->orderBy('profile_tag_subscription.created DESC'); + $subs->find(); + + return $subs; + } + + /** + * Get all and only local subscribers to this people tag + * used for distributing notices to user inboxes. + * + * @return array ids of users + */ + + function getUserSubscribers() + { + // XXX: cache this + + $user = new User(); + if(common_config('db','quote_identifiers')) + $user_table = '"user"'; + else $user_table = 'user'; + + $qry = + 'SELECT id ' . + 'FROM '. $user_table .' JOIN profile_tag_subscription '. + 'ON '. $user_table .'.id = profile_tag_subscription.profile_id ' . + 'WHERE profile_tag_subscription.profile_tag_id = %d '; + + $user->query(sprintf($qry, $this->id)); + + $ids = array(); + + while ($user->fetch()) { + $ids[] = $user->id; + } + + $user->free(); + + return $ids; + } + + /** + * Check to see if a given profile has + * subscribed to this people tag's timeline + * + * @param mixed $id User or Profile object or integer id + * + * @return boolean subscription status + */ + + function hasSubscriber($id) + { + if (!is_numeric($id)) { + $id = $id->id; + } + + $sub = Profile_tag_subscription::pkeyGet(array('profile_tag_id' => $this->id, + 'profile_id' => $id)); + return !empty($sub); + } + + /** + * Get profiles tagged with this people tag, + * include modified timestamp as a "cursor" field + * order by descending order of modified time + * + * @param integer $offset offset + * @param integer $limit maximum no of results + * @param integer $since_id=null since unix timestamp + * @param integer $upto=null maximum unix timestamp when subscription was made + * + * @return Profile results + */ + + function getTagged($offset=0, $limit=null, $since=0, $upto=0) + { + $tagged = new Profile(); + $tagged->joinAdd(array('id', 'profile_tag:tagged')); + + #@fixme: postgres + $tagged->selectAdd('unix_timestamp(profile_tag.modified) as "cursor"'); + $tagged->whereAdd('profile_tag.tagger = '.$this->tagger); + $tagged->whereAdd("profile_tag.tag = '{$this->tag}'"); + + if ($since != 0) { + $tagged->whereAdd('cursor > ' . $since); + } + + if ($upto != 0) { + $tagged->whereAdd('cursor <= ' . $upto); + } + + if ($limit != null) { + $tagged->limit($offset, $limit); + } + + $tagged->orderBy('profile_tag.modified DESC'); + $tagged->find(); + + return $tagged; + } + + /** + * Gracefully delete one or many people tags + * along with their members and subscriptions data + * + * @return boolean success + */ + + function delete() + { + // force delete one item at a time. + if (empty($this->id)) { + $this->find(); + while ($this->fetch()) { + $this->delete(); + } + } + + Profile_tag::cleanup($this); + Profile_tag_subscription::cleanup($this); + + self::blow('profile:lists:%d', $this->tagger); + + return parent::delete(); + } + + /** + * Update a people tag gracefully + * also change "tag" fields in profile_tag table + * + * @param Profile_list $orig Object's original form + * + * @return boolean success + */ + + function update($orig=null) + { + $result = true; + + if (!is_object($orig) && !$orig instanceof Profile_list) { + parent::update($orig); + } + + // if original tag was different + // check to see if the new tag already exists + // if not, rename the tag correctly + if($orig->tag != $this->tag || $orig->tagger != $this->tagger) { + $existing = Profile_list::getByTaggerAndTag($this->tagger, $this->tag); + if(!empty($existing)) { + // TRANS: Server exception. + throw new ServerException(_('The tag you are trying to rename ' . + 'to already exists.')); + } + // move the tag + // XXX: allow OStatus plugin to send out profile tag + $result = Profile_tag::moveTag($orig, $this); + } + parent::update($orig); + return $result; + } + + /** + * return an xml string representing this people tag + * as the author of an atom feed + * + * @return string atom author element + */ + + function asAtomAuthor() + { + $xs = new XMLStringer(true); + + $tagger = $this->getTagger(); + $xs->elementStart('author'); + $xs->element('name', null, '@' . $tagger->nickname . '/' . $this->tag); + $xs->element('uri', null, $this->permalink()); + $xs->elementEnd('author'); + + return $xs->getString(); + } + + /** + * return an xml string to represent this people tag + * as the subject of an activitystreams feed. + * + * @return string activitystreams subject + */ + + function asActivitySubject() + { + return $this->asActivityNoun('subject'); + } + + /** + * return an xml string to represent this people tag + * as a noun in an activitystreams feed. + * + * @param string $element the xml tag + * + * @return string activitystreams noun + */ + + function asActivityNoun($element) + { + $noun = ActivityObject::fromPeopletag($this); + return $noun->asString('activity:' . $element); + } + + /** + * get the cached number of profiles tagged with this + * people tag, re-count if the argument is true. + * + * @param boolean $recount whether to ignore cache + * + * @return integer count + */ + + function taggedCount($recount=false) + { + $keypart = sprintf('profile_list:tagged_count:%d:%s', + $this->tagger, + $this->tag); + + $count = self::cacheGet($keypart); + + if ($count === false) { + $tags = new Profile_tag(); + + $tags->tag = $this->tag; + $tags->tagger = $this->tagger; + + $count = $tags->count('distinct tagged'); + + self::cacheSet($keypart, $count); + } + + return $count; + } + + /** + * get the cached number of profiles subscribed to this + * people tag, re-count if the argument is true. + * + * @param boolean $recount whether to ignore cache + * + * @return integer count + */ + + function subscriberCount($recount=false) + { + $keypart = sprintf('profile_list:subscriber_count:%d', + $this->id); + + $count = self::cacheGet($keypart); + + if ($count === false) { + + $sub = new Profile_tag_subscription(); + $sub->profile_tag_id = $this->id; + $count = (int) $sub->count('distinct profile_id'); + + self::cacheSet($keypart, $count); + } + + return $count; + } + + /** + * get the cached number of profiles subscribed to this + * people tag, re-count if the argument is true. + * + * @param boolean $recount whether to ignore cache + * + * @return integer count + */ + + function blowNoticeStreamCache($all=false) + { + self::blow('profile_list:notice_ids:%d', $this->id); + if ($all) { + self::blow('profile_list:notice_ids:%d;last', $this->id); + } + } + + /** + * get the Profile_list object by the + * given tagger and with given tag + * + * @param integer $tagger the id of the creator profile + * @param integer $tag the tag + * + * @return integer count + */ + + static function getByTaggerAndTag($tagger, $tag) + { + $ptag = Profile_list::pkeyGet(array('tagger' => $tagger, 'tag' => $tag)); + return $ptag; + } + + /** + * create a profile_list record for a tag, tagger pair + * if it doesn't exist, return it. + * + * @param integer $tagger the tagger + * @param string $tag the tag + * @param string $description description + * @param boolean $private protected or not + * + * @return Profile_list the people tag object + */ + + static function ensureTag($tagger, $tag, $description=null, $private=false) + { + $ptag = Profile_list::getByTaggerAndTag($tagger, $tag); + + if(empty($ptag->id)) { + $args = array( + 'tag' => $tag, + 'tagger' => $tagger, + 'description' => $description, + 'private' => $private + ); + + $new_tag = Profile_list::saveNew($args); + + return $new_tag; + } + return $ptag; + } + + /** + * get the maximum number of characters + * that can be used in the description of + * a people tag. + * + * determined by $config['peopletag']['desclimit'] + * if not set, falls back to $config['site']['textlimit'] + * + * @return integer maximum number of characters + */ + + static function maxDescription() + { + $desclimit = common_config('peopletag', 'desclimit'); + // null => use global limit (distinct from 0!) + if (is_null($desclimit)) { + $desclimit = common_config('site', 'textlimit'); + } + return $desclimit; + } + + /** + * check if the length of given text exceeds + * character limit. + * + * @param string $desc the description + * + * @return boolean is the descripition too long? + */ + + static function descriptionTooLong($desc) + { + $desclimit = self::maxDescription(); + return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit)); + } + + /** + * save a new people tag, this should be always used + * since it makes uri, homeurl, created and modified + * timestamps and performs checks. + * + * @param array $fields an array with fields and their values + * + * @return mixed Profile_list on success, false on fail + */ + static function saveNew($fields) { + extract($fields); + + $ptag = new Profile_list(); + + $ptag->query('BEGIN'); + + if (empty($tagger)) { + // TRANS: Server exception saving new tag without having a tagger specified. + throw new Exception(_('No tagger specified.')); + } + + if (empty($tag)) { + // TRANS: Server exception saving new tag without having a tag specified. + throw new Exception(_('No tag specified.')); + } + + if (empty($mainpage)) { + $mainpage = null; + } + + if (empty($uri)) { + // fill in later... + $uri = null; + } + + if (empty($mainpage)) { + $mainpage = null; + } + + if (empty($description)) { + $description = null; + } + + if (empty($private)) { + $private = false; + } + + $ptag->tagger = $tagger; + $ptag->tag = $tag; + $ptag->description = $description; + $ptag->private = $private; + $ptag->uri = $uri; + $ptag->mainpage = $mainpage; + $ptag->created = common_sql_now(); + $ptag->modified = common_sql_now(); + + $result = $ptag->insert(); + + if (!$result) { + common_log_db_error($ptag, 'INSERT', __FILE__); + // TRANS: Server exception saving new tag. + throw new ServerException(_('Could not create profile tag.')); + } + + if (!isset($uri) || empty($uri)) { + $orig = clone($ptag); + $ptag->uri = common_local_url('profiletagbyid', array('id' => $ptag->id, 'tagger_id' => $ptag->tagger)); + $result = $ptag->update($orig); + if (!$result) { + common_log_db_error($ptag, 'UPDATE', __FILE__); + // TRANS: Server exception saving new tag. + throw new ServerException(_('Could not set profile tag URI.')); + } + } + + if (!isset($mainpage) || empty($mainpage)) { + $orig = clone($ptag); + $user = User::staticGet('id', $ptag->tagger); + if(!empty($user)) { + $ptag->mainpage = common_local_url('showprofiletag', array('tag' => $ptag->tag, 'tagger' => $user->nickname)); + } else { + $ptag->mainpage = $uri; // assume this is a remote peopletag and the uri works + } + + $result = $ptag->update($orig); + if (!$result) { + common_log_db_error($ptag, 'UPDATE', __FILE__); + // TRANS: Server exception saving new tag. + throw new ServerException(_('Could not set profile tag mainpage.')); + } + } + return $ptag; + } + + /** + * get all items at given cursor position for api + * + * @param callback $fn a function that takes the following arguments in order: + * $offset, $limit, $since_id, $max_id + * and returns a Profile_list object after making the DB query + * @param array $args arguments required for $fn + * @param integer $cursor the cursor + * @param integer $count max. number of results + * + * Algorithm: + * - if cursor is 0, return empty list + * - if cursor is -1, get first 21 items, next_cursor = 20th prev_cursor = 0 + * - if cursor is +ve get 22 consecutive items before starting at cursor + * - return items[1..20] if items[0] == cursor else return items[0..21] + * - prev_cursor = items[1] + * - next_cursor = id of the last item being returned + * + * - if cursor is -ve get 22 consecutive items after cursor starting at cursor + * - return items[1..20] + * + * @returns array (array (mixed items), int next_cursor, int previous_cursor) + */ + + // XXX: This should be in Memcached_DataObject... eventually. + + static function getAtCursor($fn, $args, $cursor, $count=20) + { + $items = array(); + + $since_id = 0; + $max_id = 0; + $next_cursor = 0; + $prev_cursor = 0; + + if($cursor > 0) { + // if cursor is +ve fetch $count+2 items before cursor starting at cursor + $max_id = $cursor; + $fn_args = array_merge($args, array(0, $count+2, 0, $max_id)); + $list = call_user_func_array($fn, $fn_args); + while($list->fetch()) { + $items[] = clone($list); + } + + if ((isset($items[0]->cursor) && $items[0]->cursor == $cursor) || + $items[0]->id == $cursor) { + array_shift($items); + $prev_cursor = isset($items[0]->cursor) ? + -$items[0]->cursor : -$items[0]->id; + } else { + if (count($items) > $count+1) { + array_shift($items); + } + // this means the cursor item has been deleted, check to see if there are more + $fn_args = array_merge($args, array(0, 1, $cursor)); + $more = call_user_func($fn, $fn_args); + if (!$more->fetch() || empty($more)) { + // no more items. + $prev_cursor = 0; + } else { + $prev_cursor = isset($items[0]->cursor) ? + -$items[0]->cursor : -$items[0]->id; + } + } + + if (count($items)==$count+1) { + // this means there is a next page. + $next = array_pop($items); + $next_cursor = isset($next->cursor) ? + $items[$count-1]->cursor : $items[$count-1]->id; + } + + } else if($cursor < -1) { + // if cursor is -ve fetch $count+2 items created after -$cursor-1 + $cursor = abs($cursor); + $since_id = $cursor-1; + + $fn_args = array_merge($args, array(0, $count+2, $since_id)); + $list = call_user_func_array($fn, $fn_args); + while($list->fetch()) { + $items[] = clone($list); + } + + $end = count($items)-1; + if ((isset($items[$end]->cursor) && $items[$end]->cursor == $cursor) || + $items[$end]->id == $cursor) { + array_pop($items); + $next_cursor = isset($items[$end-1]->cursor) ? + $items[$end-1]->cursor : $items[$end-1]->id; + } else { + $next_cursor = isset($items[$end]->cursor) ? + $items[$end]->cursor : $items[$end]->id; + if ($end > $count) array_pop($items); // excess item. + + // check if there are more items for next page + $fn_args = array_merge($args, array(0, 1, 0, $cursor)); + $more = call_user_func_array($fn, $fn_args); + if (!$more->fetch() || empty($more)) { + $next_cursor = 0; + } + } + + if (count($items) == $count+1) { + // this means there is a previous page. + $prev = array_shift($items); + $prev_cursor = isset($prev->cursor) ? + -$items[0]->cursor : -$items[0]->id; + } + } else if($cursor == -1) { + $fn_args = array_merge($args, array(0, $count+1)); + $list = call_user_func_array($fn, $fn_args); + + while($list->fetch()) { + $items[] = clone($list); + } + + if (count($items)==$count+1) { + $next = array_pop($items); + if(isset($next->cursor)) { + $next_cursor = $items[$count-1]->cursor; + } else { + $next_cursor = $items[$count-1]->id; + } + } + + } + return array($items, $next_cursor, $prev_cursor); + } + + /** + * save a collection of people tags into the cache + * + * @param string $ckey cache key + * @param Profile_list &$tag the results to store + * @param integer $offset offset for slicing results + * @param integer $limit maximum number of results + * + * @return boolean success + */ + + static function setCache($ckey, &$tag, $offset=0, $limit=null) { + $cache = Cache::instance(); + if (empty($cache)) { + return false; + } + $str = ''; + $tags = array(); + while ($tag->fetch()) { + $str .= $tag->tagger . ':' . $tag->tag . ';'; + $tags[] = clone($tag); + } + $str = substr($str, 0, -1); + if ($offset>=0 && !is_null($limit)) { + $tags = array_slice($tags, $offset, $limit); + } + + $tag = new ArrayWrapper($tags); + + return self::cacheSet($ckey, $str); + } + + /** + * get people tags from the cache + * + * @param string $ckey cache key + * @param integer $offset offset for slicing + * @param integer $limit limit + * + * @return Profile_list results + */ + + static function getCached($ckey, $offset=0, $limit=null) { + + $keys_str = self::cacheGet($ckey); + if ($keys_str === false) { + return false; + } + + $pairs = explode(';', $keys_str); + $keys = array(); + foreach ($pairs as $pair) { + $keys[] = explode(':', $pair); + } + + if ($offset>=0 && !is_null($limit)) { + $keys = array_slice($keys, $offset, $limit); + } + return self::getByKeys($keys); + } + + /** + * get Profile_list objects from the database + * given their (tag, tagger) key pairs. + * + * @param array $keys array of array(tagger, tag) + * + * @return Profile_list results + */ + + static function getByKeys($keys) { + $cache = Cache::instance(); + + if (!empty($cache)) { + $tags = array(); + + foreach ($keys as $key) { + $t = Profile_list::getByTaggerAndTag($key[0], $key[1]); + if (!empty($t)) { + $tags[] = $t; + } + } + return new ArrayWrapper($tags); + } else { + $tag = new Profile_list(); + if (empty($keys)) { + //if no IDs requested, just return the tag object + return $tag; + } + + $pairs = array(); + foreach ($keys as $key) { + $pairs[] = '(' . $key[0] . ', "' . $key[1] . '")'; + } + + $tag->whereAdd('(tagger, tag) in (' . implode(', ', $pairs) . ')'); + + $tag->find(); + + $temp = array(); + + while ($tag->fetch()) { + $temp[$tag->tagger.'-'.$tag->tag] = clone($tag); + } + + $wrapped = array(); + + foreach ($keys as $key) { + $id = $key[0].'-'.$key[1]; + if (array_key_exists($id, $temp)) { + $wrapped[] = $temp[$id]; + } + } + + return new ArrayWrapper($wrapped); + } + } + + function insert() + { + $result = parent::insert(); + if ($result) { + self::blow('profile:lists:%d', $this->tagger); + } + return $result; + } +} diff --git a/classes/Profile_tag.php b/classes/Profile_tag.php index bd45ce0b45..9e475e83ec 100644 --- a/classes/Profile_tag.php +++ b/classes/Profile_tag.php @@ -22,31 +22,91 @@ class Profile_tag extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - static function getTags($tagger, $tagged) { - $tags = array(); + function pkeyGet($kv) { + return Memcached_DataObject::pkeyGet('Profile_tag', $kv); + } - // XXX: store this in memcached + function links() + { + return array('tagger,tag' => 'profile_list:tagger,tag'); + } - $profile_tag = new Profile_tag(); - $profile_tag->tagger = $tagger; - $profile_tag->tagged = $tagged; + function getMeta() + { + return Profile_list::pkeyGet(array('tagger' => $this->tagger, 'tag' => $this->tag)); + } - $profile_tag->find(); + static function getTags($tagger, $tagged, $auth_user=null) { - while ($profile_tag->fetch()) { - $tags[] = $profile_tag->tag; + $profile_list = new Profile_list(); + $include_priv = 1; + + if (!($auth_user instanceof User || + $auth_user instanceof Profile) || + ($auth_user->id !== $tagger)) { + + $profile_list->private = false; + $include_priv = 0; } - $profile_tag->free(); + $key = sprintf('profile_tag:tagger_tagged_privacy:%d-%d-%d', $tagger, $tagged, $include_priv); + $tags = Profile_list::getCached($key); + if ($tags !== false) { + return $tags; + } + + $profile_tag = new Profile_tag(); + $profile_list->tagger = $tagger; + $profile_tag->tagged = $tagged; + + $profile_list->selectAdd(); + + // only fetch id, tag, mainpage and + // private hoping this will be faster + $profile_list->selectAdd('profile_list.id, ' . + 'profile_list.tag, ' . + 'profile_list.mainpage, ' . + 'profile_list.private'); + $profile_list->joinAdd($profile_tag); + $profile_list->find(); + + Profile_list::setCache($key, $profile_list); + + return $profile_list; + } + + static function getTagsArray($tagger, $tagged, $auth_user_id=null) + { + $ptag = new Profile_tag(); + $ptag->tagger = $tagger; + $ptag->tagged = $tagged; + + if ($tagger != $auth_user_id) { + $list = new Profile_list(); + $list->private = false; + $ptag->joinAdd($list); + $ptag->selectAdd(); + $ptag->selectAdd('profile_tag.tag'); + } + + $tags = array(); + $ptag->find(); + while ($ptag->fetch()) { + $tags[] = $ptag->tag; + } + $ptag->free(); return $tags; } - static function setTags($tagger, $tagged, $newtags) { - $newtags = array_unique($newtags); - $oldtags = Profile_tag::getTags($tagger, $tagged); + static function setTags($tagger, $tagged, $newtags, $privacy=array()) { - // Delete stuff that's old that not in new + $newtags = array_unique($newtags); + $oldtags = self::getTagsArray($tagger, $tagged, $tagger); + + $ptag = new Profile_tag(); + + // Delete stuff that's in old and not in new $to_delete = array_diff($oldtags, $newtags); @@ -54,33 +114,166 @@ class Profile_tag extends Memcached_DataObject $to_insert = array_diff($newtags, $oldtags); - $profile_tag = new Profile_tag(); - - $profile_tag->tagger = $tagger; - $profile_tag->tagged = $tagged; - - $profile_tag->query('BEGIN'); - foreach ($to_delete as $deltag) { - $profile_tag->tag = $deltag; - $result = $profile_tag->delete(); - if (!$result) { - common_log_db_error($profile_tag, 'DELETE', __FILE__); - return false; - } + self::unTag($tagger, $tagged, $deltag); } foreach ($to_insert as $instag) { - $profile_tag->tag = $instag; - $result = $profile_tag->insert(); - if (!$result) { - common_log_db_error($profile_tag, 'INSERT', __FILE__); - return false; - } + $private = isset($privacy[$instag]) ? $privacy[$instag] : false; + self::setTag($tagger, $tagged, $instag, null, $private); + } + return true; + } + + # set a single tag + static function setTag($tagger, $tagged, $tag, $desc=null, $private=false) { + + $ptag = Profile_tag::pkeyGet(array('tagger' => $tagger, + 'tagged' => $tagged, + 'tag' => $tag)); + + # if tag already exists, return it + if(!empty($ptag)) { + return $ptag; } - $profile_tag->query('COMMIT'); + $tagger_profile = Profile::staticGet('id', $tagger); + $tagged_profile = Profile::staticGet('id', $tagged); + if (Event::handle('StartTagProfile', array($tagger_profile, $tagged_profile, $tag))) { + + if (!$tagger_profile->canTag($tagged_profile)) { + // TRANS: Client exception thrown trying to set a tag for a user that cannot be tagged. + throw new ClientException(_('You cannot tag this user.')); + return false; + } + + $tags = new Profile_list(); + $tags->tagger = $tagger; + $count = (int) $tags->count('distinct tag'); + + if ($count >= common_config('peopletag', 'maxtags')) { + // TRANS: Client exception thrown trying to set more tags than allowed. + throw new ClientException(sprintf(_('You already have created %d or more tags ' . + 'which is the maximum allowed number of tags. ' . + 'Try using or deleting some existing tags.'), + common_config('peopletag', 'maxtags'))); + return false; + } + + $plist = new Profile_list(); + $plist->query('BEGIN'); + + $profile_list = Profile_list::ensureTag($tagger, $tag, $desc, $private); + + if ($profile_list->taggedCount() >= common_config('peopletag', 'maxpeople')) { + // TRANS: Client exception thrown when trying to add more people than allowed to a list. + throw new ClientException(sprintf(_('You already have %1$d or more people in list %2$s, ' . + 'which is the maximum allowed number.' . + 'Try unlisting others first.'), + common_config('peopletag', 'maxpeople'), $tag)); + return false; + } + + $newtag = new Profile_tag(); + + $newtag->tagger = $tagger; + $newtag->tagged = $tagged; + $newtag->tag = $tag; + + $result = $newtag->insert(); + + + if (!$result) { + common_log_db_error($newtag, 'INSERT', __FILE__); + return false; + } + + try { + $plist->query('COMMIT'); + Event::handle('EndTagProfile', array($newtag)); + } catch (Exception $e) { + $newtag->delete(); + $profile_list->delete(); + throw $e; + return false; + } + + $profile_list->taggedCount(true); + self::blowCaches($tagger, $tagged); + } + + return $newtag; + } + + static function unTag($tagger, $tagged, $tag) { + $ptag = Profile_tag::pkeyGet(array('tagger' => $tagger, + 'tagged' => $tagged, + 'tag' => $tag)); + if (!$ptag) { + return true; + } + + if (Event::handle('StartUntagProfile', array($ptag))) { + $orig = clone($ptag); + $result = $ptag->delete(); + if (!$result) { + common_log_db_error($this, 'DELETE', __FILE__); + return false; + } + Event::handle('EndUntagProfile', array($orig)); + if ($result) { + $profile_list = Profile_list::pkeyGet(array('tag' => $tag, 'tagger' => $tagger)); + if (!empty($profile_list)) { + $profile_list->taggedCount(true); + } + self::blowCaches($tagger, $tagged); + return true; + } + return false; + } + } + + // @fixme: move this to Profile_list? + static function cleanup($profile_list) { + $ptag = new Profile_tag(); + $ptag->tagger = $profile_list->tagger; + $ptag->tag = $profile_list->tag; + $ptag->find(); + + while($ptag->fetch()) { + if (Event::handle('StartUntagProfile', array($ptag))) { + $orig = clone($ptag); + $result = $ptag->delete(); + if (!$result) { + common_log_db_error($this, 'DELETE', __FILE__); + } + Event::handle('EndUntagProfile', array($orig)); + } + } + } + + // move a tag! + static function moveTag($orig, $new) { + $tags = new Profile_tag(); + $qry = 'UPDATE profile_tag SET ' . + 'tag = "%s", tagger = "%s" ' . + 'WHERE tag = "%s" ' . + 'AND tagger = "%s"'; + $result = $tags->query(sprintf($qry, $new->tag, $new->tagger, + $orig->tag, $orig->tagger)); + + if (!$result) { + common_log_db_error($tags, 'UPDATE', __FILE__); + return false; + } + return true; + } + + static function blowCaches($tagger, $tagged) { + foreach (array(0, 1) as $perm) { + self::blow(sprintf('profile_tag:tagger_tagged_privacy:%d-%d-%d', $tagger, $tagged, $perm)); + } return true; } @@ -96,6 +289,28 @@ class Profile_tag extends Memcached_DataObject while ($profile->fetch()) { $tagged[] = clone($profile); } - return $tagged; + return true; + } + + function insert() + { + $result = parent::insert(); + if ($result) { + self::blow('profile_list:tagged_count:%d:%s', + $this->tagger, + $this->tag); + } + return $result; + } + + function delete() + { + $result = parent::delete(); + if ($result) { + self::blow('profile_list:tagged_count:%d:%s', + $this->tagger, + $this->tag); + } + return $result; } } diff --git a/classes/Profile_tag_subscription.php b/classes/Profile_tag_subscription.php new file mode 100644 index 0000000000..031405f531 --- /dev/null +++ b/classes/Profile_tag_subscription.php @@ -0,0 +1,125 @@ +private) { + return false; + } + + if (Event::handle('StartSubscribePeopletag', array($peopletag, $profile))) { + $args = array('profile_tag_id' => $peopletag->id, + 'profile_id' => $profile->id); + $existing = Profile_tag_subscription::pkeyGet($args); + if(!empty($existing)) { + return $existing; + } + + $sub = new Profile_tag_subscription(); + $sub->profile_tag_id = $peopletag->id; + $sub->profile_id = $profile->id; + $sub->created = common_sql_now(); + + $result = $sub->insert(); + + if (!$result) { + common_log_db_error($sub, 'INSERT', __FILE__); + // TRANS: Exception thrown when inserting a list subscription in the database fails. + throw new Exception(_('Adding list subscription failed.')); + } + + $ptag = Profile_list::staticGet('id', $peopletag->id); + $ptag->subscriberCount(true); + + Event::handle('EndSubscribePeopletag', array($peopletag, $profile)); + return $ptag; + } + } + + static function remove($peopletag, $profile) + { + $sub = Profile_tag_subscription::pkeyGet(array('profile_tag_id' => $peopletag->id, + 'profile_id' => $profile->id)); + + if (empty($sub)) { + // silence is golden? + return true; + } + + if (Event::handle('StartUnsubscribePeopletag', array($peopletag, $profile))) { + $result = $sub->delete(); + + if (!$result) { + common_log_db_error($sub, 'DELETE', __FILE__); + // TRANS: Exception thrown when deleting a list subscription from the database fails. + throw new Exception(_('Removing list subscription failed.')); + } + + $peopletag->subscriberCount(true); + + Event::handle('EndUnsubscribePeopletag', array($peopletag, $profile)); + return true; + } + } + + // called if a tag gets deleted / made private + static function cleanup($profile_list) { + $subs = new self(); + $subs->profile_tag_id = $profile_list->id; + $subs->find(); + + while($subs->fetch()) { + $profile = Profile::staticGet('id', $subs->profile_id); + Event::handle('StartUnsubscribePeopletag', array($profile_list, $profile)); + // Delete anyway + $subs->delete(); + Event::handle('StartUnsubscribePeopletag', array($profile_list, $profile)); + } + } + + function insert() + { + $result = parent::insert(); + if ($result) { + self::blow('profile_list:subscriber_count:%d', + $this->profile_tag_id); + } + return $result; + } + + function delete() + { + $result = parent::delete(); + if ($result) { + self::blow('profile_list:subscriber_count:%d', + $this->profile_tag_id); + } + return $result; + } +} diff --git a/classes/Status_network.php b/classes/Status_network.php index 5d01e72ccc..c3acacf595 100644 --- a/classes/Status_network.php +++ b/classes/Status_network.php @@ -74,7 +74,10 @@ class Status_network extends Safe_DataObject $config['db']['database_'.$dbname] = "mysqli://$dbuser:$dbpass@$dbhost/$dbname"; $config['db']['ini_'.$dbname] = INSTALLDIR.'/classes/status_network.ini'; - $config['db']['table_status_network'] = $dbname; + + foreach (array('status_network', 'status_network_tag', 'unavailable_status_network') as $table) { + $config['db']['table_'.$table] = $dbname; + } if (class_exists('Memcache')) { self::$cache = new Memcache(); diff --git a/classes/Status_network_tag.php b/classes/Status_network_tag.php index 2273ecb2e5..00ff0d8520 100644 --- a/classes/Status_network_tag.php +++ b/classes/Status_network_tag.php @@ -129,4 +129,15 @@ class Status_network_tag extends Safe_DataObject $this->decache(); return $ret; } + + static function withTag($tag) + { + $snt = new Status_network_tag(); + + $snt->tag = $tag; + + $snt->find(); + + return $snt; + } } diff --git a/classes/Subscription.php b/classes/Subscription.php index 797e6fef1c..8af414b3a7 100644 --- a/classes/Subscription.php +++ b/classes/Subscription.php @@ -27,6 +27,7 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; class Subscription extends Memcached_DataObject { const CACHE_WINDOW = 201; + const FORCE = true; ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -58,11 +59,12 @@ class Subscription extends Memcached_DataObject * * @param Profile $subscriber party to receive new notices * @param Profile $other party sending notices; publisher + * @param bool $force pass Subscription::FORCE to override local subscription approval * - * @return Subscription new subscription + * @return mixed Subscription or Subscription_queue: new subscription info */ - static function start($subscriber, $other) + static function start($subscriber, $other, $force=false) { // @fixme should we enforce this as profiles in callers instead? if ($subscriber instanceof User) { @@ -88,35 +90,39 @@ class Subscription extends Memcached_DataObject } if (Event::handle('StartSubscribe', array($subscriber, $other))) { - $sub = self::saveNew($subscriber->id, $other->id); - $sub->notify(); - - self::blow('user:notices_with_friends:%d', $subscriber->id); - - self::blow('subscription:by-subscriber:'.$subscriber->id); - self::blow('subscription:by-subscribed:'.$other->id); - - $subscriber->blowSubscriptionCount(); - $other->blowSubscriberCount(); - $otherUser = User::staticGet('id', $other->id); + if ($otherUser && $otherUser->subscribe_policy == User::SUBSCRIBE_POLICY_MODERATE && !$force) { + $sub = Subscription_queue::saveNew($subscriber, $other); + $sub->notify(); + } else { + $sub = self::saveNew($subscriber->id, $other->id); + $sub->notify(); - if (!empty($otherUser) && - $otherUser->autosubscribe && - !self::exists($other, $subscriber) && - !$subscriber->hasBlocked($other)) { + self::blow('user:notices_with_friends:%d', $subscriber->id); - try { - self::start($other, $subscriber); - } catch (Exception $e) { - common_log(LOG_ERR, "Exception during autosubscribe of {$other->nickname} to profile {$subscriber->id}: {$e->getMessage()}"); + self::blow('subscription:by-subscriber:'.$subscriber->id); + self::blow('subscription:by-subscribed:'.$other->id); + + $subscriber->blowSubscriptionCount(); + $other->blowSubscriberCount(); + + if (!empty($otherUser) && + $otherUser->autosubscribe && + !self::exists($other, $subscriber) && + !$subscriber->hasBlocked($other)) { + + try { + self::start($other, $subscriber); + } catch (Exception $e) { + common_log(LOG_ERR, "Exception during autosubscribe of {$other->nickname} to profile {$subscriber->id}: {$e->getMessage()}"); + } } } Event::handle('EndSubscribe', array($subscriber, $other)); } - return true; + return $sub; } /** @@ -261,8 +267,8 @@ class Subscription extends Memcached_DataObject common_date_iso8601($this->created)); $act->time = strtotime($this->created); - // TRANS: Activity tile when subscribing to another person. - $act->title = _("Follow"); + // TRANS: Activity title when subscribing to another person. + $act->title = _m('TITLE','Follow'); // TRANS: Notification given when one person starts following another. // TRANS: %1$s is the subscriber, %2$s is the subscribed. $act->content = sprintf(_('%1$s is now following %2$s.'), @@ -295,7 +301,6 @@ class Subscription extends Memcached_DataObject * * @return Subscription stream of subscriptions; use fetch() to iterate */ - static function bySubscriber($subscriberId, $offset = 0, $limit = PROFILES_PER_PAGE) @@ -356,7 +361,6 @@ class Subscription extends Memcached_DataObject * * @return Subscription stream of subscriptions; use fetch() to iterate */ - static function bySubscribed($subscribedId, $offset = 0, $limit = PROFILES_PER_PAGE) @@ -414,7 +418,6 @@ class Subscription extends Memcached_DataObject * * @return boolean success flag. */ - function update($orig=null) { $result = parent::update($orig); diff --git a/classes/Subscription_queue.php b/classes/Subscription_queue.php new file mode 100644 index 0000000000..19cd71c6a8 --- /dev/null +++ b/classes/Subscription_queue.php @@ -0,0 +1,105 @@ + 'Holder for subscription requests awaiting moderation.', + 'fields' => array( + 'subscriber' => array('type' => 'int', 'not null' => true, 'description' => 'remote or local profile making the request'), + 'subscribed' => array('type' => 'int', 'not null' => true, 'description' => 'remote or local profile being subscribed to'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + ), + 'primary key' => array('subscriber', 'subscribed'), + 'indexes' => array( + 'subscription_queue_subscriber_created_idx' => array('subscriber', 'created'), + 'subscription_queue_subscribed_created_idx' => array('subscribed', 'created'), + ), + 'foreign keys' => array( + 'subscription_queue_subscriber_fkey' => array('profile', array('subscriber' => 'id')), + 'subscription_queue_subscribed_fkey' => array('profile', array('subscribed' => 'id')), + ) + ); + } + + public static function saveNew(Profile $subscriber, Profile $subscribed) + { + $rq = new Subscription_queue(); + $rq->subscriber = $subscriber->id; + $rq->subscribed = $subscribed->id; + $rq->created = common_sql_now(); + $rq->insert(); + return $rq; + } + + function exists($subscriber, $other) + { + $sub = Subscription_queue::pkeyGet(array('subscriber' => $subscriber->id, + 'subscribed' => $other->id)); + return (empty($sub)) ? false : true; + } + + /** + * Complete a pending subscription, as we've got approval of some sort. + * + * @return Subscription + */ + public function complete() + { + $subscriber = Profile::staticGet('id', $this->subscriber); + $subscribed = Profile::staticGet('id', $this->subscribed); + $sub = Subscription::start($subscriber, $subscribed, Subscription::FORCE); + if ($sub) { + $this->delete(); + } + return $sub; + } + + /** + * Cancel an outstanding subscription request to the other profile. + */ + public function abort() + { + $subscriber = Profile::staticGet('id', $this->subscriber); + $subscribed = Profile::staticGet('id', $this->subscribed); + if (Event::handle('StartCancelSubscription', array($subscriber, $subscribed))) { + $this->delete(); + Event::handle('EndCancelSubscription', array($subscriber, $subscribed)); + } + } + + /** + * Send notifications via email etc to group administrators about + * this exciting new pending moderation queue item! + */ + public function notify() + { + $other = Profile::staticGet('id', $this->subscriber); + $listenee = User::staticGet('id', $this->subscribed); + mail_subscribe_pending_notify_profile($listenee, $other); + } +} diff --git a/classes/Unavailable_status_network.php b/classes/Unavailable_status_network.php new file mode 100644 index 0000000000..7eefac2c1f --- /dev/null +++ b/classes/Unavailable_status_network.php @@ -0,0 +1,84 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2011, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Keeps a list of unavailable status network names + * + * @category Data + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see Managed_DataObject + */ + +class Unavailable_status_network extends Managed_DataObject +{ + public $__table = 'unavailable_status_network'; // table name + + public $nickname; // varchar(64) UUID + public $created; // datetime + + /** + * Get an instance by key + * + * @param string $k Key to use to lookup (usually 'id' for this class) + * @param mixed $v Value to lookup + * + * @return Happening object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('Unavailable_status_network', $k, $v); + } + + /** + * The One True Thingy that must be defined and declared. + */ + public static function schemaDef() + { + return array( + 'description' => 'An unavailable status network nickname', + 'fields' => array( + 'nickname' => array('type' => 'varchar', + 'length' => 64, + 'not null' => true, 'description' => 'nickname not to use'), + 'created' => array('type' => 'datetime', + 'not null' => true), + ), + 'primary key' => array('nickname'), + ); + } +} diff --git a/classes/User.php b/classes/User.php index 1a3a7dfd72..9f79549327 100644 --- a/classes/User.php +++ b/classes/User.php @@ -30,6 +30,9 @@ require_once 'Validate.php'; class User extends Memcached_DataObject { + const SUBSCRIBE_POLICY_OPEN = 0; + const SUBSCRIBE_POLICY_MODERATE = 1; + ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -55,10 +58,12 @@ class User extends Memcached_DataObject public $smsemail; // varchar(255) public $uri; // varchar(255) unique_key public $autosubscribe; // tinyint(1) + public $subscribe_policy; // tinyint(1) public $urlshorteningservice; // varchar(50) default_ur1.ca public $inboxed; // tinyint(1) public $design_id; // int(4) public $viewdesigns; // tinyint(1) default_1 + public $private_stream; // tinyint(1) default_0 public $created; // datetime() not_null public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP @@ -68,16 +73,21 @@ class User extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + protected $_profile = -1; + /** * @return Profile */ function getProfile() { - $profile = Profile::staticGet('id', $this->id); - if (empty($profile)) { - throw new UserNoProfileException($this); + if (is_int($this->_profile) && $this->_profile == -1) { // invalid but distinct from null + $this->_profile = Profile::staticGet('id', $this->id); + if (empty($this->_profile)) { + throw new UserNoProfileException($this); + } } - return $profile; + + return $this->_profile; } function isSubscribed($other) @@ -86,6 +96,12 @@ class User extends Memcached_DataObject return $profile->isSubscribed($other); } + function hasPendingSubscription($other) + { + $profile = $this->getProfile(); + return $profile->hasPendingSubscription($other); + } + // 'update' won't write key columns, so we have to do it ourselves. function updateKeys(&$orig) @@ -247,6 +263,8 @@ class User extends Memcached_DataObject $user->nickname = $nickname; + $invite = null; + // Users who respond to invite email have proven their ownership of that address if (!empty($code)) { @@ -337,6 +355,12 @@ class User extends Memcached_DataObject return false; } + // Mark that this invite was converted + + if (!empty($invite)) { + $invite->convert($user); + } + if (!empty($email) && !$user->email) { $confirm = new Confirm_address(); @@ -467,34 +491,45 @@ class User extends Memcached_DataObject return Fave::stream($this->id, $offset, $limit, $own, $since_id, $max_id); } + function noticeInbox($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) + { + $stream = new InboxNoticeStream($this); + return $stream->getNotices($offset, $limit, $since_id, $before_id); + } + + // DEPRECATED, use noticeInbox() + 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, false); + return $this->noticeInbox($offset, $limit, $since_id, $before_id); } + // DEPRECATED, use noticeInbox() + function noticesWithFriendsThreaded($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { - return Inbox::streamNoticesThreaded($this->id, $offset, $limit, $since_id, $before_id, false); + return $this->noticeInbox($offset, $limit, $since_id, $before_id); } - 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, true); - } + // DEPRECATED, use noticeInbox() function noticeInboxThreaded($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { - return Inbox::streamNoticesThreaded($this->id, $offset, $limit, $since_id, $before_id, true); + return $this->noticeInbox($offset, $limit, $since_id, $before_id); } + // DEPRECATED, use noticeInbox() + 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, false); + return $this->noticeInbox($offset, $limit, $since_id, $before_id); } + // DEPRECATED, use noticeInbox() + 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, true); + $this->noticeInbox($offset, $limit, $since_id, $before_id); } function blowFavesCache() @@ -505,12 +540,12 @@ class User extends Memcached_DataObject function getSelfTags() { - return Profile_tag::getTags($this->id, $this->id); + return Profile_tag::getTagsArray($this->id, $this->id, $this->id); } - function setSelfTags($newtags) + function setSelfTags($newtags, $privacy) { - return Profile_tag::setTags($this->id, $this->id, $newtags); + return Profile_tag::setTags($this->id, $this->id, $newtags, $privacy); } function block($other) @@ -782,7 +817,8 @@ class User extends Memcached_DataObject function repeatedToMe($offset=0, $limit=20, $since_id=null, $max_id=null) { - throw new Exception("Not implemented since inbox change."); + // TRANS: Exception thrown when trying view "repeated to me". + throw new Exception(_('Not implemented since inbox change.')); } function shareLocation() @@ -956,4 +992,20 @@ class User extends Memcached_DataObject return $apps; } + /** + * Magic function called at serialize() time. + * + * We use this to drop a couple process-specific references + * from DB_DataObject which can cause trouble in future + * processes. + * + * @return array of variable names to include in serialization. + */ + + function __sleep() + { + $vars = parent::__sleep(); + $skip = array('_profile'); + return array_diff($vars, $skip); + } } diff --git a/classes/User_group.php b/classes/User_group.php index 8587f15771..75de535bd1 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -28,9 +28,12 @@ class User_group extends Memcached_DataObject public $uri; // varchar(255) unique_key public $mainpage; // varchar(255) public $join_policy; // tinyint + public $force_scope; // tinyint /* Static get */ - function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('User_group',$k,$v); } + function staticGet($k,$v=NULL) { + return Memcached_DataObject::staticGet('User_group',$k,$v); + } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE @@ -514,12 +517,19 @@ class User_group extends Memcached_DataObject $group->uri = $uri; $group->mainpage = $mainpage; $group->created = common_sql_now(); + if (isset($fields['join_policy'])) { $group->join_policy = intval($fields['join_policy']); } else { $group->join_policy = 0; } + if (isset($fields['force_scope'])) { + $group->force_scope = intval($fields['force_scope']); + } else { + $group->force_scope = 0; + } + if (Event::handle('StartGroupSave', array(&$group))) { $result = $group->insert(); @@ -563,6 +573,8 @@ class User_group extends Memcached_DataObject throw new ServerException(_('Could not set group membership.')); } + self::blow('profile:groups:%d', $userid); + if ($local) { $local_group = new Local_group(); @@ -643,4 +655,10 @@ class User_group extends Memcached_DataObject } parent::delete(); } + + function isPrivate() + { + return ($this->join_policy == self::JOIN_POLICY_MODERATE && + $this->force_scope == 1); + } } diff --git a/classes/statusnet.ini b/classes/statusnet.ini index f648fb3fbf..c5c126a133 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -258,6 +258,7 @@ user_id = 129 address = 130 address_type = 130 created = 142 +registered_user_id = 1 [invitation__keys] code = K @@ -337,6 +338,7 @@ location_id = 1 location_ns = 1 repeat_of = 1 object_type = 2 +scope = 1 [notice__keys] id = N @@ -461,6 +463,34 @@ tagger = K tagged = K tag = K +[profile_list] +id = 129 +tagger = 129 +tag = 130 +description = 34 +private = 17 +created = 142 +modified = 384 +uri = 130 +mainpage = 130 +tagged_count = 129 +subscriber_count = 129 + +[profile_list__keys] +id = U +tagger = K +tag = K + +[profile_tag_subscription] +profile_tag_id = 129 +profile_id = 129 +created = 142 +modified = 384 + +[profile_tag_subscription__keys] +profile_tag_id = K +profile_id = K + [queue_item] id = 129 frame = 194 @@ -590,10 +620,12 @@ smsreplies = 17 smsemail = 2 uri = 2 autosubscribe = 17 +subscribe_policy = 17 urlshorteningservice = 2 inboxed = 17 design_id = 1 viewdesigns = 17 +private_stream = 17 created = 142 modified = 384 @@ -622,6 +654,7 @@ modified = 384 uri = 2 mainpage = 2 join_policy = 1 +force_scope = 1 [user_group__keys] id = N diff --git a/classes/statusnet.links.ini b/classes/statusnet.links.ini index b9dd5af0c9..17a8c40085 100644 --- a/classes/statusnet.links.ini +++ b/classes/statusnet.links.ini @@ -11,6 +11,7 @@ id = profile:id [notice] profile_id = profile:id reply_to = notice:id +profile_id = profile_tag:tagged [reply] notice_id = notice:id @@ -55,3 +56,19 @@ file_id = file:id file_id = file:id post_id = notice:id +[profile_list] +tagger = profile:id + +[profile_tag] +tagger = profile:id +tagged = profile:id +; in class definition: +;tag,tagger = profile_list:tag,tagger + +[profile_list] +tagger = profile:id + +[profile_tag_subscription] +profile_tag_id = profile_list:id +profile_id = profile:id + diff --git a/config.php.sample b/config.php.sample index 5481ca539e..5378ad973d 100644 --- a/config.php.sample +++ b/config.php.sample @@ -182,6 +182,29 @@ $config['sphinx']['port'] = 3312; // $config['memcached']['server'] = 'localhost'; // $config['memcached']['port'] = 11211; +// People tags +// Maximum number of tags a user can create: +// $config['peopletag']['maxtags'] = 100; +// Maximum number of people can have the same tag by the same user +// $config['peopletag']['maxpeople'] = 500; +// Types of users one can tag. +// Everyone. +// $config['peopletag']['allow_tagging']['all'] = true; +// Local only. +// $config['peopletag']['allow_tagging']['local'] = true; +// Subscriptions / Subscribers only (including remote) +// $config['peopletag']['allow_tagging']['subs'] = true; +// Remote. +// $config['peopletag']['allow_tagging']['remote'] = true; +// Examples: +// The following set of options allows tagging local users and +// remote subscribers / subscription. +// $config['peopletag']['allow_tagging']['all'] = false; +// $config['peopletag']['allow_tagging']['local'] = true; +// $config['peopletag']['allow_tagging']['subs'] = true; +// Or: +// $config['peopletag']['allow_tagging'] = array('local' => true, 'subs' =>true); + // Disable post-by-email // $config['emailpost']['enabled'] = false; diff --git a/db/095topeopletags.sql b/db/095topeopletags.sql new file mode 100644 index 0000000000..e193b98156 --- /dev/null +++ b/db/095topeopletags.sql @@ -0,0 +1,4 @@ +/* populate people tags metadata */ + +insert into profile_list (tagger, tag, modified, description, private) + select distinct tagger, tag, modified, null, false from profile_tag; diff --git a/db/core.php b/db/core.php index 928186d94d..626672bf5f 100644 --- a/db/core.php +++ b/db/core.php @@ -118,10 +118,12 @@ $schema['user'] = array( 'smsemail' => array('type' => 'varchar', 'length' => 255, 'description' => 'built from sms and carrier'), 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier, usually a tag URI'), 'autosubscribe' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'automatically subscribe to users who subscribe to us'), + 'subscribe_policy' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => '0 = anybody can subscribe; 1 = require approval'), 'urlshorteningservice' => array('type' => 'varchar', 'length' => 50, 'default' => 'internal', 'description' => 'service to use for auto-shortening URLs'), 'inboxed' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'has an inbox been created for this user?'), 'design_id' => array('type' => 'int', 'description' => 'id of a design'), 'viewdesigns' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'whether to view user-provided designs'), + 'private_stream' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'whether to limit all notices to followers only'), 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), @@ -202,6 +204,9 @@ $schema['notice'] = array( 'location_ns' => array('type' => 'int', 'description' => 'namespace for location'), 'repeat_of' => array('type' => 'int', 'description' => 'notice this is a repeat of'), 'object_type' => array('type' => 'varchar', 'length' => 255, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'), + 'scope' => array('type' => 'int', + 'default' => '1', + 'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = followers'), ), 'primary key' => array('id'), 'unique keys' => array( @@ -537,14 +542,17 @@ $schema['invitation'] = array( 'address' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'invitation sent to'), 'address_type' => array('type' => 'varchar', 'length' => 8, 'not null' => true, 'description' => 'address type ("email", "xmpp", "sms")'), 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'registered_user_id' => array('type' => 'int', 'not null' => false, 'description' => 'if the invitation is converted, who the new user is'), ), 'primary key' => array('code'), 'foreign keys' => array( 'invitation_user_id_fkey' => array('user', array('user_id' => 'id')), + 'invitation_registered_user_id_fkey' => array('user', array('registered_user_id' => 'id')), ), 'indexes' => array( 'invitation_address_idx' => array('address', 'address_type'), 'invitation_user_id_idx' => array('user_id'), + 'invitation_registered_user_id_idx' => array('registered_user_id'), ), ); @@ -605,8 +613,9 @@ $schema['profile_tag'] = array( ), 'primary key' => array('tagger', 'tagged', 'tag'), 'foreign keys' => array( - 'profile_tag_tagger_fkey' => array('user', array('tagger' => 'id')), + 'profile_tag_tagger_fkey' => array('profile', array('tagger' => 'id')), 'profile_tag_tagged_fkey' => array('profile', array('tagged' => 'id')), + 'profile_tag_tag_fkey' => array('profile_list', array('tag' => 'tag')), ), 'indexes' => array( 'profile_tag_modified_idx' => array('modified'), @@ -615,6 +624,76 @@ $schema['profile_tag'] = array( ), ); +$schema['profile_list'] = array( + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), + 'tagger' => array('type' => 'int', 'not null' => true, 'description' => 'user making the tag'), + 'tag' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'people tag'), + 'description' => array('type' => 'text', 'description' => 'description of the people tag'), + 'private' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'is this tag private'), + + 'created' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date the tag was added'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date the tag was modified'), + + 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universal identifier'), + 'mainpage' => array('type' => 'varchar', 'length' => 255, 'description' => 'page to link to'), + 'tagged_count' => array('type' => 'int', 'default' => 0, 'description' => 'number of people tagged with this tag by this user'), + 'subscriber_count' => array('type' => 'int', 'default' => 0, 'description' => 'number of subscribers to this tag'), + ), + 'primary key' => array('tagger', 'tag'), + 'unique keys' => array( + 'profile_list_id_key' => array('id') + ), + 'foreign keys' => array( + 'profile_list_tagger_fkey' => array('profile', array('tagger' => 'id')), + ), + 'indexes' => array( + 'profile_list_modified_idx' => array('modified'), + 'profile_list_tag_idx' => array('tag'), + 'profile_list_tagger_tag_idx' => array('tagger', 'tag'), + 'profile_list_tagged_count_idx' => array('tagged_count'), + 'profile_list_subscriber_count_idx' => array('subscriber_count'), + ), +); + +$schema['profile_tag_inbox'] = array( + 'description' => 'Many-many table listing notices associated with people tags.', + 'fields' => array( + 'profile_tag_id' => array('type' => 'int', 'not null' => true, 'description' => 'people tag receiving the message'), + 'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'notice received'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice was created'), + ), + 'primary key' => array('profile_tag_id', 'notice_id'), + 'foreign keys' => array( + 'profile_tag_inbox_profile_list_id_fkey' => array('profile_list', array('profile_tag_id' => 'id')), + 'profile_tag_inbox_notice_id_fkey' => array('notice', array('notice_id' => 'id')), + ), + 'indexes' => array( + 'profile_tag_inbox_created_idx' => array('created'), + 'profile_tag_inbox_profile_tag_id_idx' => array('profile_tag_id'), + ), +); + +$schema['profile_tag_subscription'] = array( + 'fields' => array( + 'profile_tag_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile_tag'), + 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile table'), + + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('profile_tag_id', 'profile_id'), + 'foreign keys' => array( + 'profile_tag_subscription_profile_list_id_fkey' => array('profile_list', array('profile_tag_id' => 'id')), + 'profile_tag_subscription_profile_id_fkey' => array('profile', array('profile_id' => 'id')), + ), + 'indexes' => array( + // @fixme probably we want a (profile_id, created) index here? + 'profile_tag_subscription_profile_id_idx' => array('profile_id'), + 'profile_tag_subscription_created_idx' => array('created'), + ), +); + $schema['profile_block'] = array( 'fields' => array( 'blocker' => array('type' => 'int', 'not null' => true, 'description' => 'user making the block'), @@ -649,7 +728,8 @@ $schema['user_group'] = array( 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universal identifier'), 'mainpage' => array('type' => 'varchar', 'length' => 255, 'description' => 'page for group info to link to'), - 'join_policy' => array('type' => 'int', 'size' => 'tiny', 'description' => '0=open; 1=requires admin approval'), + 'join_policy' => array('type' => 'int', 'size' => 'tiny', 'description' => '0=open; 1=requires admin approval'), + 'force_scope' => array('type' => 'int', 'size' => 'tiny', 'description' => '0=never,1=sometimes,-1=always'), ), 'primary key' => array('id'), 'unique keys' => array( @@ -1028,3 +1108,5 @@ $schema['schema_version'] = array( ); $schema['group_join_queue'] = Group_join_queue::schemaDef(); + +$schema['subscription_queue'] = Subscription_queue::schemaDef(); diff --git a/db/site.sql b/db/site.sql index f87995b943..c630a83d52 100644 --- a/db/site.sql +++ b/db/site.sql @@ -28,6 +28,7 @@ create table status_network_tag ( tag varchar(64) comment 'tag name', created datetime not null comment 'date the record was created', - constraint primary key (site_id, tag) + constraint primary key (site_id, tag), + index status_network_tag_tag_idx (tag) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci; diff --git a/db/statusnet.sql b/db/statusnet.sql index debe6f095c..29a555948b 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -403,7 +403,7 @@ create table notice_inbox ( ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; create table profile_tag ( - tagger integer not null comment 'user making the tag' references user (id), + tagger integer not null comment 'user making the tag' references profile (id), tagged integer not null comment 'profile tagged' references profile (id), tag varchar(64) not null comment 'hash tag associated with this notice', modified timestamp comment 'date the tag was added', @@ -414,6 +414,53 @@ create table profile_tag ( index profile_tag_tagged_idx (tagged) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; +/* people tag metadata */ +create table profile_list ( + id integer auto_increment unique key comment 'unique identifier', + tagger integer not null comment 'user making the tag' references profile (id), + tag varchar(64) not null comment 'hash tag', + description text comment 'description for the tag', + private tinyint(1) default 0 comment 'is this list private', + + 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 tag info info to link to', + tagged_count smallint not null default 0 comment 'number of people tagged', + subscriber_count smallint not null default 0 comment 'number of people subscribing', + + constraint primary key (tagger, tag), + index profile_list_tag_idx (tag), + index profile_list_tagged_count_idx (tagged_count), + index profile_list_modified_idx (modified), + index profile_list_subscriber_count_idx (subscriber_count) +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + +create table profile_tag_inbox ( + profile_tag_id integer not null comment 'peopletag receiving the message' references profile_tag (id), + notice_id integer not null comment 'notice received' references notice (id), + created datetime not null comment 'date the notice was created', + + constraint primary key (profile_tag_id, notice_id), + index profile_tag_inbox_created_idx (created), + index profile_tag_inbox_notice_id_idx (notice_id) + +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + +create table profile_tag_subscription ( + profile_tag_id integer not null comment 'foreign key to profile_tag' references profile_list (id), + + profile_id integer not null comment 'foreign key to profile table' references profile (id), + created datetime not null comment 'date this record was created', + modified timestamp comment 'date this record was modified', + + constraint primary key (profile_tag_id, profile_id), + index profile_tag_subscription_profile_id_idx (profile_id), + index profile_tag_subscription_created_idx (created) + +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + create table profile_block ( blocker integer not null comment 'user making the block' references user (id), blocked integer not null comment 'profile that is blocked' references profile (id), diff --git a/doc-src/help b/doc-src/help index 6d12b4cd76..92d5d05afa 100644 --- a/doc-src/help +++ b/doc-src/help @@ -28,8 +28,9 @@ Here are some documents that you might find helpful in understanding * [Contact](%%doc.contact%%) - who to contact with questions about the service * [IM](%%doc.im%%) - using the instant-message (IM) features of %%site.name%% * [SMS](%%doc.sms%%) - tying your cellphone to %%site.name%% -* [tags](%%doc.tags%%) - different ways to use tagging +* [Tags](%%doc.tags%%) - different ways to use tagging * [Groups](%%doc.groups%%) - joining together in groups +* [Lists](%%doc.lists%%) - organize your contacts * [OpenMicroBlogging](%%doc.openmublog%%) - subscribing to remote users * [Privacy](%%doc.privacy%%) - %%site.name%%'s privacy policy * [Source](%%doc.source%%) - How to get the StatusNet source code diff --git a/doc-src/lists b/doc-src/lists new file mode 100644 index 0000000000..b2e93aa853 --- /dev/null +++ b/doc-src/lists @@ -0,0 +1,73 @@ + + + + +%%site.name%% supports +[tags](http://en.wikipedia.org/wiki/Tag_(metadata)) to help you +organize your activities here. You can use tags for people and for +notices. + +Tagging a notice +---------------- + +You can tag a notice using a *hashtag*; a # character followed by +letters and numbers as well as '.', '-', and '_'. Note that accented +latin characters are not supported, and non-roman scripts are right out. + +The HTML for the notice will link to a stream of all the other notices +with that tag. This can be a great way to keep track of a conversation. + +The most popular current tags on the site can be found in the [public +tag cloud](%%action.publictagcloud%%). Their size shows their +popularity and recency. + +Tagging yourself +---------------- + +You can also add tags for yourself on your [profile +settings](%%action.profilesettings%%) page or by using the edit tags +button on your profile page. Use single words to +describe yourself, your experiences and your interest. The tags will +become links on your profile page to a list of all the users on the +site who use that same tag. It can be a nice way to find people who +are related to you geographically or who have a common interest. + +Tagging others +-------------- + +You can also tag other users by using the edit tags button next to +their profile. Such tags are called *people tags*. Once you have +created a people tag, you can add or remove users from it using the +tag's edit form. This makes it easy to organize your subscriptions +into groups and sort through them separately. Also, it will let +you create custom lists of people that others can subscribe to. + +You can also send a notice "to the attention of" your subscribers +whom you've marked with a particular tag (note: *not* people who've +marked themselves with that tag). "@#family hello" will send a +notice to all your subscribers you've marked with the tag 'family'. + +Private and public people tags +------------------------------ + +A private people tag is only visible to the creator, it cannot be +subscribed to, but the timeline can be viewed. To create a new +private prepend a '.' to the tag in the tags editing box. To set +an existing public tag as private or vice-versa, go to the tag's +edit page. + +The most used public tags are displayed in the +[public people tag cloud](%%action.publicpeopletagcloud%%). Their +size shows their frequency of use. + +Remote people tags +------------------ + +You can even [tag remote users](%%action.profilesettings%%). Just +enter the remote profile's URI and click on the "Fetch" button to +fetch the profile, you can then add tags and save them. + +Subscribing to the timeline of a people tag on another server also +works. Just copy the URL of the people tag's timeline page to the +[OStatus subscription](%%action.ostatussub%%) form. + diff --git a/doc-src/tags b/doc-src/tags index 091b147ad3..b2e93aa853 100644 --- a/doc-src/tags +++ b/doc-src/tags @@ -25,20 +25,49 @@ Tagging yourself ---------------- You can also add tags for yourself on your [profile -settings](%%action.profilesettings%%) page. Use single words to +settings](%%action.profilesettings%%) page or by using the edit tags +button on your profile page. Use single words to describe yourself, your experiences and your interest. The tags will become links on your profile page to a list of all the users on the site who use that same tag. It can be a nice way to find people who are related to you geographically or who have a common interest. -Tagging your subscriptions --------------------------- +Tagging others +-------------- -You can also tag your subscriptions, on the subscriptions page. This -makes it easy to organize your subscriptions into groups and sort -through them separately. +You can also tag other users by using the edit tags button next to +their profile. Such tags are called *people tags*. Once you have +created a people tag, you can add or remove users from it using the +tag's edit form. This makes it easy to organize your subscriptions +into groups and sort through them separately. Also, it will let +you create custom lists of people that others can subscribe to. + +You can also send a notice "to the attention of" your subscribers +whom you've marked with a particular tag (note: *not* people who've +marked themselves with that tag). "@#family hello" will send a +notice to all your subscribers you've marked with the tag 'family'. + +Private and public people tags +------------------------------ + +A private people tag is only visible to the creator, it cannot be +subscribed to, but the timeline can be viewed. To create a new +private prepend a '.' to the tag in the tags editing box. To set +an existing public tag as private or vice-versa, go to the tag's +edit page. + +The most used public tags are displayed in the +[public people tag cloud](%%action.publicpeopletagcloud%%). Their +size shows their frequency of use. + +Remote people tags +------------------ + +You can even [tag remote users](%%action.profilesettings%%). Just +enter the remote profile's URI and click on the "Fetch" button to +fetch the profile, you can then add tags and save them. + +Subscribing to the timeline of a people tag on another server also +works. Just copy the URL of the people tag's timeline page to the +[OStatus subscription](%%action.ostatussub%%) form. -You can also send a notice "to the attention of" everyone you've -marked with a particular tag (note: *not* people who've marked -themselves with that tag). "@#family hello" will send a notice to -everyone you've marked with the tag 'family'. \ No newline at end of file diff --git a/index.php b/index.php index c8d4fbee9b..3534739bfa 100644 --- a/index.php +++ b/index.php @@ -106,20 +106,20 @@ function handleError($error) $_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.' + // TRANS: Database error message. + _('The database for %1$s is not responding correctly, '. + 'so the site will not work properly. '. + 'The site admins probably know about the problem, '. + 'but you can contact them at %2$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..' + // TRANS: Error message. + $msg = _('An important error occured, probably related to email setup. '. + 'Check logfiles for more info.' ); } @@ -127,6 +127,7 @@ function handleError($error) $dac->showPage(); } catch (Exception $e) { + // TRANS: Error message. echo _('An error occurred.'); } exit(-1); @@ -174,15 +175,24 @@ function setupRW() static $alwaysRW = array('session', 'remember_me'); - // We ensure that these tables always are used - // on the master DB + $rwdb = $config['db']['database']; - $config['db']['database_rw'] = $config['db']['database']; - $config['db']['ini_rw'] = INSTALLDIR.'/classes/statusnet.ini'; + if (Event::handle('StartReadWriteTables', array(&$alwaysRW, &$rwdb))) { - foreach ($alwaysRW as $table) { - $config['db']['table_'.$table] = 'rw'; + // We ensure that these tables always are used + // on the master DB + + $config['db']['database_rw'] = $rwdb; + $config['db']['ini_rw'] = INSTALLDIR.'/classes/statusnet.ini'; + + foreach ($alwaysRW as $table) { + $config['db']['table_'.$table] = 'rw'; + } + + Event::handle('EndReadWriteTables', array($alwaysRW, $rwdb)); } + + return; } function checkMirror($action_obj, $args) @@ -250,9 +260,9 @@ function main() if (!_have_config()) { $msg = sprintf( - _( - "No configuration file found. Try running ". - "the installation program first." + // TRANS: Error message displayed when there is no StatusNet configuration file. + _("No configuration file found. Try running ". + "the installation program first." ) ); $sac = new ServerErrorAction($msg); @@ -281,6 +291,7 @@ function main() $args = $r->map($path); if (!$args) { + // TRANS: Error message displayed when trying to access a non-existing page. $cac = new ClientErrorAction(_('Unknown page'), 404); $cac->showPage(); return; @@ -335,6 +346,7 @@ function main() $action_class = ucfirst($action).'Action'; if (!class_exists($action_class)) { + // TRANS: Error message displayed when trying to perform an undefined action. $cac = new ClientErrorAction(_('Unknown action'), 404); $cac->showPage(); } else { diff --git a/js/jquery.infieldlabel.js b/js/jquery.infieldlabel.js new file mode 100755 index 0000000000..67608493f8 --- /dev/null +++ b/js/jquery.infieldlabel.js @@ -0,0 +1,153 @@ +/** + * @license In-Field Label jQuery Plugin + * http://fuelyourcoding.com/scripts/infield.html + * + * Copyright (c) 2009-2010 Doug Neiner + * Dual licensed under the MIT and GPL licenses. + * Uses the same license as jQuery, see: + * http://docs.jquery.com/License + * + * @version 0.1.2 + */ +(function ($) { + + $.InFieldLabels = function (label, field, options) { + // To avoid scope issues, use 'base' instead of 'this' + // to reference this class from internal events and functions. + var base = this; + + // Access to jQuery and DOM versions of each element + base.$label = $(label); + base.label = label; + + base.$field = $(field); + base.field = field; + + base.$label.data("InFieldLabels", base); + base.showing = true; + + base.init = function () { + // Merge supplied options with default options + base.options = $.extend({}, $.InFieldLabels.defaultOptions, options); + + // Check if the field is already filled in + if (base.$field.val() !== "") { + base.$label.hide(); + base.showing = false; + } + + base.$field.focus(function () { + base.fadeOnFocus(); + }).blur(function () { + base.checkForEmpty(true); + }).bind('keydown.infieldlabel', function (e) { + // Use of a namespace (.infieldlabel) allows us to + // unbind just this method later + base.hideOnChange(e); + }).bind('paste', function (e) { + // Since you can not paste an empty string we can assume + // that the fieldis not empty and the label can be cleared. + base.setOpacity(0.0); + }).change(function (e) { + base.checkForEmpty(); + }).bind('onPropertyChange', function () { + base.checkForEmpty(); + }); + }; + + // If the label is currently showing + // then fade it down to the amount + // specified in the settings + base.fadeOnFocus = function () { + if (base.showing) { + base.setOpacity(base.options.fadeOpacity); + } + }; + + base.setOpacity = function (opacity) { + base.$label.stop().animate({ opacity: opacity }, base.options.fadeDuration); + base.showing = (opacity > 0.0); + }; + + // Checks for empty as a fail safe + // set blur to true when passing from + // the blur event + base.checkForEmpty = function (blur) { + if (base.$field.val() === "") { + base.prepForShow(); + base.setOpacity(blur ? 1.0 : base.options.fadeOpacity); + } else { + base.setOpacity(0.0); + } + }; + + base.prepForShow = function (e) { + if (!base.showing) { + // Prepare for a animate in... + base.$label.css({opacity: 0.0}).show(); + + // Reattach the keydown event + base.$field.bind('keydown.infieldlabel', function (e) { + base.hideOnChange(e); + }); + } + }; + + base.hideOnChange = function (e) { + if ( + (e.keyCode === 16) || // Skip Shift + (e.keyCode === 9) // Skip Tab + ) { + return; + } + + if (base.showing) { + base.$label.hide(); + base.showing = false; + } + + // Remove keydown event to save on CPU processing + base.$field.unbind('keydown.infieldlabel'); + }; + + // Run the initialization method + base.init(); + }; + + $.InFieldLabels.defaultOptions = { + fadeOpacity: 0.5, // Once a field has focus, how transparent should the label be + fadeDuration: 300 // How long should it take to animate from 1.0 opacity to the fadeOpacity + }; + + + $.fn.inFieldLabels = function (options) { + return this.each(function () { + // Find input or textarea based on for= attribute + // The for attribute on the label must contain the ID + // of the input or textarea element + var for_attr = $(this).attr('for'), $field; + if (!for_attr) { + return; // Nothing to attach, since the for field wasn't used + } + + // Find the referenced input or textarea element + $field = $( + "input#" + for_attr + "[type='text']," + + "input#" + for_attr + "[type='search']," + + "input#" + for_attr + "[type='tel']," + + "input#" + for_attr + "[type='url']," + + "input#" + for_attr + "[type='email']," + + "input#" + for_attr + "[type='password']," + + "textarea#" + for_attr + ); + + if ($field.length === 0) { + return; // Again, nothing to attach + } + + // Only create object for input[text], input[password], or textarea + (new $.InFieldLabels(this, $field[0], options)); + }); + }; + +}(jQuery)); \ No newline at end of file diff --git a/js/jquery.infieldlabel.min.js b/js/jquery.infieldlabel.min.js new file mode 100755 index 0000000000..179306c902 --- /dev/null +++ b/js/jquery.infieldlabel.min.js @@ -0,0 +1,13 @@ +/* + In-Field Label jQuery Plugin + http://fuelyourcoding.com/scripts/infield.html + + Copyright (c) 2009 Doug Neiner + Dual licensed under the MIT and GPL licenses. + Uses the same license as jQuery, see: + http://docs.jquery.com/License + +*/ +(function(d){d.InFieldLabels=function(e,b,f){var a=this;a.$label=d(e);a.label=e;a.$field=d(b);a.field=b;a.$label.data("InFieldLabels",a);a.showing=true;a.init=function(){a.options=d.extend({},d.InFieldLabels.defaultOptions,f);if(a.$field.val()!==""){a.$label.hide();a.showing=false}a.$field.focus(function(){a.fadeOnFocus()}).blur(function(){a.checkForEmpty(true)}).bind("keydown.infieldlabel",function(c){a.hideOnChange(c)}).bind("paste",function(){a.setOpacity(0)}).change(function(){a.checkForEmpty()}).bind("onPropertyChange", +function(){a.checkForEmpty()})};a.fadeOnFocus=function(){a.showing&&a.setOpacity(a.options.fadeOpacity)};a.setOpacity=function(c){a.$label.stop().animate({opacity:c},a.options.fadeDuration);a.showing=c>0};a.checkForEmpty=function(c){if(a.$field.val()===""){a.prepForShow();a.setOpacity(c?1:a.options.fadeOpacity)}else a.setOpacity(0)};a.prepForShow=function(){if(!a.showing){a.$label.css({opacity:0}).show();a.$field.bind("keydown.infieldlabel",function(c){a.hideOnChange(c)})}};a.hideOnChange=function(c){if(!(c.keyCode=== +16||c.keyCode===9)){if(a.showing){a.$label.hide();a.showing=false}a.$field.unbind("keydown.infieldlabel")}};a.init()};d.InFieldLabels.defaultOptions={fadeOpacity:0.5,fadeDuration:300};d.fn.inFieldLabels=function(e){return this.each(function(){var b=d(this).attr("for");if(b){b=d("input#"+b+"[type='text'],input#"+b+"[type='search'],input#"+b+"[type='tel'],input#"+b+"[type='url'],input#"+b+"[type='email'],input#"+b+"[type='password'],textarea#"+b);b.length!==0&&new d.InFieldLabels(this,b[0],e)}})}})(jQuery); diff --git a/js/util.js b/js/util.js index 9c074fdaa8..979415c639 100644 --- a/js/util.js +++ b/js/util.js @@ -236,10 +236,11 @@ var SN = { // StatusNet * @fixme can't submit file uploads * * @param {jQuery} form: jQuery object whose first element is a form + * @param function onSuccess: something extra to do on success * * @access public */ - FormXHR: function(form) { + FormXHR: function(form, onSuccess) { $.ajax({ type: 'POST', dataType: 'xml', @@ -261,7 +262,7 @@ var SN = { // StatusNet errorReported = $('#error', xhr.responseXML).text(); } alert(errorReported || errorThrown || textStatus); - + // Restore the form to original state. // Hopefully. :D form @@ -274,9 +275,15 @@ var SN = { // StatusNet if (typeof($('form', data)[0]) != 'undefined') { form_new = document._importNode($('form', data)[0], true); form.replaceWith(form_new); + if (onSuccess) { + onSuccess(); + } } else if (typeof($('p', data)[0]) != 'undefined') { form.replaceWith(document._importNode($('p', data)[0], true)); + if (onSuccess) { + onSuccess(); + } } else { alert('Unknown error.'); @@ -476,6 +483,74 @@ var SN = { // StatusNet }); }, + FormProfileSearchXHR: function(form) { + $.ajax({ + type: 'POST', + dataType: 'xml', + url: form.attr('action'), + data: form.serialize() + '&ajax=1', + beforeSend: function(xhr) { + form + .addClass(SN.C.S.Processing) + .find('.submit') + .addClass(SN.C.S.Disabled) + .attr(SN.C.S.Disabled, SN.C.S.Disabled); + }, + error: function (xhr, textStatus, errorThrown) { + alert(errorThrown || textStatus); + }, + success: function(data, textStatus) { + var results_placeholder = $('#profile_search_results'); + if (typeof($('ul', data)[0]) != 'undefined') { + var list = document._importNode($('ul', data)[0], true); + results_placeholder.replaceWith(list); + } + else { + var _error = $('
  • ').append(document._importNode($('p', data)[0], true)); + results_placeholder.html(_error); + } + form + .removeClass(SN.C.S.Processing) + .find('.submit') + .removeClass(SN.C.S.Disabled) + .attr(SN.C.S.Disabled, false); + } + }); + }, + + FormPeopletagsXHR: function(form) { + $.ajax({ + type: 'POST', + dataType: 'xml', + url: form.attr('action'), + data: form.serialize() + '&ajax=1', + beforeSend: function(xhr) { + form.find('.submit') + .addClass(SN.C.S.Processing) + .addClass(SN.C.S.Disabled) + .attr(SN.C.S.Disabled, SN.C.S.Disabled); + }, + error: function (xhr, textStatus, errorThrown) { + alert(errorThrown || textStatus); + }, + success: function(data, textStatus) { + var results_placeholder = form.parents('.entity_tags'); + if (typeof($('.entity_tags', data)[0]) != 'undefined') { + var tags = document._importNode($('.entity_tags', data)[0], true); + $(tags).find('.editable').append($('').closest(".notice-options").addClass("opaque");a.find("button.close").click(function(){$(this).remove();a.removeClass("dialogbox").closest(".notice-options").removeClass("opaque");a.find(".submit_dialogbox").remove();a.find(".submit").show();return false})},NoticeAttachments:function(){$(".notice a.attachment").each(function(){SN.U.NoticeWithAttachment($(this).closest(".notice"))})},NoticeWithAttachment:function(b){if(b.find(".attachment").length===0){return}var a=b.find(".attachment.more");if(a.length>0){$(a[0]).click(function(){var c=$(this);c.addClass(SN.C.S.Processing);$.get(c.attr("href")+"/ajax",null,function(d){c.parent(".entry-content").html($(d).find("#attachment_view .entry-content").html())});return false}).attr("title",SN.msg("showmore_tooltip"))}},NoticeDataAttach:function(b){var a=b.find("input[type=file]");a.change(function(f){b.find(".attach-status").remove();var d=$(this).val();if(!d){return false}var c=$('
    ');c.find("code").text(d);c.find("button").click(function(){c.remove();a.val("");return false});b.append(c);if(typeof this.files=="object"){for(var e=0;eg){f=false}if(f){h(c,function(j){var i=$("").attr("title",e).attr("alt",e).attr("src",j).attr("style","height: 120px");d.find(".attach-status").append(i)})}else{var b=$("
    ").text(e);d.find(".attach-status").append(b)}},NoticeLocationAttach:function(a){var e=a.find("[name=lat]");var k=a.find("[name=lon]");var g=a.find("[name=location_ns]").val();var l=a.find("[name=location_id]").val();var b="";var d=a.find("[name=notice_data-geo]");var c=a.find("[name=notice_data-geo]");var j=a.find("label.notice_data-geo");function f(n){j.attr("title",jQuery.trim(j.text())).removeClass("checked");a.find("[name=lat]").val("");a.find("[name=lon]").val("");a.find("[name=location_ns]").val("");a.find("[name=location_id]").val("");a.find("[name=notice_data-geo]").attr("checked",false);$.cookie(SN.C.S.NoticeDataGeoCookie,"disabled",{path:"/"});if(n){a.find(".geo_status_wrapper").removeClass("success").addClass("error");a.find(".geo_status_wrapper .geo_status").text(n)}else{a.find(".geo_status_wrapper").remove()}}function m(n,o){SN.U.NoticeGeoStatus(a,"Looking up place name...");$.getJSON(n,o,function(p){var q,r;if(typeof(p.location_ns)!="undefined"){a.find("[name=location_ns]").val(p.location_ns);q=p.location_ns}if(typeof(p.location_id)!="undefined"){a.find("[name=location_id]").val(p.location_id);r=p.location_id}if(typeof(p.name)=="undefined"){NLN_text=o.lat+";"+o.lon}else{NLN_text=p.name}SN.U.NoticeGeoStatus(a,NLN_text,o.lat,o.lon,p.url);j.attr("title",NoticeDataGeo_text.ShareDisable+" ("+NLN_text+")");a.find("[name=lat]").val(o.lat);a.find("[name=lon]").val(o.lon);a.find("[name=location_ns]").val(q);a.find("[name=location_id]").val(r);a.find("[name=notice_data-geo]").attr("checked",true);var s={NLat:o.lat,NLon:o.lon,NLNS:q,NLID:r,NLN:NLN_text,NLNU:p.url,NDG:true};$.cookie(SN.C.S.NoticeDataGeoCookie,JSON.stringify(s),{path:"/"})})}if(c.length>0){if($.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){c.attr("checked",false)}else{c.attr("checked",true)}var h=a.find(".notice_data-geo_wrap");var i=h.attr("data-api");j.attr("title",j.text());c.change(function(){if(c.attr("checked")===true||$.cookie(SN.C.S.NoticeDataGeoCookie)===null){j.attr("title",NoticeDataGeo_text.ShareDisable).addClass("checked");if($.cookie(SN.C.S.NoticeDataGeoCookie)===null||$.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){if(navigator.geolocation){SN.U.NoticeGeoStatus(a,"Requesting location from browser...");navigator.geolocation.getCurrentPosition(function(p){a.find("[name=lat]").val(p.coords.latitude);a.find("[name=lon]").val(p.coords.longitude);var q={lat:p.coords.latitude,lon:p.coords.longitude,token:$("#token").val()};m(i,q)},function(p){switch(p.code){case p.PERMISSION_DENIED:f("Location permission denied.");break;case p.TIMEOUT:f("Location lookup timeout.");break}},{timeout:10000})}else{if(e.length>0&&k.length>0){var n={lat:e,lon:k,token:$("#token").val()};m(i,n)}else{f();c.remove();j.remove()}}}else{var o=JSON.parse($.cookie(SN.C.S.NoticeDataGeoCookie));a.find("[name=lat]").val(o.NLat);a.find("[name=lon]").val(o.NLon);a.find("[name=location_ns]").val(o.NLNS);a.find("[name=location_id]").val(o.NLID);a.find("[name=notice_data-geo]").attr("checked",o.NDG);SN.U.NoticeGeoStatus(a,o.NLN,o.NLat,o.NLon,o.NLNU);j.attr("title",NoticeDataGeo_text.ShareDisable+" ("+o.NLN+")").addClass("checked")}}else{f()}}).change()}},NoticeGeoStatus:function(e,a,f,g,c){var h=e.find(".geo_status_wrapper");if(h.length==0){h=$('
    ');h.find("button.close").click(function(){e.find("[name=notice_data-geo]").removeAttr("checked").change();return false});e.append(h)}var b;if(c){b=$("").attr("href",c)}else{b=$("")}b.text(a);if(f||g){var d=f+";"+g;b.attr("title",d);if(!a){b.text(d)}}h.find(".geo_status").empty().append(b)},NewDirectMessage:function(){NDM=$(".entity_send-a-message a");NDM.attr({href:NDM.attr("href")+"&ajax=1"});NDM.bind("click",function(){var a=$(".entity_send-a-message form");if(a.length===0){$(this).addClass(SN.C.S.Processing);$.get(NDM.attr("href"),null,function(b){$(".entity_send-a-message").append(document._importNode($("form",b)[0],true));a=$(".entity_send-a-message .form_notice");SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);a.append('');$(".entity_send-a-message button").click(function(){a.hide();return false});NDM.removeClass(SN.C.S.Processing)})}else{a.show();$(".entity_send-a-message textarea").focus()}return false})},GetFullYear:function(c,d,a){var b=new Date();b.setFullYear(c,d,a);return b},StatusNetInstance:{Set:function(b){var a=SN.U.StatusNetInstance.Get();if(a!==null){b=$.extend(a,b)}$.cookie(SN.C.S.StatusNetInstance,JSON.stringify(b),{path:"/",expires:SN.U.GetFullYear(2029,0,1)})},Get:function(){var a=$.cookie(SN.C.S.StatusNetInstance);if(a!==null){return JSON.parse(a)}return null},Delete:function(){$.cookie(SN.C.S.StatusNetInstance,null)}},belongsOnTimeline:function(b){var a=$("body").attr("id");if(a=="public"){return true}var c=$("#nav_profile a").attr("href");if(c){var d=$(b).find(".vcard.author a.url").attr("href");if(d==c){if(a=="all"||a=="showstream"){return true}}}return false},switchInputFormTab:function(a){$(".input_form_nav_tab.current").removeClass("current");if(a=="placeholder"){$("#input_form_nav_status").addClass("current")}else{$("#input_form_nav_"+a).addClass("current")}$(".input_form.current").removeClass("current");$("#input_form_"+a).addClass("current").find(".ajax-notice").each(function(){var b=$(this);SN.Init.NoticeFormSetup(b)}).find("textarea:first").focus()}},Init:{NoticeForm:function(){if($("body.user_in").length>0){$("#input_form_placeholder input.placeholder").focus(function(){SN.U.switchInputFormTab("status")});$("body").bind("click",function(g){var d=$("#content .input_forms div.current");if(d.length>0){if($("#content .input_forms").has(g.target).length==0){var a=d.find('textarea, input[type=text], input[type=""]');var c=false;a.each(function(){c=c||$(this).val()});if(!c){SN.U.switchInputFormTab("placeholder")}}}var b=$("li.notice-reply");if(b.length>0){var f=$(g.target);b.each(function(){var j=$(this);if(j.has(g.target).length==0){var h=j.find(".notice_data-text:first");var i=$.trim(h.val());if(i==""||i==h.data("initialText")){var e=j.closest("li.notice");j.remove();e.find("li.notice-reply-placeholder").show()}}})}})}},NoticeFormSetup:function(a){if(!a.data("NoticeFormSetup")){SN.U.NoticeLocationAttach(a);SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);SN.U.NoticeDataAttach(a);a.data("NoticeFormSetup",true)}},Notices:function(){if($("body.user_in").length>0){var a=$(".form_notice:first");if(a.length>0){SN.C.I.NoticeFormMaster=document._importNode(a[0],true)}SN.U.NoticeRepeat();SN.U.NoticeReply();SN.U.NoticeInlineReplySetup()}SN.U.NoticeAttachments()},EntityActions:function(){if($("body.user_in").length>0){SN.U.NewDirectMessage()}},Login:function(){if(SN.U.StatusNetInstance.Get()!==null){var a=SN.U.StatusNetInstance.Get().Nickname;if(a!==null){$("#form_login #nickname").val(a)}}$("#form_login").bind("submit",function(){SN.U.StatusNetInstance.Set({Nickname:$("#form_login #nickname").val()});return true})},AjaxForms:function(){$("form.ajax").live("submit",function(){SN.U.FormXHR($(this));return false});$("form.ajax input[type=submit]").live("click",function(){var a=$(this);var b=a.closest("form");b.find(".hidden-submit-button").remove();$('').attr("name",a.attr("name")).val(a.val()).appendTo(b)})},UploadForms:function(){$("input[type=file]").change(function(d){if(typeof this.files=="object"&&this.files.length>0){var c=0;for(var b=0;b0&&c>a){var e="File too large: maximum upload size is %d bytes.";alert(e.replace("%d",a));$(this).val("");d.preventDefault();return false}}})}}};$(document).ready(function(){SN.Init.AjaxForms();SN.Init.UploadForms();if($("."+SN.C.S.FormNotice).length>0){SN.Init.NoticeForm()}if($("#content .notices").length>0){SN.Init.Notices()}if($("#content .entity_actions").length>0){SN.Init.EntityActions()}if($("#form_login").length>0){SN.Init.Login()}});if(!document.ELEMENT_NODE){document.ELEMENT_NODE=1;document.ATTRIBUTE_NODE=2;document.TEXT_NODE=3;document.CDATA_SECTION_NODE=4;document.ENTITY_REFERENCE_NODE=5;document.ENTITY_NODE=6;document.PROCESSING_INSTRUCTION_NODE=7;document.COMMENT_NODE=8;document.DOCUMENT_NODE=9;document.DOCUMENT_TYPE_NODE=10;document.DOCUMENT_FRAGMENT_NODE=11;document.NOTATION_NODE=12}document._importNode=function(e,a){switch(e.nodeType){case document.ELEMENT_NODE:var d=document.createElement(e.nodeName);if(e.attributes&&e.attributes.length>0){for(var c=0,b=e.attributes.length;c0){for(var c=0,b=e.childNodes.length;c0){var j=c.pop();j()}}};window._google_loader_apiLoaded=function(){f()};var d=function(){return(window.google&&google.loader)};var g=function(j){if(d()){return true}h(j);e();return false};e();return{shim:true,type:"ClientLocation",lastPosition:null,getCurrentPosition:function(k,n,o){var m=this;if(!g(function(){m.getCurrentPosition(k,n,o)})){return}if(google.loader.ClientLocation){var l=google.loader.ClientLocation;var j={coords:{latitude:l.latitude,longitude:l.longitude,altitude:null,accuracy:43000,altitudeAccuracy:null,heading:null,speed:null},address:{city:l.address.city,country:l.address.country,country_code:l.address.country_code,region:l.address.region},timestamp:new Date()};k(j);this.lastPosition=j}else{if(n==="function"){n({code:3,message:"Using the Google ClientLocation API and it is not able to calculate a location."})}}},watchPosition:function(j,l,m){this.getCurrentPosition(j,l,m);var k=this;var n=setInterval(function(){k.getCurrentPosition(j,l,m)},10000);return n},clearWatch:function(j){clearInterval(j)},getPermission:function(l,j,k){return true}}});navigator.geolocation=(window.google&&google.gears)?a():b()})()}; \ No newline at end of file +var SN={C:{I:{CounterBlackout:false,MaxLength:140,PatternUsername:/^[0-9a-zA-Z\-_.]*$/,HTTP20x30x:[200,201,202,203,204,205,206,300,301,302,303,304,305,306,307],NoticeFormMaster:null},S:{Disabled:"disabled",Warning:"warning",Error:"error",Success:"success",Processing:"processing",CommandResult:"command_result",FormNotice:"form_notice",NoticeDataGeo:"notice_data-geo",NoticeDataGeoCookie:"NoticeDataGeo",NoticeDataGeoSelected:"notice_data-geo_selected",StatusNetInstance:"StatusNetInstance"}},messages:{},msg:function(a){if(typeof SN.messages[a]=="undefined"){return"["+a+"]"}else{return SN.messages[a]}},U:{FormNoticeEnhancements:function(b){if(jQuery.data(b[0],"ElementData")===undefined){MaxLength=b.find(".count").text();if(typeof(MaxLength)=="undefined"){MaxLength=SN.C.I.MaxLength}jQuery.data(b[0],"ElementData",{MaxLength:MaxLength});SN.U.Counter(b);NDT=b.find(".notice_data-text:first");NDT.bind("keyup",function(c){SN.U.Counter(b)});var a=function(c){window.setTimeout(function(){SN.U.Counter(b)},50)};NDT.bind("cut",a).bind("paste",a)}else{b.find(".count").text(jQuery.data(b[0],"ElementData").MaxLength)}},Counter:function(d){SN.C.I.FormNoticeCurrent=d;var b=jQuery.data(d[0],"ElementData").MaxLength;if(b<=0){return}var c=b-SN.U.CharacterCount(d);var a=d.find(".count");if(c.toString()!=a.text()){if(!SN.C.I.CounterBlackout||c===0){if(a.text()!=String(c)){a.text(c)}if(c<0){d.addClass(SN.C.S.Warning)}else{d.removeClass(SN.C.S.Warning)}if(!SN.C.I.CounterBlackout){SN.C.I.CounterBlackout=true;SN.C.I.FormNoticeCurrent=d;window.setTimeout("SN.U.ClearCounterBlackout(SN.C.I.FormNoticeCurrent);",500)}}}},CharacterCount:function(a){return a.find(".notice_data-text:first").val().length},ClearCounterBlackout:function(a){SN.C.I.CounterBlackout=false;SN.U.Counter(a)},RewriteAjaxAction:function(a){if(document.location.protocol=="https:"&&a.substr(0,5)=="http:"){return a.replace(/^http:\/\/[^:\/]+/,"https://"+document.location.host)}else{return a}},FormXHR:function(a,b){$.ajax({type:"POST",dataType:"xml",url:SN.U.RewriteAjaxAction(a.attr("action")),data:a.serialize()+"&ajax=1",beforeSend:function(c){a.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled)},error:function(e,f,d){var c=null;if(e.responseXML){c=$("#error",e.responseXML).text()}alert(c||d||f);a.removeClass(SN.C.S.Processing).find(".submit").removeClass(SN.C.S.Disabled).removeAttr(SN.C.S.Disabled)},success:function(c,d){if(typeof($("form",c)[0])!="undefined"){form_new=document._importNode($("form",c)[0],true);a.replaceWith(form_new);if(b){b()}}else{if(typeof($("p",c)[0])!="undefined"){a.replaceWith(document._importNode($("p",c)[0],true));if(b){b()}}else{alert("Unknown error.")}}}})},FormNoticeXHR:function(b){SN.C.I.NoticeDataGeo={};b.append('');b.attr("action",SN.U.RewriteAjaxAction(b.attr("action")));var c=function(d,e){b.append($('

    ').addClass(d).text(e))};var a=function(){b.find(".form_response").remove()};b.ajaxForm({dataType:"xml",timeout:"60000",beforeSend:function(d){if(b.find(".notice_data-text:first").val()==""){b.addClass(SN.C.S.Warning);return false}b.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled);SN.U.normalizeGeoData(b);return true},error:function(f,g,e){b.removeClass(SN.C.S.Processing).find(".submit").removeClass(SN.C.S.Disabled).removeAttr(SN.C.S.Disabled,SN.C.S.Disabled);a();if(g=="timeout"){c("error","Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists.")}else{var d=SN.U.GetResponseXML(f);if($("."+SN.C.S.Error,d).length>0){b.append(document._importNode($("."+SN.C.S.Error,d)[0],true))}else{if(parseInt(f.status)===0||jQuery.inArray(parseInt(f.status),SN.C.I.HTTP20x30x)>=0){b.resetForm().find(".attach-status").remove();SN.U.FormNoticeEnhancements(b)}else{c("error","(Sorry! We had trouble sending your notice ("+f.status+" "+f.statusText+"). Please report the problem to the site administrator if this happens again.")}}}},success:function(j,f){a();var p=$("#"+SN.C.S.Error,j);if(p.length>0){c("error",p.text())}else{if($("body")[0].id=="bookmarklet"){self.close()}var d=$("#"+SN.C.S.CommandResult,j);if(d.length>0){c("success",d.text())}else{var o=document._importNode($("li",j)[0],true);var k=$("#notices_primary .notices:first");var m=b.closest("li.notice-reply");if(m.length>0){var l=b.closest(".threaded-replies");var n=l.find(".notice-reply-placeholder");m.remove();var e=$(o).attr("id");if($("#"+e).length==0){$(o).insertBefore(n)}else{}n.show()}else{if(k.length>0&&SN.U.belongsOnTimeline(o)){if($("#"+o.id).length===0){var h=b.find("[name=inreplyto]").val();var g="#notices_primary #notice-"+h;if($("body")[0].id=="conversation"){if(h.length>0&&$(g+" .notices").length<1){$(g).append('
      ')}$($(g+" .notices")[0]).append(o)}else{k.prepend(o)}$("#"+o.id).css({display:"none"}).fadeIn(2500);SN.U.NoticeWithAttachment($("#"+o.id));SN.U.switchInputFormTab("placeholder")}}else{c("success",$("title",j).text())}}}b.resetForm();b.find("[name=inreplyto]").val("");b.find(".attach-status").remove();SN.U.FormNoticeEnhancements(b)}},complete:function(d,e){b.removeClass(SN.C.S.Processing).find(".submit").removeAttr(SN.C.S.Disabled).removeClass(SN.C.S.Disabled);b.find("[name=lat]").val(SN.C.I.NoticeDataGeo.NLat);b.find("[name=lon]").val(SN.C.I.NoticeDataGeo.NLon);b.find("[name=location_ns]").val(SN.C.I.NoticeDataGeo.NLNS);b.find("[name=location_id]").val(SN.C.I.NoticeDataGeo.NLID);b.find("[name=notice_data-geo]").attr("checked",SN.C.I.NoticeDataGeo.NDG)}})},FormProfileSearchXHR:function(a){$.ajax({type:"POST",dataType:"xml",url:a.attr("action"),data:a.serialize()+"&ajax=1",beforeSend:function(b){a.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled)},error:function(c,d,b){alert(b||d)},success:function(d,f){var b=$("#profile_search_results");if(typeof($("ul",d)[0])!="undefined"){var c=document._importNode($("ul",d)[0],true);b.replaceWith(c)}else{var e=$("
    • ").append(document._importNode($("p",d)[0],true));b.html(e)}a.removeClass(SN.C.S.Processing).find(".submit").removeClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,false)}})},FormPeopletagsXHR:function(a){$.ajax({type:"POST",dataType:"xml",url:a.attr("action"),data:a.serialize()+"&ajax=1",beforeSend:function(b){a.find(".submit").addClass(SN.C.S.Processing).addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled)},error:function(c,d,b){alert(b||d)},success:function(d,e){var c=a.parents(".entity_tags");if(typeof($(".entity_tags",d)[0])!="undefined"){var b=document._importNode($(".entity_tags",d)[0],true);$(b).find(".editable").append($('').closest(".notice-options").addClass("opaque");a.find("button.close").click(function(){$(this).remove();a.removeClass("dialogbox").closest(".notice-options").removeClass("opaque");a.find(".submit_dialogbox").remove();a.find(".submit").show();return false})},NoticeAttachments:function(){$(".notice a.attachment").each(function(){SN.U.NoticeWithAttachment($(this).closest(".notice"))})},NoticeWithAttachment:function(b){if(b.find(".attachment").length===0){return}var a=b.find(".attachment.more");if(a.length>0){$(a[0]).click(function(){var c=$(this);c.addClass(SN.C.S.Processing);$.get(c.attr("href")+"/ajax",null,function(d){c.parent(".entry-content").html($(d).find("#attachment_view .entry-content").html())});return false}).attr("title",SN.msg("showmore_tooltip"))}},NoticeDataAttach:function(b){var a=b.find("input[type=file]");a.change(function(f){b.find(".attach-status").remove();var d=$(this).val();if(!d){return false}var c=$('
      ');c.find("code").text(d);c.find("button").click(function(){c.remove();a.val("");return false});b.append(c);if(typeof this.files=="object"){for(var e=0;eg){f=false}if(f){h(c,function(k){var j=$("").attr("title",e).attr("alt",e).attr("src",k).attr("style","height: 120px");d.find(".attach-status").append(j)})}else{var b=$("
      ").text(e);d.find(".attach-status").append(b)}},NoticeLocationAttach:function(a){var e=a.find("[name=lat]");var l=a.find("[name=lon]");var g=a.find("[name=location_ns]").val();var m=a.find("[name=location_id]").val();var b="";var d=a.find("[name=notice_data-geo]");var c=a.find("[name=notice_data-geo]");var k=a.find("label.notice_data-geo");function f(o){k.attr("title",jQuery.trim(k.text())).removeClass("checked");a.find("[name=lat]").val("");a.find("[name=lon]").val("");a.find("[name=location_ns]").val("");a.find("[name=location_id]").val("");a.find("[name=notice_data-geo]").attr("checked",false);$.cookie(SN.C.S.NoticeDataGeoCookie,"disabled",{path:"/"});if(o){a.find(".geo_status_wrapper").removeClass("success").addClass("error");a.find(".geo_status_wrapper .geo_status").text(o)}else{a.find(".geo_status_wrapper").remove()}}function n(o,p){SN.U.NoticeGeoStatus(a,"Looking up place name...");$.getJSON(o,p,function(q){var r,s;if(typeof(q.location_ns)!="undefined"){a.find("[name=location_ns]").val(q.location_ns);r=q.location_ns}if(typeof(q.location_id)!="undefined"){a.find("[name=location_id]").val(q.location_id);s=q.location_id}if(typeof(q.name)=="undefined"){NLN_text=p.lat+";"+p.lon}else{NLN_text=q.name}SN.U.NoticeGeoStatus(a,NLN_text,p.lat,p.lon,q.url);k.attr("title",NoticeDataGeo_text.ShareDisable+" ("+NLN_text+")");a.find("[name=lat]").val(p.lat);a.find("[name=lon]").val(p.lon);a.find("[name=location_ns]").val(r);a.find("[name=location_id]").val(s);a.find("[name=notice_data-geo]").attr("checked",true);var t={NLat:p.lat,NLon:p.lon,NLNS:r,NLID:s,NLN:NLN_text,NLNU:q.url,NDG:true};$.cookie(SN.C.S.NoticeDataGeoCookie,JSON.stringify(t),{path:"/"})})}if(c.length>0){if($.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){c.attr("checked",false)}else{c.attr("checked",true)}var h=a.find(".notice_data-geo_wrap");var j=h.attr("data-api");k.attr("title",k.text());c.change(function(){if(c.attr("checked")===true||$.cookie(SN.C.S.NoticeDataGeoCookie)===null){k.attr("title",NoticeDataGeo_text.ShareDisable).addClass("checked");if($.cookie(SN.C.S.NoticeDataGeoCookie)===null||$.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){if(navigator.geolocation){SN.U.NoticeGeoStatus(a,"Requesting location from browser...");navigator.geolocation.getCurrentPosition(function(q){a.find("[name=lat]").val(q.coords.latitude);a.find("[name=lon]").val(q.coords.longitude);var r={lat:q.coords.latitude,lon:q.coords.longitude,token:$("#token").val()};n(j,r)},function(q){switch(q.code){case q.PERMISSION_DENIED:f("Location permission denied.");break;case q.TIMEOUT:f("Location lookup timeout.");break}},{timeout:10000})}else{if(e.length>0&&l.length>0){var o={lat:e,lon:l,token:$("#token").val()};n(j,o)}else{f();c.remove();k.remove()}}}else{var p=JSON.parse($.cookie(SN.C.S.NoticeDataGeoCookie));a.find("[name=lat]").val(p.NLat);a.find("[name=lon]").val(p.NLon);a.find("[name=location_ns]").val(p.NLNS);a.find("[name=location_id]").val(p.NLID);a.find("[name=notice_data-geo]").attr("checked",p.NDG);SN.U.NoticeGeoStatus(a,p.NLN,p.NLat,p.NLon,p.NLNU);k.attr("title",NoticeDataGeo_text.ShareDisable+" ("+p.NLN+")").addClass("checked")}}else{f()}}).change()}},NoticeGeoStatus:function(e,a,f,g,c){var h=e.find(".geo_status_wrapper");if(h.length==0){h=$('
      ');h.find("button.close").click(function(){e.find("[name=notice_data-geo]").removeAttr("checked").change();return false});e.append(h)}var b;if(c){b=$("").attr("href",c)}else{b=$("")}b.text(a);if(f||g){var d=f+";"+g;b.attr("title",d);if(!a){b.text(d)}}h.find(".geo_status").empty().append(b)},NewDirectMessage:function(){NDM=$(".entity_send-a-message a");NDM.attr({href:NDM.attr("href")+"&ajax=1"});NDM.bind("click",function(){var a=$(".entity_send-a-message form");if(a.length===0){$(this).addClass(SN.C.S.Processing);$.get(NDM.attr("href"),null,function(b){$(".entity_send-a-message").append(document._importNode($("form",b)[0],true));a=$(".entity_send-a-message .form_notice");SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);a.append('');$(".entity_send-a-message button").click(function(){a.hide();return false});NDM.removeClass(SN.C.S.Processing)})}else{a.show();$(".entity_send-a-message textarea").focus()}return false})},GetFullYear:function(c,d,a){var b=new Date();b.setFullYear(c,d,a);return b},StatusNetInstance:{Set:function(b){var a=SN.U.StatusNetInstance.Get();if(a!==null){b=$.extend(a,b)}$.cookie(SN.C.S.StatusNetInstance,JSON.stringify(b),{path:"/",expires:SN.U.GetFullYear(2029,0,1)})},Get:function(){var a=$.cookie(SN.C.S.StatusNetInstance);if(a!==null){return JSON.parse(a)}return null},Delete:function(){$.cookie(SN.C.S.StatusNetInstance,null)}},belongsOnTimeline:function(b){var a=$("body").attr("id");if(a=="public"){return true}var c=$("#nav_profile a").attr("href");if(c){var d=$(b).find(".vcard.author a.url").attr("href");if(d==c){if(a=="all"||a=="showstream"){return true}}}return false},switchInputFormTab:function(a){$(".input_form_nav_tab.current").removeClass("current");if(a=="placeholder"){$("#input_form_nav_status").addClass("current")}else{$("#input_form_nav_"+a).addClass("current")}var b=$(".input_form.current.nonav");if(b.length>0){return}$(".input_form.current").removeClass("current");$("#input_form_"+a).addClass("current").find(".ajax-notice").each(function(){var c=$(this);SN.Init.NoticeFormSetup(c)}).find(".notice_data-text").focus()}},Init:{NoticeForm:function(){if($("body.user_in").length>0){$("#input_form_placeholder input.placeholder").focus(function(){SN.U.switchInputFormTab("status")});$("body").bind("click",function(g){var d=$("#content .input_forms div.current");if(d.length>0){if($("#content .input_forms").has(g.target).length==0){var a=d.find('textarea, input[type=text], input[type=""]');var c=false;a.each(function(){c=c||$(this).val()});if(!c){SN.U.switchInputFormTab("placeholder")}}}var b=$("li.notice-reply");if(b.length>0){var f=$(g.target);b.each(function(){var k=$(this);if(k.has(g.target).length==0){var h=k.find(".notice_data-text:first");var j=$.trim(h.val());if(j==""||j==h.data("initialText")){var e=k.closest("li.notice");k.remove();e.find("li.notice-reply-placeholder").show()}}})}});$(".input_forms fieldset fieldset label").inFieldLabels({fadeOpacity:0})}},NoticeFormSetup:function(a){if(!a.data("NoticeFormSetup")){SN.U.NoticeLocationAttach(a);SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);SN.U.NoticeDataAttach(a);a.data("NoticeFormSetup",true)}},Notices:function(){if($("body.user_in").length>0){var a=$(".form_notice:first");if(a.length>0){SN.C.I.NoticeFormMaster=document._importNode(a[0],true)}SN.U.NoticeRepeat();SN.U.NoticeReply();SN.U.NoticeInlineReplySetup()}SN.U.NoticeAttachments()},EntityActions:function(){if($("body.user_in").length>0){$(".form_user_subscribe").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_user_unsubscribe").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_group_join").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_group_leave").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_user_nudge").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_peopletag_subscribe").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_peopletag_unsubscribe").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_user_add_peopletag").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_user_remove_peopletag").live("click",function(){SN.U.FormXHR($(this));return false});SN.U.NewDirectMessage()}},ProfileSearch:function(){if($("body.user_in").length>0){$(".form_peopletag_edit_user_search input.submit").live("click",function(){SN.U.FormProfileSearchXHR($(this).parents("form"));return false})}},Login:function(){if(SN.U.StatusNetInstance.Get()!==null){var a=SN.U.StatusNetInstance.Get().Nickname;if(a!==null){$("#form_login #nickname").val(a)}}$("#form_login").bind("submit",function(){SN.U.StatusNetInstance.Set({Nickname:$("#form_login #nickname").val()});return true})},PeopletagAutocomplete:function(b){var a=function(d){return d.split(/\s+/)};var c=function(d){return a(d).pop()};b.live("keydown",function(d){if(d.keyCode===$.ui.keyCode.TAB&&$(this).data("autocomplete").menu.active){d.preventDefault()}}).autocomplete({minLength:0,source:function(e,d){d($.ui.autocomplete.filter(SN.C.PtagACData,c(e.term)))},focus:function(){return false},select:function(e,f){var d=a(this.value);d.pop();d.push(f.item.value);d.push("");this.value=d.join(" ");return false}}).data("autocomplete")._renderItem=function(e,f){var d=''+f.tag+' '+f.mode+''+f.freq+"";return $("
    • ").addClass("mode-"+f.mode).addClass("ptag-ac-line").data("item.autocomplete",f).append(d).appendTo(e)}},PeopleTags:function(){$(".user_profile_tags .editable").append($('