diff --git a/EVENTS.txt b/EVENTS.txt index c7e9ee1a79..9f98637bc6 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 @@ -876,233 +898,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) @@ -1182,3 +984,26 @@ 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 + +EndAtomPubNewActivity: When a new activity comes in through Atom Pub API +- $activity: received activity +- $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 diff --git a/README b/README index 7531d3f248..3d23e2393b 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. @@ -1554,6 +1552,19 @@ maxnoticelength: If a notice is strictly longer than this limit, all URLs in the notice will be shortened. Users can override. -1 means the text limit for notices. +router +------ + +We use a router class for mapping URLs to code. This section controls +how that router works. + +cache: whether to cache the router in memcache (or another caching + mechanism). Defaults to true, but may be set to false for + developers (who might be actively adding pages, so won't want the + router cached) or others who see strange behavior. You're unlikely + to need this unless you're a developer. + + Plugins ======= diff --git a/actions/allrss.php b/actions/allrss.php index d398c8a6ad..573bb4eb2f 100644 --- a/actions/allrss.php +++ b/actions/allrss.php @@ -56,6 +56,8 @@ class AllrssAction extends Rss10Action * @param array $args Web and URL arguments * * @return boolean false if user doesn't exist + * + */ function prepare($args) { parent::prepare($args); diff --git a/actions/apiatomservice.php b/actions/apiatomservice.php new file mode 100644 index 0000000000..fb9d6aee82 --- /dev/null +++ b/actions/apiatomservice.php @@ -0,0 +1,100 @@ +. + * + * @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)) { + $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')); + $this->elementStart('workspace'); + $this->element('atom:title', null, _('Main')); + $this->elementStart('collection', + array('href' => common_local_url('ApiTimelineUser', + array('id' => $this->user->id, + 'format' => 'atom')))); + $this->element('atom:title', + null, + sprintf(_("%s timeline"), + $this->user->nickname)); + $this->element('accept', null, 'application/atom+xml;type=entry'); + $this->elementEnd('collection'); + $this->elementEnd('workspace'); + $this->elementEnd('service'); + $this->endXML(); + } +} 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/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/apisearchatom.php b/actions/apisearchatom.php index 6743e92c84..32ff918da3 100644 --- a/actions/apisearchatom.php +++ b/actions/apisearchatom.php @@ -114,7 +114,7 @@ class ApiSearchAtomAction extends ApiPrivateAuthAction $this->page = 1; } - // TODO: Suppport since_id -- we need to tweak the backend + // TODO: Suppport max_id -- we need to tweak the backend // Search classes to support it. $this->since_id = $this->trimmed('since_id'); @@ -177,6 +177,10 @@ class ApiSearchAtomAction extends ApiPrivateAuthAction $this->max_id = $notice->id; } + if ($this->since_id && $notice->id <= $this->since_id) { + break; + } + if ($cnt > $this->rpp) { break; } diff --git a/actions/apisearchjson.php b/actions/apisearchjson.php index 38e612ee39..dd442b7f24 100644 --- a/actions/apisearchjson.php +++ b/actions/apisearchjson.php @@ -85,6 +85,9 @@ class ApiSearchJSONAction extends ApiPrivateAuthAction $this->page = 1; } + // TODO: Suppport max_id -- we need to tweak the backend + // Search classes to support it. + $this->since_id = $this->trimmed('since_id'); $this->geocode = $this->trimmed('geocode'); @@ -127,9 +130,9 @@ class ApiSearchJSONAction extends ApiPrivateAuthAction $cnt = $notice->find(); } - // TODO: since_id, lang, geocode + // TODO: max_id, lang, geocode - $results = new JSONSearchResultsList($notice, $q, $this->rpp, $this->page); + $results = new JSONSearchResultsList($notice, $q, $this->rpp, $this->page, $this->since_id); $this->initDocument('json'); $results->show(); diff --git a/actions/apistatusesshow.php b/actions/apistatusesshow.php index a98e45f79c..e684a07eec 100644 --- a/actions/apistatusesshow.php +++ b/actions/apistatusesshow.php @@ -100,13 +100,23 @@ 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: + $this->clientError(_('HTTP method not supported.'), 405); + return; + } } /** @@ -117,10 +127,18 @@ 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: + throw new Exception(sprintf(_("Unsupported format: %s"), $this->format)); } } else { // XXX: Twitter just sets a 404 header and doens't bother @@ -153,9 +171,14 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction * * @return boolean true */ + function isReadOnly($args) { - return true; + if ($_SERVER['REQUEST_METHOD'] == 'GET') { + return true; + } else { + return false; + } } /** @@ -197,4 +220,31 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction return null; } + + function deleteNotice() + { + if ($this->format != 'atom') { + $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))) { + $this->clientError(_('Can\'t 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'); + print(sprintf(_('Deleted notice %d'), $this->notice->id)); + print("\n"); + } } diff --git a/actions/apistatusesupdate.php b/actions/apistatusesupdate.php index 1a3b549004..a8ec7f8bb9 100644 --- a/actions/apistatusesupdate.php +++ b/actions/apistatusesupdate.php @@ -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 diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 0046c462d7..d90507aa44 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,13 +217,18 @@ 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; @@ -188,9 +241,14 @@ class ApiTimelineUserAction extends ApiBareAuthAction * * @return boolean true */ + function isReadOnly($args) { - return true; + if ($_SERVER['REQUEST_METHOD'] == 'GET') { + return true; + } else { + return false; + } } /** @@ -221,17 +279,215 @@ 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 = 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); + + if (Event::handle('StartAtomPubNewActivity', array(&$activity))) { + + if ($activity->verb != ActivityVerb::POST) { + // TRANS: Client error displayed when not using the POST verb. + // TRANS: 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, $saved)); + } + + if (!empty($saved)) { + header("Location: " . common_local_url('ApiStatusesShow', array('notice_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. + $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/editgroup.php b/actions/editgroup.php index 4d3af34c7b..ab4dbb2836 100644 --- a/actions/editgroup.php +++ b/actions/editgroup.php @@ -177,21 +177,14 @@ class EditgroupAction extends GroupDesignAction return; } - $nickname = common_canonical_nickname($this->trimmed('nickname')); + $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 (!Validate::string($nickname, array('min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT))) { - // TRANS: Group edit form validation error. - $this->showForm(_('Nickname must have only lowercase letters '. - 'and numbers and no spaces.')); - return; - } else if ($this->nicknameExists($nickname)) { + if ($this->nicknameExists($nickname)) { // TRANS: Group edit form validation error. $this->showForm(_('Nickname already in use. Try another one.')); return; @@ -241,9 +234,7 @@ class EditgroupAction extends GroupDesignAction } foreach ($aliases as $alias) { - if (!Validate::string($alias, array('min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT))) { + if (!Nickname::isValid($alias)) { // TRANS: Group edit form validation error. $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias)); return; diff --git a/actions/emailsettings.php b/actions/emailsettings.php index 9c250fc8a9..4a7dc1b871 100644 --- a/actions/emailsettings.php +++ b/actions/emailsettings.php @@ -79,6 +79,7 @@ class EmailsettingsAction extends AccountSettingsAction function showScripts() { parent::showScripts(); + $this->script('emailsettings.js'); $this->autofocus('email'); } @@ -149,6 +150,26 @@ 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); @@ -163,13 +184,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 +208,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')); @@ -299,43 +325,48 @@ class EmailsettingsAction extends AccountSettingsAction 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(); - - assert(!is_null($user)); // should already be checked - - $user->query('BEGIN'); - - $original = clone($user); - - $user->emailnotifysub = $emailnotifysub; - $user->emailnotifyfav = $emailnotifyfav; - $user->emailnotifymsg = $emailnotifymsg; - $user->emailnotifynudge = $emailnotifynudge; - $user->emailnotifyattn = $emailnotifyattn; - $user->emailmicroid = $emailmicroid; - $user->emailpost = $emailpost; - - $result = $user->update($original); - - 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; - } - - $user->query('COMMIT'); - - // TRANS: Confirmation message for successful e-mail preferences save. - $this->showForm(_('Email preferences saved.'), true); + $user = common_current_user(); + + if (Event::handle('StartEmailSaveForm', array($this, &$user))) { + + $emailnotifysub = $this->boolean('emailnotifysub'); + $emailnotifyfav = $this->boolean('emailnotifyfav'); + $emailnotifymsg = $this->boolean('emailnotifymsg'); + $emailnotifynudge = $this->boolean('emailnotifynudge'); + $emailnotifyattn = $this->boolean('emailnotifyattn'); + $emailmicroid = $this->boolean('emailmicroid'); + $emailpost = $this->boolean('emailpost'); + + assert(!is_null($user)); // should already be checked + + $user->query('BEGIN'); + + $original = clone($user); + + $user->emailnotifysub = $emailnotifysub; + $user->emailnotifyfav = $emailnotifyfav; + $user->emailnotifymsg = $emailnotifymsg; + $user->emailnotifynudge = $emailnotifynudge; + $user->emailnotifyattn = $emailnotifyattn; + $user->emailmicroid = $emailmicroid; + $user->emailpost = $emailpost; + + $result = $user->update($original); + + 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; + } + + $user->query('COMMIT'); + + Event::handle('EndEmailSaveForm', array($this)); + + // TRANS: Confirmation message for successful e-mail preferences save. + $this->showForm(_('Email preferences saved.'), true); + } } /** @@ -501,6 +532,7 @@ class EmailsettingsAction extends AccountSettingsAction $orig = clone($user); $user->incomingemail = null; + $user->emailpost = 0; if (!$user->updateKeys($orig)) { common_log_db_error($user, 'UPDATE', __FILE__); @@ -525,6 +557,7 @@ 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__); diff --git a/actions/favorited.php b/actions/favorited.php index d8980440d1..19d49feecf 100644 --- a/actions/favorited.php +++ b/actions/favorited.php @@ -185,29 +185,11 @@ class FavoritedAction extends Action function showContent() { - $weightexpr = common_sql_weight('fave.modified', common_config('popular', 'dropoff')); - $cutoff = sprintf("fave.modified > '%s'", - common_sql_date(time() - common_config('popular', 'cutoff'))); - - $qry = 'SELECT notice.*, '. - $weightexpr . ' as weight ' . - 'FROM notice JOIN fave ON notice.id = fave.notice_id ' . - "WHERE $cutoff " . - 'GROUP BY id,profile_id,uri,content,rendered,url,created,notice.modified,reply_to,is_local,source,notice.conversation ' . - 'ORDER BY weight DESC'; - - $offset = ($this->page - 1) * NOTICES_PER_PAGE; - $limit = NOTICES_PER_PAGE + 1; - - if (common_config('db', 'type') == 'pgsql') { - $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; - } else { - $qry .= ' LIMIT ' . $offset . ', ' . $limit; - } - - $notice = Memcached_DataObject::cachedQuery('Notice', - $qry, - 600); + $pop = new Popularity(); + $pop->offset = ($this->page - 1) * NOTICES_PER_PAGE; + $pop->limit = NOTICES_PER_PAGE; + $pop->expiry = 600; + $notice = $pop->getNotices(); $nl = new NoticeList($notice, $this); diff --git a/actions/hostmeta.php b/actions/hostmeta.php index b7beee5a86..331fc8a999 100644 --- a/actions/hostmeta.php +++ b/actions/hostmeta.php @@ -51,6 +51,11 @@ class HostMetaAction extends Action $xrd->host = $domain; 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)); } diff --git a/actions/newgroup.php b/actions/newgroup.php index e0e7978c32..95af6415e5 100644 --- a/actions/newgroup.php +++ b/actions/newgroup.php @@ -113,21 +113,18 @@ class NewgroupAction extends Action function trySave() { - $nickname = $this->trimmed('nickname'); + 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))) { - // TRANS: Group create form validation error. - $this->showForm(_('Nickname must have only lowercase letters '. - 'and numbers and no spaces.')); - return; - } else if ($this->nicknameExists($nickname)) { + if ($this->nicknameExists($nickname)) { // TRANS: Group create form validation error. $this->showForm(_('Nickname already in use. Try another one.')); return; @@ -177,9 +174,7 @@ class NewgroupAction extends Action } foreach ($aliases as $alias) { - if (!Validate::string($alias, array('min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT))) { + if (!Nickname::isValid($alias)) { // TRANS: Group create form validation error. $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias)); return; 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 0d4dcfccd5..faafd9551d 100644 --- a/actions/newnotice.php +++ b/actions/newnotice.php @@ -154,7 +154,7 @@ class NewnoticeAction extends Action return; } - $content_shortened = common_shorten_links($content); + $content_shortened = $user->shortenLinks($content); if (Notice::contentTooLong($content_shortened)) { // TRANS: Client error displayed when the parameter "status" is missing. // TRANS: %d is the maximum number of character for a notice. diff --git a/actions/oembed.php b/actions/oembed.php index da3aa0c716..09d68a446e 100644 --- a/actions/oembed.php +++ b/actions/oembed.php @@ -108,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', diff --git a/actions/profilesettings.php b/actions/profilesettings.php index e1a0f8b6d0..28b1d20f34 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -225,7 +225,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 +242,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; diff --git a/actions/register.php b/actions/register.php index a600dfe447..075b1af99d 100644 --- a/actions/register.php +++ b/actions/register.php @@ -191,7 +191,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')) { @@ -199,11 +203,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)) { 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/shownotice.php b/actions/shownotice.php index 6534617319..91b0901bf4 100644 --- a/actions/shownotice.php +++ b/actions/shownotice.php @@ -325,8 +325,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/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/File.php b/classes/File.php index da029e39b6..ef9dbf14ab 100644 --- a/classes/File.php +++ b/classes/File.php @@ -116,10 +116,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 +183,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 +200,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 +213,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 +225,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 +239,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 +321,7 @@ class File extends Memcached_DataObject } $protocol = 'https'; - } else { - $path = common_config('attachments', 'path'); $server = common_config('attachments', 'server'); @@ -339,22 +366,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 +402,14 @@ 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); + } } 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 92f0125a40..1096f500bf 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,14 +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? @@ -206,11 +219,11 @@ class File_redirection extends Memcached_DataObject * @return string */ - function forceShort($long_url) + function forceShort($long_url, $user) { $canon = File_redirection::_canonUrl($long_url); - $short_url = File_redirection::_userMakeShort($canon, true); + $short_url = File_redirection::_userMakeShort($canon, $user, true); // Did we get one? Is it shorter? if (!empty($short_url)) { @@ -220,8 +233,8 @@ class File_redirection extends Memcached_DataObject } } - function _userMakeShort($long_url, $force = false) { - $short_url = common_shorten_url($long_url, $force); + function _userMakeShort($long_url, User $user=null, $force = false) { + $short_url = common_shorten_url($long_url, $user, $force); if (!empty($short_url) && $short_url != $long_url) { $short_url = (string)$short_url; // store it @@ -265,6 +278,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/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 2f8c7d5d58..c36fb702b9 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -256,9 +256,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. @@ -476,7 +481,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 +496,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 +532,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); @@ -904,7 +910,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(); @@ -1220,60 +1226,187 @@ class Notice extends Memcached_DataObject return $groups; } - function asActivity() + /** + * Convert a notice into an activity for export. + * + * @param User $cur Current user + * + * @return Activity activity object representing this Notice. + */ + + function asActivity($cur = null, $source = false) { - $profile = $this->getProfile(); + $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id)); + + if (!empty($act)) { + return $act; + } $act = new Activity(); + + if (Event::handle('StartNoticeAsActivity', array($this, &$act))) { - $act->actor = ActivityObject::fromProfile($profile); - $act->verb = ActivityVerb::POST; - $act->objects[] = ActivityObject::fromNotice($this); + $profile = $this->getProfile(); + + $act->actor = ActivityObject::fromProfile($profile); + $act->verb = ActivityVerb::POST; + $act->objects[] = ActivityObject::fromNotice($this); - $act->time = strtotime($this->created); - $act->link = $this->bestUrl(); + // XXX: should this be handled by default processing for object entry? - $act->content = common_xml_safe_str($this->rendered); - $act->id = $this->uri; - $act->title = common_xml_safe_str($this->content); + $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); - $ctx = new ActivityContext(); + // Categories - if (!empty($this->reply_to)) { - $reply = Notice::staticGet('id', $this->reply_to); - if (!empty($reply)) { - $ctx->replyToID = $reply->uri; - $ctx->replyToUrl = $reply->bestUrl(); + $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(); - - foreach ($reply_ids as $id) { - $profile = Profile::staticGet('id', $id); - if (!empty($profile)) { - $ctx->attention[] = $profile->getUri(); + + $ctx = new ActivityContext(); + + 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; + + $noticeInfoAttr = array('local_id' => $this->id); // local notice ID (useful to clients for ordering) + + $ns = $this->getSource(); + + if (!empty($ns)) { + $noticeInfoAttr['source'] = $ns->code; + if (!empty($ns->url)) { + $noticeInfoAttr['source_link'] = $ns->url; + if (!empty($ns->name)) { + $noticeInfoAttr['source'] = '' + . htmlspecialchars($ns->name) + . ''; + } + } + } + + if (!empty($cur)) { + $noticeInfoAttr['favorite'] = ($cur->hasFave($this)) ? "true" : "false"; + $cp = $cur->getProfile(); + $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this->id)) ? "true" : "false"; + } + + if (!empty($this->repeat_of)) { + $noticeInfoAttr['repeat_of'] = $this->repeat_of; + } + + $act->extra[] = array('statusnet:notice_info', $noticeInfoAttr, null); + + if ($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; } @@ -1283,343 +1416,10 @@ class Notice extends Memcached_DataObject function asAtomEntry($namespace=false, $source=false, $author=true, $cur=null) { - $profile = $this->getProfile(); - - $xs = new XMLStringer(true); - - 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(); - } - - if (Event::handle('StartActivityStart', array(&$this, &$xs, &$attrs))) { - $xs->elementStart('entry', $attrs); - Event::handle('EndActivityStart', array(&$this, &$xs, &$attrs)); - } - - 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) - - $ns = $this->getSource(); - - if (!empty($ns)) { - $noticeInfoAttr['source'] = $ns->code; - if (!empty($ns->url)) { - $noticeInfoAttr['source_link'] = $ns->url; - if (!empty($ns->name)) { - $noticeInfoAttr['source'] = '' - . htmlspecialchars($ns->name) - . ''; - } - } - } - - if (!empty($cur)) { - $noticeInfoAttr['favorite'] = ($cur->hasFave($this)) ? "true" : "false"; - $profile = $cur->getProfile(); - $noticeInfoAttr['repeated'] = ($profile->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(); + $act = $this->asActivity($cur, $source); + return $act->asString($namespace, $author); } + /** * Returns an XML string fragment with a reference to a notice as an @@ -1630,6 +1430,7 @@ class Notice extends Memcached_DataObject * @param string $element one of 'subject', 'object', 'target' * @return string */ + function asActivityNoun($element) { $noun = ActivityObject::fromNotice($this); diff --git a/classes/Profile.php b/classes/Profile.php index b56e508c6a..05df8899e9 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -125,6 +125,14 @@ class Profile extends Memcached_DataObject return $avatar; } + /** + * Delete attached avatars for this user from the database and filesystem. + * This should be used instead of a batch delete() to ensure that files + * get removed correctly. + * + * @param boolean $original true to delete only the original-size file + * @return + */ function delete_avatars($original=true) { $avatar = new Avatar(); @@ -494,6 +502,29 @@ class Profile extends Memcached_DataObject return $cnt; } + /** + * Is this profile subscribed to another profile? + * + * @param Profile $other + * @return boolean + */ + function isSubscribed($other) + { + return Subscription::exists($this, $other); + } + + /** + * Are these two profiles subscribed to each other? + * + * @param Profile $other + * @return boolean + */ + function mutuallySubscribed($other) + { + return $this->isSubscribed($other) && + $other->isSubscribed($this); + } + function hasFave($notice) { $cache = Cache::instance(); @@ -641,9 +672,11 @@ class Profile extends Memcached_DataObject $this->_deleteMessages(); $this->_deleteTags(); $this->_deleteBlocks(); + $this->delete_avatars(); - $related = array('Avatar', - 'Reply', + // Warning: delete() will run on the batch objects, + // not on individual objects. + $related = array('Reply', 'Group_member', ); Event::handle('ProfileDeleteRelated', array($this, &$related)); 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/User.php b/classes/User.php index 5914f0b806..d4f182f7ee 100644 --- a/classes/User.php +++ b/classes/User.php @@ -79,7 +79,8 @@ class User extends Memcached_DataObject function isSubscribed($other) { - return Subscription::exists($this->getProfile(), $other); + $profile = $this->getProfile(); + return $profile->isSubscribed($other); } // 'update' won't write key columns, so we have to do it ourselves. @@ -110,6 +111,16 @@ class User extends Memcached_DataObject return $result; } + /** + * Check whether the given nickname is potentially usable, or if it's + * excluded by any blacklists on this system. + * + * WARNING: INPUT IS NOT VALIDATED OR NORMALIZED. NON-NORMALIZED INPUT + * OR INVALID INPUT MAY LEAD TO FALSE RESULTS. + * + * @param string $nickname + * @return boolean true if clear, false if blacklisted + */ static function allowed_nickname($nickname) { // XXX: should already be validated for size, content, etc. @@ -413,8 +424,8 @@ class User extends Memcached_DataObject function mutuallySubscribed($other) { - return $this->isSubscribed($other) && - $other->isSubscribed($this); + $profile = $this->getProfile(); + return $profile->mutuallySubscribed($other); } function mutuallySubscribedUsers() @@ -911,4 +922,55 @@ class User extends Memcached_DataObject throw new ServerException(_('Single-user mode code called when not enabled.')); } } + + /** + * This is kind of a hack for using external setup code that's trying to + * build single-user sites. + * + * Will still return a username if the config singleuser/nickname is set + * even if the account doesn't exist, which normally indicates that the + * site is horribly misconfigured. + * + * At the moment, we need to let it through so that router setup can + * complete, otherwise we won't be able to create the account. + * + * This will be easier when we can more easily create the account and + * *then* switch the site to 1user mode without jumping through hoops. + * + * @return string + * @throws ServerException if no valid single user account is present + * @throws ServerException if called when not in single-user mode + */ + static function singleUserNickname() + { + try { + $user = User::singleUser(); + return $user->nickname; + } catch (Exception $e) { + if (common_config('singleuser', 'enabled') && common_config('singleuser', 'nickname')) { + common_log(LOG_WARN, "Warning: code attempting to pull single-user nickname when the account does not exist. If this is not setup time, this is probably a bug."); + return common_config('singleuser', 'nickname'); + } + throw $e; + } + } + + /** + * Find and shorten links in the given text using this user's URL shortening + * settings. + * + * By default, links will be left untouched if the text is shorter than the + * configured maximum notice length. Pass true for the $always parameter + * to force all links to be shortened regardless. + * + * Side effects: may save file and file_redirection records for referenced URLs. + * + * @param string $text + * @param boolean $always + * @return string + */ + public function shortenLinks($text, $always=false) + { + return common_shorten_links($text, $always, $this); + } } diff --git a/db/074to080.sql b/db/074to080.sql index ff08191596..e3631e214a 100644 --- a/db/074to080.sql +++ b/db/074to080.sql @@ -107,3 +107,15 @@ create table group_alias ( index group_alias_group_id_idx (group_id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + +create table session ( + + id varchar(32) primary key comment 'session ID', + session_data text comment 'session data', + created datetime not null comment 'date this record was created', + modified timestamp comment 'date this record was modified', + + index session_modified_idx (modified) + +) 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/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/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 = $('