Merge branch '0.9.x' into 1.0.x
@ -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');
|
||||
|
@ -165,7 +165,7 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this action read only?
|
||||
* We expose AtomPub here, so non-GET/HEAD reqs must be read/write.
|
||||
*
|
||||
* @param array $args other arguments
|
||||
*
|
||||
@ -174,11 +174,7 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction
|
||||
|
||||
function isReadOnly($args)
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -235,7 +235,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this action read only?
|
||||
* We expose AtomPub here, so non-GET/HEAD reqs must be read/write.
|
||||
*
|
||||
* @param array $args other arguments
|
||||
*
|
||||
@ -244,11 +244,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction
|
||||
|
||||
function isReadOnly($args)
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -309,9 +305,15 @@ class ApiTimelineUserAction extends ApiBareAuthAction
|
||||
return;
|
||||
}
|
||||
|
||||
$xml = file_get_contents('php://input');
|
||||
$xml = trim(file_get_contents('php://input'));
|
||||
if (empty($xml)) {
|
||||
$this->clientError(_('Atom post must not be empty.'));
|
||||
}
|
||||
|
||||
$dom = DOMDocument::loadXML($xml);
|
||||
if (!$dom) {
|
||||
$this->clientError(_('Atom post must be well-formed XML.'));
|
||||
}
|
||||
|
||||
if ($dom->documentElement->namespaceURI != Activity::ATOM ||
|
||||
$dom->documentElement->localName != 'entry') {
|
||||
@ -349,7 +351,8 @@ class ApiTimelineUserAction extends ApiBareAuthAction
|
||||
}
|
||||
|
||||
if (!empty($saved)) {
|
||||
header("Location: " . common_local_url('ApiStatusesShow', array('notice_id' => $saved->id,
|
||||
header('HTTP/1.1 201 Created');
|
||||
header("Location: " . common_local_url('ApiStatusesShow', array('id' => $saved->id,
|
||||
'format' => 'atom')));
|
||||
$this->showSingleAtomStatus($saved);
|
||||
}
|
||||
|
374
actions/atompubfavoritefeed.php
Normal file
@ -0,0 +1,374 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* Feed of ActivityStreams 'favorite' actions
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category AtomPub
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @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 <evan@status.net>
|
||||
* @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
|
||||
}
|
||||
}
|
||||
}
|
355
actions/atompubmembershipfeed.php
Normal file
@ -0,0 +1,355 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* Feed of group memberships for a user, in ActivityStreams format
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category AtomPub
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @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 <evan@status.net>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
228
actions/atompubshowfavorite.php
Normal file
@ -0,0 +1,228 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* Show a single favorite in Atom Activity Streams format
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category AtomPub
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @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 <evan@status.net>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
235
actions/atompubshowmembership.php
Normal file
@ -0,0 +1,235 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* Show a single membership as an Activity Streams entry
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category AtomPub
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @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 <evan@status.net>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
224
actions/atompubshowsubscription.php
Normal file
@ -0,0 +1,224 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* Single subscription
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category AtomPub
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @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 <evan@status.net>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
335
actions/atompubsubscriptionfeed.php
Normal file
@ -0,0 +1,335 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* AtomPub subscription feed
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category Cache
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @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 <evan@status.net>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
@ -97,7 +97,7 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
|
||||
$offset = ($this->page - 1) * APPS_PER_PAGE;
|
||||
$limit = APPS_PER_PAGE + 1;
|
||||
|
||||
$connection = $profile->getConnectedApps($offset, $limit);
|
||||
$connection = $user->getConnectedApps($offset, $limit);
|
||||
|
||||
$cnt = 0;
|
||||
|
||||
|
@ -215,4 +215,15 @@ class OembedAction extends Action
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this action read-only?
|
||||
*
|
||||
* @param array $args other arguments
|
||||
*
|
||||
* @return boolean is read only action?
|
||||
*/
|
||||
function isReadOnly($args)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -249,4 +265,5 @@ class SubscriptionsListItem extends SubscriptionListItem
|
||||
$this->out->elementEnd('form');
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -1234,7 +1234,7 @@ class Notice extends Memcached_DataObject
|
||||
* @return Activity activity object representing this Notice.
|
||||
*/
|
||||
|
||||
function asActivity($cur = null, $source = false)
|
||||
function asActivity()
|
||||
{
|
||||
$act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id));
|
||||
|
||||
@ -1332,68 +1332,37 @@ class Notice extends Memcached_DataObject
|
||||
|
||||
$act->context = $ctx;
|
||||
|
||||
$noticeInfoAttr = array('local_id' => $this->id); // local notice ID (useful to clients for ordering)
|
||||
// Source
|
||||
|
||||
$ns = $this->getSource();
|
||||
$atom_feed = $profile->getAtomFeed();
|
||||
|
||||
if (!empty($ns)) {
|
||||
$noticeInfoAttr['source'] = $ns->code;
|
||||
if (!empty($ns->url)) {
|
||||
$noticeInfoAttr['source_link'] = $ns->url;
|
||||
if (!empty($ns->name)) {
|
||||
$noticeInfoAttr['source'] = '<a href="'
|
||||
. htmlspecialchars($ns->url)
|
||||
. '" rel="nofollow">'
|
||||
. htmlspecialchars($ns->name)
|
||||
. '</a>';
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($cur)) {
|
||||
$noticeInfoAttr['favorite'] = ($cur->hasFave($this)) ? "true" : "false";
|
||||
$cp = $cur->getProfile();
|
||||
$noticeInfoAttr['repeated'] = ($cp->hasRepeated($this->id)) ? "true" : "false";
|
||||
}
|
||||
$user = User::staticGet('id', $profile->id);
|
||||
|
||||
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 (!empty($user)) {
|
||||
$act->source->links['license'] = common_config('license', 'url');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1414,12 +1383,65 @@ class Notice extends Memcached_DataObject
|
||||
// This has gotten way too long. Needs to be sliced up into functional bits
|
||||
// or ideally exported to a utility class.
|
||||
|
||||
function asAtomEntry($namespace=false, $source=false, $author=true, $cur=null)
|
||||
function asAtomEntry($namespace=false,
|
||||
$source=false,
|
||||
$author=true,
|
||||
$cur=null)
|
||||
{
|
||||
$act = $this->asActivity($cur, $source);
|
||||
return $act->asString($namespace, $author);
|
||||
$act = $this->asActivity();
|
||||
$act->extra[] = $this->noticeInfo($cur);
|
||||
return $act->asString($namespace, $author, $source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra notice info for atom entries
|
||||
*
|
||||
* Clients use some extra notice info in the atom stream.
|
||||
* This gives it to them.
|
||||
*
|
||||
* @param User $cur Current user
|
||||
*
|
||||
* @return array representation of <statusnet:notice_info> element
|
||||
*/
|
||||
|
||||
function noticeInfo($cur)
|
||||
{
|
||||
// local notice ID (useful to clients for ordering)
|
||||
|
||||
$noticeInfoAttr = array('local_id' => $this->id);
|
||||
|
||||
// notice source
|
||||
|
||||
$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'] = '<a href="'
|
||||
. htmlspecialchars($ns->url)
|
||||
. '" rel="nofollow">'
|
||||
. htmlspecialchars($ns->name)
|
||||
. '</a>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// favorite and repeated
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return array('statusnet:notice_info', $noticeInfoAttr, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an XML string fragment with a reference to a notice as an
|
||||
|
@ -380,79 +380,32 @@ class Profile extends Memcached_DataObject
|
||||
|
||||
function getSubscriptions($offset=0, $limit=null)
|
||||
{
|
||||
$qry =
|
||||
'SELECT profile.* ' .
|
||||
'FROM profile JOIN subscription ' .
|
||||
'ON profile.id = subscription.subscribed ' .
|
||||
'WHERE subscription.subscriber = %d ' .
|
||||
'AND subscription.subscribed != subscription.subscriber ' .
|
||||
'ORDER BY subscription.created DESC ';
|
||||
$subs = Subscription::bySubscriber($this->id,
|
||||
$offset,
|
||||
$limit);
|
||||
|
||||
if ($offset>0 && !is_null($limit)){
|
||||
if (common_config('db','type') == 'pgsql') {
|
||||
$qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
|
||||
} else {
|
||||
$qry .= ' LIMIT ' . $offset . ', ' . $limit;
|
||||
}
|
||||
$profiles = array();
|
||||
|
||||
while ($subs->fetch()) {
|
||||
$profiles[] = Profile::staticGet($subs->subscribed);
|
||||
}
|
||||
|
||||
$profile = new Profile();
|
||||
|
||||
$profile->query(sprintf($qry, $this->id));
|
||||
|
||||
return $profile;
|
||||
return new ArrayWrapper($profiles);
|
||||
}
|
||||
|
||||
function getSubscribers($offset=0, $limit=null)
|
||||
{
|
||||
$qry =
|
||||
'SELECT profile.* ' .
|
||||
'FROM profile JOIN subscription ' .
|
||||
'ON profile.id = subscription.subscriber ' .
|
||||
'WHERE subscription.subscribed = %d ' .
|
||||
'AND subscription.subscribed != subscription.subscriber ' .
|
||||
'ORDER BY subscription.created DESC ';
|
||||
$subs = Subscription::bySubscribed($this->id,
|
||||
$offset,
|
||||
$limit);
|
||||
|
||||
if ($offset>0 && !is_null($limit)){
|
||||
if ($offset) {
|
||||
if (common_config('db','type') == 'pgsql') {
|
||||
$qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
|
||||
} else {
|
||||
$qry .= ' LIMIT ' . $offset . ', ' . $limit;
|
||||
}
|
||||
}
|
||||
$profiles = array();
|
||||
|
||||
while ($subs->fetch()) {
|
||||
$profiles[] = Profile::staticGet($subs->subscriber);
|
||||
}
|
||||
|
||||
$profile = new Profile();
|
||||
|
||||
$cnt = $profile->query(sprintf($qry, $this->id));
|
||||
|
||||
return $profile;
|
||||
}
|
||||
|
||||
function getConnectedApps($offset = 0, $limit = null)
|
||||
{
|
||||
$qry =
|
||||
'SELECT u.* ' .
|
||||
'FROM oauth_application_user u, oauth_application a ' .
|
||||
'WHERE u.profile_id = %d ' .
|
||||
'AND a.id = u.application_id ' .
|
||||
'AND u.access_type > 0 ' .
|
||||
'ORDER BY u.created DESC ';
|
||||
|
||||
if ($offset > 0) {
|
||||
if (common_config('db','type') == 'pgsql') {
|
||||
$qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
|
||||
} else {
|
||||
$qry .= ' LIMIT ' . $offset . ', ' . $limit;
|
||||
}
|
||||
}
|
||||
|
||||
$apps = new Oauth_application_user();
|
||||
|
||||
$cnt = $apps->query(sprintf($qry, $this->id));
|
||||
|
||||
return $apps;
|
||||
return new ArrayWrapper($profiles);
|
||||
}
|
||||
|
||||
function subscriptionCount()
|
||||
|
@ -178,6 +178,18 @@ class Session extends Memcached_DataObject
|
||||
$result = session_set_save_handler('Session::open', 'Session::close', 'Session::read',
|
||||
'Session::write', 'Session::destroy', 'Session::gc');
|
||||
self::logdeb("save handlers result = $result");
|
||||
|
||||
// PHP 5.3 with APC ends up destroying a bunch of object stuff before the session
|
||||
// save handlers get called on request teardown.
|
||||
// Registering an explicit shutdown function should take care of this before
|
||||
// everything breaks on us.
|
||||
register_shutdown_function('Session::cleanup');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
static function cleanup()
|
||||
{
|
||||
session_write_close();
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
|
||||
|
||||
class Subscription extends Memcached_DataObject
|
||||
{
|
||||
const CACHE_WINDOW = 201;
|
||||
|
||||
###START_AUTOCODE
|
||||
/* the code below is auto generated do not remove the above tag */
|
||||
|
||||
@ -91,6 +93,9 @@ class Subscription extends Memcached_DataObject
|
||||
|
||||
self::blow('user:notices_with_friends:%d', $subscriber->id);
|
||||
|
||||
self::blow('subscription:by-subscriber:'.$subscriber->id);
|
||||
self::blow('subscription:by-subscribed:'.$other->id);
|
||||
|
||||
$subscriber->blowSubscriptionCount();
|
||||
$other->blowSubscriberCount();
|
||||
|
||||
@ -220,6 +225,9 @@ class Subscription extends Memcached_DataObject
|
||||
|
||||
self::blow('user:notices_with_friends:%d', $subscriber->id);
|
||||
|
||||
self::blow('subscription:by-subscriber:'.$subscriber->id);
|
||||
self::blow('subscription:by-subscribed:'.$other->id);
|
||||
|
||||
$subscriber->blowSubscriptionCount();
|
||||
$other->blowSubscriberCount();
|
||||
|
||||
@ -245,6 +253,8 @@ class Subscription extends Memcached_DataObject
|
||||
|
||||
$act->verb = ActivityVerb::FOLLOW;
|
||||
|
||||
// XXX: rationalize this with the URL
|
||||
|
||||
$act->id = TagURI::mint('follow:%d:%d:%s',
|
||||
$subscriber->id,
|
||||
$subscribed->id,
|
||||
@ -262,6 +272,156 @@ class Subscription extends Memcached_DataObject
|
||||
$act->actor = ActivityObject::fromProfile($subscriber);
|
||||
$act->objects[] = ActivityObject::fromProfile($subscribed);
|
||||
|
||||
$url = common_local_url('AtomPubShowSubscription',
|
||||
array('subscriber' => $subscriber->id,
|
||||
'subscribed' => $subscribed->id));
|
||||
|
||||
$act->selfLink = $url;
|
||||
$act->editLink = $url;
|
||||
|
||||
return $act;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream of subscriptions with the same subscriber
|
||||
*
|
||||
* Useful for showing pages that list subscriptions in reverse
|
||||
* chronological order. Has offset & limit to make paging
|
||||
* easy.
|
||||
*
|
||||
* @param integer $subscriberId Profile ID of the subscriber
|
||||
* @param integer $offset Offset from latest
|
||||
* @param integer $limit Maximum number to fetch
|
||||
*
|
||||
* @return Subscription stream of subscriptions; use fetch() to iterate
|
||||
*/
|
||||
|
||||
static function bySubscriber($subscriberId,
|
||||
$offset = 0,
|
||||
$limit = PROFILES_PER_PAGE)
|
||||
{
|
||||
if ($offset + $limit > self::CACHE_WINDOW) {
|
||||
return new ArrayWrapper(self::realBySubscriber($subscriberId,
|
||||
$offset,
|
||||
$limit));
|
||||
} else {
|
||||
$key = 'subscription:by-subscriber:'.$subscriberId;
|
||||
$window = self::cacheGet($key);
|
||||
if ($window === false) {
|
||||
$window = self::realBySubscriber($subscriberId,
|
||||
0,
|
||||
self::CACHE_WINDOW);
|
||||
self::cacheSet($key, $window);
|
||||
}
|
||||
return new ArrayWrapper(array_slice($window,
|
||||
$offset,
|
||||
$limit));
|
||||
}
|
||||
}
|
||||
|
||||
private static function realBySubscriber($subscriberId,
|
||||
$offset,
|
||||
$limit)
|
||||
{
|
||||
$sub = new Subscription();
|
||||
|
||||
$sub->subscriber = $subscriberId;
|
||||
|
||||
$sub->whereAdd('subscribed != ' . $subscriberId);
|
||||
|
||||
$sub->orderBy('created DESC');
|
||||
$sub->limit($offset, $limit);
|
||||
|
||||
$sub->find();
|
||||
|
||||
$subs = array();
|
||||
|
||||
while ($sub->fetch()) {
|
||||
$subs[] = clone($sub);
|
||||
}
|
||||
|
||||
return $subs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream of subscriptions with the same subscribed profile
|
||||
*
|
||||
* Useful for showing pages that list subscribers in reverse
|
||||
* chronological order. Has offset & limit to make paging
|
||||
* easy.
|
||||
*
|
||||
* @param integer $subscribedId Profile ID of the subscribed
|
||||
* @param integer $offset Offset from latest
|
||||
* @param integer $limit Maximum number to fetch
|
||||
*
|
||||
* @return Subscription stream of subscriptions; use fetch() to iterate
|
||||
*/
|
||||
|
||||
static function bySubscribed($subscribedId,
|
||||
$offset = 0,
|
||||
$limit = PROFILES_PER_PAGE)
|
||||
{
|
||||
if ($offset + $limit > self::CACHE_WINDOW) {
|
||||
return new ArrayWrapper(self::realBySubscribed($subscribedId,
|
||||
$offset,
|
||||
$limit));
|
||||
} else {
|
||||
$key = 'subscription:by-subscribed:'.$subscribedId;
|
||||
$window = self::cacheGet($key);
|
||||
if ($window === false) {
|
||||
$window = self::realBySubscribed($subscribedId,
|
||||
0,
|
||||
self::CACHE_WINDOW);
|
||||
self::cacheSet($key, $window);
|
||||
}
|
||||
return new ArrayWrapper(array_slice($window,
|
||||
$offset,
|
||||
$limit));
|
||||
}
|
||||
}
|
||||
|
||||
private static function realBySubscribed($subscribedId,
|
||||
$offset,
|
||||
$limit)
|
||||
{
|
||||
$sub = new Subscription();
|
||||
|
||||
$sub->subscribed = $subscribedId;
|
||||
|
||||
$sub->whereAdd('subscriber != ' . $subscribedId);
|
||||
|
||||
$sub->orderBy('created DESC');
|
||||
$sub->limit($offset, $limit);
|
||||
|
||||
$sub->find();
|
||||
|
||||
$subs = array();
|
||||
|
||||
while ($sub->fetch()) {
|
||||
$subs[] = clone($sub);
|
||||
}
|
||||
|
||||
return $subs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush cached subscriptions when subscription is updated
|
||||
*
|
||||
* Because we cache subscriptions, it's useful to flush them
|
||||
* here.
|
||||
*
|
||||
* @param mixed $orig Original version of object
|
||||
*
|
||||
* @return boolean success flag.
|
||||
*/
|
||||
|
||||
function update($orig=null)
|
||||
{
|
||||
$result = parent::update($orig);
|
||||
|
||||
self::blow('subscription:by-subscriber:'.$this->subscriber);
|
||||
self::blow('subscription:by-subscribed:'.$this->subscribed);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
@ -973,4 +973,34 @@ class User extends Memcached_DataObject
|
||||
{
|
||||
return common_shorten_links($text, $always, $this);
|
||||
}
|
||||
|
||||
/*
|
||||
* Get a list of OAuth client application that have access to this
|
||||
* user's account.
|
||||
*/
|
||||
function getConnectedApps($offset = 0, $limit = null)
|
||||
{
|
||||
$qry =
|
||||
'SELECT u.* ' .
|
||||
'FROM oauth_application_user u, oauth_application a ' .
|
||||
'WHERE u.profile_id = %d ' .
|
||||
'AND a.id = u.application_id ' .
|
||||
'AND u.access_type > 0 ' .
|
||||
'ORDER BY u.created DESC ';
|
||||
|
||||
if ($offset > 0) {
|
||||
if (common_config('db','type') == 'pgsql') {
|
||||
$qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
|
||||
} else {
|
||||
$qry .= ' LIMIT ' . $offset . ', ' . $limit;
|
||||
}
|
||||
}
|
||||
|
||||
$apps = new Oauth_application_user();
|
||||
|
||||
$cnt = $apps->query(sprintf($qry, $this->id));
|
||||
|
||||
return $apps;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -327,16 +327,8 @@ class Activity
|
||||
return null;
|
||||
}
|
||||
|
||||
function asString($namespace=false, $author=true)
|
||||
function asString($namespace=false, $author=true, $source=false)
|
||||
{
|
||||
$c = Cache::instance();
|
||||
|
||||
$str = $c->get(Cache::codeKey('activity:as-string:'.$this->id));
|
||||
|
||||
if (!empty($str)) {
|
||||
return $str;
|
||||
}
|
||||
|
||||
$xs = new XMLStringer(true);
|
||||
|
||||
if ($namespace) {
|
||||
@ -502,7 +494,7 @@ class Activity
|
||||
|
||||
// Info on the source feed
|
||||
|
||||
if (!empty($this->source)) {
|
||||
if ($source && !empty($this->source)) {
|
||||
$xs->elementStart('source');
|
||||
|
||||
$xs->element('id', null, $this->source->id);
|
||||
@ -559,8 +551,6 @@ class Activity
|
||||
|
||||
$str = $xs->getString();
|
||||
|
||||
$c->set(Cache::codeKey('activity:as-string:'.$this->id), $str);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,25 @@
|
||||
class Nickname
|
||||
{
|
||||
/**
|
||||
* Regex fragment for pulling an arbitrarily-formated nickname.
|
||||
* Regex fragment for pulling a formated nickname *OR* ID number.
|
||||
* Suitable for router def of 'id' parameters on API actions.
|
||||
*
|
||||
* Not guaranteed to be valid after normalization; run the string through
|
||||
* Nickname::normalize() to get the canonical form, or Nickname::isValid()
|
||||
* if you just need to check if it's properly formatted.
|
||||
*
|
||||
* This, DISPLAY_FMT, and CANONICAL_FMT replace the old NICKNAME_FMT,
|
||||
* but be aware that these should not be enclosed in []s.
|
||||
*
|
||||
* @fixme would prefer to define in reference to the other constants
|
||||
*/
|
||||
const INPUT_FMT = '(?:[0-9]+|[0-9a-zA-Z_]{1,64})';
|
||||
|
||||
/**
|
||||
* Regex fragment for acceptable user-formatted variant of a nickname.
|
||||
* This includes some chars such as underscore which will be removed
|
||||
* from the normalized canonical form, but still must fit within
|
||||
* field length limits.
|
||||
*
|
||||
* Not guaranteed to be valid after normalization; run the string through
|
||||
* Nickname::normalize() to get the canonical form, or Nickname::isValid()
|
||||
@ -29,7 +47,7 @@ class Nickname
|
||||
* This and CANONICAL_FMT replace the old NICKNAME_FMT, but be aware
|
||||
* that these should not be enclosed in []s.
|
||||
*/
|
||||
const DISPLAY_FMT = '[0-9a-zA-Z_]+';
|
||||
const DISPLAY_FMT = '[0-9a-zA-Z_]{1,64}';
|
||||
|
||||
/**
|
||||
* Regex fragment for checking a canonical nickname.
|
||||
|
183
lib/router.php
@ -55,14 +55,14 @@ class StatusNet_URL_Mapper extends Net_URL_Mapper
|
||||
$result = null;
|
||||
if (Event::handle('StartConnectPath', array(&$path, &$defaults, &$rules, &$result))) {
|
||||
$result = parent::connect($path, $defaults, $rules);
|
||||
if (array_key_exists('action', $defaults)) {
|
||||
$action = $defaults['action'];
|
||||
} elseif (array_key_exists('action', $rules)) {
|
||||
$action = $rules['action'];
|
||||
} else {
|
||||
$action = null;
|
||||
}
|
||||
$this->_mapAction($action, $result);
|
||||
if (array_key_exists('action', $defaults)) {
|
||||
$action = $defaults['action'];
|
||||
} elseif (array_key_exists('action', $rules)) {
|
||||
$action = $rules['action'];
|
||||
} else {
|
||||
$action = null;
|
||||
}
|
||||
$this->_mapAction($action, $result);
|
||||
Event::handle('EndConnectPath', array($path, $defaults, $rules, $result));
|
||||
}
|
||||
return $result;
|
||||
@ -70,31 +70,31 @@ class StatusNet_URL_Mapper extends Net_URL_Mapper
|
||||
|
||||
protected function _mapAction($action, $path)
|
||||
{
|
||||
if (!array_key_exists($action, $this->_actionToPath)) {
|
||||
$this->_actionToPath[$action] = array();
|
||||
}
|
||||
$this->_actionToPath[$action][] = $path;
|
||||
return;
|
||||
if (!array_key_exists($action, $this->_actionToPath)) {
|
||||
$this->_actionToPath[$action] = array();
|
||||
}
|
||||
$this->_actionToPath[$action][] = $path;
|
||||
return;
|
||||
}
|
||||
|
||||
public function generate($values = array(), $qstring = array(), $anchor = '')
|
||||
{
|
||||
if (!array_key_exists('action', $values)) {
|
||||
return parent::generate($values, $qstring, $anchor);
|
||||
}
|
||||
if (!array_key_exists('action', $values)) {
|
||||
return parent::generate($values, $qstring, $anchor);
|
||||
}
|
||||
|
||||
$action = $values['action'];
|
||||
$action = $values['action'];
|
||||
|
||||
if (!array_key_exists($action, $this->_actionToPath)) {
|
||||
return parent::generate($values, $qstring, $anchor);
|
||||
}
|
||||
if (!array_key_exists($action, $this->_actionToPath)) {
|
||||
return parent::generate($values, $qstring, $anchor);
|
||||
}
|
||||
|
||||
$oldPaths = $this->paths;
|
||||
$this->paths = $this->_actionToPath[$action];
|
||||
$result = parent::generate($values, $qstring, $anchor);
|
||||
$this->paths = $oldPaths;
|
||||
$oldPaths = $this->paths;
|
||||
$this->paths = $this->_actionToPath[$action];
|
||||
$result = parent::generate($values, $qstring, $anchor);
|
||||
$this->paths = $oldPaths;
|
||||
|
||||
return $result;
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,19 +127,19 @@ class Router
|
||||
function __construct()
|
||||
{
|
||||
if (empty($this->m)) {
|
||||
if (!common_config('router', 'cache')) {
|
||||
if (!common_config('router', 'cache')) {
|
||||
$this->m = $this->initialize();
|
||||
} else {
|
||||
$k = self::cacheKey();
|
||||
$c = Cache::instance();
|
||||
$m = $c->get($k);
|
||||
if (!empty($m)) {
|
||||
$this->m = $m;
|
||||
} else {
|
||||
$this->m = $this->initialize();
|
||||
$c->set($k, $this->m);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$k = self::cacheKey();
|
||||
$c = Cache::instance();
|
||||
$m = $c->get($k);
|
||||
if (!empty($m)) {
|
||||
$this->m = $m;
|
||||
} else {
|
||||
$this->m = $this->initialize();
|
||||
$c->set($k, $this->m);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,7 +199,7 @@ class Router
|
||||
'deleteuser',
|
||||
'geocode',
|
||||
'version',
|
||||
);
|
||||
);
|
||||
|
||||
foreach ($main as $a) {
|
||||
$m->connect('main/'.$a, array('action' => $a));
|
||||
@ -222,8 +222,8 @@ class Router
|
||||
array('action' => 'publicxrds'));
|
||||
$m->connect('.well-known/host-meta',
|
||||
array('action' => 'hostmeta'));
|
||||
$m->connect('main/xrd',
|
||||
array('action' => 'userxrd'));
|
||||
$m->connect('main/xrd',
|
||||
array('action' => 'userxrd'));
|
||||
|
||||
// these take a code
|
||||
|
||||
@ -248,19 +248,19 @@ class Router
|
||||
}
|
||||
|
||||
$m->connect('settings/oauthapps/show/:id',
|
||||
array('action' => 'showapplication'),
|
||||
array('id' => '[0-9]+')
|
||||
array('action' => 'showapplication'),
|
||||
array('id' => '[0-9]+')
|
||||
);
|
||||
$m->connect('settings/oauthapps/new',
|
||||
array('action' => 'newapplication')
|
||||
array('action' => 'newapplication')
|
||||
);
|
||||
$m->connect('settings/oauthapps/edit/:id',
|
||||
array('action' => 'editapplication'),
|
||||
array('id' => '[0-9]+')
|
||||
array('action' => 'editapplication'),
|
||||
array('id' => '[0-9]+')
|
||||
);
|
||||
$m->connect('settings/oauthapps/delete/:id',
|
||||
array('action' => 'deleteapplication'),
|
||||
array('id' => '[0-9]+')
|
||||
array('action' => 'deleteapplication'),
|
||||
array('id' => '[0-9]+')
|
||||
);
|
||||
|
||||
// search
|
||||
@ -408,7 +408,7 @@ class Router
|
||||
|
||||
$m->connect('api/statuses/friends_timeline/:id.:format',
|
||||
array('action' => 'ApiTimelineFriends',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json|rss|atom)'));
|
||||
|
||||
$m->connect('api/statuses/home_timeline.:format',
|
||||
@ -417,7 +417,7 @@ class Router
|
||||
|
||||
$m->connect('api/statuses/home_timeline/:id.:format',
|
||||
array('action' => 'ApiTimelineHome',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json|rss|atom)'));
|
||||
|
||||
$m->connect('api/statuses/user_timeline.:format',
|
||||
@ -426,7 +426,7 @@ class Router
|
||||
|
||||
$m->connect('api/statuses/user_timeline/:id.:format',
|
||||
array('action' => 'ApiTimelineUser',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json|rss|atom)'));
|
||||
|
||||
$m->connect('api/statuses/mentions.:format',
|
||||
@ -435,7 +435,7 @@ class Router
|
||||
|
||||
$m->connect('api/statuses/mentions/:id.:format',
|
||||
array('action' => 'ApiTimelineMentions',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json|rss|atom)'));
|
||||
|
||||
$m->connect('api/statuses/replies.:format',
|
||||
@ -444,7 +444,7 @@ class Router
|
||||
|
||||
$m->connect('api/statuses/replies/:id.:format',
|
||||
array('action' => 'ApiTimelineMentions',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json|rss|atom)'));
|
||||
|
||||
$m->connect('api/statuses/retweeted_by_me.:format',
|
||||
@ -465,7 +465,7 @@ class Router
|
||||
|
||||
$m->connect('api/statuses/friends/:id.:format',
|
||||
array('action' => 'ApiUserFriends',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json)'));
|
||||
|
||||
$m->connect('api/statuses/followers.:format',
|
||||
@ -474,7 +474,7 @@ class Router
|
||||
|
||||
$m->connect('api/statuses/followers/:id.:format',
|
||||
array('action' => 'ApiUserFollowers',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json)'));
|
||||
|
||||
$m->connect('api/statuses/show.:format',
|
||||
@ -517,7 +517,7 @@ class Router
|
||||
|
||||
$m->connect('api/users/show/:id.:format',
|
||||
array('action' => 'ApiUserShow',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json)'));
|
||||
|
||||
// direct messages
|
||||
@ -555,12 +555,12 @@ class Router
|
||||
|
||||
$m->connect('api/friendships/create/:id.:format',
|
||||
array('action' => 'ApiFriendshipsCreate',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json)'));
|
||||
|
||||
$m->connect('api/friendships/destroy/:id.:format',
|
||||
array('action' => 'ApiFriendshipsDestroy',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json)'));
|
||||
|
||||
// Social graph
|
||||
@ -617,17 +617,17 @@ class Router
|
||||
|
||||
$m->connect('api/favorites/:id.:format',
|
||||
array('action' => 'ApiTimelineFavorites',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json|rss|atom)'));
|
||||
|
||||
$m->connect('api/favorites/create/:id.:format',
|
||||
array('action' => 'ApiFavoriteCreate',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => '[0-9]+',
|
||||
'format' => '(xml|json)'));
|
||||
|
||||
$m->connect('api/favorites/destroy/:id.:format',
|
||||
array('action' => 'ApiFavoriteDestroy',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => '[0-9]+',
|
||||
'format' => '(xml|json)'));
|
||||
// blocks
|
||||
|
||||
@ -637,7 +637,7 @@ class Router
|
||||
|
||||
$m->connect('api/blocks/create/:id.:format',
|
||||
array('action' => 'ApiBlockCreate',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json)'));
|
||||
|
||||
$m->connect('api/blocks/destroy.:format',
|
||||
@ -646,7 +646,7 @@ class Router
|
||||
|
||||
$m->connect('api/blocks/destroy/:id.:format',
|
||||
array('action' => 'ApiBlockDestroy',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json)'));
|
||||
// help
|
||||
|
||||
@ -682,7 +682,7 @@ class Router
|
||||
|
||||
$m->connect('api/statusnet/groups/timeline/:id.:format',
|
||||
array('action' => 'ApiTimelineGroup',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json|rss|atom)'));
|
||||
|
||||
$m->connect('api/statusnet/groups/show.:format',
|
||||
@ -691,12 +691,12 @@ class Router
|
||||
|
||||
$m->connect('api/statusnet/groups/show/:id.:format',
|
||||
array('action' => 'ApiGroupShow',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json)'));
|
||||
|
||||
$m->connect('api/statusnet/groups/join.:format',
|
||||
array('action' => 'ApiGroupJoin',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json)'));
|
||||
|
||||
$m->connect('api/statusnet/groups/join/:id.:format',
|
||||
@ -705,7 +705,7 @@ class Router
|
||||
|
||||
$m->connect('api/statusnet/groups/leave.:format',
|
||||
array('action' => 'ApiGroupLeave',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json)'));
|
||||
|
||||
$m->connect('api/statusnet/groups/leave/:id.:format',
|
||||
@ -722,7 +722,7 @@ class Router
|
||||
|
||||
$m->connect('api/statusnet/groups/list/:id.:format',
|
||||
array('action' => 'ApiGroupList',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json|rss|atom)'));
|
||||
|
||||
$m->connect('api/statusnet/groups/list_all.:format',
|
||||
@ -735,7 +735,7 @@ class Router
|
||||
|
||||
$m->connect('api/statusnet/groups/membership/:id.:format',
|
||||
array('action' => 'ApiGroupMembership',
|
||||
'id' => Nickname::DISPLAY_FMT,
|
||||
'id' => Nickname::INPUT_FMT,
|
||||
'format' => '(xml|json)'));
|
||||
|
||||
$m->connect('api/statusnet/groups/create.:format',
|
||||
@ -772,13 +772,6 @@ class Router
|
||||
$m->connect('api/oauth/authorize',
|
||||
array('action' => 'ApiOauthAuthorize'));
|
||||
|
||||
$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'));
|
||||
|
||||
// Admin
|
||||
|
||||
$m->connect('admin/site', array('action' => 'siteadminpanel'));
|
||||
@ -928,6 +921,42 @@ class Router
|
||||
array('nickname' => Nickname::DISPLAY_FMT));
|
||||
}
|
||||
|
||||
// AtomPub API
|
||||
|
||||
$m->connect('api/statusnet/app/service/:id.xml',
|
||||
array('action' => 'ApiAtomService'),
|
||||
array('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));
|
||||
@ -970,14 +999,14 @@ class Router
|
||||
$qpos = strpos($url, '?');
|
||||
if ($qpos !== false) {
|
||||
$url = substr($url, 0, $qpos+1) .
|
||||
str_replace('?', '&', substr($url, $qpos+1));
|
||||
str_replace('?', '&', substr($url, $qpos+1));
|
||||
|
||||
// @fixme this is a hacky workaround for http_build_query in the
|
||||
// lower-level code and bad configs that set the default separator
|
||||
// to & instead of &. Encoded &s in parameters will not be
|
||||
// affected.
|
||||
$url = substr($url, 0, $qpos+1) .
|
||||
str_replace('&', '&', substr($url, $qpos+1));
|
||||
str_replace('&', '&', substr($url, $qpos+1));
|
||||
|
||||
}
|
||||
|
||||
|
10
lib/xrd.php
@ -130,14 +130,24 @@ class XRD
|
||||
|
||||
foreach ($this->links as $link) {
|
||||
$titles = array();
|
||||
$properties = array();
|
||||
if (isset($link['title'])) {
|
||||
$titles = $link['title'];
|
||||
unset($link['title']);
|
||||
}
|
||||
if (isset($link['property'])) {
|
||||
$properties = $link['property'];
|
||||
unset($link['property']);
|
||||
}
|
||||
$xs->elementStart('Link', $link);
|
||||
foreach ($titles as $title) {
|
||||
$xs->element('Title', null, $title);
|
||||
}
|
||||
foreach ($properties as $property) {
|
||||
$xs->element('Property',
|
||||
array('type' => $property['type']),
|
||||
$property['value']);
|
||||
}
|
||||
$xs->elementEnd('Link');
|
||||
}
|
||||
|
||||
|
@ -53,54 +53,67 @@ class XrdAction extends Action
|
||||
$xrd->subject = self::normalize($this->uri);
|
||||
}
|
||||
|
||||
if (Event::handle('StartXrdActionAliases', array(&$xrd, $this->user))) {
|
||||
if (Event::handle('StartXrdActionAliases', array(&$xrd, $this->user))) {
|
||||
|
||||
// Possible aliases for the user
|
||||
// Possible aliases for the user
|
||||
|
||||
$uris = array($this->user->uri, $profile->profileurl);
|
||||
$uris = array($this->user->uri, $profile->profileurl);
|
||||
|
||||
// FIXME: Webfinger generation code should live somewhere on its own
|
||||
// FIXME: Webfinger generation code should live somewhere on its own
|
||||
|
||||
$path = common_config('site', 'path');
|
||||
$path = common_config('site', 'path');
|
||||
|
||||
if (empty($path)) {
|
||||
$uris[] = sprintf('acct:%s@%s', $nick, common_config('site', 'server'));
|
||||
}
|
||||
if (empty($path)) {
|
||||
$uris[] = sprintf('acct:%s@%s', $nick, common_config('site', 'server'));
|
||||
}
|
||||
|
||||
foreach ($uris as $uri) {
|
||||
if ($uri != $xrd->subject) {
|
||||
$xrd->alias[] = $uri;
|
||||
}
|
||||
}
|
||||
foreach ($uris as $uri) {
|
||||
if ($uri != $xrd->subject) {
|
||||
$xrd->alias[] = $uri;
|
||||
}
|
||||
}
|
||||
|
||||
Event::handle('EndXrdActionAliases', array(&$xrd, $this->user));
|
||||
}
|
||||
Event::handle('EndXrdActionAliases', array(&$xrd, $this->user));
|
||||
}
|
||||
|
||||
if (Event::handle('StartXrdActionLinks', array(&$xrd, $this->user))) {
|
||||
if (Event::handle('StartXrdActionLinks', array(&$xrd, $this->user))) {
|
||||
|
||||
$xrd->links[] = array('rel' => self::PROFILEPAGE,
|
||||
'type' => 'text/html',
|
||||
'href' => $profile->profileurl);
|
||||
$xrd->links[] = array('rel' => self::PROFILEPAGE,
|
||||
'type' => 'text/html',
|
||||
'href' => $profile->profileurl);
|
||||
|
||||
// hCard
|
||||
$xrd->links[] = array('rel' => self::HCARD,
|
||||
'type' => 'text/html',
|
||||
'href' => common_local_url('hcard', array('nickname' => $nick)));
|
||||
// hCard
|
||||
$xrd->links[] = array('rel' => self::HCARD,
|
||||
'type' => 'text/html',
|
||||
'href' => common_local_url('hcard', array('nickname' => $nick)));
|
||||
|
||||
// XFN
|
||||
$xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11',
|
||||
'type' => 'text/html',
|
||||
'href' => $profile->profileurl);
|
||||
// FOAF
|
||||
$xrd->links[] = array('rel' => 'describedby',
|
||||
'type' => 'application/rdf+xml',
|
||||
'href' => common_local_url('foaf',
|
||||
array('nickname' => $nick)));
|
||||
// XFN
|
||||
$xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11',
|
||||
'type' => 'text/html',
|
||||
'href' => $profile->profileurl);
|
||||
// FOAF
|
||||
$xrd->links[] = array('rel' => 'describedby',
|
||||
'type' => 'application/rdf+xml',
|
||||
'href' => common_local_url('foaf',
|
||||
array('nickname' => $nick)));
|
||||
|
||||
$xrd->links[] = array('rel' => 'http://apinamespace.org/atom',
|
||||
'type' => 'application/atomsvc+xml',
|
||||
'href' => common_local_url('ApiAtomService', array('id' => $nick)));
|
||||
|
||||
Event::handle('EndXrdActionLinks', array(&$xrd, $this->user));
|
||||
}
|
||||
|
||||
if (common_config('site', 'fancy')) {
|
||||
$apiRoot = common_path('api/', true);
|
||||
} else {
|
||||
$apiRoot = common_path('index.php/api/', true);
|
||||
}
|
||||
|
||||
$xrd->links[] = array('rel' => 'http://apinamespace.org/twitter',
|
||||
'href' => $apiRoot,
|
||||
'property' => array(array('type' => 'http://apinamespace.org/twitter/username',
|
||||
'value' => $nick)));
|
||||
|
||||
Event::handle('EndXrdActionLinks', array(&$xrd, $this->user));
|
||||
}
|
||||
|
||||
header('Content-type: application/xrd+xml');
|
||||
print $xrd->toXML();
|
||||
@ -132,4 +145,16 @@ class XrdAction extends Action
|
||||
|
||||
return (substr($uri, 0, 5) == 'acct:');
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this action read-only?
|
||||
*
|
||||
* @param array $args other arguments
|
||||
*
|
||||
* @return boolean is read only action?
|
||||
*/
|
||||
function isReadOnly($args)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -165,4 +165,16 @@ class AutocompleteAction extends Action
|
||||
print json_encode($result) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this action read-only?
|
||||
*
|
||||
* @param array $args other arguments
|
||||
*
|
||||
* @return boolean is read only action?
|
||||
*/
|
||||
function isReadOnly($args)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
215
plugins/InProcessCache/InProcessCachePlugin.php
Normal file
@ -0,0 +1,215 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* Extra level of caching, in memory
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category Cache
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra level of caching
|
||||
*
|
||||
* This plugin adds an extra level of in-process caching to any regular
|
||||
* cache system like APC, XCache, or Memcache.
|
||||
*
|
||||
* Note that since most caching plugins return false for StartCache*
|
||||
* methods, you should add this plugin before them, i.e.
|
||||
*
|
||||
* addPlugin('InProcessCache');
|
||||
* addPlugin('XCache');
|
||||
*
|
||||
* @category Cache
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
class InProcessCachePlugin extends Plugin
|
||||
{
|
||||
private $_items = array();
|
||||
private $_hits = array();
|
||||
private $active;
|
||||
|
||||
/**
|
||||
* Constructor checks if it's safe to use the in-process cache.
|
||||
* On CLI scripts, we'll disable ourselves to avoid data corruption
|
||||
* due to keeping stale data around.
|
||||
*
|
||||
* On web requests we'll roll the dice; they're short-lived so have
|
||||
* less chance of stale data. Race conditions are still possible,
|
||||
* so beware!
|
||||
*/
|
||||
function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->active = (PHP_SAPI != 'cli');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item from the cache
|
||||
*
|
||||
* Called before other cache systems are called (iif this
|
||||
* plugin was loaded correctly, see class comment). If we
|
||||
* have the data, return it, and don't hit the other cache
|
||||
* systems.
|
||||
*
|
||||
* @param string &$key Key to fetch
|
||||
* @param mixed &$value Resulting value or false for miss
|
||||
*
|
||||
* @return boolean false if found, else true
|
||||
*/
|
||||
|
||||
function onStartCacheGet(&$key, &$value)
|
||||
{
|
||||
if ($this->active && array_key_exists($key, $this->_items)) {
|
||||
$value = $this->_items[$key];
|
||||
if (array_key_exists($key, $this->_hits)) {
|
||||
$this->_hits[$key]++;
|
||||
} else {
|
||||
$this->_hits[$key] = 1;
|
||||
}
|
||||
Event::handle('EndCacheGet', array($key, &$value));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at the end of a cache get
|
||||
*
|
||||
* If we don't already have the data, we cache it. This
|
||||
* keeps us from having to call the external cache if the
|
||||
* key is requested again.
|
||||
*
|
||||
* @param string $key Key to fetch
|
||||
* @param mixed &$value Resulting value or false for miss
|
||||
*
|
||||
* @return boolean hook value, true
|
||||
*/
|
||||
|
||||
function onEndCacheGet($key, &$value)
|
||||
{
|
||||
if ($this->active && (!array_key_exists($key, $this->_items) ||
|
||||
$this->_items[$key] != $value)) {
|
||||
$this->_items[$key] = $value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at the end of setting a cache element
|
||||
*
|
||||
* Always set the cache element; may overwrite existing
|
||||
* data.
|
||||
*
|
||||
* @param string $key Key to fetch
|
||||
* @param mixed $value Resulting value or false for miss
|
||||
* @param integer $flag ignored
|
||||
* @param integer $expiry ignored
|
||||
*
|
||||
* @return boolean true
|
||||
*/
|
||||
|
||||
function onEndCacheSet($key, $value, $flag, $expiry)
|
||||
{
|
||||
if ($this->active) {
|
||||
$this->_items[$key] = $value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at the end of deleting a cache element
|
||||
*
|
||||
* If stuff's deleted from the other cache, we
|
||||
* delete it too.
|
||||
*
|
||||
* @param string &$key Key to delete
|
||||
* @param boolean &$success Success flag; ignored
|
||||
*
|
||||
* @return boolean true
|
||||
*/
|
||||
|
||||
function onStartCacheDelete(&$key, &$success)
|
||||
{
|
||||
if ($this->active && array_key_exists($key, $this->_items)) {
|
||||
unset($this->_items[$key]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version info
|
||||
*
|
||||
* @param array &$versions Array of version blocks
|
||||
*
|
||||
* @return boolean true
|
||||
*/
|
||||
|
||||
function onPluginVersion(&$versions)
|
||||
{
|
||||
$url = 'http://status.net/wiki/Plugin:InProcessCache';
|
||||
|
||||
$versions[] = array('name' => 'InProcessCache',
|
||||
'version' => STATUSNET_VERSION,
|
||||
'author' => 'Evan Prodromou',
|
||||
'homepage' => $url,
|
||||
'description' =>
|
||||
_m('Additional in-process cache for plugins.'));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup function; called at end of process
|
||||
*
|
||||
* If the inprocess/stats config value is true, we dump
|
||||
* stats to the log file
|
||||
*
|
||||
* @return boolean true
|
||||
*/
|
||||
|
||||
function cleanup()
|
||||
{
|
||||
if ($this->active && common_config('inprocess', 'stats')) {
|
||||
$this->log(LOG_INFO, "cache size: " .
|
||||
count($this->_items));
|
||||
$sum = 0;
|
||||
foreach ($this->_hits as $hitcount) {
|
||||
$sum += $hitcount;
|
||||
}
|
||||
$this->log(LOG_INFO, $sum . " hits on " .
|
||||
count($this->_hits) . " keys");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
14
plugins/Mapstraction/Makefile
Normal file
@ -0,0 +1,14 @@
|
||||
.fake: clean all
|
||||
|
||||
TARGETS=usermap-mxn-openlayers.min.js
|
||||
|
||||
CORE=js/mxn.js js/mxn.core.js
|
||||
USERMAP=usermap.js
|
||||
|
||||
all: $(TARGETS)
|
||||
|
||||
clean:
|
||||
rm -f $(TARGETS)
|
||||
|
||||
usermap-mxn-openlayers.min.js: $(CORE) js/mxn.openlayers.core.js $(USERMAP)
|
||||
cat $+ | yui-compressor -o $@ --type=js
|
@ -128,8 +128,8 @@ class MapstractionPlugin extends Plugin
|
||||
$action->script((StatusNet::isHTTPS()?'https':'http') + '://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6');
|
||||
break;
|
||||
case 'openlayers':
|
||||
// XXX: is this not nice...?
|
||||
$action->script('http://openlayers.org/api/OpenLayers.js');
|
||||
// Use our included stripped & minified OpenLayers.
|
||||
$action->script(common_path('plugins/Mapstraction/OpenLayers/OpenLayers.js'));
|
||||
break;
|
||||
case 'yahoo':
|
||||
$action->script(sprintf('http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=%s',
|
||||
@ -140,11 +140,19 @@ class MapstractionPlugin extends Plugin
|
||||
return true;
|
||||
}
|
||||
|
||||
$action->script(sprintf('%s?(%s)',
|
||||
common_path('plugins/Mapstraction/js/mxn.js'),
|
||||
$this->provider));
|
||||
if ($this->provider == 'openlayers') {
|
||||
// We have an optimized path for our default case.
|
||||
//
|
||||
// Note that OpenLayers.js needs to be separate, or it won't
|
||||
// be able to find its UI images and styles.
|
||||
$action->script(common_path('plugins/Mapstraction/usermap-mxn-openlayers.min.js'));
|
||||
} else {
|
||||
$action->script(sprintf('%s?(%s)',
|
||||
common_path('plugins/Mapstraction/js/mxn.js'),
|
||||
$this->provider));
|
||||
|
||||
$action->script(common_path('plugins/Mapstraction/usermap.js'));
|
||||
$action->script(common_path('plugins/Mapstraction/usermap.js'));
|
||||
}
|
||||
|
||||
$action->inlineScript(sprintf('var _provider = "%s";', $this->provider));
|
||||
|
||||
|
15
plugins/Mapstraction/OpenLayers/Makefile
Normal file
@ -0,0 +1,15 @@
|
||||
.fake: clean all
|
||||
|
||||
TARGET=OpenLayers.js
|
||||
SOURCEDIR=OpenLayers-2.10/
|
||||
HERE=`pwd`
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET)
|
||||
|
||||
$(TARGET): statusnet.cfg
|
||||
cp -f statusnet.cfg $(SOURCEDIR)/build/statusnet.cfg
|
||||
(cd $(SOURCEDIR)/build && ./build.py statusnet.cfg)
|
||||
yui-compressor $(SOURCEDIR)/build/OpenLayers.js -o $(TARGET)
|
1
plugins/Mapstraction/OpenLayers/OpenLayers.js
Normal file
14
plugins/Mapstraction/OpenLayers/README
Normal file
@ -0,0 +1,14 @@
|
||||
The default OpenLayers.js file, minified, weighs in at a whopping 943kb uncompressed.
|
||||
With gzip compression it's still over 200kb, so we're building a stripped-down copy
|
||||
with just what we need.
|
||||
|
||||
Docs on how the stripping process works:
|
||||
http://docs.openlayers.org/library/deploying.html
|
||||
|
||||
To recreate this OpenLayers.js file:
|
||||
|
||||
# get yui-compressor (install from apt, or set up a shell script or alias to the jar)
|
||||
# download and decompress OpenLayers-2.10 zip or tgz
|
||||
make clean && make
|
||||
|
||||
If necessary, change the relative path to the OpenLayers source directory in the Makefile.
|
BIN
plugins/Mapstraction/OpenLayers/img/blank.gif
Normal file
After Width: | Height: | Size: 42 B |
BIN
plugins/Mapstraction/OpenLayers/img/cloud-popup-relative.png
Executable file
After Width: | Height: | Size: 3.1 KiB |
BIN
plugins/Mapstraction/OpenLayers/img/drag-rectangle-off.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
plugins/Mapstraction/OpenLayers/img/drag-rectangle-on.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
plugins/Mapstraction/OpenLayers/img/east-mini.png
Normal file
After Width: | Height: | Size: 451 B |
BIN
plugins/Mapstraction/OpenLayers/img/layer-switcher-maximize.png
Normal file
After Width: | Height: | Size: 451 B |
BIN
plugins/Mapstraction/OpenLayers/img/layer-switcher-minimize.png
Normal file
After Width: | Height: | Size: 249 B |
BIN
plugins/Mapstraction/OpenLayers/img/marker-blue.png
Normal file
After Width: | Height: | Size: 992 B |
BIN
plugins/Mapstraction/OpenLayers/img/marker-gold.png
Normal file
After Width: | Height: | Size: 831 B |
BIN
plugins/Mapstraction/OpenLayers/img/marker-green.png
Normal file
After Width: | Height: | Size: 967 B |
BIN
plugins/Mapstraction/OpenLayers/img/marker.png
Normal file
After Width: | Height: | Size: 606 B |
BIN
plugins/Mapstraction/OpenLayers/img/measuring-stick-off.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
plugins/Mapstraction/OpenLayers/img/measuring-stick-on.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
plugins/Mapstraction/OpenLayers/img/north-mini.png
Normal file
After Width: | Height: | Size: 484 B |
BIN
plugins/Mapstraction/OpenLayers/img/panning-hand-off.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
plugins/Mapstraction/OpenLayers/img/panning-hand-on.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
plugins/Mapstraction/OpenLayers/img/slider.png
Normal file
After Width: | Height: | Size: 285 B |
BIN
plugins/Mapstraction/OpenLayers/img/south-mini.png
Normal file
After Width: | Height: | Size: 481 B |
BIN
plugins/Mapstraction/OpenLayers/img/west-mini.png
Normal file
After Width: | Height: | Size: 453 B |
BIN
plugins/Mapstraction/OpenLayers/img/zoom-minus-mini.png
Normal file
After Width: | Height: | Size: 359 B |
BIN
plugins/Mapstraction/OpenLayers/img/zoom-plus-mini.png
Normal file
After Width: | Height: | Size: 489 B |
BIN
plugins/Mapstraction/OpenLayers/img/zoom-world-mini.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
plugins/Mapstraction/OpenLayers/img/zoombar.png
Normal file
After Width: | Height: | Size: 463 B |
43
plugins/Mapstraction/OpenLayers/statusnet.cfg
Normal file
@ -0,0 +1,43 @@
|
||||
# Modified lite.cfg for building an OpenLayers subset for StatusNet's Mapstraction plugin.
|
||||
|
||||
# This file includes a small subset of OpenLayers code, designed to be
|
||||
# integrated into another application. It includes only the Layer types
|
||||
# neccesary to create tiled or untiled WMS, and does not include any Controls.
|
||||
# This is the result of what was at the time called "Webmap.js" at the FOSS4G
|
||||
# Web Mapping BOF.
|
||||
|
||||
[first]
|
||||
OpenLayers/SingleFile.js
|
||||
OpenLayers.js
|
||||
OpenLayers/BaseTypes.js
|
||||
OpenLayers/BaseTypes/Class.js
|
||||
OpenLayers/Util.js
|
||||
|
||||
[last]
|
||||
|
||||
[include]
|
||||
OpenLayers/Map.js
|
||||
OpenLayers/Layer/WMS.js
|
||||
|
||||
# Needed for Mapstraction on StatusNet
|
||||
OpenLayers/Feature.js
|
||||
OpenLayers/Feature/Vector.js
|
||||
OpenLayers/Geometry.js
|
||||
OpenLayers/Geometry/Point.js
|
||||
OpenLayers/Geometry/LinearRing.js
|
||||
OpenLayers/Geometry/LineString.js
|
||||
OpenLayers/Icon.js
|
||||
OpenLayers/Layer/Markers.js
|
||||
OpenLayers/Layer/TMS.js
|
||||
OpenLayers/Marker.js
|
||||
OpenLayers/Popup.js
|
||||
|
||||
# Default controls, needed to keep the map dynamic
|
||||
OpenLayers/Control/ArgParser.js
|
||||
OpenLayers/Control/Attribution.js
|
||||
OpenLayers/Control/Navigation.js
|
||||
OpenLayers/Control/PanZoom.js
|
||||
|
||||
[exclude]
|
||||
|
||||
|
10
plugins/Mapstraction/OpenLayers/theme/default/google.css
Normal file
@ -0,0 +1,10 @@
|
||||
.olLayerGoogleCopyright {
|
||||
right: 3px;
|
||||
bottom: 2px;
|
||||
left: auto;
|
||||
}
|
||||
.olLayerGooglePoweredBy {
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
.olControlZoomPanel div {
|
||||
background-image: url(img/zoom-panel-NOALPHA.png);
|
||||
}
|
||||
.olControlPanPanel div {
|
||||
background-image: url(img/pan-panel-NOALPHA.png);
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.4 KiB |
BIN
plugins/Mapstraction/OpenLayers/theme/default/img/blank.gif
Normal file
After Width: | Height: | Size: 42 B |
BIN
plugins/Mapstraction/OpenLayers/theme/default/img/close.gif
Normal file
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 79 B |
After Width: | Height: | Size: 566 B |
BIN
plugins/Mapstraction/OpenLayers/theme/default/img/pan-panel.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
plugins/Mapstraction/OpenLayers/theme/default/img/pan_off.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
plugins/Mapstraction/OpenLayers/theme/default/img/pan_on.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.4 KiB |
BIN
plugins/Mapstraction/OpenLayers/theme/default/img/ruler.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 357 B |
After Width: | Height: | Size: 364 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.1 KiB |
BIN
plugins/Mapstraction/OpenLayers/theme/default/img/zoom-panel.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
397
plugins/Mapstraction/OpenLayers/theme/default/style.css
Normal file
@ -0,0 +1,397 @@
|
||||
div.olMap {
|
||||
z-index: 0;
|
||||
padding: 0px!important;
|
||||
margin: 0px!important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
div.olMapViewport {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.olLayerDiv {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.olLayerGoogleCopyright {
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
}
|
||||
.olLayerGooglePoweredBy {
|
||||
left: 2px;
|
||||
bottom: 15px;
|
||||
}
|
||||
.olControlAttribution {
|
||||
font-size: smaller;
|
||||
right: 3px;
|
||||
bottom: 4.5em;
|
||||
position: absolute;
|
||||
display: block;
|
||||
}
|
||||
.olControlScale {
|
||||
right: 3px;
|
||||
bottom: 3em;
|
||||
display: block;
|
||||
position: absolute;
|
||||
font-size: smaller;
|
||||
}
|
||||
.olControlScaleLine {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: 15px;
|
||||
font-size: xx-small;
|
||||
}
|
||||
.olControlScaleLineBottom {
|
||||
border: solid 2px black;
|
||||
border-bottom: none;
|
||||
margin-top:-2px;
|
||||
text-align: center;
|
||||
}
|
||||
.olControlScaleLineTop {
|
||||
border: solid 2px black;
|
||||
border-top: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.olControlPermalink {
|
||||
right: 3px;
|
||||
bottom: 1.5em;
|
||||
display: block;
|
||||
position: absolute;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
div.olControlMousePosition {
|
||||
bottom: 0em;
|
||||
right: 3px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
font-family: Arial;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.olControlOverviewMapContainer {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.olControlOverviewMapElement {
|
||||
padding: 10px 18px 10px 10px;
|
||||
background-color: #00008B;
|
||||
-moz-border-radius: 1em 0 0 0;
|
||||
}
|
||||
|
||||
.olControlOverviewMapMinimizeButton {
|
||||
right: 0px;
|
||||
bottom: 80px;
|
||||
}
|
||||
|
||||
.olControlOverviewMapMaximizeButton {
|
||||
right: 0px;
|
||||
bottom: 80px;
|
||||
}
|
||||
|
||||
.olControlOverviewMapExtentRectangle {
|
||||
overflow: hidden;
|
||||
background-image: url("img/blank.gif");
|
||||
cursor: move;
|
||||
border: 2px dotted red;
|
||||
}
|
||||
.olControlOverviewMapRectReplacement {
|
||||
overflow: hidden;
|
||||
cursor: move;
|
||||
background-image: url("img/overview_replacement.gif");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.olLayerGeoRSSDescription {
|
||||
float:left;
|
||||
width:100%;
|
||||
overflow:auto;
|
||||
font-size:1.0em;
|
||||
}
|
||||
.olLayerGeoRSSClose {
|
||||
float:right;
|
||||
color:gray;
|
||||
font-size:1.2em;
|
||||
margin-right:6px;
|
||||
font-family:sans-serif;
|
||||
}
|
||||
.olLayerGeoRSSTitle {
|
||||
float:left;font-size:1.2em;
|
||||
}
|
||||
|
||||
.olPopupContent {
|
||||
padding:5px;
|
||||
overflow: auto;
|
||||
}
|
||||
.olControlNavToolbar {
|
||||
width:0px;
|
||||
height:0px;
|
||||
}
|
||||
.olControlNavToolbar div {
|
||||
display:block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
top: 300px;
|
||||
left: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.olControlNavigationHistory {
|
||||
background-image: url("img/navigation_history.png");
|
||||
background-repeat: no-repeat;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
}
|
||||
.olControlNavigationHistoryPreviousItemActive {
|
||||
background-position: 0px 0px;
|
||||
}
|
||||
.olControlNavigationHistoryPreviousItemInactive {
|
||||
background-position: 0px -24px;
|
||||
}
|
||||
.olControlNavigationHistoryNextItemActive {
|
||||
background-position: -24px 0px;
|
||||
}
|
||||
.olControlNavigationHistoryNextItemInactive {
|
||||
background-position: -24px -24px;
|
||||
}
|
||||
|
||||
.olControlNavToolbar .olControlNavigationItemActive {
|
||||
background-image: url("img/panning-hand-on.png");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.olControlNavToolbar .olControlNavigationItemInactive {
|
||||
background-image: url("img/panning-hand-off.png");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.olControlNavToolbar .olControlZoomBoxItemActive {
|
||||
background-image: url("img/drag-rectangle-on.png");
|
||||
background-color: orange;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.olControlNavToolbar .olControlZoomBoxItemInactive {
|
||||
background-image: url("img/drag-rectangle-off.png");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.olControlEditingToolbar {
|
||||
float:right;
|
||||
right: 0px;
|
||||
height: 30px;
|
||||
width: 200px;
|
||||
}
|
||||
.olControlEditingToolbar div {
|
||||
background-image: url("img/editing_tool_bar.png");
|
||||
background-repeat: no-repeat;
|
||||
float:right;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 5px;
|
||||
}
|
||||
.olControlEditingToolbar .olControlNavigationItemActive {
|
||||
background-position: -103px -23px;
|
||||
}
|
||||
.olControlEditingToolbar .olControlNavigationItemInactive {
|
||||
background-position: -103px -0px;
|
||||
}
|
||||
.olControlEditingToolbar .olControlDrawFeaturePointItemActive {
|
||||
background-position: -77px -23px;
|
||||
}
|
||||
.olControlEditingToolbar .olControlDrawFeaturePointItemInactive {
|
||||
background-position: -77px -0px;
|
||||
}
|
||||
.olControlEditingToolbar .olControlDrawFeaturePathItemInactive {
|
||||
background-position: -51px 0px;
|
||||
}
|
||||
.olControlEditingToolbar .olControlDrawFeaturePathItemActive {
|
||||
background-position: -51px -23px;
|
||||
}
|
||||
.olControlEditingToolbar .olControlDrawFeaturePolygonItemInactive {
|
||||
background-position: -26px 0px;
|
||||
}
|
||||
.olControlEditingToolbar .olControlDrawFeaturePolygonItemActive {
|
||||
background-position: -26px -23px ;
|
||||
}
|
||||
div.olControlSaveFeaturesItemActive {
|
||||
background-image: url(img/save_features_on.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0px 1px;
|
||||
}
|
||||
div.olControlSaveFeaturesItemInactive {
|
||||
background-image: url(img/save_features_off.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0px 1px;
|
||||
}
|
||||
|
||||
.olHandlerBoxZoomBox {
|
||||
border: 2px solid red;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
opacity: 0.50;
|
||||
font-size: 1px;
|
||||
filter: alpha(opacity=50);
|
||||
}
|
||||
.olHandlerBoxSelectFeature {
|
||||
border: 2px solid blue;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
opacity: 0.50;
|
||||
font-size: 1px;
|
||||
filter: alpha(opacity=50);
|
||||
}
|
||||
|
||||
.olControlPanPanel {
|
||||
top: 10px;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.olControlPanPanel div {
|
||||
background-image: url(img/pan-panel.png);
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.olControlPanPanel .olControlPanNorthItemInactive {
|
||||
top: 0px;
|
||||
left: 9px;
|
||||
background-position: 0px 0px;
|
||||
}
|
||||
.olControlPanPanel .olControlPanSouthItemInactive {
|
||||
top: 36px;
|
||||
left: 9px;
|
||||
background-position: 18px 0px;
|
||||
}
|
||||
.olControlPanPanel .olControlPanWestItemInactive {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 0px;
|
||||
background-position: 0px 18px;
|
||||
}
|
||||
.olControlPanPanel .olControlPanEastItemInactive {
|
||||
top: 18px;
|
||||
left: 18px;
|
||||
background-position: 18px 18px;
|
||||
}
|
||||
|
||||
.olControlZoomPanel {
|
||||
top: 71px;
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.olControlZoomPanel div {
|
||||
background-image: url(img/zoom-panel.png);
|
||||
position: absolute;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.olControlZoomPanel .olControlZoomInItemInactive {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
background-position: 0px 0px;
|
||||
}
|
||||
|
||||
.olControlZoomPanel .olControlZoomToMaxExtentItemInactive {
|
||||
top: 18px;
|
||||
left: 0px;
|
||||
background-position: 0px -18px;
|
||||
}
|
||||
|
||||
.olControlZoomPanel .olControlZoomOutItemInactive {
|
||||
top: 36px;
|
||||
left: 0px;
|
||||
background-position: 0px 18px;
|
||||
}
|
||||
|
||||
.olPopupCloseBox {
|
||||
background: url("img/close.gif") no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.olFramedCloudPopupContent {
|
||||
padding: 5px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.olControlNoSelect {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.olImageLoadError {
|
||||
background-color: pink;
|
||||
opacity: 0.5;
|
||||
filter: alpha(opacity=50); /* IE */
|
||||
}
|
||||
|
||||
/**
|
||||
* Cursor styles
|
||||
*/
|
||||
|
||||
.olCursorWait {
|
||||
cursor: wait;
|
||||
}
|
||||
.olDragDown {
|
||||
cursor: move;
|
||||
}
|
||||
.olDrawBox {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.olControlDragFeatureOver {
|
||||
cursor: move;
|
||||
}
|
||||
.olControlDragFeatureActive.olControlDragFeatureOver.olDragDown {
|
||||
cursor: -moz-grabbing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layer switcher
|
||||
*/
|
||||
.olControlLayerSwitcher {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
right: 0px;
|
||||
width: 20em;
|
||||
font-family: sans-serif;
|
||||
font-weight: bold;
|
||||
margin-top: 3px;
|
||||
margin-left: 3px;
|
||||
margin-bottom: 3px;
|
||||
font-size: smaller;
|
||||
color: white;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.olControlLayerSwitcher .layersDiv {
|
||||
padding-top: 5px;
|
||||
padding-left: 10px;
|
||||
padding-bottom: 5px;
|
||||
padding-right: 75px;
|
||||
background-color: darkblue;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.olControlLayerSwitcher .layersDiv .baseLbl,
|
||||
.olControlLayerSwitcher .layersDiv .dataLbl {
|
||||
margin-top: 3px;
|
||||
margin-left: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.olControlLayerSwitcher .layersDiv .baseLayersDiv,
|
||||
.olControlLayerSwitcher .layersDiv .dataLayersDiv {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.olControlLayerSwitcher .maximizeDiv,
|
||||
.olControlLayerSwitcher .minimizeDiv {
|
||||
top: 5px;
|
||||
right: 0px;
|
||||
}
|
1
plugins/Mapstraction/usermap-mxn-openlayers.min.js
vendored
Normal file
@ -78,20 +78,28 @@ class PiwikAnalyticsPlugin extends Plugin
|
||||
*/
|
||||
function onEndShowScripts($action)
|
||||
{
|
||||
$piwikCode1 = <<<ENDOFPIWIK
|
||||
var pkBaseURL = (("https:" == document.location.protocol) ? "https://{$this->piwikroot}" : "http://{$this->piwikroot}");
|
||||
document.write(unescape("%3Cscript src='" + pkBaseURL + "piwik.js' type='text/javascript'%3E%3C/script%3E"));
|
||||
ENDOFPIWIK;
|
||||
$piwikCode2 = <<<ENDOFPIWIK
|
||||
// Slight modification to the default code.
|
||||
// Loading the piwik.js file from a <script> created in a document.write
|
||||
// meant that the browser had no way to preload it, ensuring that its
|
||||
// loading will be synchronous, blocking further page rendering.
|
||||
//
|
||||
// User-agents understand protocol-relative links, so instead of the
|
||||
// URL produced in JS we can just give a universal one. Since it's
|
||||
// sitting there in the DOM ready to go, the browser can preload the
|
||||
// file for us and we're less likely to have to wait for it.
|
||||
$piwikUrl = '//' . $this->piwikroot . 'piwik.js';
|
||||
$piwikCode = <<<ENDOFPIWIK
|
||||
try {
|
||||
var pkBaseURL = (("https:" == document.location.protocol) ? "https://{$this->piwikroot}" : "http://{$this->piwikroot}");
|
||||
var piwikTracker = Piwik.getTracker(pkBaseURL + "piwik.php", {$this->piwikId});
|
||||
piwikTracker.trackPageView();
|
||||
piwikTracker.enableLinkTracking();
|
||||
} catch( err ) {}
|
||||
ENDOFPIWIK;
|
||||
|
||||
$action->inlineScript($piwikCode1);
|
||||
$action->inlineScript($piwikCode2);
|
||||
// Don't use $action->script() here; it'll try to preface the URL.
|
||||
$action->element('script', array('type' => 'text/javascript', 'src' => $piwikUrl), ' ');
|
||||
$action->inlineScript($piwikCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ class TwitterQueueHandler extends QueueHandler
|
||||
|
||||
function handle($notice)
|
||||
{
|
||||
return broadcast_twitter($notice);
|
||||
$ok = broadcast_twitter($notice);
|
||||
return $ok || common_config('twitter', 'ignore_errors');
|
||||
}
|
||||
}
|
||||
|
381
tests/atompub/atompub_test.php
Normal file
@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/*
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..'));
|
||||
|
||||
$shortoptions = 'n:p:';
|
||||
$longoptions = array('nickname=', 'password=', 'dry-run');
|
||||
|
||||
$helptext = <<<END_OF_HELP
|
||||
USAGE: atompub_test.php [options]
|
||||
|
||||
Runs some tests on the AtomPub interface for the site. You must provide
|
||||
a user account to authenticate as; it will be used to make some test
|
||||
posts on the site.
|
||||
|
||||
Options:
|
||||
-n<user> --nickname=<user> Nickname of account to post as
|
||||
-p<pass> --password=<pass> Password for account
|
||||
--dry-run Skip tests that modify the site (post, delete)
|
||||
|
||||
END_OF_HELP;
|
||||
|
||||
require_once INSTALLDIR.'/scripts/commandline.inc';
|
||||
|
||||
class AtomPubClient
|
||||
{
|
||||
public $url;
|
||||
private $user, $pass;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $url collection feed URL
|
||||
* @param string $user auth username
|
||||
* @param string $pass auth password
|
||||
*/
|
||||
function __construct($url, $user, $pass)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->user = $user;
|
||||
$this->pass = $pass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up an HTTPClient with auth for our resource.
|
||||
*
|
||||
* @param string $method
|
||||
* @return HTTPClient
|
||||
*/
|
||||
private function httpClient($method='GET')
|
||||
{
|
||||
$client = new HTTPClient($this->url);
|
||||
$client->setMethod($method);
|
||||
$client->setAuth($this->user, $this->pass);
|
||||
return $client;
|
||||
}
|
||||
|
||||
function get()
|
||||
{
|
||||
$client = $this->httpClient('GET');
|
||||
$response = $client->send();
|
||||
if ($response->isOk()) {
|
||||
return $response->getBody();
|
||||
} else {
|
||||
throw new Exception("Bogus return code: " . $response->getStatus() . ': ' . $response->getBody());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new resource by POSTing it to the collection.
|
||||
* If successful, will return the URL representing the
|
||||
* canonical location of the new resource. Neat!
|
||||
*
|
||||
* @param string $data
|
||||
* @param string $type defaults to Atom entry
|
||||
* @return string URL to the created resource
|
||||
*
|
||||
* @throws exceptions on failure
|
||||
*/
|
||||
function post($data, $type='application/atom+xml;type=entry')
|
||||
{
|
||||
$client = $this->httpClient('POST');
|
||||
$client->setHeader('Content-Type', $type);
|
||||
// optional Slug header not used in this case
|
||||
$client->setBody($data);
|
||||
$response = $client->send();
|
||||
|
||||
if ($response->getStatus() != '201') {
|
||||
throw new Exception("Expected HTTP 201 on POST, got " . $response->getStatus() . ': ' . $response->getBody());
|
||||
}
|
||||
$loc = $response->getHeader('Location');
|
||||
$contentLoc = $response->getHeader('Content-Location');
|
||||
|
||||
if (empty($loc)) {
|
||||
throw new Exception("AtomPub POST response missing Location header.");
|
||||
}
|
||||
if (!empty($contentLoc)) {
|
||||
if ($loc != $contentLoc) {
|
||||
throw new Exception("AtomPub POST response Location and Content-Location headers do not match.");
|
||||
}
|
||||
|
||||
// If Content-Location and Location match, that means the response
|
||||
// body is safe to interpret as the resource itself.
|
||||
if ($type == 'application/atom+xml;type=entry') {
|
||||
self::validateAtomEntry($response->getBody());
|
||||
}
|
||||
}
|
||||
|
||||
return $loc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that StatusNet currently doesn't allow PUT editing on notices.
|
||||
*
|
||||
* @param string $data
|
||||
* @param string $type defaults to Atom entry
|
||||
* @return true on success
|
||||
*
|
||||
* @throws exceptions on failure
|
||||
*/
|
||||
function put($data, $type='application/atom+xml;type=entry')
|
||||
{
|
||||
$client = $this->httpClient('PUT');
|
||||
$client->setHeader('Content-Type', $type);
|
||||
$client->setBody($data);
|
||||
$response = $client->send();
|
||||
|
||||
if ($response->getStatus() != '200' && $response->getStatus() != '204') {
|
||||
throw new Exception("Expected HTTP 200 or 204 on PUT, got " . $response->getStatus() . ': ' . $response->getBody());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the resource.
|
||||
*
|
||||
* @return true on success
|
||||
*
|
||||
* @throws exceptions on failure
|
||||
*/
|
||||
function delete()
|
||||
{
|
||||
$client = $this->httpClient('DELETE');
|
||||
$client->setBody($data);
|
||||
$response = $client->send();
|
||||
|
||||
if ($response->getStatus() != '200' && $response->getStatus() != '204') {
|
||||
throw new Exception("Expected HTTP 200 or 204 on DELETE, got " . $response->getStatus() . ': ' . $response->getBody());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the given string is a parseable Atom entry.
|
||||
*
|
||||
* @param string $str
|
||||
* @return boolean
|
||||
* @throws Exception on invalid input
|
||||
*/
|
||||
static function validateAtomEntry($str)
|
||||
{
|
||||
if (empty($str)) {
|
||||
throw new Exception('Bad Atom entry: empty');
|
||||
}
|
||||
$dom = new DOMDocument;
|
||||
if (!$dom->loadXML($str)) {
|
||||
throw new Exception('Bad Atom entry: XML is not well formed.');
|
||||
}
|
||||
|
||||
$activity = new Activity($dom->documentRoot);
|
||||
return true;
|
||||
}
|
||||
|
||||
static function entryEditURL($str) {
|
||||
$dom = new DOMDocument;
|
||||
$dom->loadXML($str);
|
||||
$path = new DOMXPath($dom);
|
||||
$path->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
|
||||
|
||||
$links = $path->query('/atom:entry/atom:link[@rel="edit"]', $dom->documentRoot);
|
||||
if ($links && $links->length) {
|
||||
if ($links->length > 1) {
|
||||
throw new Exception('Bad Atom entry; has multiple rel=edit links.');
|
||||
}
|
||||
$link = $links->item(0);
|
||||
$url = $link->getAttribute('href');
|
||||
return $url;
|
||||
} else {
|
||||
throw new Exception('Atom entry lists no rel=edit link.');
|
||||
}
|
||||
}
|
||||
|
||||
static function entryId($str) {
|
||||
$dom = new DOMDocument;
|
||||
$dom->loadXML($str);
|
||||
$path = new DOMXPath($dom);
|
||||
$path->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
|
||||
|
||||
$links = $path->query('/atom:entry/atom:id', $dom->documentRoot);
|
||||
if ($links && $links->length) {
|
||||
if ($links->length > 1) {
|
||||
throw new Exception('Bad Atom entry; has multiple id entries.');
|
||||
}
|
||||
$link = $links->item(0);
|
||||
$url = $link->textContent;
|
||||
return $url;
|
||||
} else {
|
||||
throw new Exception('Atom entry lists no id.');
|
||||
}
|
||||
}
|
||||
|
||||
static function getEntryInFeed($str, $id)
|
||||
{
|
||||
$dom = new DOMDocument;
|
||||
$dom->loadXML($str);
|
||||
$path = new DOMXPath($dom);
|
||||
$path->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
|
||||
|
||||
$query = '/atom:feed/atom:entry[atom:id="'.$id.'"]';
|
||||
$items = $path->query($query, $dom->documentRoot);
|
||||
if ($items && $items->length) {
|
||||
return $items->item(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$user = get_option_value('n', 'nickname');
|
||||
$pass = get_option_value('p', 'password');
|
||||
|
||||
if (!$user) {
|
||||
die("Must set a user: --nickname=<username>\n");
|
||||
}
|
||||
if (!$pass) {
|
||||
die("Must set a password: --password=<username>\n");
|
||||
}
|
||||
|
||||
// discover the feed...
|
||||
// @fixme will this actually work?
|
||||
$url = common_local_url('ApiTimelineUser', array('format' => 'atom', 'id' => $user));
|
||||
|
||||
echo "Collection URL is: $url\n";
|
||||
|
||||
$collection = new AtomPubClient($url, $user, $pass);
|
||||
|
||||
// confirm the feed has edit links ..... ?
|
||||
|
||||
echo "Posting an empty message (should fail)... ";
|
||||
try {
|
||||
$noticeUrl = $collection->post('');
|
||||
die("FAILED, succeeded!\n");
|
||||
} catch (Exception $e) {
|
||||
echo "ok\n";
|
||||
}
|
||||
|
||||
echo "Posting an invalid XML message (should fail)... ";
|
||||
try {
|
||||
$noticeUrl = $collection->post('<feed<entry>barf</yomomma>');
|
||||
die("FAILED, succeeded!\n");
|
||||
} catch (Exception $e) {
|
||||
echo "ok\n";
|
||||
}
|
||||
|
||||
echo "Posting a valid XML but non-Atom message (should fail)... ";
|
||||
try {
|
||||
$noticeUrl = $collection->post('<feed xmlns="http://notatom.com"><id>arf</id><entry><id>barf</id></entry></feed>');
|
||||
die("FAILED, succeeded!\n");
|
||||
} catch (Exception $e) {
|
||||
echo "ok\n";
|
||||
}
|
||||
|
||||
// post!
|
||||
$rand = mt_rand(0, 99999);
|
||||
$atom = <<<END_ATOM
|
||||
<entry xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>This is an AtomPub test post title ($rand)</title>
|
||||
<content>This is an AtomPub test post content ($rand)</content>
|
||||
</entry>
|
||||
END_ATOM;
|
||||
|
||||
echo "Posting a new message... ";
|
||||
$noticeUrl = $collection->post($atom);
|
||||
echo "ok, got $noticeUrl\n";
|
||||
|
||||
echo "Fetching the new notice... ";
|
||||
$notice = new AtomPubClient($noticeUrl, $user, $pass);
|
||||
$body = $notice->get();
|
||||
AtomPubClient::validateAtomEntry($body);
|
||||
echo "ok\n";
|
||||
|
||||
echo "Getting the notice ID URI... ";
|
||||
$noticeUri = AtomPubClient::entryId($body);
|
||||
echo "ok: $noticeUri\n";
|
||||
|
||||
echo "Confirming new entry points to itself right... ";
|
||||
$editUrl = AtomPubClient::entryEditURL($body);
|
||||
if ($editUrl != $noticeUrl) {
|
||||
die("Entry lists edit URL as $editUrl, no match!\n");
|
||||
}
|
||||
echo "OK\n";
|
||||
|
||||
echo "Refetching the collection... ";
|
||||
$feed = $collection->get();
|
||||
echo "ok\n";
|
||||
|
||||
echo "Confirming new entry is in the feed... ";
|
||||
$entry = AtomPubClient::getEntryInFeed($feed, $noticeUri);
|
||||
if (!$entry) {
|
||||
die("missing!\n");
|
||||
}
|
||||
// edit URL should match
|
||||
echo "ok\n";
|
||||
|
||||
echo "Editing notice (should fail)... ";
|
||||
try {
|
||||
$notice->put($target, $atom2);
|
||||
die("ERROR: editing a notice should have failed.\n");
|
||||
} catch (Exception $e) {
|
||||
echo "ok (failed as expected)\n";
|
||||
}
|
||||
|
||||
echo "Deleting notice... ";
|
||||
$notice->delete();
|
||||
echo "ok\n";
|
||||
|
||||
echo "Refetching deleted notice to confirm it's gone... ";
|
||||
try {
|
||||
$body = $notice->get();
|
||||
var_dump($body);
|
||||
die("ERROR: notice should be gone now.\n");
|
||||
} catch (Exception $e) {
|
||||
echo "ok\n";
|
||||
}
|
||||
|
||||
echo "Refetching the collection.. ";
|
||||
$feed = $collection->get();
|
||||
echo "ok\n";
|
||||
|
||||
echo "Confirming deleted notice is no longer in the feed... ";
|
||||
$entry = AtomPubClient::getEntryInFeed($feed, $noticeUri);
|
||||
if ($entry) {
|
||||
die("still there!\n");
|
||||
}
|
||||
echo "ok\n";
|
||||
|
||||
// make subscriptions
|
||||
// make some posts
|
||||
// make sure the posts go through or not depending on the subs
|
||||
// remove subscriptions
|
||||
// test that they don't go through now
|
||||
|
||||
// group memberships too
|
||||
|
||||
|
||||
|
||||
|
||||
// make sure we can't post to someone else's feed!
|
||||
// make sure we can't delete someone else's messages
|
||||
// make sure we can't create/delete someone else's subscriptions
|
||||
// make sure we can't create/delete someone else's group memberships
|
||||
|