diff --git a/actions/apiatomservice.php b/actions/apiatomservice.php index fb9d6aee82..b60b312fc4 100644 --- a/actions/apiatomservice.php +++ b/actions/apiatomservice.php @@ -80,7 +80,8 @@ class ApiAtomServiceAction extends ApiBareAuthAction $this->startXML(); $this->elementStart('service', array('xmlns' => 'http://www.w3.org/2007/app', - 'xmlns:atom' => 'http://www.w3.org/2005/Atom')); + 'xmlns:atom' => 'http://www.w3.org/2005/Atom', + 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/')); $this->elementStart('workspace'); $this->element('atom:title', null, _('Main')); $this->elementStart('collection', @@ -92,6 +93,37 @@ class ApiAtomServiceAction extends ApiBareAuthAction sprintf(_("%s timeline"), $this->user->nickname)); $this->element('accept', null, 'application/atom+xml;type=entry'); + $this->element('activity:verb', null, ActivityVerb::POST); + $this->elementEnd('collection'); + $this->elementStart('collection', + array('href' => common_local_url('AtomPubSubscriptionFeed', + array('subscriber' => $this->user->id)))); + $this->element('atom:title', + null, + sprintf(_("%s subscriptions"), + $this->user->nickname)); + $this->element('accept', null, 'application/atom+xml;type=entry'); + $this->element('activity:verb', null, ActivityVerb::FOLLOW); + $this->elementEnd('collection'); + $this->elementStart('collection', + array('href' => common_local_url('AtomPubFavoriteFeed', + array('profile' => $this->user->id)))); + $this->element('atom:title', + null, + sprintf(_("%s favorites"), + $this->user->nickname)); + $this->element('accept', null, 'application/atom+xml;type=entry'); + $this->element('activity:verb', null, ActivityVerb::FAVORITE); + $this->elementEnd('collection'); + $this->elementStart('collection', + array('href' => common_local_url('AtomPubMembershipFeed', + array('profile' => $this->user->id)))); + $this->element('atom:title', + null, + sprintf(_("%s memberships"), + $this->user->nickname)); + $this->element('accept', null, 'application/atom+xml;type=entry'); + $this->element('activity:verb', null, ActivityVerb::JOIN); $this->elementEnd('collection'); $this->elementEnd('workspace'); $this->elementEnd('service'); diff --git a/actions/atompubfavoritefeed.php b/actions/atompubfavoritefeed.php new file mode 100644 index 0000000000..478a01b7c6 --- /dev/null +++ b/actions/atompubfavoritefeed.php @@ -0,0 +1,374 @@ +. + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Feed of ActivityStreams 'favorite' actions + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class AtompubfavoritefeedAction extends ApiAuthAction +{ + private $_profile = null; + private $_faves = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $this->_profile = Profile::staticGet('id', $this->trimmed('profile')); + + if (empty($this->_profile)) { + throw new ClientException(_('No such profile'), 404); + } + + $offset = ($this->page-1) * $this->count; + $limit = $this->count + 1; + + $this->_faves = Fave::byProfile($this->_profile->id, + $offset, + $limit); + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + parent::handle($argarray); + + switch ($_SERVER['REQUEST_METHOD']) { + case 'HEAD': + case 'GET': + $this->showFeed(); + break; + case 'POST': + $this->addFavorite(); + break; + default: + throw new ClientException(_('HTTP method not supported.'), 405); + return; + } + + return; + } + + /** + * Show a feed of favorite activity streams objects + * + * @return void + */ + + function showFeed() + { + header('Content-Type: application/atom+xml; charset=utf-8'); + + $url = common_local_url('AtomPubFavoriteFeed', + array('profile' => $this->_profile->id)); + + $feed = new Atom10Feed(true); + + $feed->addNamespace('activity', + 'http://activitystrea.ms/spec/1.0/'); + + $feed->addNamespace('poco', + 'http://portablecontacts.net/spec/1.0'); + + $feed->addNamespace('media', + 'http://purl.org/syndication/atommedia'); + + $feed->id = $url; + + $feed->setUpdated('now'); + + $feed->addAuthor($this->_profile->getBestName(), + $this->_profile->getURI()); + + $feed->setTitle(sprintf(_("%s favorites"), + $this->_profile->getBestName())); + + $feed->setSubtitle(sprintf(_("Notices %s has favorited to on %s"), + $this->_profile->getBestName(), + common_config('site', 'name'))); + + $feed->addLink(common_local_url('showfavorites', + array('nickname' => + $this->_profile->nickname))); + + $feed->addLink($url, + array('rel' => 'self', + 'type' => 'application/atom+xml')); + + // If there's more... + + if ($this->page > 1) { + $feed->addLink($url, + array('rel' => 'first', + 'type' => 'application/atom+xml')); + + $feed->addLink(common_local_url('AtomPubFavoriteFeed', + array('profile' => + $this->_profile->id), + array('page' => + $this->page - 1)), + array('rel' => 'prev', + 'type' => 'application/atom+xml')); + } + + if ($this->_faves->N > $this->count) { + + $feed->addLink(common_local_url('AtomPubFavoriteFeed', + array('profile' => + $this->_profile->id), + array('page' => + $this->page + 1)), + array('rel' => 'next', + 'type' => 'application/atom+xml')); + } + + $i = 0; + + while ($this->_faves->fetch()) { + + // We get one more than needed; skip that one + + $i++; + + if ($i > $this->count) { + break; + } + + $act = $this->_faves->asActivity(); + $feed->addEntryRaw($act->asString(false, false, false)); + } + + $this->raw($feed->getString()); + } + + /** + * add a new favorite + * + * @return void + */ + + function addFavorite() + { + // XXX: Refactor this; all the same for atompub + + if (empty($this->auth_user) || + $this->auth_user->id != $this->_profile->id) { + throw new ClientException(_("Can't add someone else's". + " subscription"), 403); + } + + $xml = file_get_contents('php://input'); + + $dom = DOMDocument::loadXML($xml); + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + // TRANS: Client error displayed when not using an Atom entry. + throw new ClientException(_('Atom post must be an Atom entry.')); + return; + } + + $activity = new Activity($dom->documentElement); + + $fave = null; + + if (Event::handle('StartAtomPubNewActivity', array(&$activity))) { + + if ($activity->verb != ActivityVerb::FAVORITE) { + // TRANS: Client error displayed when not using the POST verb. + // TRANS: Do not translate POST. + throw new ClientException(_('Can only handle Favorite activities.')); + return; + } + + $note = $activity->objects[0]; + + if (!in_array($note->type, array(ActivityObject::NOTE, + ActivityObject::BLOGENTRY, + ActivityObject::STATUS))) { + throw new ClientException(_('Can only fave notices.')); + return; + } + + $notice = Notice::staticGet('uri', $note->id); + + if (empty($notice)) { + // XXX: import from listed URL or something + throw new ClientException(_('Unknown note.')); + } + + $old = Fave::pkeyGet(array('user_id' => $this->auth_user->id, + 'notice_id' => $notice->id)); + + if (!empty($old)) { + throw new ClientException(_('Already a favorite.')); + } + + $profile = $this->auth_user->getProfile(); + + $fave = Fave::addNew($profile, $notice); + + if (!empty($fave)) { + $this->_profile->blowFavesCache(); + $this->notify($fave, $notice, $this->auth_user); + } + + Event::handle('EndAtomPubNewActivity', array($activity, $fave)); + } + + if (!empty($fave)) { + $act = $fave->asActivity(); + + header('Content-Type: application/atom+xml; charset=utf-8'); + header('Content-Location: ' . $act->selfLink); + + $this->startXML(); + $this->raw($act->asString(true, true, true)); + $this->endXML(); + } + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + function lastModified() + { + // For comparison with If-Last-Modified + // If not applicable, return null + return null; + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + + function etag() + { + return null; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return false; + } else { + return true; + } + } + + /** + * Notify the author of the favorite that the user likes their notice + * + * @param Favorite $fave the favorite in question + * @param Notice $notice the notice that's been faved + * @param User $user the user doing the favoriting + * + * @return void + */ + + function notify($fave, $notice, $user) + { + $other = User::staticGet('id', $notice->profile_id); + if ($other && $other->id != $user->id) { + if ($other->email && $other->emailnotifyfav) { + mail_notify_fave($other, $user, $notice); + } + // XXX: notify by IM + // XXX: notify by SMS + } + } +} diff --git a/actions/atompubmembershipfeed.php b/actions/atompubmembershipfeed.php new file mode 100644 index 0000000000..3002576c15 --- /dev/null +++ b/actions/atompubmembershipfeed.php @@ -0,0 +1,355 @@ +. + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Feed of group memberships for a user, in ActivityStreams format + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class AtompubmembershipfeedAction extends ApiAuthAction +{ + private $_profile = null; + private $_memberships = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $profileId = $this->trimmed('profile'); + + $this->_profile = Profile::staticGet('id', $profileId); + + if (empty($this->_profile)) { + throw new ClientException(_('No such profile.'), 404); + } + + $offset = ($this->page-1) * $this->count; + $limit = $this->count + 1; + + $this->_memberships = Group_member::byMember($this->_profile->id, + $offset, + $limit); + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + parent::handle($argarray); + + switch ($_SERVER['REQUEST_METHOD']) { + case 'HEAD': + case 'GET': + $this->showFeed(); + break; + case 'POST': + $this->addMembership(); + break; + default: + throw new ClientException(_('HTTP method not supported.'), 405); + return; + } + + return; + } + + /** + * Show a feed of favorite activity streams objects + * + * @return void + */ + + function showFeed() + { + header('Content-Type: application/atom+xml; charset=utf-8'); + + $url = common_local_url('AtomPubMembershipFeed', + array('profile' => $this->_profile->id)); + + $feed = new Atom10Feed(true); + + $feed->addNamespace('activity', + 'http://activitystrea.ms/spec/1.0/'); + + $feed->addNamespace('poco', + 'http://portablecontacts.net/spec/1.0'); + + $feed->addNamespace('media', + 'http://purl.org/syndication/atommedia'); + + $feed->id = $url; + + $feed->setUpdated('now'); + + $feed->addAuthor($this->_profile->getBestName(), + $this->_profile->getURI()); + + $feed->setTitle(sprintf(_("%s group memberships"), + $this->_profile->getBestName())); + + $feed->setSubtitle(sprintf(_("Groups %s is a member of on %s"), + $this->_profile->getBestName(), + common_config('site', 'name'))); + + $feed->addLink(common_local_url('usergroups', + array('nickname' => + $this->_profile->nickname))); + + $feed->addLink($url, + array('rel' => 'self', + 'type' => 'application/atom+xml')); + + // If there's more... + + if ($this->page > 1) { + $feed->addLink($url, + array('rel' => 'first', + 'type' => 'application/atom+xml')); + + $feed->addLink(common_local_url('AtomPubMembershipFeed', + array('profile' => + $this->_profile->id), + array('page' => + $this->page - 1)), + array('rel' => 'prev', + 'type' => 'application/atom+xml')); + } + + if ($this->_memberships->N > $this->count) { + + $feed->addLink(common_local_url('AtomPubMembershipFeed', + array('profile' => + $this->_profile->id), + array('page' => + $this->page + 1)), + array('rel' => 'next', + 'type' => 'application/atom+xml')); + } + + $i = 0; + + while ($this->_memberships->fetch()) { + + // We get one more than needed; skip that one + + $i++; + + if ($i > $this->count) { + break; + } + + $act = $this->_memberships->asActivity(); + $feed->addEntryRaw($act->asString(false, false, false)); + } + + $this->raw($feed->getString()); + } + + /** + * add a new favorite + * + * @return void + */ + + function addMembership() + { + // XXX: Refactor this; all the same for atompub + + if (empty($this->auth_user) || + $this->auth_user->id != $this->_profile->id) { + throw new ClientException(_("Can't add someone else's". + " membership"), 403); + } + + $xml = file_get_contents('php://input'); + + $dom = DOMDocument::loadXML($xml); + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + // TRANS: Client error displayed when not using an Atom entry. + throw new ClientException(_('Atom post must be an Atom entry.')); + return; + } + + $activity = new Activity($dom->documentElement); + + $membership = null; + + if (Event::handle('StartAtomPubNewActivity', array(&$activity))) { + + if ($activity->verb != ActivityVerb::JOIN) { + // TRANS: Client error displayed when not using the POST verb. + // TRANS: Do not translate POST. + throw new ClientException(_('Can only handle Join activities.')); + return; + } + + $groupObj = $activity->objects[0]; + + if ($groupObj->type != ActivityObject::GROUP) { + throw new ClientException(_('Can only fave notices.')); + return; + } + + $group = User_group::staticGet('uri', $groupObj->id); + + if (empty($group)) { + // XXX: import from listed URL or something + throw new ClientException(_('Unknown group.')); + } + + $old = Group_member::pkeyGet(array('profile_id' => $this->auth_user->id, + 'group_id' => $group->id)); + + if (!empty($old)) { + throw new ClientException(_('Already a member.')); + } + + $profile = $this->auth_user->getProfile(); + + if (Group_block::isBlocked($group, $profile)) { + // XXX: import from listed URL or something + throw new ClientException(_('Blocked by admin.')); + } + + if (Event::handle('StartJoinGroup', array($group, $this->auth_user))) { + $membership = Group_member::join($group->id, $this->auth_user->id); + Event::handle('EndJoinGroup', array($group, $this->auth_user)); + } + + Event::handle('EndAtomPubNewActivity', array($activity, $membership)); + } + + if (!empty($membership)) { + $act = $membership->asActivity(); + + header('Content-Type: application/atom+xml; charset=utf-8'); + header('Content-Location: ' . $act->selfLink); + + $this->startXML(); + $this->raw($act->asString(true, true, true)); + $this->endXML(); + } + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + function lastModified() + { + // For comparison with If-Last-Modified + // If not applicable, return null + return null; + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + + function etag() + { + return null; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return false; + } else { + return true; + } + } +} diff --git a/actions/atompubshowfavorite.php b/actions/atompubshowfavorite.php new file mode 100644 index 0000000000..5fe680bb7b --- /dev/null +++ b/actions/atompubshowfavorite.php @@ -0,0 +1,228 @@ +. + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Show a single favorite in Atom Activity Streams format. + * + * Can also be used to delete a favorite. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class AtompubshowfavoriteAction extends ApiAuthAction +{ + private $_profile = null; + private $_notice = null; + private $_fave = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $profileId = $this->trimmed('profile'); + $noticeId = $this->trimmed('notice'); + + $this->_profile = Profile::staticGet('id', $profileId); + + if (empty($this->_profile)) { + throw new ClientException(_('No such profile.'), 404); + } + + $this->_notice = Notice::staticGet('id', $noticeId); + + if (empty($this->_notice)) { + throw new ClientException(_('No such notice.'), 404); + } + + $this->_fave = Fave::pkeyGet(array('user_id' => $profileId, + 'notice_id' => $noticeId)); + + if (empty($this->_fave)) { + throw new ClientException(_('No such favorite.'), 404); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + parent::handle($argarray); + + switch ($_SERVER['REQUEST_METHOD']) { + case GET: + case HEAD: + $this->showFave(); + break; + case DELETE: + $this->deleteFave(); + break; + default: + throw new ClientException(_('HTTP method not supported.'), + 405); + } + return true; + } + + /** + * Show a single favorite, in ActivityStreams format + * + * @return void + */ + + function showFave() + { + $activity = $this->_fave->asActivity(); + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $this->startXML(); + $this->raw($activity->asString(true, true, true)); + $this->endXML(); + + return; + } + + /** + * Delete the favorite + * + * @return void + */ + + function deleteFave() + { + if (empty($this->auth_user) || + $this->auth_user->id != $this->_profile->id) { + throw new ClientException(_("Can't delete someone else's". + " favorite"), 403); + } + + $this->_fave->delete(); + + return; + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + + function lastModified() + { + return max(strtotime($this->_profile->modified), + strtotime($this->_notice->modified), + strtotime($this->_fave->modified)); + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + + function etag() + { + $mtime = strtotime($this->_fave->modified); + + return 'W/"' . implode(':', array('AtomPubShowFavorite', + $this->_profile->id, + $this->_notice->id, + $mtime)) . '"'; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return false; + } else { + return true; + } + } +} diff --git a/actions/atompubshowmembership.php b/actions/atompubshowmembership.php new file mode 100644 index 0000000000..6d848a2290 --- /dev/null +++ b/actions/atompubshowmembership.php @@ -0,0 +1,235 @@ +. + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Show (or delete) a single membership event as an ActivityStreams entry + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class AtompubshowmembershipAction extends ApiAuthAction +{ + private $_profile = null; + private $_group = null; + private $_membership = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $profileId = $this->trimmed('profile'); + + $this->_profile = Profile::staticGet('id', $profileId); + + if (empty($this->_profile)) { + throw new ClientException(_('No such profile.'), 404); + } + + $groupId = $this->trimmed('group'); + + $this->_group = User_group::staticGet('id', $groupId); + + if (empty($this->_group)) { + throw new ClientException(_('No such group'), 404); + } + + $kv = array('group_id' => $groupId, + 'profile_id' => $profileId); + + $this->_membership = Group_member::pkeyGet($kv); + + if (empty($this->_membership)) { + throw new ClientException(_('Not a member'), 404); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + case 'HEAD': + $this->showMembership(); + break; + case 'DELETE': + $this->deleteMembership(); + break; + default: + throw new ClientException(_('Method not supported'), 405); + break; + } + return; + } + + /** + * show a single membership + * + * @return void + */ + + function showMembership() + { + $activity = $this->_membership->asActivity(); + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $this->startXML(); + $this->raw($activity->asString(true, true, true)); + $this->endXML(); + + return; + } + + /** + * Delete the membership (leave the group) + * + * @return void + */ + + function deleteMembership() + { + if (empty($this->auth_user) || + $this->auth_user->id != $this->_profile->id) { + throw new ClientException(_("Can't delete someone else's". + " membership"), 403); + } + + if (Event::handle('StartLeaveGroup', array($this->_group, $this->auth_user))) { + Group_member::leave($this->_group->id, $this->auth_user->id); + Event::handle('EndLeaveGroup', array($this->_group, $this->auth_user)); + } + + return; + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } + + /** + * Return last modified, if applicable. + * + * Because the representation depends on the profile and group, + * our last modified value is the maximum of their mod time + * with the actual membership's mod time. + * + * @return string last modified http header + */ + function lastModified() + { + return max(strtotime($this->_profile->modified), + strtotime($this->_group->modified), + strtotime($this->_membership->modified)); + } + + /** + * Return etag, if applicable. + * + * A "weak" Etag including the profile and group id as well as + * the admin flag and ctime of the membership. + * + * @return string etag http header + */ + + function etag() + { + $ctime = strtotime($this->_membership->created); + + $adminflag = ($this->_membership->is_admin) ? 't' : 'f'; + + return 'W/"' . implode(':', array('AtomPubShowMembership', + $this->_profile->id, + $this->_group->id, + $adminflag, + $ctime)) . '"'; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return false; + } else { + return true; + } + } +} diff --git a/actions/atompubshowsubscription.php b/actions/atompubshowsubscription.php new file mode 100644 index 0000000000..55ba68adc5 --- /dev/null +++ b/actions/atompubshowsubscription.php @@ -0,0 +1,224 @@ +. + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Show a single subscription + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class AtompubshowsubscriptionAction extends ApiAuthAction +{ + private $_subscriber = null; + private $_subscribed = null; + private $_subscription = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + $subscriberId = $this->trimmed('subscriber'); + + $this->_subscriber = Profile::staticGet('id', $subscriberId); + + if (empty($this->_subscriber)) { + throw new ClientException(sprintf(_('No such profile id: %d'), + $subscriberId), 404); + } + + $subscribedId = $this->trimmed('subscribed'); + + $this->_subscribed = Profile::staticGet('id', $subscribedId); + + if (empty($this->_subscribed)) { + throw new ClientException(sprintf(_('No such profile id: %d'), + $subscribedId), 404); + } + + $this->_subscription = + Subscription::pkeyGet(array('subscriber' => $subscriberId, + 'subscribed' => $subscribedId)); + + if (empty($this->_subscription)) { + $msg = sprintf(_('Profile %d not subscribed to profile %d'), + $subscriberId, $subscribedId); + throw new ClientException($msg, 404); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + parent::handle($argarray); + switch ($_SERVER['REQUEST_METHOD']) { + case 'HEAD': + case 'GET': + $this->showSubscription(); + break; + case 'DELETE': + $this->deleteSubscription(); + break; + default: + $this->clientError(_('HTTP method not supported.'), 405); + return; + } + + return; + } + + /** + * Show the subscription in ActivityStreams Atom format. + * + * @return void + */ + + function showSubscription() + { + $activity = $this->_subscription->asActivity(); + + header('Content-Type: application/atom+xml; charset=utf-8'); + + $this->startXML(); + $this->raw($activity->asString(true, true, true)); + $this->endXML(); + + return; + } + + /** + * Delete the subscription + * + * @return void + */ + + function deleteSubscription() + { + if (empty($this->auth_user) || + $this->auth_user->id != $this->_subscriber->id) { + throw new ClientException(_("Can't delete someone else's". + " subscription"), 403); + } + + Subscription::cancel($this->_subscriber, + $this->_subscribed); + + return; + } + + /** + * Is this action read only? + * + * @param array $args other arguments + * + * @return boolean true + */ + + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'DELETE') { + return false; + } else { + return true; + } + } + + /** + * Return last modified, if applicable. + * + * @return string last modified http header + */ + + function lastModified() + { + return max(strtotime($this->_subscriber->modified), + strtotime($this->_subscribed->modified), + strtotime($this->_subscription->modified)); + } + + /** + * Etag for this object + * + * @return string etag http header + */ + + function etag() + { + $mtime = strtotime($this->_subscription->modified); + + return 'W/"' . implode(':', array('AtomPubShowSubscription', + $this->_subscriber->id, + $this->_subscribed->id, + $mtime)) . '"'; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'DELETE') { + return true; + } else { + return false; + } + } +} diff --git a/actions/atompubsubscriptionfeed.php b/actions/atompubsubscriptionfeed.php new file mode 100644 index 0000000000..15ae79f6a6 --- /dev/null +++ b/actions/atompubsubscriptionfeed.php @@ -0,0 +1,335 @@ +. + * + * @category Cache + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * Subscription feed class for AtomPub + * + * Generates a list of the user's subscriptions + * + * @category AtomPub + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class AtompubsubscriptionfeedAction extends ApiAuthAction +{ + private $_profile = null; + private $_subscriptions = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $subscriber = $this->trimmed('subscriber'); + + $this->_profile = Profile::staticGet('id', $subscriber); + + if (empty($this->_profile)) { + throw new ClientException(sprintf(_('No such profile id: %d'), + $subscriber), 404); + } + + // page and count from ApiAction + + $offset = ($this->page-1) * $this->count; + + $this->_subscriptions = Subscription::bySubscriber($subscriber, + $offset, + $this->count + 1); + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + parent::handle($argarray); + switch ($_SERVER['REQUEST_METHOD']) { + case 'HEAD': + case 'GET': + $this->showFeed(); + break; + case 'POST': + $this->addSubscription(); + break; + default: + $this->clientError(_('HTTP method not supported.'), 405); + return; + } + + return; + } + + /** + * Show the feed of subscriptions + * + * @return void + */ + + function showFeed() + { + header('Content-Type: application/atom+xml; charset=utf-8'); + + $url = common_local_url('AtomPubSubscriptionFeed', + array('subscriber' => $this->_profile->id)); + + $feed = new Atom10Feed(true); + + $feed->addNamespace('activity', + 'http://activitystrea.ms/spec/1.0/'); + + $feed->addNamespace('poco', + 'http://portablecontacts.net/spec/1.0'); + + $feed->addNamespace('media', + 'http://purl.org/syndication/atommedia'); + + $feed->id = $url; + + $feed->setUpdated('now'); + + $feed->addAuthor($this->_profile->getBestName(), + $this->_profile->getURI()); + + $feed->setTitle(sprintf(_("%s subscriptions"), + $this->_profile->getBestName())); + + $feed->setSubtitle(sprintf(_("People %s has subscribed to on %s"), + $this->_profile->getBestName(), + common_config('site', 'name'))); + + $feed->addLink(common_local_url('subscriptions', + array('nickname' => + $this->_profile->nickname))); + + $feed->addLink($url, + array('rel' => 'self', + 'type' => 'application/atom+xml')); + + // If there's more... + + if ($this->page > 1) { + $feed->addLink($url, + array('rel' => 'first', + 'type' => 'application/atom+xml')); + + $feed->addLink(common_local_url('AtomPubSubscriptionFeed', + array('subscriber' => + $this->_profile->id), + array('page' => + $this->page - 1)), + array('rel' => 'prev', + 'type' => 'application/atom+xml')); + } + + if ($this->_subscriptions->N > $this->count) { + + $feed->addLink(common_local_url('AtomPubSubscriptionFeed', + array('subscriber' => + $this->_profile->id), + array('page' => + $this->page + 1)), + array('rel' => 'next', + 'type' => 'application/atom+xml')); + } + + $i = 0; + + // XXX: This is kind of inefficient + + while ($this->_subscriptions->fetch()) { + + // We get one more than needed; skip that one + + $i++; + + if ($i > $this->count) { + break; + } + + $act = $this->_subscriptions->asActivity(); + $feed->addEntryRaw($act->asString(false, false, false)); + } + + $this->raw($feed->getString()); + } + + /** + * Add a new subscription + * + * Handling the POST method for AtomPub + * + * @return void + */ + + function addSubscription() + { + if (empty($this->auth_user) || + $this->auth_user->id != $this->_profile->id) { + throw new ClientException(_("Can't add someone else's". + " subscription"), 403); + } + + $xml = file_get_contents('php://input'); + + $dom = DOMDocument::loadXML($xml); + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + // TRANS: Client error displayed when not using an Atom entry. + $this->clientError(_('Atom post must be an Atom entry.')); + return; + } + + $activity = new Activity($dom->documentElement); + + $sub = null; + + if (Event::handle('StartAtomPubNewActivity', array(&$activity))) { + + if ($activity->verb != ActivityVerb::FOLLOW) { + // TRANS: Client error displayed when not using the POST verb. + // TRANS: Do not translate POST. + $this->clientError(_('Can only handle Follow activities.')); + return; + } + + $person = $activity->objects[0]; + + if ($person->type != ActivityObject::PERSON) { + $this->clientError(_('Can only follow people.')); + return; + } + + // XXX: OStatus discovery (maybe) + + $profile = Profile::fromURI($person->id); + + if (empty($profile)) { + $this->clientError(sprintf(_('Unknown profile %s'), $person->id)); + return; + } + + if (Subscription::start($this->_profile, $profile)) { + $sub = Subscription::pkeyGet(array('subscriber' => $this->_profile->id, + 'subscribed' => $profile->id)); + } + + Event::handle('EndAtomPubNewActivity', array($activity, $sub)); + } + + if (!empty($sub)) { + $act = $sub->asActivity(); + + header('Content-Type: application/atom+xml; charset=utf-8'); + header('Content-Location: ' . $act->selfLink); + + $this->startXML(); + $this->raw($act->asString(true, true, true)); + $this->endXML(); + } + } + + /** + * Return true if read only. + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + return $_SERVER['REQUEST_METHOD'] != 'POST'; + } + + /** + * Return last modified, if applicable. + * + * @return string last modified http header + */ + + function lastModified() + { + return null; + } + + /** + * Return etag, if applicable. + * + * @return string etag http header + */ + + function etag() + { + return null; + } + + /** + * Does this require authentication? + * + * @return boolean true if delete, else false + */ + + function requiresAuth() + { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + return true; + } else { + return false; + } + } +} diff --git a/actions/subscriptions.php b/actions/subscriptions.php index ba2f67f2da..a814a4f354 100644 --- a/actions/subscriptions.php +++ b/actions/subscriptions.php @@ -163,6 +163,22 @@ class SubscriptionsAction extends GalleryAction $cloud2 = new SubscriptionsPeopleSelfTagCloudSection($this); $cloud2->show(); } + + /** + * Link to feeds of subscriptions + * + * @return array of Feed objects + */ + + function getFeeds() + { + return array(new Feed(Feed::ATOM, + common_local_url('AtomPubSubscriptionFeed', + array('subscriber' => $this->profile->id)), + sprintf(_('Subscription feed for %s (Atom)'), + $this->profile->nickname))); + + } } // XXX SubscriptionsList and SubscriptionList are dangerously close @@ -247,4 +263,5 @@ class SubscriptionsListItem extends SubscriptionListItem $this->out->elementEnd('form'); return; } + } diff --git a/classes/Fave.php b/classes/Fave.php index 9922ae45c5..3aa23e7b4e 100644 --- a/classes/Fave.php +++ b/classes/Fave.php @@ -138,6 +138,9 @@ class Fave extends Memcached_DataObject $act = new Activity(); $act->verb = ActivityVerb::FAVORITE; + + // FIXME: rationalize this with URL below + $act->id = TagURI::mint('favor:%d:%d:%s', $profile->id, $notice->id, @@ -155,6 +158,41 @@ class Fave extends Memcached_DataObject $act->actor = ActivityObject::fromProfile($profile); $act->objects[] = ActivityObject::fromNotice($notice); + $url = common_local_url('AtomPubShowFavorite', + array('profile' => $this->user_id, + 'notice' => $this->notice_id)); + + $act->selfLink = $url; + $act->editLink = $url; + return $act; } + + /** + * Fetch a stream of favorites by profile + * + * @param integer $profileId Profile that faved + * @param integer $offset Offset from last + * @param integer $limit Number to get + * + * @return mixed stream of faves, use fetch() to iterate + * + * @todo Cache results + * @todo integrate with Fave::stream() + */ + + static function byProfile($profileId, $offset, $limit) + { + $fav = new Fave(); + + $fav->user_id = $profileId; + + $fav->orderBy('modified DESC'); + + $fav->limit($offset, $limit); + + $fav->find(); + + return $fav; + } } diff --git a/classes/Group_member.php b/classes/Group_member.php index c40d06a1db..2cf31cf123 100644 --- a/classes/Group_member.php +++ b/classes/Group_member.php @@ -26,6 +26,15 @@ class Group_member extends Memcached_DataObject return Memcached_DataObject::pkeyGet('Group_member', $kv); } + /** + * Method to add a user to a group. + * + * @param integer $group_id Group to add to + * @param integer $profile_id Profile being added + * + * @return Group_member new membership object + */ + static function join($group_id, $profile_id) { $member = new Group_member(); @@ -42,7 +51,7 @@ class Group_member extends Memcached_DataObject throw new Exception(_("Group join failed.")); } - return true; + return $member; } static function leave($group_id, $profile_id) @@ -92,6 +101,31 @@ class Group_member extends Memcached_DataObject return $group; } + /** + * Get stream of memberships by member + * + * @param integer $memberId profile ID of the member to fetch for + * @param integer $offset offset from start of stream to get + * @param integer $limit number of memberships to get + * + * @return Group_member stream of memberships, use fetch() to iterate + */ + + static function byMember($memberId, $offset=0, $limit=GROUPS_PER_PAGE) + { + $membership = new Group_member(); + + $membership->profile_id = $memberId; + + $membership->orderBy('created DESC'); + + $membership->limit($offset, $limit); + + $membership->find(); + + return $membership; + } + function asActivity() { $member = $this->getMember(); @@ -118,6 +152,13 @@ class Group_member extends Memcached_DataObject $member->getBestName(), $group->getBestName()); + $url = common_local_url('AtomPubShowMembership', + array('profile' => $member->id, + 'group' => $group->id)); + + $act->selfLink = $url; + $act->editLink = $url; + return $act; } } diff --git a/classes/Subscription.php b/classes/Subscription.php index 763e3835b8..1d4f37929b 100644 --- a/classes/Subscription.php +++ b/classes/Subscription.php @@ -253,6 +253,8 @@ class Subscription extends Memcached_DataObject $act->verb = ActivityVerb::FOLLOW; + // XXX: rationalize this with the URL + $act->id = TagURI::mint('follow:%d:%d:%s', $subscriber->id, $subscribed->id, @@ -270,6 +272,13 @@ class Subscription extends Memcached_DataObject $act->actor = ActivityObject::fromProfile($subscriber); $act->objects[] = ActivityObject::fromProfile($subscribed); + $url = common_local_url('AtomPubShowSubscription', + array('subscriber' => $subscriber->id, + 'subscribed' => $subscribed->id)); + + $act->selfLink = $url; + $act->editLink = $url; + return $act; } diff --git a/lib/router.php b/lib/router.php index d747493ded..c42cca5f60 100644 --- a/lib/router.php +++ b/lib/router.php @@ -761,13 +761,6 @@ class Router $m->connect('api/oauth/authorize', array('action' => 'ApiOauthAuthorize')); - $m->connect('api/statusnet/app/service/:id.xml', - array('action' => 'ApiAtomService', - 'id' => Nickname::INPUT_FMT)); - - $m->connect('api/statusnet/app/service.xml', - array('action' => 'ApiAtomService')); - // Admin $m->connect('admin/site', array('action' => 'siteadminpanel')); @@ -909,6 +902,42 @@ class Router array('nickname' => Nickname::DISPLAY_FMT)); } + // AtomPub API + + $m->connect('api/statusnet/app/service/:id.xml', + array('action' => 'ApiAtomService', + 'id' => Nickname::DISPLAY_FMT)); + + $m->connect('api/statusnet/app/service.xml', + array('action' => 'ApiAtomService')); + + $m->connect('api/statusnet/app/subscriptions/:subscriber/:subscribed.atom', + array('action' => 'AtomPubShowSubscription'), + array('subscriber' => '[0-9]+', + 'subscribed' => '[0-9]+')); + + $m->connect('api/statusnet/app/subscriptions/:subscriber.atom', + array('action' => 'AtomPubSubscriptionFeed'), + array('subscriber' => '[0-9]+')); + + $m->connect('api/statusnet/app/favorites/:profile/:notice.atom', + array('action' => 'AtomPubShowFavorite'), + array('profile' => '[0-9]+', + 'notice' => '[0-9]+')); + + $m->connect('api/statusnet/app/favorites/:profile.atom', + array('action' => 'AtomPubFavoriteFeed'), + array('profile' => '[0-9]+')); + + $m->connect('api/statusnet/app/memberships/:profile/:group.atom', + array('action' => 'AtomPubShowMembership'), + array('profile' => '[0-9]+', + 'group' => '[0-9]+')); + + $m->connect('api/statusnet/app/memberships/:profile.atom', + array('action' => 'AtomPubMembershipFeed'), + array('profile' => '[0-9]+')); + // user stuff Event::handle('RouterInitialized', array($m));