diff --git a/.gitignore b/.gitignore index d84e5e7424..bf491ec3c2 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ lac08.log php.log .DS_Store nbproject +*.mo diff --git a/EVENTS.txt b/EVENTS.txt index 8e730945a4..f675c199a0 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -118,16 +118,16 @@ EndShowHTML: Showing after the html element - $action: the current action StartPublicGroupNav: Showing the public group nav menu -- $action: the current action +- $menu: the menu widget; use $menu->action for output EndPublicGroupNav: At the end of the public group nav menu -- $action: the current action +- $menu: the menu widget; use $menu->action for output StartSubGroupNav: Showing the subscriptions group nav menu -- $action: the current action +- $menu: the menu widget; use $menu->action for output EndSubGroupNav: At the end of the subscriptions group nav menu -- $action: the current action +- $menu: the menu widget; use $menu->action for output StartInitializeRouter: Before the router instance has been initialized; good place to add routes - $m: the Net_URL_Mapper that has just been set up @@ -302,6 +302,20 @@ StartProfileSaveForm: before starting to save a profile settings form EndProfileSaveForm: after saving a profile settings form (after commit, no profile or user object!) - $action: action object being shown +StartEmailFormData: just before showing text entry fields on email settings page +- $action: action object being shown + +EndEmailFormData: just after showing text entry fields on email settings page +- $action: action object being shown + +StartEmailSaveForm: before starting to save a email settings form +- $action: action object being shown +- &$user: user being saved + +EndEmailSaveForm: after saving a email settings form (after commit) +- $action: action object being shown +- &$user: user being saved + StartRegistrationFormData: just before showing text entry fields on registration page - $action: action object being shown @@ -365,6 +379,14 @@ GetValidDaemons: Just before determining which daemons to run HandleQueuedNotice: Handle a queued notice at queue time (or immediately if no queue) - &$notice: notice to handle +StartHtmlElement: Reight before outputting the HTML element - allows plugins to add namespaces +- $action: the current action +- &$attrs: attributes for the HTML element + +EndHtmlElement: Right after outputting the HTML element +- $action: the current action +- &$attrs: attributes for the HTML element + StartShowHeadElements: Right after the tag - $action: the current action @@ -569,6 +591,12 @@ EndPublicXRDS: End XRDS output (right before the closing XRDS tag) - $action: the current action - &$xrdsoutputter - XRDSOutputter object to write to +StartHostMetaLinks: Start /.well-known/host-meta links +- &links: array containing the links elements to be written + +EndHostMetaLinks: End /.well-known/host-meta links +- &links: array containing the links elements to be written + StartCheckPassword: Check a username/password - $nickname: The nickname to check - $password: The password to check @@ -852,233 +880,13 @@ EndDeleteUser: handling the post for deleting a user - $action: action being shown - $user: user being deleted -StartActivityStart: starting the output for a notice activity -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$attrs: attributes (mostly namespace declarations, if any) +StartNoticeAsActivity: before converting a notice to an activity +- $notice: notice being converted +- &$activity: initially empty activity -EndActivityStart: end the opening tag for an activity -- &$notice: notice being output -- &$xs: XMLStringer for output -- $attrs: attributes (mostly namespace declarations, if any) - -StartActivitySource: before outputting the element for a notice activity -- &$notice: notice being output -- &$xs: XMLStringer for output - -EndActivitySource: after outputting the element for a notice activity -- &$notice: notice being output -- &$xs: XMLStringer for output - -StartActivityTitle: before outputting notice activity title -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$title: title of the notice, mutable - -EndActivityTitle: after outputting notice activity title -- $notice: notice being output -- &$xs: XMLStringer for output -- $title: title of the notice - -StartActivityAuthor: before outputting atom author -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$atomAuthor: string for XML representing atom author - -EndActivityAuthor: after outputting atom author -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$atomAuthor: string for XML representing atom author - -StartActivityActor: before outputting activity actor element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$actor: string for XML representing activity actor - -EndActivityActor: after outputting activity actor element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$actor: string for XML representing activity actor - -StartActivityLink: before outputting activity HTML link element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$url: URL for activity HTML link element for a notice activity entry - -EndActivityLink: before outputting activity HTML link element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $url: URL for activity HTML link element for a notice activity entry - -StartActivityId: before outputting atom:id element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$id: atom:id (notice URI by default) - -EndActivityId: after outputting atom:id element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $id: atom:id (notice URI by default) - -StartActivityPublished: before outputting atom:published element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$published: atom:published value (notice created by default) - -EndActivityPublished: before outputting atom:published element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $published: atom:published value (notice created by default) - -StartActivityUpdated: before outputting atom:updated element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$updated: atom:updated value (same as atom:published by default) - -EndActivityUpdated: after outputting atom:updated element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $updated: atom:updated value (same as atom:published by default) - -StartActivityContent: before outputting atom:content element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$content: atom:content value (notice rendered HTML by default) - -EndActivityContent: after outputting atom:content element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $content: atom:content value (notice rendered HTML by default) - -StartActivityVerb: before outputting activity:verb element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$verb: activity:verb URI ('http://activitystrea.ms/schema/1.0/post' by default) - -EndActivityVerb: after outputting activity:verb element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $verb: activity:verb URI ('http://activitystrea.ms/schema/1.0/post' by default) - -StartActivityDefaultObjectType: before outputting activity:object-type element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$type: activity:object-type URI for default object ('http://activitystrea.ms/schema/1.0/note' by default) - -EndActivityDefaultObjectType: after outputting activity:verb element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $type: activity:object-type URI for default object ('http://activitystrea.ms/schema/1.0/note' by default) - -StartActivityObjects: before outputting activity:object elements for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$objects: array of ActivityObject objects to output (empty by default) - -EndActivityObjects: after outputting activity:object elements for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $objects: array of ActivityObject objects to output (empty by default) - -StartActivityNoticeInfo: before outputting statusnet:notice-info element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$noticeInfoAttr: array of attributes for notice info element - -EndActivityNoticeInfo: after outputting statusnet:notice-info element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $noticeInfoAttr: array of attributes for notice info element - -StartActivityInReplyTo: before outputting thr:in-reply-to element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$replyNotice: Notice object the main notice is in-reply-to - -EndActivityInReplyTo: after outputting thr:in-reply-to element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $replyNotice: Notice object the main notice is in-reply-to - -StartActivityConversation: before outputting ostatus:conversation link element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$conv: Conversation object - -EndActivityConversation: after outputting ostatus:conversation link element for a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $conv: Conversation object - -StartActivityAttentionProfiles: before outputting ostatus:attention link element for people in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$replyProfiles: array of profiles of people being replied to - -EndActivityAttentionProfiles: after outputting ostatus:attention link element for people in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $replyProfiles: array of Profile object of people being replied to - -StartActivityAttentionGroups: before outputting ostatus:attention link element for groups in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$groups: array of Group objects of groups being addressed - -EndActivityAttentionGroups: after outputting ostatus:attention link element for groups in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $groups: array of Group objects of groups being addressed - -StartActivityForward: before outputting ostatus:forward link element in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$repeat: Notice that was repeated - -EndActivityForward: after outputting ostatus:forward link element in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $repeat: Notice that was repeated - -StartActivityCategories: before outputting atom:category elements in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$tags: array of strings for tags on the notice (used for categories) - -EndActivityCategories: after outputting atom:category elements in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $tags: array of strings for tags on the notice (used for categories) - -StartActivityEnclosures: before outputting enclosure link elements in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$enclosures: array of enclosure objects (see File::getEnclosure() for details) - -EndActivityEnclosures: after outputting enclosure link elements in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $enclosures: array of enclosure objects (see File::getEnclosure() for details) - -StartActivityGeo: before outputting geo:rss element in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- &$lat: latitude -- &$lon: longitude - -EndActivityGeo: after outputting geo:rss element in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output -- $lat: latitude -- $lon: longitude - -StartActivityEnd: before the closing in a notice activity entry (last chance for data!) -- &$notice: notice being output -- &$xs: XMLStringer for output - -EndActivityEnd: after the closing in a notice activity entry -- &$notice: notice being output -- &$xs: XMLStringer for output +EndNoticeAsActivity: after converting a notice to an activity (good time to customize!) +- $notice: notice being converted +- &$activity: activity, now more-or-less full StartNoticeSaveWeb: before saving a notice through the Web interface - $action: action being executed (instance of NewNoticeAction) @@ -1158,3 +966,134 @@ StartRevokeRole: when a role is being revoked EndRevokeRole: when a role has been revoked - $profile: profile that lost the role - $role: string name of the role + +StartAtomPubNewActivity: When a new activity comes in through Atom Pub API +- &$activity: received activity +- $user: user publishing the entry +- &$notice: notice created; initially null, can be set + +EndAtomPubNewActivity: When a new activity comes in through Atom Pub API +- $activity: received activity +- $user: user publishing the entry +- $notice: notice that was created + +StartXrdActionAliases: About to set aliases for the XRD object for a user +- &$xrd: XRD object being shown +- $user: User being shown + +EndXrdActionAliases: Done with aliases for the XRD object for a user +- &$xrd: XRD object being shown +- $user: User being shown + +StartXrdActionLinks: About to set links for the XRD object for a user +- &$xrd: XRD object being shown +- $user: User being shown + +EndXrdActionLinks: Done with links for the XRD object for a user +- &$xrd: XRD object being shown +- $user: User being shown + +AdminPanelCheck: When checking whether the current user can access a given admin panel +- $name: Name of the admin panel +- &$isOK: Boolean whether the user is allowed to use the panel + +StartAdminPanelNav: Before displaying the first item in the list of admin panels +- $nav The AdminPanelNav widget + +EndAdminPanelNav: After displaying the last item in the list of admin panels +- $nav The AdminPanelNav widget + +StartActivityObjectFromNotice: When converting a notice to an activity:object +- $notice: The notice being converted +- &$object: The resulting object. Fill this and return false to override defaults. + +EndActivityObjectFromNotice: After converting a notice to an activity:object +- $notice: The notice being converted +- &$object: The resulting object. Can be edited + +StartActivityObjectFromProfile: When converting a profile to an activity:object +- $profile: The profile being converted +- &$object: The (empty) object. Fill it up and return false to override defaults. + +EndActivityObjectFromProfile: After converting a profile to an activity:object +- $profile: The profile being converted +- &$object: The finished object. Can be tweaked + +StartActivityObjectFromGroup: When converting a group to an activity:object +- $group: The group being converted +- &$object: The (empty) object. Fill and return false to override. + +EndActivityObjectFromGroup: After converting a group to an activity:object +- $group: The group being converted +- &$object: The finished object. Tweak as needed. + +StartImportActivity: when we start to import an activity +- $user: User to make the author import +- $author: Author of the feed; good for comparisons +- $activity: The current activity +- $trusted: How "trusted" the process is +- &$done: Return value; whether to continue + +EndImportActivity: when we finish importing an activity +- $user: User to make the author import +- $author: Author of the feed; good for comparisons +- $activity: The current activity +- $trusted: How "trusted" the process is + +StartProfileSettingsActions: when we're showing account-management action list +- $action: Action being shown (use for output) + +EndProfileSettingsActions: when we're showing account-management action list +- $action: Action being shown (use for output) + +StartOpenNoticeListItemElement: Before the opening
  • of a notice list element +- $nli: The notice list item being shown + +EndOpenNoticeListItemElement: After the opening
  • of a notice list element +- $nli: The notice list item being shown + +StartCloseNoticeListItemElement: Before the closing
  • of a notice list element +- $nli: The notice list item being shown + +EndCloseNoticeListItemElement: After the closing of a notice list element +- $nli: The notice list item being shown + +StartGroupEditFormData: Beginning the group edit form entries +- $form: The form widget being shown + +EndGroupEditFormData: Ending the group edit form entries +- $form: The form widget being shown + +StartGroupSave: After initializing but before saving a group +- &$group: group about to be saved + +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' +- $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' +- $arg: Argument, if any, like 'argument' in 'foo argument' +- $user: User who issued the command +- $result: Resulting command + +StartGroupActionsList: Start the list of actions on a group profile page (after , after last ) +- $action: action being executed (for output and params) +- $group: group for the page + +StartGroupProfileElements: Start showing stuff about the group on its profile page +- $action: action being executed (for output and params) +- $group: group for the page + +EndGroupProfileElements: Start showing stuff about the group on its profile page +- $action: action being executed (for output and params) +- $group: group for the page diff --git a/README b/README index 5731ab3433..d972bf5676 100644 --- a/README +++ b/README @@ -220,14 +220,12 @@ and the URLs are listed here for your convenience. version may render your StatusNet site unable to send or receive XMPP messages. - Facebook library. Used for the Facebook application. -- PEAR Services_oEmbed. Used for some multimedia integration. -- PEAR HTTP_Request is an oEmbed dependency. -- PEAR Validate is an oEmbed dependency. -- PEAR Net_URL2 is an oEmbed dependency. +- PEAR Validate is used for URL and email validation. - Console_GetOpt for parsing command-line options. - libomb. a library for implementing OpenMicroBlogging 0.1, the predecessor to OStatus. - HTTP_Request2, a library for making HTTP requests. +- PEAR Net_URL2 is an HTTP_Request2 dependency. A design goal of StatusNet is that the basic Web functionality should work on even the most restrictive commercial hosting services. @@ -1278,6 +1276,12 @@ Profile management. biolimit: max character length of bio; 0 means no limit; null means to use the site text limit default. +backup: whether users can backup their own profiles. Defaults to true. +restore: whether users can restore their profiles from backup files. Defaults + to true. +delete: whether users can delete their own accounts. Defaults to true. +move: whether users can move their accounts to another server. Defaults + to true. newuser ------- @@ -1552,6 +1556,22 @@ cache: whether to cache the router in memcache (or another caching router cached) or others who see strange behavior. You're unlikely to need this unless you're a developer. +http +---- + +Settings for the HTTP client. + +ssl_cafile: location of the CA file for SSL. If not set, won't verify + SSL peers. Default unset. +curl: Use cURL for doing HTTP calls. You must + have the PHP curl extension installed for this to work. +proxy_host: Host to use for proxying HTTP requests. If unset, doesn't + do any HTTP proxy stuff. Default unset. +proxy_port: Port to use to connect to HTTP proxy host. Default null. +proxy_user: Username to use for authenticating to the HTTP proxy. Default null. +proxy_password: Password to use for authenticating to the HTTP proxy. Default null. +proxy_auth_scheme: Scheme to use for authenticating to the HTTP proxy. Default null. + Plugins ======= diff --git a/actions/apiaccountupdatedeliverydevice.php b/actions/apiaccountupdatedeliverydevice.php index ec87c4c953..a9ccb4fca8 100644 --- a/actions/apiaccountupdatedeliverydevice.php +++ b/actions/apiaccountupdatedeliverydevice.php @@ -143,7 +143,7 @@ class ApiAccountUpdateDeliveryDeviceAction extends ApiAuthAction if ($this->format == 'xml') { $this->initDocument('xml'); - $this->showTwitterXmlUser($twitter_user); + $this->showTwitterXmlUser($twitter_user, 'user', true); $this->endDocument('xml'); } elseif ($this->format == 'json') { $this->initDocument('json'); diff --git a/actions/apiaccountupdateprofile.php b/actions/apiaccountupdateprofile.php index 163dac42d8..d0b9abe9b7 100644 --- a/actions/apiaccountupdateprofile.php +++ b/actions/apiaccountupdateprofile.php @@ -154,7 +154,7 @@ class ApiAccountUpdateProfileAction extends ApiAuthAction if ($this->format == 'xml') { $this->initDocument('xml'); - $this->showTwitterXmlUser($twitter_user); + $this->showTwitterXmlUser($twitter_user, 'user', true); $this->endDocument('xml'); } elseif ($this->format == 'json') { $this->initDocument('json'); diff --git a/actions/apiaccountupdateprofilebackgroundimage.php b/actions/apiaccountupdateprofilebackgroundimage.php index badd8db002..f26c30198d 100644 --- a/actions/apiaccountupdateprofilebackgroundimage.php +++ b/actions/apiaccountupdateprofilebackgroundimage.php @@ -204,7 +204,7 @@ class ApiAccountUpdateProfileBackgroundImageAction extends ApiAuthAction if ($this->format == 'xml') { $this->initDocument('xml'); - $this->showTwitterXmlUser($twitter_user); + $this->showTwitterXmlUser($twitter_user, 'user', true); $this->endDocument('xml'); } elseif ($this->format == 'json') { $this->initDocument('json'); diff --git a/actions/apiaccountupdateprofilecolors.php b/actions/apiaccountupdateprofilecolors.php index 109fbf959e..4c102c4090 100644 --- a/actions/apiaccountupdateprofilecolors.php +++ b/actions/apiaccountupdateprofilecolors.php @@ -188,7 +188,7 @@ class ApiAccountUpdateProfileColorsAction extends ApiAuthAction if ($this->format == 'xml') { $this->initDocument('xml'); - $this->showTwitterXmlUser($twitter_user); + $this->showTwitterXmlUser($twitter_user, 'user', true); $this->endDocument('xml'); } elseif ($this->format == 'json') { $this->initDocument('json'); diff --git a/actions/apiaccountupdateprofileimage.php b/actions/apiaccountupdateprofileimage.php index f2886509d7..986a8f3f1e 100644 --- a/actions/apiaccountupdateprofileimage.php +++ b/actions/apiaccountupdateprofileimage.php @@ -112,16 +112,17 @@ class ApiAccountUpdateProfileImageAction extends ApiAuthAction return; } + $type = $imagefile->preferredType(); $filename = Avatar::filename( $user->id, - image_type_to_extension($imagefile->type), + image_type_to_extension($type), null, 'tmp'.common_timestamp() ); $filepath = Avatar::path($filename); - move_uploaded_file($imagefile->filepath, $filepath); + $imagefile->copyTo($filepath); $profile = $this->user->getProfile(); @@ -139,7 +140,7 @@ class ApiAccountUpdateProfileImageAction extends ApiAuthAction if ($this->format == 'xml') { $this->initDocument('xml'); - $this->showTwitterXmlUser($twitter_user); + $this->showTwitterXmlUser($twitter_user, 'user', true); $this->endDocument('xml'); } elseif ($this->format == 'json') { $this->initDocument('json'); diff --git a/actions/apiatomservice.php b/actions/apiatomservice.php new file mode 100644 index 0000000000..5d786723e9 --- /dev/null +++ b/actions/apiatomservice.php @@ -0,0 +1,135 @@ +. + * + * @category API + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +require_once INSTALLDIR.'/lib/apibareauth.php'; + +/** + * Shows an AtomPub service document for a user + * + * @category API + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ +class ApiAtomServiceAction extends ApiBareAuthAction +{ + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + function prepare($args) + { + parent::prepare($args); + $this->user = $this->getTargetUser($this->arg('id')); + + if (empty($this->user)) { + // TRANS: Client error displayed when making an Atom API request for an unknown user. + $this->clientError(_('No such user.'), 404, $this->format); + return; + } + + return true; + } + + /** + * Handle the arguments. In our case, show a service document. + * + * @param Array $args unused. + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + header('Content-Type: application/atomsvc+xml'); + + $this->startXML(); + $this->elementStart('service', array('xmlns' => 'http://www.w3.org/2007/app', + 'xmlns:atom' => 'http://www.w3.org/2005/Atom', + 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/')); + $this->elementStart('workspace'); + // TRANS: Title for Atom feed. + $this->element('atom:title', null, _m('ATOM','Main')); + $this->elementStart('collection', + array('href' => common_local_url('ApiTimelineUser', + array('id' => $this->user->id, + 'format' => 'atom')))); + $this->element('atom:title', + null, + // TRANS: Title for Atom feed. %s is a user nickname. + sprintf(_("%s timeline"), + $this->user->nickname)); + $this->element('accept', null, 'application/atom+xml;type=entry'); + $this->element('activity:verb', null, ActivityVerb::POST); + $this->elementEnd('collection'); + $this->elementStart('collection', + array('href' => common_local_url('AtomPubSubscriptionFeed', + array('subscriber' => $this->user->id)))); + $this->element('atom:title', + null, + // TRANS: Title for Atom feed with a user's subscriptions. %s is a user nickname. + sprintf(_("%s subscriptions"), + $this->user->nickname)); + $this->element('accept', null, 'application/atom+xml;type=entry'); + $this->element('activity:verb', null, ActivityVerb::FOLLOW); + $this->elementEnd('collection'); + $this->elementStart('collection', + array('href' => common_local_url('AtomPubFavoriteFeed', + array('profile' => $this->user->id)))); + $this->element('atom:title', + null, + // TRANS: Title for Atom feed with a user's favorite notices. %s is a user nickname. + sprintf(_("%s favorites"), + $this->user->nickname)); + $this->element('accept', null, 'application/atom+xml;type=entry'); + $this->element('activity:verb', null, ActivityVerb::FAVORITE); + $this->elementEnd('collection'); + $this->elementStart('collection', + array('href' => common_local_url('AtomPubMembershipFeed', + array('profile' => $this->user->id)))); + $this->element('atom:title', + null, + // TRANS: Title for Atom feed with a user's memberships. %s is a user nickname. + sprintf(_("%s memberships"), + $this->user->nickname)); + $this->element('accept', null, 'application/atom+xml;type=entry'); + $this->element('activity:verb', null, ActivityVerb::JOIN); + $this->elementEnd('collection'); + $this->elementEnd('workspace'); + $this->elementEnd('service'); + $this->endXML(); + } +} diff --git a/actions/apiblockcreate.php b/actions/apiblockcreate.php index a9f31e791b..6942a53bb8 100644 --- a/actions/apiblockcreate.php +++ b/actions/apiblockcreate.php @@ -92,6 +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. $this->clientError(_('No such user.'), 404, $this->format); return; } diff --git a/actions/apidirectmessagenew.php b/actions/apidirectmessagenew.php index b335a9c93e..978c753532 100644 --- a/actions/apidirectmessagenew.php +++ b/actions/apidirectmessagenew.php @@ -119,7 +119,7 @@ class ApiDirectMessageNewAction extends ApiAuthAction $this->format ); } else { - $content_shortened = common_shorten_links($this->content); + $content_shortened = $this->auth_user->shortenLinks($this->content); if (Message::contentTooLong($content_shortened)) { $this->clientError( // TRANS: Client error displayed when message content is too long. diff --git a/actions/apifriendshipsexists.php b/actions/apifriendshipsexists.php index c8766633b6..43b1daf4fc 100644 --- a/actions/apifriendshipsexists.php +++ b/actions/apifriendshipsexists.php @@ -85,7 +85,7 @@ class ApiFriendshipsExistsAction extends ApiPrivateAuthAction if (empty($this->profile_a) || empty($this->profile_b)) { $this->clientError( // TRANS: Client error displayed when supplying invalid parameters to an API call checking if a friendship exists. - _('Two valid IDs or screen_names must be supplied.'), + _('Two valid IDs or nick names must be supplied.'), 400, $this->format ); diff --git a/actions/apigroupcreate.php b/actions/apigroupcreate.php index 54875a7188..d01504bc80 100644 --- a/actions/apigroupcreate.php +++ b/actions/apigroupcreate.php @@ -73,7 +73,7 @@ class ApiGroupCreateAction extends ApiAuthAction $this->user = $this->auth_user; - $this->nickname = $this->arg('nickname'); + $this->nickname = Nickname::normalize($this->arg('nickname')); $this->fullname = $this->arg('full_name'); $this->homepage = $this->arg('homepage'); $this->description = $this->arg('description'); @@ -150,26 +150,7 @@ class ApiGroupCreateAction extends ApiAuthAction */ function validateParams() { - $valid = Validate::string( - $this->nickname, array( - 'min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT - ) - ); - - if (!$valid) { - $this->clientError( - // TRANS: Validation error in form for group creation. - _( - 'Nickname must have only lowercase letters ' . - 'and numbers and no spaces.' - ), - 403, - $this->format - ); - return false; - } elseif ($this->groupNicknameExists($this->nickname)) { + if ($this->groupNicknameExists($this->nickname)) { $this->clientError( // TRANS: Client error trying to create a group with a nickname this is already in use. _('Nickname already in use. Try another one.'), @@ -265,15 +246,7 @@ class ApiGroupCreateAction extends ApiAuthAction foreach ($this->aliases as $alias) { - $valid = Validate::string( - $alias, array( - 'min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT - ) - ); - - if (!$valid) { + if (!Nickname::isValid($alias)) { $this->clientError( // TRANS: Client error shown when providing an invalid alias during group creation. // TRANS: %s is the invalid alias. diff --git a/actions/apigroupmembership.php b/actions/apigroupmembership.php index 99ac965fa1..939d22d757 100644 --- a/actions/apigroupmembership.php +++ b/actions/apigroupmembership.php @@ -66,6 +66,12 @@ class ApiGroupMembershipAction extends ApiPrivateAuthAction parent::prepare($args); $this->group = $this->getTargetGroup($this->arg('id')); + if (empty($this->group)) { + // TRANS: Client error displayed trying to show group membership on a non-existing group. + $this->clientError(_('Group not found.'), 404, $this->format); + return false; + } + $this->profiles = $this->getProfiles(); return true; @@ -84,12 +90,6 @@ class ApiGroupMembershipAction extends ApiPrivateAuthAction { parent::handle($args); - if (empty($this->group)) { - // TRANS: Client error displayed trying to show group membership on a non-existing group. - $this->clientError(_('Group not found.'), 404, $this->format); - return false; - } - // XXX: RSS and Atom switch($this->format) { diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php index b2c0de719a..d76ae060f2 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -421,7 +421,7 @@ class ApiOauthAuthorizeAction extends Action if ($this->app->name == 'anonymous') { // Special message for the anonymous app and consumer. // TRANS: User notification of external application requesting account access. - // TRANS: %3$s is the access type requested, %4$s is the StatusNet sitename. + // TRANS: %3$s is the access type requested (read-write or read-only), %4$s is the StatusNet sitename. $msg = _('An application would like the ability ' . 'to %3$s your %4$s account data. ' . 'You should only give access to your %4$s account ' . diff --git a/actions/apistatusesshow.php b/actions/apistatusesshow.php index a98e45f79c..9a7f36bb3a 100644 --- a/actions/apistatusesshow.php +++ b/actions/apistatusesshow.php @@ -100,13 +100,24 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction { parent::handle($args); - if (!in_array($this->format, array('xml', 'json'))) { + if (!in_array($this->format, array('xml', 'json', 'atom'))) { // TRANS: Client error displayed when trying to handle an unknown API method. - $this->clientError(_('API method not found.'), $code = 404); + $this->clientError(_('API method not found.'), 404); return; } - $this->showNotice(); + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + $this->showNotice(); + break; + case 'DELETE': + $this->deleteNotice(); + break; + default: + // TRANS: Client error displayed calling an unsupported HTTP error in API status show. + $this->clientError(_('HTTP method not supported.'), 405); + return; + } } /** @@ -117,10 +128,20 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction function showNotice() { if (!empty($this->notice)) { - if ($this->format == 'xml') { + switch ($this->format) { + case 'xml': $this->showSingleXmlStatus($this->notice); - } elseif ($this->format == 'json') { + break; + case 'json': $this->show_single_json_status($this->notice); + break; + case 'atom': + $this->showSingleAtomStatus($this->notice); + break; + default: + // TRANS: Exception thrown requesting an unsupported notice output format. + // TRANS: %s is the requested output format. + throw new Exception(sprintf(_("Unsupported format: %s"), $this->format)); } } else { // XXX: Twitter just sets a 404 header and doens't bother @@ -147,15 +168,16 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction } /** - * Is this action read only? + * We expose AtomPub here, so non-GET/HEAD reqs must be read/write. * * @param array $args other arguments * * @return boolean true */ + function isReadOnly($args) { - return true; + return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD'); } /** @@ -197,4 +219,34 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction return null; } + + function deleteNotice() + { + if ($this->format != 'atom') { + // TRANS: Client error displayed when trying to delete a notice not using the Atom format. + $this->clientError(_("Can only delete using the Atom format.")); + return; + } + + if (empty($this->auth_user) || + ($this->notice->profile_id != $this->auth_user->id && + !$this->auth_user->hasRight(Right::DELETEOTHERSNOTICE))) { + // TRANS: Client error displayed when a user has no rights to delete notices of other users. + $this->clientError(_('Cannot delete this notice.'), 403); + return; + } + + if (Event::handle('StartDeleteOwnNotice', array($this->auth_user, $this->notice))) { + $this->notice->delete(); + Event::handle('EndDeleteOwnNotice', array($this->auth_user, $this->notice)); + } + + // @fixme is there better output we could do here? + + header('HTTP/1.1 200 OK'); + header('Content-Type: text/plain'); + // TRANS: Confirmation of notice deletion in API. %d is the ID (number) of the deleted notice. + print(sprintf(_('Deleted notice %d'), $this->notice->id)); + print("\n"); + } } diff --git a/actions/apistatusesupdate.php b/actions/apistatusesupdate.php index 666ed9fa32..5773bdc2e8 100644 --- a/actions/apistatusesupdate.php +++ b/actions/apistatusesupdate.php @@ -55,7 +55,7 @@ Yes @param status (Required) The URL-encoded text of the status update. - @param source (Optional) The source of the status. + @param source (Optional) The source application name, if using HTTP authentication or an anonymous OAuth consumer. @param in_reply_to_status_id (Optional) The ID of an existing status that the update is in reply to. @param lat (Optional) The latitude the status refers to. @param long (Optional) The longitude the status refers to. @@ -67,7 +67,7 @@ @subsection usagenotes Usage notes @li The URL pattern is relative to the @ref apiroot. - @li If the @e source parameter is not supplied the source of the status will default to 'api'. + @li If the @e source parameter is not supplied the source of the status will default to 'api'. When authenticated via a registered OAuth application, the application's registered name and URL will always override the source parameter. @li The XML response uses GeoRSS to encode the latitude and longitude (see example response below ). @li Data uploaded via the @e media parameter should be multipart/form-data encoded. @@ -231,7 +231,7 @@ class ApiStatusesUpdateAction extends ApiAuthAction return; } - $status_shortened = common_shorten_links($this->status); + $status_shortened = $this->auth_user->shortenlinks($this->status); if (Notice::contentTooLong($status_shortened)) { // Note: Twitter truncates anything over 140, flags the status @@ -377,7 +377,7 @@ class ApiStatusesUpdateAction extends ApiAuthAction function supported($cmd) { static $cmdlist = array('MessageCommand', 'SubCommand', 'UnsubCommand', - 'FavCommand', 'OnCommand', 'OffCommand'); + 'FavCommand', 'OnCommand', 'OffCommand', 'JoinCommand', 'LeaveCommand'); if (in_array(get_class($cmd), $cmdlist)) { return true; diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index a85da4b0f3..e1bc102e45 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -125,10 +125,6 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction header('Content-Type: application/atom+xml; charset=utf-8'); try { - $atom->addAuthorRaw($this->group->asAtomAuthor()); - $atom->setActivitySubject($this->group->asActivitySubject()); - $atom->setId($self); - $atom->setSelfLink($self); $atom->addEntryFromNotices($this->notices); $this->raw($atom->getString()); } catch (Atom10FeedException $e) { diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 0046c462d7..b3b908accc 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -97,7 +97,12 @@ class ApiTimelineUserAction extends ApiBareAuthAction function handle($args) { parent::handle($args); - $this->showTimeline(); + + if ($this->isPost()) { + $this->handlePost(); + } else { + $this->showTimeline(); + } } /** @@ -114,9 +119,9 @@ class ApiTimelineUserAction extends ApiBareAuthAction $atom = new AtomUserNoticeFeed($this->user, $this->auth_user); $link = common_local_url( - 'showstream', - array('nickname' => $this->user->nickname) - ); + 'showstream', + array('nickname' => $this->user->nickname) + ); $self = $this->getSelfUri(); @@ -132,20 +137,63 @@ class ApiTimelineUserAction extends ApiBareAuthAction break; case 'rss': $this->showRssTimeline( - $this->notices, - $atom->title, - $link, - $atom->subtitle, - $suplink, - $atom->logo, - $self - ); + $this->notices, + $atom->title, + $link, + $atom->subtitle, + $suplink, + $atom->logo, + $self + ); break; case 'atom': header('Content-Type: application/atom+xml; charset=utf-8'); $atom->setId($self); $atom->setSelfLink($self); + + // Add navigation links: next, prev, first + // Note: we use IDs rather than pages for navigation; page boundaries + // change too quickly! + + if (!empty($this->next_id)) { + $nextUrl = common_local_url('ApiTimelineUser', + array('format' => 'atom', + 'id' => $this->user->id), + array('max_id' => $this->next_id)); + + $atom->addLink($nextUrl, + array('rel' => 'next', + 'type' => 'application/atom+xml')); + } + + if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) { + + $lastNotice = $this->notices[0]; + $lastId = $lastNotice->id; + + $prevUrl = common_local_url('ApiTimelineUser', + array('format' => 'atom', + 'id' => $this->user->id), + array('since_id' => $lastId)); + + $atom->addLink($prevUrl, + array('rel' => 'prev', + 'type' => 'application/atom+xml')); + } + + if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) { + + $firstUrl = common_local_url('ApiTimelineUser', + array('format' => 'atom', + 'id' => $this->user->id)); + + $atom->addLink($firstUrl, + array('rel' => 'first', + 'type' => 'application/atom+xml')); + + } + $atom->addEntryFromNotices($this->notices); $this->raw($atom->getString()); @@ -169,28 +217,34 @@ class ApiTimelineUserAction extends ApiBareAuthAction { $notices = array(); - $notice = $this->user->getNotices( - ($this->page-1) * $this->count, $this->count, - $this->since_id, $this->max_id - ); + $notice = $this->user->getNotices(($this->page-1) * $this->count, + $this->count + 1, + $this->since_id, + $this->max_id); while ($notice->fetch()) { - $notices[] = clone($notice); + if (count($notices) < $this->count) { + $notices[] = clone($notice); + } else { + $this->next_id = $notice->id; + break; + } } return $notices; } /** - * Is this action read only? + * We expose AtomPub here, so non-GET/HEAD reqs must be read/write. * * @param array $args other arguments * * @return boolean true */ + function isReadOnly($args) { - return true; + return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD'); } /** @@ -221,17 +275,226 @@ class ApiTimelineUserAction extends ApiBareAuthAction $last = count($this->notices) - 1; return '"' . implode( - ':', - array($this->arg('action'), - common_user_cache_hash($this->auth_user), - common_language(), - $this->user->id, - strtotime($this->notices[0]->created), - strtotime($this->notices[$last]->created)) - ) - . '"'; + ':', + array($this->arg('action'), + common_user_cache_hash($this->auth_user), + common_language(), + $this->user->id, + strtotime($this->notices[0]->created), + strtotime($this->notices[$last]->created)) + ) + . '"'; } return null; } + + function handlePost() + { + if (empty($this->auth_user) || + $this->auth_user->id != $this->user->id) { + // TRANS: Client error displayed trying to add a notice to another user's timeline. + $this->clientError(_('Only the user can add to their own timeline.')); + return; + } + + // Only handle posts for Atom + if ($this->format != 'atom') { + // TRANS: Client error displayed when using another format than AtomPub. + $this->clientError(_('Only accept AtomPub for Atom feeds.')); + return; + } + + $xml = trim(file_get_contents('php://input')); + if (empty($xml)) { + // TRANS: Client error displayed attempting to post an empty API notice. + $this->clientError(_('Atom post must not be empty.')); + } + + $dom = DOMDocument::loadXML($xml); + if (!$dom) { + // TRANS: Client error displayed attempting to post an API that is not well-formed XML. + $this->clientError(_('Atom post must be well-formed XML.')); + } + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + // TRANS: Client error displayed when not using an Atom entry. + $this->clientError(_('Atom post must be an Atom entry.')); + return; + } + + $activity = new Activity($dom->documentElement); + + $saved = null; + + if (Event::handle('StartAtomPubNewActivity', array(&$activity, $this->user, &$saved))) { + + if ($activity->verb != ActivityVerb::POST) { + // TRANS: Client error displayed when not using the POST verb. Do not translate POST. + $this->clientError(_('Can only handle POST activities.')); + return; + } + + $note = $activity->objects[0]; + + if (!in_array($note->type, array(ActivityObject::NOTE, + ActivityObject::BLOGENTRY, + ActivityObject::STATUS))) { + // TRANS: Client error displayed when using an unsupported activity object type. + // TRANS: %s is the unsupported activity object type. + $this->clientError(sprintf(_('Cannot handle activity object type "%s".'), + $note->type)); + return; + } + + $saved = $this->postNote($activity); + + Event::handle('EndAtomPubNewActivity', array($activity, $this->user, $saved)); + } + + if (!empty($saved)) { + header('HTTP/1.1 201 Created'); + header("Location: " . common_local_url('ApiStatusesShow', array('id' => $saved->id, + 'format' => 'atom'))); + $this->showSingleAtomStatus($saved); + } + } + + function postNote($activity) + { + $note = $activity->objects[0]; + + // Use summary as fallback for content + + if (!empty($note->content)) { + $sourceContent = $note->content; + } else if (!empty($note->summary)) { + $sourceContent = $note->summary; + } else if (!empty($note->title)) { + $sourceContent = $note->title; + } else { + // @fixme fetch from $sourceUrl? + // TRANS: Client error displayed when posting a notice without content through the API. + // TRANS: %d is the notice ID (number). + $this->clientError(sprintf(_('No content for notice %d.'), + $note->id)); + return; + } + + // Get (safe!) HTML and text versions of the content + + $rendered = $this->purify($sourceContent); + $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8'); + + $shortened = $this->auth_user->shortenLinks($content); + + $options = array('is_local' => Notice::LOCAL_PUBLIC, + 'rendered' => $rendered, + 'replies' => array(), + 'groups' => array(), + 'tags' => array(), + 'urls' => array()); + + // accept remote URI (not necessarily a good idea) + + common_debug("Note ID is {$note->id}"); + + if (!empty($note->id)) { + $notice = Notice::staticGet('uri', trim($note->id)); + + if (!empty($notice)) { + // TRANS: Client error displayed when using another format than AtomPub. + $this->clientError(sprintf(_('Notice with URI "%s" already exists.'), + $note->id)); + return; + } + common_log(LOG_NOTICE, "Saving client-supplied notice URI '$note->id'"); + $options['uri'] = $note->id; + } + + // accept remote create time (also maybe not such a good idea) + + if (!empty($activity->time)) { + common_log(LOG_NOTICE, "Saving client-supplied create time {$activity->time}"); + $options['created'] = common_sql_date($activity->time); + } + + // Check for optional attributes... + + if (!empty($activity->context)) { + + foreach ($activity->context->attention as $uri) { + + $profile = Profile::fromURI($uri); + + if (!empty($profile)) { + $options['replies'][] = $uri; + } else { + $group = User_group::staticGet('uri', $uri); + if (!empty($group)) { + $options['groups'][] = $uri; + } else { + // @fixme: hook for discovery here + common_log(LOG_WARNING, sprintf('AtomPub post with unknown attention URI %s', $uri)); + } + } + } + + // Maintain direct reply associations + // @fixme what about conversation ID? + + if (!empty($activity->context->replyToID)) { + $orig = Notice::staticGet('uri', + $activity->context->replyToID); + if (!empty($orig)) { + $options['reply_to'] = $orig->id; + } + } + + $location = $activity->context->location; + + if ($location) { + $options['lat'] = $location->lat; + $options['lon'] = $location->lon; + if ($location->location_id) { + $options['location_ns'] = $location->location_ns; + $options['location_id'] = $location->location_id; + } + } + } + + // Atom categories <-> hashtags + + foreach ($activity->categories as $cat) { + if ($cat->term) { + $term = common_canonical_tag($cat->term); + if ($term) { + $options['tags'][] = $term; + } + } + } + + // Atom enclosures -> attachment URLs + foreach ($activity->enclosures as $href) { + // @fixme save these locally or....? + $options['urls'][] = $href; + } + + $saved = Notice::saveNew($this->user->id, + $content, + 'atompub', // TODO: deal with this + $options); + + return $saved; + } + + function purify($content) + { + require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php'; + + $config = array('safe' => 1, + 'deny_attribute' => 'id,style,on*'); + return htmLawed($content, $config); + } } diff --git a/actions/apiuserprofileimage.php b/actions/apiuserprofileimage.php new file mode 100644 index 0000000000..d2cf9a3e57 --- /dev/null +++ b/actions/apiuserprofileimage.php @@ -0,0 +1,135 @@ +. + * + * @category API + * @package StatusNet + * @author Brion Vibber + * @copyright 2010 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')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiprivateauth.php'; + +/** + * Ouputs avatar URL for a user, specified by screen name. + * Unlike most API endpoints, this returns an HTTP redirect rather than direct data. + * + * @category API + * @package StatusNet + * @author Brion Vibber + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class ApiUserProfileImageAction extends ApiPrivateAuthAction +{ + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + function prepare($args) + { + parent::prepare($args); + $this->user = User::staticGet('nickname', $this->arg('screen_name')); + $this->size = $this->arg('size'); + + return true; + } + + /** + * Handle the request + * + * Check the format and show the user info + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + if (empty($this->user)) { + // TRANS: Client error displayed when requesting user information for a non-existing user. + $this->clientError(_('User not found.'), 404, $this->format); + return; + } + + $profile = $this->user->getProfile(); + + if (empty($profile)) { + // TRANS: Client error displayed when requesting user information for a user without a profile. + $this->clientError(_('User has no profile.')); + return; + } + + $size = $this->avatarSize(); + $avatar = $profile->getAvatar($size); + if ($avatar) { + $url = $avatar->displayUrl(); + } else { + $url = Avatar::defaultImage($size); + } + + // We don't actually output JSON or XML data -- redirect! + common_redirect($url, 302); + } + + /** + * Get the appropriate pixel size for an avatar based on the request... + * + * @return int + */ + private function avatarSize() + { + switch ($this->size) { + case 'mini': + return AVATAR_MINI_SIZE; // 24x24 + case 'bigger': + return AVATAR_PROFILE_SIZE; // Twitter does 73x73, but we do 96x96 + case 'normal': // fall through + default: + return AVATAR_STREAM_SIZE; // 48x48 + } + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return true; + } +} diff --git a/actions/atompubfavoritefeed.php b/actions/atompubfavoritefeed.php new file mode 100644 index 0000000000..c31fcbd72a --- /dev/null +++ b/actions/atompubfavoritefeed.php @@ -0,0 +1,374 @@ +. + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Feed of ActivityStreams 'favorite' actions + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class AtompubfavoritefeedAction extends ApiAuthAction +{ + private $_profile = null; + private $_faves = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + + $this->_profile = Profile::staticGet('id', $this->trimmed('profile')); + + if (empty($this->_profile)) { + // TRANS: Client exception thrown when requesting a favorite feed for a non-existing profile. + throw new ClientException(_('No such profile.'), 404); + } + + $offset = ($this->page-1) * $this->count; + $limit = $this->count + 1; + + $this->_faves = Fave::byProfile($this->_profile->id, + $offset, + $limit); + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + parent::handle($argarray); + + switch ($_SERVER['REQUEST_METHOD']) { + case 'HEAD': + case 'GET': + $this->showFeed(); + break; + case 'POST': + $this->addFavorite(); + break; + default: + // TRANS: Client exception thrown when using an unsupported HTTP method. + throw new ClientException(_('HTTP method not supported.'), 405); + return; + } + + return; + } + + /** + * Show a feed of favorite activity streams objects + * + * @return void + */ + function showFeed() + { + header('Content-Type: application/atom+xml; charset=utf-8'); + + $url = common_local_url('AtomPubFavoriteFeed', + array('profile' => $this->_profile->id)); + + $feed = new Atom10Feed(true); + + $feed->addNamespace('activity', + 'http://activitystrea.ms/spec/1.0/'); + + $feed->addNamespace('poco', + 'http://portablecontacts.net/spec/1.0'); + + $feed->addNamespace('media', + 'http://purl.org/syndication/atommedia'); + + $feed->id = $url; + + $feed->setUpdated('now'); + + $feed->addAuthor($this->_profile->getBestName(), + $this->_profile->getURI()); + + // TRANS: Title for Atom favorites feed. + // TRANS: %s is a user nickname. + $feed->setTitle(sprintf(_("%s favorites"), + $this->_profile->getBestName())); + + // TRANS: Subtitle for Atom favorites feed. + // TRANS: %1$s is a user nickname, %2$s is the StatusNet sitename. + $feed->setSubtitle(sprintf(_('Notices %1$s has favorited on %2$s'), + $this->_profile->getBestName(), + common_config('site', 'name'))); + + $feed->addLink(common_local_url('showfavorites', + array('nickname' => + $this->_profile->nickname))); + + $feed->addLink($url, + array('rel' => 'self', + 'type' => 'application/atom+xml')); + + // If there's more... + + if ($this->page > 1) { + $feed->addLink($url, + array('rel' => 'first', + 'type' => 'application/atom+xml')); + + $feed->addLink(common_local_url('AtomPubFavoriteFeed', + array('profile' => + $this->_profile->id), + array('page' => + $this->page - 1)), + array('rel' => 'prev', + 'type' => 'application/atom+xml')); + } + + if ($this->_faves->N > $this->count) { + + $feed->addLink(common_local_url('AtomPubFavoriteFeed', + array('profile' => + $this->_profile->id), + array('page' => + $this->page + 1)), + array('rel' => 'next', + 'type' => 'application/atom+xml')); + } + + $i = 0; + + while ($this->_faves->fetch()) { + + // We get one more than needed; skip that one + + $i++; + + if ($i > $this->count) { + break; + } + + $act = $this->_faves->asActivity(); + $feed->addEntryRaw($act->asString(false, false, false)); + } + + $this->raw($feed->getString()); + } + + /** + * add a new favorite + * + * @return void + */ + function addFavorite() + { + // XXX: Refactor this; all the same for atompub + + if (empty($this->auth_user) || + $this->auth_user->id != $this->_profile->id) { + // TRANS: Client exception thrown when trying to set a favorite for another user. + throw new ClientException(_("Cannot add someone else's". + " subscription."), 403); + } + + $xml = file_get_contents('php://input'); + + $dom = DOMDocument::loadXML($xml); + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + // TRANS: Client error displayed when not using an Atom entry. + throw new ClientException(_('Atom post must be an Atom entry.')); + return; + } + + $activity = new Activity($dom->documentElement); + + $fave = null; + + if (Event::handle('StartAtomPubNewActivity', array(&$activity))) { + + if ($activity->verb != ActivityVerb::FAVORITE) { + // TRANS: Client exception thrown when trying use an incorrect activity verb for the Atom pub method. + throw new ClientException(_('Can only handle favorite activities.')); + return; + } + + $note = $activity->objects[0]; + + if (!in_array($note->type, array(ActivityObject::NOTE, + ActivityObject::BLOGENTRY, + ActivityObject::STATUS))) { + // TRANS: Client exception thrown when trying favorite an object that is not a notice. + throw new ClientException(_('Can only fave notices.')); + return; + } + + $notice = Notice::staticGet('uri', $note->id); + + if (empty($notice)) { + // XXX: import from listed URL or something + // TRANS: Client exception thrown when trying favorite a notice without content. + throw new ClientException(_('Unknown note.')); + } + + $old = Fave::pkeyGet(array('user_id' => $this->auth_user->id, + 'notice_id' => $notice->id)); + + if (!empty($old)) { + // TRANS: Client exception thrown when trying favorite an already favorited notice. + throw new ClientException(_('Already a favorite.')); + } + + $profile = $this->auth_user->getProfile(); + + $fave = Fave::addNew($profile, $notice); + + if (!empty($fave)) { + $this->_profile->blowFavesCache(); + $this->notify($fave, $notice, $this->auth_user); + } + + Event::handle('EndAtomPubNewActivity', array($activity, $fave)); + } + + if (!empty($fave)) { + $act = $fave->asActivity(); + + header('Content-Type: application/atom+xml; charset=utf-8'); + header('Content-Location: ' . $act->selfLink); + + $this->startXML(); + $this->raw($act->asString(true, true, true)); + $this->endXML(); + } + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + function lastModified() + { + // For comparison with If-Last-Modified + // If not applicable, return null + return null; + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + function etag() + { + return null; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return false; + } else { + return true; + } + } + + /** + * Notify the author of the favorite that the user likes their notice + * + * @param Favorite $fave the favorite in question + * @param Notice $notice the notice that's been faved + * @param User $user the user doing the favoriting + * + * @return void + */ + function notify($fave, $notice, $user) + { + $other = User::staticGet('id', $notice->profile_id); + if ($other && $other->id != $user->id) { + if ($other->email && $other->emailnotifyfav) { + mail_notify_fave($other, $user, $notice); + } + // XXX: notify by IM + // XXX: notify by SMS + } + } +} diff --git a/actions/atompubmembershipfeed.php b/actions/atompubmembershipfeed.php new file mode 100644 index 0000000000..b52583314d --- /dev/null +++ b/actions/atompubmembershipfeed.php @@ -0,0 +1,357 @@ +. + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Feed of group memberships for a user, in ActivityStreams format + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class AtompubmembershipfeedAction extends ApiAuthAction +{ + private $_profile = null; + private $_memberships = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + + $profileId = $this->trimmed('profile'); + + $this->_profile = Profile::staticGet('id', $profileId); + + if (empty($this->_profile)) { + // TRANS: Client exception. + throw new ClientException(_('No such profile.'), 404); + } + + $offset = ($this->page-1) * $this->count; + $limit = $this->count + 1; + + $this->_memberships = Group_member::byMember($this->_profile->id, + $offset, + $limit); + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + parent::handle($argarray); + + switch ($_SERVER['REQUEST_METHOD']) { + case 'HEAD': + case 'GET': + $this->showFeed(); + break; + case 'POST': + $this->addMembership(); + break; + default: + // TRANS: Client exception thrown when using an unsupported HTTP method. + throw new ClientException(_('HTTP method not supported.'), 405); + return; + } + + return; + } + + /** + * Show a feed of favorite activity streams objects + * + * @return void + */ + function showFeed() + { + header('Content-Type: application/atom+xml; charset=utf-8'); + + $url = common_local_url('AtomPubMembershipFeed', + array('profile' => $this->_profile->id)); + + $feed = new Atom10Feed(true); + + $feed->addNamespace('activity', + 'http://activitystrea.ms/spec/1.0/'); + + $feed->addNamespace('poco', + 'http://portablecontacts.net/spec/1.0'); + + $feed->addNamespace('media', + 'http://purl.org/syndication/atommedia'); + + $feed->id = $url; + + $feed->setUpdated('now'); + + $feed->addAuthor($this->_profile->getBestName(), + $this->_profile->getURI()); + + // TRANS: Title for group membership feed. + // TRANS: %s is a username. + $feed->setTitle(sprintf(_("%s group memberships"), + $this->_profile->getBestName())); + + // TRANS: Subtitle for group membership feed. + // TRANS: %1$s is a username, %2$s is the StatusNet sitename. + $feed->setSubtitle(sprintf(_('Groups %1$s is a member of on %2$s'), + $this->_profile->getBestName(), + common_config('site', 'name'))); + + $feed->addLink(common_local_url('usergroups', + array('nickname' => + $this->_profile->nickname))); + + $feed->addLink($url, + array('rel' => 'self', + 'type' => 'application/atom+xml')); + + // If there's more... + + if ($this->page > 1) { + $feed->addLink($url, + array('rel' => 'first', + 'type' => 'application/atom+xml')); + + $feed->addLink(common_local_url('AtomPubMembershipFeed', + array('profile' => + $this->_profile->id), + array('page' => + $this->page - 1)), + array('rel' => 'prev', + 'type' => 'application/atom+xml')); + } + + if ($this->_memberships->N > $this->count) { + + $feed->addLink(common_local_url('AtomPubMembershipFeed', + array('profile' => + $this->_profile->id), + array('page' => + $this->page + 1)), + array('rel' => 'next', + 'type' => 'application/atom+xml')); + } + + $i = 0; + + while ($this->_memberships->fetch()) { + + // We get one more than needed; skip that one + + $i++; + + if ($i > $this->count) { + break; + } + + $act = $this->_memberships->asActivity(); + $feed->addEntryRaw($act->asString(false, false, false)); + } + + $this->raw($feed->getString()); + } + + /** + * add a new favorite + * + * @return void + */ + function addMembership() + { + // XXX: Refactor this; all the same for atompub + + if (empty($this->auth_user) || + $this->auth_user->id != $this->_profile->id) { + // TRANS: Client exception thrown when trying subscribe someone else to a group. + throw new ClientException(_("Cannot add someone else's". + " membership."), 403); + } + + $xml = file_get_contents('php://input'); + + $dom = DOMDocument::loadXML($xml); + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + // TRANS: Client error displayed when not using an Atom entry. + throw new ClientException(_('Atom post must be an Atom entry.')); + return; + } + + $activity = new Activity($dom->documentElement); + + $membership = null; + + if (Event::handle('StartAtomPubNewActivity', array(&$activity))) { + if ($activity->verb != ActivityVerb::JOIN) { + // TRANS: Client error displayed when not using the POST verb. + // TRANS: Do not translate POST. + throw new ClientException(_('Can only handle join activities.')); + return; + } + + $groupObj = $activity->objects[0]; + + if ($groupObj->type != ActivityObject::GROUP) { + // TRANS: Client exception thrown when trying favorite an object that is not a notice. + throw new ClientException(_('Can only fave notices.')); + return; + } + + $group = User_group::staticGet('uri', $groupObj->id); + + if (empty($group)) { + // XXX: import from listed URL or something + // TRANS: Client exception thrown when trying to subscribe to a non-existing group. + throw new ClientException(_('Unknown group.')); + } + + $old = Group_member::pkeyGet(array('profile_id' => $this->auth_user->id, + 'group_id' => $group->id)); + + if (!empty($old)) { + // TRANS: Client exception thrown when trying to subscribe to an already subscribed group. + throw new ClientException(_('Already a member.')); + } + + $profile = $this->auth_user->getProfile(); + + if (Group_block::isBlocked($group, $profile)) { + // XXX: import from listed URL or something + // TRANS: Client exception thrown when trying to subscribe to group while blocked from that group. + throw new ClientException(_('Blocked by admin.')); + } + + if (Event::handle('StartJoinGroup', array($group, $this->auth_user))) { + $membership = Group_member::join($group->id, $this->auth_user->id); + Event::handle('EndJoinGroup', array($group, $this->auth_user)); + } + + Event::handle('EndAtomPubNewActivity', array($activity, $membership)); + } + + if (!empty($membership)) { + $act = $membership->asActivity(); + + header('Content-Type: application/atom+xml; charset=utf-8'); + header('Content-Location: ' . $act->selfLink); + + $this->startXML(); + $this->raw($act->asString(true, true, true)); + $this->endXML(); + } + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + function lastModified() + { + // For comparison with If-Last-Modified + // If not applicable, return null + return null; + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + function etag() + { + return null; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return false; + } else { + return true; + } + } +} diff --git a/actions/atompubshowfavorite.php b/actions/atompubshowfavorite.php new file mode 100644 index 0000000000..1727e0c3cf --- /dev/null +++ b/actions/atompubshowfavorite.php @@ -0,0 +1,224 @@ +. + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Show a single favorite in Atom Activity Streams format. + * + * Can also be used to delete a favorite. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class AtompubshowfavoriteAction extends ApiAuthAction +{ + private $_profile = null; + private $_notice = null; + private $_fave = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + + $profileId = $this->trimmed('profile'); + $noticeId = $this->trimmed('notice'); + + $this->_profile = Profile::staticGet('id', $profileId); + + if (empty($this->_profile)) { + // TRANS: Client exception. + throw new ClientException(_('No such profile.'), 404); + } + + $this->_notice = Notice::staticGet('id', $noticeId); + + if (empty($this->_notice)) { + // TRANS: Client exception thrown when referencing a non-existing notice. + throw new ClientException(_('No such notice.'), 404); + } + + $this->_fave = Fave::pkeyGet(array('user_id' => $profileId, + 'notice_id' => $noticeId)); + + if (empty($this->_fave)) { + // TRANS: Client exception thrown when referencing a non-existing favorite. + throw new ClientException(_('No such favorite.'), 404); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + parent::handle($argarray); + + switch ($_SERVER['REQUEST_METHOD']) { + case GET: + case HEAD: + $this->showFave(); + break; + case DELETE: + $this->deleteFave(); + break; + default: + // TRANS: Client exception thrown using an unsupported HTTP method. + throw new ClientException(_('HTTP method not supported.'), + 405); + } + return true; + } + + /** + * Show a single favorite, in ActivityStreams format + * + * @return void + */ + function showFave() + { + $activity = $this->_fave->asActivity(); + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $this->startXML(); + $this->raw($activity->asString(true, true, true)); + $this->endXML(); + + return; + } + + /** + * Delete the favorite + * + * @return void + */ + function deleteFave() + { + if (empty($this->auth_user) || + $this->auth_user->id != $this->_profile->id) { + // TRANS: Client exception thrown when trying to remove a favorite notice of another user. + throw new ClientException(_("Cannot delete someone else's". + " favorite."), 403); + } + + $this->_fave->delete(); + + return; + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + function lastModified() + { + return max(strtotime($this->_profile->modified), + strtotime($this->_notice->modified), + strtotime($this->_fave->modified)); + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + function etag() + { + $mtime = strtotime($this->_fave->modified); + + return 'W/"' . implode(':', array('AtomPubShowFavorite', + $this->_profile->id, + $this->_notice->id, + $mtime)) . '"'; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return false; + } else { + return true; + } + } +} diff --git a/actions/atompubshowmembership.php b/actions/atompubshowmembership.php new file mode 100644 index 0000000000..a845ea23df --- /dev/null +++ b/actions/atompubshowmembership.php @@ -0,0 +1,233 @@ +. + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Show (or delete) a single membership event as an ActivityStreams entry + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class AtompubshowmembershipAction extends ApiAuthAction +{ + private $_profile = null; + private $_group = null; + private $_membership = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + + $profileId = $this->trimmed('profile'); + + $this->_profile = Profile::staticGet('id', $profileId); + + if (empty($this->_profile)) { + // TRANS: Client exception. + throw new ClientException(_('No such profile.'), 404); + } + + $groupId = $this->trimmed('group'); + + $this->_group = User_group::staticGet('id', $groupId); + + if (empty($this->_group)) { + // TRANS: Client exception thrown when referencing a non-existing group. + throw new ClientException(_('No such group.'), 404); + } + + $kv = array('group_id' => $groupId, + 'profile_id' => $profileId); + + $this->_membership = Group_member::pkeyGet($kv); + + if (empty($this->_membership)) { + // TRANS: Client exception thrown when trying to show membership of a non-subscribed group + throw new ClientException(_('Not a member.'), 404); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + case 'HEAD': + $this->showMembership(); + break; + case 'DELETE': + $this->deleteMembership(); + break; + default: + // TRANS: Client exception thrown when using an unsupported HTTP method. + throw new ClientException(_('HTTP method not supported.'), 405); + break; + } + return; + } + + /** + * show a single membership + * + * @return void + */ + function showMembership() + { + $activity = $this->_membership->asActivity(); + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $this->startXML(); + $this->raw($activity->asString(true, true, true)); + $this->endXML(); + + return; + } + + /** + * Delete the membership (leave the group) + * + * @return void + */ + + function deleteMembership() + { + if (empty($this->auth_user) || + $this->auth_user->id != $this->_profile->id) { + // TRANS: Client exception thrown when deleting someone else's membership. + throw new ClientException(_("Cannot delete someone else's". + " membership."), 403); + } + + if (Event::handle('StartLeaveGroup', array($this->_group, $this->auth_user))) { + Group_member::leave($this->_group->id, $this->auth_user->id); + Event::handle('EndLeaveGroup', array($this->_group, $this->auth_user)); + } + + return; + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } + + /** + * Return last modified, if applicable. + * + * Because the representation depends on the profile and group, + * our last modified value is the maximum of their mod time + * with the actual membership's mod time. + * + * @return string last modified http header + */ + function lastModified() + { + return max(strtotime($this->_profile->modified), + strtotime($this->_group->modified), + strtotime($this->_membership->modified)); + } + + /** + * Return etag, if applicable. + * + * A "weak" Etag including the profile and group id as well as + * the admin flag and ctime of the membership. + * + * @return string etag http header + */ + function etag() + { + $ctime = strtotime($this->_membership->created); + + $adminflag = ($this->_membership->is_admin) ? 't' : 'f'; + + return 'W/"' . implode(':', array('AtomPubShowMembership', + $this->_profile->id, + $this->_group->id, + $adminflag, + $ctime)) . '"'; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return false; + } else { + return true; + } + } +} diff --git a/actions/atompubshowsubscription.php b/actions/atompubshowsubscription.php new file mode 100644 index 0000000000..aeff0cbf2a --- /dev/null +++ b/actions/atompubshowsubscription.php @@ -0,0 +1,223 @@ +. + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Show a single subscription + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class AtompubshowsubscriptionAction extends ApiAuthAction +{ + private $_subscriber = null; + private $_subscribed = null; + private $_subscription = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + $subscriberId = $this->trimmed('subscriber'); + + $this->_subscriber = Profile::staticGet('id', $subscriberId); + + if (empty($this->_subscriber)) { + // TRANS: Client exception thrown when trying to display a subscription for a non-existing profile ID. + // TRANS: %d is the non-existing profile ID number. + throw new ClientException(sprintf(_('No such profile id: %d.'), + $subscriberId), 404); + } + + $subscribedId = $this->trimmed('subscribed'); + + $this->_subscribed = Profile::staticGet('id', $subscribedId); + + if (empty($this->_subscribed)) { + // TRANS: Client exception thrown when trying to display a subscription for a non-existing profile ID. + // TRANS: %d is the non-existing profile ID number. + throw new ClientException(sprintf(_('No such profile id: %d.'), + $subscribedId), 404); + } + + $this->_subscription = + Subscription::pkeyGet(array('subscriber' => $subscriberId, + 'subscribed' => $subscribedId)); + + if (empty($this->_subscription)) { + // TRANS: Client exception thrown when trying to display a subscription for a non-subscribed profile ID. + // TRANS: %1$d is the non-existing subscriber ID number, $2$d is the ID of the profile that was not subscribed to. + $msg = sprintf(_('Profile %1$d not subscribed to profile %2$d.'), + $subscriberId, $subscribedId); + throw new ClientException($msg, 404); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + parent::handle($argarray); + switch ($_SERVER['REQUEST_METHOD']) { + case 'HEAD': + case 'GET': + $this->showSubscription(); + break; + case 'DELETE': + $this->deleteSubscription(); + break; + default: + // TRANS: Client error shown when using a non-supported HTTP method. + $this->clientError(_('HTTP method not supported.'), 405); + return; + } + + return; + } + + /** + * Show the subscription in ActivityStreams Atom format. + * + * @return void + */ + function showSubscription() + { + $activity = $this->_subscription->asActivity(); + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $this->startXML(); + $this->raw($activity->asString(true, true, true)); + $this->endXML(); + + return; + } + + /** + * Delete the subscription + * + * @return void + */ + function deleteSubscription() + { + if (empty($this->auth_user) || + $this->auth_user->id != $this->_subscriber->id) { + // TRANS: Client exception thrown when trying to delete a subscription of another user. + throw new ClientException(_("Cannot delete someone else's ". + "subscription."), 403); + } + + Subscription::cancel($this->_subscriber, + $this->_subscribed); + + return; + } + + /** + * Is this action read only? + * + * @param array $args other arguments + * + * @return boolean true + */ + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'DELETE') { + return false; + } else { + return true; + } + } + + /** + * Return last modified, if applicable. + * + * @return string last modified http header + */ + function lastModified() + { + return max(strtotime($this->_subscriber->modified), + strtotime($this->_subscribed->modified), + strtotime($this->_subscription->modified)); + } + + /** + * Etag for this object + * + * @return string etag http header + */ + function etag() + { + $mtime = strtotime($this->_subscription->modified); + + return 'W/"' . implode(':', array('AtomPubShowSubscription', + $this->_subscriber->id, + $this->_subscribed->id, + $mtime)) . '"'; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'DELETE') { + return true; + } else { + return false; + } + } +} diff --git a/actions/atompubsubscriptionfeed.php b/actions/atompubsubscriptionfeed.php new file mode 100644 index 0000000000..26740da835 --- /dev/null +++ b/actions/atompubsubscriptionfeed.php @@ -0,0 +1,346 @@ +. + * + * @category Cache + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Subscription feed class for AtomPub + * + * Generates a list of the user's subscriptions + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class AtompubsubscriptionfeedAction extends ApiAuthAction +{ + private $_profile = null; + private $_subscriptions = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + + $subscriber = $this->trimmed('subscriber'); + + $this->_profile = Profile::staticGet('id', $subscriber); + + if (empty($this->_profile)) { + // TRANS: Client exception thrown when trying to display a subscription for a non-existing profile ID. + // TRANS: %d is the non-existing profile ID number. + throw new ClientException(sprintf(_('No such profile id: %d.'), + $subscriber), 404); + } + + // page and count from ApiAction + + $offset = ($this->page-1) * $this->count; + + $this->_subscriptions = Subscription::bySubscriber($subscriber, + $offset, + $this->count + 1); + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + parent::handle($argarray); + switch ($_SERVER['REQUEST_METHOD']) { + case 'HEAD': + case 'GET': + $this->showFeed(); + break; + case 'POST': + $this->addSubscription(); + break; + default: + // TRANS: Client exception thrown when using an unsupported HTTP method. + $this->clientError(_('HTTP method not supported.'), 405); + return; + } + + return; + } + + /** + * Show the feed of subscriptions + * + * @return void + */ + function showFeed() + { + header('Content-Type: application/atom+xml; charset=utf-8'); + + $url = common_local_url('AtomPubSubscriptionFeed', + array('subscriber' => $this->_profile->id)); + + $feed = new Atom10Feed(true); + + $feed->addNamespace('activity', + 'http://activitystrea.ms/spec/1.0/'); + + $feed->addNamespace('poco', + 'http://portablecontacts.net/spec/1.0'); + + $feed->addNamespace('media', + 'http://purl.org/syndication/atommedia'); + + $feed->id = $url; + + $feed->setUpdated('now'); + + $feed->addAuthor($this->_profile->getBestName(), + $this->_profile->getURI()); + + // TRANS: Title for Atom subscription feed. + // TRANS: %s is a user nickname. + $feed->setTitle(sprintf(_("%s subscriptions"), + $this->_profile->getBestName())); + + // TRANS: Subtitle for Atom subscription feed. + // TRANS: %1$s is a user nickname, %s$s is the StatusNet sitename. + $feed->setSubtitle(sprintf(_("People %1\$s has subscribed to on %2\$s"), + $this->_profile->getBestName(), + common_config('site', 'name'))); + + $feed->addLink(common_local_url('subscriptions', + array('nickname' => + $this->_profile->nickname))); + + $feed->addLink($url, + array('rel' => 'self', + 'type' => 'application/atom+xml')); + + // If there's more... + + if ($this->page > 1) { + $feed->addLink($url, + array('rel' => 'first', + 'type' => 'application/atom+xml')); + + $feed->addLink(common_local_url('AtomPubSubscriptionFeed', + array('subscriber' => + $this->_profile->id), + array('page' => + $this->page - 1)), + array('rel' => 'prev', + 'type' => 'application/atom+xml')); + } + + if ($this->_subscriptions->N > $this->count) { + + $feed->addLink(common_local_url('AtomPubSubscriptionFeed', + array('subscriber' => + $this->_profile->id), + array('page' => + $this->page + 1)), + array('rel' => 'next', + 'type' => 'application/atom+xml')); + } + + $i = 0; + + // XXX: This is kind of inefficient + + while ($this->_subscriptions->fetch()) { + + // We get one more than needed; skip that one + + $i++; + + if ($i > $this->count) { + break; + } + + $act = $this->_subscriptions->asActivity(); + $feed->addEntryRaw($act->asString(false, false, false)); + } + + $this->raw($feed->getString()); + } + + /** + * Add a new subscription + * + * Handling the POST method for AtomPub + * + * @return void + */ + function addSubscription() + { + if (empty($this->auth_user) || + $this->auth_user->id != $this->_profile->id) { + // TRANS: Client exception thrown when trying to subscribe another user. + throw new ClientException(_("Cannot add someone else's". + " subscription."), 403); + } + + $xml = file_get_contents('php://input'); + + $dom = DOMDocument::loadXML($xml); + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + // TRANS: Client error displayed when not using an Atom entry. + $this->clientError(_('Atom post must be an Atom entry.')); + return; + } + + $activity = new Activity($dom->documentElement); + + $sub = null; + + if (Event::handle('StartAtomPubNewActivity', array(&$activity))) { + + if ($activity->verb != ActivityVerb::FOLLOW) { + // TRANS: Client error displayed when not using the follow verb. + $this->clientError(_('Can only handle Follow activities.')); + return; + } + + $person = $activity->objects[0]; + + if ($person->type != ActivityObject::PERSON) { + // TRANS: Client exception thrown when subscribing to an object that is not a person. + $this->clientError(_('Can only follow people.')); + return; + } + + // XXX: OStatus discovery (maybe) + + $profile = Profile::fromURI($person->id); + + if (empty($profile)) { + // TRANS: Client exception thrown when subscribing to a non-existing profile. + // TRANS: %s is the unknown profile ID. + $this->clientError(sprintf(_('Unknown profile %s.'), $person->id)); + return; + } + + if (Subscription::exists($this->_profile, $profile)) { + // 409 Conflict + // TRANS: Client error displayed trying to subscribe to an already subscribed profile. + // TRANS: %s is the profile the user already has a subscription on. + $this->clientError(sprintf(_('Already subscribed to %s.'), + $person->id), + 409); + return; + } + + if (Subscription::start($this->_profile, $profile)) { + $sub = Subscription::pkeyGet(array('subscriber' => $this->_profile->id, + 'subscribed' => $profile->id)); + } + + Event::handle('EndAtomPubNewActivity', array($activity, $sub)); + } + + if (!empty($sub)) { + $act = $sub->asActivity(); + + header('Content-Type: application/atom+xml; charset=utf-8'); + header('Content-Location: ' . $act->selfLink); + + $this->startXML(); + $this->raw($act->asString(true, true, true)); + $this->endXML(); + } + } + + /** + * Return true if read only. + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return $_SERVER['REQUEST_METHOD'] != 'POST'; + } + + /** + * Return last modified, if applicable. + * + * @return string last modified http header + */ + function lastModified() + { + return null; + } + + /** + * Return etag, if applicable. + * + * @return string etag http header + */ + function etag() + { + return null; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + return true; + } else { + return false; + } + } +} diff --git a/actions/attachment.php b/actions/attachment.php index 6981354d10..45aa78728a 100644 --- a/actions/attachment.php +++ b/actions/attachment.php @@ -42,7 +42,6 @@ require_once INSTALLDIR.'/lib/attachmentlist.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 AttachmentAction extends Action { /** @@ -70,6 +69,7 @@ class AttachmentAction extends Action } if (empty($this->attachment)) { + // TRANS: Client error displayed trying to get a non-existing attachment. $this->clientError(_('No such attachment.'), 404); return false; } @@ -81,7 +81,6 @@ class AttachmentAction extends Action * * @return boolean true */ - function isReadOnly($args) { return true; @@ -129,7 +128,6 @@ class AttachmentAction extends Action * * @return void */ - function handle($args) { parent::handle($args); @@ -150,7 +148,6 @@ class AttachmentAction extends Action * * @return void */ - function showLocalNavBlock() { } @@ -162,7 +159,6 @@ class AttachmentAction extends Action * * @return void */ - function showContent() { $ali = new Attachment($this->attachment, $this); @@ -174,7 +170,6 @@ class AttachmentAction extends Action * * @return void */ - function showPageNoticeBlock() { } @@ -191,4 +186,3 @@ class AttachmentAction extends Action $atcs->show(); } } - diff --git a/actions/attachment_ajax.php b/actions/attachment_ajax.php index 1e07280750..fb7d15f8a2 100644 --- a/actions/attachment_ajax.php +++ b/actions/attachment_ajax.php @@ -42,7 +42,6 @@ require_once INSTALLDIR.'/actions/attachment.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 Attachment_ajaxAction extends AttachmentAction { /** @@ -80,4 +79,3 @@ class Attachment_ajaxAction extends AttachmentAction $this->elementEnd('div'); } } - diff --git a/actions/attachment_thumbnail.php b/actions/attachment_thumbnail.php index 7d0ac97a69..38648b8bef 100644 --- a/actions/attachment_thumbnail.php +++ b/actions/attachment_thumbnail.php @@ -42,10 +42,8 @@ require_once INSTALLDIR.'/actions/attachment.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 Attachment_thumbnailAction extends AttachmentAction { - function handle($args) { $this->showPage(); @@ -79,6 +77,4 @@ class Attachment_thumbnailAction extends AttachmentAction } $this->element('img', array('src' => $file_thumbnail->url, 'alt' => 'Thumbnail')); } - } - diff --git a/actions/avatarbynickname.php b/actions/avatarbynickname.php index 537950792f..fa97a86ebf 100644 --- a/actions/avatarbynickname.php +++ b/actions/avatarbynickname.php @@ -48,7 +48,7 @@ class AvatarbynicknameAction extends Action * Class handler. * * @param array $args query arguments - * + * * @return boolean false if nickname or user isn't found */ function handle($args) @@ -56,27 +56,32 @@ class AvatarbynicknameAction extends Action parent::handle($args); $nickname = $this->trimmed('nickname'); if (!$nickname) { + // TRANS: Client error displayed trying to get an avatar without providing a nickname. $this->clientError(_('No nickname.')); return; } $size = $this->trimmed('size'); if (!$size) { + // TRANS: Client error displayed trying to get an avatar without providing an avatar size. $this->clientError(_('No size.')); return; } $size = strtolower($size); if (!in_array($size, array('original', '96', '48', '24'))) { + // TRANS: Client error displayed trying to get an avatar providing an invalid avatar size. $this->clientError(_('Invalid size.')); return; } $user = User::staticGet('nickname', $nickname); if (!$user) { + // TRANS: Client error displayed trying to get an avatar for a non-existing user. $this->clientError(_('No such user.')); return; } $profile = $user->getProfile(); if (!$profile) { + // TRANS: Client error displayed trying to get an avatar for a user without a profile. $this->clientError(_('User has no profile.')); return; } @@ -103,4 +108,3 @@ class AvatarbynicknameAction extends Action return true; } } - diff --git a/actions/avatarsettings.php b/actions/avatarsettings.php index 9d4040e75a..c81c024081 100644 --- a/actions/avatarsettings.php +++ b/actions/avatarsettings.php @@ -49,7 +49,6 @@ define('MAX_ORIGINAL', 480); * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class AvatarsettingsAction extends AccountSettingsAction { var $mode = null; @@ -61,9 +60,9 @@ class AvatarsettingsAction extends AccountSettingsAction * * @return string Title of the page */ - function title() { + // TRANS: Title for avatar upload page. return _('Avatar'); } @@ -72,10 +71,12 @@ class AvatarsettingsAction extends AccountSettingsAction * * @return instructions for use */ - function getInstructions() { - return sprintf(_('You can upload your personal avatar. The maximum file size is %s.'), ImageFile::maxFileSize()); + // TRANS: Instruction for avatar upload page. + // TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB". + return sprintf(_('You can upload your personal avatar. The maximum file size is %s.'), + ImageFile::maxFileSize()); } /** @@ -103,6 +104,7 @@ class AvatarsettingsAction extends AccountSettingsAction 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.')); return; } @@ -116,14 +118,16 @@ class AvatarsettingsAction extends AccountSettingsAction 'action' => common_local_url('avatarsettings'))); $this->elementStart('fieldset'); + // TRANS: Avatar upload page form legend. $this->element('legend', null, _('Avatar settings')); $this->hidden('token', common_session_token()); - + if (Event::handle('StartAvatarFormData', array($this))) { $this->elementStart('ul', 'form_data'); if ($original) { $this->elementStart('li', array('id' => 'avatar_original', 'class' => 'avatar_view')); + // TRANS: Header on avatar upload page for thumbnail of originally uploaded avatar (h2). $this->element('h2', null, _("Original")); $this->elementStart('div', array('id'=>'avatar_original_view')); $this->element('img', array('src' => $original->url, @@ -139,6 +143,7 @@ class AvatarsettingsAction extends AccountSettingsAction if ($avatar) { $this->elementStart('li', array('id' => 'avatar_preview', 'class' => 'avatar_view')); + // 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, @@ -146,24 +151,26 @@ class AvatarsettingsAction extends AccountSettingsAction 'height' => AVATAR_PROFILE_SIZE, 'alt' => $user->nickname)); $this->elementEnd('div'); - $this->submit('delete', _('Delete')); + // TRANS: Button on avatar upload page to delete current avatar. + $this->submit('delete', _m('BUTTON','Delete')); $this->elementEnd('li'); } $this->elementStart('li', array ('id' => 'settings_attach')); - $this->element('input', array('name' => 'avatarfile', - 'type' => 'file', - 'id' => 'avatarfile')); $this->element('input', array('name' => 'MAX_FILE_SIZE', 'type' => 'hidden', 'id' => 'MAX_FILE_SIZE', 'value' => ImageFile::maxFileSizeInt())); + $this->element('input', array('name' => 'avatarfile', + 'type' => 'file', + 'id' => 'avatarfile')); $this->elementEnd('li'); $this->elementEnd('ul'); $this->elementStart('ul', 'form_actions'); $this->elementStart('li'); - $this->submit('upload', _('Upload')); + // TRANS: Button on avatar upload page to upload an avatar. + $this->submit('upload', _m('BUTTON','Upload')); $this->elementEnd('li'); $this->elementEnd('ul'); } @@ -171,7 +178,6 @@ class AvatarsettingsAction extends AccountSettingsAction $this->elementEnd('fieldset'); $this->elementEnd('form'); - } function showCropForm() @@ -182,6 +188,7 @@ class AvatarsettingsAction extends AccountSettingsAction 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.')); return; } @@ -194,6 +201,7 @@ class AvatarsettingsAction extends AccountSettingsAction 'action' => common_local_url('avatarsettings'))); $this->elementStart('fieldset'); + // TRANS: Avatar upload page crop form legend. $this->element('legend', null, _('Avatar settings')); $this->hidden('token', common_session_token()); @@ -202,6 +210,7 @@ class AvatarsettingsAction extends AccountSettingsAction $this->elementStart('li', array('id' => 'avatar_original', 'class' => 'avatar_view')); + // TRANS: Header on avatar upload crop form for thumbnail of originally uploaded avatar (h2). $this->element('h2', null, _("Original")); $this->elementStart('div', array('id'=>'avatar_original_view')); $this->element('img', array('src' => Avatar::url($this->filedata['filename']), @@ -214,6 +223,7 @@ class AvatarsettingsAction extends AccountSettingsAction $this->elementStart('li', array('id' => 'avatar_preview', 'class' => 'avatar_view')); + // TRANS: Header on avatar upload crop form 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' => Avatar::url($this->filedata['filename']), @@ -228,13 +238,14 @@ class AvatarsettingsAction extends AccountSettingsAction 'type' => 'hidden', 'id' => $crop_info)); } - $this->submit('crop', _('Crop')); + + // TRANS: Button on avatar upload crop form to confirm a selected crop as avatar. + $this->submit('crop', _m('BUTTON','Crop')); $this->elementEnd('li'); $this->elementEnd('ul'); $this->elementEnd('fieldset'); $this->elementEnd('form'); - } /** @@ -244,7 +255,6 @@ class AvatarsettingsAction extends AccountSettingsAction * * @return void */ - function handlePost() { // Workaround for PHP returning empty $_POST and $_FILES when POST @@ -271,7 +281,7 @@ class AvatarsettingsAction extends AccountSettingsAction 'Try again, please.')); return; } - + if (Event::handle('StartAvatarSaveForm', array($this))) { if ($this->arg('upload')) { $this->uploadAvatar(); @@ -280,6 +290,7 @@ class AvatarsettingsAction extends AccountSettingsAction } else if ($this->arg('delete')) { $this->deleteAvatar(); } else { + // TRANS: Unexpected validation error on avatar upload form. $this->showForm(_('Unexpected form submission.')); } Event::handle('EndAvatarSaveForm', array($this)); @@ -294,7 +305,6 @@ class AvatarsettingsAction extends AccountSettingsAction * * @return void */ - function uploadAvatar() { try { @@ -304,26 +314,26 @@ class AvatarsettingsAction extends AccountSettingsAction return; } if ($imagefile === null) { + // TRANS: Validation error on avatar upload form when no file was uploaded. $this->showForm(_('No file uploaded.')); return; } $cur = common_current_user(); - + $type = $imagefile->preferredType(); $filename = Avatar::filename($cur->id, - image_type_to_extension($imagefile->type), + image_type_to_extension($type), null, 'tmp'.common_timestamp()); $filepath = Avatar::path($filename); - - move_uploaded_file($imagefile->filepath, $filepath); + $imagefile->copyTo($filepath); $filedata = array('filename' => $filename, 'filepath' => $filepath, 'width' => $imagefile->width, 'height' => $imagefile->height, - 'type' => $imagefile->type); + 'type' => $type); $_SESSION['FILEDATA'] = $filedata; @@ -331,6 +341,7 @@ class AvatarsettingsAction extends AccountSettingsAction $this->mode = 'crop'; + // TRANS: Avatar upload form unstruction after uploading a file. $this->showForm(_('Pick a square area of the image to be your avatar'), true); } @@ -340,12 +351,12 @@ class AvatarsettingsAction extends AccountSettingsAction * * @return void */ - function cropAvatar() { $filedata = $_SESSION['FILEDATA']; if (!$filedata) { + // TRANS: Server error displayed if an avatar upload went wrong somehow server side. $this->serverError(_('Lost our file data.')); return; } @@ -369,24 +380,25 @@ class AvatarsettingsAction extends AccountSettingsAction @unlink($filedata['filepath']); unset($_SESSION['FILEDATA']); $this->mode = 'upload'; + // TRANS: Success message for having updated a user avatar. $this->showForm(_('Avatar updated.'), true); common_broadcast_profile($profile); } else { + // TRANS: Error displayed on the avatar upload page if the avatar could not be updated for an unknown reason. $this->showForm(_('Failed updating avatar.')); } } - + /** * Get rid of the current avatar. * * @return void */ - function deleteAvatar() { $user = common_current_user(); $profile = $user->getProfile(); - + $avatar = $profile->getOriginalAvatar(); if($avatar) $avatar->delete(); $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); @@ -396,6 +408,7 @@ class AvatarsettingsAction extends AccountSettingsAction $avatar = $profile->getAvatar(AVATAR_MINI_SIZE); if($avatar) $avatar->delete(); + // TRANS: Success message for deleting a user avatar. $this->showForm(_('Avatar deleted.'), true); } @@ -416,7 +429,6 @@ class AvatarsettingsAction extends AccountSettingsAction * * @return void */ - function showScripts() { parent::showScripts(); diff --git a/actions/backupaccount.php b/actions/backupaccount.php new file mode 100644 index 0000000000..7794826760 --- /dev/null +++ b/actions/backupaccount.php @@ -0,0 +1,254 @@ +. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Download a backup of your own account to the browser + * + * We go through some hoops to make this only respond to POST, since + * it's kind of expensive and there's probably some downside to having + * your account in all kinds of search engines. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class BackupaccountAction extends Action +{ + /** + * Returns the title of the page + * + * @return string page title + */ + function title() + { + // TRANS: Title for backup account page. + return _("Backup account"); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + + $cur = common_current_user(); + + if (empty($cur)) { + // TRANS: Client exception thrown when trying to backup an account while not logged in. + throw new ClientException(_('Only logged-in users can backup their account.'), 403); + } + + if (!$cur->hasRight(Right::BACKUPACCOUNT)) { + // TRANS: Client exception thrown when trying to backup an account without having backup rights. + throw new ClientException(_('You may not backup your account.'), 403); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + parent::handle($argarray); + + if ($this->isPost()) { + $this->sendFeed(); + } else { + $this->showPage(); + } + return; + } + + /** + * Send a feed of the user's activities to the browser + * + * Uses the UserActivityStream class; may take a long time! + * + * @return void + */ + + function sendFeed() + { + $cur = common_current_user(); + + $stream = new UserActivityStream($cur); + + header('Content-Disposition: attachment; filename='.$cur->nickname.'.atom'); + header('Content-Type: application/atom+xml; charset=utf-8'); + + $this->raw($stream->getString()); + } + + /** + * Show a little form so that the person can request a backup. + * + * @return void + */ + + function showContent() + { + $form = new BackupAccountForm($this); + $form->show(); + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return false; + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + function lastModified() + { + // For comparison with If-Last-Modified + // If not applicable, return null + return null; + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + function etag() + { + return null; + } +} + +/** + * A form for backing up the account. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class BackupAccountForm extends Form +{ + /** + * Class of the form. + * + * @return string the form's class + */ + function formClass() + { + return 'form_profile_backup'; + } + + /** + * URL the form posts to + * + * @return string the form's action URL + */ + function action() + { + return common_local_url('backupaccount'); + } + + /** + * Output form data + * + * Really, just instructions for doing a backup. + * + * @return void + */ + function formData() + { + $msg = + // TRANS: Information displayed on the backup account page. + _('You can backup your account data in '. + 'Activity Streams '. + 'format. This is an experimental feature and provides an '. + 'incomplete backup; private account '. + 'information like email and IM addresses is not backed up. '. + 'Additionally, uploaded files and direct messages are not '. + 'backed up.'); + $this->out->elementStart('p'); + $this->out->raw($msg); + $this->out->elementEnd('p'); + } + + /** + * Buttons for the form + * + * In this case, a single submit button + * + * @return void + */ + function formActions() + { + $this->out->submit('submit', + // TRANS: Submit button to backup an account on the backup account page. + _m('BUTTON', 'Backup'), + 'submit', + null, + // TRANS: Title for submit button to backup an account on the backup account page. + _('Backup your account')); + } +} diff --git a/actions/block.php b/actions/block.php index 93f8ec9370..e87353b4e1 100644 --- a/actions/block.php +++ b/actions/block.php @@ -42,7 +42,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class BlockAction extends ProfileFormAction { var $profile = null; @@ -54,7 +53,6 @@ class BlockAction extends ProfileFormAction * * @return boolean success flag */ - function prepare($args) { if (!parent::prepare($args)) { @@ -66,6 +64,7 @@ class BlockAction extends ProfileFormAction assert(!empty($cur)); // checked by parent if ($cur->hasBlocked($this->profile)) { + // TRANS: Client error displayed when blocking a user that has already been blocked. $this->clientError(_('You already blocked that user.')); return false; } @@ -82,7 +81,6 @@ class BlockAction extends ProfileFormAction * * @return void */ - function handle($args) { if ($_SERVER['REQUEST_METHOD'] == 'POST') { @@ -104,6 +102,7 @@ class BlockAction extends ProfileFormAction } function title() { + // TRANS: Title for block user page. return _('Block user'); } @@ -133,8 +132,10 @@ class BlockAction extends ProfileFormAction 'action' => common_local_url('block'))); $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); + // TRANS: Legend for block user form. $this->element('legend', _('Block user')); $this->element('p', null, + // TRANS: Explanation of consequences when blocking a user on the block user page. _('Are you sure you want to block this user? '. 'Afterwards, they will be unsubscribed from you, '. 'unable to subscribe to you in the future, and '. @@ -184,6 +185,7 @@ class BlockAction extends ProfileFormAction } if (!$result) { + // TRANS: Server error displayed when blocking a user fails. $this->serverError(_('Failed to save block information.')); return; } @@ -199,7 +201,7 @@ class BlockAction extends ProfileFormAction * Override for form session token checks; on our first hit we're just * requesting confirmation, which doesn't need a token. We need to be * able to take regular GET requests from email! - * + * * @throws ClientException if token is bad on POST request or if we have * confirmation parameters which could trigger something. */ @@ -216,7 +218,7 @@ class BlockAction extends ProfileFormAction /** * If we reached this form without returnto arguments, return to the * current user's subscription list. - * + * * @return string URL */ function defaultReturnTo() diff --git a/actions/blockedfromgroup.php b/actions/blockedfromgroup.php index a0598db270..6ff572c05d 100644 --- a/actions/blockedfromgroup.php +++ b/actions/blockedfromgroup.php @@ -40,7 +40,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 BlockedfromgroupAction extends GroupDesignAction { var $page = null; @@ -70,6 +69,7 @@ class BlockedfromgroupAction extends GroupDesignAction } if (!$nickname) { + // TRANS: Client error displayed when requesting a list of blocked users for a group without providing a group nickname. $this->clientError(_('No nickname.'), 404); return false; } @@ -77,6 +77,7 @@ class BlockedfromgroupAction extends GroupDesignAction $local = Local_group::staticGet('nickname', $nickname); if (!$local) { + // TRANS: Client error displayed when requesting a list of blocked users for a non-local group. $this->clientError(_('No such group.'), 404); return false; } @@ -84,6 +85,7 @@ class BlockedfromgroupAction extends GroupDesignAction $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { + // TRANS: Client error displayed when requesting a list of blocked users for a non-existing group. $this->clientError(_('No such group.'), 404); return false; } @@ -94,9 +96,13 @@ class BlockedfromgroupAction extends GroupDesignAction function title() { if ($this->page == 1) { + // TRANS: Title for first page with list of users blocked from a group. + // TRANS: %s is a group nickname. return sprintf(_('%s blocked profiles'), $this->group->nickname); } else { + // TRANS: Title for any but the first page with list of users blocked from a group. + // TRANS: %1$s is a group nickname, %2$d is a page number. return sprintf(_('%1$s blocked profiles, page %2$d'), $this->group->nickname, $this->page); @@ -112,6 +118,7 @@ class BlockedfromgroupAction extends GroupDesignAction function showPageNotice() { $this->element('p', 'instructions', + // TRANS: Instructions for list of users blocked from a group. _('A list of the users blocked from joining this group.')); } @@ -205,7 +212,6 @@ class GroupBlockListItem extends ProfileListItem * * @see UnblockForm */ - class GroupUnblockForm extends Form { /** @@ -234,7 +240,6 @@ class GroupUnblockForm extends Form * @param User_group $group group to block user from * @param array $args return-to args */ - function __construct($out=null, $profile=null, $group=null, $args=null) { parent::__construct($out); @@ -249,7 +254,6 @@ class GroupUnblockForm extends Form * * @return int ID of the form */ - function id() { // This should be unique for the page. @@ -261,7 +265,6 @@ class GroupUnblockForm extends Form * * @return string class of the form */ - function formClass() { return 'form_group_unblock'; @@ -272,7 +275,6 @@ class GroupUnblockForm extends Form * * @return string URL of the action */ - function action() { return common_local_url('groupunblock'); @@ -285,6 +287,7 @@ class GroupUnblockForm extends Form */ function formLegend() { + // TRANS: Form legend for unblocking a user from a group. $this->out->element('legend', null, _('Unblock user from group')); } @@ -293,7 +296,6 @@ class GroupUnblockForm extends Form * * @return void */ - function formData() { $this->out->hidden('unblockto-' . $this->profile->id, @@ -314,9 +316,14 @@ class GroupUnblockForm extends Form * * @return void */ - function formActions() { - $this->out->submit('submit', _('Unblock'), 'submit', null, _('Unblock this user')); + $this->out->submit('submit', + // TRANS: Button text for unblocking a user from a group. + _m('BUTTON','Unblock'), + 'submit', + null, + // TRANS: Tooltip for button for unblocking a user from a group. + _('Unblock this user')); } } diff --git a/actions/bookmarklet.php b/actions/bookmarklet.php index 041c2e8947..9cf4e58f89 100644 --- a/actions/bookmarklet.php +++ b/actions/bookmarklet.php @@ -34,7 +34,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { require_once INSTALLDIR . '/actions/newnotice.php'; /** - * Action for posting a notice + * Action for posting a notice * * @category Bookmarklet * @package StatusNet @@ -42,12 +42,12 @@ require_once INSTALLDIR . '/actions/newnotice.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 BookmarkletAction extends NewnoticeAction { function showTitle() { // TRANS: Title for mini-posting window loaded from bookmarklet. + // TRANS: %s is the StatusNet site name. $this->element('title', null, sprintf(_('Post to %s'), common_config('site', 'name'))); } @@ -73,4 +73,3 @@ class BookmarkletAction extends NewnoticeAction { } } - diff --git a/actions/confirmaddress.php b/actions/confirmaddress.php index 8bf8c8c4d4..238e70551c 100644 --- a/actions/confirmaddress.php +++ b/actions/confirmaddress.php @@ -44,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 ConfirmaddressAction extends Action { /** type of confirmation. */ @@ -61,7 +60,6 @@ class ConfirmaddressAction extends Action * * @return void */ - function handle($args) { parent::handle($args); @@ -72,27 +70,30 @@ class ConfirmaddressAction extends Action } $code = $this->trimmed('code'); if (!$code) { + // TRANS: Client error displayed when not providing a confirmation code in the contact address confirmation action. $this->clientError(_('No confirmation code.')); return; } $confirm = Confirm_address::staticGet('code', $code); if (!$confirm) { + // TRANS: Client error displayed when providing a non-existing confirmation code in the contact address confirmation action. $this->clientError(_('Confirmation code not found.')); return; } $cur = common_current_user(); if ($cur->id != $confirm->user_id) { + // TRANS: Client error displayed when not providing a confirmation code for another user in the contact address confirmation action. $this->clientError(_('That confirmation code is not for you!')); return; } $type = $confirm->address_type; if (!in_array($type, array('email', 'jabber', 'sms'))) { - // TRANS: Server error for an unknow address type, which can be 'email', 'jabber', or 'sms'. + // TRANS: Server error for a unknow address type %s, which can be 'email', 'jabber', or 'sms'. $this->serverError(sprintf(_('Unrecognized address type %s.'), $type)); return; } if ($cur->$type == $confirm->address) { - // TRANS: Client error for an already confirmed email/jabbel/sms address. + // TRANS: Client error for an already confirmed email/jabber/sms address. $this->clientError(_('That address has already been confirmed.')); return; } @@ -113,7 +114,8 @@ class ConfirmaddressAction extends Action if (!$result) { common_log_db_error($cur, 'UPDATE', __FILE__); - $this->serverError(_('Couldn\'t update user.')); + // TRANS: Server error displayed when a user update to the database fails in the contact address confirmation action. + $this->serverError(_('Could not update user.')); return; } @@ -125,7 +127,9 @@ class ConfirmaddressAction extends Action if (!$result) { common_log_db_error($confirm, 'DELETE', __FILE__); - $this->serverError(_('Couldn\'t delete email confirmation.')); + // TRANS: Server error displayed when an address confirmation code deletion from the + // TRANS: database fails in the contact address confirmation action. + $this->serverError(_('Could not delete address confirmation.')); return; } @@ -140,9 +144,9 @@ class ConfirmaddressAction extends Action * * @return string title */ - function title() { + // TRANS: Title for the contact address confirmation action. return _('Confirm address'); } @@ -151,13 +155,14 @@ class ConfirmaddressAction extends Action * * @return void */ - function showContent() { $cur = common_current_user(); $type = $this->type; $this->element('p', null, + // TRANS: Success message for the contact address confirmation action. + // TRANS: %s can be 'email', 'jabber', or 'sms'. sprintf(_('The address "%s" has been '. 'confirmed for your account.'), $cur->$type)); diff --git a/actions/conversation.php b/actions/conversation.php index 900a724ef3..8d11df37bc 100644 --- a/actions/conversation.php +++ b/actions/conversation.php @@ -45,7 +45,6 @@ require_once INSTALLDIR.'/lib/noticelist.php'; * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class ConversationAction extends Action { var $id = null; @@ -58,7 +57,6 @@ class ConversationAction extends Action * * @return boolean false if id not passed in */ - function prepare($args) { parent::prepare($args); @@ -81,7 +79,6 @@ class ConversationAction extends Action * * @return void */ - function handle($args) { parent::handle($args); @@ -93,10 +90,10 @@ class ConversationAction extends Action * * @return string page title */ - function title() { - return _("Conversation"); + // TRANS: Title for page with a conversion (multiple notices in context). + return _('Conversation'); } /** @@ -107,7 +104,6 @@ class ConversationAction extends Action * * @return void */ - function showContent() { $notices = Notice::conversationStream($this->id, null, null); @@ -134,7 +130,6 @@ class ConversationAction extends Action * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class ConversationTree extends NoticeList { var $tree = null; @@ -145,12 +140,12 @@ class ConversationTree extends NoticeList * * @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')); @@ -200,7 +195,6 @@ class ConversationTree extends NoticeList * * @return void */ - function showNoticePlus($id) { $notice = $this->table[$id]; @@ -237,7 +231,6 @@ class ConversationTree extends NoticeList * * @return NoticeListItem a list item to show */ - function newListItem($notice) { return new ConversationTreeItem($notice, $this->out); @@ -255,7 +248,6 @@ class ConversationTree extends NoticeList * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class ConversationTreeItem extends NoticeListItem { /** @@ -266,7 +258,6 @@ class ConversationTreeItem extends NoticeListItem * * @return void */ - function showStart() { return; @@ -280,7 +271,6 @@ class ConversationTreeItem extends NoticeListItem * * @return void */ - function showEnd() { return; @@ -293,7 +283,6 @@ class ConversationTreeItem extends NoticeListItem * * @return void */ - function showContext() { return; diff --git a/actions/deleteaccount.php b/actions/deleteaccount.php new file mode 100644 index 0000000000..614519d474 --- /dev/null +++ b/actions/deleteaccount.php @@ -0,0 +1,325 @@ +. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Action to delete your own account + * + * Note that this is distinct from DeleteuserAction, which see. I thought + * that making that action do both things (delete another user and delete the + * current user) would open a lot of holes. I'm open to refactoring, however. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class DeleteaccountAction extends Action +{ + private $_complete = false; + private $_error = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + + $cur = common_current_user(); + + if (empty($cur)) { + // TRANS: Client exception displayed trying to delete a user account while not logged in. + throw new ClientException(_("Only logged-in users ". + "can delete their account."), 403); + } + + if (!$cur->hasRight(Right::DELETEACCOUNT)) { + // TRANS: Client exception displayed trying to delete a user account without have the rights to do that. + throw new ClientException(_("You cannot delete your account."), 403); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + parent::handle($argarray); + + if ($this->isPost()) { + $this->deleteAccount(); + } else { + $this->showPage(); + } + return; + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return false; + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + function lastModified() + { + // For comparison with If-Last-Modified + // If not applicable, return null + return null; + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + function etag() + { + return null; + } + + /** + * Delete the current user's account + * + * Checks for the "I am sure." string to make sure the user really + * wants to delete their account. + * + * Then, marks the account as deleted and begins the deletion process + * (actually done by a back-end handler). + * + * If successful it logs the user out, and shows a brief completion message. + * + * @return void + */ + function deleteAccount() + { + $this->checkSessionToken(); + // !!! If this string is changed, it also needs to be changed in DeleteAccountForm::formData() + // TRANS: Confirmation text for user deletion. The user has to type this exactly the same, including punctuation. + $iamsure = _('I am sure.'); + if ($this->trimmed('iamsure') != $iamsure ) { + // TRANS: Notification for user about the text that must be input to be able to delete a user account. + // TRANS: %s is the text that needs to be input. + $this->_error = sprintf(_('You must write "%s" exactly in the box.'), $iamsure); + $this->showPage(); + return; + } + + $cur = common_current_user(); + + // Mark the account as deleted and shove low-level deletion tasks + // to background queues. Removing a lot of posts can take a while... + + if (!$cur->hasRole(Profile_role::DELETED)) { + $cur->grantRole(Profile_role::DELETED); + } + + $qm = QueueManager::get(); + $qm->enqueue($cur, 'deluser'); + + // The user is really-truly logged out + + common_set_user(null); + common_real_login(false); // not logged in + common_forgetme(); // don't log back in! + + $this->_complete = true; + $this->showPage(); + } + + /** + * Shows the page content. + * + * If the deletion is complete, just shows a completion message. + * + * Otherwise, shows the deletion form. + * + * @return void + * + */ + function showContent() + { + if ($this->_complete) { + $this->element('p', 'confirmation', + // TRANS: Confirmation that a user account has been deleted. + _('Account deleted.')); + return; + } + + if (!empty($this->_error)) { + $this->element('p', 'error', $this->_error); + $this->_error = null; + } + + $form = new DeleteAccountForm($this); + $form->show(); + } + + /** + * Show the title of the page + * + * @return string title + */ + + function title() + { + // TRANS: Page title for page on which a user account can be deleted. + return _('Delete account'); + } +} + +/** + * Form for deleting your account + * + * Note that this mostly is here to keep you from accidentally deleting your + * account. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class DeleteAccountForm extends Form +{ + /** + * Class of the form. + * + * @return string the form's class + */ + function formClass() + { + return 'form_profile_delete'; + } + + /** + * URL the form posts to + * + * @return string the form's action URL + */ + function action() + { + return common_local_url('deleteaccount'); + } + + /** + * Output form data + * + * Instructions plus an 'i am sure' entry box. + * + * @return void + */ + function formData() + { + $cur = common_current_user(); + + // TRANS: Form text for user deletion form. + $msg = '

    ' . _('This will permanently delete '. + 'your account data from this server.') . '

    '; + + if ($cur->hasRight(Right::BACKUPACCOUNT)) { + // TRANS: Additional form text for user deletion form shown if a user has account backup rights. + // TRANS: %s is a URL to the backup page. + $msg .= '

    ' . sprintf(_('You are strongly advised to '. + 'back up your data'. + ' before deletion.'), + common_local_url('backupaccount')) . '

    '; + } + + $this->out->elementStart('p'); + $this->out->raw($msg); + $this->out->elementEnd('p'); + + // !!! If this string is changed, it also needs to be changed in class DeleteaccountAction. + // TRANS: Confirmation text for user deletion. The user has to type this exactly the same, including punctuation. + $iamsure = _("I am sure."); + $this->out->input('iamsure', + // TRANS: Field label for delete account confirmation entry. + _('Confirm'), + null, + // TRANS: Input title for the delete account field. + // TRANS: %s is the text that needs to be input. + sprintf(_('Enter "%s" to confirm that '. + 'you want to delete your account.'),$iamsure )); + } + + /** + * Buttons for the form + * + * In this case, a single submit button + * + * @return void + */ + function formActions() + { + $this->out->submit('submit', + // TRANS: Button text for user account deletion. + _m('BUTTON', 'Delete'), + 'submit', + null, + // TRANS: Button title for user account deletion. + _('Permanently delete your account')); + } +} diff --git a/actions/deleteapplication.php b/actions/deleteapplication.php index 806de0be6e..272a91762c 100644 --- a/actions/deleteapplication.php +++ b/actions/deleteapplication.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class DeleteapplicationAction extends Action { var $app = null; @@ -52,7 +51,6 @@ class DeleteapplicationAction extends Action * * @return boolean success flag */ - function prepare($args) { if (!parent::prepare($args)) { @@ -60,6 +58,7 @@ class DeleteapplicationAction extends Action } if (!common_logged_in()) { + // TRANS: Client error displayed trying to delete an application while not logged in. $this->clientError(_('You must be logged in to delete an application.')); return false; } @@ -68,6 +67,7 @@ class DeleteapplicationAction extends Action $this->app = Oauth_application::staticGet('id', $id); if (empty($this->app)) { + // TRANS: Client error displayed trying to delete an application that does not exist. $this->clientError(_('Application not found.')); return false; } @@ -75,6 +75,7 @@ class DeleteapplicationAction extends Action $cur = common_current_user(); if ($cur->id != $this->app->owner) { + // TRANS: Client error displayed trying to delete an application the current user does not own. $this->clientError(_('You are not the owner of this application.'), 401); return false; } @@ -91,7 +92,6 @@ class DeleteapplicationAction extends Action * * @return void */ - function handle($args) { if ($_SERVER['REQUEST_METHOD'] == 'POST') { @@ -120,6 +120,7 @@ class DeleteapplicationAction extends Action } function title() { + // TRANS: Title for delete application page. return _('Delete application'); } @@ -144,8 +145,10 @@ class DeleteapplicationAction extends Action array('id' => $this->app->id)))); $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); + // TRANS: Fieldset legend on delete application page. $this->element('legend', _('Delete application')); $this->element('p', null, + // TRANS: Confirmation text on delete application page. _('Are you sure you want to delete this application? '. 'This will clear all data about the application from the '. 'database, including all existing user connections.')); @@ -171,10 +174,8 @@ class DeleteapplicationAction extends Action * * @return void */ - function handlePost() { $this->app->delete(); } } - diff --git a/actions/deletegroup.php b/actions/deletegroup.php index 62fff00c48..4e9b9851f1 100644 --- a/actions/deletegroup.php +++ b/actions/deletegroup.php @@ -172,7 +172,7 @@ class DeletegroupAction extends RedirectingAction } function title() { - // TRANS: Title. + // TRANS: Title of delete group page. return _('Delete group'); } @@ -201,8 +201,8 @@ class DeletegroupAction extends RedirectingAction // TRANS: Form legend for deleting a group. $this->element('legend', _('Delete group')); if (Event::handle('StartDeleteGroupForm', array($this, $this->group))) { - // TRANS: Warning in form for deleleting a group. $this->element('p', null, + // TRANS: Warning in form for deleleting a group. _('Are you sure you want to delete this group? '. 'This will clear all data about the group from the '. 'database, without a backup. ' . diff --git a/actions/deletenotice.php b/actions/deletenotice.php index 2879faa5df..ff57bbd61e 100644 --- a/actions/deletenotice.php +++ b/actions/deletenotice.php @@ -32,6 +32,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +// @todo FIXME: documentation needed. class DeletenoticeAction extends Action { var $error = null; @@ -47,6 +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. common_user_error(_('Not logged in.')); exit; } @@ -55,6 +57,7 @@ class DeletenoticeAction extends Action $this->notice = Notice::staticGet($notice_id); if (!$this->notice) { + // TRANS: Error message displayed trying to delete a non-existing notice. common_user_error(_('No such notice.')); exit; } @@ -71,7 +74,8 @@ class DeletenoticeAction extends Action if ($this->notice->profile_id != $this->user_profile->id && !$this->user->hasRight(Right::DELETEOTHERSNOTICE)) { - common_user_error(_('Can\'t delete this notice.')); + // TRANS: Error message displayed trying to delete a notice that was not made by the current user. + common_user_error(_('Cannot delete this notice.')); exit; } // XXX: Ajax! @@ -90,7 +94,6 @@ class DeletenoticeAction extends Action * * @return void */ - function showPageNotice() { $instr = $this->getInstructions(); @@ -103,12 +106,14 @@ class DeletenoticeAction extends Action function getInstructions() { + // TRANS: Instructions for deleting a notice. return _('You are about to permanently delete a notice. ' . 'Once this is done, it cannot be undone.'); } function title() { + // TRANS: Page title when deleting a notice. return _('Delete notice'); } @@ -121,7 +126,6 @@ class DeletenoticeAction extends Action * * @return void */ - function showForm($error = null) { $this->error = $error; @@ -133,7 +137,6 @@ class DeletenoticeAction extends Action * * @return void */ - function showContent() { $this->elementStart('form', array('id' => 'form_notice_delete', @@ -141,9 +144,11 @@ class DeletenoticeAction extends Action 'method' => 'post', 'action' => common_local_url('deletenotice'))); $this->elementStart('fieldset'); + // TRANS: Fieldset legend for the delete notice form. $this->element('legend', null, _('Delete notice')); $this->hidden('token', common_session_token()); $this->hidden('notice', $this->trimmed('notice')); + // TRANS: Message for the delete notice form. $this->element('p', null, _('Are you sure you want to delete this notice?')); $this->submit('form_action-no', // TRANS: Button label on the delete notice form. diff --git a/actions/deleteuser.php b/actions/deleteuser.php index 02ded68b31..ac96b7b5d5 100644 --- a/actions/deleteuser.php +++ b/actions/deleteuser.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class DeleteuserAction extends ProfileFormAction { var $user = null; @@ -52,7 +51,6 @@ class DeleteuserAction extends ProfileFormAction * * @return boolean success flag */ - function prepare($args) { if (!parent::prepare($args)) { @@ -64,6 +62,7 @@ class DeleteuserAction extends ProfileFormAction assert(!empty($cur)); // checked by parent if (!$cur->hasRight(Right::DELETEUSER)) { + // TRANS: Client error displayed when trying to delete a user without having the right to delete users. $this->clientError(_('You cannot delete users.')); return false; } @@ -71,6 +70,7 @@ class DeleteuserAction extends ProfileFormAction $this->user = User::staticGet('id', $this->profile->id); if (empty($this->user)) { + // TRANS: Client error displayed when trying to delete a non-local user. $this->clientError(_('You can only delete local users.')); return false; } @@ -87,7 +87,6 @@ class DeleteuserAction extends ProfileFormAction * * @return void */ - function handle($args) { if ($_SERVER['REQUEST_METHOD'] == 'POST') { @@ -107,7 +106,8 @@ class DeleteuserAction extends ProfileFormAction } function title() { - return _('Delete user'); + // TRANS: Title of delete user page. + return _m('TITLE','Delete user'); } function showNoticeForm() { @@ -130,9 +130,11 @@ class DeleteuserAction extends ProfileFormAction 'action' => common_local_url('deleteuser'))); $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); + // TRANS: Fieldset legend on delete user page. $this->element('legend', _('Delete user')); if (Event::handle('StartDeleteUserForm', array($this, $this->user))) { $this->element('p', null, + // TRANS: Information text to request if a user is certain that the described action has to be performed. _('Are you sure you want to delete this user? '. 'This will clear all data about the user from the '. 'database, without a backup.')); @@ -153,7 +155,7 @@ class DeleteuserAction extends ProfileFormAction 'submit form_action-primary', 'no', // TRANS: Submit button title for 'No' when deleting a user. - _('Do not block this user')); + _('Do not delete this user')); $this->submit('form_action-yes', // TRANS: Button label on the delete user form. _m('BUTTON','Yes'), @@ -170,7 +172,6 @@ class DeleteuserAction extends ProfileFormAction * * @return void */ - function handlePost() { if (Event::handle('StartDeleteUser', array($this, $this->user))) { diff --git a/actions/designadminpanel.php b/actions/designadminpanel.php index 321a8ee5eb..34e6de851a 100644 --- a/actions/designadminpanel.php +++ b/actions/designadminpanel.php @@ -44,10 +44,8 @@ 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 DesignadminpanelAction extends AdminPanelAction { - /* The default site design */ var $design = null; @@ -56,7 +54,6 @@ class DesignadminpanelAction extends AdminPanelAction * * @return string page title */ - function title() { // TRANS: Message used as title for design settings for the site. @@ -68,9 +65,9 @@ class DesignadminpanelAction extends AdminPanelAction * * @return string instructions */ - function getInstructions() { + // TRANS: Instructions for design adminsitration panel. return _('Design settings for this StatusNet site'); } @@ -79,7 +76,6 @@ class DesignadminpanelAction extends AdminPanelAction * * @return void */ - function showForm() { $this->design = Design::siteDesign(); @@ -93,7 +89,6 @@ class DesignadminpanelAction extends AdminPanelAction * * @return void */ - function saveSettings() { if ($this->arg('save')) { @@ -101,6 +96,7 @@ class DesignadminpanelAction extends AdminPanelAction } else if ($this->arg('defaults')) { $this->restoreDefaults(); } else { + // TRANS: Client error displayed when the submitted form contains unexpected data. $this->clientError(_('Unexpected form submission.')); } } @@ -110,7 +106,6 @@ class DesignadminpanelAction extends AdminPanelAction * * @return void */ - function saveDesignSettings() { // Workaround for PHP returning empty $_POST and $_FILES when POST @@ -225,11 +220,10 @@ class DesignadminpanelAction extends AdminPanelAction } /** - * Restore the default design - * - * @return void - */ - + * Restore the default design + * + * @return void + */ function restoreDefaults() { $this->deleteSetting('site', 'logo'); @@ -257,7 +251,6 @@ class DesignadminpanelAction extends AdminPanelAction * * @return string $filename the filename of the image */ - function saveBackgroundImage() { $filename = null; @@ -302,7 +295,6 @@ class DesignadminpanelAction extends AdminPanelAction * @throws ClientException for invalid theme archives * @throws ServerException if trouble saving the theme files */ - function saveCustomTheme() { if (common_config('theme_upload', 'enabled') && @@ -327,20 +319,23 @@ class DesignadminpanelAction extends AdminPanelAction * * @return void */ - function validate(&$values) { if (!empty($values['logo']) && !Validate::uri($values['logo'], array('allowed_schemes' => array('http', 'https')))) { + // TRANS: Client error displayed when a logo URL does is not valid. $this->clientError(_('Invalid logo URL.')); } if (!empty($values['ssllogo']) && !Validate::uri($values['ssllogo'], array('allowed_schemes' => array('https')))) { + // TRANS: Client error displayed when an SSL logo URL is invalid. $this->clientError(_('Invalid SSL logo URL.')); } if (!in_array($values['theme'], Theme::listAvailable())) { + // TRANS: Client error displayed when a theme is submitted through the form that is not in the theme list. + // TRANS: %s is the chosen unavailable theme. $this->clientError(sprintf(_("Theme not available: %s."), $values['theme'])); } } @@ -350,7 +345,6 @@ class DesignadminpanelAction extends AdminPanelAction * * @return void */ - function showStylesheets() { parent::showStylesheets(); @@ -362,7 +356,6 @@ class DesignadminpanelAction extends AdminPanelAction * * @return void */ - function showScripts() { parent::showScripts(); @@ -383,7 +376,6 @@ class DesignAdminPanelForm extends AdminForm * * @return int ID of the form */ - function id() { return 'form_design_admin_panel'; @@ -394,7 +386,6 @@ class DesignAdminPanelForm extends AdminForm * * @return string class of the form */ - function formClass() { return 'form_settings'; @@ -408,7 +399,6 @@ class DesignAdminPanelForm extends AdminForm * * @return string the method to use for submitting */ - function method() { $this->enctype = 'multipart/form-data'; @@ -421,7 +411,6 @@ class DesignAdminPanelForm extends AdminForm * * @return string URL of the action */ - function action() { return common_local_url('designadminpanel'); @@ -432,7 +421,6 @@ class DesignAdminPanelForm extends AdminForm * * @return void */ - function formData() { $this->showLogo(); @@ -445,16 +433,25 @@ class DesignAdminPanelForm extends AdminForm function showLogo() { $this->out->elementStart('fieldset', array('id' => 'settings_design_logo')); + // TRANS: Fieldset legend for form to change logo. $this->out->element('legend', null, _('Change logo')); $this->out->elementStart('ul', 'form_data'); $this->li(); - $this->input('logo', _('Site logo'), 'Logo for the site (full URL)'); + $this->input('logo', + // TRANS: Field label for StatusNet site logo. + _('Site logo'), + // TRANS: Title for field label for StatusNet site logo. + 'Logo for the site (full URL)'); $this->unli(); $this->li(); - $this->input('ssllogo', _('SSL logo'), 'Logo to show on SSL pages'); + $this->input('ssllogo', + // TRANS: Field label for SSL StatusNet site logo. + _('SSL logo'), + // TRANS: Title for field label for SSL StatusNet site logo. + 'Logo to show on SSL pages'); $this->unli(); $this->out->elementEnd('ul'); @@ -466,6 +463,7 @@ class DesignAdminPanelForm extends AdminForm function showTheme() { $this->out->elementStart('fieldset', array('id' => 'settings_design_theme')); + // TRANS: Fieldset legend for form change StatusNet site's theme. $this->out->element('legend', null, _('Change theme')); $this->out->elementStart('ul', 'form_data'); @@ -483,17 +481,21 @@ class DesignAdminPanelForm extends AdminForm $themes = array_combine($themes, $themes); $this->li(); + // TRANS: Field label for dropdown to choose site theme. $this->out->dropdown('theme', _('Site theme'), + // TRANS: Title for field label for dropdown to choose site theme. $themes, _('Theme for the site.'), false, $this->value('theme')); $this->unli(); if (common_config('theme_upload', 'enabled')) { $this->li(); + // TRANS: Field label for uploading a cutom theme. $this->out->element('label', array('for' => 'design_upload_theme'), _('Custom theme')); $this->out->element('input', array('id' => 'design_upload_theme', 'name' => 'design_upload_theme', 'type' => 'file')); + // TRANS: Form instructions for uploading a cutom StatusNet theme. $this->out->element('p', 'form_guide', _('You can upload a custom StatusNet theme as a .ZIP archive.')); $this->unli(); } @@ -509,22 +511,25 @@ class DesignAdminPanelForm extends AdminForm $this->out->elementStart('fieldset', array('id' => 'settings_design_background-image')); + // TRANS: Fieldset legend for theme background image. $this->out->element('legend', null, _('Change background image')); $this->out->elementStart('ul', 'form_data'); $this->li(); + $this->out->element('input', array('name' => 'MAX_FILE_SIZE', + 'type' => 'hidden', + 'id' => 'MAX_FILE_SIZE', + 'value' => ImageFile::maxFileSizeInt())); $this->out->element('label', array('for' => 'design_background-image_file'), + // TRANS: Field label for background image on theme designer page. _('Background')); $this->out->element('input', array('name' => 'design_background-image_file', 'type' => 'file', 'id' => 'design_background-image_file')); $this->out->element('p', 'form_guide', + // TRANS: Form guide for background image upload form on theme designer page. sprintf(_('You can upload a background image for the site. ' . 'The maximum file size is %1$s.'), ImageFile::maxFileSize())); - $this->out->element('input', array('name' => 'MAX_FILE_SIZE', - 'type' => 'hidden', - 'id' => 'MAX_FILE_SIZE', - 'value' => ImageFile::maxFileSizeInt())); $this->unli(); if (!empty($design->backgroundimage)) { @@ -568,11 +573,13 @@ class DesignAdminPanelForm extends AdminForm 'class' => 'radio'), // TRANS: Used as radio button label to not add a background image. _('Off')); + // TRANS: Form guide for turning background image on or off on theme designer page. $this->out->element('p', 'form_guide', _('Turn background image on or off.')); $this->unli(); $this->li(); $this->out->checkbox('design_background-image_repeat', + // TRANS: Checkbox label to title background image on theme designer page. _('Tile background image'), ($design->disposition & BACKGROUND_TILE) ? true : false); $this->unli(); @@ -587,7 +594,8 @@ class DesignAdminPanelForm extends AdminForm $design = $this->out->design; $this->out->elementStart('fieldset', array('id' => 'settings_design_color')); - $this->out->element('legend', null, _('Change colours')); + // TRANS: Fieldset legend for theme colors. + $this->out->element('legend', null, _('Change colors')); $this->out->elementStart('ul', 'form_data'); @@ -597,6 +605,7 @@ class DesignAdminPanelForm extends AdminForm $bgcolor = new WebColor($design->backgroundcolor); $this->li(); + // TRANS: Field label for background color selector. $this->out->element('label', array('for' => 'swatch-1'), _('Background')); $this->out->element('input', array('name' => 'design_background', 'type' => 'text', @@ -610,6 +619,7 @@ class DesignAdminPanelForm extends AdminForm $ccolor = new WebColor($design->contentcolor); $this->li(); + // TRANS: Field label for content color selector. $this->out->element('label', array('for' => 'swatch-2'), _('Content')); $this->out->element('input', array('name' => 'design_content', 'type' => 'text', @@ -623,6 +633,7 @@ class DesignAdminPanelForm extends AdminForm $sbcolor = new WebColor($design->sidebarcolor); $this->li(); + // TRANS: Field label for sidebar color selector. $this->out->element('label', array('for' => 'swatch-3'), _('Sidebar')); $this->out->element('input', array('name' => 'design_sidebar', 'type' => 'text', @@ -636,6 +647,7 @@ class DesignAdminPanelForm extends AdminForm $tcolor = new WebColor($design->textcolor); $this->li(); + // TRANS: Field label for text color selector. $this->out->element('label', array('for' => 'swatch-4'), _('Text')); $this->out->element('input', array('name' => 'design_text', 'type' => 'text', @@ -649,6 +661,7 @@ class DesignAdminPanelForm extends AdminForm $lcolor = new WebColor($design->linkcolor); $this->li(); + // TRANS: Field label for link color selector. $this->out->element('label', array('for' => 'swatch-5'), _('Links')); $this->out->element('input', array('name' => 'design_links', 'type' => 'text', @@ -674,10 +687,12 @@ class DesignAdminPanelForm extends AdminForm { if (common_config('custom_css', 'enabled')) { $this->out->elementStart('fieldset', array('id' => 'settings_design_advanced')); + // TRANS: Fieldset legend for advanced theme design settings. $this->out->element('legend', null, _('Advanced')); $this->out->elementStart('ul', 'form_data'); $this->li(); + // TRANS: Field label for custom CSS. $this->out->element('label', array('for' => 'css'), _('Custom CSS')); $this->out->element('textarea', array('name' => 'css', 'id' => 'css', @@ -699,17 +714,25 @@ class DesignAdminPanelForm extends AdminForm function formActions() { - $this->out->submit('defaults', _('Use defaults'), 'submit form_action-default', + // TRANS: Button text for resetting theme settings. + $this->out->submit('defaults', _m('BUTTON','Use defaults'), 'submit form_action-default', + // TRANS: Title for button for resetting theme settings. 'defaults', _('Restore default designs')); $this->out->element('input', array('id' => 'settings_design_reset', 'type' => 'reset', + // TRANS: Button text for resetting theme settings. 'value' => 'Reset', 'class' => 'submit form_action-primary', + // TRANS: Title for button for resetting theme settings. 'title' => _('Reset back to default'))); - $this->out->submit('save', _('Save'), 'submit form_action-secondary', - 'save', _('Save design')); + $this->out->submit('save', + // TRANS: Button text for saving theme settings. + _m('BUTTON','Save'), + 'submit form_action-secondary', + 'save', + // TRANS: Title for button for saving theme settings. + _('Save design')); } - } diff --git a/actions/disfavor.php b/actions/disfavor.php index 3ccdd69af2..39598d60bf 100644 --- a/actions/disfavor.php +++ b/actions/disfavor.php @@ -1,5 +1,4 @@ clientError(_('Not logged in.')); return; } @@ -71,6 +71,7 @@ class DisfavorAction 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; } @@ -78,12 +79,14 @@ class DisfavorAction extends Action $fave->user_id = $user->id; $fave->notice_id = $notice->id; if (!$fave->find(true)) { + // TRANS: Client error displayed when trying to remove favorite status for a notice that is not a favorite. $this->clientError(_('This notice is not a favorite!')); return; } $result = $fave->delete(); if (!$result) { common_log_db_error($fave, 'DELETE', __FILE__); + // TRANS: Server error displayed when removing a favorite from the database fails. $this->serverError(_('Could not delete favorite.')); return; } @@ -91,6 +94,7 @@ class DisfavorAction extends Action if ($this->boolean('ajax')) { $this->startHTML('text/xml;charset=utf-8'); $this->elementStart('head'); + // TRANS: Title for page on which favorites can be added. $this->element('title', null, _('Add to favorites')); $this->elementEnd('head'); $this->elementStart('body'); @@ -105,4 +109,3 @@ class DisfavorAction extends Action } } } - diff --git a/actions/doc.php b/actions/doc.php index f876fb8beb..20cf9e2810 100644 --- a/actions/doc.php +++ b/actions/doc.php @@ -1,5 +1,4 @@ element('h1', array('class' => 'entry-title'), $this->title()); @@ -96,7 +94,6 @@ class DocAction extends Action * * @return void. */ - function showContentBlock() { $this->elementStart('div', array('id' => 'content', 'class' => 'hentry')); @@ -117,7 +114,6 @@ class DocAction extends Action * * @return void */ - function showContent() { $this->raw($this->output); @@ -142,7 +138,6 @@ class DocAction extends Action * * @return boolean read-only flag (false) */ - function isReadOnly($args) { return true; @@ -155,7 +150,9 @@ class DocAction extends Action $this->filename = $this->getFilename(); if (empty($this->filename)) { - throw new ClientException(sprintf(_('No such document "%s"'), $this->title), 404); + // 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); diff --git a/actions/editapplication.php b/actions/editapplication.php index d1c7a5c3d5..4e67d9e57b 100644 --- a/actions/editapplication.php +++ b/actions/editapplication.php @@ -185,7 +185,7 @@ class EditApplicationAction extends OwnerDesignAction return; } elseif (mb_strlen($name) > 255) { // TRANS: Validation error shown when providing too long a name in the "Edit application" form. - $this->showForm(_('Name is too long (max 255 characters).')); + $this->showForm(_('Name is too long (maximum 255 characters).')); return; } else if ($this->nameExists($name)) { // TRANS: Validation error shown when providing a name for an application that already exists in the "Edit application" form. @@ -198,6 +198,7 @@ class EditApplicationAction extends OwnerDesignAction } elseif (Oauth_application::descriptionTooLong($description)) { $this->showForm(sprintf( // TRANS: Validation error shown when providing too long a description in the "Edit application" form. + // TRANS: %d is the maximum number of allowed characters. _m('Description is too long (maximum %d character).', 'Description is too long (maximum %d characters).', Oauth_application::maxDesc()), @@ -223,6 +224,7 @@ class EditApplicationAction extends OwnerDesignAction $this->showForm(_('Organization is too long (maximum 255 characters).')); return; } elseif (empty($homepage)) { + // TRANS: Form validation error show when an organisation name has not been provided in the edit application form. $this->showForm(_('Organization homepage is required.')); return; } elseif ((mb_strlen($homepage) > 0) diff --git a/actions/editgroup.php b/actions/editgroup.php index eaadbabe45..0e04170051 100644 --- a/actions/editgroup.php +++ b/actions/editgroup.php @@ -45,14 +45,13 @@ 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 EditgroupAction extends GroupDesignAction { - var $msg; function title() { + // TRANS: Title for form to edit a group. %s is a group nickname. return sprintf(_('Edit %s group'), $this->group->nickname); } @@ -65,6 +64,7 @@ class EditgroupAction extends GroupDesignAction parent::prepare($args); if (!common_logged_in()) { + // TRANS: Client error displayed trying to edit a group while not logged in. $this->clientError(_('You must be logged in to create a group.')); return false; } @@ -81,6 +81,7 @@ class EditgroupAction extends GroupDesignAction } if (!$nickname) { + // TRANS: Client error displayed trying to edit a group while not proving a nickname for the group to edit. $this->clientError(_('No nickname.'), 404); return false; } @@ -97,6 +98,7 @@ class EditgroupAction extends GroupDesignAction } if (!$this->group) { + // TRANS: Client error displayed trying to edit a non-existing group. $this->clientError(_('No such group.'), 404); return false; } @@ -104,6 +106,7 @@ class EditgroupAction extends GroupDesignAction $cur = common_current_user(); if (!$cur->isAdmin($this->group)) { + // TRANS: Client error displayed trying to edit a group while not being a group admin. $this->clientError(_('You must be an admin to edit the group.'), 403); return false; } @@ -120,7 +123,6 @@ class EditgroupAction extends GroupDesignAction * * @return void */ - function handle($args) { parent::handle($args); @@ -155,6 +157,7 @@ class EditgroupAction extends GroupDesignAction $this->element('p', 'error', $this->msg); } else { $this->element('p', 'instructions', + // TRANS: Form instructions for group edit form. _('Use this form to edit the group.')); } } @@ -169,114 +172,132 @@ class EditgroupAction extends GroupDesignAction { $cur = common_current_user(); if (!$cur->isAdmin($this->group)) { + // TRANS: Client error displayed trying to edit a group while not being a group admin. $this->clientError(_('You must be an admin to edit the group.'), 403); return; } - $nickname = common_canonical_nickname($this->trimmed('nickname')); - $fullname = $this->trimmed('fullname'); - $homepage = $this->trimmed('homepage'); - $description = $this->trimmed('description'); - $location = $this->trimmed('location'); - $aliasstring = $this->trimmed('aliases'); + if (Event::handle('StartGroupSaveForm', array($this))) { - if (!Validate::string($nickname, array('min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT))) { - $this->showForm(_('Nickname must have only lowercase letters '. - 'and numbers and no spaces.')); - return; - } else if ($this->nicknameExists($nickname)) { - $this->showForm(_('Nickname already in use. Try another one.')); - return; - } else if (!User_group::allowedNickname($nickname)) { - $this->showForm(_('Not a valid nickname.')); - return; - } else if (!is_null($homepage) && (strlen($homepage) > 0) && - !Validate::uri($homepage, - array('allowed_schemes' => - array('http', 'https')))) { - $this->showForm(_('Homepage is not a valid URL.')); - return; - } else if (!is_null($fullname) && mb_strlen($fullname) > 255) { - $this->showForm(_('Full name is too long (maximum 255 characters).')); - return; - } else if (User_group::descriptionTooLong($description)) { - $this->showForm(sprintf(_('Description is too long (max %d chars).'), User_group::maxDescription())); - return; - } else if (!is_null($location) && mb_strlen($location) > 255) { - $this->showForm(_('Location is too long (maximum 255 characters).')); - return; - } + $nickname = Nickname::normalize($this->trimmed('nickname')); + $fullname = $this->trimmed('fullname'); + $homepage = $this->trimmed('homepage'); + $description = $this->trimmed('description'); + $location = $this->trimmed('location'); + $aliasstring = $this->trimmed('aliases'); - if (!empty($aliasstring)) { - $aliases = array_map('common_canonical_nickname', array_unique(preg_split('/[\s,]+/', $aliasstring))); - } else { - $aliases = array(); - } - - if (count($aliases) > common_config('group', 'maxaliases')) { - $this->showForm(sprintf(_('Too many aliases! Maximum %d.'), - common_config('group', 'maxaliases'))); - return; - } - - foreach ($aliases as $alias) { - if (!Validate::string($alias, array('min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT))) { - $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias)); + if ($this->nicknameExists($nickname)) { + // TRANS: Group edit form validation error. + $this->showForm(_('Nickname already in use. Try another one.')); + return; + } else if (!User_group::allowedNickname($nickname)) { + // TRANS: Group edit form validation error. + $this->showForm(_('Not a valid nickname.')); + return; + } else if (!is_null($homepage) && (strlen($homepage) > 0) && + !Validate::uri($homepage, + array('allowed_schemes' => + array('http', 'https')))) { + // TRANS: Group edit form validation error. + $this->showForm(_('Homepage is not a valid URL.')); + return; + } else if (!is_null($fullname) && mb_strlen($fullname) > 255) { + // TRANS: Group edit form validation error. + $this->showForm(_('Full name is too long (maximum 255 characters).')); + return; + } else if (User_group::descriptionTooLong($description)) { + $this->showForm(sprintf( + // TRANS: Group edit form validation error. + _m('Description is too long (maximum %d character).', + 'Description is too long (maximum %d characters).', + User_group::maxDescription()), + User_group::maxDescription())); + return; + } else if (!is_null($location) && mb_strlen($location) > 255) { + // TRANS: Group edit form validation error. + $this->showForm(_('Location is too long (maximum 255 characters).')); return; } - if ($this->nicknameExists($alias)) { - $this->showForm(sprintf(_('Alias "%s" already in use. Try another one.'), - $alias)); + + if (!empty($aliasstring)) { + $aliases = array_map('common_canonical_nickname', array_unique(preg_split('/[\s,]+/', $aliasstring))); + } else { + $aliases = array(); + } + + if (count($aliases) > common_config('group', 'maxaliases')) { + // TRANS: Group edit form validation error. + // TRANS: %d is the maximum number of allowed aliases. + $this->showForm(sprintf(_m('Too many aliases! Maximum %d allowed.', + 'Too many aliases! Maximum %d allowed.', + common_config('group', 'maxaliases')), + common_config('group', 'maxaliases'))); return; } - // XXX assumes alphanum nicknames - if (strcmp($alias, $nickname) == 0) { - $this->showForm(_('Alias can\'t be the same as nickname.')); - return; + + foreach ($aliases as $alias) { + if (!Nickname::isValid($alias)) { + // TRANS: Group edit form validation error. + $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias)); + return; + } + if ($this->nicknameExists($alias)) { + // TRANS: Group edit form validation error. + $this->showForm(sprintf(_('Alias "%s" already in use. Try another one.'), + $alias)); + return; + } + // XXX assumes alphanum nicknames + if (strcmp($alias, $nickname) == 0) { + // TRANS: Group edit form validation error. + $this->showForm(_('Alias can\'t be the same as nickname.')); + return; + } } + + $this->group->query('BEGIN'); + + $orig = clone($this->group); + + $this->group->nickname = $nickname; + $this->group->fullname = $fullname; + $this->group->homepage = $homepage; + $this->group->description = $description; + $this->group->location = $location; + $this->group->mainpage = common_local_url('showgroup', array('nickname' => $nickname)); + + $result = $this->group->update($orig); + + if (!$result) { + common_log_db_error($this->group, 'UPDATE', __FILE__); + // TRANS: Server error displayed when editing a group fails. + $this->serverError(_('Could not update group.')); + } + + $result = $this->group->setAliases($aliases); + + if (!$result) { + // TRANS: Server error displayed when group aliases could not be added. + $this->serverError(_('Could not create aliases.')); + } + + if ($nickname != $orig->nickname) { + common_log(LOG_INFO, "Saving local group info."); + $local = Local_group::staticGet('group_id', $this->group->id); + $local->setNickname($nickname); + } + + $this->group->query('COMMIT'); + + Event::handle('EndGroupSaveForm', array($this)); } - $this->group->query('BEGIN'); - - $orig = clone($this->group); - - $this->group->nickname = $nickname; - $this->group->fullname = $fullname; - $this->group->homepage = $homepage; - $this->group->description = $description; - $this->group->location = $location; - $this->group->mainpage = common_local_url('showgroup', array('nickname' => $nickname)); - - $result = $this->group->update($orig); - - if (!$result) { - common_log_db_error($this->group, 'UPDATE', __FILE__); - $this->serverError(_('Could not update group.')); - } - - $result = $this->group->setAliases($aliases); - - if (!$result) { - $this->serverError(_('Could not create aliases.')); - } - - if ($nickname != $orig->nickname) { - common_log(LOG_INFO, "Saving local group info."); - $local = Local_group::staticGet('group_id', $this->group->id); - $local->setNickname($nickname); - } - - $this->group->query('COMMIT'); - if ($this->group->nickname != $orig->nickname) { common_redirect(common_local_url('editgroup', array('nickname' => $nickname)), 303); } else { + // TRANS: Group edit form success message. $this->showForm(_('Options saved.')); } } @@ -300,4 +321,3 @@ class EditgroupAction extends GroupDesignAction return false; } } - diff --git a/actions/emailsettings.php b/actions/emailsettings.php index 9c250fc8a9..3e977074fc 100644 --- a/actions/emailsettings.php +++ b/actions/emailsettings.php @@ -46,7 +46,6 @@ require_once INSTALLDIR.'/lib/accountsettingsaction.php'; * * @see Widget */ - class EmailsettingsAction extends AccountSettingsAction { /** @@ -54,7 +53,6 @@ class EmailsettingsAction extends AccountSettingsAction * * @return string Title of the page */ - function title() { // TRANS: Title for e-mail settings. @@ -66,7 +64,6 @@ class EmailsettingsAction extends AccountSettingsAction * * @return instructions for use */ - function getInstructions() { // XXX: For consistency of parameters in messages, this should be a @@ -79,6 +76,7 @@ class EmailsettingsAction extends AccountSettingsAction function showScripts() { parent::showScripts(); + $this->script('emailsettings.js'); $this->autofocus('email'); } @@ -90,7 +88,6 @@ class EmailsettingsAction extends AccountSettingsAction * * @return void */ - function showContent() { $user = common_current_user(); @@ -117,8 +114,8 @@ class EmailsettingsAction extends AccountSettingsAction $confirm = $this->getConfirmation(); if ($confirm) { $this->element('p', array('id' => 'form_unconfirmed'), $confirm->address); - // TRANS: Form note in e-mail settings form. $this->element('p', array('class' => 'form_note'), + // TRANS: Form note in e-mail settings form. _('Awaiting confirmation on this address. '. 'Check your inbox (and spam box!) for a message '. 'with further instructions.')); @@ -149,10 +146,30 @@ class EmailsettingsAction extends AccountSettingsAction $this->elementStart('fieldset', array('id' => 'settings_email_incoming')); // TRANS: Form legend for incoming e-mail settings form. $this->element('legend', null, _('Incoming email')); + + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + $this->checkbox('emailpost', + // TRANS: Checkbox label in e-mail preferences form. + _('I want to post notices by email.'), + $user->emailpost); + $this->elementEnd('li'); + $this->elementEnd('ul'); + + // Our stylesheets make the form_data list items all floats, which + // creates lots of problems with trying to wrap divs around things. + // This should force a break before the next section, which needs + // to be separate so we can disable the things in it when the + // checkbox is off. + $this->elementStart('div', array('style' => 'clear: both')); + $this->elementEnd('div'); + + $this->elementStart('div', array('id' => 'emailincoming')); + if ($user->incomingemail) { $this->elementStart('p'); $this->element('span', 'address', $user->incomingemail); - // XXX: Looks a little awkward in the UI. + // @todo XXX: Looks a little awkward in the UI. // Something like "xxxx@identi.ca Send email ..". Needs improvement. $this->element('span', 'input_instructions', // TRANS: Form instructions for incoming e-mail form in e-mail settings. @@ -163,13 +180,22 @@ class EmailsettingsAction extends AccountSettingsAction } $this->elementStart('p'); - $this->element('span', 'input_instructions', - // TRANS: Instructions for incoming e-mail address input form. - _('Make a new email address for posting to; '. - 'cancels the old one.')); + if ($user->incomingemail) { + // TRANS: Instructions for incoming e-mail address input form, when an address has already been assigned. + $msg = _('Make a new email address for posting to; '. + 'cancels the old one.'); + } else { + // TRANS: Instructions for incoming e-mail address input form. + $msg = _('To send notices via email, we need to create a unique email address for you on this server:'); + } + $this->element('span', 'input_instructions', $msg); $this->elementEnd('p'); + // TRANS: Button label for adding an e-mail address to send notices from. $this->submit('newincoming', _m('BUTTON','New')); + + $this->elementEnd('div'); // div#emailincoming + $this->elementEnd('fieldset'); } @@ -178,51 +204,47 @@ class EmailsettingsAction extends AccountSettingsAction $this->element('legend', null, _('Email preferences')); $this->elementStart('ul', 'form_data'); - $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'); - if (common_config('emailpost', 'enabled')) { - $this->elementStart('li'); - $this->checkbox('emailpost', - // TRANS: Checkbox label in e-mail preferences form. - _('I want to post notices by email.'), - $user->emailpost); - $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'); + + 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->elementEnd('ul'); // TRANS: Button label to save e-mail preferences. $this->submit('save', _m('BUTTON','Save')); @@ -236,7 +258,6 @@ class EmailsettingsAction extends AccountSettingsAction * * @return Confirm_address Email address confirmation for user, or null */ - function getConfirmation() { $user = common_current_user(); @@ -262,7 +283,6 @@ class EmailsettingsAction extends AccountSettingsAction * * @return void */ - function handlePost() { // CSRF protection @@ -296,46 +316,50 @@ class EmailsettingsAction extends AccountSettingsAction * * @return void */ - function savePreferences() { - $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'); + $user = common_current_user(); - $user = common_current_user(); + if (Event::handle('StartEmailSaveForm', array($this, &$user))) { - assert(!is_null($user)); // should already be checked + $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'); - $user->query('BEGIN'); + assert(!is_null($user)); // should already be checked - $original = clone($user); + $user->query('BEGIN'); - $user->emailnotifysub = $emailnotifysub; - $user->emailnotifyfav = $emailnotifyfav; - $user->emailnotifymsg = $emailnotifymsg; - $user->emailnotifynudge = $emailnotifynudge; - $user->emailnotifyattn = $emailnotifyattn; - $user->emailmicroid = $emailmicroid; - $user->emailpost = $emailpost; + $original = clone($user); - $result = $user->update($original); + $user->emailnotifysub = $emailnotifysub; + $user->emailnotifyfav = $emailnotifyfav; + $user->emailnotifymsg = $emailnotifymsg; + $user->emailnotifynudge = $emailnotifynudge; + $user->emailnotifyattn = $emailnotifyattn; + $user->emailmicroid = $emailmicroid; + $user->emailpost = $emailpost; - if ($result === false) { - common_log_db_error($user, 'UPDATE', __FILE__); - // TRANS: Server error thrown on database error updating e-mail preferences. - $this->serverError(_('Couldn\'t update user.')); - return; - } + $result = $user->update($original); - $user->query('COMMIT'); + 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; + } - // TRANS: Confirmation message for successful e-mail preferences save. - $this->showForm(_('Email preferences saved.'), true); + $user->query('COMMIT'); + + Event::handle('EndEmailSaveForm', array($this)); + + // TRANS: Confirmation message for successful e-mail preferences save. + $this->showForm(_('Email preferences saved.'), true); + } } /** @@ -343,7 +367,6 @@ class EmailsettingsAction extends AccountSettingsAction * * @return void */ - function addAddress() { $user = common_current_user(); @@ -362,7 +385,7 @@ class EmailsettingsAction extends AccountSettingsAction if (!$email) { // TRANS: Message given saving e-mail address that cannot be normalised. - $this->showForm(_('Cannot normalize that email address')); + $this->showForm(_('Cannot normalize that email address.')); return; } if (!Validate::email($email, common_config('email', 'check_domain'))) { @@ -392,7 +415,7 @@ class EmailsettingsAction extends AccountSettingsAction if ($result === false) { common_log_db_error($confirm, 'INSERT', __FILE__); // TRANS: Server error thrown on database error adding e-mail confirmation code. - $this->serverError(_('Couldn\'t insert confirmation code.')); + $this->serverError(_('Could not insert confirmation code.')); return; } @@ -411,7 +434,6 @@ class EmailsettingsAction extends AccountSettingsAction * * @return void */ - function cancelConfirmation() { $email = $this->arg('email'); @@ -434,7 +456,7 @@ class EmailsettingsAction extends AccountSettingsAction if (!$result) { common_log_db_error($confirm, 'DELETE', __FILE__); // TRANS: Server error thrown on database error canceling e-mail address confirmation. - $this->serverError(_('Couldn\'t delete email confirmation.')); + $this->serverError(_('Could not delete email confirmation.')); return; } @@ -447,7 +469,6 @@ class EmailsettingsAction extends AccountSettingsAction * * @return void */ - function removeAddress() { $user = common_current_user(); @@ -474,7 +495,7 @@ class EmailsettingsAction extends AccountSettingsAction if (!$result) { common_log_db_error($user, 'UPDATE', __FILE__); // TRANS: Server error thrown on database error removing a registered e-mail address. - $this->serverError(_('Couldn\'t update user.')); + $this->serverError(_('Could not update user.')); return; } $user->query('COMMIT'); @@ -488,12 +509,12 @@ class EmailsettingsAction extends AccountSettingsAction * * @return void */ - function removeIncoming() { $user = common_current_user(); if (!$user->incomingemail) { + // TRANS: Form validation error displayed when trying to remove an incoming e-mail address while no address has been set. $this->showForm(_('No incoming email address.')); return; } @@ -501,11 +522,12 @@ class EmailsettingsAction extends AccountSettingsAction $orig = clone($user); $user->incomingemail = null; + $user->emailpost = 0; if (!$user->updateKeys($orig)) { common_log_db_error($user, 'UPDATE', __FILE__); // TRANS: Server error thrown on database error removing incoming e-mail address. - $this->serverError(_("Couldn't update user record.")); + $this->serverError(_("Could not update user record.")); } // TRANS: Message given after successfully removing an incoming e-mail address. @@ -517,7 +539,6 @@ class EmailsettingsAction extends AccountSettingsAction * * @return void */ - function newIncoming() { $user = common_current_user(); @@ -525,11 +546,12 @@ class EmailsettingsAction extends AccountSettingsAction $orig = clone($user); $user->incomingemail = mail_new_incoming_address(); + $user->emailpost = 1; if (!$user->updateKeys($orig)) { common_log_db_error($user, 'UPDATE', __FILE__); // TRANS: Server error thrown on database error adding incoming e-mail address. - $this->serverError(_("Couldn't update user record.")); + $this->serverError(_("Could not update user record.")); } // TRANS: Message given after successfully adding an incoming e-mail address. diff --git a/actions/favor.php b/actions/favor.php index 01976a38f5..bcefcd26e7 100644 --- a/actions/favor.php +++ b/actions/favor.php @@ -1,5 +1,4 @@ clientError(_('Not logged in.')); return; } @@ -76,11 +76,13 @@ class FavorAction extends Action return; } if ($user->hasFave($notice)) { + // TRANS: Client error displayed when trying to mark a notice as favorite that already is a favorite. $this->clientError(_('This notice is already a favorite!')); return; } $fave = Fave::addNew($user->getProfile(), $notice); if (!$fave) { + // TRANS: Server error displayed when trying to mark a notice as favorite fails in the database. $this->serverError(_('Could not create favorite.')); return; } @@ -89,6 +91,7 @@ class FavorAction extends Action if ($this->boolean('ajax')) { $this->startHTML('text/xml;charset=utf-8'); $this->elementStart('head'); + // TRANS: Page title for page on which favorite notices can be unfavourited. $this->element('title', null, _('Disfavor favorite')); $this->elementEnd('head'); $this->elementStart('body'); @@ -123,4 +126,3 @@ class FavorAction extends Action } } } - diff --git a/actions/favorited.php b/actions/favorited.php index 19d49feecf..c137cf424f 100644 --- a/actions/favorited.php +++ b/actions/favorited.php @@ -48,7 +48,6 @@ require_once INSTALLDIR.'/lib/noticelist.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 FavoritedAction extends Action { var $page = null; @@ -62,8 +61,11 @@ class FavoritedAction extends Action function title() { if ($this->page == 1) { + // TRANS: Page title for first page of favorited notices. return _('Popular notices'); } else { + // TRANS: Page title for all but first page of favorited notices. + // TRANS: %d is the page number being displayed. return sprintf(_('Popular notices, page %d'), $this->page); } } @@ -73,9 +75,9 @@ class FavoritedAction extends Action * * @return instructions for use */ - function getInstructions() { + // TRANS: Description on page displaying favorited notices. return _('The most popular notices on the site right now.'); } @@ -84,7 +86,6 @@ class FavoritedAction extends Action * * @return boolean true */ - function isReadOnly($args) { return true; @@ -99,7 +100,6 @@ class FavoritedAction extends Action * * @todo move queries from showContent() to here */ - function prepare($args) { parent::prepare($args); @@ -119,7 +119,6 @@ class FavoritedAction extends Action * * @return void */ - function handle($args) { parent::handle($args); @@ -134,7 +133,6 @@ class FavoritedAction extends Action * * @return void */ - function showPageNotice() { $instr = $this->getInstructions(); @@ -147,12 +145,16 @@ class FavoritedAction extends Action function showEmptyList() { + // TRANS: Text displayed instead of a list when a site does not yet have any favourited notices. $message = _('Favorite notices appear on this page but no one has favorited one yet.') . ' '; if (common_logged_in()) { + // TRANS: Additional text displayed instead of a list when a site does not yet have any favourited notices for logged in users. $message .= _('Be the first to add a notice to your favorites by clicking the fave button next to any notice you like.'); } else { + // TRANS: Additional text displayed instead of a list when a site does not yet have any favourited notices for not logged in users. + // TRANS: %%action.register%% is a registration link. "[link text](link)" is Mark Down. Do not change the formatting. $message .= _('Why not [register an account](%%action.register%%) and be the first to add a notice to your favorites!'); } @@ -168,7 +170,6 @@ class FavoritedAction extends Action * * @return void */ - function showLocalNav() { $nav = new PublicGroupNav($this); @@ -182,7 +183,6 @@ class FavoritedAction extends Action * * @return void */ - function showContent() { $pop = new Popularity(); diff --git a/actions/favoritesrss.php b/actions/favoritesrss.php index 51c92af933..494327674d 100644 --- a/actions/favoritesrss.php +++ b/actions/favoritesrss.php @@ -1,5 +1,4 @@ user = User::staticGet('nickname', $nickname); if (!$this->user) { + // TRANS: Client error displayed when trying to get the RSS feed with favorites of a user that does not exist. $this->clientError(_('No such user.')); return false; } else { @@ -108,10 +106,14 @@ class FavoritesrssAction extends Rss10Action $c = array('url' => common_local_url('favoritesrss', array('nickname' => $user->nickname)), + // TRANS: Title of RSS feed with favourite notices of a user. + // TRANS: %s is a user's nickname. 'title' => sprintf(_("%s's favorite notices"), $user->nickname), 'link' => common_local_url('showfavorites', array('nickname' => $user->nickname)), + // TRANS: Desciption of RSS feed with favourite notices of a user. + // TRANS: %1$s is a user's nickname, %2$s is the name of the StatusNet site. 'description' => sprintf(_('Updates favored by %1$s on %2$s!'), $user->nickname, common_config('site', 'name'))); return $c; diff --git a/actions/featured.php b/actions/featured.php index dd1056dd58..9a7f128b57 100644 --- a/actions/featured.php +++ b/actions/featured.php @@ -45,7 +45,6 @@ require_once INSTALLDIR.'/lib/publicgroupnav.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 FeaturedAction extends Action { var $page = null; @@ -66,8 +65,11 @@ class FeaturedAction extends Action function title() { if ($this->page == 1) { + // TRANS: Page title for first page of featured users. return _('Featured users'); } else { + // TRANS: Page title for all but first page of featured users. + // TRANS: %d is the page number being displayed. return sprintf(_('Featured users, page %d'), $this->page); } } @@ -96,7 +98,8 @@ class FeaturedAction extends Action function getInstructions() { - return sprintf(_('A selection of some great users on %s'), + // TRANS: Description on page displaying featured users. + return sprintf(_('A selection of some great users on %s.'), common_config('site', 'name')); } diff --git a/actions/file.php b/actions/file.php index c6f7b998a2..49ed8af1d6 100644 --- a/actions/file.php +++ b/actions/file.php @@ -21,6 +21,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } require_once(INSTALLDIR.'/actions/shownotice.php'); +// @todo FIXME: Add documentation. class FileAction extends Action { var $id = null; @@ -31,14 +32,17 @@ class FileAction extends Action parent::prepare($args); $this->id = $this->trimmed('notice'); if (empty($this->id)) { + // TRANS: Client error displayed when no notice ID was given trying do display a file. $this->clientError(_('No notice ID.')); } $notice = Notice::staticGet('id', $this->id); if (empty($notice)) { + // TRANS: Client error displayed when an invalid notice ID was given trying do display a file. $this->clientError(_('No notice.')); } $atts = $notice->attachments(); if (empty($atts)) { + // TRANS: Client error displayed when trying do display a file for a notice without a file attachement. $this->clientError(_('No attachments.')); } foreach ($atts as $att) { @@ -48,6 +52,9 @@ class FileAction extends Action } } if (empty($this->filerec)) { + // XXX: Is this translation hint correct? If yes, please remove comment, if no, please correct and remove comment. + // TRANS: Client error displayed when trying do display a file for a notice with file attachements + // TRANS: that could not be found. $this->clientError(_('No uploaded attachments.')); } return true; @@ -62,11 +69,8 @@ class FileAction extends Action * * @return boolean true */ - function isReadOnly($args) { return true; } - } - diff --git a/actions/finishremotesubscribe.php b/actions/finishremotesubscribe.php index 0325f6adbb..59725af27f 100644 --- a/actions/finishremotesubscribe.php +++ b/actions/finishremotesubscribe.php @@ -48,7 +48,6 @@ require_once INSTALLDIR.'/lib/omb.php'; */ class FinishremotesubscribeAction extends Action { - /** * Class handler. * @@ -56,7 +55,7 @@ class FinishremotesubscribeAction extends Action * * @return nothing * - **/ + */ function handle($args) { parent::handle($args); @@ -66,6 +65,7 @@ class FinishremotesubscribeAction extends Action $service = unserialize($_SESSION['oauth_authorization_request']); if (!$service) { + // TRANS: Client error displayed when subscribing to a remote profile and an unexpected response is received. $this->clientError(_('Not expecting this response!')); return; } @@ -77,6 +77,7 @@ class FinishremotesubscribeAction extends Action $user = User::staticGet('uri', $service->getListeneeURI()); if (!$user) { + // TRANS: Client error displayed when subscribing to a remote profile that does not exist. $this->clientError(_('User being listened to does not exist.')); return; } @@ -84,6 +85,7 @@ class FinishremotesubscribeAction extends Action $other = User::staticGet('uri', $service->getListenerURI()); if ($other) { + // TRANS: Client error displayed when subscribing to a remote profile that is a local profile. $this->clientError(_('You can use the local subscription!')); return; } @@ -96,6 +98,7 @@ class FinishremotesubscribeAction extends Action $profile = Profile::staticGet($remote->id); if ($user->hasBlocked($profile)) { + // TRANS: Client error displayed when subscribing to a remote profile that is blocked form subscribing to. $this->clientError(_('That user has blocked you from subscribing.')); return; } @@ -107,14 +110,17 @@ class FinishremotesubscribeAction extends Action } catch (OAuthException $e) { if ($e->getMessage() == 'The authorized token does not equal the ' . 'submitted token.') { + // TRANS: Client error displayed when subscribing to a remote profile without providing an authorised token. $this->clientError(_('You are not authorized.')); return; } else { + // TRANS: Client error displayed when subscribing to a remote profile and conversion of the request token to access token fails. $this->clientError(_('Could not convert request token to ' . 'access token.')); return; } } catch (OMB_RemoteServiceException $e) { + // TRANS: Client error displayed when subscribing to a remote profile fails because of an unsupported version of the OMB protocol. $this->clientError(_('Remote service uses unknown version of ' . 'OMB protocol.')); return; @@ -135,6 +141,7 @@ class FinishremotesubscribeAction extends Action $service->getServiceURI(OMB_ENDPOINT_UPDATEPROFILE); if (!$remote->update($orig_remote)) { + // TRANS: Server error displayed when subscribing to a remote profile fails because the remote profile could not be updated. $this->serverError(_('Error updating remote profile.')); return; } diff --git a/actions/foaf.php b/actions/foaf.php index 09af7b5026..ceb575c736 100644 --- a/actions/foaf.php +++ b/actions/foaf.php @@ -23,6 +23,7 @@ define('LISTENER', 1); define('LISTENEE', -1); define('BOTH', 0); +// @todo XXX: Documentation missing. class FoafAction extends Action { function isReadOnly($args) @@ -37,6 +38,7 @@ class FoafAction extends Action $nickname_arg = $this->arg('nickname'); if (empty($nickname_arg)) { + // TRANS: Client error displayed when requesting Friends of a Friend feed without providing a user nickname. $this->clientError(_('No such user.'), 404); return false; } @@ -55,6 +57,7 @@ class FoafAction extends Action $this->user = User::staticGet('nickname', $this->nickname); if (!$this->user) { + // TRANS: Client error displayed when requesting Friends of a Friend feed for an object that is not a user. $this->clientError(_('No such user.'), 404); return false; } @@ -62,6 +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. $this->serverError(_('User has no profile.'), 500); return false; } @@ -110,7 +114,7 @@ class FoafAction extends Action if ($this->profile->bio) { $this->element('bio:olb', null, $this->profile->bio); } - + $location = $this->profile->getLocation(); if ($location) { $attr = array(); @@ -118,7 +122,7 @@ class FoafAction extends Action $attr['rdf:about'] = $location->getRdfURL(); } $location_name = $location->getName(); - + $this->elementStart('based_near'); $this->elementStart('geo:SpatialThing', $attr); if ($location_name) { @@ -193,7 +197,7 @@ class FoafAction extends Action $this->element('knows', array('rdf:resource' => $uri)); } } - + $this->elementEnd('Agent'); @@ -239,18 +243,17 @@ class FoafAction extends Action /** * Output FOAF bit for the given profile. - * + * * @param Profile $profile * @param mixed $service Root URL of this StatusNet instance for a local * user, otherwise null. * @param mixed $useruri URI string for the referenced profile.. * @param boolean $fetchSubscriptions Should we load and list all their subscriptions? * @param boolean $isSubscriber if not fetching subs, we can still mark the user as following the current page. - * + * * @return array if $fetchSubscribers is set, return a list of info on those * subscriptions. */ - function showMicrobloggingAccount($profile, $service=null, $useruri=null, $fetchSubscriptions=false, $isSubscriber=false) { $attr = array(); diff --git a/actions/foafgroup.php b/actions/foafgroup.php index 4db40c28be..9638ea0f34 100644 --- a/actions/foafgroup.php +++ b/actions/foafgroup.php @@ -27,6 +27,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +// @todo XXX: Documentation missing. class FoafGroupAction extends Action { function isReadOnly($args) @@ -41,6 +42,7 @@ class FoafGroupAction extends Action $nickname_arg = $this->arg('nickname'); if (empty($nickname_arg)) { + // TRANS: Client error displayed when requesting Friends of a Friend feed without providing a group nickname. $this->clientError(_('No such group.'), 404); return false; } @@ -59,6 +61,7 @@ class FoafGroupAction extends Action $local = Local_group::staticGet('nickname', $this->nickname); if (!$local) { + // TRANS: Client error displayed when requesting Friends of a Friend feed for a non-local group. $this->clientError(_('No such group.'), 404); return false; } @@ -66,6 +69,7 @@ class FoafGroupAction extends Action $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { + // TRANS: Client error displayed when requesting Friends of a Friend feed for a nickname that is not a group. $this->clientError(_('No such group.'), 404); return false; } diff --git a/actions/geocode.php b/actions/geocode.php index d934930608..123a839f56 100644 --- a/actions/geocode.php +++ b/actions/geocode.php @@ -68,7 +68,7 @@ class GeocodeAction extends Action * * @return nothing * - **/ + */ function handle($args) { header('Content-Type: application/json; charset=utf-8'); @@ -89,7 +89,6 @@ class GeocodeAction extends Action * * @return boolean true */ - function isReadOnly($args) { return true; diff --git a/actions/getfile.php b/actions/getfile.php index 9cbe8e1d99..ad41122503 100644 --- a/actions/getfile.php +++ b/actions/getfile.php @@ -47,13 +47,11 @@ require_once 'MIME/Type.php'; * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ - class GetfileAction extends Action { /** * Path of file to return */ - var $path = null; /** @@ -63,7 +61,6 @@ class GetfileAction extends Action * * @return success flag */ - function prepare($args) { parent::prepare($args); @@ -76,10 +73,12 @@ class GetfileAction extends Action } if (empty($path) or !file_exists($path)) { + // TRANS: Client error displayed when requesting a non-existent file. $this->clientError(_('No such file.'), 404); return false; } if (!is_readable($path)) { + // TRANS: Client error displayed when requesting a file without having read access to it. $this->clientError(_('Cannot read file.'), 403); return false; } @@ -93,7 +92,6 @@ class GetfileAction extends Action * * @return boolean true */ - function isReadOnly($args) { return true; @@ -104,7 +102,6 @@ class GetfileAction extends Action * * @return int last-modified date as unix timestamp */ - function lastModified() { if (common_config('site', 'use_x_sendfile')) { @@ -122,7 +119,6 @@ class GetfileAction extends Action * * @return string etag http header */ - function etag() { if (common_config('site', 'use_x_sendfile')) { @@ -151,7 +147,6 @@ class GetfileAction extends Action * * @return void */ - function handle($args) { // undo headers set by PHP sessions diff --git a/actions/grantrole.php b/actions/grantrole.php index b8b23d02e9..36369a8600 100644 --- a/actions/grantrole.php +++ b/actions/grantrole.php @@ -2,7 +2,7 @@ /** * StatusNet, the distributed open-source microblogging tool * - * Action class to sandbox an abusive user + * Action class to grant user roles. * * PHP version 5 * @@ -32,7 +32,7 @@ if (!defined('STATUSNET')) { } /** - * Sandbox a user. + * Assign role to user. * * @category Action * @package StatusNet @@ -40,7 +40,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class GrantRoleAction extends ProfileFormAction { /** @@ -50,19 +49,20 @@ class GrantRoleAction extends ProfileFormAction * * @return boolean success flag */ - function prepare($args) { if (!parent::prepare($args)) { return false; } - + $this->role = $this->arg('role'); if (!Profile_role::isValid($this->role)) { + // TRANS: Client error displayed when trying to assign an invalid role to a user. $this->clientError(_('Invalid role.')); return false; } if (!Profile_role::isSettable($this->role)) { + // TRANS: Client error displayed when trying to assign an reserved role to a user. $this->clientError(_('This role is reserved and cannot be set.')); return false; } @@ -72,6 +72,7 @@ class GrantRoleAction extends ProfileFormAction assert(!empty($cur)); // checked by parent if (!$cur->hasRight(Right::GRANTROLE)) { + // TRANS: Client error displayed when trying to assign a role to a user while not being allowed to set roles. $this->clientError(_('You cannot grant user roles on this site.')); return false; } @@ -79,6 +80,7 @@ class GrantRoleAction extends ProfileFormAction assert(!empty($this->profile)); // checked by parent if ($this->profile->hasRole($this->role)) { + // TRANS: Client error displayed when trying to assign a role to a user that already has that role. $this->clientError(_('User already has this role.')); return false; } @@ -91,7 +93,6 @@ class GrantRoleAction extends ProfileFormAction * * @return void */ - function handlePost() { $this->profile->grantRole($this->role); diff --git a/actions/groupblock.php b/actions/groupblock.php index 39f783397a..2ac0f633bb 100644 --- a/actions/groupblock.php +++ b/actions/groupblock.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class GroupblockAction extends RedirectingAction { var $profile = null; @@ -53,11 +52,11 @@ class GroupblockAction extends RedirectingAction * * @return boolean success flag */ - function prepare($args) { parent::prepare($args); if (!common_logged_in()) { + // TRANS: Client error displayed trying to block a user from a group while not logged in. $this->clientError(_('Not logged in.')); return false; } @@ -68,35 +67,42 @@ class GroupblockAction extends RedirectingAction } $id = $this->trimmed('blockto'); if (empty($id)) { + // TRANS: Client error displayed trying to block a user from a group while not specifying a to be blocked user profile. $this->clientError(_('No profile specified.')); return false; } $this->profile = Profile::staticGet('id', $id); if (empty($this->profile)) { + // TRANS: Client error displayed trying to block a user from a group while specifying a non-existing profile. $this->clientError(_('No profile with that ID.')); return false; } $group_id = $this->trimmed('blockgroup'); if (empty($group_id)) { + // TRANS: Client error displayed trying to block a user from a group while not specifying a group to block a profile from. $this->clientError(_('No group specified.')); return false; } $this->group = User_group::staticGet('id', $group_id); if (empty($this->group)) { + // TRANS: Client error displayed trying to block a user from a group while specifying a non-existing group. $this->clientError(_('No such group.')); return false; } $user = common_current_user(); if (!$user->isAdmin($this->group)) { + // TRANS: Client error displayed trying to block a user from a group while not being an admin user. $this->clientError(_('Only an admin can block group members.'), 401); return false; } if (Group_block::isBlocked($this->group, $this->profile)) { + // TRANS: Client error displayed trying to block a user from a group while user is already blocked from the given group. $this->clientError(_('User is already blocked from group.')); return false; } // XXX: could have proactive blocks, but we don't have UI for it. if (!$this->profile->isMember($this->group)) { + // TRANS: Client error displayed trying to block a user from a group while user is not a member of given group. $this->clientError(_('User is not a member of group.')); return false; } @@ -131,6 +137,7 @@ class GroupblockAction extends RedirectingAction } function title() { + // TRANS: Title for block user from group page. return _('Block user from group'); } @@ -145,7 +152,6 @@ class GroupblockAction extends RedirectingAction * * @return void */ - function areYouSureForm() { $id = $this->profile->id; @@ -155,8 +161,11 @@ class GroupblockAction extends RedirectingAction 'action' => common_local_url('groupblock'))); $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); + // TRANS: Fieldset legend for block user from group form. $this->element('legend', _('Block user')); $this->element('p', null, + // TRANS: Explanatory text for block user from group form before setting the block. + // TRANS: %1$s is that to be blocked user, %2$s is the group the user will be blocked from. sprintf(_('Are you sure you want to block user "%1$s" from the group "%2$s"? '. 'They will be removed from the group, unable to post, and '. 'unable to subscribe to the group in the future.'), @@ -196,24 +205,24 @@ class GroupblockAction extends RedirectingAction * * @return void */ - function blockProfile() { $block = Group_block::blockProfile($this->group, $this->profile, common_current_user()); if (empty($block)) { + // TRANS: Server error displayed when trying to block a user from a group fails because of an application error. $this->serverError(_("Database error blocking user from group.")); return false; } - + $this->returnToPrevious(); } /** * If we reached this form without returnto arguments, default to * the top of the group's member list. - * + * * @return string URL */ function defaultReturnTo() @@ -227,6 +236,4 @@ class GroupblockAction extends RedirectingAction parent::showScripts(); $this->autofocus('form_action-yes'); } - } - diff --git a/actions/groupbyid.php b/actions/groupbyid.php index 5af7109cb4..f18e4540c0 100644 --- a/actions/groupbyid.php +++ b/actions/groupbyid.php @@ -47,7 +47,6 @@ require_once INSTALLDIR.'/lib/feedlist.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 GroupbyidAction extends Action { /** group we're viewing. */ @@ -58,7 +57,6 @@ class GroupbyidAction extends Action * * @return boolean true */ - function isReadOnly($args) { return true; @@ -71,6 +69,7 @@ class GroupbyidAction extends Action $id = $this->arg('id'); if (!$id) { + // TRANS: Client error displayed referring to a group's permalink without providing a group ID. $this->clientError(_('No ID.')); return false; } @@ -80,6 +79,7 @@ class GroupbyidAction extends Action $this->group = User_group::staticGet('id', $id); if (!$this->group) { + // TRANS: Client error displayed referring to a group's permalink for a non-existing group ID. $this->clientError(_('No such group.'), 404); return false; } @@ -95,9 +95,8 @@ class GroupbyidAction extends Action * * @return void */ - function handle($args) { common_redirect($this->group->homeUrl(), 303); } -} \ No newline at end of file +} diff --git a/actions/groupdesignsettings.php b/actions/groupdesignsettings.php index 526226a285..3ef5e20e44 100644 --- a/actions/groupdesignsettings.php +++ b/actions/groupdesignsettings.php @@ -2,7 +2,7 @@ /** * StatusNet, the distributed open-source microblogging tool * - * Change user password + * Saves a design for a given group. * * PHP version 5 * @@ -46,7 +46,6 @@ require_once INSTALLDIR . '/lib/designsettings.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 GroupDesignSettingsAction extends DesignSettingsAction { var $group = null; @@ -59,12 +58,12 @@ class GroupDesignSettingsAction extends DesignSettingsAction * * @return boolean true */ - function prepare($args) { parent::prepare($args); if (!common_logged_in()) { + // TRANS: Client error displayed trying to change group design settings while not logged in. $this->clientError(_('You must be logged in to edit a group.')); return false; } @@ -81,6 +80,7 @@ class GroupDesignSettingsAction extends DesignSettingsAction } if (!$nickname) { + // TRANS: Client error displayed trying to change group design settings without providing a group nickname. $this->clientError(_('No nickname.'), 404); return false; } @@ -97,6 +97,7 @@ class GroupDesignSettingsAction extends DesignSettingsAction } if (!$this->group) { + // TRANS: Client error displayed trying to change group design settings while providing a nickname for a non-existing group. $this->clientError(_('No such group.'), 404); return false; } @@ -104,6 +105,7 @@ class GroupDesignSettingsAction extends DesignSettingsAction $cur = common_current_user(); if (!$cur->isAdmin($this->group)) { + // TRANS: Client error displayed trying to change group design settings without being a (group) admin. $this->clientError(_('You must be an admin to edit the group.'), 403); return false; } @@ -122,7 +124,6 @@ class GroupDesignSettingsAction extends DesignSettingsAction * * @return Design a design object to use */ - function getDesign() { @@ -141,6 +142,7 @@ class GroupDesignSettingsAction extends DesignSettingsAction function title() { + // TRANS: Title group design settings page. return _('Group design'); } @@ -149,9 +151,9 @@ class GroupDesignSettingsAction extends DesignSettingsAction * * @return instructions for use */ - function getInstructions() { + // TRANS: Instructions for group design settings page. return _('Customize the way your group looks ' . 'with a background image and a colour palette of your choice.'); } @@ -161,7 +163,6 @@ class GroupDesignSettingsAction extends DesignSettingsAction * * @return nothing */ - function showLocalNav() { $nav = new GroupNav($this, $this->group); @@ -173,7 +174,6 @@ class GroupDesignSettingsAction extends DesignSettingsAction * * @return Design */ - function getWorkingDesign() { $design = null; @@ -192,7 +192,6 @@ class GroupDesignSettingsAction extends DesignSettingsAction * * @return void */ - function showContent() { $design = $this->getWorkingDesign(); @@ -209,17 +208,14 @@ class GroupDesignSettingsAction extends DesignSettingsAction * * @return void */ - function saveDesign() { try { - $bgcolor = new WebColor($this->trimmed('design_background')); $ccolor = new WebColor($this->trimmed('design_content')); $sbcolor = new WebColor($this->trimmed('design_sidebar')); $tcolor = new WebColor($this->trimmed('design_text')); $lcolor = new WebColor($this->trimmed('design_links')); - } catch (WebColorException $e) { $this->showForm($e->getMessage()); return; @@ -246,7 +242,6 @@ class GroupDesignSettingsAction extends DesignSettingsAction $design = $this->group->getDesign(); if (!empty($design)) { - // update design $original = clone($design); @@ -263,12 +258,11 @@ class GroupDesignSettingsAction extends DesignSettingsAction if ($result === false) { common_log_db_error($design, 'UPDATE', __FILE__); - $this->showForm(_('Couldn\'t update your design.')); + // TRANS: Form validation error displayed when group design settings could not be updated because of an application issue. + $this->showForm(_('Unable to update your design settings.')); return; } - } else { - $this->group->query('BEGIN'); // save new design @@ -287,6 +281,7 @@ class GroupDesignSettingsAction extends DesignSettingsAction if (empty($id)) { common_log_db_error($id, 'INSERT', __FILE__); + // TRANS: Form validation error displayed when group design settings could not be saved because of an application issue. $this->showForm(_('Unable to save your design settings.')); return; } @@ -297,18 +292,18 @@ class GroupDesignSettingsAction extends DesignSettingsAction if (empty($result)) { common_log_db_error($original, 'UPDATE', __FILE__); + // TRANS: Form validation error displayed when group design settings could not be saved because of an application issue. $this->showForm(_('Unable to save your design settings.')); $this->group->query('ROLLBACK'); return; } $this->group->query('COMMIT'); - } $this->saveBackgroundImage($design); + // TRANS: Form text to confirm saved group design settings. $this->showForm(_('Design preferences saved.'), true); } - } diff --git a/actions/grouplogo.php b/actions/grouplogo.php index f414a23cc3..856c860a02 100644 --- a/actions/grouplogo.php +++ b/actions/grouplogo.php @@ -49,7 +49,6 @@ define('MAX_ORIGINAL', 480); * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class GrouplogoAction extends GroupDesignAction { var $mode = null; @@ -61,12 +60,12 @@ class GrouplogoAction extends GroupDesignAction /** * Prepare to run */ - function prepare($args) { parent::prepare($args); if (!common_logged_in()) { + // TRANS: Client error displayed when trying to create a group while not logged in. $this->clientError(_('You must be logged in to create a group.')); return false; } @@ -83,6 +82,7 @@ class GrouplogoAction extends GroupDesignAction } if (!$nickname) { + // TRANS: Client error displayed when trying to change group logo settings without providing a nickname. $this->clientError(_('No nickname.'), 404); return false; } @@ -99,6 +99,7 @@ class GrouplogoAction extends GroupDesignAction } if (!$this->group) { + // TRANS: Client error displayed when trying to update logo settings for a non-existing group. $this->clientError(_('No such group.'), 404); return false; } @@ -106,6 +107,7 @@ class GrouplogoAction extends GroupDesignAction $cur = common_current_user(); if (!$cur->isAdmin($this->group)) { + // TRANS: Client error displayed when trying to change group logo settings while not being a group admin. $this->clientError(_('You must be an admin to edit the group.'), 403); return false; } @@ -136,9 +138,9 @@ class GrouplogoAction extends GroupDesignAction * * @return string Title of the page */ - function title() { + // TRANS: Title for group logo settings page. return _('Group logo'); } @@ -147,9 +149,10 @@ class GrouplogoAction extends GroupDesignAction * * @return instructions for use */ - function getInstructions() { + // TRANS: Instructions for group logo page. + // TRANS: %s is the maximum file size for that site. return sprintf(_('You can upload a logo image for your group. The maximum file size is %s.'), ImageFile::maxFileSize()); } @@ -160,7 +163,6 @@ class GrouplogoAction extends GroupDesignAction * * @return void */ - function showContent() { if ($this->mode == 'crop') { @@ -178,6 +180,7 @@ 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.')); return; } @@ -192,6 +195,7 @@ class GrouplogoAction extends GroupDesignAction common_local_url('grouplogo', array('nickname' => $this->group->nickname)))); $this->elementStart('fieldset'); + // TRANS: Group logo form legend. $this->element('legend', null, _('Group logo')); $this->hidden('token', common_session_token()); @@ -199,6 +203,7 @@ class GrouplogoAction extends GroupDesignAction if ($original) { $this->elementStart('li', array('id' => 'avatar_original', 'class' => 'avatar_view')); + // TRANS: Uploaded original file in group logo form. $this->element('h2', null, _("Original")); $this->elementStart('div', array('id'=>'avatar_original_view')); $this->element('img', array('src' => $this->group->original_logo, @@ -210,6 +215,7 @@ class GrouplogoAction extends GroupDesignAction if ($this->group->homepage_logo) { $this->elementStart('li', array('id' => 'avatar_preview', 'class' => 'avatar_view')); + // TRANS: Header for preview of to be displayed group logo. $this->element('h2', null, _("Preview")); $this->elementStart('div', array('id'=>'avatar_preview_view')); $this->element('img', array('src' => $this->group->homepage_logo, @@ -221,25 +227,25 @@ class GrouplogoAction extends GroupDesignAction } $this->elementStart('li', array ('id' => 'settings_attach')); - $this->element('input', array('name' => 'avatarfile', - 'type' => 'file', - 'id' => 'avatarfile')); $this->element('input', array('name' => 'MAX_FILE_SIZE', 'type' => 'hidden', 'id' => 'MAX_FILE_SIZE', 'value' => ImageFile::maxFileSizeInt())); + $this->element('input', array('name' => 'avatarfile', + 'type' => 'file', + 'id' => 'avatarfile')); $this->elementEnd('li'); $this->elementEnd('ul'); $this->elementStart('ul', 'form_actions'); $this->elementStart('li'); + // TRANS: Submit button for uploading a group logo. $this->submit('upload', _('Upload')); $this->elementEnd('li'); $this->elementEnd('ul'); $this->elementEnd('fieldset'); $this->elementEnd('form'); - } function showCropForm() @@ -251,6 +257,7 @@ class GrouplogoAction extends GroupDesignAction common_local_url('grouplogo', array('nickname' => $this->group->nickname)))); $this->elementStart('fieldset'); + // TRANS: Legend for group logo settings fieldset. $this->element('legend', null, _('Avatar settings')); $this->hidden('token', common_session_token()); @@ -259,6 +266,7 @@ class GrouplogoAction extends GroupDesignAction $this->elementStart('li', array('id' => 'avatar_original', 'class' => 'avatar_view')); + // TRANS: Header for originally uploaded file before a crop on the group logo page. $this->element('h2', null, _("Original")); $this->elementStart('div', array('id'=>'avatar_original_view')); $this->element('img', array('src' => Avatar::url($this->filedata['filename']), @@ -271,6 +279,7 @@ class GrouplogoAction extends GroupDesignAction $this->elementStart('li', array('id' => 'avatar_preview', 'class' => 'avatar_view')); + // TRANS: Header for the cropped group logo on the group logo page. $this->element('h2', null, _("Preview")); $this->elementStart('div', array('id'=>'avatar_preview_view')); $this->element('img', array('src' => Avatar::url($this->filedata['filename']), @@ -286,13 +295,13 @@ class GrouplogoAction extends GroupDesignAction 'id' => $crop_info)); } + // TRANS: Button text for cropping an uploaded group logo. $this->submit('crop', _('Crop')); $this->elementEnd('li'); $this->elementEnd('ul'); $this->elementEnd('fieldset'); $this->elementEnd('form'); - } /** @@ -302,13 +311,13 @@ class GrouplogoAction extends GroupDesignAction * * @return void */ - function handlePost() { // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Form validation error message. $this->show_form(_('There was a problem with your session token. '. 'Try again, please.')); return; @@ -319,6 +328,7 @@ class GrouplogoAction extends GroupDesignAction } else if ($this->arg('crop')) { $this->cropLogo(); } else { + // TRANS: Form validation error message when an unsupported argument is used. $this->showForm(_('Unexpected form submission.')); } } @@ -331,7 +341,6 @@ class GrouplogoAction extends GroupDesignAction * * @return void */ - function uploadLogo() { try { @@ -341,20 +350,21 @@ class GrouplogoAction extends GroupDesignAction return; } + $type = $imagefile->preferredType(); $filename = Avatar::filename($this->group->id, - image_type_to_extension($imagefile->type), + image_type_to_extension($type), null, 'group-temp-'.common_timestamp()); $filepath = Avatar::path($filename); - move_uploaded_file($imagefile->filepath, $filepath); + $imagefile->copyTo($filepath); $filedata = array('filename' => $filename, 'filepath' => $filepath, 'width' => $imagefile->width, 'height' => $imagefile->height, - 'type' => $imagefile->type); + 'type' => $type); $_SESSION['FILEDATA'] = $filedata; @@ -362,6 +372,7 @@ class GrouplogoAction extends GroupDesignAction $this->mode = 'crop'; + // TRANS: Form instructions on the group logo page. $this->showForm(_('Pick a square area of the image to be the logo.'), true); } @@ -371,12 +382,12 @@ class GrouplogoAction extends GroupDesignAction * * @return void */ - function cropLogo() { $filedata = $_SESSION['FILEDATA']; if (!$filedata) { + // TRANS: Server error displayed trying to crop an uploaded group logo that is no longer present. $this->serverError(_('Lost our file data.')); return; } @@ -396,8 +407,10 @@ class GrouplogoAction extends GroupDesignAction @unlink($filedata['filepath']); unset($_SESSION['FILEDATA']); $this->mode = 'upload'; + // TRANS: Form success message after updating a group logo. $this->showForm(_('Logo updated.'), true); } else { + // TRANS: Form failure message after failing to update a group logo. $this->showForm(_('Failed updating logo.')); } } @@ -422,7 +435,6 @@ class GrouplogoAction extends GroupDesignAction * * @return void */ - function showStylesheets() { parent::showStylesheets(); @@ -434,7 +446,6 @@ class GrouplogoAction extends GroupDesignAction * * @return void */ - function showScripts() { parent::showScripts(); diff --git a/actions/groupmembers.php b/actions/groupmembers.php index d03d0b5a3a..7b1512dfab 100644 --- a/actions/groupmembers.php +++ b/actions/groupmembers.php @@ -43,7 +43,6 @@ require_once INSTALLDIR.'/lib/publicgroupnav.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 GroupmembersAction extends GroupDesignAction { var $page = null; @@ -73,6 +72,7 @@ class GroupmembersAction extends GroupDesignAction } if (!$nickname) { + // TRANS: Client error displayed when trying to view group members without providing a group nickname. $this->clientError(_('No nickname.'), 404); return false; } @@ -80,6 +80,7 @@ class GroupmembersAction extends GroupDesignAction $local = Local_group::staticGet('nickname', $nickname); if (!$local) { + // TRANS: Client error displayed when trying to view group members for a non-existing group. $this->clientError(_('No such group.'), 404); return false; } @@ -87,6 +88,7 @@ class GroupmembersAction extends GroupDesignAction $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { + // TRANS: Client error displayed when trying to view group members for an object that is not a group. $this->clientError(_('No such group.'), 404); return false; } @@ -119,6 +121,7 @@ class GroupmembersAction extends GroupDesignAction function showPageNotice() { $this->element('p', 'instructions', + // TRANS: Page notice for group members page. _('A list of the users in this group.')); } @@ -182,7 +185,8 @@ class GroupMemberListItem extends ProfileListItem { parent::showFullName(); if ($this->profile->isAdmin($this->group)) { - $this->out->text(' '); + $this->out->text(' '); // for separating the classes. + // TRANS: Indicator in group members list that this user is a group administrator. $this->out->element('span', 'role', _('Admin')); } } @@ -254,7 +258,7 @@ class GroupMemberListItem extends ProfileListItem /** * Fetch necessary return-to arguments for the profile forms * to return to this list when they're done. - * + * * @return array */ protected function returnToArgs() @@ -281,7 +285,6 @@ class GroupMemberListItem extends ProfileListItem * * @see BlockForm */ - class GroupBlockForm extends Form { /** @@ -310,7 +313,6 @@ class GroupBlockForm extends Form * @param User_group $group group to block user from * @param array $args return-to args */ - function __construct($out=null, $profile=null, $group=null, $args=null) { parent::__construct($out); @@ -325,7 +327,6 @@ class GroupBlockForm extends Form * * @return int ID of the form */ - function id() { // This should be unique for the page. @@ -337,7 +338,6 @@ class GroupBlockForm extends Form * * @return string class of the form */ - function formClass() { return 'form_group_block'; @@ -348,7 +348,6 @@ class GroupBlockForm extends Form * * @return string URL of the action */ - function action() { return common_local_url('groupblock'); @@ -361,6 +360,7 @@ class GroupBlockForm extends Form */ function formLegend() { + // TRANS: Form legend for form to block user from a group. $this->out->element('legend', null, _('Block user from group')); } @@ -369,7 +369,6 @@ class GroupBlockForm extends Form * * @return void */ - function formData() { $this->out->hidden('blockto-' . $this->profile->id, @@ -390,7 +389,6 @@ class GroupBlockForm extends Form * * @return void */ - function formActions() { $this->out->submit( @@ -414,25 +412,21 @@ class GroupBlockForm extends Form * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class MakeAdminForm extends Form { /** * Profile of user to block */ - var $profile = null; /** * Group to block the user from */ - var $group = null; /** * Return-to args */ - var $args = null; /** @@ -443,7 +437,6 @@ class MakeAdminForm extends Form * @param User_group $group group to block user from * @param array $args return-to args */ - function __construct($out=null, $profile=null, $group=null, $args=null) { parent::__construct($out); @@ -458,7 +451,6 @@ class MakeAdminForm extends Form * * @return int ID of the form */ - function id() { // This should be unique for the page. @@ -470,7 +462,6 @@ class MakeAdminForm extends Form * * @return string class of the form */ - function formClass() { return 'form_make_admin'; @@ -481,7 +472,6 @@ class MakeAdminForm extends Form * * @return string URL of the action */ - function action() { return common_local_url('makeadmin', array('nickname' => $this->group->nickname)); @@ -492,9 +482,9 @@ class MakeAdminForm extends Form * * @return void */ - function formLegend() { + // TRANS: Form legend for form to make a user a group admin. $this->out->element('legend', null, _('Make user an admin of the group')); } @@ -503,7 +493,6 @@ class MakeAdminForm extends Form * * @return void */ - function formData() { $this->out->hidden('profileid-' . $this->profile->id, @@ -524,7 +513,6 @@ class MakeAdminForm extends Form * * @return void */ - function formActions() { $this->out->submit( diff --git a/actions/grouprss.php b/actions/grouprss.php index 98fdea38de..39dcff83d9 100644 --- a/actions/grouprss.php +++ b/actions/grouprss.php @@ -45,7 +45,6 @@ define('MEMBERS_PER_SECTION', 27); * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class groupRssAction extends Rss10Action { /** group we're viewing. */ @@ -56,7 +55,6 @@ class groupRssAction extends Rss10Action * * @return boolean true */ - function isReadOnly($args) { return true; @@ -71,7 +69,6 @@ class groupRssAction extends Rss10Action * * @return boolean success flag */ - function prepare($args) { parent::prepare($args); @@ -88,6 +85,7 @@ class groupRssAction extends Rss10Action } if (!$nickname) { + // TRANS: Client error displayed when requesting a group RSS feed without providing a group nickname. $this->clientError(_('No nickname.'), 404); return false; } @@ -95,6 +93,7 @@ class groupRssAction extends Rss10Action $local = Local_group::staticGet('nickname', $nickname); if (!$local) { + // TRANS: Client error displayed when requesting a group RSS feed for group that does not exist. $this->clientError(_('No such group.'), 404); return false; } @@ -102,6 +101,7 @@ class groupRssAction extends Rss10Action $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { + // TRANS: Client error displayed when requesting a group RSS feed for an object that is not a group. $this->clientError(_('No such group.'), 404); return false; } @@ -112,7 +112,6 @@ class groupRssAction extends Rss10Action function getNotices($limit=0) { - $group = $this->group; if (is_null($group)) { diff --git a/actions/groups.php b/actions/groups.php index 8aacff8b0e..958c5921bf 100644 --- a/actions/groups.php +++ b/actions/groups.php @@ -45,7 +45,6 @@ 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 GroupsAction extends Action { var $page = null; @@ -59,9 +58,12 @@ class GroupsAction extends Action function title() { if ($this->page == 1) { - return _("Groups"); + // TRANS: Title for first page of the groups list. + return _m('TITLE',"Groups"); } else { - return sprintf(_("Groups, page %d"), $this->page); + // TRANS: Title for all but the first page of the groups list. + // TRANS: %d is the page number. + return sprintf(_m('TITLE',"Groups, page %d"), $this->page); } } @@ -87,12 +89,15 @@ class GroupsAction extends Action function showPageNotice() { $notice = + // TRANS: Page notice of group list. %%%%site.name%%%% is the StatusNet site name, + // TRANS: %%%%action.groupsearch%%%% and %%%%action.newgroup%%%% are URLs. Do not change them. + // TRANS: This message contains Markdown links in the form [link text](link). sprintf(_('%%%%site.name%%%% groups let you find and talk with ' . 'people of similar interests. After you join a group ' . 'you can send messages to all other members using the ' . 'syntax "!groupname". Don\'t see a group you like? Try ' . '[searching for one](%%%%action.groupsearch%%%%) or ' . - '[start your own!](%%%%action.newgroup%%%%)')); + '[start your own](%%%%action.newgroup%%%%)!')); $this->elementStart('div', 'instructions'); $this->raw(common_markup_to_html($notice)); $this->elementEnd('div'); @@ -104,6 +109,7 @@ class GroupsAction extends Action $this->elementStart('p', array('id' => 'new_group')); $this->element('a', array('href' => common_local_url('newgroup'), 'class' => 'more'), + // TRANS: Link to create a new group on the group list page. _('Create a new group')); $this->elementEnd('p'); } diff --git a/actions/groupsearch.php b/actions/groupsearch.php index 55f4cee625..fce5c2b16a 100644 --- a/actions/groupsearch.php +++ b/actions/groupsearch.php @@ -49,12 +49,14 @@ class GroupsearchAction extends SearchAction { function getInstructions() { + // TRANS: Instructions for page where groups can be searched. %%site.name%% is the name of the StatusNet site. return _('Search for groups on %%site.name%% by their name, location, or description. ' . 'Separate the terms by spaces; they must be 3 characters or more.'); } function title() { + // TRANS: Title for page where groups can be searched. return _('Group search'); } @@ -76,12 +78,17 @@ class GroupsearchAction extends SearchAction $this->pagination($page > 1, $cnt > GROUPS_PER_PAGE, $page, 'groupsearch', array('q' => $q)); } else { + // TRANS: Text on page where groups can be searched if no results were found for a query. $this->element('p', 'error', _('No results.')); $this->searchSuggestions($q); if (common_logged_in()) { - $message = _('If you can\'t find the group you\'re looking for, you can [create it](%%action.newgroup%%) yourself.'); + // TRANS: Additional text on page where groups can be searched if no results were found for a query for a logged in user. + // TRANS: This message contains Markdown links in the form [link text](link). + $message = _('If you cannot find the group you\'re looking for, you can [create it](%%action.newgroup%%) yourself.'); } else { + // TRANS: Additional text on page where groups can be searched if no results were found for a query for a not logged in user. + // TRANS: This message contains Markdown links in the form [link text](link). $message = _('Why not [register an account](%%action.register%%) and [create the group](%%action.newgroup%%) yourself!'); } $this->elementStart('div', 'guide'); @@ -116,4 +123,3 @@ class GroupSearchResults extends GroupList return preg_replace($this->pattern, '\\1', htmlspecialchars($text)); } } - diff --git a/actions/groupunblock.php b/actions/groupunblock.php index dd6b15c26c..de0af59821 100644 --- a/actions/groupunblock.php +++ b/actions/groupunblock.php @@ -32,7 +32,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { } /** - * Unlock a user from a group + * Unblock a user from a group * * @category Action * @package StatusNet @@ -40,7 +40,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class GroupunblockAction extends Action { var $profile = null; @@ -53,11 +52,11 @@ class GroupunblockAction extends Action * * @return boolean success flag */ - function prepare($args) { parent::prepare($args); if (!common_logged_in()) { + // TRANS: Client error displayed when trying to unblock a user from a group while not logged in. $this->clientError(_('Not logged in.')); return false; } @@ -68,30 +67,36 @@ class GroupunblockAction extends Action } $id = $this->trimmed('unblockto'); if (empty($id)) { + // TRANS: Client error displayed when trying to unblock a user from a group without providing a profile. $this->clientError(_('No profile specified.')); return false; } $this->profile = Profile::staticGet('id', $id); if (empty($this->profile)) { + // TRANS: Client error displayed when trying to unblock a user from a group without providing an existing profile. $this->clientError(_('No profile with that ID.')); return false; } $group_id = $this->trimmed('unblockgroup'); if (empty($group_id)) { + // TRANS: Client error displayed when trying to unblock a user from a group without providing a group. $this->clientError(_('No group specified.')); return false; } $this->group = User_group::staticGet('id', $group_id); if (empty($this->group)) { + // TRANS: Client error displayed when trying to unblock a user from a non-existing group. $this->clientError(_('No such group.')); return false; } $user = common_current_user(); if (!$user->isAdmin($this->group)) { + // TRANS: Client error displayed when trying to unblock a user from a group without being an administrator for the group. $this->clientError(_('Only an admin can unblock group members.'), 401); return false; } if (!Group_block::isBlocked($this->group, $this->profile)) { + // TRANS: Client error displayed when trying to unblock a non-blocked user from a group. $this->clientError(_('User is not blocked from group.')); return false; } @@ -105,7 +110,6 @@ class GroupunblockAction extends Action * * @return void */ - function handle($args) { parent::handle($args); @@ -119,12 +123,12 @@ class GroupunblockAction extends Action * * @return void */ - function unblockProfile() { $result = Group_block::unblockProfile($this->group, $this->profile); if (!$result) { + // TRANS: Server error displayed when unblocking a user from a group fails because of an unknown error. $this->serverError(_('Error removing the block.')); return; } @@ -146,4 +150,3 @@ class GroupunblockAction extends Action } } } - diff --git a/actions/hcard.php b/actions/hcard.php index 55d0f65c8f..8781f6f882 100644 --- a/actions/hcard.php +++ b/actions/hcard.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 * @link http://status.net/ */ - class HcardAction extends Action { var $user; @@ -64,6 +63,7 @@ class HcardAction extends Action $this->user = User::staticGet('nickname', $nickname); if (!$this->user) { + // TRANS: Client error displayed when trying to get a user hCard for a non-existing user. $this->clientError(_('No such user.'), 404); return false; } @@ -71,6 +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. $this->serverError(_('User has no profile.')); return false; } @@ -117,4 +118,4 @@ class ShortUserProfile extends UserProfile { return; } -} \ No newline at end of file +} diff --git a/plugins/OStatus/actions/hostmeta.php b/actions/hostmeta.php similarity index 63% rename from plugins/OStatus/actions/hostmeta.php rename to actions/hostmeta.php index 14f69ac192..7093a441d7 100644 --- a/plugins/OStatus/actions/hostmeta.php +++ b/actions/hostmeta.php @@ -18,29 +18,46 @@ */ /** - * @package OStatusPlugin + * @category Action + * @package StatusNet * @maintainer James Walker + * @author Craig Andrews */ if (!defined('STATUSNET')) { exit(1); } +// @todo XXX: Add documentation. class HostMetaAction extends Action { + /** + * Is read only? + * + * @return boolean true + */ + function isReadOnly() + { + return true; + } + function handle() { parent::handle(); $domain = common_config('site', 'server'); - $url = common_local_url('userxrd'); - $url.= '?uri={uri}'; $xrd = new XRD(); $xrd->host = $domain; - $xrd->links[] = array('rel' => Discovery::LRDD_REL, - 'template' => $url, - 'title' => array('Resource Descriptor')); + + if(Event::handle('StartHostMetaLinks', array(&$xrd->links))) { + $url = common_local_url('userxrd'); + $url.= '?uri={uri}'; + $xrd->links[] = array('rel' => Discovery::LRDD_REL, + 'template' => $url, + 'title' => array('Resource Descriptor')); + Event::handle('EndHostMetaLinks', array(&$xrd->links)); + } header('Content-type: application/xrd+xml'); print $xrd->toXML(); diff --git a/actions/imsettings.php b/actions/imsettings.php index 29cbeb82e3..eb3f0cfd15 100644 --- a/actions/imsettings.php +++ b/actions/imsettings.php @@ -45,7 +45,6 @@ require_once INSTALLDIR.'/lib/jabber.php'; * * @see SettingsAction */ - class ImsettingsAction extends ConnectSettingsAction { /** @@ -53,10 +52,9 @@ class ImsettingsAction extends ConnectSettingsAction * * @return string Title of the page */ - function title() { - // TRANS: Title for instance messaging settings. + // TRANS: Title for Instant Messaging settings. return _('IM settings'); } @@ -65,14 +63,13 @@ class ImsettingsAction extends ConnectSettingsAction * * @return instructions for use */ - function getInstructions() { // TRANS: Instant messaging settings page instructions. // TRANS: [instant messages] is link text, "(%%doc.im%%)" is the link. // TRANS: the order and formatting of link text and link should remain unchanged. return _('You can send and receive notices through '. - 'Jabber/GTalk [instant messages](%%doc.im%%). '. + 'Jabber/Google Talk [instant messages](%%doc.im%%). '. 'Configure your address and settings below.'); } @@ -85,12 +82,11 @@ class ImsettingsAction extends ConnectSettingsAction * * @return void */ - function showContent() { if (!common_config('xmpp', 'enabled')) { $this->element('div', array('class' => 'error'), - // TRANS: Message given in the IM settings if XMPP is not enabled on the site. + // TRANS: Message given in the Instant Messaging settings if XMPP is not enabled on the site. _('IM is not available.')); return; } @@ -102,88 +98,88 @@ class ImsettingsAction extends ConnectSettingsAction 'action' => common_local_url('imsettings'))); $this->elementStart('fieldset', array('id' => 'settings_im_address')); - // TRANS: Form legend for IM settings form. + // TRANS: Form legend for Instant Messaging settings form. $this->element('legend', null, _('IM address')); $this->hidden('token', common_session_token()); if ($user->jabber) { $this->element('p', 'form_confirmed', $user->jabber); - // TRANS: Form note in IM settings form. + // TRANS: Form note in Instant Messaging settings form. $this->element('p', 'form_note', - _('Current confirmed Jabber/GTalk address.')); + _('Current confirmed Jabber/Google Talk address.')); $this->hidden('jabber', $user->jabber); - // TRANS: Button label to remove a confirmed IM address. + // TRANS: Button label to remove a confirmed Instant Messaging address. $this->submit('remove', _m('BUTTON','Remove')); } else { $confirm = $this->getConfirmation(); if ($confirm) { $this->element('p', 'form_unconfirmed', $confirm->address); $this->element('p', 'form_note', - // TRANS: Form note in IM settings form. - // TRANS: %s is the IM address set for the site. + // TRANS: Form note in Instant Messaging settings form. + // TRANS: %s is the Instant Messaging address set for the site. sprintf(_('Awaiting confirmation on this address. '. - 'Check your Jabber/GTalk account for a '. + 'Check your Jabber/Google Talk account for a '. 'message with further instructions. '. '(Did you add %s to your buddy list?)'), jabber_daemon_address())); $this->hidden('jabber', $confirm->address); - // TRANS: Button label to cancel an IM address confirmation procedure. + // TRANS: Button label to cancel an Instant Messaging address confirmation procedure. $this->submit('cancel', _m('BUTTON','Cancel')); } else { $this->elementStart('ul', 'form_data'); $this->elementStart('li'); - // TRANS: Field label for IM address input in IM settings form. + // TRANS: Field label for Instant Messaging address input in Instant Messaging settings form. $this->input('jabber', _('IM address'), ($this->arg('jabber')) ? $this->arg('jabber') : null, - // TRANS: IM address input field instructions in IM settings form. - // TRANS: %s is the IM address set for the site. + // TRANS: IM address input field instructions in Instant Messaging settings form. + // TRANS: %s is the Instant Messaging address set for the site. // TRANS: Do not translate "example.org". It is one of the domain names reserved for use in examples by // TRANS: http://www.rfc-editor.org/rfc/rfc2606.txt. Any other domain may be owned by a legitimate // TRANS: person or organization. - sprintf(_('Jabber or GTalk address, '. + sprintf(_('Jabber or Google Talk address, '. 'like "UserName@example.org". '. 'First, make sure to add %s to your '. - 'buddy list in your IM client or on GTalk.'), + 'buddy list in your IM client or on Google Talk.'), jabber_daemon_address())); $this->elementEnd('li'); $this->elementEnd('ul'); - // TRANS: Button label for adding an IM address in IM settings form. + // TRANS: Button label for adding an Instant Messaging address in Instant Messaging settings form. $this->submit('add', _m('BUTTON','Add')); } } $this->elementEnd('fieldset'); - + $this->elementStart('fieldset', array('id' => 'settings_im_preferences')); - // TRANS: Form legend for IM preferences form. + // TRANS: Form legend for Instant Messaging preferences form. $this->element('legend', null, _('IM preferences')); $this->elementStart('ul', 'form_data'); $this->elementStart('li'); $this->checkbox('jabbernotify', - // TRANS: Checkbox label in IM preferences form. - _('Send me notices through Jabber/GTalk.'), + // TRANS: Checkbox label in Instant Messaging preferences form. + _('Send me notices through Jabber/Google Talk.'), $user->jabbernotify); $this->elementEnd('li'); $this->elementStart('li'); $this->checkbox('updatefrompresence', - // TRANS: Checkbox label in IM preferences form. - _('Post a notice when my Jabber/GTalk status changes.'), + // TRANS: Checkbox label in Instant Messaging preferences form. + _('Post a notice when my Jabber/Google Talk status changes.'), $user->updatefrompresence); $this->elementEnd('li'); $this->elementStart('li'); $this->checkbox('jabberreplies', - // TRANS: Checkbox label in IM preferences form. - _('Send me replies through Jabber/GTalk '. + // TRANS: Checkbox label in Instant Messaging preferences form. + _('Send me replies through Jabber/Google Talk '. 'from people I\'m not subscribed to.'), $user->jabberreplies); $this->elementEnd('li'); $this->elementStart('li'); $this->checkbox('jabbermicroid', - // TRANS: Checkbox label in IM preferences form. - _('Publish a MicroID for my Jabber/GTalk address.'), + // TRANS: Checkbox label in Instant Messaging preferences form. + _('Publish a MicroID for my Jabber/Google Talk address.'), $user->jabbermicroid); $this->elementEnd('li'); $this->elementEnd('ul'); - // TRANS: Button label to save IM preferences. + // TRANS: Button label to save Instant Messaging preferences. $this->submit('save', _m('BUTTON','Save')); $this->elementEnd('fieldset'); $this->elementEnd('form'); @@ -194,7 +190,6 @@ class ImsettingsAction extends ConnectSettingsAction * * @return Confirm_address address object for this user */ - function getConfirmation() { $user = common_current_user(); @@ -221,7 +216,6 @@ class ImsettingsAction extends ConnectSettingsAction * * @return void */ - function handlePost() { // CSRF protection @@ -241,7 +235,7 @@ class ImsettingsAction extends ConnectSettingsAction } else if ($this->arg('remove')) { $this->removeAddress(); } else { - // TRANS: Message given submitting a form with an unknown action in IM settings. + // TRANS: Message given submitting a form with an unknown action in Instant Messaging settings. $this->showForm(_('Unexpected form submission.')); } } @@ -254,7 +248,6 @@ class ImsettingsAction extends ConnectSettingsAction * * @return void */ - function savePreferences() { $jabbernotify = $this->boolean('jabbernotify'); @@ -279,14 +272,14 @@ class ImsettingsAction extends ConnectSettingsAction if ($result === false) { common_log_db_error($user, 'UPDATE', __FILE__); - // TRANS: Server error thrown on database error updating IM preferences. - $this->serverError(_('Couldn\'t update user.')); + // TRANS: Server error thrown on database error updating Instant Messaging preferences. + $this->serverError(_('Could not update user.')); return; } $user->query('COMMIT'); - // TRANS: Confirmation message for successful IM preferences save. + // TRANS: Confirmation message for successful Instant Messaging preferences save. $this->showForm(_('Preferences saved.'), true); } @@ -298,7 +291,6 @@ class ImsettingsAction extends ConnectSettingsAction * * @return void */ - function addAddress() { $user = common_current_user(); @@ -308,7 +300,7 @@ class ImsettingsAction extends ConnectSettingsAction // Some validation if (!$jabber) { - // TRANS: Message given saving IM address without having provided one. + // TRANS: Message given saving Instant Messaging address without having provided one. $this->showForm(_('No Jabber ID.')); return; } @@ -316,20 +308,20 @@ class ImsettingsAction extends ConnectSettingsAction $jabber = jabber_normalize_jid($jabber); if (!$jabber) { - // TRANS: Message given saving IM address that cannot be normalised. - $this->showForm(_('Cannot normalize that Jabber ID')); + // TRANS: Message given saving Instant Messaging address that cannot be normalised. + $this->showForm(_('Cannot normalize that Jabber ID.')); return; } if (!jabber_valid_base_jid($jabber, common_config('email', 'domain_check'))) { - // TRANS: Message given saving IM address that not valid. - $this->showForm(_('Not a valid Jabber ID')); + // TRANS: Message given saving Instant Messaging address that not valid. + $this->showForm(_('Not a valid Jabber ID.')); return; } else if ($user->jabber == $jabber) { - // TRANS: Message given saving IM address that is already set. + // TRANS: Message given saving Instant Messaging address that is already set. $this->showForm(_('That is already your Jabber ID.')); return; } else if ($this->jabberExists($jabber)) { - // TRANS: Message given saving IM address that is already set for another user. + // TRANS: Message given saving Instant Messaging address that is already set for another user. $this->showForm(_('Jabber ID already belongs to another user.')); return; } @@ -347,8 +339,8 @@ class ImsettingsAction extends ConnectSettingsAction if ($result === false) { common_log_db_error($confirm, 'INSERT', __FILE__); - // TRANS: Server error thrown on database error adding IM confirmation code. - $this->serverError(_('Couldn\'t insert confirmation code.')); + // TRANS: Server error thrown on database error adding Instant Messaging confirmation code. + $this->serverError(_('Could not insert confirmation code.')); return; } @@ -356,8 +348,8 @@ class ImsettingsAction extends ConnectSettingsAction $user->nickname, $jabber); - // TRANS: Message given saving valid IM address that is to be confirmed. - // TRANS: %s is the IM address set for the site. + // TRANS: Message given saving valid Instant Messaging address that is to be confirmed. + // TRANS: %s is the Instant Messaging address set for the site. $msg = sprintf(_('A confirmation code was sent '. 'to the IM address you added. '. 'You must approve %s for '. @@ -374,7 +366,6 @@ class ImsettingsAction extends ConnectSettingsAction * * @return void */ - function cancelConfirmation() { $jabber = $this->arg('jabber'); @@ -382,12 +373,12 @@ class ImsettingsAction extends ConnectSettingsAction $confirm = $this->getConfirmation(); if (!$confirm) { - // TRANS: Message given canceling IM address confirmation that is not pending. + // TRANS: Message given canceling Instant Messaging address confirmation that is not pending. $this->showForm(_('No pending confirmation to cancel.')); return; } if ($confirm->address != $jabber) { - // TRANS: Message given canceling IM address confirmation for the wrong IM address. + // TRANS: Message given canceling Instant Messaging address confirmation for the wrong IM address. $this->showForm(_('That is the wrong IM address.')); return; } @@ -396,12 +387,12 @@ class ImsettingsAction extends ConnectSettingsAction if (!$result) { common_log_db_error($confirm, 'DELETE', __FILE__); - // TRANS: Server error thrown on database error canceling IM address confirmation. - $this->serverError(_('Couldn\'t delete IM confirmation.')); + // TRANS: Server error thrown on database error canceling Instant Messaging address confirmation. + $this->serverError(_('Could not delete IM confirmation.')); return; } - // TRANS: Message given after successfully canceling IM address confirmation. + // TRANS: Message given after successfully canceling Instant Messaging address confirmation. $this->showForm(_('IM confirmation cancelled.'), true); } @@ -412,7 +403,6 @@ class ImsettingsAction extends ConnectSettingsAction * * @return void */ - function removeAddress() { $user = common_current_user(); @@ -422,7 +412,7 @@ class ImsettingsAction extends ConnectSettingsAction // Maybe an old tab open...? if ($user->jabber != $jabber) { - // TRANS: Message given trying to remove an IM address that is not + // TRANS: Message given trying to remove an Instant Messaging address that is not // TRANS: registered for the active user. $this->showForm(_('That is not your Jabber ID.')); return; @@ -438,15 +428,15 @@ class ImsettingsAction extends ConnectSettingsAction if (!$result) { common_log_db_error($user, 'UPDATE', __FILE__); - // TRANS: Server error thrown on database error removing a registered IM address. - $this->serverError(_('Couldn\'t update user.')); + // TRANS: Server error thrown on database error removing a registered Instant Messaging address. + $this->serverError(_('Could not update user.')); return; } $user->query('COMMIT'); // XXX: unsubscribe to the old address - // TRANS: Message given after successfully removing a registered IM address. + // TRANS: Message given after successfully removing a registered Instant Messaging address. $this->showForm(_('The IM address was removed.'), true); } @@ -459,7 +449,6 @@ class ImsettingsAction extends ConnectSettingsAction * * @return boolean whether the Jabber ID exists */ - function jabberExists($jabber) { $user = common_current_user(); diff --git a/actions/inbox.php b/actions/inbox.php index 8330f753ff..6ab58f9751 100644 --- a/actions/inbox.php +++ b/actions/inbox.php @@ -43,7 +43,6 @@ require_once INSTALLDIR.'/lib/mailbox.php'; * @link http://status.net/ * @see MailboxAction */ - class InboxAction extends MailboxAction { @@ -52,13 +51,16 @@ class InboxAction extends MailboxAction * * @return string page title */ - function title() { if ($this->page > 1) { + // TRANS: Title for all but the first page of the inbox page. + // TRANS: %1$s is the user's nickname, %2$s is the page number. return sprintf(_('Inbox for %1$s - page %2$d'), $this->user->nickname, $this->page); } else { + // TRANS: Title for the first page of the inbox page. + // TRANS: %s is the user's nickname. return sprintf(_('Inbox for %s'), $this->user->nickname); } } @@ -72,7 +74,6 @@ class InboxAction extends MailboxAction * * @see MailboxAction::getMessages() */ - function getMessages() { $message = new Message(); @@ -89,19 +90,9 @@ class InboxAction extends MailboxAction } } - /** - * Returns the profile we want to show with the message - * - * For inboxes, we show the sender; for outboxes, the recipient. - * - * @param Message $message The message to get the profile for - * - * @return Profile The profile that matches the message - */ - - function getMessageProfile($message) + function getMessageList($message) { - return $message->getFrom(); + return new InboxMessageList($this, $message); } /** @@ -109,9 +100,30 @@ class InboxAction extends MailboxAction * * @return string localised instructions for using the page */ - function getInstructions() { + // TRANS: Instructions for user inbox page. return _('This is your inbox, which lists your incoming private messages.'); } } + +class InboxMessageList extends MessageList +{ + function newItem($message) + { + return new InboxMessageListItem($this->out, $message); + } +} + +class InboxMessageListItem extends MessageListItem +{ + /** + * Returns the profile we want to show with the message + * + * @return Profile The profile that matches the message + */ + function getMessageProfile() + { + return $this->message->getFrom(); + } +} \ No newline at end of file diff --git a/actions/invite.php b/actions/invite.php index 2779437e0c..a2a0e0714a 100644 --- a/actions/invite.php +++ b/actions/invite.php @@ -19,6 +19,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +// @todo XXX: Add documentation. class InviteAction extends CurrentUserDesignAction { var $mode = null; @@ -142,7 +143,7 @@ class InviteAction extends CurrentUserDesignAction $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(_('%1$s (%2$s)'), $other->nickname, $other->email)); + $this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email)); } $this->elementEnd('ul'); } @@ -156,7 +157,7 @@ class InviteAction extends CurrentUserDesignAction $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(_('%1$s (%2$s)'), $other->nickname, $other->email)); + $this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email)); } $this->elementEnd('ul'); } @@ -217,7 +218,7 @@ class InviteAction extends CurrentUserDesignAction $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)')); + _('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. diff --git a/actions/joingroup.php b/actions/joingroup.php index f87e5dae29..7beaf4f64a 100644 --- a/actions/joingroup.php +++ b/actions/joingroup.php @@ -43,7 +43,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 JoingroupAction extends Action { var $group = null; @@ -51,12 +50,12 @@ class JoingroupAction extends Action /** * Prepare to run */ - function prepare($args) { parent::prepare($args); if (!common_logged_in()) { + // TRANS: Client error displayed when trying to join a group while not logged in. $this->clientError(_('You must be logged in to join a group.')); return false; } @@ -79,17 +78,20 @@ class JoingroupAction extends Action $local = Local_group::staticGet('nickname', $nickname); if (!$local) { + // TRANS: Client error displayed when trying to join a non-local group. $this->clientError(_('No such group.'), 404); return false; } $this->group = User_group::staticGet('id', $local->group_id); } else { + // TRANS: Client error displayed when trying to join a group without providing a group name or group ID. $this->clientError(_('No nickname or ID.'), 404); return false; } if (!$this->group) { + // TRANS: Client error displayed when trying to join a non-existing group. $this->clientError(_('No such group.'), 404); return false; } @@ -97,11 +99,13 @@ class JoingroupAction extends Action $cur = common_current_user(); if ($cur->isMember($this->group)) { + // TRANS: Client error displayed when trying to join a group while already a member. $this->clientError(_('You are already a member of that group.'), 403); return false; } if (Group_block::isBlocked($this->group, $cur->getProfile())) { + // TRANS: Client error displayed when trying to join a group while being blocked form joining it. $this->clientError(_('You have been blocked from that group by the admin.'), 403); return false; } @@ -118,7 +122,6 @@ class JoingroupAction extends Action * * @return void */ - function handle($args) { parent::handle($args); @@ -131,6 +134,8 @@ class JoingroupAction extends Action Event::handle('EndJoinGroup', array($this->group, $cur)); } } catch (Exception $e) { + // TRANS: Server error displayed when joining a group failed in the database. + // TRANS: %1$s is the joining user's nickname, $2$s is the group nickname for which the join failed. $this->serverError(sprintf(_('Could not join user %1$s to group %2$s.'), $cur->nickname, $this->group->nickname)); } @@ -138,7 +143,8 @@ class JoingroupAction extends Action if ($this->boolean('ajax')) { $this->startHTML('text/xml;charset=utf-8'); $this->elementStart('head'); - $this->element('title', null, sprintf(_('%1$s joined group %2$s'), + // TRANS: Title for join group page after joining. + $this->element('title', null, sprintf(_m('TITLE','%1$s joined group %2$s'), $cur->nickname, $this->group->nickname)); $this->elementEnd('head'); @@ -153,4 +159,4 @@ class JoingroupAction extends Action 303); } } -} \ No newline at end of file +} diff --git a/actions/leavegroup.php b/actions/leavegroup.php index 329b5aafe1..f5d1ccd08c 100644 --- a/actions/leavegroup.php +++ b/actions/leavegroup.php @@ -43,7 +43,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 LeavegroupAction extends Action { var $group = null; @@ -51,12 +50,12 @@ class LeavegroupAction extends Action /** * Prepare to run */ - function prepare($args) { parent::prepare($args); if (!common_logged_in()) { + // TRANS: Client error displayed when trying to leave a group while not logged in. $this->clientError(_('You must be logged in to leave a group.')); return false; } @@ -79,17 +78,20 @@ class LeavegroupAction extends Action $local = Local_group::staticGet('nickname', $nickname); if (!$local) { + // TRANS: Client error displayed when trying to leave a non-local group. $this->clientError(_('No such group.'), 404); return false; } $this->group = User_group::staticGet('id', $local->group_id); } else { + // TRANS: Client error displayed when trying to leave a group without providing a group name or group ID. $this->clientError(_('No nickname or ID.'), 404); return false; } if (!$this->group) { + // TRANS: Client error displayed when trying to leave a non-existing group. $this->clientError(_('No such group.'), 404); return false; } @@ -97,6 +99,7 @@ class LeavegroupAction extends Action $cur = common_current_user(); if (!$cur->isMember($this->group)) { + // TRANS: Client error displayed when trying to join a group while already a member. $this->clientError(_('You are not a member of that group.'), 403); return false; } @@ -113,7 +116,6 @@ class LeavegroupAction extends Action * * @return void */ - function handle($args) { parent::handle($args); @@ -126,6 +128,8 @@ class LeavegroupAction extends Action Event::handle('EndLeaveGroup', array($this->group, $cur)); } } catch (Exception $e) { + // TRANS: Server error displayed when leaving a group failed in the database. + // 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 remove user %1$s from group %2$s.'), $cur->nickname, $this->group->nickname)); return; @@ -134,7 +138,8 @@ class LeavegroupAction extends Action if ($this->boolean('ajax')) { $this->startHTML('text/xml;charset=utf-8'); $this->elementStart('head'); - $this->element('title', null, sprintf(_('%1$s left group %2$s'), + // TRANS: Title for leave group page after leaving. + $this->element('title', null, sprintf(_m('TITLE','%1$s left group %2$s'), $cur->nickname, $this->group->nickname)); $this->elementEnd('head'); diff --git a/actions/licenseadminpanel.php b/actions/licenseadminpanel.php index 9165ca19d9..4adeb5c3c6 100644 --- a/actions/licenseadminpanel.php +++ b/actions/licenseadminpanel.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 LicenseadminpanelAction extends AdminPanelAction { @@ -61,7 +60,6 @@ class LicenseadminpanelAction extends AdminPanelAction * * @return string instructions */ - function getInstructions() { return _('License for this StatusNet site'); @@ -72,7 +70,6 @@ class LicenseadminpanelAction extends AdminPanelAction * * @return void */ - function showForm() { $form = new LicenseAdminPanelForm($this); @@ -85,7 +82,6 @@ class LicenseadminpanelAction extends AdminPanelAction * * @return void */ - function saveSettings() { static $settings = array( @@ -128,7 +124,6 @@ class LicenseadminpanelAction extends AdminPanelAction * * @return nothing */ - function validate(&$values) { // Validate license type (shouldn't have to do it, but just in case) @@ -153,7 +148,7 @@ class LicenseadminpanelAction extends AdminPanelAction // Make sure the license title is not too long if (mb_strlen($values['license']['type']) > 255) { $this->clientError( - _("Invalid license title. Max length is 255 characters.") + _('Invalid license title. Maximum length is 255 characters.') ); } @@ -197,7 +192,6 @@ class LicenseAdminPanelForm extends AdminForm * * @return int ID of the form */ - function id() { return 'licenseadminpanel'; @@ -208,7 +202,6 @@ class LicenseAdminPanelForm extends AdminForm * * @return string class of the form */ - function formClass() { return 'form_settings'; @@ -312,7 +305,6 @@ class LicenseAdminPanelForm extends AdminForm * * @return void */ - function formActions() { $this->out->submit( diff --git a/actions/newapplication.php b/actions/newapplication.php index 033c0852d9..eb13593536 100644 --- a/actions/newapplication.php +++ b/actions/newapplication.php @@ -55,7 +55,6 @@ class NewApplicationAction extends OwnerDesignAction /** * Prepare to run */ - function prepare($args) { parent::prepare($args); @@ -78,7 +77,6 @@ class NewApplicationAction extends OwnerDesignAction * * @return void */ - function handle($args) { parent::handle($args); @@ -122,6 +120,7 @@ class NewApplicationAction extends OwnerDesignAction } elseif ($this->arg('save')) { $this->trySave(); } else { + // TRANS: Client error displayed when encountering an unexpected action on form submission. $this->clientError(_('Unexpected form submission.')); } } @@ -144,6 +143,7 @@ class NewApplicationAction extends OwnerDesignAction $this->element('p', 'error', $this->msg); } else { $this->element('p', 'instructions', + // TRANS: Form instructions for registering a new application. _('Use this form to register a new application.')); } } @@ -160,15 +160,19 @@ class NewApplicationAction extends OwnerDesignAction $access_type = $this->arg('default_access_type'); if (empty($name)) { + // TRANS: Validation error shown when not providing a name in the "New application" form. $this->showForm(_('Name is required.')); return; } else if ($this->nameExists($name)) { + // TRANS: Validation error shown when providing a name for an application that already exists in the "New application" form. $this->showForm(_('Name already in use. Try another one.')); return; } elseif (mb_strlen($name) > 255) { - $this->showForm(_('Name is too long (maximum 255 chars).')); + // TRANS: Validation error shown when providing too long a name in the "New application" form. + $this->showForm(_('Name is too long (maximum 255 characters).')); return; } elseif (empty($description)) { + // TRANS: Validation error shown when not providing a description in the "New application" form. $this->showForm(_('Description is required.')); return; } elseif (Oauth_application::descriptionTooLong($description)) { @@ -181,6 +185,7 @@ class NewApplicationAction extends OwnerDesignAction Oauth_application::maxDesc())); return; } elseif (empty($source_url)) { + // TRANS: Validation error shown when not providing a source URL in the "New application" form. $this->showForm(_('Source URL is required.')); return; } elseif ((strlen($source_url) > 0) @@ -190,15 +195,19 @@ class NewApplicationAction extends OwnerDesignAction ) ) { + // TRANS: Validation error shown when providing an invalid source URL in the "New application" form. $this->showForm(_('Source URL is not valid.')); return; } elseif (empty($organization)) { + // TRANS: Validation error shown when not providing an organisation in the "New application" form. $this->showForm(_('Organization is required.')); return; } elseif (mb_strlen($organization) > 255) { - $this->showForm(_('Organization is too long (maximum 255 chars).')); + // TRANS: Validation error shown when providing too long an arganisation name in the "Edit application" form. + $this->showForm(_('Organization is too long (maximum 255 characters).')); return; } elseif (empty($homepage)) { + // TRANS: Form validation error show when an organisation name has not been provided in the new application form. $this->showForm(_('Organization homepage is required.')); return; } elseif ((strlen($homepage) > 0) @@ -208,9 +217,11 @@ class NewApplicationAction extends OwnerDesignAction ) ) { + // TRANS: Validation error shown when providing an invalid homepage URL in the "New application" form. $this->showForm(_('Homepage is not a valid URL.')); return; } elseif (mb_strlen($callback_url) > 255) { + // TRANS: Validation error shown when providing too long a callback URL in the "New application" form. $this->showForm(_('Callback is too long.')); return; } elseif (strlen($callback_url) > 0 @@ -220,6 +231,7 @@ class NewApplicationAction extends OwnerDesignAction ) ) { + // TRANS: Validation error shown when providing an invalid callback URL in the "New application" form. $this->showForm(_('Callback URL is not valid.')); return; } @@ -263,6 +275,7 @@ class NewApplicationAction extends OwnerDesignAction if (!$result) { common_log_db_error($consumer, 'INSERT', __FILE__); + // TRANS: Server error displayed when an application could not be registered in the database through the "New application" form. $this->serverError(_('Could not create application.')); } @@ -272,6 +285,7 @@ class NewApplicationAction extends OwnerDesignAction if (!$this->app_id) { common_log_db_error($app, 'INSERT', __FILE__); + // TRANS: Server error displayed when an application could not be registered in the database through the "New application" form. $this->serverError(_('Could not create application.')); $app->query('ROLLBACK'); } @@ -281,7 +295,6 @@ class NewApplicationAction extends OwnerDesignAction $app->query('COMMIT'); common_redirect(common_local_url('oauthappssettings'), 303); - } /** @@ -294,12 +307,9 @@ class NewApplicationAction extends OwnerDesignAction * * @return boolean true if the name already exists */ - function nameExists($name) { $app = Oauth_application::staticGet('name', $name); return !empty($app); } - } - diff --git a/actions/newgroup.php b/actions/newgroup.php index 04441e71c6..9682b875cb 100644 --- a/actions/newgroup.php +++ b/actions/newgroup.php @@ -43,25 +43,25 @@ 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 NewgroupAction extends Action { var $msg; function title() { + // TRANS: Title for form to create a group. return _('New group'); } /** * Prepare to run */ - function prepare($args) { parent::prepare($args); if (!common_logged_in()) { + // TRANS: Client error displayed trying to create a group while not logged in. $this->clientError(_('You must be logged in to create a group.')); return false; } @@ -85,7 +85,6 @@ class NewgroupAction extends Action * * @return void */ - function handle($args) { parent::handle($args); @@ -114,107 +113,116 @@ class NewgroupAction extends Action $this->element('p', 'error', $this->msg); } else { $this->element('p', 'instructions', + // TRANS: Form instructions for group create form. _('Use this form to create a new group.')); } } function trySave() { - $nickname = $this->trimmed('nickname'); - $fullname = $this->trimmed('fullname'); - $homepage = $this->trimmed('homepage'); - $description = $this->trimmed('description'); - $location = $this->trimmed('location'); - $aliasstring = $this->trimmed('aliases'); + if (Event::handle('StartGroupSaveForm', array($this))) { + try { + $nickname = Nickname::normalize($this->trimmed('nickname')); + } catch (NicknameException $e) { + $this->showForm($e->getMessage()); + } + $fullname = $this->trimmed('fullname'); + $homepage = $this->trimmed('homepage'); + $description = $this->trimmed('description'); + $location = $this->trimmed('location'); + $aliasstring = $this->trimmed('aliases'); - if (!Validate::string($nickname, array('min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT))) { - $this->showForm(_('Nickname must have only lowercase letters '. - 'and numbers and no spaces.')); - return; - } else if ($this->nicknameExists($nickname)) { - $this->showForm(_('Nickname already in use. Try another one.')); - return; - } else if (!User_group::allowedNickname($nickname)) { - $this->showForm(_('Not a valid nickname.')); - return; - } else if (!is_null($homepage) && (strlen($homepage) > 0) && - !Validate::uri($homepage, - array('allowed_schemes' => - array('http', 'https')))) { - $this->showForm(_('Homepage is not a valid URL.')); - return; - } else if (!is_null($fullname) && mb_strlen($fullname) > 255) { - $this->showForm(_('Full name is too long (maximum 255 characters).')); - return; - } else if (User_group::descriptionTooLong($description)) { - // TRANS: Form validation error creating a new group because the description is too long. - // TRANS: %d is the maximum number of allowed characters. - $this->showForm(sprintf(_m('Description is too long (maximum %d character).', - 'Description is too long (maximum %d characters).', - User_group::maxDescription()), - User_group::maxDescription())); - return; - } else if (!is_null($location) && mb_strlen($location) > 255) { - $this->showForm(_('Location is too long (maximum 255 characters).')); - return; - } - - if (!empty($aliasstring)) { - $aliases = array_map('common_canonical_nickname', array_unique(preg_split('/[\s,]+/', $aliasstring))); - } else { - $aliases = array(); - } - - if (count($aliases) > common_config('group', 'maxaliases')) { - // TRANS: Client error shown when providing too many aliases during group creation. - // TRANS: %d is the maximum number of allowed aliases. - $this->showForm(sprintf(_m('Too many aliases! Maximum %d allowed.', - 'Too many aliases! Maximum %d allowed.', - common_config('group', 'maxaliases')), - common_config('group', 'maxaliases'))); - return; - } - - foreach ($aliases as $alias) { - if (!Validate::string($alias, array('min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT))) { - $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias)); + if ($this->nicknameExists($nickname)) { + // TRANS: Group create form validation error. + $this->showForm(_('Nickname already in use. Try another one.')); + return; + } else if (!User_group::allowedNickname($nickname)) { + // TRANS: Group create form validation error. + $this->showForm(_('Not a valid nickname.')); + return; + } else if (!is_null($homepage) && (strlen($homepage) > 0) && + !Validate::uri($homepage, + array('allowed_schemes' => + array('http', 'https')))) { + // TRANS: Group create form validation error. + $this->showForm(_('Homepage is not a valid URL.')); + return; + } else if (!is_null($fullname) && mb_strlen($fullname) > 255) { + // TRANS: Group create form validation error. + $this->showForm(_('Full name is too long (maximum 255 characters).')); + return; + } else if (User_group::descriptionTooLong($description)) { + // TRANS: Group create form validation error. + // TRANS: %d is the maximum number of allowed characters. + $this->showForm(sprintf(_m('Description is too long (maximum %d character).', + 'Description is too long (maximum %d characters).', + User_group::maxDescription()), + User_group::maxDescription())); + return; + } else if (!is_null($location) && mb_strlen($location) > 255) { + // TRANS: Group create form validation error. + $this->showForm(_('Location is too long (maximum 255 characters).')); return; } - if ($this->nicknameExists($alias)) { - $this->showForm(sprintf(_('Alias "%s" already in use. Try another one.'), - $alias)); + + if (!empty($aliasstring)) { + $aliases = array_map('common_canonical_nickname', array_unique(preg_split('/[\s,]+/', $aliasstring))); + } else { + $aliases = array(); + } + + if (count($aliases) > common_config('group', 'maxaliases')) { + // TRANS: Group create form validation error. + // TRANS: %d is the maximum number of allowed aliases. + $this->showForm(sprintf(_m('Too many aliases! Maximum %d allowed.', + 'Too many aliases! Maximum %d allowed.', + common_config('group', 'maxaliases')), + common_config('group', 'maxaliases'))); return; } - // XXX assumes alphanum nicknames - if (strcmp($alias, $nickname) == 0) { - $this->showForm(_('Alias can\'t be the same as nickname.')); - return; + + foreach ($aliases as $alias) { + if (!Nickname::isValid($alias)) { + // TRANS: Group create form validation error. + // TRANS: %s is the invalid alias. + $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias)); + return; + } + if ($this->nicknameExists($alias)) { + // TRANS: Group create form validation error. %s is the already used alias. + $this->showForm(sprintf(_('Alias "%s" already in use. Try another one.'), + $alias)); + return; + } + // XXX assumes alphanum nicknames + if (strcmp($alias, $nickname) == 0) { + // TRANS: Group create form validation error. + $this->showForm(_('Alias cannot be the same as nickname.')); + return; + } } + + $cur = common_current_user(); + + // Checked in prepare() above + + assert(!is_null($cur)); + + $group = User_group::register(array('nickname' => $nickname, + 'fullname' => $fullname, + 'homepage' => $homepage, + 'description' => $description, + 'location' => $location, + 'aliases' => $aliases, + 'userid' => $cur->id, + 'local' => true)); + + $this->group = $group; + + Event::handle('EndGroupSaveForm', array($this)); + + common_redirect($group->homeUrl(), 303); } - - $mainpage = common_local_url('showgroup', array('nickname' => $nickname)); - - $cur = common_current_user(); - - // Checked in prepare() above - - assert(!is_null($cur)); - - $group = User_group::register(array('nickname' => $nickname, - 'fullname' => $fullname, - 'homepage' => $homepage, - 'description' => $description, - 'location' => $location, - 'aliases' => $aliases, - 'userid' => $cur->id, - 'mainpage' => $mainpage, - 'local' => true)); - - common_redirect($group->homeUrl(), 303); } function nicknameExists($nickname) @@ -234,4 +242,3 @@ class NewgroupAction extends Action return false; } } - diff --git a/actions/newmessage.php b/actions/newmessage.php index c58ed3849e..447a00580c 100644 --- a/actions/newmessage.php +++ b/actions/newmessage.php @@ -144,7 +144,7 @@ class NewmessageAction extends Action $this->showForm(_('No content!')); return; } else { - $content_shortened = common_shorten_links($this->content); + $content_shortened = $user->shortenLinks($this->content); if (Message::contentTooLong($content_shortened)) { // TRANS: Form validation error displayed when message content is too long. diff --git a/actions/newnotice.php b/actions/newnotice.php index 57cd847c6e..faafd9551d 100644 --- a/actions/newnotice.php +++ b/actions/newnotice.php @@ -154,10 +154,13 @@ class NewnoticeAction extends Action return; } - $content_shortened = common_shorten_links($content); + $content_shortened = $user->shortenLinks($content); if (Notice::contentTooLong($content_shortened)) { - $this->clientError(sprintf(_('That\'s too long. '. - 'Max notice size is %d chars.'), + // TRANS: Client error displayed when the parameter "status" is missing. + // TRANS: %d is the maximum number of character for a notice. + $this->clientError(sprintf(_m('That\'s too long. Maximum notice size is %d character.', + 'That\'s too long. Maximum notice size is %d characters.', + Notice::maxContent()), Notice::maxContent())); } @@ -178,12 +181,10 @@ class NewnoticeAction extends Action if (Notice::contentTooLong($content_shortened)) { $upload->delete(); - $this->clientError( - sprintf( - _('Max notice size is %d chars, including attachment URL.'), - Notice::maxContent() - ) - ); + $this->clientError(sprintf(_m('Maximum notice size is %d character, including attachment URL.', + 'Maximum notice size is %d characters, including attachment URL.', + Notice::maxContent()), + Notice::maxContent())); } } diff --git a/actions/oembed.php b/actions/oembed.php index e25e4cb259..bef707f92a 100644 --- a/actions/oembed.php +++ b/actions/oembed.php @@ -79,11 +79,7 @@ class OembedAction extends Action if (empty($profile)) { $this->serverError(_('Notice has no profile.'), 500); } - if (!empty($profile->fullname)) { - $authorname = $profile->fullname . ' (' . $profile->nickname . ')'; - } else { - $authorname = $profile->nickname; - } + $authorname = $profile->getFancyName(); $oembed['title'] = sprintf(_('%1$s\'s status on %2$s'), $authorname, common_exact_date($notice->created)); @@ -112,10 +108,23 @@ class OembedAction extends Action $oembed['url']=$file_oembed->url; }else if(substr($attachment->mimetype,0,strlen('image/'))=='image/'){ $oembed['type']='photo'; - //TODO set width and height - //$oembed['width']= - //$oembed['height']= + if ($attachment->filename) { + $filepath = File::path($attachment->filename); + $gis = @getimagesize($filepath); + if ($gis) { + $oembed['width'] = $gis[0]; + $oembed['height'] = $gis[1]; + } else { + // TODO Either throw an error or find a fallback? + } + } $oembed['url']=$attachment->url; + $thumb = $attachment->getThumbnail(); + if ($thumb) { + $oembed['thumbnail_url'] = $thumb->url; + $oembed['thumbnail_width'] = $thumb->width; + $oembed['thumbnail_height'] = $thumb->height; + } }else{ $oembed['type']='link'; $oembed['url']=common_local_url('attachment', @@ -206,4 +215,15 @@ class OembedAction extends Action return; } + /** + * Is this action read-only? + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return true; + } } diff --git a/actions/othersettings.php b/actions/othersettings.php index 10e9873b39..ef2c9f32b0 100644 --- a/actions/othersettings.php +++ b/actions/othersettings.php @@ -46,7 +46,6 @@ require_once INSTALLDIR.'/lib/accountsettingsaction.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 OthersettingsAction extends AccountSettingsAction { /** @@ -54,9 +53,9 @@ class OthersettingsAction extends AccountSettingsAction * * @return string Title of the page */ - function title() { + // Page title for a tab in user profile settings. return _('Other settings'); } @@ -68,6 +67,7 @@ class OthersettingsAction extends AccountSettingsAction function getInstructions() { + // TRANS: Instructions for tab "Other" in user profile settings. return _('Manage various other options.'); } @@ -105,6 +105,9 @@ class OthersettingsAction extends AccountSettingsAction { $services[$name]=$name; if($value['freeService']){ + // TRANS: Used as a suffix for free URL shorteners in a dropdown list in the tab "Other" of a + // TRANS: user's profile settings. This message has one space at the beginning. Use your + // TRANS: language's word separator here if it has one (most likely a single space). $services[$name].=_(' (free service)'); } } @@ -113,17 +116,22 @@ class OthersettingsAction extends AccountSettingsAction asort($services); $this->elementStart('li'); + // TRANS: Label for dropdown with URL shortener services. $this->dropdown('urlshorteningservice', _('Shorten URLs with'), + // TRANS: Tooltip for for dropdown with URL shortener services. $services, _('Automatic shortening service to use.'), false, $user->urlshorteningservice); $this->elementEnd('li'); } $this->elementStart('li'); + // TRANS: Label for checkbox. $this->checkbox('viewdesigns', _('View profile designs'), + // TRANS: Tooltip for checkbox. $user->viewdesigns, _('Show or hide profile designs.')); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('save', _('Save')); + // TRANS: Button text for saving "Other settings" in profile. + $this->submit('save', _m('BUTTON','Save')); $this->elementEnd('fieldset'); $this->elementEnd('form'); } @@ -150,7 +158,8 @@ class OthersettingsAction extends AccountSettingsAction $urlshorteningservice = $this->trimmed('urlshorteningservice'); if (!is_null($urlshorteningservice) && strlen($urlshorteningservice) > 50) { - $this->showForm(_('URL shortening service is too long (max 50 chars).')); + // TRANS: Form validation error for form "Other settings" in user profile. + $this->showForm(_('URL shortening service is too long (maximum 50 characters).')); return; } @@ -171,7 +180,8 @@ class OthersettingsAction extends AccountSettingsAction if ($result === false) { common_log_db_error($user, 'UPDATE', __FILE__); - $this->serverError(_('Couldn\'t update user.')); + // TRANS: Server error displayed when "Other" settings in user profile could not be updated on the server. + $this->serverError(_('Could not update user.')); return; } diff --git a/actions/outbox.php b/actions/outbox.php index b81d4b9d0d..cad19bba24 100644 --- a/actions/outbox.php +++ b/actions/outbox.php @@ -88,21 +88,9 @@ class OutboxAction extends MailboxAction } } - /** - * returns the profile we want to show with the message - * - * For outboxes, we show the recipient. - * - * @param Message $message The message to get the profile for - * - * @return Profile The profile of the message recipient - * - * @see MailboxAction::getMessageProfile() - */ - - function getMessageProfile($message) + function getMessageList($message) { - return $message->getTo(); + return new OutboxMessageList($this, $message); } /** @@ -116,3 +104,24 @@ class OutboxAction extends MailboxAction return _('This is your outbox, which lists private messages you have sent.'); } } + +class OutboxMessageList extends MessageList +{ + function newItem($message) + { + return new OutboxMessageListItem($this->out, $message); + } +} + +class OutboxMessageListItem extends MessageListItem +{ + /** + * Returns the profile we want to show with the message + * + * @return Profile The profile that matches the message + */ + function getMessageProfile() + { + return $this->message->getTo(); + } +} \ No newline at end of file diff --git a/actions/profilesettings.php b/actions/profilesettings.php index e1a0f8b6d0..ee50e104d8 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -46,7 +46,6 @@ require_once INSTALLDIR.'/lib/accountsettingsaction.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 ProfilesettingsAction extends AccountSettingsAction { /** @@ -54,7 +53,6 @@ class ProfilesettingsAction extends AccountSettingsAction * * @return string Title of the page */ - function title() { // TRANS: Page title for profile settings. @@ -66,7 +64,6 @@ class ProfilesettingsAction extends AccountSettingsAction * * @return instructions for use */ - function getInstructions() { // TRANS: Usage instructions for profile settings. @@ -87,7 +84,6 @@ class ProfilesettingsAction extends AccountSettingsAction * * @return void */ - function showContent() { $user = common_current_user(); @@ -212,12 +208,12 @@ class ProfilesettingsAction extends AccountSettingsAction * * @return void */ - function handlePost() { // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Form validation error. $this->showForm(_('There was a problem with your session token. '. 'Try again, please.')); return; @@ -225,7 +221,13 @@ class ProfilesettingsAction extends AccountSettingsAction if (Event::handle('StartProfileSaveForm', array($this))) { - $nickname = $this->trimmed('nickname'); + try { + $nickname = Nickname::normalize($this->trimmed('nickname')); + } catch (NicknameException $e) { + $this->showForm($e->getMessage()); + return; + } + $fullname = $this->trimmed('fullname'); $homepage = $this->trimmed('homepage'); $bio = $this->trimmed('bio'); @@ -236,13 +238,7 @@ class ProfilesettingsAction extends AccountSettingsAction $tagstring = $this->trimmed('tags'); // Some validation - if (!Validate::string($nickname, array('min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT))) { - // TRANS: Validation error in form for profile settings. - $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.')); - return; - } else if (!User::allowed_nickname($nickname)) { + if (!User::allowed_nickname($nickname)) { // TRANS: Validation error in form for profile settings. $this->showForm(_('Not a valid nickname.')); return; @@ -323,7 +319,7 @@ class ProfilesettingsAction extends AccountSettingsAction if ($result === false) { common_log_db_error($user, 'UPDATE', __FILE__); // TRANS: Server error thrown when user profile settings could not be updated. - $this->serverError(_('Couldn\'t update user.')); + $this->serverError(_('Could not update user.')); return; } else { // Re-initialize language environment if it changed @@ -348,7 +344,7 @@ class ProfilesettingsAction extends AccountSettingsAction 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(_('Couldn\'t update user for autosubscribe.')); + $this->serverError(_('Could not update user for autosubscribe.')); return; } } @@ -406,7 +402,7 @@ class ProfilesettingsAction extends AccountSettingsAction if ($result === false) { common_log_db_error($prefs, ($exists) ? 'UPDATE' : 'INSERT', __FILE__); // TRANS: Server error thrown when user profile location preference settings could not be updated. - $this->serverError(_('Couldn\'t save location prefs.')); + $this->serverError(_('Could not save location prefs.')); return; } } @@ -419,7 +415,7 @@ class ProfilesettingsAction extends AccountSettingsAction if ($result === false) { common_log_db_error($profile, 'UPDATE', __FILE__); // TRANS: Server error thrown when user profile settings could not be saved. - $this->serverError(_('Couldn\'t save profile.')); + $this->serverError(_('Could not save profile.')); return; } @@ -428,7 +424,7 @@ class ProfilesettingsAction extends AccountSettingsAction if (!$result) { // TRANS: Server error thrown when user profile settings tags could not be saved. - $this->serverError(_('Couldn\'t save tags.')); + $this->serverError(_('Could not save tags.')); return; } @@ -452,4 +448,45 @@ class ProfilesettingsAction extends AccountSettingsAction return $other->id != $user->id; } } + + function showAside() { + $user = common_current_user(); + + $this->elementStart('div', array('id' => 'aside_primary', + 'class' => 'aside')); + + $this->elementStart('div', array('id' => 'account_actions', + 'class' => 'section')); + $this->elementStart('ul'); + if (Event::handle('StartProfileSettingsActions', array($this))) { + if ($user->hasRight(Right::BACKUPACCOUNT)) { + $this->elementStart('li'); + $this->element('a', + array('href' => common_local_url('backupaccount')), + // TRANS: Option in profile settings to create a backup of the account of the currently logged in user. + _('Backup account')); + $this->elementEnd('li'); + } + if ($user->hasRight(Right::DELETEACCOUNT)) { + $this->elementStart('li'); + $this->element('a', + array('href' => common_local_url('deleteaccount')), + // TRANS: Option in profile settings to delete the account of the currently logged in user. + _('Delete account')); + $this->elementEnd('li'); + } + if ($user->hasRight(Right::RESTOREACCOUNT)) { + $this->elementStart('li'); + $this->element('a', + array('href' => common_local_url('restoreaccount')), + // TRANS: Option in profile settings to restore the account of the currently logged in user from a backup. + _('Restore account')); + $this->elementEnd('li'); + } + Event::handle('EndProfileSettingsActions', array($this)); + } + $this->elementEnd('ul'); + $this->elementEnd('div'); + $this->elementEnd('div'); + } } diff --git a/actions/recoverpassword.php b/actions/recoverpassword.php index f9956897f6..c41edaeea4 100644 --- a/actions/recoverpassword.php +++ b/actions/recoverpassword.php @@ -33,6 +33,7 @@ class RecoverpasswordAction extends Action { parent::handle($args); if (common_logged_in()) { + // TRANS: Client error displayed trying to recover password while already logged in. $this->clientError(_('You are already logged in!')); return; } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { @@ -41,6 +42,7 @@ class RecoverpasswordAction extends Action } else if ($this->arg('reset')) { $this->resetPassword(); } else { + // TRANS: Client error displayed when unexpected data is posted in the password recovery form. $this->clientError(_('Unexpected form submission.')); } } else { @@ -54,15 +56,16 @@ class RecoverpasswordAction extends Action function checkCode() { - $code = $this->trimmed('code'); $confirm = Confirm_address::staticGet('code', $code); if (!$confirm) { + // TRANS: Client error displayed when password recovery code is not correct. $this->clientError(_('No such recovery code.')); return; } if ($confirm->address_type != 'recover') { + // TRANS: Client error displayed when no proper password recovery code was submitted. $this->clientError(_('Not a recovery code.')); return; } @@ -70,6 +73,7 @@ class RecoverpasswordAction extends Action $user = User::staticGet($confirm->user_id); if (!$user) { + // TRANS: Server error displayed trying to recover password without providing a user. $this->serverError(_('Recovery code for unknown user.')); return; } @@ -83,6 +87,7 @@ class RecoverpasswordAction extends Action if (!$result) { common_log_db_error($confirm, 'DELETE', __FILE__); + // TRANS: Server error displayed removing a password recovery code from the database. $this->serverError(_('Error with confirmation code.')); return; } @@ -94,6 +99,7 @@ class RecoverpasswordAction extends Action common_log(LOG_WARNING, 'Attempted redemption on recovery code ' . 'that is ' . $touched . ' seconds old. '); + // TRANS: Client error displayed trying to recover password with too old a recovery code. $this->clientError(_('This confirmation code is too old. ' . 'Please start again.')); return; @@ -108,6 +114,7 @@ class RecoverpasswordAction extends Action $result = $user->updateKeys($orig); if (!$result) { common_log_db_error($user, 'UPDATE', __FILE__); + // TRANS: Server error displayed when updating a user's e-mail address in the database fails while recovering a password. $this->serverError(_('Could not update user with confirmed email address.')); return; } @@ -149,14 +156,16 @@ class RecoverpasswordAction extends Action $this->elementStart('div', 'instructions'); if ($this->mode == 'recover') { $this->element('p', null, + // TRANS: Page notice for password recovery page. _('If you have forgotten or lost your' . ' password, you can get a new one sent to' . ' the email address you have stored' . ' in your account.')); } else if ($this->mode == 'reset') { + // TRANS: Page notice for password change page. $this->element('p', null, _('You have been identified. Enter a' . - ' new password below. ')); + ' new password below.')); } $this->elementEnd('div'); } @@ -185,19 +194,24 @@ class RecoverpasswordAction extends Action 'class' => 'form_settings', 'action' => common_local_url('recoverpassword'))); $this->elementStart('fieldset'); + // TRANS: Fieldset legend for password recovery page. $this->element('legend', null, _('Password recovery')); $this->elementStart('ul', 'form_data'); $this->elementStart('li'); + // TRANS: Field label on password recovery page. $this->input('nicknameoremail', _('Nickname or email address'), $this->trimmed('nicknameoremail'), + // TRANS: Title for field label on password recovery page. _('Your nickname on this server, ' . 'or your registered email address.')); $this->elementEnd('li'); $this->elementEnd('ul'); $this->element('input', array('name' => 'recover', 'type' => 'hidden', + // TRANS: Field label on password recovery page. 'value' => _('Recover'))); - $this->submit('recover', _('Recover')); + // TRANS: Button text on password recovery page. + $this->submit('recover', _m('BUTTON','Recover')); $this->elementEnd('fieldset'); $this->elementEnd('form'); } @@ -205,11 +219,16 @@ class RecoverpasswordAction extends Action function title() { switch ($this->mode) { + // TRANS: Title for password recovery page in password reset mode. case 'reset': return _('Reset password'); + // TRANS: Title for password recovery page in password recover mode. case 'recover': return _('Recover password'); + // TRANS: Title for password recovery page in email sent mode. case 'sent': return _('Password recovery requested'); + // TRANS: Title for password recovery page in password saved mode. case 'saved': return _('Password saved.'); default: + // TRANS: Title for password recovery page when an unknown action has been specified. return _('Unknown action'); } } @@ -228,19 +247,25 @@ class RecoverpasswordAction extends Action 'class' => 'form_settings', 'action' => common_local_url('recoverpassword'))); $this->elementStart('fieldset'); + // TRANS: Fieldset legend for password reset form. $this->element('legend', null, _('Password change')); $this->hidden('token', common_session_token()); $this->elementStart('ul', 'form_data'); $this->elementStart('li'); + // TRANS: Field label for password reset form. $this->password('newpassword', _('New password'), - _('6 or more characters, and don\'t forget it!')); + // TRANS: Title for field label for password reset form. + _('6 or more characters, and do not forget it!')); $this->elementEnd('li'); $this->elementStart('li'); + // TRANS: Field label for password reset form where the password has to be typed again. $this->password('confirm', _('Confirm'), - _('Same as password above')); + // TRANS: Ttile for field label for password reset form where the password has to be typed again. + _('Same as password above.')); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('reset', _('Reset')); + // TRANS: Button text for password reset form. + $this->submit('reset', _m('BUTTON','Reset')); $this->elementEnd('fieldset'); $this->elementEnd('form'); } @@ -249,6 +274,7 @@ class RecoverpasswordAction extends Action { $nore = $this->trimmed('nicknameoremail'); if (!$nore) { + // TRANS: Form instructions for password recovery form. $this->showForm(_('Enter a nickname or email address.')); return; } @@ -279,6 +305,7 @@ class RecoverpasswordAction extends Action } if (!$user) { + // TRANS: Information on password recovery form if no known username or e-mail address was specified. $this->showForm(_('No user with that email address or username.')); return; } @@ -296,6 +323,7 @@ class RecoverpasswordAction extends Action } if (!$user->email && !$confirm_email) { + // TRANS: Client error displayed on password recovery form if a user does not have a registered e-mail address. $this->clientError(_('No registered email address for that user.')); return; } @@ -310,10 +338,12 @@ class RecoverpasswordAction extends Action if (!$confirm->insert()) { common_log_db_error($confirm, 'INSERT', __FILE__); + // TRANS: Server error displayed if e-mail address confirmation fails in the database on the password recovery form. $this->serverError(_('Error saving address confirmation.')); return; } + // @todo FIXME: needs i18n. $body = "Hey, $user->nickname."; $body .= "\n\n"; $body .= 'Someone just asked for a new password ' . @@ -332,9 +362,11 @@ class RecoverpasswordAction extends Action $body .= "\n"; $headers = _mail_prepare_headers('recoverpassword', $user->nickname, $user->nickname); + // TRANS: Subject for password recovery e-mail. mail_to_user($user, _('Password recovery requested'), $body, $headers, $confirm->address); $this->mode = 'sent'; + // TRANS: User notification after an e-mail with instructions was sent from the password recovery form. $this->msg = _('Instructions for recovering your password ' . 'have been sent to the email address registered to your ' . 'account.'); @@ -347,6 +379,7 @@ class RecoverpasswordAction extends Action # CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { + // TRANS: Form validation error message. $this->showForm(_('There was a problem with your session token. Try again, please.')); return; } @@ -354,6 +387,7 @@ class RecoverpasswordAction extends Action $user = $this->getTempUser(); if (!$user) { + // TRANS: Client error displayed when trying to reset as password without providing a user. $this->clientError(_('Unexpected password reset.')); return; } @@ -362,10 +396,12 @@ class RecoverpasswordAction extends Action $confirm = $this->trimmed('confirm'); if (!$newpassword || strlen($newpassword) < 6) { - $this->showPasswordForm(_('Password must be 6 chars or more.')); + // TRANS: Reset password form validation error message. + $this->showPasswordForm(_('Password must be 6 characters or more.')); return; } if ($newpassword != $confirm) { + // TRANS: Reset password form validation error message. $this->showPasswordForm(_('Password and confirmation do not match.')); return; } @@ -378,13 +414,15 @@ class RecoverpasswordAction extends Action if (!$user->update($original)) { common_log_db_error($user, 'UPDATE', __FILE__); - $this->serverError(_('Can\'t save new password.')); + // TRANS: Reset password form validation error message. + $this->serverError(_('Cannot save new password.')); return; } $this->clearTempUser(); if (!common_set_user($user->nickname)) { + // TRANS: Server error displayed when something does wrong with the user object during password reset. $this->serverError(_('Error setting user.')); return; } @@ -392,6 +430,7 @@ class RecoverpasswordAction extends Action common_real_login(true); $this->mode = 'saved'; + // TRANS: Success message for user after password reset. $this->msg = _('New password successfully saved. ' . 'You are now logged in.'); $this->success = true; diff --git a/actions/register.php b/actions/register.php index 3ae3f56f60..488d8901fc 100644 --- a/actions/register.php +++ b/actions/register.php @@ -198,7 +198,11 @@ class RegisterAction extends Action } // Input scrubbing - $nickname = common_canonical_nickname($nickname); + try { + $nickname = Nickname::normalize($nickname); + } catch (NicknameException $e) { + $this->showForm($e->getMessage()); + } $email = common_canonical_email($email); if (!$this->boolean('license')) { @@ -206,11 +210,6 @@ class RegisterAction extends Action 'agree to the license.')); } else if ($email && !Validate::email($email, common_config('email', 'check_domain'))) { $this->showForm(_('Not a valid email address.')); - } else if (!Validate::string($nickname, array('min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT))) { - $this->showForm(_('Nickname must have only lowercase letters '. - 'and numbers and no spaces.')); } else if ($this->nicknameExists($nickname)) { $this->showForm(_('Nickname already in use. Try another one.')); } else if (!User::allowed_nickname($nickname)) { @@ -431,16 +430,15 @@ class RegisterAction extends Action if (Event::handle('StartRegistrationFormData', array($this))) { $this->elementStart('li'); $this->input('nickname', _('Nickname'), $this->trimmed('nickname'), - _('1-64 lowercase letters or numbers, '. - 'no punctuation or spaces. Required.')); + _('1-64 lowercase letters or numbers, no punctuation or spaces.')); $this->elementEnd('li'); $this->elementStart('li'); $this->password('password', _('Password'), - _('6 or more characters. Required.')); + _('6 or more characters.')); $this->elementEnd('li'); $this->elementStart('li'); $this->password('confirm', _('Confirm'), - _('Same as password above. Required.')); + _('Same as password above.')); $this->elementEnd('li'); $this->elementStart('li'); if ($this->invite && $this->invite->address_type == 'email') { diff --git a/actions/remotesubscribe.php b/actions/remotesubscribe.php index 9fc235e743..63ba618c41 100644 --- a/actions/remotesubscribe.php +++ b/actions/remotesubscribe.php @@ -44,7 +44,6 @@ require_once INSTALLDIR.'/extlib/libomb/profile.php'; * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class RemotesubscribeAction extends Action { var $nickname; @@ -173,14 +172,14 @@ class RemotesubscribeAction extends Action if ($service->getServiceURI(OAUTH_ENDPOINT_REQUEST) == common_local_url('requesttoken') || User::staticGet('uri', $service->getRemoteUserURI())) { - $this->showForm(_('That’s a local profile! Login to subscribe.')); + $this->showForm(_('That is a local profile! Login to subscribe.')); return; } try { $service->requestToken(); } catch (OMB_RemoteServiceException $e) { - $this->showForm(_('Couldn’t get a request token.')); + $this->showForm(_('Could not get a request token.')); return; } @@ -204,4 +203,3 @@ class RemotesubscribeAction extends Action common_redirect($target_url, 303); } } -?> diff --git a/actions/repeat.php b/actions/repeat.php index 893cae4ffd..60b238f3a7 100644 --- a/actions/repeat.php +++ b/actions/repeat.php @@ -41,7 +41,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class RepeatAction extends Action { var $user = null; @@ -73,7 +72,7 @@ class RepeatAction extends Action } if ($this->user->id == $this->notice->profile_id) { - $this->clientError(_("You can't repeat your own notice.")); + $this->clientError(_("You cannot repeat your own notice.")); return false; } @@ -101,7 +100,6 @@ class RepeatAction extends Action * * @return void */ - function handle($args) { $repeat = $this->notice->repeat($this->user->id, 'web'); diff --git a/actions/restoreaccount.php b/actions/restoreaccount.php new file mode 100644 index 0000000000..b32553a68d --- /dev/null +++ b/actions/restoreaccount.php @@ -0,0 +1,375 @@ +. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Restore a backup of your own account from the browser + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class RestoreaccountAction extends Action +{ + private $success = false; + private $inprogress = false; + + /** + * Returns the title of the page + * + * @return string page title + */ + function title() + { + // TRANS: Page title for page where a user account can be restored from backup. + return _("Restore account"); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + + $cur = common_current_user(); + + if (empty($cur)) { + // TRANS: Client exception displayed when trying to restore an account while not logged in. + throw new ClientException(_('Only logged-in users can restore their account.'), 403); + } + + if (!$cur->hasRight(Right::RESTOREACCOUNT)) { + // TRANS: Client exception displayed when trying to restore an account without having restore rights. + throw new ClientException(_('You may not restore your account.'), 403); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + parent::handle($argarray); + + if ($this->isPost()) { + $this->restoreAccount(); + } else { + $this->showPage(); + } + return; + } + + /** + * Queue a file for restoration + * + * Uses the UserActivityStream class; may take a long time! + * + * @return void + */ + function restoreAccount() + { + $this->checkSessionToken(); + + if (!isset($_FILES['restorefile']['error'])) { + // TRANS: Client exception displayed trying to restore an account while something went wrong uploading a file. + throw new ClientException(_('No uploaded file.')); + } + + switch ($_FILES['restorefile']['error']) { + case UPLOAD_ERR_OK: // success, jump out + break; + case UPLOAD_ERR_INI_SIZE: + // TRANS: Client exception thrown when an uploaded file is larger than set in php.ini. + throw new ClientException(_('The uploaded file exceeds the ' . + 'upload_max_filesize directive in php.ini.')); + return; + case UPLOAD_ERR_FORM_SIZE: + throw new ClientException( + // TRANS: Client exception. + _('The uploaded file exceeds the MAX_FILE_SIZE directive' . + ' that was specified in the HTML form.')); + return; + case UPLOAD_ERR_PARTIAL: + @unlink($_FILES['restorefile']['tmp_name']); + // TRANS: Client exception. + throw new ClientException(_('The uploaded file was only' . + ' partially uploaded.')); + return; + case UPLOAD_ERR_NO_FILE: + // TRANS: Client exception. No file; probably just a non-AJAX submission. + throw new ClientException(_('No uploaded file.')); + return; + case UPLOAD_ERR_NO_TMP_DIR: + // TRANS: Client exception thrown when a temporary folder is not present to store a file upload. + throw new ClientException(_('Missing a temporary folder.')); + return; + case UPLOAD_ERR_CANT_WRITE: + // TRANS: Client exception thrown when writing to disk is not possible during a file upload operation. + throw new ClientException(_('Failed to write file to disk.')); + return; + case UPLOAD_ERR_EXTENSION: + // TRANS: Client exception thrown when a file upload operation has been stopped by an extension. + throw new ClientException(_('File upload stopped by extension.')); + return; + default: + common_log(LOG_ERR, __METHOD__ . ": Unknown upload error " . + $_FILES['restorefile']['error']); + // TRANS: Client exception thrown when a file upload operation has failed with an unknown reason. + throw new ClientException(_('System error uploading file.')); + return; + } + + $filename = $_FILES['restorefile']['tmp_name']; + + try { + if (!file_exists($filename)) { + // TRANS: Server exception thrown when an expected file upload could not be found. + throw new ServerException(_("No such file '$filename'.")); + } + + if (!is_file($filename)) { + // TRANS: Server exception thrown when an expected file upload is not an actual file. + throw new ServerException(_("Not a regular file: '$filename'.")); + } + + if (!is_readable($filename)) { + // TRANS: Server exception thrown when an expected file upload could not be read. + throw new ServerException(_("File '$filename' not readable.")); + } + + common_debug(sprintf("Getting backup from file '%s'.", $filename)); + + $xml = file_get_contents($filename); + + // This check is costly but we should probably give + // the user some info ahead of time. + $doc = new DOMDocument(); + + // Disable PHP warnings so we don't spew low-level XML errors to output... + // would be nice if we can just get exceptions instead. + $old_err = error_reporting(); + error_reporting($old_err & ~E_WARNING); + $doc->loadXML($xml); + error_reporting($old_err); + + $feed = $doc->documentElement; + + if (!$feed || + $feed->namespaceURI != Activity::ATOM || + $feed->localName != 'feed') { + // TRANS: Client exception thrown when a feed is not an Atom feed. + throw new ClientException(_("Not an Atom feed.")); + } + + // Enqueue for processing. + + $qm = QueueManager::get(); + $qm->enqueue(array(common_current_user(), $xml, false), 'feedimp'); + + if ($qm instanceof UnQueueManager) { + // No active queuing means we've actually just completed the job! + $this->success = true; + } else { + // We've fed data into background queues, and it's probably still running. + $this->inprogress = true; + } + $this->showPage(); + + } catch (Exception $e) { + // Delete the file and re-throw + @unlink($_FILES['restorefile']['tmp_name']); + throw $e; + } + } + + /** + * Show a little form so that the person can upload a file to restore + * + * @return void + */ + function showContent() + { + if ($this->success) { + $this->element('p', null, + // TRANS: Success message when a feed has been restored. + _('Feed has been restored. Your old posts should now appear in search and your profile page.')); + } else if ($this->inprogress) { + $this->element('p', null, + // TRANS: Message when a feed restore is in progress. + _('Feed will be restored. Please wait a few minutes for results.')); + } else { + $form = new RestoreAccountForm($this); + $form->show(); + } + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return false; + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + function lastModified() + { + // For comparison with If-Last-Modified + // If not applicable, return null + return null; + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + function etag() + { + return null; + } +} + +/** + * A form for backing up the account. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class RestoreAccountForm extends Form +{ + function __construct($out=null) { + parent::__construct($out); + $this->enctype = 'multipart/form-data'; + } + + /** + * Class of the form. + * + * @return string the form's class + */ + function formClass() + { + return 'form_profile_restore'; + } + + /** + * URL the form posts to + * + * @return string the form's action URL + */ + function action() + { + return common_local_url('restoreaccount'); + } + + /** + * Output form data + * + * Really, just instructions for doing a backup. + * + * @return void + */ + function formData() + { + $this->out->elementStart('p', 'instructions'); + + // TRANS: Form instructions for feed restore. + $this->out->raw(_('You can upload a backed-up stream in '. + 'Activity Streams format.')); + + $this->out->elementEnd('p'); + + $this->out->elementStart('ul', 'form_data'); + + $this->out->elementStart('li', array ('id' => 'settings_attach')); + $this->out->element('input', array('name' => 'restorefile', + 'type' => 'file', + 'id' => 'restorefile')); + $this->out->elementEnd('li'); + + $this->out->elementEnd('ul'); + } + + /** + * Buttons for the form + * + * In this case, a single submit button + * + * @return void + */ + function formActions() + { + $this->out->submit('submit', + // TRANS: Submit button to confirm upload of a user backup file for account restore. + _m('BUTTON', 'Upload'), + 'submit', + null, + // TRANS: Title for submit button to confirm upload of a user backup file for account restore. + _('Upload the file')); + } +} diff --git a/actions/rsd.php b/actions/rsd.php index f88bf2e9a8..e02c85c41b 100644 --- a/actions/rsd.php +++ b/actions/rsd.php @@ -162,6 +162,20 @@ class RsdAction extends Action 'true'); $this->elementEnd('settings'); $this->elementEnd('api'); + + // Atom API + + if (empty($this->user)) { + $service = common_local_url('ApiAtomService'); + } else { + $service = common_local_url('ApiAtomService', array('id' => $this->user->nickname)); + } + + $this->element('api', array('name' => 'Atom', + 'preferred' => 'false', + 'apiLink' => $service, + 'blogID' => $blogID)); + Event::handle('EndRsdListApis', array($this, $this->user)); } $this->elementEnd('apis'); diff --git a/actions/showfavorites.php b/actions/showfavorites.php index 77b73711d2..eda2cf38eb 100644 --- a/actions/showfavorites.php +++ b/actions/showfavorites.php @@ -44,7 +44,6 @@ require_once INSTALLDIR.'/lib/feedlist.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 ShowfavoritesAction extends OwnerDesignAction { /** User we're getting the faves of */ @@ -57,7 +56,6 @@ class ShowfavoritesAction extends OwnerDesignAction * * @return boolean true */ - function isReadOnly($args) { return true; @@ -70,12 +68,15 @@ class ShowfavoritesAction extends OwnerDesignAction * * @return string title of page */ - function title() { if ($this->page == 1) { + // TRANS: Title for first page of favourite notices of a user. + // TRANS: %s is the user for whom the favourite notices are displayed. return sprintf(_('%s\'s favorite notices'), $this->user->nickname); } else { + // TRANS: Title for all but the first page of favourite notices of a user. + // TRANS: %1$s is the user for whom the favourite notices are displayed, %2$d is the page number. return sprintf(_('%1$s\'s favorite notices, page %2$d'), $this->user->nickname, $this->page); @@ -92,7 +93,6 @@ class ShowfavoritesAction extends OwnerDesignAction * * @return boolean success flag */ - function prepare($args) { parent::prepare($args); @@ -102,6 +102,7 @@ class ShowfavoritesAction extends OwnerDesignAction $this->user = User::staticGet('nickname', $nickname); if (!$this->user) { + // TRANS: Client error displayed when trying to display favourite notices for a non-existing user. $this->clientError(_('No such user.')); return false; } @@ -129,6 +130,7 @@ class ShowfavoritesAction extends OwnerDesignAction } if (empty($this->notice)) { + // TRANS: Server error displayed when favourite notices could not be retrieved from the database. $this->serverError(_('Could not retrieve favorite notices.')); return; } @@ -150,7 +152,6 @@ class ShowfavoritesAction extends OwnerDesignAction * * @return void */ - function handle($args) { parent::handle($args); @@ -162,12 +163,12 @@ class ShowfavoritesAction extends OwnerDesignAction * * @return array Feed objects to show */ - function getFeeds() { return array(new Feed(Feed::RSS1, common_local_url('favoritesrss', array('nickname' => $this->user->nickname)), + // TRANS: Feed link text. %s is a username. sprintf(_('Feed for favorites of %s (RSS 1.0)'), $this->user->nickname)), new Feed(Feed::RSS2, @@ -175,6 +176,7 @@ class ShowfavoritesAction extends OwnerDesignAction array( 'id' => $this->user->nickname, 'format' => 'rss')), + // TRANS: Feed link text. %s is a username. sprintf(_('Feed for favorites of %s (RSS 2.0)'), $this->user->nickname)), new Feed(Feed::ATOM, @@ -182,6 +184,7 @@ class ShowfavoritesAction extends OwnerDesignAction array( 'id' => $this->user->nickname, 'format' => 'atom')), + // TRANS: Feed link text. %s is a username. sprintf(_('Feed for favorites of %s (Atom)'), $this->user->nickname))); } @@ -191,7 +194,6 @@ class ShowfavoritesAction extends OwnerDesignAction * * @return void */ - function showLocalNav() { $nav = new PersonalGroupNav($this); @@ -203,12 +205,18 @@ class ShowfavoritesAction extends OwnerDesignAction if (common_logged_in()) { $current_user = common_current_user(); if ($this->user->id === $current_user->id) { + // TRANS: Text displayed instead of favourite notices for the current logged in user that has no favourites. $message = _('You haven\'t chosen any favorite notices yet. Click the fave button on notices you like to bookmark them for later or shed a spotlight on them.'); } else { + // TRANS: Text displayed instead of favourite notices for a user that has no favourites while logged in. + // TRANS: %s is a username. $message = sprintf(_('%s hasn\'t added any favorite notices yet. Post something interesting they would add to their favorites :)'), $this->user->nickname); } } else { + // TRANS: Text displayed instead of favourite notices for a user that has no favourites while not logged in. + // TRANS: %s is a username, %%%%action.register%%%% is a link to the user registration page. + // TRANS: (link text)[link] is a Mark Down link. $message = sprintf(_('%s hasn\'t added any favorite notices yet. Why not [register an account](%%%%action.register%%%%) and then post something interesting they would add to their favorites :)'), $this->user->nickname); } @@ -224,7 +232,6 @@ class ShowfavoritesAction extends OwnerDesignAction * * @return void */ - function showContent() { $nl = new FavoritesNoticeList($this->notice, $this); @@ -240,6 +247,7 @@ class ShowfavoritesAction extends OwnerDesignAction } function showPageNotice() { + // TRANS: Page notice for show favourites page. $this->element('p', 'instructions', _('This is a way to share what you like.')); } } diff --git a/actions/showgroup.php b/actions/showgroup.php index 8e8ff717c1..2806944452 100644 --- a/actions/showgroup.php +++ b/actions/showgroup.php @@ -46,10 +46,8 @@ define('MEMBERS_PER_SECTION', 27); * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class ShowgroupAction extends GroupDesignAction { - /** page we're viewing. */ var $page = null; @@ -58,7 +56,6 @@ class ShowgroupAction extends GroupDesignAction * * @return boolean true */ - function isReadOnly($args) { return true; @@ -69,18 +66,16 @@ class ShowgroupAction extends GroupDesignAction * * @return string page title, with page number */ - function title() { - if (!empty($this->group->fullname)) { - $base = $this->group->fullname . ' (' . $this->group->nickname . ')'; - } else { - $base = $this->group->nickname; - } + $base = $this->group->getFancyName(); if ($this->page == 1) { + // TRANS: Page title for first group page. %s is a group name. return sprintf(_('%s group'), $base); } else { + // TRANS: Page title for any but first group page. + // TRANS: %1$s is a group name, $2$s is a page number. return sprintf(_('%1$s group, page %2$d'), $base, $this->page); @@ -96,7 +91,6 @@ class ShowgroupAction extends GroupDesignAction * * @return boolean success flag */ - function prepare($args) { parent::prepare($args); @@ -118,6 +112,7 @@ class ShowgroupAction extends GroupDesignAction } if (!$nickname) { + // TRANS: Client error displayed if no nickname argument was given requesting a group page. $this->clientError(_('No nickname.'), 404); return false; } @@ -135,6 +130,7 @@ class ShowgroupAction extends GroupDesignAction return false; } else { common_log(LOG_NOTICE, "Couldn't find local group for nickname '$nickname'"); + // TRANS: Client error displayed if no remote group with a given name was found requesting group page. $this->clientError(_('No such group.'), 404); return false; } @@ -143,6 +139,7 @@ class ShowgroupAction extends GroupDesignAction $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { + // TRANS: Client error displayed if no local group with a given name was found requesting group page. $this->clientError(_('No such group.'), 404); return false; } @@ -160,7 +157,6 @@ class ShowgroupAction extends GroupDesignAction * * @return void */ - function handle($args) { $this->showPage(); @@ -171,7 +167,6 @@ class ShowgroupAction extends GroupDesignAction * * @return void */ - function showLocalNav() { $nav = new GroupNav($this, $this->group); @@ -183,10 +178,10 @@ class ShowgroupAction extends GroupDesignAction * * Shows a group profile and a list of group notices */ - function showContent() { $this->showGroupProfile(); + $this->showGroupActions(); $this->showGroupNotices(); } @@ -195,7 +190,6 @@ class ShowgroupAction extends GroupDesignAction * * @return void */ - function showGroupNotices() { $notice = $this->group->getNotices(($this->page-1)*NOTICES_PER_PAGE, @@ -218,109 +212,128 @@ class ShowgroupAction extends GroupDesignAction * * @return void */ - function showGroupProfile() { $this->elementStart('div', array('id' => 'i', 'class' => 'entity_profile vcard author')); - $this->element('h2', null, _('Group profile')); + if (Event::handle('StartGroupProfileElements', array($this, $this->group))) { - $this->elementStart('dl', 'entity_depiction'); - $this->element('dt', null, _('Avatar')); - $this->elementStart('dd'); + // TRANS: Group profile header (h2). Text hidden by default. + $this->element('h2', null, _('Group profile')); - $logo = ($this->group->homepage_logo) ? - $this->group->homepage_logo : User_group::defaultLogo(AVATAR_PROFILE_SIZE); - - $this->element('img', array('src' => $logo, - 'class' => 'photo avatar', - 'width' => AVATAR_PROFILE_SIZE, - 'height' => AVATAR_PROFILE_SIZE, - 'alt' => $this->group->nickname)); - $this->elementEnd('dd'); - $this->elementEnd('dl'); - - $this->elementStart('dl', 'entity_nickname'); - $this->element('dt', null, _('Nickname')); - $this->elementStart('dd'); - $hasFN = ($this->group->fullname) ? 'nickname url uid' : 'fn org nickname url uid'; - $this->element('a', array('href' => $this->group->homeUrl(), - 'rel' => 'me', 'class' => $hasFN), - $this->group->nickname); - $this->elementEnd('dd'); - $this->elementEnd('dl'); - - if ($this->group->fullname) { - $this->elementStart('dl', 'entity_fn'); - $this->element('dt', null, _('Full name')); + $this->elementStart('dl', 'entity_depiction'); + // TRANS: Label for group avatar (dt). Text hidden by default. + $this->element('dt', null, _('Avatar')); $this->elementStart('dd'); - $this->element('span', 'fn org', $this->group->fullname); + + $logo = ($this->group->homepage_logo) ? + $this->group->homepage_logo : User_group::defaultLogo(AVATAR_PROFILE_SIZE); + + $this->element('img', array('src' => $logo, + 'class' => 'photo avatar', + 'width' => AVATAR_PROFILE_SIZE, + 'height' => AVATAR_PROFILE_SIZE, + 'alt' => $this->group->nickname)); $this->elementEnd('dd'); $this->elementEnd('dl'); - } - if ($this->group->location) { - $this->elementStart('dl', 'entity_location'); - $this->element('dt', null, _('Location')); - $this->element('dd', 'label', $this->group->location); - $this->elementEnd('dl'); - } - - if ($this->group->homepage) { - $this->elementStart('dl', 'entity_url'); - $this->element('dt', null, _('URL')); + $this->elementStart('dl', 'entity_nickname'); + // TRANS: Label for group nickname (dt). Text hidden by default. + $this->element('dt', null, _('Nickname')); $this->elementStart('dd'); - $this->element('a', array('href' => $this->group->homepage, - 'rel' => 'me', 'class' => 'url'), - $this->group->homepage); + $hasFN = ($this->group->fullname) ? 'nickname url uid' : 'fn org nickname url uid'; + $this->element('a', array('href' => $this->group->homeUrl(), + 'rel' => 'me', 'class' => $hasFN), + $this->group->nickname); $this->elementEnd('dd'); $this->elementEnd('dl'); - } - if ($this->group->description) { - $this->elementStart('dl', 'entity_note'); - $this->element('dt', null, _('Note')); - $this->element('dd', 'note', $this->group->description); - $this->elementEnd('dl'); - } - - if (common_config('group', 'maxaliases') > 0) { - $aliases = $this->group->getAliases(); - - if (!empty($aliases)) { - $this->elementStart('dl', 'entity_aliases'); - $this->element('dt', null, _('Aliases')); - $this->element('dd', 'aliases', implode(' ', $aliases)); + if ($this->group->fullname) { + $this->elementStart('dl', 'entity_fn'); + // TRANS: Label for full group name (dt). Text hidden by default. + $this->element('dt', null, _('Full name')); + $this->elementStart('dd'); + $this->element('span', 'fn org', $this->group->fullname); + $this->elementEnd('dd'); $this->elementEnd('dl'); } + + if ($this->group->location) { + $this->elementStart('dl', 'entity_location'); + // TRANS: Label for group location (dt). Text hidden by default. + $this->element('dt', null, _('Location')); + $this->element('dd', 'label', $this->group->location); + $this->elementEnd('dl'); + } + + if ($this->group->homepage) { + $this->elementStart('dl', 'entity_url'); + // TRANS: Label for group URL (dt). Text hidden by default. + $this->element('dt', null, _('URL')); + $this->elementStart('dd'); + $this->element('a', array('href' => $this->group->homepage, + 'rel' => 'me', 'class' => 'url'), + $this->group->homepage); + $this->elementEnd('dd'); + $this->elementEnd('dl'); + } + + if ($this->group->description) { + $this->elementStart('dl', 'entity_note'); + // TRANS: Label for group description or group note (dt). Text hidden by default. + $this->element('dt', null, _('Note')); + $this->element('dd', 'note', $this->group->description); + $this->elementEnd('dl'); + } + + if (common_config('group', 'maxaliases') > 0) { + $aliases = $this->group->getAliases(); + + if (!empty($aliases)) { + $this->elementStart('dl', 'entity_aliases'); + // TRANS: Label for group aliases (dt). Text hidden by default. + $this->element('dt', null, _('Aliases')); + $this->element('dd', 'aliases', implode(' ', $aliases)); + $this->elementEnd('dl'); + } + } + + Event::handle('EndGroupProfileElements', array($this, $this->group)); } $this->elementEnd('div'); + } + function showGroupActions() + { $cur = common_current_user(); $this->elementStart('div', 'entity_actions'); + // TRANS: Group actions header (h2). Text hidden by default. $this->element('h2', null, _('Group actions')); $this->elementStart('ul'); - $this->elementStart('li', 'entity_subscribe'); - if (Event::handle('StartGroupSubscribe', array($this, $this->group))) { - if ($cur) { - if ($cur->isMember($this->group)) { - $lf = new LeaveForm($this, $this->group); - $lf->show(); - } else if (!Group_block::isBlocked($this->group, $cur->getProfile())) { - $jf = new JoinForm($this, $this->group); - $jf->show(); + if (Event::handle('StartGroupActionsList', array($this, $this->group))) { + $this->elementStart('li', 'entity_subscribe'); + if (Event::handle('StartGroupSubscribe', array($this, $this->group))) { + if ($cur) { + if ($cur->isMember($this->group)) { + $lf = new LeaveForm($this, $this->group); + $lf->show(); + } else if (!Group_block::isBlocked($this->group, $cur->getProfile())) { + $jf = new JoinForm($this, $this->group); + $jf->show(); + } } + Event::handle('EndGroupSubscribe', array($this, $this->group)); } - Event::handle('EndGroupSubscribe', array($this, $this->group)); - } - $this->elementEnd('li'); - if ($cur && $cur->hasRight(Right::DELETEGROUP)) { - $this->elementStart('li', 'entity_delete'); - $df = new DeleteGroupForm($this, $this->group); - $df->show(); $this->elementEnd('li'); + if ($cur && $cur->hasRight(Right::DELETEGROUP)) { + $this->elementStart('li', 'entity_delete'); + $df = new DeleteGroupForm($this, $this->group); + $df->show(); + $this->elementEnd('li'); + } + Event::handle('EndGroupActionsList', array($this, $this->group)); } $this->elementEnd('ul'); $this->elementEnd('div'); @@ -331,7 +344,6 @@ class ShowgroupAction extends GroupDesignAction * * @return void */ - function getFeeds() { $url = @@ -341,23 +353,27 @@ class ShowgroupAction extends GroupDesignAction return array(new Feed(Feed::RSS1, common_local_url('grouprss', array('nickname' => $this->group->nickname)), + // TRANS: Tooltip for feed link. %s is a group nickname. sprintf(_('Notice feed for %s group (RSS 1.0)'), $this->group->nickname)), new Feed(Feed::RSS2, common_local_url('ApiTimelineGroup', array('format' => 'rss', 'id' => $this->group->id)), + // TRANS: Tooltip for feed link. %s is a group nickname. sprintf(_('Notice feed for %s group (RSS 2.0)'), $this->group->nickname)), new Feed(Feed::ATOM, common_local_url('ApiTimelineGroup', array('format' => 'atom', 'id' => $this->group->id)), + // TRANS: Tooltip for feed link. %s is a group nickname. sprintf(_('Notice feed for %s group (Atom)'), $this->group->nickname)), new Feed(Feed::FOAF, common_local_url('foafgroup', array('nickname' => $this->group->nickname)), + // TRANS: Tooltip for feed link. %s is a group nickname. sprintf(_('FOAF for %s group'), $this->group->nickname))); } @@ -367,7 +383,6 @@ class ShowgroupAction extends GroupDesignAction * * @return void */ - function showSections() { $this->showMembers(); @@ -382,7 +397,6 @@ class ShowgroupAction extends GroupDesignAction * * @return void */ - function showMembers() { $member = $this->group->getMembers(0, MEMBERS_PER_SECTION); @@ -396,17 +410,22 @@ class ShowgroupAction extends GroupDesignAction if (Event::handle('StartShowGroupMembersMiniList', array($this))) { + // TRANS: Header for mini list of group members on a group page (h2). $this->element('h2', null, _('Members')); $gmml = new GroupMembersMiniList($member, $this); $cnt = $gmml->show(); if ($cnt == 0) { + // TRANS: Description for mini list of group members on a group page when the group has no members. $this->element('p', null, _('(None)')); } + // @todo FIXME: Should be shown if a group has more than 27 members, but I do not see it displayed at + // for example http://identi.ca/group/statusnet. Broken? if ($cnt > MEMBERS_PER_SECTION) { $this->element('a', array('href' => common_local_url('groupmembers', array('nickname' => $this->group->nickname))), + // TRANS: Link to all group members from mini list of group members if group has more than n members. _('All members')); } @@ -421,7 +440,6 @@ class ShowgroupAction extends GroupDesignAction * * @return void */ - function showAdmins() { $adminSection = new GroupAdminSection($this, $this->group); @@ -433,22 +451,26 @@ class ShowgroupAction extends GroupDesignAction * * @return void */ - function showStatistics() { $this->elementStart('div', array('id' => 'entity_statistics', 'class' => 'section')); + // TRANS: Header for group statistics on a group page (h2). $this->element('h2', null, _('Statistics')); $this->elementStart('dl', 'entity_created'); - $this->element('dt', null, _('Created')); + // @todo FIXME: i18n issue. This label gets a colon added from somewhere. Should be part of the message. + // TRANS: Label for creation date in statistics on group page. + $this->element('dt', null, _m('LABEL','Created')); $this->element('dd', null, date('j M Y', strtotime($this->group->created))); $this->elementEnd('dl'); $this->elementStart('dl', 'entity_members'); - $this->element('dt', null, _('Members')); + // @todo FIXME: i18n issue. This label gets a colon added from somewhere. Should be part of the message. + // TRANS: Label for member count in statistics on group page. + $this->element('dt', null, _m('LABEL','Members')); $this->element('dd', null, $this->group->getMemberCount()); $this->elementEnd('dl'); @@ -458,12 +480,21 @@ class ShowgroupAction extends GroupDesignAction function showAnonymousMessage() { if (!(common_config('site','closed') || common_config('site','inviteonly'))) { + // @todo FIXME: use group full name here if available instead of (uglier) primary alias. + // TRANS: Notice on group pages for anonymous users for StatusNet sites that accept new registrations. + // TRANS: **%s** is the group alias, %%%%site.name%%%% is the site name, + // TRANS: %%%%action.register%%%% is the URL for registration, %%%%doc.help%%%% is a URL to help. + // TRANS: This message contains Markdown links. Ensure they are formatted correctly: [Description](link). $m = sprintf(_('**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . 'based on the Free Software [StatusNet](http://status.net/) tool. Its members share ' . 'short messages about their life and interests. '. '[Join now](%%%%action.register%%%%) to become part of this group and many more! ([Read more](%%%%doc.help%%%%))'), $this->group->nickname); } else { + // @todo FIXME: use group full name here if available instead of (uglier) primary alias. + // TRANS: Notice on group pages for anonymous users for StatusNet sites that accept no new registrations. + // TRANS: **%s** is the group alias, %%%%site.name%%%% is the site name, + // TRANS: This message contains Markdown links. Ensure they are formatted correctly: [Description](link). $m = sprintf(_('**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . 'based on the Free Software [StatusNet](http://status.net/) tool. Its members share ' . 'short messages about their life and interests. '), @@ -492,6 +523,7 @@ class GroupAdminSection extends ProfileSection function title() { + // TRANS: Header for list of group administrators on a group page (h2). return _('Admins'); } @@ -527,4 +559,3 @@ class GroupMembersMiniListItem extends ProfileMiniListItem return $aAttrs; } } - diff --git a/actions/showmessage.php b/actions/showmessage.php index db757948ba..1c867af119 100644 --- a/actions/showmessage.php +++ b/actions/showmessage.php @@ -26,17 +26,13 @@ * @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); +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); } -require_once INSTALLDIR.'/lib/mailbox.php'; - /** * Show a single message * - * // XXX: It is totally weird how this works! - * * @category Personal * @package StatusNet * @author Evan Prodromou @@ -44,18 +40,17 @@ require_once INSTALLDIR.'/lib/mailbox.php'; * @link http://status.net/ */ -class ShowmessageAction extends MailboxAction +class ShowmessageAction extends Action { /** * Message object to show */ - var $message = null; - + /** * The current user */ - + var $user = null; /** @@ -67,63 +62,94 @@ class ShowmessageAction extends MailboxAction * * @return success flag */ - function prepare($args) { parent::prepare($args); - + $this->page = 1; - + $id = $this->trimmed('message'); $this->message = Message::staticGet('id', $id); if (!$this->message) { + // TRANS: Client error displayed requesting a single message that does not exist. $this->clientError(_('No such message.'), 404); return false; } $this->user = common_current_user(); + if (empty($this->user) || + ($this->user->id != $this->message->from_profile && + $this->user->id != $this->message->to_profile)) { + // TRANS: Client error displayed requesting a single direct message the requesting user was not a party in. + throw new ClientException(_('Only the sender and recipient ' . + 'may read this message.'), 403); + } + return true; } function handle($args) { - Action::handle($args); - - if ($this->user && ($this->user->id == $this->message->from_profile || - $this->user->id == $this->message->to_profile)) { - $this->showPage(); - } else { - $this->clientError(_('Only the sender and recipient ' . - 'may read this message.'), 403); - return; - } + $this->showPage(); } - + function title() - { + { if ($this->user->id == $this->message->from_profile) { $to = $this->message->getTo(); - return sprintf(_("Message to %1\$s on %2\$s"), + // @todo FIXME: Might be nice if the timestamp could be localised. + // TRANS: Page title for single direct message display when viewing user is the sender. + // TRANS: %1$s is the addressed user's nickname, $2$s is a timestamp. + return sprintf(_('Message to %1$s on %2$s'), $to->nickname, common_exact_date($this->message->created)); } else if ($this->user->id == $this->message->to_profile) { $from = $this->message->getFrom(); - return sprintf(_("Message from %1\$s on %2\$s"), + // @todo FIXME: Might be nice if the timestamp could be localised. + // TRANS: Page title for single message display. + // TRANS: %1$s is the sending user's nickname, $2$s is a timestamp. + return sprintf(_('Message from %1$s on %2$s'), $from->nickname, common_exact_date($this->message->created)); } } - - function getMessages() - { - $message = new Message(); - $message->id = $this->message->id; - $message->find(); - return $message; + + + function showContent() + { + $this->elementStart('ul', 'notices messages'); + $ml = new ShowMessageListItem($this, $this->message, $this->user); + $ml->show(); + $this->elementEnd('ul'); } - + + function isReadOnly($args) + { + return true; + } + + /** + * Don't show aside + * + * @return void + */ + + function showAside() { + } +} + +class ShowMessageListItem extends MessageListItem +{ + var $user; + + function __construct($out, $message, $user) + { + parent::__construct($out, $message); + $this->user = $user; + } + function getMessageProfile() { if ($this->user->id == $this->message->from_profile) { @@ -135,50 +161,4 @@ class ShowmessageAction extends MailboxAction return null; } } - - /** - * Don't show local navigation - * - * @return void - */ - - function showLocalNavBlock() - { - } - - /** - * Don't show page notice - * - * @return void - */ - - function showPageNoticeBlock() - { - } - - /** - * Don't show aside - * - * @return void - */ - - function showAside() - { - } - - /** - * Don't show any instructions - * - * @return string - */ - - function getInstructions() - { - return ''; - } - - function isReadOnly($args) - { - return true; - } } diff --git a/actions/shownotice.php b/actions/shownotice.php index 5fc863486c..b4af7dbaa2 100644 --- a/actions/shownotice.php +++ b/actions/shownotice.php @@ -167,11 +167,7 @@ class ShownoticeAction extends OwnerDesignAction function title() { - if (!empty($this->profile->fullname)) { - $base = $this->profile->fullname . ' (' . $this->profile->nickname . ')'; - } else { - $base = $this->profile->nickname; - } + $base = $this->profile->getFancyName(); return sprintf(_('%1$s\'s status on %2$s'), $base, @@ -335,8 +331,38 @@ class SingleNoticeItem extends DoFollowListItem $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)); + } } diff --git a/actions/showstream.php b/actions/showstream.php index fb5b061fbe..8a67d3fc9c 100644 --- a/actions/showstream.php +++ b/actions/showstream.php @@ -63,21 +63,26 @@ class ShowstreamAction extends ProfileAction function title() { - if (!empty($this->profile->fullname)) { - $base = $this->profile->fullname . ' (' . $this->user->nickname . ') '; - } else { - $base = $this->user->nickname; - } + $base = $this->profile->getFancyName(); if (!empty($this->tag)) { - $base .= sprintf(_(' tagged %s'), $this->tag); - } - - if ($this->page == 1) { - return $base; + if ($this->page == 1) { + // TRANS: Page title showing tagged notices in one user's stream. %1$s is the username, %2$s is the hash tag. + return sprintf(_('%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, %1$d is the page number. + return sprintf(_('%1$s tagged %2$s, page %3$d'), $base, $this->tag, $this->page); + } } else { - return sprintf(_('%1$s, page %2$d'), - $base, - $this->page); + if ($this->page == 1) { + return $base; + } 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'), + $base, + $this->page); + } } } @@ -117,6 +122,8 @@ class ShowstreamAction extends ProfileAction common_local_url('userrss', array('nickname' => $this->user->nickname, 'tag' => $this->tag)), + // TRANS: Title for link to notice feed. + // TRANS: %1$s is a user nickname, %2$s is a hashtag. sprintf(_('Notice feed for %1$s tagged %2$s (RSS 1.0)'), $this->user->nickname, $this->tag))); } @@ -124,6 +131,8 @@ class ShowstreamAction extends ProfileAction return array(new Feed(Feed::RSS1, common_local_url('userrss', array('nickname' => $this->user->nickname)), + // TRANS: Title for link to notice feed. + // TRANS: %s is a user nickname. sprintf(_('Notice feed for %s (RSS 1.0)'), $this->user->nickname)), new Feed(Feed::RSS2, @@ -131,6 +140,8 @@ class ShowstreamAction extends ProfileAction array( 'id' => $this->user->id, 'format' => 'rss')), + // TRANS: Title for link to notice feed. + // TRANS: %s is a user nickname. sprintf(_('Notice feed for %s (RSS 2.0)'), $this->user->nickname)), new Feed(Feed::ATOM, @@ -143,6 +154,8 @@ class ShowstreamAction extends ProfileAction new Feed(Feed::FOAF, common_local_url('foaf', array('nickname' => $this->user->nickname)), + // TRANS: Title for link to notice feed. FOAF stands for Friend of a Friend. + // TRANS: More information at http://www.foaf-project.org. %s is a user nickname. sprintf(_('FOAF for %s'), $this->user->nickname))); } @@ -194,17 +207,23 @@ class ShowstreamAction extends ProfileAction function showEmptyListMessage() { - $message = sprintf(_('This is the timeline for %1$s but %2$s hasn\'t posted anything yet.'), $this->user->nickname, $this->user->nickname) . ' '; + // TRANS: First sentence of empty list message for a stream. $1%s is a user nickname. + $message = sprintf(_('This is the timeline for %1$s, but %1$s hasn\'t posted anything yet.'), $this->user->nickname) . ' '; if (common_logged_in()) { $current_user = common_current_user(); if ($this->user->id === $current_user->id) { + // TRANS: Second sentence of empty list message for a stream for the user themselves. $message .= _('Seen anything interesting recently? You haven\'t posted any notices yet, now would be a good time to start :)'); } else { + // TRANS: Second sentence of empty list message for a non-self stream. %1$s is a user nickname, %2$s is a part of a URL. + // TRANS: This message contains a Markdown link. Keep "](" together. $message .= sprintf(_('You can try to nudge %1$s or [post something to them](%%%%action.newnotice%%%%?status_textarea=%2$s).'), $this->user->nickname, '@' . $this->user->nickname); } } else { + // TRANS: Second sentence of empty message for anonymous users. %s is a user nickname. + // TRANS: This message contains a Markdown link. Keep "](" together. $message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to them.'), $this->user->nickname); } @@ -240,11 +259,15 @@ class ShowstreamAction extends ProfileAction function showAnonymousMessage() { if (!(common_config('site','closed') || common_config('site','inviteonly'))) { + // TRANS: Announcement for anonymous users showing a stream if site registrations are open. + // TRANS: This message contains a Markdown link. Keep "](" together. $m = sprintf(_('**%s** has an account on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . 'based on the Free Software [StatusNet](http://status.net/) tool. ' . '[Join now](%%%%action.register%%%%) to follow **%s**\'s notices and many more! ([Read more](%%%%doc.help%%%%))'), $this->user->nickname, $this->user->nickname); } else { + // TRANS: Announcement for anonymous users showing a stream if site registrations are closed or invite only. + // TRANS: This message contains a Markdown link. Keep "](" together. $m = sprintf(_('**%s** has an account on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . 'based on the Free Software [StatusNet](http://status.net/) tool. '), $this->user->nickname, $this->user->nickname); @@ -284,7 +307,6 @@ class ProfileNoticeListItem extends DoFollowListItem * * @return void */ - function showRepeat() { if (!empty($this->repeat)) { @@ -295,13 +317,14 @@ class ProfileNoticeListItem extends DoFollowListItem 'class' => 'url'); if (!empty($this->profile->fullname)) { - $attrs['title'] = $this->profile->fullname . ' (' . $this->profile->nickname . ')'; + $attrs['title'] = $this->profile->getFancyName(); } $this->out->elementStart('span', 'repeat'); $text_link = XMLStringer::estring('a', $attrs, $this->profile->nickname); + // TRANS: Link to the author of a repeated notice. %s is a linked nickname. $this->out->raw(sprintf(_('Repeat of %s'), $text_link)); $this->out->elementEnd('span'); diff --git a/actions/sitenoticeadminpanel.php b/actions/sitenoticeadminpanel.php index bdcaa23557..797a6c4f4c 100644 --- a/actions/sitenoticeadminpanel.php +++ b/actions/sitenoticeadminpanel.php @@ -42,7 +42,6 @@ require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.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 SitenoticeadminpanelAction extends AdminPanelAction { /** @@ -50,9 +49,9 @@ class SitenoticeadminpanelAction extends AdminPanelAction * * @return string page title */ - function title() { + // TRANS: Page title for site-wide notice tab in admin panel. return _('Site Notice'); } @@ -61,9 +60,9 @@ class SitenoticeadminpanelAction extends AdminPanelAction * * @return string instructions */ - function getInstructions() { + // TRANS: Instructions for site-wide notice tab in admin panel. return _('Edit site-wide message'); } @@ -72,7 +71,6 @@ class SitenoticeadminpanelAction extends AdminPanelAction * * @return void */ - function showForm() { $form = new SiteNoticeAdminPanelForm($this); @@ -85,7 +83,6 @@ class SitenoticeadminpanelAction extends AdminPanelAction * * @return void */ - function saveSettings() { $siteNotice = $this->trimmed('site-notice'); @@ -100,6 +97,7 @@ class SitenoticeadminpanelAction extends AdminPanelAction $result = Config::save('site', 'notice', $siteNotice); if (!$result) { + // TRANS: Server error displayed when saving a site-wide notice was impossible. $this->ServerError(_("Unable to save site notice.")); } } @@ -110,7 +108,8 @@ class SitenoticeadminpanelAction extends AdminPanelAction if (mb_strlen($siteNotice) > 255) { $this->clientError( - _('Max length for the site-wide notice is 255 chars.') + // TRANS: Client error displayed when a site-wide notice was longer than allowed. + _('Maximum length for the site-wide notice is 255 characters.') ); } @@ -173,9 +172,11 @@ class SiteNoticeAdminPanelForm extends AdminForm $this->out->elementStart('li'); $this->out->textarea( 'site-notice', + // TRANS: Label for site-wide notice text field in admin panel. _('Site notice text'), common_config('site', 'notice'), - _('Site-wide notice text (255 chars max; HTML okay)') + // TRANS: Tooltip for site-wide notice text field in admin panel. + _('Site-wide notice text (255 characters maximum; HTML allowed)') ); $this->out->elementEnd('li'); @@ -192,9 +193,11 @@ class SiteNoticeAdminPanelForm extends AdminForm { $this->out->submit( 'submit', - _('Save'), + // TRANS: Button text for saving site notice in admin panel. + _m('BUTTON','Save'), 'submit', null, + // TRANS: Title for button to save site notice in admin panel. _('Save site notice') ); } diff --git a/actions/smssettings.php b/actions/smssettings.php index 6af1872a0e..f9a79e1664 100644 --- a/actions/smssettings.php +++ b/actions/smssettings.php @@ -44,7 +44,6 @@ require_once INSTALLDIR.'/lib/connectsettingsaction.php'; * * @see SettingsAction */ - class SmssettingsAction extends ConnectSettingsAction { /** @@ -52,7 +51,6 @@ class SmssettingsAction extends ConnectSettingsAction * * @return string Title of the page */ - function title() { // TRANS: Title for SMS settings. @@ -64,7 +62,6 @@ class SmssettingsAction extends ConnectSettingsAction * * @return instructions for use */ - function getInstructions() { // XXX: For consistency of parameters in messages, this should be a @@ -88,7 +85,6 @@ class SmssettingsAction extends ConnectSettingsAction * * @return void */ - function showContent() { if (!common_config('sms', 'enabled')) { @@ -219,7 +215,6 @@ class SmssettingsAction extends ConnectSettingsAction * * @todo very similar to EmailsettingsAction::getConfirmation(); refactor? */ - function getConfirmation() { $user = common_current_user(); @@ -246,7 +241,6 @@ class SmssettingsAction extends ConnectSettingsAction * * @return void */ - function handlePost() { // CSRF protection @@ -285,7 +279,6 @@ class SmssettingsAction extends ConnectSettingsAction * * @return void */ - function savePreferences() { $smsnotify = $this->boolean('smsnotify'); @@ -305,7 +298,7 @@ class SmssettingsAction extends ConnectSettingsAction if ($result === false) { common_log_db_error($user, 'UPDATE', __FILE__); // TRANS: Server error thrown on database error updating SMS preferences. - $this->serverError(_('Couldn\'t update user.')); + $this->serverError(_('Could not update user.')); return; } @@ -323,7 +316,6 @@ class SmssettingsAction extends ConnectSettingsAction * * @return void */ - function addAddress() { $user = common_current_user(); @@ -370,7 +362,7 @@ class SmssettingsAction extends ConnectSettingsAction if ($result === false) { common_log_db_error($confirm, 'INSERT', __FILE__); // TRANS: Server error thrown on database error adding SMS confirmation code. - $this->serverError(_('Couldn\'t insert confirmation code.')); + $this->serverError(_('Could not insert confirmation code.')); return; } @@ -395,7 +387,6 @@ class SmssettingsAction extends ConnectSettingsAction * * @return void */ - function cancelConfirmation() { $sms = $this->trimmed('sms'); @@ -419,7 +410,7 @@ class SmssettingsAction extends ConnectSettingsAction if (!$result) { common_log_db_error($confirm, 'DELETE', __FILE__); // TRANS: Server error thrown on database error canceling SMS phone number confirmation. - $this->serverError(_('Couldn\'t delete email confirmation.')); + $this->serverError(_('Could not delete email confirmation.')); return; } @@ -432,7 +423,6 @@ class SmssettingsAction extends ConnectSettingsAction * * @return void */ - function removeAddress() { $user = common_current_user(); @@ -461,7 +451,7 @@ class SmssettingsAction extends ConnectSettingsAction if (!$result) { common_log_db_error($user, 'UPDATE', __FILE__); // TRANS: Server error thrown on database error removing a registered SMS phone number. - $this->serverError(_('Couldn\'t update user.')); + $this->serverError(_('Could not update user.')); return; } $user->query('COMMIT'); @@ -479,7 +469,6 @@ class SmssettingsAction extends ConnectSettingsAction * * @return boolean does the number exist */ - function smsExists($sms) { $user = common_current_user(); @@ -498,7 +487,6 @@ class SmssettingsAction extends ConnectSettingsAction * * @return void */ - function carrierSelect() { $carrier = new Sms_carrier(); @@ -538,14 +526,13 @@ class SmssettingsAction extends ConnectSettingsAction * * @return void */ - function confirmCode() { $code = $this->trimmed('code'); if (!$code) { // TRANS: Message given saving SMS phone number confirmation code without having provided one. - $this->showForm(_('No code entered')); + $this->showForm(_('No code entered.')); return; } @@ -559,12 +546,12 @@ class SmssettingsAction extends ConnectSettingsAction * * @return void */ - function removeIncoming() { $user = common_current_user(); if (!$user->incomingemail) { + // TRANS: Form validation error displayed when trying to remove an incoming e-mail address while no address has been set. $this->showForm(_('No incoming email address.')); return; } @@ -575,7 +562,7 @@ class SmssettingsAction extends ConnectSettingsAction if (!$user->updateKeys($orig)) { common_log_db_error($user, 'UPDATE', __FILE__); - $this->serverError(_("Couldn't update user record.")); + $this->serverError(_("Could not update user record.")); } $this->showForm(_('Incoming email address removed.'), true); @@ -588,7 +575,6 @@ class SmssettingsAction extends ConnectSettingsAction * * @see Emailsettings::newIncoming */ - function newIncoming() { $user = common_current_user(); @@ -599,7 +585,7 @@ class SmssettingsAction extends ConnectSettingsAction if (!$user->updateKeys($orig)) { common_log_db_error($user, 'UPDATE', __FILE__); - $this->serverError(_("Couldn't update user record.")); + $this->serverError(_("Could not update user record.")); } $this->showForm(_('New incoming email address added.'), true); diff --git a/actions/subedit.php b/actions/subedit.php index cf6589e504..3b77aff584 100644 --- a/actions/subedit.php +++ b/actions/subedit.php @@ -19,6 +19,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +// @todo FIXME: Documentation needed. class SubeditAction extends Action { var $profile = null; @@ -28,6 +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. $this->clientError(_('Not logged in.')); return false; } @@ -43,6 +45,7 @@ class SubeditAction extends Action $id = $this->trimmed('profile'); if (!$id) { + // TRANS: Client error displayed trying a change a subscription without providing a profile. $this->clientError(_('No profile specified.')); return false; } @@ -50,6 +53,7 @@ class SubeditAction extends Action $this->profile = Profile::staticGet('id', $id); if (!$this->profile) { + // TRANS: Client error displayed trying a change a subscription for a non-existant profile ID. $this->clientError(_('No profile with that ID.')); return false; } @@ -67,6 +71,7 @@ class SubeditAction extends Action 'subscribed' => $this->profile->id)); if (!$sub) { + // TRANS: Client error displayed trying a change a subscription for a non-subscribed profile. $this->clientError(_('You are not subscribed to that profile.')); return false; } @@ -80,6 +85,7 @@ class SubeditAction extends Action if (!$result) { common_log_db_error($sub, 'UPDATE', __FILE__); + // TRANS: Server error displayed when updating a subscription fails with a database error. $this->serverError(_('Could not save subscription.')); return false; } diff --git a/actions/subscribers.php b/actions/subscribers.php index 2862f35c6d..ad522a4bae 100644 --- a/actions/subscribers.php +++ b/actions/subscribers.php @@ -100,8 +100,6 @@ class SubscribersAction extends GalleryAction } } - $subscribers->free(); - $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, $this->page, 'subscribers', array('nickname' => $this->user->nickname)); diff --git a/actions/subscriptions.php b/actions/subscriptions.php index ba2f67f2da..697577c107 100644 --- a/actions/subscriptions.php +++ b/actions/subscriptions.php @@ -106,8 +106,6 @@ class SubscriptionsAction extends GalleryAction } } - $subscriptions->free(); - $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, $this->page, 'subscriptions', array('nickname' => $this->user->nickname)); @@ -163,6 +161,21 @@ class SubscriptionsAction extends GalleryAction $cloud2 = new SubscriptionsPeopleSelfTagCloudSection($this); $cloud2->show(); } + + /** + * Link to feeds of subscriptions + * + * @return array of Feed objects + */ + function getFeeds() + { + return array(new Feed(Feed::ATOM, + common_local_url('AtomPubSubscriptionFeed', + array('subscriber' => $this->profile->id)), + // TRANS: Atom feed title. %s is a profile nickname. + sprintf(_('Subscription feed for %s (Atom)'), + $this->profile->nickname))); + } } // XXX SubscriptionsList and SubscriptionList are dangerously close diff --git a/actions/useradminpanel.php b/actions/useradminpanel.php index 04e0ca3e75..fc75e83b2d 100644 --- a/actions/useradminpanel.php +++ b/actions/useradminpanel.php @@ -45,7 +45,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 UseradminpanelAction extends AdminPanelAction { /** @@ -53,7 +52,6 @@ class UseradminpanelAction extends AdminPanelAction * * @return string page title */ - function title() { // TRANS: User admin panel title @@ -65,9 +63,9 @@ class UseradminpanelAction extends AdminPanelAction * * @return string instructions */ - function getInstructions() { + // TRANS: Instruction for user admin panel. return _('User settings for this StatusNet site'); } @@ -76,7 +74,6 @@ class UseradminpanelAction extends AdminPanelAction * * @return void */ - function showForm() { $form = new UserAdminPanelForm($this); @@ -89,7 +86,6 @@ class UseradminpanelAction extends AdminPanelAction * * @return void */ - function saveSettings() { static $settings = array( @@ -147,13 +143,15 @@ class UseradminpanelAction extends AdminPanelAction // Validate biolimit if (!Validate::number($values['profile']['biolimit'])) { - $this->clientError(_("Invalid bio limit. Must be numeric.")); + // TRANS: Form validation error in user admin panel when a non-numeric character limit was set. + $this->clientError(_('Invalid bio limit. Must be numeric.')); } // Validate welcome text if (mb_strlen($values['newuser']['welcome']) > 255) { - $this->clientError(_("Invalid welcome text. Max length is 255 characters.")); + // TRANS: Form validation error in user admin panel when welcome text is too long. + $this->clientError(_('Invalid welcome text. Maximum length is 255 characters.')); } // Validate default subscription @@ -163,7 +161,9 @@ class UseradminpanelAction extends AdminPanelAction if (empty($defuser)) { $this->clientError( sprintf( - _('Invalid default subscripton: \'%1$s\' is not user.'), + // TRANS: Client error displayed when trying to set a non-existing user as default subscription for new + // TRANS: users in user admin panel. %1$s is the invalid nickname. + _('Invalid default subscripton: \'%1$s\' is not a user.'), $values['newuser']['default'] ) ); @@ -179,7 +179,6 @@ class UserAdminPanelForm extends AdminForm * * @return int ID of the form */ - function id() { return 'useradminpanel'; @@ -190,7 +189,6 @@ class UserAdminPanelForm extends AdminForm * * @return string class of the form */ - function formClass() { return 'form_settings'; @@ -201,7 +199,6 @@ class UserAdminPanelForm extends AdminForm * * @return string URL of the action */ - function action() { return common_local_url('useradminpanel'); @@ -212,7 +209,6 @@ class UserAdminPanelForm extends AdminForm * * @return void */ - function formData() { $this->out->elementStart('fieldset', array('id' => 'settings_user-profile')); @@ -220,7 +216,9 @@ class UserAdminPanelForm extends AdminForm $this->out->elementStart('ul', 'form_data'); $this->li(); + // TRANS: Field label in user admin panel for setting the character limit for the bio field. $this->input('biolimit', _('Bio Limit'), + // TRANS: Tooltip in user admin panel for setting the character limit for the bio field. _('Maximum length of a profile bio in characters.'), 'profile'); $this->unli(); @@ -229,17 +227,22 @@ class UserAdminPanelForm extends AdminForm $this->out->elementEnd('fieldset'); $this->out->elementStart('fieldset', array('id' => 'settings_user-newusers')); + // TRANS: Form legend in user admin panel. $this->out->element('legend', null, _('New users')); $this->out->elementStart('ul', 'form_data'); $this->li(); + // TRANS: Field label in user admin panel for setting new user welcome text. $this->input('welcome', _('New user welcome'), - _('Welcome text for new users (Max 255 chars).'), + // TRANS: Tooltip in user admin panel for setting new user welcome text. + _('Welcome text for new users (maximum 255 characters).'), 'newuser'); $this->unli(); $this->li(); + // TRANS: Field label in user admin panel for setting default subscription for new users. $this->input('default', _('Default subscription'), + // TRANS: Tooltip in user admin panel for setting default subscription for new users. _('Automatically subscribe new users to this user.'), 'newuser'); $this->unli(); @@ -249,21 +252,21 @@ class UserAdminPanelForm extends AdminForm $this->out->elementEnd('fieldset'); $this->out->elementStart('fieldset', array('id' => 'settings_user-invitations')); + // TRANS: Form legend in user admin panel. $this->out->element('legend', null, _('Invitations')); $this->out->elementStart('ul', 'form_data'); $this->li(); + // TRANS: Field label for checkbox in user admin panel for allowing users to invite friend using site e-mail. $this->out->checkbox('invite-enabled', _('Invitations enabled'), (bool) $this->value('enabled', 'invite'), + // TRANS: Tooltip for checkbox in user admin panel for allowing users to invite friend using site e-mail. _('Whether to allow users to invite new users.')); $this->unli(); $this->out->elementEnd('ul'); $this->out->elementEnd('fieldset'); - - - } /** @@ -278,7 +281,6 @@ class UserAdminPanelForm extends AdminForm * * @return void */ - function input($setting, $title, $instructions, $section='site') { $this->out->input("$section-$setting", $title, $this->value($setting, $section), $instructions); @@ -289,9 +291,14 @@ class UserAdminPanelForm extends AdminForm * * @return void */ - function formActions() { - $this->out->submit('submit', _('Save'), 'submit', null, _('Save user settings')); + $this->out->submit('submit', + // TRANS: Button text to save user settings in user admin panel. + _m('BUTTON','Save'), + 'submit', + null, + // TRANS: Title for button to save user settings in user admin panel. + _('Save user settings')); } } diff --git a/actions/userdesignsettings.php b/actions/userdesignsettings.php index 1cf8780006..b82dea8dd6 100644 --- a/actions/userdesignsettings.php +++ b/actions/userdesignsettings.php @@ -46,7 +46,6 @@ require_once INSTALLDIR . '/lib/designsettings.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 UserDesignSettingsAction extends DesignSettingsAction { /** @@ -70,7 +69,6 @@ class UserDesignSettingsAction extends DesignSettingsAction * * @return string Title of the page */ - function title() { return _('Profile design'); @@ -81,7 +79,6 @@ class UserDesignSettingsAction extends DesignSettingsAction * * @return instructions for use */ - function getInstructions() { return _('Customize the way your profile looks ' . @@ -93,7 +90,6 @@ class UserDesignSettingsAction extends DesignSettingsAction * * @return Design */ - function getWorkingDesign() { $user = common_current_user(); @@ -108,7 +104,6 @@ class UserDesignSettingsAction extends DesignSettingsAction * * @return void */ - function showContent() { $design = $this->getWorkingDesign(); @@ -125,7 +120,6 @@ class UserDesignSettingsAction extends DesignSettingsAction * * @return void */ - function saveDesign() { foreach ($this->args as $key => $val) { @@ -168,7 +162,6 @@ class UserDesignSettingsAction extends DesignSettingsAction $design = $user->getDesign(); if (!empty($design)) { - $original = clone($design); $design->backgroundcolor = $bgcolor->intValue(); @@ -183,13 +176,11 @@ class UserDesignSettingsAction extends DesignSettingsAction if ($result === false) { common_log_db_error($design, 'UPDATE', __FILE__); - $this->showForm(_('Couldn\'t update your design.')); + $this->showForm(_('Could not update your design.')); return; } - // update design } else { - $user->query('BEGIN'); // save new design @@ -236,7 +227,6 @@ class UserDesignSettingsAction extends DesignSettingsAction * * @return nothing */ - function sethd() { @@ -281,5 +271,4 @@ class UserDesignSettingsAction extends DesignSettingsAction $this->showForm(_('Enjoy your hotdog!'), true); } - } diff --git a/plugins/OStatus/actions/userxrd.php b/actions/userxrd.php similarity index 95% rename from plugins/OStatus/actions/userxrd.php rename to actions/userxrd.php index 575a07c409..582f7a35e7 100644 --- a/plugins/OStatus/actions/userxrd.php +++ b/actions/userxrd.php @@ -32,9 +32,9 @@ class UserxrdAction extends XrdAction parent::prepare($args); $this->uri = $this->trimmed('uri'); - $this->uri = Discovery::normalize($this->uri); + $this->uri = self::normalize($this->uri); - if (Discovery::isWebfinger($this->uri)) { + if (self::isWebfinger($this->uri)) { $parts = explode('@', substr(urldecode($this->uri), 5)); if (count($parts) == 2) { list($nick, $domain) = $parts; diff --git a/classes/Design.php b/classes/Design.php index a8fdb72191..f4834c714e 100644 --- a/classes/Design.php +++ b/classes/Design.php @@ -107,7 +107,7 @@ class Design extends Memcached_DataObject static function toWebColor($color) { - if ($color == null) { + if ($color === null || $color === '') { return null; } diff --git a/classes/Fave.php b/classes/Fave.php index 9922ae45c5..4a9cfaae06 100644 --- a/classes/Fave.php +++ b/classes/Fave.php @@ -85,6 +85,19 @@ class Fave extends Memcached_DataObject return $ids; } + /** + * Note that the sorting for this is by order of *fave* not order of *notice*. + * + * @fixme add since_id, max_id support? + * + * @param $user_id + * @param $own + * @param $offset + * @param $limit + * @param $since_id + * @param $max_id + * @return + */ function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id) { $fav = new Fave(); @@ -138,6 +151,9 @@ class Fave extends Memcached_DataObject $act = new Activity(); $act->verb = ActivityVerb::FAVORITE; + + // FIXME: rationalize this with URL below + $act->id = TagURI::mint('favor:%d:%d:%s', $profile->id, $notice->id, @@ -155,6 +171,41 @@ class Fave extends Memcached_DataObject $act->actor = ActivityObject::fromProfile($profile); $act->objects[] = ActivityObject::fromNotice($notice); + $url = common_local_url('AtomPubShowFavorite', + array('profile' => $this->user_id, + 'notice' => $this->notice_id)); + + $act->selfLink = $url; + $act->editLink = $url; + return $act; } + + /** + * Fetch a stream of favorites by profile + * + * @param integer $profileId Profile that faved + * @param integer $offset Offset from last + * @param integer $limit Number to get + * + * @return mixed stream of faves, use fetch() to iterate + * + * @todo Cache results + * @todo integrate with Fave::stream() + */ + + static function byProfile($profileId, $offset, $limit) + { + $fav = new Fave(); + + $fav->user_id = $profileId; + + $fav->orderBy('modified DESC'); + + $fav->limit($offset, $limit); + + $fav->find(); + + return $fav; + } } diff --git a/classes/File.php b/classes/File.php index da029e39b6..e9a0131c4e 100644 --- a/classes/File.php +++ b/classes/File.php @@ -55,14 +55,20 @@ class File extends Memcached_DataObject return 'http://www.facebook.com/login.php' === $url; } - function getAttachments($post_id) { - $query = "select file.* from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $this->escape($post_id); - $this->query($query); + /** + * Get the attachments for a particlar notice. + * + * @param int $post_id + * @return array of File objects + */ + static function getAttachments($post_id) { + $file = new File(); + $query = "select file.* from file join file_to_post on (file_id = file.id) where post_id = " . $file->escape($post_id); + $file = Memcached_DataObject::cachedQuery('File', $query); $att = array(); - while ($this->fetch()) { - $att[] = clone($this); + while ($file->fetch()) { + $att[] = clone($file); } - $this->free(); return $att; } @@ -116,10 +122,24 @@ class File extends Memcached_DataObject } /** + * Go look at a URL and possibly save data about it if it's new: + * - follow redirect chains and store them in file_redirection + * - look up oEmbed data and save it in file_oembed + * - if a thumbnail is available, save it in file_thumbnail + * - save file record with basic info + * - optionally save a file_to_post record + * - return the File object with the full reference + * * @fixme refactor this mess, it's gotten pretty scary. - * @param bool $followRedirects + * @param string $given_url the URL we're looking at + * @param int $notice_id (optional) + * @param bool $followRedirects defaults to true + * + * @return mixed File on success, -1 on some errors + * + * @throws ServerException on some errors */ - function processNew($given_url, $notice_id=null, $followRedirects=true) { + public function processNew($given_url, $notice_id=null, $followRedirects=true) { if (empty($given_url)) return -1; // error, no url to process $given_url = File_redirection::_canonUrl($given_url); if (empty($given_url)) return -1; // error, no url to process @@ -169,9 +189,9 @@ class File extends Memcached_DataObject if (empty($x)) { $x = File::staticGet($file_id); if (empty($x)) { - // FIXME: This could possibly be a clearer message :) + // @todo FIXME: This could possibly be a clearer message :) // TRANS: Server exception thrown when... Robin thinks something is impossible! - throw new ServerException(_("Robin thinks something is impossible.")); + throw new ServerException(_('Robin thinks something is impossible.')); } } @@ -186,8 +206,10 @@ class File extends Memcached_DataObject if ($fileSize > common_config('attachments', 'file_quota')) { // TRANS: Message given if an upload is larger than the configured maximum. // TRANS: %1$d is the byte limit for uploads, %2$d is the byte count for the uploaded file. - return sprintf(_('No file may be larger than %1$d bytes ' . - 'and the file you sent was %2$d bytes. Try to upload a smaller version.'), + // TRANS: %1$s is used for plural. + return sprintf(_m('No file may be larger than %1$d byte and the file you sent was %2$d bytes. Try to upload a smaller version.', + 'No file may be larger than %1$d bytes and the file you sent was %2$d bytes. Try to upload a smaller version.', + common_config('attachments', 'file_quota')), common_config('attachments', 'file_quota'), $fileSize); } @@ -197,8 +219,11 @@ class File extends Memcached_DataObject $total = $this->total + $fileSize; if ($total > common_config('attachments', 'user_quota')) { // TRANS: Message given if an upload would exceed user quota. - // TRANS: %d (number) is the user quota in bytes. - return sprintf(_('A file this large would exceed your user quota of %d bytes.'), common_config('attachments', 'user_quota')); + // TRANS: %d (number) is the user quota in bytes and is used for plural. + return sprintf(_m('A file this large would exceed your user quota of %d byte.', + 'A file this large would exceed your user quota of %d bytes.', + common_config('attachments', 'user_quota')), + common_config('attachments', 'user_quota')); } $query .= ' AND EXTRACT(month FROM file.modified) = EXTRACT(month FROM now()) and EXTRACT(year FROM file.modified) = EXTRACT(year FROM now())'; $this->query($query); @@ -206,8 +231,11 @@ class File extends Memcached_DataObject $total = $this->total + $fileSize; if ($total > common_config('attachments', 'monthly_quota')) { // TRANS: Message given id an upload would exceed a user's monthly quota. - // TRANS: $d (number) is the monthly user quota in bytes. - return sprintf(_('A file this large would exceed your monthly quota of %d bytes.'), common_config('attachments', 'monthly_quota')); + // TRANS: $d (number) is the monthly user quota in bytes and is used for plural. + return sprintf(_m('A file this large would exceed your monthly quota of %d byte.', + 'A file this large would exceed your monthly quota of %d bytes.', + common_config('attachments', 'monthly_quota')), + common_config('attachments', 'monthly_quota')); } return true; } @@ -217,12 +245,19 @@ class File extends Memcached_DataObject static function filename($profile, $basename, $mimetype) { require_once 'MIME/Type/Extension.php'; + + // We have to temporarily disable auto handling of PEAR errors... + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $mte = new MIME_Type_Extension(); - try { - $ext = $mte->getExtension($mimetype); - } catch ( Exception $e) { + $ext = $mte->getExtension($mimetype); + if (PEAR::isError($ext)) { $ext = strtolower(preg_replace('/\W/', '', $mimetype)); } + + // Restore error handling. + PEAR::staticPopErrorHandling(); + $nickname = $profile->nickname; $datestamp = strftime('%Y%m%dT%H%M%S', time()); $random = strtolower(common_confirmation_code(32)); @@ -292,9 +327,7 @@ class File extends Memcached_DataObject } $protocol = 'https'; - } else { - $path = common_config('attachments', 'path'); $server = common_config('attachments', 'server'); @@ -339,22 +372,28 @@ class File extends Memcached_DataObject $mimetype = substr($mimetype,0,$semicolon); } if(in_array($mimetype,$notEnclosureMimeTypes)){ + // Never treat generic HTML links as an enclosure type! + // But if we have oEmbed info, we'll consider it golden. $oembed = File_oembed::staticGet('file_id',$this->id); - if($oembed){ + if($oembed && in_array($oembed->type, array('photo', 'video'))){ $mimetype = strtolower($oembed->mimetype); $semicolon = strpos($mimetype,';'); if($semicolon){ $mimetype = substr($mimetype,0,$semicolon); } - if(in_array($mimetype,$notEnclosureMimeTypes)){ - return false; - }else{ + // @fixme uncertain if this is right. + // we want to expose things like YouTube videos as + // viewable attachments, but don't expose them as + // downloadable enclosures.....? + //if (in_array($mimetype, $notEnclosureMimeTypes)) { + // return false; + //} else { if($oembed->mimetype) $enclosure->mimetype=$oembed->mimetype; if($oembed->url) $enclosure->url=$oembed->url; if($oembed->title) $enclosure->title=$oembed->title; if($oembed->modified) $enclosure->modified=$oembed->modified; unset($oembed->size); - } + //} } else { return false; } @@ -369,4 +408,112 @@ class File extends Memcached_DataObject $enclosure = $this->getEnclosure(); return !empty($enclosure); } + + /** + * Get the attachment's thumbnail record, if any. + * + * @return File_thumbnail + */ + function getThumbnail() + { + return File_thumbnail::staticGet('file_id', $this->id); + } + + /** + * Blow the cache of notices that link to this URL + * + * @param boolean $last Whether to blow the "last" cache too + * + * @return void + */ + + function blowCache($last=false) + { + self::blow('file:notice-ids:%s', $this->url); + if ($last) { + self::blow('file:notice-ids:%s;last', $this->url); + } + self::blow('file:notice-count:%d', $this->id); + } + + /** + * Stream of notices linking to this URL + * + * @param integer $offset Offset to show; default is 0 + * @param integer $limit Limit of notices to show + * @param integer $since_id Since this notice + * @param integer $max_id Before this notice + * + * @return array ids of notices that link to this file + */ + + function stream($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) + { + $ids = Notice::stream(array($this, '_streamDirect'), + array(), + 'file:notice-ids:'.$this->url, + $offset, $limit, $since_id, $max_id); + + return Notice::getStreamByIds($ids); + } + + /** + * Stream of notices linking to this URL + * + * @param integer $offset Offset to show; default is 0 + * @param integer $limit Limit of notices to show + * @param integer $since_id Since this notice + * @param integer $max_id Before this notice + * + * @return array ids of notices that link to this file + */ + + function _streamDirect($offset, $limit, $since_id, $max_id) + { + $f2p = new File_to_post(); + + $f2p->selectAdd(); + $f2p->selectAdd('post_id'); + + $f2p->file_id = $this->id; + + Notice::addWhereSinceId($f2p, $since_id, 'post_id', 'modified'); + Notice::addWhereMaxId($f2p, $max_id, 'post_id', 'modified'); + + $f2p->orderBy('modified DESC, post_id DESC'); + + if (!is_null($offset)) { + $f2p->limit($offset, $limit); + } + + $ids = array(); + + if ($f2p->find()) { + while ($f2p->fetch()) { + $ids[] = $f2p->post_id; + } + } + + return $ids; + } + + function noticeCount() + { + $cacheKey = sprintf('file:notice-count:%d', $this->id); + + $count = self::cacheGet($cacheKey); + + if ($count === false) { + + $f2p = new File_to_post(); + + $f2p->file_id = $this->id; + + $count = $f2p->count(); + + self::cacheSet($cacheKey, $count); + } + + return $count; + } } diff --git a/classes/File_oembed.php b/classes/File_oembed.php index 4813d5dda5..b7bf3a5dae 100644 --- a/classes/File_oembed.php +++ b/classes/File_oembed.php @@ -58,26 +58,16 @@ class File_oembed extends Memcached_DataObject return array(false, false, false); } - function _getOembed($url, $maxwidth = 500, $maxheight = 400) { - require_once INSTALLDIR.'/extlib/Services/oEmbed.php'; + function _getOembed($url) { $parameters = array( - 'maxwidth'=>$maxwidth, - 'maxheight'=>$maxheight, + 'maxwidth' => common_config('attachments', 'thumb_width'), + 'maxheight' => common_config('attachments', 'thumb_height'), ); - try{ - $oEmbed = new Services_oEmbed($url); - $object = $oEmbed->getObject($parameters); - return $object; - }catch(Exception $e){ - try{ - $oEmbed = new Services_oEmbed($url, array( - Services_oEmbed::OPTION_API => common_config('oohembed', 'endpoint') - )); - $object = $oEmbed->getObject($parameters); - return $object; - }catch(Exception $ex){ - return false; - } + try { + return oEmbedHelper::getObject($url, $parameters); + } catch (Exception $e) { + common_log(LOG_ERR, "Error during oembed lookup for $url - " . $e->getMessage()); + return false; } } @@ -120,7 +110,7 @@ class File_oembed extends Memcached_DataObject } } $file_oembed->insert(); - if (!empty($data->thumbnail_url)) { + if (!empty($data->thumbnail_url) || ($data->type == 'photo')) { $ft = File_thumbnail::staticGet('file_id', $file_id); if (!empty($ft)) { common_log(LOG_WARNING, "Strangely, a File_thumbnail object exists for new file $file_id", diff --git a/classes/File_redirection.php b/classes/File_redirection.php index 68fed77e8b..53c15bf8b2 100644 --- a/classes/File_redirection.php +++ b/classes/File_redirection.php @@ -91,9 +91,16 @@ class File_redirection extends Memcached_DataObject $request->setMethod(HTTP_Request2::METHOD_HEAD); $response = $request->send(); - if (405 == $response->getStatus()) { + if (405 == $response->getStatus() || 204 == $response->getStatus()) { + // HTTP 405 Unsupported Method // Server doesn't support HEAD method? Can this really happen? // We'll try again as a GET and ignore the response data. + // + // HTTP 204 No Content + // YFrog sends 204 responses back for our HEAD checks, which + // seems like it may be a logic error in their servers. If + // we get a 204 back, re-run it as a GET... if there's really + // no content it'll be cheap. :) $request = self::_commonHttp($short_url, $redirs); $response = $request->send(); } @@ -132,6 +139,7 @@ class File_redirection extends Memcached_DataObject * reached. * * @param string $in_url + * @param boolean $discover true to attempt dereferencing the redirect if we don't know it already * @return mixed one of: * string - target URL, if this is a direct link or a known redirect * array - redirect info if this is an *unknown* redirect: @@ -143,7 +151,7 @@ class File_redirection extends Memcached_DataObject * size (optional): byte size from Content-Length header * time (optional): timestamp from Last-Modified header */ - public function where($in_url) { + public function where($in_url, $discover=true) { // let's see if we know this... $a = File::staticGet('url', $in_url); @@ -159,8 +167,13 @@ class File_redirection extends Memcached_DataObject } } - $ret = File_redirection::lookupWhere($in_url); - return $ret; + if ($discover) { + $ret = File_redirection::lookupWhere($in_url); + return $ret; + } else { + // No manual dereferencing; leave the unknown URL as is. + return $in_url; + } } /** @@ -174,13 +187,14 @@ class File_redirection extends Memcached_DataObject * may be saved. * * @param string $long_url + * @param User $user whose shortening options to use; defaults to the current web session user * @return string */ - function makeShort($long_url) { + function makeShort($long_url, $user=null) { $canon = File_redirection::_canonUrl($long_url); - $short_url = File_redirection::_userMakeShort($canon); + $short_url = File_redirection::_userMakeShort($canon, $user); // Did we get one? Is it shorter? if (!empty($short_url) && mb_strlen($short_url) < mb_strlen($long_url)) { @@ -190,8 +204,8 @@ class File_redirection extends Memcached_DataObject } } - function _userMakeShort($long_url) { - $short_url = common_shorten_url($long_url); + function _userMakeShort($long_url, User $user=null) { + $short_url = common_shorten_url($long_url, $user); if (!empty($short_url) && $short_url != $long_url) { $short_url = (string)$short_url; // store it @@ -235,6 +249,18 @@ class File_redirection extends Memcached_DataObject return null; } + /** + * Basic attempt to canonicalize a URL, cleaning up some standard variants + * such as funny syntax or a missing path. Used internally when cleaning + * up URLs for storage and following redirect chains. + * + * Note that despite being on File_redirect, this function DOES NOT perform + * any dereferencing of redirects. + * + * @param string $in_url input URL + * @param string $default_scheme if given a bare link; defaults to 'http://' + * @return string + */ function _canonUrl($in_url, $default_scheme = 'http://') { if (empty($in_url)) return false; $out_url = $in_url; diff --git a/classes/File_thumbnail.php b/classes/File_thumbnail.php index edae8ac21a..17bac7f08c 100644 --- a/classes/File_thumbnail.php +++ b/classes/File_thumbnail.php @@ -48,12 +48,45 @@ class File_thumbnail extends Memcached_DataObject return array(false, false, false); } - function saveNew($data, $file_id) { + /** + * Save oEmbed-provided thumbnail data + * + * @param object $data + * @param int $file_id + */ + public static function saveNew($data, $file_id) { + if (!empty($data->thumbnail_url)) { + // Non-photo types such as video will usually + // show us a thumbnail, though it's not required. + self::saveThumbnail($file_id, + $data->thumbnail_url, + $data->thumbnail_width, + $data->thumbnail_height); + } else if ($data->type == 'photo') { + // The inline photo URL given should also fit within + // our requested thumbnail size, per oEmbed spec. + self::saveThumbnail($file_id, + $data->url, + $data->width, + $data->height); + } + } + + /** + * Save a thumbnail record for the referenced file record. + * + * @param int $file_id + * @param string $url + * @param int $width + * @param int $height + */ + static function saveThumbnail($file_id, $url, $width, $height) + { $tn = new File_thumbnail; $tn->file_id = $file_id; - $tn->url = $data->thumbnail_url; - $tn->width = intval($data->thumbnail_width); - $tn->height = intval($data->thumbnail_height); + $tn->url = $url; + $tn->width = intval($width); + $tn->height = intval($height); $tn->insert(); } } diff --git a/classes/File_to_post.php b/classes/File_to_post.php index 530921adcb..bcb6771f4f 100644 --- a/classes/File_to_post.php +++ b/classes/File_to_post.php @@ -52,6 +52,12 @@ class File_to_post extends Memcached_DataObject $f2p->file_id = $file_id; $f2p->post_id = $notice_id; $f2p->insert(); + + $f = File::staticGet($file_id); + + if (!empty($f)) { + $f->blowCache(); + } } if (empty($seen[$notice_id])) { @@ -66,4 +72,13 @@ class File_to_post extends Memcached_DataObject { return Memcached_DataObject::pkeyGet('File_to_post', $kv); } + + function delete() + { + $f = File::staticGet('id', $this->file_id); + if (!empty($f)) { + $f->blowCache(); + } + return parent::delete(); + } } diff --git a/classes/Group_member.php b/classes/Group_member.php index c40d06a1db..2cf31cf123 100644 --- a/classes/Group_member.php +++ b/classes/Group_member.php @@ -26,6 +26,15 @@ class Group_member extends Memcached_DataObject return Memcached_DataObject::pkeyGet('Group_member', $kv); } + /** + * Method to add a user to a group. + * + * @param integer $group_id Group to add to + * @param integer $profile_id Profile being added + * + * @return Group_member new membership object + */ + static function join($group_id, $profile_id) { $member = new Group_member(); @@ -42,7 +51,7 @@ class Group_member extends Memcached_DataObject throw new Exception(_("Group join failed.")); } - return true; + return $member; } static function leave($group_id, $profile_id) @@ -92,6 +101,31 @@ class Group_member extends Memcached_DataObject return $group; } + /** + * Get stream of memberships by member + * + * @param integer $memberId profile ID of the member to fetch for + * @param integer $offset offset from start of stream to get + * @param integer $limit number of memberships to get + * + * @return Group_member stream of memberships, use fetch() to iterate + */ + + static function byMember($memberId, $offset=0, $limit=GROUPS_PER_PAGE) + { + $membership = new Group_member(); + + $membership->profile_id = $memberId; + + $membership->orderBy('created DESC'); + + $membership->limit($offset, $limit); + + $membership->find(); + + return $membership; + } + function asActivity() { $member = $this->getMember(); @@ -118,6 +152,13 @@ class Group_member extends Memcached_DataObject $member->getBestName(), $group->getBestName()); + $url = common_local_url('AtomPubShowMembership', + array('profile' => $member->id, + 'group' => $group->id)); + + $act->selfLink = $url; + $act->editLink = $url; + return $act; } } diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index ccfd886a1d..867b40cf3c 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -74,7 +74,7 @@ class Memcached_DataObject extends Safe_DataObject return $i; } else { $i = DB_DataObject::factory($cls); - if (empty($i)) { + if (empty($i) || PEAR::isError($i)) { return false; } foreach ($kv as $k => $v) { @@ -338,7 +338,12 @@ class Memcached_DataObject extends Safe_DataObject } $start = microtime(true); - $result = parent::_query($string); + $result = null; + if (Event::handle('StartDBQuery', array($this, $string, &$result))) { + common_perf_counter('query', $string); + $result = parent::_query($string); + Event::handle('EndDBQuery', array($this, $string, &$result)); + } $delta = microtime(true) - $start; $limit = common_config('db', 'log_slow_queries'); @@ -476,6 +481,10 @@ class Memcached_DataObject extends Safe_DataObject } } } + // Needed to make timestamp values usefully comparable. + if (common_config('db', 'type') == 'mysql') { + parent::_query("set time_zone='+0:00'"); + } } return $result; diff --git a/classes/Message.php b/classes/Message.php index 353dc01f99..484d1f724c 100644 --- a/classes/Message.php +++ b/classes/Message.php @@ -45,12 +45,19 @@ class Message extends Memcached_DataObject throw new ClientException(_('You are banned from sending direct messages.')); } + $user = User::staticGet('id', $sender->id); + $msg = new Message(); $msg->from_profile = $from; $msg->to_profile = $to; - $msg->content = common_shorten_links($content); - $msg->rendered = common_render_text($content); + if ($user) { + // Use the sender's URL shortening options. + $msg->content = $user->shortenLinks($content); + } else { + $msg->content = common_shorten_links($content); + } + $msg->rendered = common_render_text($msg->content); $msg->created = common_sql_now(); $msg->source = $source; diff --git a/classes/Notice.php b/classes/Notice.php index 60989f9bac..4522d1fc38 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -109,6 +109,11 @@ class Notice extends Memcached_DataObject // @fixme we have some cases where things get re-run and so the // insert fails. $deleted = Deleted_notice::staticGet('id', $this->id); + + if (!$deleted) { + $deleted = Deleted_notice::staticGet('uri', $this->uri); + } + if (!$deleted) { $deleted = new Deleted_notice(); @@ -130,6 +135,7 @@ class Notice extends Memcached_DataObject $this->clearFaves(); $this->clearTags(); $this->clearGroupInboxes(); + $this->clearFiles(); // NOTE: we don't clear inboxes // NOTE: we don't clear queue items @@ -234,6 +240,8 @@ class Notice extends Memcached_DataObject * in place of extracting # tags from content * array 'urls' list of attached/referred URLs to save with the * notice in place of extracting links from content + * boolean 'distribute' whether to distribute the notice, default true + * * @fixme tag override * * @return Notice @@ -243,7 +251,8 @@ class Notice extends Memcached_DataObject $defaults = array('uri' => null, 'url' => null, 'reply_to' => null, - 'repeat_of' => null); + 'repeat_of' => null, + 'distribute' => true); if (!empty($options)) { $options = $options + $defaults; @@ -256,9 +265,14 @@ class Notice extends Memcached_DataObject $is_local = Notice::LOCAL_PUBLIC; } - $profile = Profile::staticGet($profile_id); - - $final = common_shorten_links($content); + $profile = Profile::staticGet('id', $profile_id); + $user = User::staticGet('id', $profile_id); + if ($user) { + // Use the local user's shortening preferences, if applicable. + $final = $user->shortenLinks($content); + } else { + $final = common_shorten_links($content); + } if (Notice::contentTooLong($final)) { // TRANS: Client exception thrown if a notice contains too many characters. @@ -421,8 +435,10 @@ class Notice extends Memcached_DataObject $notice->saveUrls(); } - // Prepare inbox delivery, may be queued to background. - $notice->distribute(); + if ($distribute) { + // Prepare inbox delivery, may be queued to background. + $notice->distribute(); + } return $notice; } @@ -430,7 +446,10 @@ class Notice extends Memcached_DataObject function blowOnInsert($conversation = false) { self::blow('profile:notice_ids:%d', $this->profile_id); - self::blow('public'); + + if ($this->isPublic()) { + self::blow('public'); + } // XXX: Before we were blowing the casche only if the notice id // was not the root of the conversation. What to do now? @@ -465,7 +484,10 @@ class Notice extends Memcached_DataObject $this->blowOnInsert(); self::blow('profile:notice_ids:%d;last', $this->profile_id); - self::blow('public;last'); + + if ($this->isPublic()) { + self::blow('public;last'); + } } /** save all urls in the notice to the db @@ -476,7 +498,9 @@ class Notice extends Memcached_DataObject * @return void */ function saveUrls() { - common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); + if (common_config('attachments', 'process_links')) { + common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); + } } /** @@ -489,17 +513,18 @@ class Notice extends Memcached_DataObject */ function saveKnownUrls($urls) { - // @fixme validation? - foreach (array_unique($urls) as $url) { - File::processNew($url, $this->id); + if (common_config('attachments', 'process_links')) { + // @fixme validation? + foreach (array_unique($urls) as $url) { + File::processNew($url, $this->id); + } } } /** * @private callback */ - function saveUrl($data) { - list($url, $notice_id) = $data; + function saveUrl($url, $notice_id) { File::processNew($url, $notice_id); } @@ -524,10 +549,8 @@ class Notice extends Memcached_DataObject $notice = new Notice(); $notice->profile_id = $profile_id; $notice->content = $content; - if (common_config('db','type') == 'pgsql') - $notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit')); - else - $notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit')); + $threshold = common_sql_date(time() - common_config('site', 'dupelimit')); + $notice->whereAdd(sprintf("created > '%s'", $notice->escape($threshold))); $cnt = $notice->count(); return ($cnt == 0); @@ -648,7 +671,7 @@ class Notice extends Memcached_DataObject $notice->selectAdd(); // clears it $notice->selectAdd('id'); - $notice->orderBy('id DESC'); + $notice->orderBy('created DESC, id DESC'); if (!is_null($offset)) { $notice->limit($offset, $limit); @@ -662,13 +685,8 @@ class Notice extends Memcached_DataObject $notice->whereAdd('is_local !='. Notice::GATEWAY); } - if ($since_id != 0) { - $notice->whereAdd('id > ' . $since_id); - } - - if ($max_id != 0) { - $notice->whereAdd('id <= ' . $max_id); - } + Notice::addWhereSinceId($notice, $since_id); + Notice::addWhereMaxId($notice, $max_id); $ids = array(); @@ -703,19 +721,14 @@ class Notice extends Memcached_DataObject $notice->conversation = $id; - $notice->orderBy('id DESC'); + $notice->orderBy('created DESC, id DESC'); if (!is_null($offset)) { $notice->limit($offset, $limit); } - if ($since_id != 0) { - $notice->whereAdd('id > ' . $since_id); - } - - if ($max_id != 0) { - $notice->whereAdd('id <= ' . $max_id); - } + Notice::addWhereSinceId($notice, $since_id); + Notice::addWhereMaxId($notice, $max_id); $ids = array(); @@ -812,9 +825,18 @@ class Notice extends Memcached_DataObject // 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)) { + if (empty($user) || $user->hasBlocked($profile) || + ($originalProfile && $user->hasBlocked($originalProfile))) { unset($ni[$id]); } } @@ -904,7 +926,7 @@ class Notice extends Memcached_DataObject { if (!is_array($group_ids)) { // TRANS: Server exception thrown when no array is provided to the method saveKnownGroups(). - throw new ServerException(_("Bad type provided to saveKnownGroups")); + throw new ServerException(_('Bad type provided to saveKnownGroups.')); } $groups = array(); @@ -942,7 +964,7 @@ class Notice extends Memcached_DataObject $groups = array(); /* extract all !group */ - $count = preg_match_all('/(?:^|\s)!([A-Za-z0-9]{1,64})/', + $count = preg_match_all('/(?:^|\s)!(' . Nickname::DISPLAY_FMT . ')/', strtolower($this->content), $match); if (!$count) { @@ -1220,60 +1242,156 @@ class Notice extends Memcached_DataObject return $groups; } + /** + * Convert a notice into an activity for export. + * + * @param User $cur Current user + * + * @return Activity activity object representing this Notice. + */ + function asActivity() { - $profile = $this->getProfile(); + $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id)); + + if (!empty($act)) { + return $act; + } $act = new Activity(); - $act->actor = ActivityObject::fromProfile($profile); - $act->verb = ActivityVerb::POST; - $act->objects[] = ActivityObject::fromNotice($this); + if (Event::handle('StartNoticeAsActivity', array($this, &$act))) { - $act->time = strtotime($this->created); - $act->link = $this->bestUrl(); + $profile = $this->getProfile(); - $act->content = common_xml_safe_str($this->rendered); - $act->id = $this->uri; - $act->title = common_xml_safe_str($this->content); + $act->actor = ActivityObject::fromProfile($profile); + $act->verb = ActivityVerb::POST; + $act->objects[] = ActivityObject::fromNotice($this); - $ctx = new ActivityContext(); + // XXX: should this be handled by default processing for object entry? - if (!empty($this->reply_to)) { - $reply = Notice::staticGet('id', $this->reply_to); - if (!empty($reply)) { - $ctx->replyToID = $reply->uri; - $ctx->replyToUrl = $reply->bestUrl(); + $act->time = strtotime($this->created); + $act->link = $this->bestUrl(); + + $act->content = common_xml_safe_str($this->rendered); + $act->id = $this->uri; + $act->title = common_xml_safe_str($this->content); + + // Categories + + $tags = $this->getTags(); + + foreach ($tags as $tag) { + $cat = new AtomCategory(); + $cat->term = $tag; + + $act->categories[] = $cat; } - } - $ctx->location = $this->getLocation(); + // Enclosures + // XXX: use Atom Media and/or File activity objects instead - $conv = null; + $attachments = $this->attachments(); - if (!empty($this->conversation)) { - $conv = Conversation::staticGet('id', $this->conversation); - if (!empty($conv)) { - $ctx->conversation = $conv->uri; + foreach ($attachments as $attachment) { + $enclosure = $attachment->getEnclosure(); + if ($enclosure) { + $act->enclosures[] = $enclosure; + } } - } - $reply_ids = $this->getReplies(); + $ctx = new ActivityContext(); - foreach ($reply_ids as $id) { - $profile = Profile::staticGet('id', $id); - if (!empty($profile)) { - $ctx->attention[] = $profile->getUri(); + if (!empty($this->reply_to)) { + $reply = Notice::staticGet('id', $this->reply_to); + if (!empty($reply)) { + $ctx->replyToID = $reply->uri; + $ctx->replyToUrl = $reply->bestUrl(); + } } + + $ctx->location = $this->getLocation(); + + $conv = null; + + if (!empty($this->conversation)) { + $conv = Conversation::staticGet('id', $this->conversation); + if (!empty($conv)) { + $ctx->conversation = $conv->uri; + } + } + + $reply_ids = $this->getReplies(); + + foreach ($reply_ids as $id) { + $profile = Profile::staticGet('id', $id); + if (!empty($profile)) { + $ctx->attention[] = $profile->getUri(); + } + } + + $groups = $this->getGroups(); + + foreach ($groups as $group) { + $ctx->attention[] = $group->uri; + } + + // XXX: deprecated; use ActivityVerb::SHARE instead + + $repeat = null; + + if (!empty($this->repeat_of)) { + $repeat = Notice::staticGet('id', $this->repeat_of); + $ctx->forwardID = $repeat->uri; + $ctx->forwardUrl = $repeat->bestUrl(); + } + + $act->context = $ctx; + + // Source + + $atom_feed = $profile->getAtomFeed(); + + if (!empty($atom_feed)) { + + $act->source = new ActivitySource(); + + // XXX: we should store the actual feed ID + + $act->source->id = $atom_feed; + + // XXX: we should store the actual feed title + + $act->source->title = $profile->getBestName(); + + $act->source->links['alternate'] = $profile->profileurl; + $act->source->links['self'] = $atom_feed; + + $act->source->icon = $profile->avatarUrl(AVATAR_PROFILE_SIZE); + + $notice = $profile->getCurrentNotice(); + + if (!empty($notice)) { + $act->source->updated = self::utcDate($notice->created); + } + + $user = User::staticGet('id', $profile->id); + + if (!empty($user)) { + $act->source->links['license'] = common_config('license', 'url'); + } + } + + if ($this->isLocal()) { + $act->selfLink = common_local_url('ApiStatusesShow', array('id' => $this->id, + 'format' => 'atom')); + $act->editLink = $act->selfLink; + } + + Event::handle('EndNoticeAsActivity', array($this, &$act)); } - $groups = $this->getGroups(); - - foreach ($groups as $group) { - $ctx->attention[] = $group->uri; - } - - $act->context = $ctx; + self::cacheSet(Cache::codeKey('notice:as-activity:'.$this->id), $act); return $act; } @@ -1281,178 +1399,34 @@ class Notice extends Memcached_DataObject // This has gotten way too long. Needs to be sliced up into functional bits // or ideally exported to a utility class. - function asAtomEntry($namespace=false, $source=false, $author=true, $cur=null) + function asAtomEntry($namespace=false, + $source=false, + $author=true, + $cur=null) { - $profile = $this->getProfile(); + $act = $this->asActivity(); + $act->extra[] = $this->noticeInfo($cur); + return $act->asString($namespace, $author, $source); + } - $xs = new XMLStringer(true); + /** + * Extra notice info for atom entries + * + * Clients use some extra notice info in the atom stream. + * This gives it to them. + * + * @param User $cur Current user + * + * @return array representation of element + */ - if ($namespace) { - $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', - 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', - 'xmlns:georss' => 'http://www.georss.org/georss', - 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', - 'xmlns:media' => 'http://purl.org/syndication/atommedia', - 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0', - 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0', - 'xmlns:statusnet' => 'http://status.net/schema/api/1/'); - } else { - $attrs = array(); - } + function noticeInfo($cur) + { + // local notice ID (useful to clients for ordering) - if (Event::handle('StartActivityStart', array(&$this, &$xs, &$attrs))) { - $xs->elementStart('entry', $attrs); - Event::handle('EndActivityStart', array(&$this, &$xs, &$attrs)); - } + $noticeInfoAttr = array('local_id' => $this->id); - if (Event::handle('StartActivitySource', array(&$this, &$xs))) { - if ($source) { - $atom_feed = $profile->getAtomFeed(); - - if (!empty($atom_feed)) { - $xs->elementStart('source'); - - // XXX: we should store the actual feed ID - - $xs->element('id', null, $atom_feed); - - // XXX: we should store the actual feed title - - $xs->element('title', null, $profile->getBestName()); - - $xs->element('link', array('rel' => 'alternate', - 'type' => 'text/html', - 'href' => $profile->profileurl)); - - $xs->element('link', array('rel' => 'self', - 'type' => 'application/atom+xml', - 'href' => $atom_feed)); - - $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE)); - - $notice = $profile->getCurrentNotice(); - - if (!empty($notice)) { - $xs->element('updated', null, self::utcDate($notice->created)); - } - - $user = User::staticGet('id', $profile->id); - - if (!empty($user)) { - $xs->element('link', array('rel' => 'license', - 'href' => common_config('license', 'url'))); - } - - $xs->elementEnd('source'); - } - } - Event::handle('EndActivitySource', array(&$this, &$xs)); - } - - $title = common_xml_safe_str($this->content); - - if (Event::handle('StartActivityTitle', array(&$this, &$xs, &$title))) { - $xs->element('title', null, $title); - Event::handle('EndActivityTitle', array($this, &$xs, $title)); - } - - $atomAuthor = ''; - - if ($author) { - $atomAuthor = $profile->asAtomAuthor($cur); - } - - if (Event::handle('StartActivityAuthor', array(&$this, &$xs, &$atomAuthor))) { - if (!empty($atomAuthor)) { - $xs->raw($atomAuthor); - Event::handle('EndActivityAuthor', array(&$this, &$xs, &$atomAuthor)); - } - } - - $actor = ''; - - if ($author) { - $actor = $profile->asActivityActor(); - } - - if (Event::handle('StartActivityActor', array(&$this, &$xs, &$actor))) { - if (!empty($actor)) { - $xs->raw($actor); - Event::handle('EndActivityActor', array(&$this, &$xs, &$actor)); - } - } - - $url = $this->bestUrl(); - - if (Event::handle('StartActivityLink', array(&$this, &$xs, &$url))) { - $xs->element('link', array('rel' => 'alternate', - 'type' => 'text/html', - 'href' => $url)); - Event::handle('EndActivityLink', array(&$this, &$xs, $url)); - } - - $id = $this->uri; - - if (Event::handle('StartActivityId', array(&$this, &$xs, &$id))) { - $xs->element('id', null, $id); - Event::handle('EndActivityId', array(&$this, &$xs, $id)); - } - - $published = self::utcDate($this->created); - - if (Event::handle('StartActivityPublished', array(&$this, &$xs, &$published))) { - $xs->element('published', null, $published); - Event::handle('EndActivityPublished', array(&$this, &$xs, $published)); - } - - $updated = $published; // XXX: notices are usually immutable - - if (Event::handle('StartActivityUpdated', array(&$this, &$xs, &$updated))) { - $xs->element('updated', null, $updated); - Event::handle('EndActivityUpdated', array(&$this, &$xs, $updated)); - } - - $content = common_xml_safe_str($this->rendered); - - if (Event::handle('StartActivityContent', array(&$this, &$xs, &$content))) { - $xs->element('content', array('type' => 'html'), $content); - Event::handle('EndActivityContent', array(&$this, &$xs, $content)); - } - - // Most of our notices represent POSTing a NOTE. This is the default verb - // for activity streams, so we normally just leave it out. - - $verb = ActivityVerb::POST; - - if (Event::handle('StartActivityVerb', array(&$this, &$xs, &$verb))) { - $xs->element('activity:verb', null, $verb); - Event::handle('EndActivityVerb', array(&$this, &$xs, $verb)); - } - - // We use the default behavior for activity streams: if there's no activity:object, - // then treat the entry itself as the object. Here, you can set the type of that object, - // which is normally a NOTE. - - $type = ActivityObject::NOTE; - - if (Event::handle('StartActivityDefaultObjectType', array(&$this, &$xs, &$type))) { - $xs->element('activity:object-type', null, $type); - Event::handle('EndActivityDefaultObjectType', array(&$this, &$xs, $type)); - } - - // Since we usually use the entry itself as an object, we don't have an explicit - // object. Some extensions may want to add them (for photo, event, music, etc.). - - $objects = array(); - - if (Event::handle('StartActivityObjects', array(&$this, &$xs, &$objects))) { - foreach ($objects as $object) { - $xs->raw($object->asString()); - } - Event::handle('EndActivityObjects', array(&$this, &$xs, $objects)); - } - - $noticeInfoAttr = array('local_id' => $this->id); // local notice ID (useful to clients for ordering) + // notice source $ns = $this->getSource(); @@ -1462,163 +1436,27 @@ class Notice extends Memcached_DataObject $noticeInfoAttr['source_link'] = $ns->url; if (!empty($ns->name)) { $noticeInfoAttr['source'] = '' - . htmlspecialchars($ns->name) + . htmlspecialchars($ns->name) . ''; } } } + // favorite and repeated + if (!empty($cur)) { $noticeInfoAttr['favorite'] = ($cur->hasFave($this)) ? "true" : "false"; - $profile = $cur->getProfile(); - $noticeInfoAttr['repeated'] = ($profile->hasRepeated($this->id)) ? "true" : "false"; + $cp = $cur->getProfile(); + $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this->id)) ? "true" : "false"; } if (!empty($this->repeat_of)) { $noticeInfoAttr['repeat_of'] = $this->repeat_of; } - if (Event::handle('StartActivityNoticeInfo', array(&$this, &$xs, &$noticeInfoAttr))) { - $xs->element('statusnet:notice_info', $noticeInfoAttr, null); - Event::handle('EndActivityNoticeInfo', array(&$this, &$xs, $noticeInfoAttr)); - } - - $replyNotice = null; - - if ($this->reply_to) { - $replyNotice = Notice::staticGet('id', $this->reply_to); - } - - if (Event::handle('StartActivityInReplyTo', array(&$this, &$xs, &$replyNotice))) { - if (!empty($replyNotice)) { - $xs->element('link', array('rel' => 'related', - 'href' => $replyNotice->bestUrl())); - $xs->element('thr:in-reply-to', - array('ref' => $replyNotice->uri, - 'href' => $replyNotice->bestUrl())); - Event::handle('EndActivityInReplyTo', array(&$this, &$xs, $replyNotice)); - } - } - - $conv = null; - - if (!empty($this->conversation)) { - $conv = Conversation::staticGet('id', $this->conversation); - } - - if (Event::handle('StartActivityConversation', array(&$this, &$xs, &$conv))) { - if (!empty($conv)) { - $xs->element('link', array('rel' => 'ostatus:conversation', - 'href' => $conv->uri)); - } - Event::handle('EndActivityConversation', array(&$this, &$xs, $conv)); - } - - $replyProfiles = array(); - - $reply_ids = $this->getReplies(); - - foreach ($reply_ids as $id) { - $profile = Profile::staticGet('id', $id); - if (!empty($profile)) { - $replyProfiles[] = $profile; - } - } - - if (Event::handle('StartActivityAttentionProfiles', array(&$this, &$xs, &$replyProfiles))) { - foreach ($replyProfiles as $profile) { - $xs->element('link', array('rel' => 'ostatus:attention', - 'href' => $profile->getUri())); - $xs->element('link', array('rel' => 'mentioned', - 'href' => $profile->getUri())); - } - Event::handle('EndActivityAttentionProfiles', array(&$this, &$xs, $replyProfiles)); - } - - $groups = $this->getGroups(); - - if (Event::handle('StartActivityAttentionGroups', array(&$this, &$xs, &$groups))) { - foreach ($groups as $group) { - $xs->element('link', array('rel' => 'ostatus:attention', - 'href' => $group->permalink())); - $xs->element('link', array('rel' => 'mentioned', - 'href' => $group->permalink())); - } - Event::handle('EndActivityAttentionGroups', array(&$this, &$xs, $groups)); - } - - $repeat = null; - - if (!empty($this->repeat_of)) { - $repeat = Notice::staticGet('id', $this->repeat_of); - } - - if (Event::handle('StartActivityForward', array(&$this, &$xs, &$repeat))) { - if (!empty($repeat)) { - $xs->element('ostatus:forward', - array('ref' => $repeat->uri, - 'href' => $repeat->bestUrl())); - } - - Event::handle('EndActivityForward', array(&$this, &$xs, $repeat)); - } - - $tags = $this->getTags(); - - if (Event::handle('StartActivityCategories', array(&$this, &$xs, &$tags))) { - foreach ($tags as $tag) { - $xs->element('category', array('term' => $tag)); - } - Event::handle('EndActivityCategories', array(&$this, &$xs, $tags)); - } - - // Enclosures - - $enclosures = array(); - - $attachments = $this->attachments(); - - foreach ($attachments as $attachment) { - $enclosure = $attachment->getEnclosure(); - if ($enclosure) { - $enclosures[] = $enclosure; - } - } - - if (Event::handle('StartActivityEnclosures', array(&$this, &$xs, &$enclosures))) { - foreach ($enclosures as $enclosure) { - $attributes = array('rel' => 'enclosure', - 'href' => $enclosure->url, - 'type' => $enclosure->mimetype, - 'length' => $enclosure->size); - - if ($enclosure->title) { - $attributes['title'] = $enclosure->title; - } - - $xs->element('link', $attributes, null); - } - Event::handle('EndActivityEnclosures', array(&$this, &$xs, $enclosures)); - } - - $lat = $this->lat; - $lon = $this->lon; - - if (Event::handle('StartActivityGeo', array(&$this, &$xs, &$lat, &$lon))) { - if (!empty($lat) && !empty($lon)) { - $xs->element('georss:point', null, $lat . ' ' . $lon); - } - Event::handle('EndActivityGeo', array(&$this, &$xs, $lat, $lon)); - } - - if (Event::handle('StartActivityEnd', array(&$this, &$xs))) { - $xs->elementEnd('entry'); - Event::handle('EndActivityEnd', array(&$this, &$xs)); - } - - return $xs->getString(); + return array('statusnet:notice_info', $noticeInfoAttr, null); } /** @@ -1630,6 +1468,7 @@ class Notice extends Memcached_DataObject * @param string $element one of 'subject', 'object', 'target' * @return string */ + function asActivityNoun($element) { $noun = ActivityObject::fromNotice($this); @@ -1872,10 +1711,10 @@ class Notice extends Memcached_DataObject $notice->repeat_of = $this->id; - $notice->orderBy('created'); // NB: asc! + $notice->orderBy('created, id'); // NB: asc! - if (!is_null($offset)) { - $notice->limit($offset, $limit); + if (!is_null($limit)) { + $notice->limit(0, $limit); } $ids = array(); @@ -1962,6 +1801,21 @@ class Notice extends Memcached_DataObject $reply->free(); } + function clearFiles() + { + $f2p = new File_to_post(); + + $f2p->post_id = $this->id; + + if ($f2p->find()) { + while ($f2p->fetch()) { + $f2p->delete(); + } + } + // FIXME: decide whether to delete File objects + // ...and related (actual) files + } + function clearRepeats() { $repeatNotice = new Notice(); @@ -2155,4 +2009,118 @@ class Notice extends Memcached_DataObject $d = new DateTime($dateStr, new DateTimeZone('UTC')); return $d->format(DATE_W3C); } + + /** + * Look up the creation timestamp for a given notice ID, even + * if it's been deleted. + * + * @param int $id + * @return mixed string recorded creation timestamp, or false if can't be found + */ + public static function getAsTimestamp($id) + { + if (!$id) { + return false; + } + + $notice = Notice::staticGet('id', $id); + if ($notice) { + return $notice->created; + } + + $deleted = Deleted_notice::staticGet('id', $id); + if ($deleted) { + return $deleted->created; + } + + return false; + } + + /** + * Build an SQL 'where' fragment for timestamp-based sorting from a since_id + * parameter, matching notices posted after the given one (exclusive). + * + * If the referenced notice can't be found, will return false. + * + * @param int $id + * @param string $idField + * @param string $createdField + * @return mixed string or false if no match + */ + public static function whereSinceId($id, $idField='id', $createdField='created') + { + $since = Notice::getAsTimestamp($id); + if ($since) { + return sprintf("($createdField = '%s' and $idField > %d) or ($createdField > '%s')", $since, $id, $since); + } + return false; + } + + /** + * Build an SQL 'where' fragment for timestamp-based sorting from a since_id + * parameter, matching notices posted after the given one (exclusive), and + * if necessary add it to the data object's query. + * + * @param DB_DataObject $obj + * @param int $id + * @param string $idField + * @param string $createdField + * @return mixed string or false if no match + */ + public static function addWhereSinceId(DB_DataObject $obj, $id, $idField='id', $createdField='created') + { + $since = self::whereSinceId($id, $idField, $createdField); + if ($since) { + $obj->whereAdd($since); + } + } + + /** + * Build an SQL 'where' fragment for timestamp-based sorting from a max_id + * parameter, matching notices posted before the given one (inclusive). + * + * If the referenced notice can't be found, will return false. + * + * @param int $id + * @param string $idField + * @param string $createdField + * @return mixed string or false if no match + */ + public static function whereMaxId($id, $idField='id', $createdField='created') + { + $max = Notice::getAsTimestamp($id); + if ($max) { + return sprintf("($createdField < '%s') or ($createdField = '%s' and $idField <= %d)", $max, $max, $id); + } + return false; + } + + /** + * Build an SQL 'where' fragment for timestamp-based sorting from a max_id + * parameter, matching notices posted before the given one (inclusive), and + * if necessary add it to the data object's query. + * + * @param DB_DataObject $obj + * @param int $id + * @param string $idField + * @param string $createdField + * @return mixed string or false if no match + */ + public static function addWhereMaxId(DB_DataObject $obj, $id, $idField='id', $createdField='created') + { + $max = self::whereMaxId($id, $idField, $createdField); + if ($max) { + $obj->whereAdd($max); + } + } + + function isPublic() + { + if (common_config('public', 'localonly')) { + return ($this->is_local == Notice::LOCAL_PUBLIC); + } else { + return (($this->is_local != Notice::LOCAL_NONPUBLIC) && + ($this->is_local != Notice::GATEWAY)); + } + } } diff --git a/classes/Notice_tag.php b/classes/Notice_tag.php index 6eada70224..f795bfc601 100644 --- a/classes/Notice_tag.php +++ b/classes/Notice_tag.php @@ -55,15 +55,10 @@ class Notice_tag extends Memcached_DataObject $nt->selectAdd(); $nt->selectAdd('notice_id'); - if ($since_id != 0) { - $nt->whereAdd('notice_id > ' . $since_id); - } + Notice::addWhereSinceId($nt, $since_id, 'notice_id'); + Notice::addWhereMaxId($nt, $max_id, 'notice_id'); - if ($max_id != 0) { - $nt->whereAdd('notice_id <= ' . $max_id); - } - - $nt->orderBy('notice_id DESC'); + $nt->orderBy('created DESC, notice_id DESC'); if (!is_null($offset)) { $nt->limit($offset, $limit); @@ -92,4 +87,19 @@ class Notice_tag extends Memcached_DataObject { return Memcached_DataObject::pkeyGet('Notice_tag', $kv); } + + static function url($tag) + { + if (common_config('singleuser', 'enabled')) { + // regular TagAction isn't set up in 1user mode + $nickname = User::singleUserNickname(); + $url = common_local_url('showstream', + array('nickname' => $nickname, + 'tag' => $tag)); + } else { + $url = common_local_url('tag', array('tag' => $tag)); + } + + return $url; + } } diff --git a/classes/Profile.php b/classes/Profile.php index e29e6db7b7..adad0c6157 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -149,11 +149,32 @@ class Profile extends Memcached_DataObject return true; } + /** + * Gets either the full name (if filled) or the nickname. + * + * @return string + */ function getBestName() { return ($this->fullname) ? $this->fullname : $this->nickname; } + /** + * Gets the full name (if filled) with nickname as a parenthetical, or the nickname alone + * if no fullname is provided. + * + * @return string + */ + function getFancyName() + { + if ($this->fullname) { + // TRANS: Full name of a profile or group followed by nickname in parens + return sprintf(_m('FANCYNAME','%1$s (%2$s)'), $this->fullname, $this->nickname); + } else { + return $this->nickname; + } + } + /** * Get the most recent notice posted by this user, if any. * @@ -194,26 +215,29 @@ class Profile extends Memcached_DataObject function _streamTaggedDirect($tag, $offset, $limit, $since_id, $max_id) { // XXX It would be nice to do this without a join + // (necessary to do it efficiently on accounts with long history) $notice = new Notice(); $query = "select id from notice join notice_tag on id=notice_id where tag='". $notice->escape($tag) . - "' and profile_id=" . $notice->escape($this->id); + "' and profile_id=" . intval($this->id); - if ($since_id != 0) { - $query .= " and id > $since_id"; + $since = Notice::whereSinceId($since_id, 'id', 'notice.created'); + if ($since) { + $query .= " and ($since)"; } - if ($max_id != 0) { - $query .= " and id <= $max_id"; + $max = Notice::whereMaxId($max_id, 'id', 'notice.created'); + if ($max) { + $query .= " and ($max)"; } - $query .= ' order by id DESC'; + $query .= ' order by notice.created DESC, id DESC'; if (!is_null($offset)) { - $query .= " LIMIT $limit OFFSET $offset"; + $query .= " LIMIT " . intval($limit) . " OFFSET " . intval($offset); } $notice->query($query); @@ -231,58 +255,22 @@ class Profile extends Memcached_DataObject { $notice = new Notice(); - // Temporary hack until notice_profile_id_idx is updated - // to (profile_id, id) instead of (profile_id, created, id). - // It's been falling back to PRIMARY instead, which is really - // very inefficient for a profile that hasn't posted in a few - // months. Even though forcing the index will cause a filesort, - // it's usually going to be better. - if (common_config('db', 'type') == 'mysql') { - $index = ''; - $query = - "select id from notice force index (notice_profile_id_idx) ". - "where profile_id=" . $notice->escape($this->id); + $notice->profile_id = $this->id; - if ($since_id != 0) { - $query .= " and id > $since_id"; - } + $notice->selectAdd(); + $notice->selectAdd('id'); - if ($max_id != 0) { - $query .= " and id <= $max_id"; - } + Notice::addWhereSinceId($notice, $since_id); + Notice::addWhereMaxId($notice, $max_id); - $query .= ' order by id DESC'; + $notice->orderBy('created DESC, id DESC'); - if (!is_null($offset)) { - $query .= " LIMIT $limit OFFSET $offset"; - } - - $notice->query($query); - } else { - $index = ''; - - $notice->profile_id = $this->id; - - $notice->selectAdd(); - $notice->selectAdd('id'); - - if ($since_id != 0) { - $notice->whereAdd('id > ' . $since_id); - } - - if ($max_id != 0) { - $notice->whereAdd('id <= ' . $max_id); - } - - $notice->orderBy('id DESC'); - - if (!is_null($offset)) { - $notice->limit($offset, $limit); - } - - $notice->find(); + if (!is_null($offset)) { + $notice->limit($offset, $limit); } + $notice->find(); + $ids = array(); while ($notice->fetch()) { @@ -359,54 +347,32 @@ class Profile extends Memcached_DataObject function getSubscriptions($offset=0, $limit=null) { - $qry = - 'SELECT profile.* ' . - 'FROM profile JOIN subscription ' . - 'ON profile.id = subscription.subscribed ' . - 'WHERE subscription.subscriber = %d ' . - 'AND subscription.subscribed != subscription.subscriber ' . - 'ORDER BY subscription.created DESC '; + $subs = Subscription::bySubscriber($this->id, + $offset, + $limit); - if ($offset>0 && !is_null($limit)){ - if (common_config('db','type') == 'pgsql') { - $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; - } else { - $qry .= ' LIMIT ' . $offset . ', ' . $limit; - } + $profiles = array(); + + while ($subs->fetch()) { + $profiles[] = Profile::staticGet($subs->subscribed); } - $profile = new Profile(); - - $profile->query(sprintf($qry, $this->id)); - - return $profile; + return new ArrayWrapper($profiles); } function getSubscribers($offset=0, $limit=null) { - $qry = - 'SELECT profile.* ' . - 'FROM profile JOIN subscription ' . - 'ON profile.id = subscription.subscriber ' . - 'WHERE subscription.subscribed = %d ' . - 'AND subscription.subscribed != subscription.subscriber ' . - 'ORDER BY subscription.created DESC '; + $subs = Subscription::bySubscribed($this->id, + $offset, + $limit); - if ($offset>0 && !is_null($limit)){ - if ($offset) { - if (common_config('db','type') == 'pgsql') { - $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; - } else { - $qry .= ' LIMIT ' . $offset . ', ' . $limit; - } - } + $profiles = array(); + + while ($subs->fetch()) { + $profiles[] = Profile::staticGet($subs->subscriber); } - $profile = new Profile(); - - $cnt = $profile->query(sprintf($qry, $this->id)); - - return $profile; + return new ArrayWrapper($profiles); } function subscriptionCount() @@ -893,6 +859,18 @@ class Profile extends Memcached_DataObject case Right::EMAILONFAVE: $result = !$this->isSandboxed(); break; + case Right::BACKUPACCOUNT: + $result = common_config('profile', 'backup'); + break; + case Right::RESTOREACCOUNT: + $result = common_config('profile', 'restore'); + break; + case Right::DELETEACCOUNT: + $result = common_config('profile', 'delete'); + break; + case Right::MOVEACCOUNT: + $result = common_config('profile', 'move'); + break; default: $result = false; break; diff --git a/classes/Queue_item.php b/classes/Queue_item.php index c7e17be6e8..007d4ed232 100644 --- a/classes/Queue_item.php +++ b/classes/Queue_item.php @@ -32,7 +32,7 @@ class Queue_item extends Memcached_DataObject if ($transports) { if (is_array($transports)) { // @fixme use safer escaping - $list = implode("','", array_map('addslashes', $transports)); + $list = implode("','", array_map(array($qi, 'escape'), $transports)); $qi->whereAdd("transport in ('$list')"); } else { $qi->transport = $transports; diff --git a/classes/Reply.php b/classes/Reply.php index da8a4f685b..371c16cf48 100644 --- a/classes/Reply.php +++ b/classes/Reply.php @@ -50,15 +50,10 @@ class Reply extends Memcached_DataObject $reply = new Reply(); $reply->profile_id = $user_id; - if ($since_id != 0) { - $reply->whereAdd('notice_id > ' . $since_id); - } + Notice::addWhereSinceId($reply, $since_id, 'notice_id', 'modified'); + Notice::addWhereMaxId($reply, $max_id, 'notice_id', 'modified'); - if ($max_id != 0) { - $reply->whereAdd('notice_id <= ' . $max_id); - } - - $reply->orderBy('notice_id DESC'); + $reply->orderBy('modified DESC, notice_id DESC'); if (!is_null($offset)) { $reply->limit($offset, $limit); diff --git a/classes/Session.php b/classes/Session.php index b9daf364db..166b89815a 100644 --- a/classes/Session.php +++ b/classes/Session.php @@ -87,7 +87,6 @@ class Session extends Memcached_DataObject $session->id = $id; $session->session_data = $session_data; $session->created = common_sql_now(); - $session->modified = common_sql_now(); $result = $session->insert(); @@ -109,7 +108,6 @@ class Session extends Memcached_DataObject $orig = clone($session); $session->session_data = $session_data; - $session->modified = common_sql_now(); $result = $session->update($orig); diff --git a/classes/Subscription.php b/classes/Subscription.php index e9ad2a5a20..1d4f37929b 100644 --- a/classes/Subscription.php +++ b/classes/Subscription.php @@ -26,6 +26,8 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; class Subscription extends Memcached_DataObject { + const CACHE_WINDOW = 201; + ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -91,6 +93,9 @@ class Subscription extends Memcached_DataObject 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(); @@ -220,6 +225,9 @@ class Subscription extends Memcached_DataObject 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(); @@ -245,6 +253,8 @@ class Subscription extends Memcached_DataObject $act->verb = ActivityVerb::FOLLOW; + // XXX: rationalize this with the URL + $act->id = TagURI::mint('follow:%d:%d:%s', $subscriber->id, $subscribed->id, @@ -262,6 +272,156 @@ class Subscription extends Memcached_DataObject $act->actor = ActivityObject::fromProfile($subscriber); $act->objects[] = ActivityObject::fromProfile($subscribed); + $url = common_local_url('AtomPubShowSubscription', + array('subscriber' => $subscriber->id, + 'subscribed' => $subscribed->id)); + + $act->selfLink = $url; + $act->editLink = $url; + return $act; } + + /** + * Stream of subscriptions with the same subscriber + * + * Useful for showing pages that list subscriptions in reverse + * chronological order. Has offset & limit to make paging + * easy. + * + * @param integer $subscriberId Profile ID of the subscriber + * @param integer $offset Offset from latest + * @param integer $limit Maximum number to fetch + * + * @return Subscription stream of subscriptions; use fetch() to iterate + */ + + static function bySubscriber($subscriberId, + $offset = 0, + $limit = PROFILES_PER_PAGE) + { + if ($offset + $limit > self::CACHE_WINDOW) { + return new ArrayWrapper(self::realBySubscriber($subscriberId, + $offset, + $limit)); + } else { + $key = 'subscription:by-subscriber:'.$subscriberId; + $window = self::cacheGet($key); + if ($window === false) { + $window = self::realBySubscriber($subscriberId, + 0, + self::CACHE_WINDOW); + self::cacheSet($key, $window); + } + return new ArrayWrapper(array_slice($window, + $offset, + $limit)); + } + } + + private static function realBySubscriber($subscriberId, + $offset, + $limit) + { + $sub = new Subscription(); + + $sub->subscriber = $subscriberId; + + $sub->whereAdd('subscribed != ' . $subscriberId); + + $sub->orderBy('created DESC'); + $sub->limit($offset, $limit); + + $sub->find(); + + $subs = array(); + + while ($sub->fetch()) { + $subs[] = clone($sub); + } + + return $subs; + } + + /** + * Stream of subscriptions with the same subscribed profile + * + * Useful for showing pages that list subscribers in reverse + * chronological order. Has offset & limit to make paging + * easy. + * + * @param integer $subscribedId Profile ID of the subscribed + * @param integer $offset Offset from latest + * @param integer $limit Maximum number to fetch + * + * @return Subscription stream of subscriptions; use fetch() to iterate + */ + + static function bySubscribed($subscribedId, + $offset = 0, + $limit = PROFILES_PER_PAGE) + { + if ($offset + $limit > self::CACHE_WINDOW) { + return new ArrayWrapper(self::realBySubscribed($subscribedId, + $offset, + $limit)); + } else { + $key = 'subscription:by-subscribed:'.$subscribedId; + $window = self::cacheGet($key); + if ($window === false) { + $window = self::realBySubscribed($subscribedId, + 0, + self::CACHE_WINDOW); + self::cacheSet($key, $window); + } + return new ArrayWrapper(array_slice($window, + $offset, + $limit)); + } + } + + private static function realBySubscribed($subscribedId, + $offset, + $limit) + { + $sub = new Subscription(); + + $sub->subscribed = $subscribedId; + + $sub->whereAdd('subscriber != ' . $subscribedId); + + $sub->orderBy('created DESC'); + $sub->limit($offset, $limit); + + $sub->find(); + + $subs = array(); + + while ($sub->fetch()) { + $subs[] = clone($sub); + } + + return $subs; + } + + /** + * Flush cached subscriptions when subscription is updated + * + * Because we cache subscriptions, it's useful to flush them + * here. + * + * @param mixed $orig Original version of object + * + * @return boolean success flag. + */ + + function update($orig=null) + { + $result = parent::update($orig); + + self::blow('subscription:by-subscriber:'.$this->subscriber); + self::blow('subscription:by-subscribed:'.$this->subscribed); + + return $result; + } } diff --git a/classes/User.php b/classes/User.php index b339d61702..654ac8fb50 100644 --- a/classes/User.php +++ b/classes/User.php @@ -755,19 +755,14 @@ class User extends Memcached_DataObject $notice->profile_id = $this->id; $notice->whereAdd('repeat_of IS NOT NULL'); - $notice->orderBy('id DESC'); + $notice->orderBy('created DESC, id DESC'); if (!is_null($offset)) { $notice->limit($offset, $limit); } - if ($since_id != 0) { - $notice->whereAdd('id > ' . $since_id); - } - - if ($max_id != 0) { - $notice->whereAdd('id <= ' . $max_id); - } + Notice::addWhereSinceId($notice, $since_id); + Notice::addWhereMaxId($notice, $max_id); $ids = array(); @@ -800,17 +795,17 @@ class User extends Memcached_DataObject 'FROM notice original JOIN notice rept ON original.id = rept.repeat_of ' . 'WHERE original.profile_id = ' . $this->id . ' '; - if ($since_id != 0) { - $qry .= 'AND original.id > ' . $since_id . ' '; + $since = Notice::whereSinceId($since_id, 'original.id', 'original.created'); + if ($since) { + $qry .= "AND ($since) "; } - if ($max_id != 0) { - $qry .= 'AND original.id <= ' . $max_id . ' '; + $max = Notice::whereMaxId($max_id, 'original.id', 'original.created'); + if ($max) { + $qry .= "AND ($max) "; } - // NOTE: we sort by fave time, not by notice time! - - $qry .= 'ORDER BY original.id DESC '; + $qry .= 'ORDER BY original.created, original.id DESC '; if (!is_null($offset)) { $qry .= "LIMIT $limit OFFSET $offset"; diff --git a/classes/User_group.php b/classes/User_group.php index f223164d04..5a9991fe9e 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -100,15 +100,10 @@ class User_group extends Memcached_DataObject $inbox->selectAdd(); $inbox->selectAdd('notice_id'); - if ($since_id != 0) { - $inbox->whereAdd('notice_id > ' . $since_id); - } + Notice::addWhereSinceId($inbox, $since_id, 'notice_id'); + Notice::addWhereMaxId($inbox, $max_id, 'notice_id'); - if ($max_id != 0) { - $inbox->whereAdd('notice_id <= ' . $max_id); - } - - $inbox->orderBy('notice_id DESC'); + $inbox->orderBy('created DESC, notice_id DESC'); if (!is_null($offset)) { $inbox->limit($offset, $limit); @@ -234,6 +229,22 @@ class User_group extends Memcached_DataObject return ($this->fullname) ? $this->fullname : $this->nickname; } + /** + * Gets the full name (if filled) with nickname as a parenthetical, or the nickname alone + * if no fullname is provided. + * + * @return string + */ + function getFancyName() + { + if ($this->fullname) { + // TRANS: Full name of a profile or group followed by nickname in parens + return sprintf(_m('FANCYNAME','%1$s (%2$s)'), $this->fullname, $this->nickname); + } else { + return $this->nickname; + } + } + function getAliases() { $aliases = array(); @@ -476,6 +487,7 @@ class User_group extends Memcached_DataObject } // MAGICALLY put fields into current scope + // @fixme kill extract(); it makes debugging absurdly hard extract($fields); @@ -487,6 +499,9 @@ class User_group extends Memcached_DataObject // fill in later... $uri = null; } + if (empty($mainpage)) { + $mainpage = common_local_url('showgroup', array('nickname' => $nickname)); + } $group->nickname = $nickname; $group->fullname = $fullname; @@ -497,64 +512,70 @@ class User_group extends Memcached_DataObject $group->mainpage = $mainpage; $group->created = common_sql_now(); - $result = $group->insert(); + if (Event::handle('StartGroupSave', array(&$group))) { - if (!$result) { - common_log_db_error($group, 'INSERT', __FILE__); - // TRANS: Server exception thrown when creating a group failed. - throw new ServerException(_('Could not create group.')); - } - - if (!isset($uri) || empty($uri)) { - $orig = clone($group); - $group->uri = common_local_url('groupbyid', array('id' => $group->id)); - $result = $group->update($orig); - if (!$result) { - common_log_db_error($group, 'UPDATE', __FILE__); - // TRANS: Server exception thrown when updating a group URI failed. - throw new ServerException(_('Could not set group URI.')); - } - } - - $result = $group->setAliases($aliases); - - if (!$result) { - // TRANS: Server exception thrown when creating group aliases failed. - throw new ServerException(_('Could not create aliases.')); - } - - $member = new Group_member(); - - $member->group_id = $group->id; - $member->profile_id = $userid; - $member->is_admin = 1; - $member->created = $group->created; - - $result = $member->insert(); - - if (!$result) { - common_log_db_error($member, 'INSERT', __FILE__); - // TRANS: Server exception thrown when setting group membership failed. - throw new ServerException(_('Could not set group membership.')); - } - - if ($local) { - $local_group = new Local_group(); - - $local_group->group_id = $group->id; - $local_group->nickname = $nickname; - $local_group->created = common_sql_now(); - - $result = $local_group->insert(); + $result = $group->insert(); if (!$result) { - common_log_db_error($local_group, 'INSERT', __FILE__); - // TRANS: Server exception thrown when saving local group information failed. - throw new ServerException(_('Could not save local group info.')); + common_log_db_error($group, 'INSERT', __FILE__); + // TRANS: Server exception thrown when creating a group failed. + throw new ServerException(_('Could not create group.')); } + + if (!isset($uri) || empty($uri)) { + $orig = clone($group); + $group->uri = common_local_url('groupbyid', array('id' => $group->id)); + $result = $group->update($orig); + if (!$result) { + common_log_db_error($group, 'UPDATE', __FILE__); + // TRANS: Server exception thrown when updating a group URI failed. + throw new ServerException(_('Could not set group URI.')); + } + } + + $result = $group->setAliases($aliases); + + if (!$result) { + // TRANS: Server exception thrown when creating group aliases failed. + throw new ServerException(_('Could not create aliases.')); + } + + $member = new Group_member(); + + $member->group_id = $group->id; + $member->profile_id = $userid; + $member->is_admin = 1; + $member->created = $group->created; + + $result = $member->insert(); + + if (!$result) { + common_log_db_error($member, 'INSERT', __FILE__); + // TRANS: Server exception thrown when setting group membership failed. + throw new ServerException(_('Could not set group membership.')); + } + + if ($local) { + $local_group = new Local_group(); + + $local_group->group_id = $group->id; + $local_group->nickname = $nickname; + $local_group->created = common_sql_now(); + + $result = $local_group->insert(); + + if (!$result) { + common_log_db_error($local_group, 'INSERT', __FILE__); + // TRANS: Server exception thrown when saving local group information failed. + throw new ServerException(_('Could not save local group info.')); + } + } + + $group->query('COMMIT'); + + Event::handle('EndGroupSave', array($group)); } - $group->query('COMMIT'); return $group; } diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 29fde93b5d..ef631e28d3 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -513,20 +513,7 @@ profile_id = K id = 130 session_data = 34 created = 142 -modified = 142 -; Warning: using DB_DATAOBJECT_MYSQLTIMESTAMP (256) causes DB_DataObject -; to SILENTLY REMOVE ATTEMPTS TO SET THIS FIELD DIRECTLY, which is pretty -; bad because the default behavior for auto-updated TIMESTAMP fields is -; to use local time. Local time can't be compared to UTC in any useful -; way, so doing that breaks session GC. -; -; Instead we'll use the plain datetime settings so it'll actually save the -; UTC value we provide when updating. -; -; Long-term fix: punch MySQL in the face until it understands that local -; time is a tool of the cyber-devil. -; -;modified = 384 +modified = 384 [session__keys] id = K diff --git a/db/096to097.sql b/db/096to097.sql new file mode 100644 index 0000000000..209a3a8811 --- /dev/null +++ b/db/096to097.sql @@ -0,0 +1,26 @@ +-- Add indexes for sorting changes in 0.9.7 + +-- Allows sorting public timeline, api/statuses/repeats, and conversations by timestamp efficiently +alter table notice + add index notice_created_id_is_local_idx (created,id,is_local), + + add index notice_repeat_of_created_id_idx (repeat_of, created, id), + drop index notice_repeatof_idx, + + add index notice_conversation_created_id_idx (conversation, created, id), + drop index notice_conversation_idx; + +-- Allows sorting tag-filtered public timeline by timestamp efficiently +alter table notice_tag add index notice_tag_tag_created_notice_id_idx (tag, created, notice_id); + +-- Needed for sorting reply/mentions timelines +alter table reply add index reply_profile_id_modified_notice_id_idx (profile_id, modified, notice_id); + +-- Needed for sorting group messages by timestamp +alter table group_inbox add index group_inbox_group_id_created_notice_id_idx (group_id, created, notice_id); + +-- Helps make some reverse role lookups more efficient if there's a lot of assigned accounts +alter table profile_role add index profile_role_role_created_profile_id_idx (role, created, profile_id); + +-- Fix for sorting a user's group memberships by order joined +alter table group_member add index group_member_profile_id_created_idx (profile_id, created); diff --git a/db/notice_source.sql b/db/notice_source.sql index 06b1348898..dec8080a01 100644 --- a/db/notice_source.sql +++ b/db/notice_source.sql @@ -78,4 +78,5 @@ VALUES ('twitvim','TwitVim','http://vim.sourceforge.net/scripts/script.php?script_id=2204', now()), ('Updating.Me','Updating.Me','http://updating.me/', now()), ('urfastr','urfastr','http://urfastr.net/', now()), - ('yatca','Yatca','http://www.yatca.com/', now()); + ('yatca','Yatca','http://www.yatca.com/', now()), + ('rss.me', 'rss.me', 'http://rss.me/', now()); diff --git a/db/sms_carrier.sql b/db/sms_carrier.sql index 0e94df296e..14074e7189 100644 --- a/db/sms_carrier.sql +++ b/db/sms_carrier.sql @@ -62,4 +62,5 @@ VALUES (100114, 'Vodafone Germany', '%s@vodafone-sms.de', now()), (100115, 'E-Plus', '%s@smsmail.eplus.de', now()), (100116, 'Cellular South', '%s@csouth1.com', now()), - (100117, 'ChinaMobile (139)', '%s@139.com', now()); + (100117, 'ChinaMobile (139)', '%s@139.com', now()), + (100118, 'Dialog Axiata', '%s@dialog.lk', now()); diff --git a/db/statusnet.sql b/db/statusnet.sql index ac48e6253a..4a24d016a2 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -131,11 +131,21 @@ create table notice ( location_ns integer comment 'namespace for location', repeat_of integer comment 'notice this is a repeat of' references notice (id), + -- For public timeline... + index notice_created_id_is_local_idx (created,id,is_local), + + -- For profile timelines... index notice_profile_id_idx (profile_id,created,id), - index notice_conversation_idx (conversation), - index notice_created_idx (created), + + -- For api/statuses/repeats... + index notice_repeat_of_created_id_idx (repeat_of, created, id), + + -- For conversation views + index notice_conversation_created_id_idx (conversation, created, id), + + -- Are these needed/used? index notice_replyto_idx (reply_to), - index notice_repeatof_idx (repeat_of), + FULLTEXT(content) ) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci; @@ -156,7 +166,10 @@ create table reply ( constraint primary key (notice_id, profile_id), index reply_notice_id_idx (notice_id), index reply_profile_id_idx (profile_id), - index reply_replied_id_idx (replied_id) + index reply_replied_id_idx (replied_id), + + -- Needed for sorting reply/mentions timelines + index reply_profile_id_modified_notice_id_idx (profile_id, modified, notice_id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; @@ -301,7 +314,10 @@ create table notice_tag ( constraint primary key (tag, notice_id), index notice_tag_created_idx (created), - index notice_tag_notice_id_idx (notice_id) + index notice_tag_notice_id_idx (notice_id), + + -- For sorting tag-filtered public timeline + index notice_tag_tag_created_notice_id_idx (tag, created, notice_id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; /* Synching with foreign services */ @@ -447,7 +463,10 @@ create table group_member ( constraint primary key (group_id, profile_id), index group_member_profile_id_idx (profile_id), - index group_member_created_idx (created) + index group_member_created_idx (created), + + -- To pull up a list of someone's groups in order joined + index group_member_profile_id_created_idx (profile_id, created) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; @@ -468,7 +487,10 @@ create table group_inbox ( constraint primary key (group_id, notice_id), index group_inbox_created_idx (created), - index group_inbox_notice_id_idx (notice_id) + index group_inbox_notice_id_idx (notice_id), + + -- Needed for sorting group messages by timestamp + index group_inbox_group_id_created_notice_id_idx (group_id, created, notice_id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; @@ -608,7 +630,8 @@ create table profile_role ( role varchar(32) not null comment 'string representing the role', created datetime not null comment 'date the role was granted', - constraint primary key (profile_id, role) + constraint primary key (profile_id, role), + index profile_role_role_created_profile_id_idx (role, created, profile_id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; diff --git a/extlib/HTTP/Request.php b/extlib/HTTP/Request.php deleted file mode 100644 index 42eac3b141..0000000000 --- a/extlib/HTTP/Request.php +++ /dev/null @@ -1,1521 +0,0 @@ - - * @author Alexey Borzov - * @copyright 2002-2007 Richard Heyes - * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Request.php,v 1.63 2008/10/11 11:07:10 avb Exp $ - * @link http://pear.php.net/package/HTTP_Request/ - */ - -/** - * PEAR and PEAR_Error classes (for error handling) - */ -require_once 'PEAR.php'; -/** - * Socket class - */ -require_once 'Net/Socket.php'; -/** - * URL handling class - */ -require_once 'Net/URL.php'; - -/**#@+ - * Constants for HTTP request methods - */ -define('HTTP_REQUEST_METHOD_GET', 'GET', true); -define('HTTP_REQUEST_METHOD_HEAD', 'HEAD', true); -define('HTTP_REQUEST_METHOD_POST', 'POST', true); -define('HTTP_REQUEST_METHOD_PUT', 'PUT', true); -define('HTTP_REQUEST_METHOD_DELETE', 'DELETE', true); -define('HTTP_REQUEST_METHOD_OPTIONS', 'OPTIONS', true); -define('HTTP_REQUEST_METHOD_TRACE', 'TRACE', true); -/**#@-*/ - -/**#@+ - * Constants for HTTP request error codes - */ -define('HTTP_REQUEST_ERROR_FILE', 1); -define('HTTP_REQUEST_ERROR_URL', 2); -define('HTTP_REQUEST_ERROR_PROXY', 4); -define('HTTP_REQUEST_ERROR_REDIRECTS', 8); -define('HTTP_REQUEST_ERROR_RESPONSE', 16); -define('HTTP_REQUEST_ERROR_GZIP_METHOD', 32); -define('HTTP_REQUEST_ERROR_GZIP_READ', 64); -define('HTTP_REQUEST_ERROR_GZIP_DATA', 128); -define('HTTP_REQUEST_ERROR_GZIP_CRC', 256); -/**#@-*/ - -/**#@+ - * Constants for HTTP protocol versions - */ -define('HTTP_REQUEST_HTTP_VER_1_0', '1.0', true); -define('HTTP_REQUEST_HTTP_VER_1_1', '1.1', true); -/**#@-*/ - -if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) { - /** - * Whether string functions are overloaded by their mbstring equivalents - */ - define('HTTP_REQUEST_MBSTRING', true); -} else { - /** - * @ignore - */ - define('HTTP_REQUEST_MBSTRING', false); -} - -/** - * Class for performing HTTP requests - * - * Simple example (fetches yahoo.com and displays it): - * - * $a = &new HTTP_Request('http://www.yahoo.com/'); - * $a->sendRequest(); - * echo $a->getResponseBody(); - * - * - * @category HTTP - * @package HTTP_Request - * @author Richard Heyes - * @author Alexey Borzov - * @version Release: 1.4.4 - */ -class HTTP_Request -{ - /**#@+ - * @access private - */ - /** - * Instance of Net_URL - * @var Net_URL - */ - var $_url; - - /** - * Type of request - * @var string - */ - var $_method; - - /** - * HTTP Version - * @var string - */ - var $_http; - - /** - * Request headers - * @var array - */ - var $_requestHeaders; - - /** - * Basic Auth Username - * @var string - */ - var $_user; - - /** - * Basic Auth Password - * @var string - */ - var $_pass; - - /** - * Socket object - * @var Net_Socket - */ - var $_sock; - - /** - * Proxy server - * @var string - */ - var $_proxy_host; - - /** - * Proxy port - * @var integer - */ - var $_proxy_port; - - /** - * Proxy username - * @var string - */ - var $_proxy_user; - - /** - * Proxy password - * @var string - */ - var $_proxy_pass; - - /** - * Post data - * @var array - */ - var $_postData; - - /** - * Request body - * @var string - */ - var $_body; - - /** - * A list of methods that MUST NOT have a request body, per RFC 2616 - * @var array - */ - var $_bodyDisallowed = array('TRACE'); - - /** - * Methods having defined semantics for request body - * - * Content-Length header (indicating that the body follows, section 4.3 of - * RFC 2616) will be sent for these methods even if no body was added - * - * @var array - */ - var $_bodyRequired = array('POST', 'PUT'); - - /** - * Files to post - * @var array - */ - var $_postFiles = array(); - - /** - * Connection timeout. - * @var float - */ - var $_timeout; - - /** - * HTTP_Response object - * @var HTTP_Response - */ - var $_response; - - /** - * Whether to allow redirects - * @var boolean - */ - var $_allowRedirects; - - /** - * Maximum redirects allowed - * @var integer - */ - var $_maxRedirects; - - /** - * Current number of redirects - * @var integer - */ - var $_redirects; - - /** - * Whether to append brackets [] to array variables - * @var bool - */ - var $_useBrackets = true; - - /** - * Attached listeners - * @var array - */ - var $_listeners = array(); - - /** - * Whether to save response body in response object property - * @var bool - */ - var $_saveBody = true; - - /** - * Timeout for reading from socket (array(seconds, microseconds)) - * @var array - */ - var $_readTimeout = null; - - /** - * Options to pass to Net_Socket::connect. See stream_context_create - * @var array - */ - var $_socketOptions = null; - /**#@-*/ - - /** - * Constructor - * - * Sets up the object - * @param string The url to fetch/access - * @param array Associative array of parameters which can have the following keys: - *
      - *
    • method - Method to use, GET, POST etc (string)
    • - *
    • http - HTTP Version to use, 1.0 or 1.1 (string)
    • - *
    • user - Basic Auth username (string)
    • - *
    • pass - Basic Auth password (string)
    • - *
    • proxy_host - Proxy server host (string)
    • - *
    • proxy_port - Proxy server port (integer)
    • - *
    • proxy_user - Proxy auth username (string)
    • - *
    • proxy_pass - Proxy auth password (string)
    • - *
    • timeout - Connection timeout in seconds (float)
    • - *
    • allowRedirects - Whether to follow redirects or not (bool)
    • - *
    • maxRedirects - Max number of redirects to follow (integer)
    • - *
    • useBrackets - Whether to append [] to array variable names (bool)
    • - *
    • saveBody - Whether to save response body in response object property (bool)
    • - *
    • readTimeout - Timeout for reading / writing data over the socket (array (seconds, microseconds))
    • - *
    • socketOptions - Options to pass to Net_Socket object (array)
    • - *
    - * @access public - */ - function HTTP_Request($url = '', $params = array()) - { - $this->_method = HTTP_REQUEST_METHOD_GET; - $this->_http = HTTP_REQUEST_HTTP_VER_1_1; - $this->_requestHeaders = array(); - $this->_postData = array(); - $this->_body = null; - - $this->_user = null; - $this->_pass = null; - - $this->_proxy_host = null; - $this->_proxy_port = null; - $this->_proxy_user = null; - $this->_proxy_pass = null; - - $this->_allowRedirects = false; - $this->_maxRedirects = 3; - $this->_redirects = 0; - - $this->_timeout = null; - $this->_response = null; - - foreach ($params as $key => $value) { - $this->{'_' . $key} = $value; - } - - if (!empty($url)) { - $this->setURL($url); - } - - // Default useragent - $this->addHeader('User-Agent', 'PEAR HTTP_Request class ( http://pear.php.net/ )'); - - // We don't do keep-alives by default - $this->addHeader('Connection', 'close'); - - // Basic authentication - if (!empty($this->_user)) { - $this->addHeader('Authorization', 'Basic ' . base64_encode($this->_user . ':' . $this->_pass)); - } - - // Proxy authentication (see bug #5913) - if (!empty($this->_proxy_user)) { - $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($this->_proxy_user . ':' . $this->_proxy_pass)); - } - - // Use gzip encoding if possible - if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && extension_loaded('zlib')) { - $this->addHeader('Accept-Encoding', 'gzip'); - } - } - - /** - * Generates a Host header for HTTP/1.1 requests - * - * @access private - * @return string - */ - function _generateHostHeader() - { - if ($this->_url->port != 80 AND strcasecmp($this->_url->protocol, 'http') == 0) { - $host = $this->_url->host . ':' . $this->_url->port; - - } elseif ($this->_url->port != 443 AND strcasecmp($this->_url->protocol, 'https') == 0) { - $host = $this->_url->host . ':' . $this->_url->port; - - } elseif ($this->_url->port == 443 AND strcasecmp($this->_url->protocol, 'https') == 0 AND strpos($this->_url->url, ':443') !== false) { - $host = $this->_url->host . ':' . $this->_url->port; - - } else { - $host = $this->_url->host; - } - - return $host; - } - - /** - * Resets the object to its initial state (DEPRECATED). - * Takes the same parameters as the constructor. - * - * @param string $url The url to be requested - * @param array $params Associative array of parameters - * (see constructor for details) - * @access public - * @deprecated deprecated since 1.2, call the constructor if this is necessary - */ - function reset($url, $params = array()) - { - $this->HTTP_Request($url, $params); - } - - /** - * Sets the URL to be requested - * - * @param string The url to be requested - * @access public - */ - function setURL($url) - { - $this->_url = &new Net_URL($url, $this->_useBrackets); - - if (!empty($this->_url->user) || !empty($this->_url->pass)) { - $this->setBasicAuth($this->_url->user, $this->_url->pass); - } - - if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http) { - $this->addHeader('Host', $this->_generateHostHeader()); - } - - // set '/' instead of empty path rather than check later (see bug #8662) - if (empty($this->_url->path)) { - $this->_url->path = '/'; - } - } - - /** - * Returns the current request URL - * - * @return string Current request URL - * @access public - */ - function getUrl() - { - return empty($this->_url)? '': $this->_url->getUrl(); - } - - /** - * Sets a proxy to be used - * - * @param string Proxy host - * @param int Proxy port - * @param string Proxy username - * @param string Proxy password - * @access public - */ - function setProxy($host, $port = 8080, $user = null, $pass = null) - { - $this->_proxy_host = $host; - $this->_proxy_port = $port; - $this->_proxy_user = $user; - $this->_proxy_pass = $pass; - - if (!empty($user)) { - $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($user . ':' . $pass)); - } - } - - /** - * Sets basic authentication parameters - * - * @param string Username - * @param string Password - */ - function setBasicAuth($user, $pass) - { - $this->_user = $user; - $this->_pass = $pass; - - $this->addHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $pass)); - } - - /** - * Sets the method to be used, GET, POST etc. - * - * @param string Method to use. Use the defined constants for this - * @access public - */ - function setMethod($method) - { - $this->_method = $method; - } - - /** - * Sets the HTTP version to use, 1.0 or 1.1 - * - * @param string Version to use. Use the defined constants for this - * @access public - */ - function setHttpVer($http) - { - $this->_http = $http; - } - - /** - * Adds a request header - * - * @param string Header name - * @param string Header value - * @access public - */ - function addHeader($name, $value) - { - $this->_requestHeaders[strtolower($name)] = $value; - } - - /** - * Removes a request header - * - * @param string Header name to remove - * @access public - */ - function removeHeader($name) - { - if (isset($this->_requestHeaders[strtolower($name)])) { - unset($this->_requestHeaders[strtolower($name)]); - } - } - - /** - * Adds a querystring parameter - * - * @param string Querystring parameter name - * @param string Querystring parameter value - * @param bool Whether the value is already urlencoded or not, default = not - * @access public - */ - function addQueryString($name, $value, $preencoded = false) - { - $this->_url->addQueryString($name, $value, $preencoded); - } - - /** - * Sets the querystring to literally what you supply - * - * @param string The querystring data. Should be of the format foo=bar&x=y etc - * @param bool Whether data is already urlencoded or not, default = already encoded - * @access public - */ - function addRawQueryString($querystring, $preencoded = true) - { - $this->_url->addRawQueryString($querystring, $preencoded); - } - - /** - * Adds postdata items - * - * @param string Post data name - * @param string Post data value - * @param bool Whether data is already urlencoded or not, default = not - * @access public - */ - function addPostData($name, $value, $preencoded = false) - { - if ($preencoded) { - $this->_postData[$name] = $value; - } else { - $this->_postData[$name] = $this->_arrayMapRecursive('urlencode', $value); - } - } - - /** - * Recursively applies the callback function to the value - * - * @param mixed Callback function - * @param mixed Value to process - * @access private - * @return mixed Processed value - */ - function _arrayMapRecursive($callback, $value) - { - if (!is_array($value)) { - return call_user_func($callback, $value); - } else { - $map = array(); - foreach ($value as $k => $v) { - $map[$k] = $this->_arrayMapRecursive($callback, $v); - } - return $map; - } - } - - /** - * Adds a file to form-based file upload - * - * Used to emulate file upload via a HTML form. The method also sets - * Content-Type of HTTP request to 'multipart/form-data'. - * - * If you just want to send the contents of a file as the body of HTTP - * request you should use setBody() method. - * - * @access public - * @param string name of file-upload field - * @param mixed file name(s) - * @param mixed content-type(s) of file(s) being uploaded - * @return bool true on success - * @throws PEAR_Error - */ - function addFile($inputName, $fileName, $contentType = 'application/octet-stream') - { - if (!is_array($fileName) && !is_readable($fileName)) { - return PEAR::raiseError("File '{$fileName}' is not readable", HTTP_REQUEST_ERROR_FILE); - } elseif (is_array($fileName)) { - foreach ($fileName as $name) { - if (!is_readable($name)) { - return PEAR::raiseError("File '{$name}' is not readable", HTTP_REQUEST_ERROR_FILE); - } - } - } - $this->addHeader('Content-Type', 'multipart/form-data'); - $this->_postFiles[$inputName] = array( - 'name' => $fileName, - 'type' => $contentType - ); - return true; - } - - /** - * Adds raw postdata (DEPRECATED) - * - * @param string The data - * @param bool Whether data is preencoded or not, default = already encoded - * @access public - * @deprecated deprecated since 1.3.0, method setBody() should be used instead - */ - function addRawPostData($postdata, $preencoded = true) - { - $this->_body = $preencoded ? $postdata : urlencode($postdata); - } - - /** - * Sets the request body (for POST, PUT and similar requests) - * - * @param string Request body - * @access public - */ - function setBody($body) - { - $this->_body = $body; - } - - /** - * Clears any postdata that has been added (DEPRECATED). - * - * Useful for multiple request scenarios. - * - * @access public - * @deprecated deprecated since 1.2 - */ - function clearPostData() - { - $this->_postData = null; - } - - /** - * Appends a cookie to "Cookie:" header - * - * @param string $name cookie name - * @param string $value cookie value - * @access public - */ - function addCookie($name, $value) - { - $cookies = isset($this->_requestHeaders['cookie']) ? $this->_requestHeaders['cookie']. '; ' : ''; - $this->addHeader('Cookie', $cookies . $name . '=' . $value); - } - - /** - * Clears any cookies that have been added (DEPRECATED). - * - * Useful for multiple request scenarios - * - * @access public - * @deprecated deprecated since 1.2 - */ - function clearCookies() - { - $this->removeHeader('Cookie'); - } - - /** - * Sends the request - * - * @access public - * @param bool Whether to store response body in Response object property, - * set this to false if downloading a LARGE file and using a Listener - * @return mixed PEAR error on error, true otherwise - */ - function sendRequest($saveBody = true) - { - if (!is_a($this->_url, 'Net_URL')) { - return PEAR::raiseError('No URL given', HTTP_REQUEST_ERROR_URL); - } - - $host = isset($this->_proxy_host) ? $this->_proxy_host : $this->_url->host; - $port = isset($this->_proxy_port) ? $this->_proxy_port : $this->_url->port; - - if (strcasecmp($this->_url->protocol, 'https') == 0) { - // Bug #14127, don't try connecting to HTTPS sites without OpenSSL - if (version_compare(PHP_VERSION, '4.3.0', '<') || !extension_loaded('openssl')) { - return PEAR::raiseError('Need PHP 4.3.0 or later with OpenSSL support for https:// requests', - HTTP_REQUEST_ERROR_URL); - } elseif (isset($this->_proxy_host)) { - return PEAR::raiseError('HTTPS proxies are not supported', HTTP_REQUEST_ERROR_PROXY); - } - $host = 'ssl://' . $host; - } - - // magic quotes may fuck up file uploads and chunked response processing - $magicQuotes = ini_get('magic_quotes_runtime'); - ini_set('magic_quotes_runtime', false); - - // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive - // connection token to a proxy server... - if (isset($this->_proxy_host) && !empty($this->_requestHeaders['connection']) && - 'Keep-Alive' == $this->_requestHeaders['connection']) - { - $this->removeHeader('connection'); - } - - $keepAlive = (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && empty($this->_requestHeaders['connection'])) || - (!empty($this->_requestHeaders['connection']) && 'Keep-Alive' == $this->_requestHeaders['connection']); - $sockets = &PEAR::getStaticProperty('HTTP_Request', 'sockets'); - $sockKey = $host . ':' . $port; - unset($this->_sock); - - // There is a connected socket in the "static" property? - if ($keepAlive && !empty($sockets[$sockKey]) && - !empty($sockets[$sockKey]->fp)) - { - $this->_sock =& $sockets[$sockKey]; - $err = null; - } else { - $this->_notify('connect'); - $this->_sock =& new Net_Socket(); - $err = $this->_sock->connect($host, $port, null, $this->_timeout, $this->_socketOptions); - } - PEAR::isError($err) or $err = $this->_sock->write($this->_buildRequest()); - - if (!PEAR::isError($err)) { - if (!empty($this->_readTimeout)) { - $this->_sock->setTimeout($this->_readTimeout[0], $this->_readTimeout[1]); - } - - $this->_notify('sentRequest'); - - // Read the response - $this->_response = &new HTTP_Response($this->_sock, $this->_listeners); - $err = $this->_response->process( - $this->_saveBody && $saveBody, - HTTP_REQUEST_METHOD_HEAD != $this->_method - ); - - if ($keepAlive) { - $keepAlive = (isset($this->_response->_headers['content-length']) - || (isset($this->_response->_headers['transfer-encoding']) - && strtolower($this->_response->_headers['transfer-encoding']) == 'chunked')); - if ($keepAlive) { - if (isset($this->_response->_headers['connection'])) { - $keepAlive = strtolower($this->_response->_headers['connection']) == 'keep-alive'; - } else { - $keepAlive = 'HTTP/'.HTTP_REQUEST_HTTP_VER_1_1 == $this->_response->_protocol; - } - } - } - } - - ini_set('magic_quotes_runtime', $magicQuotes); - - if (PEAR::isError($err)) { - return $err; - } - - if (!$keepAlive) { - $this->disconnect(); - // Store the connected socket in "static" property - } elseif (empty($sockets[$sockKey]) || empty($sockets[$sockKey]->fp)) { - $sockets[$sockKey] =& $this->_sock; - } - - // Check for redirection - if ( $this->_allowRedirects - AND $this->_redirects <= $this->_maxRedirects - AND $this->getResponseCode() > 300 - AND $this->getResponseCode() < 399 - AND !empty($this->_response->_headers['location'])) { - - - $redirect = $this->_response->_headers['location']; - - // Absolute URL - if (preg_match('/^https?:\/\//i', $redirect)) { - $this->_url = &new Net_URL($redirect); - $this->addHeader('Host', $this->_generateHostHeader()); - // Absolute path - } elseif ($redirect{0} == '/') { - $this->_url->path = $redirect; - - // Relative path - } elseif (substr($redirect, 0, 3) == '../' OR substr($redirect, 0, 2) == './') { - if (substr($this->_url->path, -1) == '/') { - $redirect = $this->_url->path . $redirect; - } else { - $redirect = dirname($this->_url->path) . '/' . $redirect; - } - $redirect = Net_URL::resolvePath($redirect); - $this->_url->path = $redirect; - - // Filename, no path - } else { - if (substr($this->_url->path, -1) == '/') { - $redirect = $this->_url->path . $redirect; - } else { - $redirect = dirname($this->_url->path) . '/' . $redirect; - } - $this->_url->path = $redirect; - } - - $this->_redirects++; - return $this->sendRequest($saveBody); - - // Too many redirects - } elseif ($this->_allowRedirects AND $this->_redirects > $this->_maxRedirects) { - return PEAR::raiseError('Too many redirects', HTTP_REQUEST_ERROR_REDIRECTS); - } - - return true; - } - - /** - * Disconnect the socket, if connected. Only useful if using Keep-Alive. - * - * @access public - */ - function disconnect() - { - if (!empty($this->_sock) && !empty($this->_sock->fp)) { - $this->_notify('disconnect'); - $this->_sock->disconnect(); - } - } - - /** - * Returns the response code - * - * @access public - * @return mixed Response code, false if not set - */ - function getResponseCode() - { - return isset($this->_response->_code) ? $this->_response->_code : false; - } - - /** - * Returns the response reason phrase - * - * @access public - * @return mixed Response reason phrase, false if not set - */ - function getResponseReason() - { - return isset($this->_response->_reason) ? $this->_response->_reason : false; - } - - /** - * Returns either the named header or all if no name given - * - * @access public - * @param string The header name to return, do not set to get all headers - * @return mixed either the value of $headername (false if header is not present) - * or an array of all headers - */ - function getResponseHeader($headername = null) - { - if (!isset($headername)) { - return isset($this->_response->_headers)? $this->_response->_headers: array(); - } else { - $headername = strtolower($headername); - return isset($this->_response->_headers[$headername]) ? $this->_response->_headers[$headername] : false; - } - } - - /** - * Returns the body of the response - * - * @access public - * @return mixed response body, false if not set - */ - function getResponseBody() - { - return isset($this->_response->_body) ? $this->_response->_body : false; - } - - /** - * Returns cookies set in response - * - * @access public - * @return mixed array of response cookies, false if none are present - */ - function getResponseCookies() - { - return isset($this->_response->_cookies) ? $this->_response->_cookies : false; - } - - /** - * Builds the request string - * - * @access private - * @return string The request string - */ - function _buildRequest() - { - $separator = ini_get('arg_separator.output'); - ini_set('arg_separator.output', '&'); - $querystring = ($querystring = $this->_url->getQueryString()) ? '?' . $querystring : ''; - ini_set('arg_separator.output', $separator); - - $host = isset($this->_proxy_host) ? $this->_url->protocol . '://' . $this->_url->host : ''; - $port = (isset($this->_proxy_host) AND $this->_url->port != 80) ? ':' . $this->_url->port : ''; - $path = $this->_url->path . $querystring; - $url = $host . $port . $path; - - if (!strlen($url)) { - $url = '/'; - } - - $request = $this->_method . ' ' . $url . ' HTTP/' . $this->_http . "\r\n"; - - if (in_array($this->_method, $this->_bodyDisallowed) || - (0 == strlen($this->_body) && (HTTP_REQUEST_METHOD_POST != $this->_method || - (empty($this->_postData) && empty($this->_postFiles))))) - { - $this->removeHeader('Content-Type'); - } else { - if (empty($this->_requestHeaders['content-type'])) { - // Add default content-type - $this->addHeader('Content-Type', 'application/x-www-form-urlencoded'); - } elseif ('multipart/form-data' == $this->_requestHeaders['content-type']) { - $boundary = 'HTTP_Request_' . md5(uniqid('request') . microtime()); - $this->addHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary); - } - } - - // Request Headers - if (!empty($this->_requestHeaders)) { - foreach ($this->_requestHeaders as $name => $value) { - $canonicalName = implode('-', array_map('ucfirst', explode('-', $name))); - $request .= $canonicalName . ': ' . $value . "\r\n"; - } - } - - // Method does not allow a body, simply add a final CRLF - if (in_array($this->_method, $this->_bodyDisallowed)) { - - $request .= "\r\n"; - - // Post data if it's an array - } elseif (HTTP_REQUEST_METHOD_POST == $this->_method && - (!empty($this->_postData) || !empty($this->_postFiles))) { - - // "normal" POST request - if (!isset($boundary)) { - $postdata = implode('&', array_map( - create_function('$a', 'return $a[0] . \'=\' . $a[1];'), - $this->_flattenArray('', $this->_postData) - )); - - // multipart request, probably with file uploads - } else { - $postdata = ''; - if (!empty($this->_postData)) { - $flatData = $this->_flattenArray('', $this->_postData); - foreach ($flatData as $item) { - $postdata .= '--' . $boundary . "\r\n"; - $postdata .= 'Content-Disposition: form-data; name="' . $item[0] . '"'; - $postdata .= "\r\n\r\n" . urldecode($item[1]) . "\r\n"; - } - } - foreach ($this->_postFiles as $name => $value) { - if (is_array($value['name'])) { - $varname = $name . ($this->_useBrackets? '[]': ''); - } else { - $varname = $name; - $value['name'] = array($value['name']); - } - foreach ($value['name'] as $key => $filename) { - $fp = fopen($filename, 'r'); - $basename = basename($filename); - $type = is_array($value['type'])? @$value['type'][$key]: $value['type']; - - $postdata .= '--' . $boundary . "\r\n"; - $postdata .= 'Content-Disposition: form-data; name="' . $varname . '"; filename="' . $basename . '"'; - $postdata .= "\r\nContent-Type: " . $type; - $postdata .= "\r\n\r\n" . fread($fp, filesize($filename)) . "\r\n"; - fclose($fp); - } - } - $postdata .= '--' . $boundary . "--\r\n"; - } - $request .= 'Content-Length: ' . - (HTTP_REQUEST_MBSTRING? mb_strlen($postdata, 'iso-8859-1'): strlen($postdata)) . - "\r\n\r\n"; - $request .= $postdata; - - // Explicitly set request body - } elseif (0 < strlen($this->_body)) { - - $request .= 'Content-Length: ' . - (HTTP_REQUEST_MBSTRING? mb_strlen($this->_body, 'iso-8859-1'): strlen($this->_body)) . - "\r\n\r\n"; - $request .= $this->_body; - - // No body: send a Content-Length header nonetheless (request #12900), - // but do that only for methods that require a body (bug #14740) - } else { - - if (in_array($this->_method, $this->_bodyRequired)) { - $request .= "Content-Length: 0\r\n"; - } - $request .= "\r\n"; - } - - return $request; - } - - /** - * Helper function to change the (probably multidimensional) associative array - * into the simple one. - * - * @param string name for item - * @param mixed item's values - * @return array array with the following items: array('item name', 'item value'); - * @access private - */ - function _flattenArray($name, $values) - { - if (!is_array($values)) { - return array(array($name, $values)); - } else { - $ret = array(); - foreach ($values as $k => $v) { - if (empty($name)) { - $newName = $k; - } elseif ($this->_useBrackets) { - $newName = $name . '[' . $k . ']'; - } else { - $newName = $name; - } - $ret = array_merge($ret, $this->_flattenArray($newName, $v)); - } - return $ret; - } - } - - - /** - * Adds a Listener to the list of listeners that are notified of - * the object's events - * - * Events sent by HTTP_Request object - * - 'connect': on connection to server - * - 'sentRequest': after the request was sent - * - 'disconnect': on disconnection from server - * - * Events sent by HTTP_Response object - * - 'gotHeaders': after receiving response headers (headers are passed in $data) - * - 'tick': on receiving a part of response body (the part is passed in $data) - * - 'gzTick': on receiving a gzip-encoded part of response body (ditto) - * - 'gotBody': after receiving the response body (passes the decoded body in $data if it was gzipped) - * - * @param HTTP_Request_Listener listener to attach - * @return boolean whether the listener was successfully attached - * @access public - */ - function attach(&$listener) - { - if (!is_a($listener, 'HTTP_Request_Listener')) { - return false; - } - $this->_listeners[$listener->getId()] =& $listener; - return true; - } - - - /** - * Removes a Listener from the list of listeners - * - * @param HTTP_Request_Listener listener to detach - * @return boolean whether the listener was successfully detached - * @access public - */ - function detach(&$listener) - { - if (!is_a($listener, 'HTTP_Request_Listener') || - !isset($this->_listeners[$listener->getId()])) { - return false; - } - unset($this->_listeners[$listener->getId()]); - return true; - } - - - /** - * Notifies all registered listeners of an event. - * - * @param string Event name - * @param mixed Additional data - * @access private - * @see HTTP_Request::attach() - */ - function _notify($event, $data = null) - { - foreach (array_keys($this->_listeners) as $id) { - $this->_listeners[$id]->update($this, $event, $data); - } - } -} - - -/** - * Response class to complement the Request class - * - * @category HTTP - * @package HTTP_Request - * @author Richard Heyes - * @author Alexey Borzov - * @version Release: 1.4.4 - */ -class HTTP_Response -{ - /** - * Socket object - * @var Net_Socket - */ - var $_sock; - - /** - * Protocol - * @var string - */ - var $_protocol; - - /** - * Return code - * @var string - */ - var $_code; - - /** - * Response reason phrase - * @var string - */ - var $_reason; - - /** - * Response headers - * @var array - */ - var $_headers; - - /** - * Cookies set in response - * @var array - */ - var $_cookies; - - /** - * Response body - * @var string - */ - var $_body = ''; - - /** - * Used by _readChunked(): remaining length of the current chunk - * @var string - */ - var $_chunkLength = 0; - - /** - * Attached listeners - * @var array - */ - var $_listeners = array(); - - /** - * Bytes left to read from message-body - * @var null|int - */ - var $_toRead; - - /** - * Constructor - * - * @param Net_Socket socket to read the response from - * @param array listeners attached to request - */ - function HTTP_Response(&$sock, &$listeners) - { - $this->_sock =& $sock; - $this->_listeners =& $listeners; - } - - - /** - * Processes a HTTP response - * - * This extracts response code, headers, cookies and decodes body if it - * was encoded in some way - * - * @access public - * @param bool Whether to store response body in object property, set - * this to false if downloading a LARGE file and using a Listener. - * This is assumed to be true if body is gzip-encoded. - * @param bool Whether the response can actually have a message-body. - * Will be set to false for HEAD requests. - * @throws PEAR_Error - * @return mixed true on success, PEAR_Error in case of malformed response - */ - function process($saveBody = true, $canHaveBody = true) - { - do { - $line = $this->_sock->readLine(); - if (!preg_match('!^(HTTP/\d\.\d) (\d{3})(?: (.+))?!', $line, $s)) { - return PEAR::raiseError('Malformed response', HTTP_REQUEST_ERROR_RESPONSE); - } else { - $this->_protocol = $s[1]; - $this->_code = intval($s[2]); - $this->_reason = empty($s[3])? null: $s[3]; - } - while ('' !== ($header = $this->_sock->readLine())) { - $this->_processHeader($header); - } - } while (100 == $this->_code); - - $this->_notify('gotHeaders', $this->_headers); - - // RFC 2616, section 4.4: - // 1. Any response message which "MUST NOT" include a message-body ... - // is always terminated by the first empty line after the header fields - // 3. ... If a message is received with both a - // Transfer-Encoding header field and a Content-Length header field, - // the latter MUST be ignored. - $canHaveBody = $canHaveBody && $this->_code >= 200 && - $this->_code != 204 && $this->_code != 304; - - // If response body is present, read it and decode - $chunked = isset($this->_headers['transfer-encoding']) && ('chunked' == $this->_headers['transfer-encoding']); - $gzipped = isset($this->_headers['content-encoding']) && ('gzip' == $this->_headers['content-encoding']); - $hasBody = false; - if ($canHaveBody && ($chunked || !isset($this->_headers['content-length']) || - 0 != $this->_headers['content-length'])) - { - if ($chunked || !isset($this->_headers['content-length'])) { - $this->_toRead = null; - } else { - $this->_toRead = $this->_headers['content-length']; - } - while (!$this->_sock->eof() && (is_null($this->_toRead) || 0 < $this->_toRead)) { - if ($chunked) { - $data = $this->_readChunked(); - } elseif (is_null($this->_toRead)) { - $data = $this->_sock->read(4096); - } else { - $data = $this->_sock->read(min(4096, $this->_toRead)); - $this->_toRead -= HTTP_REQUEST_MBSTRING? mb_strlen($data, 'iso-8859-1'): strlen($data); - } - if ('' == $data && (!$this->_chunkLength || $this->_sock->eof())) { - break; - } else { - $hasBody = true; - if ($saveBody || $gzipped) { - $this->_body .= $data; - } - $this->_notify($gzipped? 'gzTick': 'tick', $data); - } - } - } - - if ($hasBody) { - // Uncompress the body if needed - if ($gzipped) { - $body = $this->_decodeGzip($this->_body); - if (PEAR::isError($body)) { - return $body; - } - $this->_body = $body; - $this->_notify('gotBody', $this->_body); - } else { - $this->_notify('gotBody'); - } - } - return true; - } - - - /** - * Processes the response header - * - * @access private - * @param string HTTP header - */ - function _processHeader($header) - { - if (false === strpos($header, ':')) { - return; - } - list($headername, $headervalue) = explode(':', $header, 2); - $headername = strtolower($headername); - $headervalue = ltrim($headervalue); - - if ('set-cookie' != $headername) { - if (isset($this->_headers[$headername])) { - $this->_headers[$headername] .= ',' . $headervalue; - } else { - $this->_headers[$headername] = $headervalue; - } - } else { - $this->_parseCookie($headervalue); - } - } - - - /** - * Parse a Set-Cookie header to fill $_cookies array - * - * @access private - * @param string value of Set-Cookie header - */ - function _parseCookie($headervalue) - { - $cookie = array( - 'expires' => null, - 'domain' => null, - 'path' => null, - 'secure' => false - ); - - // Only a name=value pair - if (!strpos($headervalue, ';')) { - $pos = strpos($headervalue, '='); - $cookie['name'] = trim(substr($headervalue, 0, $pos)); - $cookie['value'] = trim(substr($headervalue, $pos + 1)); - - // Some optional parameters are supplied - } else { - $elements = explode(';', $headervalue); - $pos = strpos($elements[0], '='); - $cookie['name'] = trim(substr($elements[0], 0, $pos)); - $cookie['value'] = trim(substr($elements[0], $pos + 1)); - - for ($i = 1; $i < count($elements); $i++) { - if (false === strpos($elements[$i], '=')) { - $elName = trim($elements[$i]); - $elValue = null; - } else { - list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i])); - } - $elName = strtolower($elName); - if ('secure' == $elName) { - $cookie['secure'] = true; - } elseif ('expires' == $elName) { - $cookie['expires'] = str_replace('"', '', $elValue); - } elseif ('path' == $elName || 'domain' == $elName) { - $cookie[$elName] = urldecode($elValue); - } else { - $cookie[$elName] = $elValue; - } - } - } - $this->_cookies[] = $cookie; - } - - - /** - * Read a part of response body encoded with chunked Transfer-Encoding - * - * @access private - * @return string - */ - function _readChunked() - { - // at start of the next chunk? - if (0 == $this->_chunkLength) { - $line = $this->_sock->readLine(); - if (preg_match('/^([0-9a-f]+)/i', $line, $matches)) { - $this->_chunkLength = hexdec($matches[1]); - // Chunk with zero length indicates the end - if (0 == $this->_chunkLength) { - $this->_sock->readLine(); // make this an eof() - return ''; - } - } else { - return ''; - } - } - $data = $this->_sock->read($this->_chunkLength); - $this->_chunkLength -= HTTP_REQUEST_MBSTRING? mb_strlen($data, 'iso-8859-1'): strlen($data); - if (0 == $this->_chunkLength) { - $this->_sock->readLine(); // Trailing CRLF - } - return $data; - } - - - /** - * Notifies all registered listeners of an event. - * - * @param string Event name - * @param mixed Additional data - * @access private - * @see HTTP_Request::_notify() - */ - function _notify($event, $data = null) - { - foreach (array_keys($this->_listeners) as $id) { - $this->_listeners[$id]->update($this, $event, $data); - } - } - - - /** - * Decodes the message-body encoded by gzip - * - * The real decoding work is done by gzinflate() built-in function, this - * method only parses the header and checks data for compliance with - * RFC 1952 - * - * @access private - * @param string gzip-encoded data - * @return string decoded data - */ - function _decodeGzip($data) - { - if (HTTP_REQUEST_MBSTRING) { - $oldEncoding = mb_internal_encoding(); - mb_internal_encoding('iso-8859-1'); - } - $length = strlen($data); - // If it doesn't look like gzip-encoded data, don't bother - if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) { - return $data; - } - $method = ord(substr($data, 2, 1)); - if (8 != $method) { - return PEAR::raiseError('_decodeGzip(): unknown compression method', HTTP_REQUEST_ERROR_GZIP_METHOD); - } - $flags = ord(substr($data, 3, 1)); - if ($flags & 224) { - return PEAR::raiseError('_decodeGzip(): reserved bits are set', HTTP_REQUEST_ERROR_GZIP_DATA); - } - - // header is 10 bytes minimum. may be longer, though. - $headerLength = 10; - // extra fields, need to skip 'em - if ($flags & 4) { - if ($length - $headerLength - 2 < 8) { - return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); - } - $extraLength = unpack('v', substr($data, 10, 2)); - if ($length - $headerLength - 2 - $extraLength[1] < 8) { - return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); - } - $headerLength += $extraLength[1] + 2; - } - // file name, need to skip that - if ($flags & 8) { - if ($length - $headerLength - 1 < 8) { - return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); - } - $filenameLength = strpos(substr($data, $headerLength), chr(0)); - if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) { - return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); - } - $headerLength += $filenameLength + 1; - } - // comment, need to skip that also - if ($flags & 16) { - if ($length - $headerLength - 1 < 8) { - return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); - } - $commentLength = strpos(substr($data, $headerLength), chr(0)); - if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) { - return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); - } - $headerLength += $commentLength + 1; - } - // have a CRC for header. let's check - if ($flags & 1) { - if ($length - $headerLength - 2 < 8) { - return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA); - } - $crcReal = 0xffff & crc32(substr($data, 0, $headerLength)); - $crcStored = unpack('v', substr($data, $headerLength, 2)); - if ($crcReal != $crcStored[1]) { - return PEAR::raiseError('_decodeGzip(): header CRC check failed', HTTP_REQUEST_ERROR_GZIP_CRC); - } - $headerLength += 2; - } - // unpacked data CRC and size at the end of encoded data - $tmp = unpack('V2', substr($data, -8)); - $dataCrc = $tmp[1]; - $dataSize = $tmp[2]; - - // finally, call the gzinflate() function - // don't pass $dataSize to gzinflate, see bugs #13135, #14370 - $unpacked = gzinflate(substr($data, $headerLength, -8)); - if (false === $unpacked) { - return PEAR::raiseError('_decodeGzip(): gzinflate() call failed', HTTP_REQUEST_ERROR_GZIP_READ); - } elseif ($dataSize != strlen($unpacked)) { - return PEAR::raiseError('_decodeGzip(): data size check failed', HTTP_REQUEST_ERROR_GZIP_READ); - } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) { - return PEAR::raiseError('_decodeGzip(): data CRC check failed', HTTP_REQUEST_ERROR_GZIP_CRC); - } - if (HTTP_REQUEST_MBSTRING) { - mb_internal_encoding($oldEncoding); - } - return $unpacked; - } -} // End class HTTP_Response -?> diff --git a/extlib/HTTP/Request/Listener.php b/extlib/HTTP/Request/Listener.php deleted file mode 100644 index b4fe444b35..0000000000 --- a/extlib/HTTP/Request/Listener.php +++ /dev/null @@ -1,106 +0,0 @@ - - * @copyright 2002-2007 Richard Heyes - * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Listener.php,v 1.3 2007/05/18 10:33:31 avb Exp $ - * @link http://pear.php.net/package/HTTP_Request/ - */ - -/** - * Listener for HTTP_Request and HTTP_Response objects - * - * This class implements the Observer part of a Subject-Observer - * design pattern. - * - * @category HTTP - * @package HTTP_Request - * @author Alexey Borzov - * @version Release: 1.4.4 - */ -class HTTP_Request_Listener -{ - /** - * A listener's identifier - * @var string - */ - var $_id; - - /** - * Constructor, sets the object's identifier - * - * @access public - */ - function HTTP_Request_Listener() - { - $this->_id = md5(uniqid('http_request_', 1)); - } - - - /** - * Returns the listener's identifier - * - * @access public - * @return string - */ - function getId() - { - return $this->_id; - } - - - /** - * This method is called when Listener is notified of an event - * - * @access public - * @param object an object the listener is attached to - * @param string Event name - * @param mixed Additional data - * @abstract - */ - function update(&$subject, $event, $data = null) - { - echo "Notified of event: '$event'\n"; - if (null !== $data) { - echo "Additional data: "; - var_dump($data); - } - } -} -?> diff --git a/extlib/Net/URL/Mapper.php b/extlib/Net/URL/Mapper.php index 65e38818bb..009c135214 100644 --- a/extlib/Net/URL/Mapper.php +++ b/extlib/Net/URL/Mapper.php @@ -37,7 +37,7 @@ * @package Net_URL_Mapper * @author Bertrand Mansion * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Mapper.php,v 1.1 2007/03/28 10:23:04 mansion Exp $ + * @version CVS: $Id: Mapper.php 232857 2007-03-28 10:23:04Z mansion $ * @link http://pear.php.net/package/Net_URL_Mapper */ diff --git a/extlib/Net/URL/Mapper/Exception.php b/extlib/Net/URL/Mapper/Exception.php index ac3ad172b1..1915ad9782 100644 --- a/extlib/Net/URL/Mapper/Exception.php +++ b/extlib/Net/URL/Mapper/Exception.php @@ -37,7 +37,7 @@ * @package Net_URL_Mapper * @author Bertrand Mansion * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Exception.php,v 1.1 2007/03/28 10:23:04 mansion Exp $ + * @version CVS: $Id: Exception.php 232857 2007-03-28 10:23:04Z mansion $ * @link http://pear.php.net/package/Net_URL_Mapper */ diff --git a/extlib/Net/URL/Mapper/Part.php b/extlib/Net/URL/Mapper/Part.php index 2f15b2c162..087c368eea 100644 --- a/extlib/Net/URL/Mapper/Part.php +++ b/extlib/Net/URL/Mapper/Part.php @@ -37,7 +37,7 @@ * @package Net_URL_Mapper * @author Bertrand Mansion * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Part.php,v 1.1 2007/03/28 10:23:04 mansion Exp $ + * @version CVS: $Id: Part.php 232857 2007-03-28 10:23:04Z mansion $ * @link http://pear.php.net/package/Net_URL_Mapper */ diff --git a/extlib/Net/URL/Mapper/Part/Dynamic.php b/extlib/Net/URL/Mapper/Part/Dynamic.php index 349d87338c..914afa4211 100644 --- a/extlib/Net/URL/Mapper/Part/Dynamic.php +++ b/extlib/Net/URL/Mapper/Part/Dynamic.php @@ -37,7 +37,7 @@ * @package Net_URL_Mapper * @author Bertrand Mansion * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Dynamic.php,v 1.1 2007/03/28 10:23:04 mansion Exp $ + * @version CVS: $Id: Dynamic.php 232857 2007-03-28 10:23:04Z mansion $ * @link http://pear.php.net/package/Net_URL_Mapper */ diff --git a/extlib/Net/URL/Mapper/Part/Fixed.php b/extlib/Net/URL/Mapper/Part/Fixed.php index b315b442db..7d94973eac 100644 --- a/extlib/Net/URL/Mapper/Part/Fixed.php +++ b/extlib/Net/URL/Mapper/Part/Fixed.php @@ -37,7 +37,7 @@ * @package Net_URL_Mapper * @author Bertrand Mansion * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Fixed.php,v 1.1 2007/03/28 10:23:04 mansion Exp $ + * @version CVS: $Id: Fixed.php 232857 2007-03-28 10:23:04Z mansion $ * @link http://pear.php.net/package/Net_URL_Mapper */ diff --git a/extlib/Net/URL/Mapper/Part/Wildcard.php b/extlib/Net/URL/Mapper/Part/Wildcard.php index 6085ff6489..84e29e1306 100644 --- a/extlib/Net/URL/Mapper/Part/Wildcard.php +++ b/extlib/Net/URL/Mapper/Part/Wildcard.php @@ -37,7 +37,7 @@ * @package Net_URL_Mapper * @author Bertrand Mansion * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Wildcard.php,v 1.1 2007/03/28 10:23:04 mansion Exp $ + * @version CVS: $Id: Wildcard.php 232857 2007-03-28 10:23:04Z mansion $ * @link http://pear.php.net/package/Net_URL_Mapper */ diff --git a/extlib/Net/URL/Mapper/Path.php b/extlib/Net/URL/Mapper/Path.php index b541002c7a..b459fa1fd3 100644 --- a/extlib/Net/URL/Mapper/Path.php +++ b/extlib/Net/URL/Mapper/Path.php @@ -5,7 +5,7 @@ * PHP version 5 * * LICENSE: - * + * * Copyright (c) 2006, Bertrand Mansion * All rights reserved. * @@ -16,9 +16,9 @@ * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the + * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. - * * The names of the authors may not be used to endorse or promote products + * * The names of the authors may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS @@ -37,7 +37,7 @@ * @package Net_URL_Mapper * @author Bertrand Mansion * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Path.php,v 1.1 2007/03/28 10:23:04 mansion Exp $ + * @version CVS: $Id: Path.php 296456 2010-03-20 00:41:08Z kguest $ * @link http://pear.php.net/package/Net_URL_Mapper */ @@ -84,6 +84,22 @@ class Net_URL_Mapper_Path $this->getRequired(); } + /** + * Called when the object is serialized + * Make sure we do not store too much info when the object is serialized + * and call the regular expressions generator functions so that they will + * not need to be generated again on wakeup. + * + * @return array Name of properties to store when serialized + */ + public function __sleep() + { + $this->getFormat(); + $this->getRule(); + return array('alias', 'path', 'defaults', 'rule', 'format', + 'parts', 'minKeys', 'maxKeys', 'fixed', 'required'); + } + public function getPath() { return $this->path; @@ -127,7 +143,7 @@ class Net_URL_Mapper_Path /** * Set the path parts default values * @param array Associative array with format partname => value - */ + */ public function setDefaults($defaults) { if (is_array($defaults)) { @@ -140,11 +156,11 @@ class Net_URL_Mapper_Path /** * Set the path parts default values * @param array Associative array with format partname => value - */ + */ public function setRules($rules) { if (is_array($rules)) { - $this->rules = $rules; + $this->rules = $rules; } else { $this->rules = array(); } @@ -153,7 +169,7 @@ class Net_URL_Mapper_Path /** * Returns the regular expression used to match this path * @return string PERL Regular expression - */ + */ public function getRule() { if (is_null($this->rule)) { @@ -213,10 +229,10 @@ class Net_URL_Mapper_Path /** * Checks whether the path contains the given part by name - * If value parameter is given, the part also checks if the + * If value parameter is given, the part also checks if the * given value conforms to the part rule. * @param string Part name - * @param mixed The value to check against + * @param mixed The value to check against */ public function hasKey($partName, $value = null) { @@ -241,7 +257,12 @@ class Net_URL_Mapper_Path } $path = '/'.trim(Net_URL::resolvePath($path), '/'); if (!empty($qstring)) { - $path .= '?'.http_build_query($qstring); + if(strpos($path, '?') === false) { + $path .= '?'; + } else { + $path .= '&'; + } + $path .= http_build_query($qstring); } if (!empty($anchor)) { $path .= '#'.ltrim($anchor, '#'); @@ -427,4 +448,4 @@ class Net_URL_Mapper_Path } -?> \ No newline at end of file +?> diff --git a/extlib/Net/URL/Mapper/Path.plex b/extlib/Net/URL/Mapper/Path.plex new file mode 100644 index 0000000000..c5ef1f88ea --- /dev/null +++ b/extlib/Net/URL/Mapper/Path.plex @@ -0,0 +1,334 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Net + * @package Net_URL_Mapper + * @author Bertrand Mansion + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Path.plex 283937 2009-07-12 11:37:21Z mansion $ + * @link http://pear.php.net/package/Net_URL_Mapper + */ + +require_once 'Net/URL/Mapper/Part/Dynamic.php'; +require_once 'Net/URL/Mapper/Part/Wildcard.php'; +require_once 'Net/URL/Mapper/Part/Fixed.php'; + +class Net_URL_Mapper_Path +{ + private $path = ''; + private $N = 0; + public $token; + public $value; + private $line = 1; + private $state = 1; + + + protected $alias; + protected $rules = array(); + protected $defaults = array(); + protected $parts = array(); + protected $rule; + protected $format; + protected $minKeys; + protected $maxKeys; + protected $fixed = true; + protected $required; + + public function __construct($path = '', $defaults = array(), $rules = array()) + { + $this->path = '/'.trim(Net_URL::resolvePath($path), '/'); + $this->setDefaults($defaults); + $this->setRules($rules); + + try { + $this->parsePath(); + } catch (Exception $e) { + // The path could not be parsed correctly, treat it as fixed + $this->fixed = true; + $part = self::createPart(Net_URL_Mapper_Part::FIXED, $this->path, $this->path); + $this->parts = array($part); + } + $this->getRequired(); + } + + /** + * Called when the object is serialized + * Make sure we do not store too much info when the object is serialized + * and call the regular expressions generator functions so that they will + * not need to be generated again on wakeup. + * + * @return array Name of properties to store when serialized + */ + public function __sleep() + { + $this->getFormat(); + $this->getRule(); + return array('alias', 'path', 'defaults', 'rule', 'format', + 'parts', 'minKeys', 'maxKeys', 'fixed', 'required'); + } + + public function getPath() + { + return $this->path; + } + + protected function parsePath() + { + while ($this->yylex()) { } + } + + /** + * Get the path alias + * Path aliases can be used instead of full path + * @return null|string + */ + public function getAlias() + { + return $this->alias; + } + + /** + * Set the path name + * @param string Set the path name + * @see getAlias() + */ + public function setAlias($alias) + { + $this->alias = $alias; + return $this; + } + + /** + * Get the path parts default values + * @return null|array + */ + public function getDefaults() + { + return $this->defaults; + } + + /** + * Set the path parts default values + * @param array Associative array with format partname => value + */ + public function setDefaults($defaults) + { + if (is_array($defaults)) { + $this->defaults = $defaults; + } else { + $this->defaults = array(); + } + } + + /** + * Set the path parts default values + * @param array Associative array with format partname => value + */ + public function setRules($rules) + { + if (is_array($rules)) { + $this->rules = $rules; + } else { + $this->rules = array(); + } + } + + /** + * Returns the regular expression used to match this path + * @return string PERL Regular expression + */ + public function getRule() + { + if (is_null($this->rule)) { + $this->rule = '/^'; + foreach ($this->parts as $path => $part) { + $this->rule .= $part->getRule(); + } + $this->rule .= '$/'; + } + return $this->rule; + } + + public function getFormat() + { + if (is_null($this->format)) { + $this->format = '/^'; + foreach ($this->parts as $path => $part) { + $this->format .= $part->getFormat(); + } + $this->format .= '$/'; + } + return $this->format; + } + + protected function addPart($part) + { + if (array_key_exists($part->content, $this->defaults)) { + $part->setRequired(false); + $part->setDefaults($this->defaults[$part->content]); + } + if (isset($this->rules[$part->content])) { + $part->setRule($this->rules[$part->content]); + } + $this->rule = null; + if ($part->getType() != Net_URL_Mapper_Part::FIXED) { + $this->fixed = false; + $this->parts[$part->content] = $part; + } else { + $this->parts[] = $part; + } + return $part; + } + + public static function createPart($type, $content, $path) + { + switch ($type) { + case Net_URL_Mapper_Part::DYNAMIC: + return new Net_URL_Mapper_Part_Dynamic($content, $path); + break; + case Net_URL_Mapper_Part::WILDCARD: + return new Net_URL_Mapper_Part_Wildcard($content, $path); + break; + default: + return new Net_URL_Mapper_Part_Fixed($content, $path); + } + } + + /** + * Checks whether the path contains the given part by name + * If value parameter is given, the part also checks if the + * given value conforms to the part rule. + * @param string Part name + * @param mixed The value to check against + */ + public function hasKey($partName, $value = null) + { + if (array_key_exists($partName, $this->parts)) { + if (!is_null($value) && $value !== false) { + return $this->parts[$partName]->match($value); + } else { + return true; + } + } elseif (array_key_exists($partName, $this->defaults) && + $value == $this->defaults[$partName]) { + return true; + } + return false; + } + + public function generate($values = array(), $qstring = array(), $anchor = '') + { + $path = ''; + foreach ($this->parts as $part) { + $path .= $part->generate($values); + } + $path = '/'.trim(Net_URL::resolvePath($path), '/'); + if (!empty($qstring)) { + $path .= '?'.http_build_query($qstring); + } + if (!empty($anchor)) { + $path .= '#'.ltrim($anchor, '#'); + } + return $path; + } + + public function getRequired() + { + if (!isset($this->required)) { + $req = array(); + foreach ($this->parts as $part) { + if ($part->isRequired()) { + $req[] = $part->content; + } + } + $this->required = $req; + } + return $this->required; + } + + public function getMaxKeys() + { + if (is_null($this->maxKeys)) { + $this->maxKeys = count($this->required); + $this->maxKeys += count($this->defaults); + } + return $this->maxKeys; + } + + + +/*!lex2php +%input $this->path +%counter $this->N +%token $this->token +%value $this->value +%line $this->line +static = /\/?([^\/:\*]+)/ +variable = /([a-zA-Z0-9_]+)/ +dynamic = /\/?:/ +wildcard = @/?\*@ +grouping = /\/?\(([a-zA-Z0-9_]+)\)/ +*/ +/*!lex2php +%statename START +dynamic grouping { + $c = $yy_subpatterns[0]; + $part = self::createPart(Net_URL_Mapper_Part::DYNAMIC, $c, $this->value); + $this->addPart($part); +} +wildcard grouping { + $c = $yy_subpatterns[0]; + $part = self::createPart(Net_URL_Mapper_Part::WILDCARD, $c, $this->value); + $this->addPart($part); +} +dynamic variable { + $c = $yy_subpatterns[0]; + $part = self::createPart(Net_URL_Mapper_Part::DYNAMIC, $c, $this->value); + $this->addPart($part); +} +wildcard variable { + $c = $yy_subpatterns[0]; + $part = self::createPart(Net_URL_Mapper_Part::WILDCARD, $c, $this->value); + $this->addPart($part); +} +static { + $c = $yy_subpatterns[0]; + $part = self::createPart(Net_URL_Mapper_Part::FIXED, $c, $this->value); + $this->addPart($part); +} +*/ +} + +?> \ No newline at end of file diff --git a/extlib/Services/oEmbed.php b/extlib/Services/oEmbed.php deleted file mode 100644 index 0dc8f01b2f..0000000000 --- a/extlib/Services/oEmbed.php +++ /dev/null @@ -1,357 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Validate.php'; -require_once 'Net/URL2.php'; -require_once 'HTTP/Request.php'; -require_once 'Services/oEmbed/Exception.php'; -require_once 'Services/oEmbed/Exception/NoSupport.php'; -require_once 'Services/oEmbed/Object.php'; - -/** - * Base class for consuming oEmbed objects - * - * - * 'http://www.flickr.com/services/oembed/' - * )); - * $object = $oEmbed->getObject(); - * - * // All of the objects have somewhat sane __toString() methods that allow - * // you to output them directly. - * echo (string)$object; - * - * ?> - * - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed -{ - /** - * HTTP timeout in seconds - * - * All HTTP requests made by Services_oEmbed will respect this timeout. - * This can be passed to {@link Services_oEmbed::setOption()} or to the - * options parameter in {@link Services_oEmbed::__construct()}. - * - * @var string OPTION_TIMEOUT Timeout in seconds - */ - const OPTION_TIMEOUT = 'http_timeout'; - - /** - * HTTP User-Agent - * - * All HTTP requests made by Services_oEmbed will be sent with the - * string set by this option. - * - * @var string OPTION_USER_AGENT The HTTP User-Agent string - */ - const OPTION_USER_AGENT = 'http_user_agent'; - - /** - * The API's URI - * - * If the API is known ahead of time this option can be used to explicitly - * set it. If not present then the API is attempted to be discovered - * through the auto-discovery mechanism. - * - * @var string OPTION_API - */ - const OPTION_API = 'oembed_api'; - - /** - * Options for oEmbed requests - * - * @var array $options The options for making requests - */ - protected $options = array( - self::OPTION_TIMEOUT => 3, - self::OPTION_API => null, - self::OPTION_USER_AGENT => 'Services_oEmbed 0.1.0' - ); - - /** - * URL of object to get embed information for - * - * @var object $url {@link Net_URL2} instance of URL of object - */ - protected $url = null; - - /** - * Constructor - * - * @param string $url The URL to fetch an oEmbed for - * @param array $options A list of options for the oEmbed lookup - * - * @throws {@link Services_oEmbed_Exception} if the $url is invalid - * @throws {@link Services_oEmbed_Exception} when no valid API is found - * @return void - */ - public function __construct($url, array $options = array()) - { - if (Validate::uri($url)) { - $this->url = new Net_URL2($url); - } else { - throw new Services_oEmbed_Exception('URL is invalid'); - } - - if (count($options)) { - foreach ($options as $key => $val) { - $this->setOption($key, $val); - } - } - - if ($this->options[self::OPTION_API] === null) { - $this->options[self::OPTION_API] = $this->discover($url); - } - } - - /** - * Set an option for the oEmbed request - * - * @param mixed $option The option name - * @param mixed $value The option value - * - * @see Services_oEmbed::OPTION_API, Services_oEmbed::OPTION_TIMEOUT - * @throws {@link Services_oEmbed_Exception} on invalid option - * @access public - * @return void - */ - public function setOption($option, $value) - { - switch ($option) { - case self::OPTION_API: - case self::OPTION_TIMEOUT: - break; - default: - throw new Services_oEmbed_Exception( - 'Invalid option "' . $option . '"' - ); - } - - $func = '_set_' . $option; - if (method_exists($this, $func)) { - $this->options[$option] = $this->$func($value); - } else { - $this->options[$option] = $value; - } - } - - /** - * Set the API option - * - * @param string $value The API's URI - * - * @throws {@link Services_oEmbed_Exception} on invalid API URI - * @see Validate::uri() - * @return string - */ - protected function _set_oembed_api($value) - { - if (!Validate::uri($value)) { - throw new Services_oEmbed_Exception( - 'API URI provided is invalid' - ); - } - - return $value; - } - - /** - * Get the oEmbed response - * - * @param array $params Optional parameters for - * - * @throws {@link Services_oEmbed_Exception} on cURL errors - * @throws {@link Services_oEmbed_Exception} on HTTP errors - * @throws {@link Services_oEmbed_Exception} when result is not parsable - * @return object The oEmbed response as an object - */ - public function getObject(array $params = array()) - { - $params['url'] = $this->url->getURL(); - if (!isset($params['format'])) { - $params['format'] = 'json'; - } - - $sets = array(); - foreach ($params as $var => $val) { - $sets[] = $var . '=' . urlencode($val); - } - - $url = $this->options[self::OPTION_API] . '?' . implode('&', $sets); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HEADER, false); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->options[self::OPTION_TIMEOUT]); - $result = curl_exec($ch); - - if (curl_errno($ch)) { - throw new Services_oEmbed_Exception( - curl_error($ch), curl_errno($ch) - ); - } - - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if (substr($code, 0, 1) != '2') { - throw new Services_oEmbed_Exception('Non-200 code returned. Got code ' . $code); - } - - curl_close($ch); - - switch ($params['format']) { - case 'json': - $res = json_decode($result); - if (!is_object($res)) { - throw new Services_oEmbed_Exception( - 'Could not parse JSON response' - ); - } - break; - case 'xml': - libxml_use_internal_errors(true); - $res = simplexml_load_string($result); - if (!$res instanceof SimpleXMLElement) { - $errors = libxml_get_errors(); - $err = array_shift($errors); - libxml_clear_errors(); - libxml_use_internal_errors(false); - throw new Services_oEmbed_Exception( - $err->message, $error->code - ); - } - break; - } - - return Services_oEmbed_Object::factory($res); - } - - /** - * Discover an oEmbed API - * - * @param string $url The URL to attempt to discover oEmbed for - * - * @throws {@link Services_oEmbed_Exception} if the $url is invalid - * @return string The oEmbed API endpoint discovered - */ - protected function discover($url) - { - $body = $this->sendRequest($url); - - // Find all tags that have a valid oembed type set. We then - // extract the href attribute for each type. - $regexp = '#]*)type[\s\n]*=[\s\n]*"' . - '(application/json|text/xml)\+oembed"([^>]*)>#im'; - - $m = $ret = array(); - if (!preg_match_all($regexp, $body, $m)) { - throw new Services_oEmbed_Exception_NoSupport( - 'No valid oEmbed links found on page' - ); - } - - foreach ($m[0] as $i => $link) { - $h = array(); - if (preg_match('/[\s\n]+href[\s\n]*=[\s\n]*"([^"]+)"/im', $link, $h)) { - $ret[$m[2][$i]] = $h[1]; - } - } - - return (isset($ret['application/json']) ? $ret['application/json'] : array_pop($ret)); - } - - /** - * Send a GET request to the provider - * - * @param mixed $url The URL to send the request to - * - * @throws {@link Services_oEmbed_Exception} on HTTP errors - * @return string The contents of the response - */ - private function sendRequest($url) - { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HEADER, false); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->options[self::OPTION_TIMEOUT]); - curl_setopt($ch, CURLOPT_USERAGENT, $this->options[self::OPTION_USER_AGENT]); - $result = curl_exec($ch); - if (curl_errno($ch)) { - throw new Services_oEmbed_Exception( - curl_error($ch), curl_errno($ch) - ); - } - - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if (substr($code, 0, 1) != '2') { - throw new Services_oEmbed_Exception('Non-200 code returned. Got code ' . $code); - } - - return $result; - } -} - -?> diff --git a/extlib/Services/oEmbed/Exception.php b/extlib/Services/oEmbed/Exception.php deleted file mode 100644 index 446ac2a706..0000000000 --- a/extlib/Services/oEmbed/Exception.php +++ /dev/null @@ -1,65 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'PEAR/Exception.php'; - -/** - * Base exception class for {@link Services_oEmbed} - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Exception extends PEAR_Exception -{ - -} - -?> diff --git a/extlib/Services/oEmbed/Exception/NoSupport.php b/extlib/Services/oEmbed/Exception/NoSupport.php deleted file mode 100644 index 384c7191f2..0000000000 --- a/extlib/Services/oEmbed/Exception/NoSupport.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -/** - * Exception class when no oEmbed support is discovered - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Exception_NoSupport extends Services_oEmbed_Exception -{ - -} - -?> diff --git a/extlib/Services/oEmbed/Object.php b/extlib/Services/oEmbed/Object.php deleted file mode 100644 index 9eedd7efb6..0000000000 --- a/extlib/Services/oEmbed/Object.php +++ /dev/null @@ -1,126 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Services/oEmbed/Object/Exception.php'; - -/** - * Base class for consuming oEmbed objects - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -abstract class Services_oEmbed_Object -{ - - /** - * Valid oEmbed object types - * - * @var array $types Array of valid object types - * @see Services_oEmbed_Object::factory() - */ - static protected $types = array( - 'photo' => 'Photo', - 'video' => 'Video', - 'link' => 'Link', - 'rich' => 'Rich' - ); - - /** - * Create an oEmbed object from result - * - * @param object $object Raw object returned from API - * - * @throws {@link Services_oEmbed_Object_Exception} on object error - * @return object Instance of object driver - * @see Services_oEmbed_Object_Link, Services_oEmbed_Object_Photo - * @see Services_oEmbed_Object_Rich, Services_oEmbed_Object_Video - */ - static public function factory($object) - { - if (!isset($object->type)) { - throw new Services_oEmbed_Object_Exception( - 'Object has no type' - ); - } - - $type = (string)$object->type; - if (!isset(self::$types[$type])) { - throw new Services_oEmbed_Object_Exception( - 'Object type is unknown or invalid: ' . $type - ); - } - - $file = 'Services/oEmbed/Object/' . self::$types[$type] . '.php'; - include_once $file; - - $class = 'Services_oEmbed_Object_' . self::$types[$type]; - if (!class_exists($class)) { - throw new Services_oEmbed_Object_Exception( - 'Object class is invalid or not present' - ); - } - - $instance = new $class($object); - return $instance; - } - - /** - * Instantiation is not allowed - * - * @return void - */ - private function __construct() - { - - } -} - -?> diff --git a/extlib/Services/oEmbed/Object/Common.php b/extlib/Services/oEmbed/Object/Common.php deleted file mode 100644 index f568ec89f5..0000000000 --- a/extlib/Services/oEmbed/Object/Common.php +++ /dev/null @@ -1,139 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -/** - * Base class for oEmbed objects - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -abstract class Services_oEmbed_Object_Common -{ - /** - * Raw object returned from API - * - * @var object $object The raw object from the API - */ - protected $object = null; - - /** - * Required fields per the specification - * - * @var array $required Array of required fields - * @link http://oembed.com - */ - protected $required = array(); - - /** - * Constructor - * - * @param object $object Raw object returned from the API - * - * @throws {@link Services_oEmbed_Object_Exception} on missing fields - * @return void - */ - public function __construct($object) - { - $this->object = $object; - - $this->required[] = 'version'; - foreach ($this->required as $field) { - if (!isset($this->$field)) { - throw new Services_oEmbed_Object_Exception( - 'Object is missing required ' . $field . ' attribute' - ); - } - } - } - - /** - * Get object variable - * - * @param string $var Variable to get - * - * @see Services_oEmbed_Object_Common::$object - * @return mixed Attribute's value or null if it's not set/exists - */ - public function __get($var) - { - if (property_exists($this->object, $var)) { - return $this->object->$var; - } - - return null; - } - - /** - * Is variable set? - * - * @param string $var Variable name to check - * - * @return boolean True if set, false if not - * @see Services_oEmbed_Object_Common::$object - */ - public function __isset($var) - { - if (property_exists($this->object, $var)) { - return (isset($this->object->$var)); - } - - return false; - } - - /** - * Require a sane __toString for all objects - * - * @return string - */ - abstract public function __toString(); -} - -?> diff --git a/extlib/Services/oEmbed/Object/Exception.php b/extlib/Services/oEmbed/Object/Exception.php deleted file mode 100644 index 6025ffd494..0000000000 --- a/extlib/Services/oEmbed/Object/Exception.php +++ /dev/null @@ -1,65 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Services/oEmbed/Exception.php'; - -/** - * Exception for {@link Services_oEmbed_Object} - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Object_Exception extends Services_oEmbed_Exception -{ - -} - -?> diff --git a/extlib/Services/oEmbed/Object/Link.php b/extlib/Services/oEmbed/Object/Link.php deleted file mode 100644 index 9b627a89ac..0000000000 --- a/extlib/Services/oEmbed/Object/Link.php +++ /dev/null @@ -1,73 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Services/oEmbed/Object/Common.php'; - -/** - * Link object for {@link Services_oEmbed} - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Object_Link extends Services_oEmbed_Object_Common -{ - /** - * Output a sane link - * - * @return string An HTML link of the object - */ - public function __toString() - { - return '' . $this->title . ''; - } -} - -?> diff --git a/extlib/Services/oEmbed/Object/Photo.php b/extlib/Services/oEmbed/Object/Photo.php deleted file mode 100644 index 5fbf4292fa..0000000000 --- a/extlib/Services/oEmbed/Object/Photo.php +++ /dev/null @@ -1,89 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Services/oEmbed/Object/Common.php'; - -/** - * Photo object for {@link Services_oEmbed} - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Object_Photo extends Services_oEmbed_Object_Common -{ - /** - * Required fields for photo objects - * - * @var array $required Required fields - */ - protected $required = array( - 'url', 'width', 'height' - ); - - /** - * Output a valid HTML tag for image - * - * @return string HTML tag for Photo - */ - public function __toString() - { - $img = 'title)) { - $img .= ' alt="' . $this->title . '"'; - } - - return $img . ' />'; - } -} - -?> diff --git a/extlib/Services/oEmbed/Object/Rich.php b/extlib/Services/oEmbed/Object/Rich.php deleted file mode 100644 index dbf6933ac7..0000000000 --- a/extlib/Services/oEmbed/Object/Rich.php +++ /dev/null @@ -1,82 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Services/oEmbed/Object/Common.php'; - -/** - * Photo object for {@link Services_oEmbed} - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Object_Rich extends Services_oEmbed_Object_Common -{ - /** - * Required fields for rich objects - * - * @var array $required Required fields - */ - protected $required = array( - 'html', 'width', 'height' - ); - - /** - * Output a the HTML tag for rich object - * - * @return string HTML for rich object - */ - public function __toString() - { - return $this->html; - } -} - -?> diff --git a/extlib/Services/oEmbed/Object/Video.php b/extlib/Services/oEmbed/Object/Video.php deleted file mode 100644 index 7461081151..0000000000 --- a/extlib/Services/oEmbed/Object/Video.php +++ /dev/null @@ -1,82 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Services/oEmbed/Object/Common.php'; - -/** - * Photo object for {@link Services_oEmbed} - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Object_Video extends Services_oEmbed_Object_Common -{ - /** - * Required fields for video objects - * - * @var array $required Required fields - */ - protected $required = array( - 'html', 'width', 'height' - ); - - /** - * Output a valid embed tag for video - * - * @return string HTML for video - */ - public function __toString() - { - return $this->html; - } -} - -?> diff --git a/index.php b/index.php index b2cf2d4a9d..7f2afffb5a 100644 --- a/index.php +++ b/index.php @@ -37,12 +37,17 @@ * @license GNU Affero General Public License http://www.gnu.org/licenses/ */ +$_startTime = microtime(true); +$_perfCounters = array(); + define('INSTALLDIR', dirname(__FILE__)); define('STATUSNET', true); define('LACONICA', true); // compatibility require_once INSTALLDIR . '/lib/common.php'; +register_shutdown_function('common_log_perf_counters'); + $user = null; $action = null; @@ -189,7 +194,7 @@ function checkMirror($action_obj, $args) function isLoginAction($action) { - static $loginActions = array('login', 'recoverpassword', 'api', 'doc', 'register', 'publicxrds', 'otp', 'opensearch', 'rsd'); + static $loginActions = array('login', 'recoverpassword', 'api', 'doc', 'register', 'publicxrds', 'otp', 'opensearch', 'rsd', 'hostmeta'); $login = null; diff --git a/js/Makefile b/js/Makefile new file mode 100644 index 0000000000..2f3dfcdf70 --- /dev/null +++ b/js/Makefile @@ -0,0 +1,15 @@ +.fake: all clean + +TARGETS=util.min.js json2.min.js +UTIL_SOURCES=util.js xbImportNode.js geometa.js + +all: $(TARGETS) + +clean: + rm -f $(TARGETS) + +util.min.js: $(UTIL_SOURCES) + cat $+ | yui-compressor --type js > $@ + +json2.min.js: json2.js + yui-compressor $+ > $@ diff --git a/js/emailsettings.js b/js/emailsettings.js new file mode 100644 index 0000000000..c7f85fe9ae --- /dev/null +++ b/js/emailsettings.js @@ -0,0 +1,23 @@ +$(function() { + +function toggleIncomingOptions() { + var enabled = $('#emailpost').attr('checked'); + if (enabled) { + // Note: button style currently does not respond to disabled in our main themes. + // Graying out the whole section with a 50% transparency will do for now. :) + // @todo: add a general 'disabled' class style to the base themes. + $('#emailincoming').removeAttr('style') + .find('input').removeAttr('disabled'); + } else { + $('#emailincoming').attr('style', 'opacity: 0.5') + .find('input').attr('disabled', 'disabled'); + } +} + +toggleIncomingOptions(); + +$('#emailpost').click(function() { + toggleIncomingOptions(); +}); + +}); diff --git a/js/geometa.js b/js/geometa.js new file mode 100644 index 0000000000..bba59b4486 --- /dev/null +++ b/js/geometa.js @@ -0,0 +1,217 @@ +// A shim to implement the W3C Geolocation API Specification using Gears or the Ajax API +if (typeof navigator.geolocation == "undefined" || navigator.geolocation.shim ) { (function(){ + +// -- BEGIN GEARS_INIT +(function() { + // We are already defined. Hooray! + if (window.google && google.gears) { + return; + } + + var factory = null; + + // Firefox + if (typeof GearsFactory != 'undefined') { + factory = new GearsFactory(); + } else { + // IE + try { + factory = new ActiveXObject('Gears.Factory'); + // privateSetGlobalObject is only required and supported on WinCE. + if (factory.getBuildInfo().indexOf('ie_mobile') != -1) { + factory.privateSetGlobalObject(this); + } + } catch (e) { + // Safari + if ((typeof navigator.mimeTypes != 'undefined') && navigator.mimeTypes["application/x-googlegears"]) { + factory = document.createElement("object"); + factory.style.display = "none"; + factory.width = 0; + factory.height = 0; + factory.type = "application/x-googlegears"; + document.documentElement.appendChild(factory); + } + } + } + + // *Do not* define any objects if Gears is not installed. This mimics the + // behavior of Gears defining the objects in the future. + if (!factory) { + return; + } + + // Now set up the objects, being careful not to overwrite anything. + // + // Note: In Internet Explorer for Windows Mobile, you can't add properties to + // the window object. However, global objects are automatically added as + // properties of the window object in all browsers. + if (!window.google) { + google = {}; + } + + if (!google.gears) { + google.gears = {factory: factory}; + } +})(); +// -- END GEARS_INIT + +var GearsGeoLocation = (function() { + // -- PRIVATE + var geo = google.gears.factory.create('beta.geolocation'); + + var wrapSuccess = function(callback, self) { // wrap it for lastPosition love + return function(position) { + callback(position); + self.lastPosition = position; + }; + }; + + // -- PUBLIC + return { + shim: true, + + type: "Gears", + + lastPosition: null, + + getCurrentPosition: function(successCallback, errorCallback, options) { + var self = this; + var sc = wrapSuccess(successCallback, self); + geo.getCurrentPosition(sc, errorCallback, options); + }, + + watchPosition: function(successCallback, errorCallback, options) { + geo.watchPosition(successCallback, errorCallback, options); + }, + + clearWatch: function(watchId) { + geo.clearWatch(watchId); + }, + + getPermission: function(siteName, imageUrl, extraMessage) { + geo.getPermission(siteName, imageUrl, extraMessage); + } + + }; +}); + +var AjaxGeoLocation = (function() { + // -- PRIVATE + var loading = false; + var loadGoogleLoader = function() { + if (!hasGoogleLoader() && !loading) { + loading = true; + var s = document.createElement('script'); + s.src = (document.location.protocol == "https:"?"https://":"http://") + 'www.google.com/jsapi?callback=_google_loader_apiLoaded'; + s.type = "text/javascript"; + document.getElementsByTagName('body')[0].appendChild(s); + } + }; + + var queue = []; + var addLocationQueue = function(callback) { + queue.push(callback); + }; + + var runLocationQueue = function() { + if (hasGoogleLoader()) { + while (queue.length > 0) { + var call = queue.pop(); + call(); + } + } + }; + + window['_google_loader_apiLoaded'] = function() { + runLocationQueue(); + }; + + var hasGoogleLoader = function() { + return (window['google'] && google['loader']); + }; + + var checkGoogleLoader = function(callback) { + if (hasGoogleLoader()) { return true; } + + addLocationQueue(callback); + + loadGoogleLoader(); + + return false; + }; + + loadGoogleLoader(); // start to load as soon as possible just in case + + // -- PUBLIC + return { + shim: true, + + type: "ClientLocation", + + lastPosition: null, + + getCurrentPosition: function(successCallback, errorCallback, options) { + var self = this; + if (!checkGoogleLoader(function() { + self.getCurrentPosition(successCallback, errorCallback, options); + })) { return; } + + if (google.loader.ClientLocation) { + var cl = google.loader.ClientLocation; + + var position = { + coords: { + latitude: cl.latitude, + longitude: cl.longitude, + altitude: null, + accuracy: 43000, // same as Gears accuracy over wifi? + altitudeAccuracy: null, + heading: null, + speed: null + }, + // extra info that is outside of the bounds of the core API + address: { + city: cl.address.city, + country: cl.address.country, + country_code: cl.address.country_code, + region: cl.address.region + }, + timestamp: new Date() + }; + + successCallback(position); + + this.lastPosition = position; + } else if (errorCallback === "function") { + errorCallback({ code: 3, message: "Using the Google ClientLocation API and it is not able to calculate a location."}); + } + }, + + watchPosition: function(successCallback, errorCallback, options) { + this.getCurrentPosition(successCallback, errorCallback, options); + + var self = this; + var watchId = setInterval(function() { + self.getCurrentPosition(successCallback, errorCallback, options); + }, 10000); + + return watchId; + }, + + clearWatch: function(watchId) { + clearInterval(watchId); + }, + + getPermission: function(siteName, imageUrl, extraMessage) { + // for now just say yes :) + return true; + } + + }; +}); + +// If you have Gears installed use that, else use Ajax ClientLocation +navigator.geolocation = (window.google && google.gears) ? GearsGeoLocation() : AjaxGeoLocation(); + +})(); +} diff --git a/js/jquery.cookie.min.js b/js/jquery.cookie.min.js new file mode 100644 index 0000000000..eb129db969 --- /dev/null +++ b/js/jquery.cookie.min.js @@ -0,0 +1 @@ +jQuery.cookie=function(b,j,m){if(typeof j!="undefined"){m=m||{};if(j===null){j="";m.expires=-1}var e="";if(m.expires&&(typeof m.expires=="number"||m.expires.toUTCString)){var f;if(typeof m.expires=="number"){f=new Date();f.setTime(f.getTime()+(m.expires*24*60*60*1000))}else{f=m.expires}e="; expires="+f.toUTCString()}var l=m.path?"; path="+(m.path):"";var g=m.domain?"; domain="+(m.domain):"";var a=m.secure?"; secure":"";document.cookie=[b,"=",encodeURIComponent(j),e,l,g,a].join("")}else{var d=null;if(document.cookie&&document.cookie!=""){var k=document.cookie.split(";");for(var h=0;h= 0 ? '&' : '?') + q; - options.data = null; // data is null for 'get' - } - else - options.data = q; // data is the query string for 'post' - - var $form = this, callbacks = []; - if (options.resetForm) callbacks.push(function() { $form.resetForm(); }); - if (options.clearForm) callbacks.push(function() { $form.clearForm(); }); - - // perform a load on the target only if dataType is not provided - if (!options.dataType && options.target) { - var oldSuccess = options.success || function(){}; - callbacks.push(function(data) { - $(options.target).html(data).each(oldSuccess, arguments); - }); - } - else if (options.success) - callbacks.push(options.success); - - options.success = function(data, status) { - for (var i=0, max=callbacks.length; i < max; i++) - callbacks[i].apply(options, [data, status, $form]); - }; - - // are there files to upload? - var files = $('input:file', this).fieldValue(); - var found = false; - for (var j=0; j < files.length; j++) - if (files[j]) - found = true; - - // options.iframe allows user to force iframe mode - if (options.iframe || found) { - // hack to fix Safari hang (thanks to Tim Molendijk for this) - // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d - if ($.browser.safari && options.closeKeepAlive) - $.get(options.closeKeepAlive, fileUpload); - else - fileUpload(); - } - else - $.ajax(options); - - // fire 'notify' event - this.trigger('form-submit-notify', [this, options]); - return this; - - - // private function for handling file uploads (hat tip to YAHOO!) - function fileUpload() { - var form = $form[0]; - - if ($(':input[name=submit]', form).length) { - alert('Error: Form elements must not be named "submit".'); - return; - } - - var opts = $.extend({}, $.ajaxSettings, options); - var s = jQuery.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts); - - var id = 'jqFormIO' + (new Date().getTime()); - var $io = $('