Merge branch '1.0.x' of gitorious.org:statusnet/mainline into 1.0.x

This commit is contained in:
Evan Prodromou
2011-08-05 10:42:42 -04:00
1452 changed files with 26738 additions and 16989 deletions

View File

@@ -135,6 +135,9 @@ class Activity
} else if ($entry->namespaceURI == Activity::RSS &&
$entry->localName == 'item') {
$this->_fromRssItem($entry, $feed);
} else if ($entry->namespaceURI == Activity::SPEC &&
$entry->localName == 'object') {
$this->_fromAtomEntry($entry, $feed);
} else {
// Low level exception. No need for i18n.
throw new Exception("Unknown DOM element: {$entry->namespaceURI} {$entry->localName}");
@@ -168,14 +171,22 @@ class Activity
// XXX: do other implied stuff here
}
$objectEls = $entry->getElementsByTagNameNS(self::SPEC, self::OBJECT);
// get immediate object children
if ($objectEls->length > 0) {
for ($i = 0; $i < $objectEls->length; $i++) {
$objectEl = $objectEls->item($i);
$this->objects[] = new ActivityObject($objectEl);
$objectEls = ActivityUtils::children($entry, self::OBJECT, self::SPEC);
if (count($objectEls) > 0) {
foreach ($objectEls as $objectEl) {
// Special case for embedded activities
$objectType = ActivityUtils::childContent($objectEl, self::OBJECTTYPE, self::SPEC);
if (!empty($objectType) && $objectType == ActivityObject::ACTIVITY) {
$this->objects[] = new Activity($objectEl);
} else {
$this->objects[] = new ActivityObject($objectEl);
}
}
} else {
// XXX: really?
$this->objects[] = new ActivityObject($entry);
}
@@ -431,7 +442,13 @@ class Activity
} else {
$activity['object'] = array();
foreach($this->objects as $object) {
$activity['object'][] = $object->asArray();
$oa = $object->asArray();
if ($object instanceof Activity) {
// throw in a type
// XXX: hackety-hack
$oa['objectType'] = 'activity';
}
$activity['object'][] = $oa;
}
}
@@ -495,7 +512,7 @@ class Activity
return $xs->getString();
}
function outputTo($xs, $namespace=false, $author=true, $source=false)
function outputTo($xs, $namespace=false, $author=true, $source=false, $tag='entry')
{
if ($namespace) {
$attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
@@ -510,9 +527,13 @@ class Activity
$attrs = array();
}
$xs->elementStart('entry', $attrs);
$xs->elementStart($tag, $attrs);
if ($this->verb == ActivityVerb::POST && count($this->objects) == 1) {
if ($tag != 'entry') {
$xs->element('activity:object-type', null, ActivityObject::ACTIVITY);
}
if ($this->verb == ActivityVerb::POST && count($this->objects) == 1 && $tag == 'entry') {
$obj = $this->objects[0];
$obj->outputTo($xs, null);
@@ -558,9 +579,13 @@ class Activity
$this->actor->outputTo($xs, 'activity:actor');
}
if ($this->verb != ActivityVerb::POST || count($this->objects) != 1) {
if ($this->verb != ActivityVerb::POST || count($this->objects) != 1 || $tag != 'entry') {
foreach($this->objects as $object) {
$object->outputTo($xs, 'activity:object');
if ($object instanceof Activity) {
$object->outputTo($xs, false, true, true, 'activity:object');
} else {
$object->outputTo($xs, 'activity:object');
}
}
}
@@ -694,7 +719,7 @@ class Activity
$xs->element($tag, $attrs, $content);
}
$xs->elementEnd('entry');
$xs->elementEnd($tag);
return;
}

View File

@@ -68,6 +68,7 @@ class ActivityObject
const PLACE = 'http://activitystrea.ms/schema/1.0/place';
const COMMENT = 'http://activitystrea.ms/schema/1.0/comment';
// ^^^^^^^^^^ tea!
const ACTIVITY = 'http://activitystrea.ms/schema/1.0/activity';
// Atom elements we snarf

View File

@@ -145,6 +145,34 @@ class ActivityUtils
}
}
/**
* Gets all immediate child elements with the given tag
*
* @param DOMElement $element element to pick at
* @param string $tag tag to look for
* @param string $namespace Namespace to look under
*
* @return array found element or null
*/
static function children(DOMNode $element, $tag, $namespace=self::ATOM)
{
$results = array();
$els = $element->childNodes;
if (!empty($els) && $els->length > 0) {
for ($i = 0; $i < $els->length; $i++) {
$el = $els->item($i);
if ($el->localName == $tag && $el->namespaceURI == $namespace) {
$results[] = $el;
}
}
}
return $results;
}
/**
* Grab the text content of a DOM element child of the current element
*

251
lib/atompubclient.php Normal file
View File

@@ -0,0 +1,251 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2011, StatusNet, Inc.
*
* Client class for AtomPub
*
* 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 2011 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);
}
/**
* Client class for AtomPub
*
* @category General
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
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;
}
}
}

View File

@@ -76,7 +76,7 @@ class AttachmentList extends Widget
*/
function show()
{
$att = File::getAttachments($this->notice->id);
$att = $this->notice->attachments();
if (empty($att)) return 0;
$this->showListStart();

View File

@@ -58,7 +58,7 @@ $default =
'sslserver' => null,
'shorturllength' => 30,
'dupelimit' => 60, // default for same person saying the same thing
'textlimit' => 140,
'textlimit' => 0, // in chars; 0 == no limit
'indent' => true,
'use_x_sendfile' => false,
'notice' => null, // site wide notice text

View File

@@ -81,9 +81,16 @@ abstract class FilteringNoticeStream extends NoticeStream
break;
}
while ($raw->fetch()) {
if ($this->filter($raw)) {
$filtered[] = clone($raw);
$notices = $raw->fetchAll();
// XXX: this should probably only be in the scoping one.
Notice::fillGroups($notices);
Notice::fillReplies($notices);
foreach ($notices as $notice) {
if ($this->filter($notice)) {
$filtered[] = $notice;
if (count($filtered) >= $total) {
break;
}

View File

@@ -20,7 +20,7 @@
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
define('STATUSNET_BASE_VERSION', '1.0.0');
define('STATUSNET_LIFECYCLE', 'beta1'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
define('STATUSNET_LIFECYCLE', 'beta2'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
define('STATUSNET_VERSION', STATUSNET_BASE_VERSION . STATUSNET_LIFECYCLE);
define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
@@ -156,4 +156,5 @@ function PEAR_ErrorToPEAR_Exception($err)
}
throw new PEAR_Exception($err->getMessage());
}
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'PEAR_ErrorToPEAR_Exception');

View File

@@ -193,6 +193,7 @@ abstract class MicroAppPlugin extends Plugin
function isMyActivity($activity) {
$types = $this->types();
return (count($activity->objects) == 1 &&
($activity->objects[0] instanceof ActivityObject) &&
in_array($activity->objects[0]->type, $types));
}
@@ -345,7 +346,7 @@ abstract class MicroAppPlugin extends Plugin
*
* @return boolean hook value
*/
function onStartHandleFeedEntryWithProfile($activity, $oprofile)
function onStartHandleFeedEntryWithProfile($activity, $oprofile, &$notice)
{
if ($this->isMyActivity($activity)) {
@@ -364,7 +365,7 @@ abstract class MicroAppPlugin extends Plugin
'source' => 'ostatus');
// $actor is an ostatus_profile
$this->saveNoticeFromActivity($activity, $actor->localProfile(), $options);
$notice = $this->saveNoticeFromActivity($activity, $actor->localProfile(), $options);
return false;
}
@@ -446,9 +447,9 @@ abstract class MicroAppPlugin extends Plugin
$options = array('source' => 'atompub');
// $user->getProfile() is a Profile
$this->saveNoticeFromActivity($activity,
$user->getProfile(),
$options);
$notice = $this->saveNoticeFromActivity($activity,
$user->getProfile(),
$options);
return false;
}

View File

@@ -83,17 +83,16 @@ class NoticeList extends Widget
$this->out->elementStart('div', array('id' =>'notices_primary'));
$this->out->elementStart('ol', array('class' => 'notices xoxo'));
$cnt = 0;
while ($this->notice->fetch() && $cnt <= NOTICES_PER_PAGE) {
$cnt++;
if ($cnt > NOTICES_PER_PAGE) {
break;
}
$notices = $this->notice->fetchAll();
$total = count($notices);
$notices = array_slice($notices, 0, NOTICES_PER_PAGE);
self::prefill($notices);
foreach ($notices as $notice) {
try {
$item = $this->newListItem($this->notice);
$item = $this->newListItem($notice);
$item->show();
} catch (Exception $e) {
// we log exceptions and continue
@@ -105,7 +104,7 @@ class NoticeList extends Widget
$this->out->elementEnd('ol');
$this->out->elementEnd('div');
return $cnt;
return $total;
}
/**
@@ -122,4 +121,28 @@ class NoticeList extends Widget
{
return new NoticeListItem($notice, $this->out);
}
static function prefill(&$notices, $avatarSize=AVATAR_STREAM_SIZE)
{
// Prefill attachments
Notice::fillAttachments($notices);
// Prefill attachments
Notice::fillFaves($notices);
// Prefill the profiles
$profiles = Notice::fillProfiles($notices);
// Prefill the avatars
Profile::fillAvatars($profiles, $avatarSize);
$p = Profile::current();
$ids = array();
foreach ($notices as $notice) {
$ids[] = $notice->id;
}
if (!empty($p)) {
Memcached_DataObject::pivotGet('Fave', 'notice_id', $ids, array('user_id' => $p->id));
}
}
}

View File

@@ -76,17 +76,18 @@ class ThreadedNoticeList extends NoticeList
$this->out->element('h2', null, _m('HEADER','Notices'));
$this->out->elementStart('ol', array('class' => 'notices threaded-notices xoxo'));
$cnt = 0;
$notices = $this->notice->fetchAll();
$total = count($notices);
$notices = array_slice($notices, 0, NOTICES_PER_PAGE);
self::prefill($notices);
$conversations = array();
while ($this->notice->fetch() && $cnt <= NOTICES_PER_PAGE) {
$cnt++;
if ($cnt > NOTICES_PER_PAGE) {
break;
}
foreach ($notices as $notice) {
// Collapse repeats into their originals...
$notice = $this->notice;
if ($notice->repeat_of) {
$orig = Notice::staticGet('id', $notice->repeat_of);
if ($orig) {
@@ -119,7 +120,7 @@ class ThreadedNoticeList extends NoticeList
$this->out->elementEnd('ol');
$this->out->elementEnd('div');
return $cnt;
return $total;
}
/**
@@ -223,6 +224,7 @@ class ThreadedNoticeListItem extends NoticeListItem
$item = new ThreadedNoticeListMoreItem($moreCutoff, $this->out, count($notices));
$item->show();
}
NoticeList::prefill($notices, AVATAR_MINI_SIZE);
foreach (array_reverse($notices) as $notice) {
if (Event::handle('StartShowThreadedNoticeSub', array($this, $this->notice, $notice))) {
$item = new ThreadedNoticeListSubItem($notice, $this->notice, $this->out);
@@ -468,9 +470,9 @@ class ThreadedNoticeListFavesItem extends NoticeListActorsItem
{
function getProfiles()
{
$fave = Fave::byNotice($this->notice->id);
$faves = $this->notice->getFaves();
$profiles = array();
while ($fave->fetch()) {
foreach ($faves as $fave) {
$profiles[] = $fave->user_id;
}
return $profiles;

View File

@@ -1127,8 +1127,11 @@ function common_tag_link($tag)
function common_canonical_tag($tag)
{
// only alphanum
$tag = preg_replace('/[^\pL\pN]/u', '', $tag);
$tag = mb_convert_case($tag, MB_CASE_LOWER, "UTF-8");
return str_replace(array('-', '_', '.'), '', $tag);
$tag = substr($tag, 0, 64);
return $tag;
}
function common_valid_profile_tag($str)
@@ -1501,16 +1504,18 @@ function common_enqueue_notice($notice)
}
/**
* Broadcast profile updates to remote subscribers.
* Legacy function to broadcast profile updates to OMB remote subscribers.
*
* XXX: This probably needs killing, but there are several bits of code
* that broadcast profile changes that need to be dealt with. AFAIK
* this function is only used for OMB. -z
*
* Since this may be slow with a lot of subscribers or bad remote sites,
* this is run through the background queues if possible.
*/
function common_broadcast_profile(Profile $profile)
{
$qm = QueueManager::get();
$qm->enqueue($profile, "profile");
return true;
Event::handle('BroadcastProfile', array($profile));
}
function common_profile_url($nickname)