Merge branch '0.9.x' into 1.0.x

This commit is contained in:
Brion Vibber 2010-12-15 11:59:31 -08:00
commit bf20258f4b
97 changed files with 3547 additions and 282 deletions

View File

@ -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');

View File

@ -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');
}
/**

View File

@ -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);
}

View 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
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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()

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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.

View File

@ -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 &amp; instead of &. Encoded &s in parameters will not be
// affected.
$url = substr($url, 0, $qpos+1) .
str_replace('&amp;', '&', substr($url, $qpos+1));
str_replace('&amp;', '&', substr($url, $qpos+1));
}

View File

@ -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');
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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;
}
}

View 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

View File

@ -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));

View 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)

File diff suppressed because one or more lines are too long

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

View 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]

View File

@ -0,0 +1,10 @@
.olLayerGoogleCopyright {
right: 3px;
bottom: 2px;
left: auto;
}
.olLayerGooglePoweredBy {
left: 2px;
bottom: 2px;
}

View File

@ -0,0 +1,7 @@
.olControlZoomPanel div {
background-image: url(img/zoom-panel-NOALPHA.png);
}
.olControlPanPanel div {
background-image: url(img/pan-panel-NOALPHA.png);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View 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;
}

File diff suppressed because one or more lines are too long

View 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;
}

View File

@ -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');
}
}

View 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