Merge branch '0.9.x' into 1.0.x

Conflicts:
	lib/queuemanager.php
	lib/xmppmanager.php
	plugins/Xmpp/Fake_XMPP.php
	scripts/imdaemon.php
This commit is contained in:
Craig Andrews 2010-02-16 13:15:09 -05:00
commit 20d6a7caed
55 changed files with 1719 additions and 543 deletions

View File

@ -1,4 +1,4 @@
InitializePlugin: a chance to initialize a plugin in a complete environment \InitializePlugin: a chance to initialize a plugin in a complete environment
CleanupPlugin: a chance to cleanup a plugin at the end of a program CleanupPlugin: a chance to cleanup a plugin at the end of a program
@ -355,6 +355,14 @@ EndShowHeadElements: Right before the </head> tag; put <script>s here if you nee
CheckSchema: chance to check the schema CheckSchema: chance to check the schema
StartProfileRemoteSubscribe: Before showing the link to remote subscription
- $userprofile: UserProfile widget
- &$profile: the profile being shown
EndProfileRemoteSubscribe: After showing the link to remote subscription
- $userprofile: UserProfile widget
- &$profile: the profile being shown
StartProfilePageProfileSection: Starting to show the section of the StartProfilePageProfileSection: Starting to show the section of the
profile page with the actual profile data; profile page with the actual profile data;
hook to prevent showing the profile (e.g.) hook to prevent showing the profile (e.g.)

19
README
View File

@ -1192,6 +1192,8 @@ server: If set, defines another server where avatars are stored in the
typically only make 2 connections to a single server at a typically only make 2 connections to a single server at a
time <http://ur1.ca/6ih>, so this can parallelize the job. time <http://ur1.ca/6ih>, so this can parallelize the job.
Defaults to null. Defaults to null.
ssl: Whether to access avatars using HTTPS. Defaults to null, meaning
to guess based on site-wide SSL settings.
public public
------ ------
@ -1221,6 +1223,19 @@ path: Path part of theme URLs, before the theme name. Relative to the
(using version numbers as the path) to make sure that all files are (using version numbers as the path) to make sure that all files are
reloaded by caching clients or proxies. Defaults to null, reloaded by caching clients or proxies. Defaults to null,
which means to use the site path + '/theme'. which means to use the site path + '/theme'.
ssl: Whether to use SSL for theme elements. Default is null, which means
guess based on site SSL settings.
javascript
----------
server: You can speed up page loading by pointing the
theme file lookup to another server (virtual or real).
Defaults to NULL, meaning to use the site server.
path: Path part of Javascript URLs. Defaults to null,
which means to use the site path + '/js/'.
ssl: Whether to use SSL for JavaScript files. Default is null, which means
guess based on site SSL settings.
xmpp xmpp
---- ----
@ -1447,6 +1462,8 @@ server: server name to use when creating URLs for uploaded files.
a virtual server here can speed up Web performance. a virtual server here can speed up Web performance.
path: URL path, relative to the server, to find files. Defaults to path: URL path, relative to the server, to find files. Defaults to
main path + '/file/'. main path + '/file/'.
ssl: whether to use HTTPS for file URLs. Defaults to null, meaning to
guess based on other SSL settings.
filecommand: command to use for determining the type of a file. May be filecommand: command to use for determining the type of a file. May be
skipped if fileinfo extension is installed. Defaults to skipped if fileinfo extension is installed. Defaults to
'/usr/bin/file'. '/usr/bin/file'.
@ -1506,6 +1523,8 @@ dir: directory to write backgrounds too. Default is '/background/'
subdir of install dir. subdir of install dir.
path: path to backgrounds. Default is sub-path of install path; note path: path to backgrounds. Default is sub-path of install path; note
that you may need to change this if you change site-path too. that you may need to change this if you change site-path too.
ssl: Whether or not to use HTTPS for background files. Defaults to
null, meaning to guess from site-wide SSL settings.
ping ping
---- ----

View File

@ -138,7 +138,19 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction
try { try {
$atom = new AtomNoticeFeed(); // If this was called using an integer ID, i.e.: using the canonical
// URL for this group's feed, then pass the Group object into the feed,
// so the OStatus plugin, and possibly other plugins, can access it.
// Feels sorta hacky. -- Z
$atom = null;
$id = $this->arg('id');
if (strval(intval($id)) === strval($id)) {
$atom = new AtomGroupNoticeFeed($this->group);
} else {
$atom = new AtomGroupNoticeFeed();
}
$atom->setId($id); $atom->setId($id);
$atom->setTitle($title); $atom->setTitle($title);

View File

@ -148,7 +148,19 @@ class ApiTimelineUserAction extends ApiBareAuthAction
header('Content-Type: application/atom+xml; charset=utf-8'); header('Content-Type: application/atom+xml; charset=utf-8');
$atom = new AtomNoticeFeed(); // If this was called using an integer ID, i.e.: using the canonical
// URL for this user's feed, then pass the User object into the feed,
// so the OStatus plugin, and possibly other plugins, can access it.
// Feels sorta hacky. -- Z
$atom = null;
$id = $this->arg('id');
if (strval(intval($id)) === strval($id)) {
$atom = new AtomUserNoticeFeed($this->user);
} else {
$atom = new AtomUserNoticeFeed();
}
$atom->setId($id); $atom->setId($id);
$atom->setTitle($title); $atom->setTitle($title);

View File

@ -82,9 +82,20 @@ class Avatar extends Memcached_DataObject
$server = common_config('site', 'server'); $server = common_config('site', 'server');
} }
// XXX: protocol $ssl = common_config('avatar', 'ssl');
return 'http://'.$server.$path.$filename; if (is_null($ssl)) { // null -> guess
if (common_config('site', 'ssl') == 'always' &&
!common_config('avatar', 'server')) {
$ssl = true;
} else {
$ssl = false;
}
}
$protocol = ($ssl) ? 'https' : 'http';
return $protocol.'://'.$server.$path.$filename;
} }
function displayUrl() function displayUrl()

View File

@ -155,9 +155,20 @@ class Design extends Memcached_DataObject
$server = common_config('site', 'server'); $server = common_config('site', 'server');
} }
// XXX: protocol $ssl = common_config('background', 'ssl');
return 'http://'.$server.$path.$filename; if (is_null($ssl)) { // null -> guess
if (common_config('site', 'ssl') == 'always' &&
!common_config('background', 'server')) {
$ssl = true;
} else {
$ssl = false;
}
}
$protocol = ($ssl) ? 'https' : 'http';
return $protocol.'://'.$server.$path.$filename;
} }
function setDisposition($on, $off, $tile) function setDisposition($on, $off, $tile)

View File

@ -228,9 +228,20 @@ class File extends Memcached_DataObject
$server = common_config('site', 'server'); $server = common_config('site', 'server');
} }
// XXX: protocol $ssl = common_config('attachments', 'ssl');
return 'http://'.$server.$path.$filename; if (is_null($ssl)) { // null -> guess
if (common_config('site', 'ssl') == 'always' &&
!common_config('attachments', 'server')) {
$ssl = true;
} else {
$ssl = false;
}
}
$protocol = ($ssl) ? 'https' : 'http';
return $protocol.'://'.$server.$path.$filename;
} }
} }

View File

@ -19,57 +19,8 @@
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
class Memcached_DataObject extends DB_DataObject class Memcached_DataObject extends Safe_DataObject
{ {
/**
* Destructor to free global memory resources associated with
* this data object when it's unset or goes out of scope.
* DB_DataObject doesn't do this yet by itself.
*/
function __destruct()
{
$this->free();
if (method_exists('DB_DataObject', '__destruct')) {
parent::__destruct();
}
}
/**
* Magic function called at serialize() time.
*
* We use this to drop a couple process-specific references
* from DB_DataObject which can cause trouble in future
* processes.
*
* @return array of variable names to include in serialization.
*/
function __sleep()
{
$vars = array_keys(get_object_vars($this));
$skip = array('_DB_resultid', '_link_loaded');
return array_diff($vars, $skip);
}
/**
* Magic function called at unserialize() time.
*
* Clean out some process-specific variables which might
* be floating around from a previous process's cached
* objects.
*
* Old cached objects may still have them.
*/
function __wakeup()
{
// Refers to global state info from a previous process.
// Clear this out so we don't accidentally break global
// state in *this* process.
$this->_DB_resultid = null;
// We don't have any local DBO refs, so clear these out.
$this->_link_loaded = false;
}
/** /**
* Wrapper for DB_DataObject's static lookup using memcached * Wrapper for DB_DataObject's static lookup using memcached
* as backing instead of an in-process cache array. * as backing instead of an in-process cache array.
@ -579,3 +530,4 @@ class Memcached_DataObject extends DB_DataObject
return $c->set($cacheKey, $value); return $c->set($cacheKey, $value);
} }
} }

250
classes/Safe_DataObject.php Normal file
View File

@ -0,0 +1,250 @@
<?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/>.
*/
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
/**
* Extended DB_DataObject to improve a few things:
* - free global resources from destructor
* - remove bogus global references from serialized objects
* - don't leak memory when loading already-used .ini files
* (eg when using the same schema on thousands of databases)
*/
class Safe_DataObject extends DB_DataObject
{
/**
* Destructor to free global memory resources associated with
* this data object when it's unset or goes out of scope.
* DB_DataObject doesn't do this yet by itself.
*/
function __destruct()
{
$this->free();
if (method_exists('DB_DataObject', '__destruct')) {
parent::__destruct();
}
}
/**
* Magic function called at serialize() time.
*
* We use this to drop a couple process-specific references
* from DB_DataObject which can cause trouble in future
* processes.
*
* @return array of variable names to include in serialization.
*/
function __sleep()
{
$vars = array_keys(get_object_vars($this));
$skip = array('_DB_resultid', '_link_loaded');
return array_diff($vars, $skip);
}
/**
* Magic function called at unserialize() time.
*
* Clean out some process-specific variables which might
* be floating around from a previous process's cached
* objects.
*
* Old cached objects may still have them.
*/
function __wakeup()
{
// Refers to global state info from a previous process.
// Clear this out so we don't accidentally break global
// state in *this* process.
$this->_DB_resultid = null;
// We don't have any local DBO refs, so clear these out.
$this->_link_loaded = false;
}
/**
* Work around memory-leak bugs...
* Had to copy-paste the whole function in order to patch a couple lines of it.
* Would be nice if this code was better factored.
*
* @param optional string name of database to assign / read
* @param optional array structure of database, and keys
* @param optional array table links
*
* @access public
* @return true or PEAR:error on wrong paramenters.. or false if no file exists..
* or the array(tablename => array(column_name=>type)) if called with 1 argument.. (databasename)
*/
function databaseStructure()
{
global $_DB_DATAOBJECT;
// Assignment code
if ($args = func_get_args()) {
if (count($args) == 1) {
// this returns all the tables and their structure..
if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
$this->debug("Loading Generator as databaseStructure called with args",1);
}
$x = new DB_DataObject;
$x->_database = $args[0];
$this->_connect();
$DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
$tables = $DB->getListOf('tables');
class_exists('DB_DataObject_Generator') ? '' :
require_once 'DB/DataObject/Generator.php';
foreach($tables as $table) {
$y = new DB_DataObject_Generator;
$y->fillTableSchema($x->_database,$table);
}
return $_DB_DATAOBJECT['INI'][$x->_database];
} else {
$_DB_DATAOBJECT['INI'][$args[0]] = isset($_DB_DATAOBJECT['INI'][$args[0]]) ?
$_DB_DATAOBJECT['INI'][$args[0]] + $args[1] : $args[1];
if (isset($args[1])) {
$_DB_DATAOBJECT['LINKS'][$args[0]] = isset($_DB_DATAOBJECT['LINKS'][$args[0]]) ?
$_DB_DATAOBJECT['LINKS'][$args[0]] + $args[2] : $args[2];
}
return true;
}
}
if (!$this->_database) {
$this->_connect();
}
// loaded already?
if (!empty($_DB_DATAOBJECT['INI'][$this->_database])) {
// database loaded - but this is table is not available..
if (
empty($_DB_DATAOBJECT['INI'][$this->_database][$this->__table])
&& !empty($_DB_DATAOBJECT['CONFIG']['proxy'])
) {
if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
$this->debug("Loading Generator to fetch Schema",1);
}
class_exists('DB_DataObject_Generator') ? '' :
require_once 'DB/DataObject/Generator.php';
$x = new DB_DataObject_Generator;
$x->fillTableSchema($this->_database,$this->__table);
}
return true;
}
if (empty($_DB_DATAOBJECT['CONFIG'])) {
DB_DataObject::_loadConfig();
}
// if you supply this with arguments, then it will take those
// as the database and links array...
$schemas = isset($_DB_DATAOBJECT['CONFIG']['schema_location']) ?
array("{$_DB_DATAOBJECT['CONFIG']['schema_location']}/{$this->_database}.ini") :
array() ;
if (isset($_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"])) {
$schemas = is_array($_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"]) ?
$_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"] :
explode(PATH_SEPARATOR,$_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"]);
}
/* BEGIN CHANGED FROM UPSTREAM */
$_DB_DATAOBJECT['INI'][$this->_database] = $this->parseIniFiles($schemas);
/* END CHANGED FROM UPSTREAM */
// now have we loaded the structure..
if (!empty($_DB_DATAOBJECT['INI'][$this->_database][$this->__table])) {
return true;
}
// - if not try building it..
if (!empty($_DB_DATAOBJECT['CONFIG']['proxy'])) {
class_exists('DB_DataObject_Generator') ? '' :
require_once 'DB/DataObject/Generator.php';
$x = new DB_DataObject_Generator;
$x->fillTableSchema($this->_database,$this->__table);
// should this fail!!!???
return true;
}
$this->debug("Cant find database schema: {$this->_database}/{$this->__table} \n".
"in links file data: " . print_r($_DB_DATAOBJECT['INI'],true),"databaseStructure",5);
// we have to die here!! - it causes chaos if we dont (including looping forever!)
$this->raiseError( "Unable to load schema for database and table (turn debugging up to 5 for full error message)", DB_DATAOBJECT_ERROR_INVALIDARGS, PEAR_ERROR_DIE);
return false;
}
/** For parseIniFiles */
protected static $iniCache = array();
/**
* When switching site configurations, DB_DataObject was loading its
* .ini files over and over, leaking gobs of memory.
* This refactored helper function uses a local cache of .ini files
* to minimize the leaks.
*
* @param array of .ini file names $schemas
* @return array
*/
protected function parseIniFiles($schemas)
{
$key = implode("|", $schemas);
if (!isset(Safe_DataObject::$iniCache[$key])) {
$data = array();
foreach ($schemas as $ini) {
if (file_exists($ini) && is_file($ini)) {
$data = array_merge($data, parse_ini_file($ini, true));
if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
if (!is_readable ($ini)) {
$this->debug("ini file is not readable: $ini","databaseStructure",1);
} else {
$this->debug("Loaded ini file: $ini","databaseStructure",1);
}
}
} else {
if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
$this->debug("Missing ini file: $ini","databaseStructure",1);
}
}
}
Safe_DataObject::$iniCache[$key] = $data;
}
return Safe_DataObject::$iniCache[$key];
}
}

View File

@ -21,7 +21,7 @@
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
class Status_network extends DB_DataObject class Status_network extends Safe_DataObject
{ {
###START_AUTOCODE ###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */ /* the code below is auto generated do not remove the above tag */
@ -57,6 +57,7 @@ class Status_network extends DB_DataObject
###END_AUTOCODE ###END_AUTOCODE
static $cache = null; static $cache = null;
static $cacheInitialized = false;
static $base = null; static $base = null;
static $wildcard = null; static $wildcard = null;
@ -78,11 +79,15 @@ class Status_network extends DB_DataObject
if (class_exists('Memcache')) { if (class_exists('Memcache')) {
self::$cache = new Memcache(); self::$cache = new Memcache();
// Can't close persistent connections, making forking painful. // If we're a parent command-line process we need
// to be able to close out the connection after
// forking, so disable persistence.
// //
// @fixme only do this in *parent* CLI processes. // We'll turn it back on again the second time
// single-process and child-processes *should* use persistent. // through which will either be in a child process,
$persist = php_sapi_name() != 'cli'; // or a single-process script which is switching
// configurations.
$persist = php_sapi_name() != 'cli' || self::$cacheInitialized;
if (is_array($servers)) { if (is_array($servers)) {
foreach($servers as $server) { foreach($servers as $server) {
self::$cache->addServer($server, 11211, $persist); self::$cache->addServer($server, 11211, $persist);
@ -90,6 +95,7 @@ class Status_network extends DB_DataObject
} else { } else {
self::$cache->addServer($servers, 11211, $persist); self::$cache->addServer($servers, 11211, $persist);
} }
self::$cacheInitialized = true;
} }
self::$base = $dbname; self::$base = $dbname;

View File

@ -285,6 +285,10 @@ class OMB_Service_Provider {
list($consumer, $token) = $this->getOAuthServer()->verify_request($req); list($consumer, $token) = $this->getOAuthServer()->verify_request($req);
} catch (OAuthException $e) { } catch (OAuthException $e) {
header('HTTP/1.1 403 Forbidden'); header('HTTP/1.1 403 Forbidden');
// @debug hack
throw OMB_RemoteServiceException::forRequest($uri,
'Revoked accesstoken for ' . $listenee . ': ' . $e->getMessage());
// @end debug
throw OMB_RemoteServiceException::forRequest($uri, throw OMB_RemoteServiceException::forRequest($uri,
'Revoked accesstoken for ' . $listenee); 'Revoked accesstoken for ' . $listenee);
} }

View File

@ -404,12 +404,10 @@ var SN = { // StatusNet
}, },
NoticeWithAttachment: function(notice) { NoticeWithAttachment: function(notice) {
if ($('.attachment', notice).length === 0) { if (notice.find('.attachment').length === 0) {
return; return;
} }
var notice_id = notice.attr('id');
$.fn.jOverlay.options = { $.fn.jOverlay.options = {
method : 'GET', method : 'GET',
data : '', data : '',
@ -429,16 +427,17 @@ var SN = { // StatusNet
css : {'max-width':'542px', 'top':'5%', 'left':'32.5%'} css : {'max-width':'542px', 'top':'5%', 'left':'32.5%'}
}; };
$('#'+notice_id+' a.attachment').click(function() { notice.find('a.attachment').click(function() {
$().jOverlay({url: $('address .url')[0].href+'attachment/' + ($(this).attr('id').substring('attachment'.length + 1)) + '/ajax'}); $().jOverlay({url: $('address .url')[0].href+'attachment/' + ($(this).attr('id').substring('attachment'.length + 1)) + '/ajax'});
return false; return false;
}); });
if ($('#shownotice').length == 0) {
var t; var t;
$("body:not(#shownotice) #"+notice_id+" a.thumbnail").hover( notice.find('a.thumbnail').hover(
function() { function() {
var anchor = $(this); var anchor = $(this);
$("a.thumbnail").children('img').hide(); $('a.thumbnail').children('img').hide();
anchor.closest(".entry-title").addClass('ov'); anchor.closest(".entry-title").addClass('ov');
if (anchor.children('img').length === 0) { if (anchor.children('img').length === 0) {
@ -454,10 +453,11 @@ var SN = { // StatusNet
}, },
function() { function() {
clearTimeout(t); clearTimeout(t);
$("a.thumbnail").children('img').hide(); $('a.thumbnail').children('img').hide();
$(this).closest(".entry-title").removeClass('ov'); $(this).closest('.entry-title').removeClass('ov');
} }
); );
}
}, },
NoticeDataAttach: function() { NoticeDataAttach: function() {

View File

@ -405,6 +405,7 @@ class Action extends HTMLOutputter // lawsuit
'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'), 'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'),
'alt' => common_config('site', 'name'))); 'alt' => common_config('site', 'name')));
} }
$this->text(' ');
$this->element('span', array('class' => 'fn org'), common_config('site', 'name')); $this->element('span', array('class' => 'fn org'), common_config('site', 'name'));
$this->elementEnd('a'); $this->elementEnd('a');
Event::handle('EndAddressData', array($this)); Event::handle('EndAddressData', array($this));
@ -822,12 +823,14 @@ class Action extends HTMLOutputter // lawsuit
'alt' => common_config('license', 'title'), 'alt' => common_config('license', 'title'),
'width' => '80', 'width' => '80',
'height' => '15')); 'height' => '15'));
$this->text(' ');
//TODO: This is dirty: i18n //TODO: This is dirty: i18n
$this->text(_('All '.common_config('site', 'name').' content and data are available under the ')); $this->text(_('All '.common_config('site', 'name').' content and data are available under the '));
$this->element('a', array('class' => 'license', $this->element('a', array('class' => 'license',
'rel' => 'external license', 'rel' => 'external license',
'href' => common_config('license', 'url')), 'href' => common_config('license', 'url')),
common_config('license', 'title')); common_config('license', 'title'));
$this->text(' ');
$this->text(_('license.')); $this->text(_('license.'));
$this->elementEnd('p'); $this->elementEnd('p');
break; break;

View File

@ -1155,7 +1155,6 @@ class ApiAction extends Action
$this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom', $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
'xml:lang' => 'en-US', 'xml:lang' => 'en-US',
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0')); 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
Event::handle('StartApiAtom', array($this));
} }
function endTwitterAtom() function endTwitterAtom()

View File

@ -175,6 +175,8 @@ class Atom10Feed extends XMLStringer
$this->element('updated', null, $this->updated); $this->element('updated', null, $this->updated);
$this->renderAuthors();
$this->renderLinks(); $this->renderLinks();
} }
@ -221,10 +223,10 @@ class Atom10Feed extends XMLStringer
function getString() function getString()
{ {
$this->validate(); if (Event::handle('StartApiAtom', array($this))) {
$this->validate();
$this->initFeed(); $this->initFeed();
$this->renderAuthors();
if (!empty($this->subject)) { if (!empty($this->subject)) {
$this->raw($this->subject); $this->raw($this->subject);
@ -233,6 +235,9 @@ class Atom10Feed extends XMLStringer
$this->renderEntries(); $this->renderEntries();
$this->endFeed(); $this->endFeed();
Event::handle('EndApiAtom', array($this));
}
return $this->xw->outputMemory(); return $this->xw->outputMemory();
} }

View File

@ -0,0 +1,67 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Class for building an in-memory Atom feed for a particular group's
* timeline.
*
* PHP version 5
*
* LICENCE: 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 Feed
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET'))
{
exit(1);
}
/**
* Class for group notice feeds. May contains a reference to the group.
*
* @category Feed
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class AtomGroupNoticeFeed extends AtomNoticeFeed
{
private $group;
/**
* Constructor
*
* @param Group $group the group for the feed (optional)
* @param boolean $indent flag to turn indenting on or off
*
* @return void
*/
function __construct($group = null, $indent = true) {
parent::__construct($indent);
$this->group = $group;
}
function getGroup()
{
return $this->group;
}
}

View File

@ -2,7 +2,7 @@
/** /**
* StatusNet, the distributed open-source microblogging tool * StatusNet, the distributed open-source microblogging tool
* *
* Class for building and Atom feed from a collection of notices * Class for building an Atom feed from a collection of notices
* *
* PHP version 5 * PHP version 5
* *
@ -101,3 +101,5 @@ class AtomNoticeFeed extends Atom10Feed
} }
} }

View File

@ -0,0 +1,66 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Class for building an in-memory Atom feed for a particular user's
* timeline.
*
* PHP version 5
*
* LICENCE: 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 Feed
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET'))
{
exit(1);
}
/**
* Class for user notice feeds. May contain a reference to the user.
*
* @category Feed
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class AtomUserNoticeFeed extends AtomNoticeFeed
{
private $user;
/**
* Constructor
*
* @param User $user the user for the feed (optional)
* @param boolean $indent flag to turn indenting on or off
*
* @return void
*/
function __construct($user = null, $indent = true) {
parent::__construct($indent);
$this->user = $user;
}
function getUser()
{
return $this->user;
}
}

View File

@ -159,6 +159,32 @@ class Cache
return $success; return $success;
} }
/**
* Atomically increment an existing numeric value.
* Existing expiration time should remain unchanged, if any.
*
* @param string $key The key to use for lookups
* @param int $step Amount to increment (default 1)
*
* @return mixed incremented value, or false if not set.
*/
function increment($key, $step=1)
{
$value = false;
if (Event::handle('StartCacheIncrement', array(&$key, &$step, &$value))) {
// Fallback is not guaranteed to be atomic,
// and may original expiry value.
$value = $this->get($key);
if ($value !== false) {
$value += $step;
$ok = $this->set($key, $value);
$got = $this->get($key);
}
Event::handle('EndCacheIncrement', array($key, $step, $value));
}
return $value;
}
/** /**
* Delete the value associated with a key * Delete the value associated with a key
* *

View File

@ -72,7 +72,7 @@ class DBQueueManager extends QueueManager
public function poll() public function poll()
{ {
$this->_log(LOG_DEBUG, 'Checking for notices...'); $this->_log(LOG_DEBUG, 'Checking for notices...');
$qi = Queue_item::top($this->getQueues()); $qi = Queue_item::top($this->activeQueues());
if (empty($qi)) { if (empty($qi)) {
$this->_log(LOG_DEBUG, 'No notices waiting; idling.'); $this->_log(LOG_DEBUG, 'No notices waiting; idling.');
return false; return false;
@ -142,9 +142,4 @@ class DBQueueManager extends QueueManager
$this->stats('error', $queue); $this->stats('error', $queue);
} }
protected function _log($level, $msg)
{
common_log($level, 'DBQueueManager: '.$msg);
}
} }

View File

@ -81,7 +81,7 @@ $default =
'subsystem' => 'db', # default to database, or 'stomp' 'subsystem' => 'db', # default to database, or 'stomp'
'stomp_server' => null, 'stomp_server' => null,
'queue_basename' => '/queue/statusnet/', 'queue_basename' => '/queue/statusnet/',
'control_channel' => '/topic/statusnet-control', // broadcasts to all queue daemons 'control_channel' => '/topic/statusnet/control', // broadcasts to all queue daemons
'stomp_username' => null, 'stomp_username' => null,
'stomp_password' => null, 'stomp_password' => null,
'stomp_persistent' => true, // keep items across queue server restart, if persistence is enabled 'stomp_persistent' => true, // keep items across queue server restart, if persistence is enabled
@ -91,6 +91,12 @@ $default =
'spawndelay' => 1, // Wait at least N seconds between (re)spawns of child processes to avoid slamming the queue server with subscription startup 'spawndelay' => 1, // Wait at least N seconds between (re)spawns of child processes to avoid slamming the queue server with subscription startup
'debug_memory' => false, // true to spit memory usage to log 'debug_memory' => false, // true to spit memory usage to log
'inboxes' => true, // true to do inbox distribution & output queueing from in background via 'distrib' queue 'inboxes' => true, // true to do inbox distribution & output queueing from in background via 'distrib' queue
'breakout' => array('*' => 'shared'), // set global or per-handler queue breakout
// 'shared': use a shared queue for all sites
// 'handler': share each/this handler over multiple sites
// 'site': break out for each/this handler on this site
'max_retries' => 10, // drop messages after N failed attempts to process (Stomp)
'dead_letter_dir' => false, // set to directory to save dropped messages into (Stomp)
), ),
'license' => 'license' =>
array('type' => 'cc', # can be 'cc', 'allrightsreserved', 'private' array('type' => 'cc', # can be 'cc', 'allrightsreserved', 'private'
@ -111,11 +117,13 @@ $default =
'avatar' => 'avatar' =>
array('server' => null, array('server' => null,
'dir' => INSTALLDIR . '/avatar/', 'dir' => INSTALLDIR . '/avatar/',
'path' => $_path . '/avatar/'), 'path' => $_path . '/avatar/',
'ssl' => null),
'background' => 'background' =>
array('server' => null, array('server' => null,
'dir' => INSTALLDIR . '/background/', 'dir' => INSTALLDIR . '/background/',
'path' => $_path . '/background/'), 'path' => $_path . '/background/',
'ssl' => null),
'public' => 'public' =>
array('localonly' => true, array('localonly' => true,
'blacklist' => array(), 'blacklist' => array(),
@ -123,10 +131,12 @@ $default =
'theme' => 'theme' =>
array('server' => null, array('server' => null,
'dir' => null, 'dir' => null,
'path'=> null), 'path'=> null,
'ssl' => null),
'javascript' => 'javascript' =>
array('server' => null, array('server' => null,
'path'=> null), 'path'=> null,
'ssl' => null),
'throttle' => 'throttle' =>
array('enabled' => false, // whether to throttle edits; false by default array('enabled' => false, // whether to throttle edits; false by default
'count' => 20, // number of allowed messages in timespan 'count' => 20, // number of allowed messages in timespan
@ -184,6 +194,7 @@ $default =
array('server' => null, array('server' => null,
'dir' => INSTALLDIR . '/file/', 'dir' => INSTALLDIR . '/file/',
'path' => $_path . '/file/', 'path' => $_path . '/file/',
'ssl' => null,
'supported' => array('image/png', 'supported' => array('image/png',
'image/jpeg', 'image/jpeg',
'image/gif', 'image/gif',

View File

@ -105,6 +105,7 @@ class GroupList extends Widget
'alt' => 'alt' =>
($this->group->fullname) ? $this->group->fullname : ($this->group->fullname) ? $this->group->fullname :
$this->group->nickname)); $this->group->nickname));
$this->out->text(' ');
$hasFN = ($this->group->fullname) ? 'nickname' : 'fn org nickname'; $hasFN = ($this->group->fullname) ? 'nickname' : 'fn org nickname';
$this->out->elementStart('span', $hasFN); $this->out->elementStart('span', $hasFN);
$this->out->raw($this->highlight($this->group->nickname)); $this->out->raw($this->highlight($this->group->nickname));
@ -112,16 +113,19 @@ class GroupList extends Widget
$this->out->elementEnd('a'); $this->out->elementEnd('a');
if ($this->group->fullname) { if ($this->group->fullname) {
$this->out->text(' ');
$this->out->elementStart('span', 'fn org'); $this->out->elementStart('span', 'fn org');
$this->out->raw($this->highlight($this->group->fullname)); $this->out->raw($this->highlight($this->group->fullname));
$this->out->elementEnd('span'); $this->out->elementEnd('span');
} }
if ($this->group->location) { if ($this->group->location) {
$this->out->text(' ');
$this->out->elementStart('span', 'label'); $this->out->elementStart('span', 'label');
$this->out->raw($this->highlight($this->group->location)); $this->out->raw($this->highlight($this->group->location));
$this->out->elementEnd('span'); $this->out->elementEnd('span');
} }
if ($this->group->homepage) { if ($this->group->homepage) {
$this->out->text(' ');
$this->out->elementStart('a', array('href' => $this->group->homepage, $this->out->elementStart('a', array('href' => $this->group->homepage,
'class' => 'url')); 'class' => 'url'));
$this->out->raw($this->highlight($this->group->homepage)); $this->out->raw($this->highlight($this->group->homepage));

View File

@ -85,9 +85,9 @@ class GroupSection extends Section
'href' => $group->homeUrl(), 'href' => $group->homeUrl(),
'rel' => 'contact group', 'rel' => 'contact group',
'class' => 'url')); 'class' => 'url'));
$this->out->text(' ');
$logo = ($group->stream_logo) ? $logo = ($group->stream_logo) ?
$group->stream_logo : User_group::defaultLogo(AVATAR_STREAM_SIZE); $group->stream_logo : User_group::defaultLogo(AVATAR_STREAM_SIZE);
$this->out->element('img', array('src' => $logo, $this->out->element('img', array('src' => $logo,
'width' => AVATAR_MINI_SIZE, 'width' => AVATAR_MINI_SIZE,
'height' => AVATAR_MINI_SIZE, 'height' => AVATAR_MINI_SIZE,
@ -95,6 +95,7 @@ class GroupSection extends Section
'alt' => ($group->fullname) ? 'alt' => ($group->fullname) ?
$group->fullname : $group->fullname :
$group->nickname)); $group->nickname));
$this->out->text(' ');
$this->out->element('span', 'fn org nickname', $group->nickname); $this->out->element('span', 'fn org nickname', $group->nickname);
$this->out->elementEnd('a'); $this->out->elementEnd('a');
$this->out->elementEnd('span'); $this->out->elementEnd('span');

View File

@ -376,9 +376,20 @@ class HTMLOutputter extends XMLOutputter
$server = common_config('site', 'server'); $server = common_config('site', 'server');
} }
// XXX: protocol $ssl = common_config('javascript', 'ssl');
$src = 'http://'.$server.$path.$src . '?version=' . STATUSNET_VERSION; if (is_null($ssl)) { // null -> guess
if (common_config('site', 'ssl') == 'always' &&
!common_config('javascript', 'server')) {
$ssl = true;
} else {
$ssl = false;
}
}
$protocol = ($ssl) ? 'https' : 'http';
$src = $protocol.'://'.$server.$path.$src . '?version=' . STATUSNET_VERSION;
} }
$this->element('script', array('type' => $type, $this->element('script', array('type' => $type,

View File

@ -42,8 +42,7 @@ abstract class ImManager extends IoManager
function __construct($imPlugin) function __construct($imPlugin)
{ {
$this->plugin = $imPlugin; $this->plugin = $imPlugin;
//TODO We only really want to register this event if this is the thread that runs the ImManager $this->plugin->imManager = $this;
Event::addHandler('EndInitializeQueueManager', array($this, 'onEndInitializeQueueManager'));
} }
/** /**
@ -54,17 +53,4 @@ abstract class ImManager extends IoManager
{ {
throw new Exception('ImManager should be created using it\'s constructor, not the static get method'); throw new Exception('ImManager should be created using it\'s constructor, not the static get method');
} }
/**
* Register notice queue handler
*
* @param QueueManager $manager
*
* @return boolean hook return
*/
function onEndInitializeQueueManager($manager)
{
$manager->connect($this->plugin->transport . '-out', new ImSenderQueueHandler($this->plugin, $this), 'imdaemon');
return true;
}
} }

View File

@ -507,8 +507,9 @@ abstract class ImPlugin extends Plugin
*/ */
function onEndInitializeQueueManager($manager) function onEndInitializeQueueManager($manager)
{ {
$manager->connect($this->transport . '-in', new ImReceiverQueueHandler($this)); $manager->connect($this->transport . '-in', new ImReceiverQueueHandler($this), 'im');
$manager->connect($this->transport, new ImQueueHandler($this)); $manager->connect($this->transport, new ImQueueHandler($this));
$manager->connect($this->transport . '-out', new ImSenderQueueHandler($this), 'im');
return true; return true;
} }

View File

@ -25,10 +25,9 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
class ImSenderQueueHandler extends QueueHandler class ImSenderQueueHandler extends QueueHandler
{ {
function __construct($plugin, $immanager) function __construct($plugin)
{ {
$this->plugin = $plugin; $this->plugin = $plugin;
$this->immanager = $immanager;
} }
/** /**
@ -38,7 +37,7 @@ class ImSenderQueueHandler extends QueueHandler
*/ */
function handle($data) function handle($data)
{ {
return $this->immanager->send_raw_message($data); return $this->plugin->imManager->send_raw_message($data);
} }
} }

View File

@ -59,9 +59,10 @@ abstract class IoManager
* your manager about each site you'll have to handle so you * your manager about each site you'll have to handle so you
* can do any necessary per-site setup. * can do any necessary per-site setup.
* *
* @param string $site target site server name * The new site will be the currently live configuration during
* this call.
*/ */
public function addSite($site) public function addSite()
{ {
/* no-op */ /* no-op */
} }

View File

@ -56,9 +56,9 @@ abstract class IoMaster
$this->multiSite = $multiSite; $this->multiSite = $multiSite;
} }
if ($this->multiSite) { if ($this->multiSite) {
$this->sites = $this->findAllSites(); $this->sites = StatusNet::findAllSites();
} else { } else {
$this->sites = array(common_config('site', 'server')); $this->sites = array(StatusNet::currentSite());
} }
if (empty($this->sites)) { if (empty($this->sites)) {
@ -66,9 +66,7 @@ abstract class IoMaster
} }
foreach ($this->sites as $site) { foreach ($this->sites as $site) {
if ($site != common_config('site', 'server')) { StatusNet::switchSite($site);
StatusNet::init($site);
}
$this->initManagers(); $this->initManagers();
} }
} }
@ -81,62 +79,32 @@ abstract class IoMaster
*/ */
abstract function initManagers(); abstract function initManagers();
/**
* Pull all local sites from status_network table.
* @return array of hostnames
*/
protected function findAllSites()
{
$hosts = array();
$sn = new Status_network();
$sn->find();
while ($sn->fetch()) {
$hosts[] = $sn->getServerName();
}
return $hosts;
}
/** /**
* Instantiate an i/o manager class for the current site. * Instantiate an i/o manager class for the current site.
* If a multi-site capable handler is already present, * If a multi-site capable handler is already present,
* we don't need to build a new one. * we don't need to build a new one.
* *
* @param string $class * @param mixed $manager class name (to run $class::get()) or object
*/ */
protected function instantiate($class) protected function instantiate($manager)
{ {
if (is_string($class) && isset($this->singletons[$class])) { if (is_string($manager)) {
// Already instantiated a multi-site-capable handler. $manager = call_user_func(array($class, 'get'));
// Just let it know it should listen to this site too!
$this->singletons[$class]->addSite(common_config('site', 'server'));
return;
} }
$manager = $this->getManager($class);
if ($this->multiSite) {
$caps = $manager->multiSite(); $caps = $manager->multiSite();
if ($caps == IoManager::SINGLE_ONLY) { if ($caps == IoManager::SINGLE_ONLY) {
if ($this->multiSite) {
throw new Exception("$class can't run with --all; aborting."); throw new Exception("$class can't run with --all; aborting.");
} }
if ($caps == IoManager::INSTANCE_PER_PROCESS) { } else if ($caps == IoManager::INSTANCE_PER_PROCESS) {
// Save this guy for later! $manager->addSite();
// We'll only need the one to cover multiple sites.
$this->singletons[$class] = $manager;
$manager->addSite(common_config('site', 'server'));
}
} }
if (!in_array($manager, $this->managers, true)) {
// Only need to save singletons once
$this->managers[] = $manager; $this->managers[] = $manager;
} }
protected function getManager($class)
{
if(is_object($class)){
return $class;
} else {
return call_user_func(array($class, 'get'));
}
} }
/** /**
@ -150,6 +118,7 @@ abstract class IoMaster
{ {
$this->logState('init'); $this->logState('init');
$this->start(); $this->start();
$this->checkMemory(false);
while (!$this->shutdown) { while (!$this->shutdown) {
$timeouts = array_values($this->pollTimeouts); $timeouts = array_values($this->pollTimeouts);
@ -213,17 +182,24 @@ abstract class IoMaster
/** /**
* Check runtime memory usage, possibly triggering a graceful shutdown * Check runtime memory usage, possibly triggering a graceful shutdown
* and thread respawn if we've crossed the soft limit. * and thread respawn if we've crossed the soft limit.
*
* @param boolean $respawn if false we'll shut down instead of respawning
*/ */
protected function checkMemory() protected function checkMemory($respawn=true)
{ {
$memoryLimit = $this->softMemoryLimit(); $memoryLimit = $this->softMemoryLimit();
if ($memoryLimit > 0) { if ($memoryLimit > 0) {
$usage = memory_get_usage(); $usage = memory_get_usage();
if ($usage > $memoryLimit) { if ($usage > $memoryLimit) {
common_log(LOG_INFO, "Queue thread hit soft memory limit ($usage > $memoryLimit); gracefully restarting."); common_log(LOG_INFO, "Queue thread hit soft memory limit ($usage > $memoryLimit); gracefully restarting.");
if ($respawn) {
$this->requestRestart(); $this->requestRestart();
} else {
$this->requestShutdown();
}
} else if (common_config('queue', 'debug_memory')) { } else if (common_config('queue', 'debug_memory')) {
common_log(LOG_DEBUG, "Memory usage $usage"); $fmt = number_format($usage);
common_log(LOG_DEBUG, "Memory usage $fmt");
} }
} }
} }

View File

@ -294,6 +294,7 @@ class NoticeListItem extends Widget
} }
$this->out->elementStart('a', $attrs); $this->out->elementStart('a', $attrs);
$this->showAvatar(); $this->showAvatar();
$this->out->text(' ');
$this->showNickname(); $this->showNickname();
$this->out->elementEnd('a'); $this->out->elementEnd('a');
$this->out->elementEnd('span'); $this->out->elementEnd('span');
@ -432,8 +433,10 @@ class NoticeListItem extends Widget
$url = $location->getUrl(); $url = $location->getUrl();
$this->out->text(' ');
$this->out->elementStart('span', array('class' => 'location')); $this->out->elementStart('span', array('class' => 'location'));
$this->out->text(_('at')); $this->out->text(_('at'));
$this->out->text(' ');
if (empty($url)) { if (empty($url)) {
$this->out->element('span', array('class' => 'geo', $this->out->element('span', array('class' => 'geo',
'title' => $latlon), 'title' => $latlon),
@ -473,9 +476,11 @@ class NoticeListItem extends Widget
function showNoticeSource() function showNoticeSource()
{ {
if ($this->notice->source) { if ($this->notice->source) {
$this->out->text(' ');
$this->out->elementStart('span', 'source'); $this->out->elementStart('span', 'source');
$this->out->text(_('from')); $this->out->text(_('from'));
$source_name = _($this->notice->source); $source_name = _($this->notice->source);
$this->out->text(' ');
switch ($this->notice->source) { switch ($this->notice->source) {
case 'web': case 'web':
case 'xmpp': case 'xmpp':
@ -540,6 +545,7 @@ class NoticeListItem extends Widget
} }
} }
if ($hasConversation){ if ($hasConversation){
$this->out->text(' ');
$convurl = common_local_url('conversation', $convurl = common_local_url('conversation',
array('id' => $this->notice->conversation)); array('id' => $this->notice->conversation));
$this->out->element('a', array('href' => $convurl.'#notice-'.$this->notice->id, $this->out->element('a', array('href' => $convurl.'#notice-'.$this->notice->id,
@ -591,12 +597,14 @@ class NoticeListItem extends Widget
function showReplyLink() function showReplyLink()
{ {
if (common_logged_in()) { if (common_logged_in()) {
$this->out->text(' ');
$reply_url = common_local_url('newnotice', $reply_url = common_local_url('newnotice',
array('replyto' => $this->profile->nickname, 'inreplyto' => $this->notice->id)); array('replyto' => $this->profile->nickname, 'inreplyto' => $this->notice->id));
$this->out->elementStart('a', array('href' => $reply_url, $this->out->elementStart('a', array('href' => $reply_url,
'class' => 'notice_reply', 'class' => 'notice_reply',
'title' => _('Reply to this notice'))); 'title' => _('Reply to this notice')));
$this->out->text(_('Reply')); $this->out->text(_('Reply'));
$this->out->text(' ');
$this->out->element('span', 'notice_id', $this->notice->id); $this->out->element('span', 'notice_id', $this->notice->id);
$this->out->elementEnd('a'); $this->out->elementEnd('a');
} }
@ -616,7 +624,7 @@ class NoticeListItem extends Widget
if (!empty($user) && if (!empty($user) &&
($todel->profile_id == $user->id || $user->hasRight(Right::DELETEOTHERSNOTICE))) { ($todel->profile_id == $user->id || $user->hasRight(Right::DELETEOTHERSNOTICE))) {
$this->out->text(' ');
$deleteurl = common_local_url('deletenotice', $deleteurl = common_local_url('deletenotice',
array('notice' => $todel->id)); array('notice' => $todel->id));
$this->out->element('a', array('href' => $deleteurl, $this->out->element('a', array('href' => $deleteurl,
@ -635,6 +643,7 @@ class NoticeListItem extends Widget
{ {
$user = common_current_user(); $user = common_current_user();
if ($user && $user->id != $this->notice->profile_id) { if ($user && $user->id != $this->notice->profile_id) {
$this->out->text(' ');
$profile = $user->getProfile(); $profile = $user->getProfile();
if ($profile->hasRepeated($this->notice->id)) { if ($profile->hasRepeated($this->notice->id)) {
$this->out->element('span', array('class' => 'repeated', $this->out->element('span', array('class' => 'repeated',

View File

@ -90,6 +90,7 @@ class NoticeSection extends Section
'alt' => ($profile->fullname) ? 'alt' => ($profile->fullname) ?
$profile->fullname : $profile->fullname :
$profile->nickname)); $profile->nickname));
$this->out->text(' ');
$this->out->element('span', 'fn nickname', $profile->nickname); $this->out->element('span', 'fn nickname', $profile->nickname);
$this->out->elementEnd('a'); $this->out->elementEnd('a');
$this->out->elementEnd('span'); $this->out->elementEnd('span');

View File

@ -191,6 +191,7 @@ class ProfileListItem extends Widget
'alt' => 'alt' =>
($this->profile->fullname) ? $this->profile->fullname : ($this->profile->fullname) ? $this->profile->fullname :
$this->profile->nickname)); $this->profile->nickname));
$this->out->text(' ');
$hasFN = (!empty($this->profile->fullname)) ? 'nickname' : 'fn nickname'; $hasFN = (!empty($this->profile->fullname)) ? 'nickname' : 'fn nickname';
$this->out->elementStart('span', $hasFN); $this->out->elementStart('span', $hasFN);
$this->out->raw($this->highlight($this->profile->nickname)); $this->out->raw($this->highlight($this->profile->nickname));
@ -201,6 +202,7 @@ class ProfileListItem extends Widget
function showFullName() function showFullName()
{ {
if (!empty($this->profile->fullname)) { if (!empty($this->profile->fullname)) {
$this->out->text(' ');
$this->out->elementStart('span', 'fn'); $this->out->elementStart('span', 'fn');
$this->out->raw($this->highlight($this->profile->fullname)); $this->out->raw($this->highlight($this->profile->fullname));
$this->out->elementEnd('span'); $this->out->elementEnd('span');
@ -210,6 +212,7 @@ class ProfileListItem extends Widget
function showLocation() function showLocation()
{ {
if (!empty($this->profile->location)) { if (!empty($this->profile->location)) {
$this->out->text(' ');
$this->out->elementStart('span', 'location'); $this->out->elementStart('span', 'location');
$this->out->raw($this->highlight($this->profile->location)); $this->out->raw($this->highlight($this->profile->location));
$this->out->elementEnd('span'); $this->out->elementEnd('span');
@ -219,6 +222,7 @@ class ProfileListItem extends Widget
function showHomepage() function showHomepage()
{ {
if (!empty($this->profile->homepage)) { if (!empty($this->profile->homepage)) {
$this->out->text(' ');
$this->out->elementStart('a', array('href' => $this->profile->homepage, $this->out->elementStart('a', array('href' => $this->profile->homepage,
'class' => 'url')); 'class' => 'url'));
$this->out->raw($this->highlight($this->profile->homepage)); $this->out->raw($this->highlight($this->profile->homepage));

View File

@ -85,6 +85,7 @@ class ProfileSection extends Section
'href' => $profile->profileurl, 'href' => $profile->profileurl,
'rel' => 'contact member', 'rel' => 'contact member',
'class' => 'url')); 'class' => 'url'));
$this->out->text(' ');
$avatar = $profile->getAvatar(AVATAR_MINI_SIZE); $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
$this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_MINI_SIZE)), $this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_MINI_SIZE)),
'width' => AVATAR_MINI_SIZE, 'width' => AVATAR_MINI_SIZE,
@ -93,6 +94,7 @@ class ProfileSection extends Section
'alt' => ($profile->fullname) ? 'alt' => ($profile->fullname) ?
$profile->fullname : $profile->fullname :
$profile->nickname)); $profile->nickname));
$this->out->text(' ');
$this->out->element('span', 'fn nickname', $profile->nickname); $this->out->element('span', 'fn nickname', $profile->nickname);
$this->out->elementEnd('a'); $this->out->elementEnd('a');
$this->out->elementEnd('span'); $this->out->elementEnd('span');

View File

@ -39,9 +39,10 @@ abstract class QueueManager extends IoManager
{ {
static $qm = null; static $qm = null;
public $master = null; protected $master = null;
public $handlers = array(); protected $handlers = array();
public $groups = array(); protected $groups = array();
protected $activeGroups = array();
/** /**
* Factory function to pull the appropriate QueueManager object * Factory function to pull the appropriate QueueManager object
@ -162,7 +163,7 @@ abstract class QueueManager extends IoManager
*/ */
protected function encode($item) protected function encode($item)
{ {
return serialize($object); return serialize($item);
} }
/** /**
@ -192,40 +193,48 @@ abstract class QueueManager extends IoManager
} else if (class_exists($class)) { } else if (class_exists($class)) {
return new $class(); return new $class();
} else { } else {
common_log(LOG_ERR, "Nonexistent handler class '$class' for queue '$queue'"); $this->_log(LOG_ERR, "Nonexistent handler class '$class' for queue '$queue'");
} }
} else { } else {
common_log(LOG_ERR, "Requested handler for unkown queue '$queue'"); $this->_log(LOG_ERR, "Requested handler for unkown queue '$queue'");
} }
return null; return null;
} }
/** /**
* Get a list of registered queue transport names to be used * Get a list of registered queue transport names to be used
* for this daemon. * for listening in this daemon.
* *
* @return array of strings * @return array of strings
*/ */
function getQueues() function activeQueues()
{ {
$group = $this->activeGroup(); $queues = array();
return array_keys($this->groups[$group]); foreach ($this->activeGroups as $group) {
if (isset($this->groups[$group])) {
$queues = array_merge($queues, $this->groups[$group]);
}
}
return array_keys($queues);
} }
/** /**
* Initialize the list of queue handlers * Initialize the list of queue handlers for the current site.
* *
* @event StartInitializeQueueManager * @event StartInitializeQueueManager
* @event EndInitializeQueueManager * @event EndInitializeQueueManager
*/ */
function initialize() function initialize()
{ {
// @fixme we'll want to be able to listen to particular queues... $this->handlers = array();
$this->groups = array();
$this->groupsByTransport = array();
if (Event::handle('StartInitializeQueueManager', array($this))) { if (Event::handle('StartInitializeQueueManager', array($this))) {
$this->connect('plugin', 'PluginQueueHandler'); $this->connect('distrib', 'DistribQueueHandler');
$this->connect('omb', 'OmbQueueHandler'); $this->connect('omb', 'OmbQueueHandler');
$this->connect('ping', 'PingQueueHandler'); $this->connect('ping', 'PingQueueHandler');
$this->connect('distrib', 'DistribQueueHandler');
if (common_config('sms', 'enabled')) { if (common_config('sms', 'enabled')) {
$this->connect('sms', 'SmsQueueHandler'); $this->connect('sms', 'SmsQueueHandler');
} }
@ -244,25 +253,41 @@ abstract class QueueManager extends IoManager
* @param string $class class name or object instance * @param string $class class name or object instance
* @param string $group * @param string $group
*/ */
public function connect($transport, $class, $group='queuedaemon') public function connect($transport, $class, $group='main')
{ {
$this->handlers[$transport] = $class; $this->handlers[$transport] = $class;
$this->groups[$group][$transport] = $class; $this->groups[$group][$transport] = $class;
$this->groupsByTransport[$transport] = $group;
} }
/** /**
* @return string queue group to use for this request * Set the active group which will be used for listening.
* @param string $group
*/ */
function activeGroup() function setActiveGroup($group)
{ {
$group = 'queuedaemon'; $this->activeGroups = array($group);
if ($this->master) {
// hack hack
if ($this->master instanceof ImMaster) {
return 'imdaemon';
} }
/**
* Set the active group(s) which will be used for listening.
* @param array $groups
*/
function setActiveGroups($groups)
{
$this->activeGroups = $groups;
}
/**
* @return string queue group for this queue
*/
function queueGroup($queue)
{
if (isset($this->groupsByTransport[$queue])) {
return $this->groupsByTransport[$queue];
} else {
throw new Exception("Requested group for unregistered transport $queue");
} }
return $group;
} }
/** /**
@ -286,4 +311,15 @@ abstract class QueueManager extends IoManager
$monitor->stats($key, $owners); $monitor->stats($key, $owners);
} }
} }
protected function _log($level, $msg)
{
$class = get_class($this);
if ($this->activeGroups) {
$groups = ' (' . implode(',', $this->activeGroups) . ')';
} else {
$groups = '';
}
common_log($level, "$class$groups: $msg");
}
} }

View File

@ -90,18 +90,24 @@ abstract class SpawningDaemon extends Daemon
while (count($children) > 0) { while (count($children) > 0) {
$status = null; $status = null;
$pid = pcntl_wait($status); $pid = pcntl_wait($status);
if ($pid > 0 && pcntl_wifexited($status)) { if ($pid > 0) {
$exitCode = pcntl_wexitstatus($status);
$i = array_search($pid, $children); $i = array_search($pid, $children);
if ($i === false) { if ($i === false) {
$this->log(LOG_ERR, "Unrecognized child pid $pid exited with status $exitCode"); $this->log(LOG_ERR, "Ignoring exit of unrecognized child pid $pid");
continue; continue;
} }
if (pcntl_wifexited($status)) {
$exitCode = pcntl_wexitstatus($status);
$info = "status $exitCode";
} else if (pcntl_wifsignaled($status)) {
$exitCode = self::EXIT_ERR;
$signal = pcntl_wtermsig($status);
$info = "signal $signal";
}
unset($children[$i]); unset($children[$i]);
if ($this->shouldRespawn($exitCode)) { if ($this->shouldRespawn($exitCode)) {
$this->log(LOG_INFO, "Thread $i pid $pid exited with status $exitCode; respawing."); $this->log(LOG_INFO, "Thread $i pid $pid exited with $info; respawing.");
$pid = pcntl_fork(); $pid = pcntl_fork();
if ($pid < 0) { if ($pid < 0) {

View File

@ -102,6 +102,60 @@ class StatusNet
self::initPlugins(); self::initPlugins();
} }
/**
* Get identifier of the currently active site configuration
* @return string
*/
public static function currentSite()
{
return common_config('site', 'nickname');
}
/**
* Change site configuration to site specified by nickname,
* if set up via Status_network. If not, sites other than
* the current will fail horribly.
*
* May throw exception or trigger a fatal error if the given
* site is missing or configured incorrectly.
*
* @param string $nickname
*/
public static function switchSite($nickname)
{
if ($nickname == StatusNet::currentSite()) {
return true;
}
$sn = Status_network::staticGet($nickname);
if (empty($sn)) {
return false;
throw new Exception("No such site nickname '$nickname'");
}
$server = $sn->getServerName();
StatusNet::init($server);
}
/**
* Pull all local sites from status_network table.
*
* Behavior undefined if site is not configured via Status_network.
*
* @return array of nicknames
*/
public static function findAllSites()
{
$sites = array();
$sn = new Status_network();
$sn->find();
while ($sn->fetch()) {
$sites[] = $sn->nickname;
}
return $sites;
}
/** /**
* Fire initialization events for all instantiated plugins. * Fire initialization events for all instantiated plugins.
*/ */

View File

@ -63,6 +63,7 @@ class StompQueueManager extends QueueManager
$this->password = common_config('queue', 'stomp_password'); $this->password = common_config('queue', 'stomp_password');
$this->base = common_config('queue', 'queue_basename'); $this->base = common_config('queue', 'queue_basename');
$this->control = common_config('queue', 'control_channel'); $this->control = common_config('queue', 'control_channel');
$this->subscriptions = array($this->control => $this->control);
} }
/** /**
@ -75,17 +76,25 @@ class StompQueueManager extends QueueManager
} }
/** /**
* Record each site we'll be handling input for in this process, * Record queue subscriptions we'll need to handle the current site.
* so we can listen to the necessary queues for it.
*
* @fixme possibly actually do subscription here to save another
* loop over all sites later?
* @fixme possibly don't assume it's the current site
*/ */
public function addSite($server) public function addSite()
{ {
$this->sites[] = $server; $this->sites[] = StatusNet::currentSite();
// Set up handlers active for this site...
$this->initialize(); $this->initialize();
foreach ($this->activeGroups as $group) {
if (isset($this->groups[$group])) {
// Actual queues may be broken out or consolidated...
// Subscribe to all the target queues we'll need.
foreach ($this->groups[$group] as $transport => $class) {
$target = $this->queueName($transport);
$this->subscriptions[$target] = $target;
}
}
}
} }
/** /**
@ -121,59 +130,11 @@ class StompQueueManager extends QueueManager
} }
/** /**
* Instantiate the appropriate QueueHandler class for the given queue. * Saves an object into the queue item table.
* *
* @param mixed $object
* @param string $queue * @param string $queue
* @return mixed QueueHandler or null
*/
function getHandler($queue)
{
$handlers = $this->handlers[$this->currentSite()];
if (isset($handlers[$queue])) {
$class = $handlers[$queue];
if (class_exists($class)) {
return new $class();
} else {
common_log(LOG_ERR, "Nonexistent handler class '$class' for queue '$queue'");
}
} else {
common_log(LOG_ERR, "Requested handler for unkown queue '$queue'");
}
return null;
}
/**
* Get a list of all registered queue transport names.
* *
* @return array of strings
*/
function getQueues()
{
$group = $this->activeGroup();
$site = $this->currentSite();
if (empty($this->groups[$site][$group])) {
return array();
} else {
return array_keys($this->groups[$site][$group]);
}
}
/**
* Register a queue transport name and handler class for your plugin.
* Only registered transports will be reliably picked up!
*
* @param string $transport
* @param string $class
* @param string $group
*/
public function connect($transport, $class, $group='queuedaemon')
{
$this->handlers[$this->currentSite()][$transport] = $class;
$this->groups[$this->currentSite()][$group][$transport] = $class;
}
/**
* Saves a notice object reference into the queue item table.
* @return boolean true on success * @return boolean true on success
* @throws StompException on connection or send error * @throws StompException on connection or send error
*/ */
@ -192,8 +153,11 @@ class StompQueueManager extends QueueManager
*/ */
protected function _doEnqueue($object, $queue, $idx) protected function _doEnqueue($object, $queue, $idx)
{ {
$msg = $this->encode($object);
$rep = $this->logrep($object); $rep = $this->logrep($object);
$envelope = array('site' => common_config('site', 'nickname'),
'handler' => $queue,
'payload' => $this->encode($object));
$msg = serialize($envelope);
$props = array('created' => common_sql_now()); $props = array('created' => common_sql_now());
if ($this->isPersistent($queue)) { if ($this->isPersistent($queue)) {
@ -205,11 +169,11 @@ class StompQueueManager extends QueueManager
$result = $con->send($this->queueName($queue), $msg, $props); $result = $con->send($this->queueName($queue), $msg, $props);
if (!$result) { if (!$result) {
common_log(LOG_ERR, "Error sending $rep to $queue queue on $host"); $this->_log(LOG_ERR, "Error sending $rep to $queue queue on $host");
return false; return false;
} }
common_log(LOG_DEBUG, "complete remote queueing $rep for $queue on $host"); $this->_log(LOG_DEBUG, "complete remote queueing $rep for $queue on $host");
$this->stats('enqueued', $queue); $this->stats('enqueued', $queue);
return true; return true;
} }
@ -275,12 +239,14 @@ class StompQueueManager extends QueueManager
$idx = $this->connectionFromSocket($socket); $idx = $this->connectionFromSocket($socket);
$con = $this->cons[$idx]; $con = $this->cons[$idx];
$host = $con->getServer(); $host = $con->getServer();
$this->defaultIdx = $idx;
$ok = true; $ok = true;
try { try {
$frames = $con->readFrames(); $frames = $con->readFrames();
} catch (StompException $e) { } catch (StompException $e) {
common_log(LOG_ERR, "Lost connection to $host: " . $e->getMessage()); $this->_log(LOG_ERR, "Lost connection to $host: " . $e->getMessage());
fclose($socket); // ???
$this->cons[$idx] = null; $this->cons[$idx] = null;
$this->transaction[$idx] = null; $this->transaction[$idx] = null;
$this->disconnect[$idx] = time(); $this->disconnect[$idx] = time();
@ -289,14 +255,17 @@ class StompQueueManager extends QueueManager
foreach ($frames as $frame) { foreach ($frames as $frame) {
$dest = $frame->headers['destination']; $dest = $frame->headers['destination'];
if ($dest == $this->control) { if ($dest == $this->control) {
if (!$this->handleControlSignal($idx, $frame)) { if (!$this->handleControlSignal($frame)) {
// We got a control event that requests a shutdown; // We got a control event that requests a shutdown;
// close out and stop handling anything else! // close out and stop handling anything else!
break; break;
} }
} else { } else {
$ok = $ok && $this->handleItem($idx, $frame); $ok = $this->handleItem($frame) && $ok;
} }
$this->ack($idx, $frame);
$this->commit($idx);
$this->begin($idx);
} }
return $ok; return $ok;
} }
@ -333,22 +302,9 @@ class StompQueueManager extends QueueManager
parent::start($master); parent::start($master);
$this->_connectAll(); $this->_connectAll();
common_log(LOG_INFO, "Subscribing to $this->control");
foreach ($this->cons as $con) {
if ($con) {
$con->subscribe($this->control);
}
}
if ($this->sites) {
foreach ($this->sites as $server) {
StatusNet::init($server);
$this->doSubscribe();
}
} else {
$this->doSubscribe();
}
foreach ($this->cons as $i => $con) { foreach ($this->cons as $i => $con) {
if ($con) { if ($con) {
$this->doSubscribe($con);
$this->begin($i); $this->begin($i);
} }
} }
@ -356,9 +312,7 @@ class StompQueueManager extends QueueManager
} }
/** /**
* Subscribe to all the queues we're going to need to handle... * Close out any active connections.
*
* Side effects: in multi-site mode, may reset site configuration.
* *
* @return bool return false on failure * @return bool return false on failure
*/ */
@ -376,15 +330,6 @@ class StompQueueManager extends QueueManager
return true; return true;
} }
/**
* Get identifier of the currently active site configuration
* @return string
*/
protected function currentSite()
{
return common_config('site', 'server'); // @fixme switch to nickname
}
/** /**
* Lazy open a single connection to Stomp queue server. * Lazy open a single connection to Stomp queue server.
* If multiple servers are configured, we let the Stomp client library * If multiple servers are configured, we let the Stomp client library
@ -441,6 +386,10 @@ class StompQueueManager extends QueueManager
} }
} }
/**
* Attempt to manually reconnect to the Stomp server for the given
* slot. If successful, set up our subscriptions on it.
*/
protected function _reconnect($idx) protected function _reconnect($idx)
{ {
try { try {
@ -453,17 +402,7 @@ class StompQueueManager extends QueueManager
$this->cons[$idx] = $con; $this->cons[$idx] = $con;
$this->disconnect[$idx] = null; $this->disconnect[$idx] = null;
// now we have to listen to everything... $this->doSubscribe($con);
// @fixme refactor this nicer. :P
$host = $con->getServer();
$this->_log(LOG_INFO, "Resubscribing to $this->control on $host");
$con->subscribe($this->control);
foreach ($this->subscriptions as $site => $queues) {
foreach ($queues as $queue) {
$this->_log(LOG_INFO, "Resubscribing to $queue on $host");
$con->subscribe($queue);
}
}
$this->begin($idx); $this->begin($idx);
} else { } else {
// Try again later... // Try again later...
@ -487,41 +426,15 @@ class StompQueueManager extends QueueManager
} }
/** /**
* Subscribe to all enabled notice queues for the current site. * Set up all our raw queue subscriptions on the given connection
* @param LiberalStomp $con
*/ */
protected function doSubscribe() protected function doSubscribe(LiberalStomp $con)
{ {
$site = $this->currentSite(); $host = $con->getServer();
$this->_connect(); foreach ($this->subscriptions as $queue) {
foreach ($this->getQueues() as $queue) { $this->_log(LOG_INFO, "Subscribing to $queue on $host");
$rawqueue = $this->queueName($queue); $con->subscribe($queue);
$this->subscriptions[$site][$queue] = $rawqueue;
$this->_log(LOG_INFO, "Subscribing to $rawqueue");
foreach ($this->cons as $con) {
if ($con) {
$con->subscribe($rawqueue);
}
}
}
}
/**
* Subscribe from all enabled notice queues for the current site.
*/
protected function doUnsubscribe()
{
$site = $this->currentSite();
$this->_connect();
if (!empty($this->subscriptions[$site])) {
foreach ($this->subscriptions[$site] as $queue => $rawqueue) {
$this->_log(LOG_INFO, "Unsubscribing from $rawqueue");
foreach ($this->cons as $con) {
if ($con) {
$con->unsubscribe($rawqueue);
}
}
unset($this->subscriptions[$site][$queue]);
}
} }
} }
@ -534,25 +447,29 @@ class StompQueueManager extends QueueManager
* Side effects: in multi-site mode, may reset site configuration to * Side effects: in multi-site mode, may reset site configuration to
* match the site that queued the event. * match the site that queued the event.
* *
* @param int $idx connection index
* @param StompFrame $frame * @param StompFrame $frame
* @return bool * @return bool success
*/ */
protected function handleItem($idx, $frame) protected function handleItem($frame)
{ {
$this->defaultIdx = $idx; $host = $this->cons[$this->defaultIdx]->getServer();
$message = unserialize($frame->body);
$site = $message['site'];
$queue = $message['handler'];
list($site, $queue) = $this->parseDestination($frame->headers['destination']); if ($this->isDeadletter($frame, $message)) {
if ($site != $this->currentSite()) { $this->stats('deadletter', $queue);
$this->stats('switch'); return false;
StatusNet::init($site);
} }
$host = $this->cons[$idx]->getServer(); // @fixme detect failing site switches
$item = $this->decode($frame->body); $this->switchSite($site);
$item = $this->decode($message['payload']);
if (empty($item)) { if (empty($item)) {
$this->_log(LOG_ERR, "Skipping empty or deleted item in queue $queue from $host"); $this->_log(LOG_ERR, "Skipping empty or deleted item in queue $queue from $host");
return true; $this->stats('baditem', $queue);
return false;
} }
$info = $this->logrep($item) . " posted at " . $info = $this->logrep($item) . " posted at " .
$frame->headers['created'] . " in queue $queue from $host"; $frame->headers['created'] . " in queue $queue from $host";
@ -561,16 +478,10 @@ class StompQueueManager extends QueueManager
$handler = $this->getHandler($queue); $handler = $this->getHandler($queue);
if (!$handler) { if (!$handler) {
$this->_log(LOG_ERR, "Missing handler class; skipping $info"); $this->_log(LOG_ERR, "Missing handler class; skipping $info");
$this->ack($idx, $frame);
$this->commit($idx);
$this->begin($idx);
$this->stats('badhandler', $queue); $this->stats('badhandler', $queue);
return false; return false;
} }
// If there's an exception when handling,
// log the error and let it get requeued.
try { try {
$ok = $handler->handle($item); $ok = $handler->handle($item);
} catch (Exception $e) { } catch (Exception $e) {
@ -578,25 +489,80 @@ class StompQueueManager extends QueueManager
$ok = false; $ok = false;
} }
if (!$ok) { if ($ok) {
$this->_log(LOG_INFO, "Successfully handled $info");
$this->stats('handled', $queue);
} else {
$this->_log(LOG_WARNING, "Failed handling $info"); $this->_log(LOG_WARNING, "Failed handling $info");
// FIXME we probably shouldn't have to do // Requeing moves the item to the end of the line for its next try.
// this kind of queue management ourselves; // @fixme add a manual retry count
// if we don't ack, it should resend...
$this->ack($idx, $frame);
$this->enqueue($item, $queue); $this->enqueue($item, $queue);
$this->commit($idx);
$this->begin($idx);
$this->stats('requeued', $queue); $this->stats('requeued', $queue);
}
return $ok;
}
/**
* Check if a redelivered message has been run through enough
* that we're going to give up on it.
*
* @param StompFrame $frame
* @param array $message unserialized message body
* @return boolean true if we should discard
*/
protected function isDeadLetter($frame, $message)
{
if (isset($frame->headers['redelivered']) && $frame->headers['redelivered'] == 'true') {
// Message was redelivered, possibly indicating a previous failure.
$msgId = $frame->headers['message-id'];
$site = $message['site'];
$queue = $message['handler'];
$msgInfo = "message $msgId for $site in queue $queue";
$deliveries = $this->incDeliveryCount($msgId);
if ($deliveries > common_config('queue', 'max_retries')) {
$info = "DEAD-LETTER FILE: Gave up after retry $deliveries on $msgInfo";
$outdir = common_config('queue', 'dead_letter_dir');
if ($outdir) {
$filename = $outdir . "/$site-$queue-" . rawurlencode($msgId);
$info .= ": dumping to $filename";
file_put_contents($filename, $message['payload']);
}
common_log(LOG_ERR, $info);
return true;
} else {
common_log(LOG_INFO, "retry $deliveries on $msgInfo");
}
}
return false; return false;
} }
$this->_log(LOG_INFO, "Successfully handled $info"); /**
$this->ack($idx, $frame); * Update count of times we've re-encountered this message recently,
$this->commit($idx); * triggered when we get a message marked as 'redelivered'.
$this->begin($idx); *
$this->stats('handled', $queue); * Requires a CLI-friendly cache configuration.
return true; *
* @param string $msgId message-id header from message
* @return int number of retries recorded
*/
function incDeliveryCount($msgId)
{
$count = 0;
$cache = common_memcache();
if ($cache) {
$key = 'statusnet:stomp:message-retries:' . $msgId;
$count = $cache->increment($key);
if (!$count) {
$count = 1;
$cache->set($key, $count, null, 3600);
$got = $cache->get($key);
}
}
return $count;
} }
/** /**
@ -629,13 +595,22 @@ class StompQueueManager extends QueueManager
} else { } else {
$this->_log(LOG_ERR, "Ignoring unrecognized control message: $message"); $this->_log(LOG_ERR, "Ignoring unrecognized control message: $message");
} }
$this->ack($idx, $frame);
$this->commit($idx);
$this->begin($idx);
return $shutdown; return $shutdown;
} }
/**
* Switch site, if necessary, and reset current handler assignments
* @param string $site
*/
function switchSite($site)
{
if ($site != StatusNet::currentSite()) {
$this->stats('switch');
StatusNet::switchSite($site);
$this->initialize();
}
}
/** /**
* Set us up with queue subscriptions for a new site added at runtime, * Set us up with queue subscriptions for a new site added at runtime,
* triggered by a broadcast to the 'statusnet-control' topic. * triggered by a broadcast to the 'statusnet-control' topic.
@ -648,22 +623,17 @@ class StompQueueManager extends QueueManager
if (empty($this->sites)) { if (empty($this->sites)) {
if ($nickname == common_config('site', 'nickname')) { if ($nickname == common_config('site', 'nickname')) {
StatusNet::init(common_config('site', 'server')); StatusNet::init(common_config('site', 'server'));
$this->doUnsubscribe();
$this->doSubscribe();
} else { } else {
$this->_log(LOG_INFO, "Ignoring update ping for other site $nickname"); $this->_log(LOG_INFO, "Ignoring update ping for other site $nickname");
} }
} else { } else {
$sn = Status_network::staticGet($nickname); $sn = Status_network::staticGet($nickname);
if ($sn) { if ($sn) {
$server = $sn->getServerName(); // @fixme do config-by-nick $this->switchSite($nickname);
StatusNet::init($server); if (!in_array($nickname, $this->sites)) {
if (empty($this->sites[$server])) { $this->addSite();
$this->addSite($server);
} }
$this->_log(LOG_INFO, "(Re)subscribing to queues for site $nickname / $server"); // @fixme update subscriptions, if applicable
$this->doUnsubscribe();
$this->doSubscribe();
$this->stats('siteupdate'); $this->stats('siteupdate');
} else { } else {
$this->_log(LOG_ERR, "Ignoring ping for unrecognized new site $nickname"); $this->_log(LOG_ERR, "Ignoring ping for unrecognized new site $nickname");
@ -673,42 +643,47 @@ class StompQueueManager extends QueueManager
/** /**
* Combines the queue_basename from configuration with the * Combines the queue_basename from configuration with the
* site server name and queue name to give eg: * group name for this queue to give eg:
* *
* /queue/statusnet/identi.ca/sms * /queue/statusnet/main
* *
* @param string $queue * @param string $queue
* @return string * @return string
*/ */
protected function queueName($queue) protected function queueName($queue)
{ {
return common_config('queue', 'queue_basename') . $base = common_config('queue', 'queue_basename');
$this->currentSite() . '/' . $queue; $group = $this->queueGroup($queue);
$breakout = $this->breakoutMode($queue);
if ($breakout == 'shared') {
return $base . "$group";
} else if ($breakout == 'handler') {
return $base . "$group/$queue";
} else if ($breakout == 'site') {
$site = StatusNet::currentSite();
return $base . "$group/$queue/$site";
}
throw Exception("Unrecognized queue breakout mode '$breakout' for '$queue'");
} }
/** /**
* Returns the site and queue name from the server-side queue. * Get the breakout mode for the given queue on the current site.
* *
* @param string queue destination (eg '/queue/statusnet/identi.ca/sms') * @param string $queue
* @return array of site and queue: ('identi.ca','sms') or false if unrecognized * @return string one of 'shared', 'handler', 'site'
*/ */
protected function parseDestination($dest) protected function breakoutMode($queue)
{ {
$prefix = common_config('queue', 'queue_basename'); $breakout = common_config('queue', 'breakout');
if (substr($dest, 0, strlen($prefix)) == $prefix) { if (isset($breakout[$queue])) {
$rest = substr($dest, strlen($prefix)); return $breakout[$queue];
return explode("/", $rest, 2); } else if (isset($breakout['*'])) {
return $breakout['*'];
} else { } else {
common_log(LOG_ERR, "Got a message from unrecognized stomp queue: $dest"); return 'shared';
return array(false, false);
} }
} }
function _log($level, $msg)
{
common_log($level, 'StompQueueManager: '.$msg);
}
protected function begin($idx) protected function begin($idx)
{ {
if ($this->useTransactions) { if ($this->useTransactions) {

View File

@ -110,9 +110,20 @@ class Theme
$server = common_config('site', 'server'); $server = common_config('site', 'server');
} }
// XXX: protocol $ssl = common_config('theme', 'ssl');
$this->path = 'http://'.$server.$path.$name; if (is_null($ssl)) { // null -> guess
if (common_config('site', 'ssl') == 'always' &&
!common_config('theme', 'server')) {
$ssl = true;
} else {
$ssl = false;
}
}
$protocol = ($ssl) ? 'https' : 'http';
$this->path = $protocol . '://'.$server.$path.$name;
} }
} }

View File

@ -238,9 +238,12 @@ class UserProfile extends Widget
if (Event::handle('StartProfilePageActionsElements', array(&$this->out, $this->profile))) { if (Event::handle('StartProfilePageActionsElements', array(&$this->out, $this->profile))) {
if (empty($cur)) { // not logged in if (empty($cur)) { // not logged in
if (Event::handle('StartProfileRemoteSubscribe', array(&$this->out, $this->profile))) {
$this->out->elementStart('li', 'entity_subscribe'); $this->out->elementStart('li', 'entity_subscribe');
$this->showRemoteSubscribeLink(); $this->showRemoteSubscribeLink();
$this->out->elementEnd('li'); $this->out->elementEnd('li');
Event::handle('EndProfileRemoteSubscribe', array(&$this->out, $this->profile));
}
} else { } else {
if ($cur->id == $this->profile->id) { // your own page if ($cur->id == $this->profile->id) { // your own page
$this->out->elementStart('li', 'entity_edit'); $this->out->elementStart('li', 'entity_edit');

View File

@ -1001,7 +1001,6 @@ function common_enqueue_notice($notice)
$transports[] = 'plugin'; $transports[] = 'plugin';
} }
// @fixme move these checks into QueueManager and/or individual handlers // @fixme move these checks into QueueManager and/or individual handlers
if ($notice->is_local == Notice::LOCAL_PUBLIC || if ($notice->is_local == Notice::LOCAL_PUBLIC ||
$notice->is_local == Notice::LOCAL_NONPUBLIC) { $notice->is_local == Notice::LOCAL_NONPUBLIC) {
@ -1558,3 +1557,56 @@ function common_client_ip()
return array($proxy, $ip); return array($proxy, $ip);
} }
function common_url_to_nickname($url)
{
static $bad = array('query', 'user', 'password', 'port', 'fragment');
$parts = parse_url($url);
# If any of these parts exist, this won't work
foreach ($bad as $badpart) {
if (array_key_exists($badpart, $parts)) {
return null;
}
}
# We just have host and/or path
# If it's just a host...
if (array_key_exists('host', $parts) &&
(!array_key_exists('path', $parts) || strcmp($parts['path'], '/') == 0))
{
$hostparts = explode('.', $parts['host']);
# Try to catch common idiom of nickname.service.tld
if ((count($hostparts) > 2) &&
(strlen($hostparts[count($hostparts) - 2]) > 3) && # try to skip .co.uk, .com.au
(strcmp($hostparts[0], 'www') != 0))
{
return common_nicknamize($hostparts[0]);
} else {
# Do the whole hostname
return common_nicknamize($parts['host']);
}
} else {
if (array_key_exists('path', $parts)) {
# Strip starting, ending slashes
$path = preg_replace('@/$@', '', $parts['path']);
$path = preg_replace('@^/@', '', $path);
if (strpos($path, '/') === false) {
return common_nicknamize($path);
}
}
}
return null;
}
function common_nicknamize($str)
{
$str = preg_replace('/\W/', '', $str);
return strtolower($str);
}

View File

@ -51,6 +51,8 @@ if (!defined('STATUSNET')) {
class MemcachePlugin extends Plugin class MemcachePlugin extends Plugin
{ {
static $cacheInitialized = false;
private $_conn = null; private $_conn = null;
public $servers = array('127.0.0.1;11211'); public $servers = array('127.0.0.1;11211');
@ -71,10 +73,21 @@ class MemcachePlugin extends Plugin
function onInitializePlugin() function onInitializePlugin()
{ {
if (is_null($this->persistent)) { if (self::$cacheInitialized) {
$this->persistent = true;
} else {
// If we're a parent command-line process we need
// to be able to close out the connection after
// forking, so disable persistence.
//
// We'll turn it back on again the second time
// through which will either be in a child process,
// or a single-process script which is switching
// configurations.
$this->persistent = (php_sapi_name() == 'cli') ? false : true; $this->persistent = (php_sapi_name() == 'cli') ? false : true;
} }
$this->_ensureConn(); $this->_ensureConn();
self::$cacheInitialized = true;
return true; return true;
} }
@ -121,6 +134,24 @@ class MemcachePlugin extends Plugin
return false; return false;
} }
/**
* Atomically increment an existing numeric key value.
* Existing expiration time will not be changed.
*
* @param string &$key in; Key to use for lookups
* @param int &$step in; Amount to increment (default 1)
* @param mixed &$value out; Incremented value, or false if key not set.
*
* @return boolean hook success
*/
function onStartCacheIncrement(&$key, &$step, &$value)
{
$this->_ensureConn();
$value = $this->_conn->increment($key, $step);
Event::handle('EndCacheIncrement', array($key, $step, $value));
return false;
}
/** /**
* Delete a value associated with a key * Delete a value associated with a key
* *

View File

@ -112,32 +112,31 @@ class OStatusPlugin extends Plugin
* Set up a PuSH hub link to our internal link for canonical timeline * Set up a PuSH hub link to our internal link for canonical timeline
* Atom feeds for users and groups. * Atom feeds for users and groups.
*/ */
function onStartApiAtom(Action $action) function onStartApiAtom(AtomNoticeFeed $feed)
{ {
if ($action instanceof ApiTimelineUserAction) { $id = null;
if ($feed instanceof AtomUserNoticeFeed) {
$salmonAction = 'salmon'; $salmonAction = 'salmon';
} else if ($action instanceof ApiTimelineGroupAction) { $id = $feed->getUser()->id;
} else if ($feed instanceof AtomGroupNoticeFeed) {
$salmonAction = 'salmongroup'; $salmonAction = 'salmongroup';
$id = $feed->getGroup()->id;
} else { } else {
return; return;
} }
$id = $action->arg('id'); if (!empty($id)) {
if (strval(intval($id)) === strval($id)) {
// Canonical form of id in URL? These are used for OStatus syndication.
$hub = common_config('ostatus', 'hub'); $hub = common_config('ostatus', 'hub');
if (empty($hub)) { if (empty($hub)) {
// Updates will be handled through our internal PuSH hub. // Updates will be handled through our internal PuSH hub.
$hub = common_local_url('pushhub'); $hub = common_local_url('pushhub');
} }
$action->element('link', array('rel' => 'hub', $feed->addLink($hub, array('rel' => 'hub'));
'href' => $hub));
// Also, we'll add in the salmon link // Also, we'll add in the salmon link
$salmon = common_local_url($salmonAction, array('id' => $id)); $salmon = common_local_url($salmonAction, array('id' => $id));
$action->element('link', array('rel' => 'salmon', $feed->addLink($salmon, array('rel' => 'salmon'));
'href' => $salmon));
} }
} }
@ -189,7 +188,7 @@ class OStatusPlugin extends Plugin
/** /**
* Add in an OStatus subscribe button * Add in an OStatus subscribe button
*/ */
function onStartProfilePageActionsElements($output, $profile) function onStartProfileRemoteSubscribe($output, $profile)
{ {
$cur = common_current_user(); $cur = common_current_user();
@ -200,10 +199,12 @@ class OStatusPlugin extends Plugin
array('nickname' => $profile->nickname)); array('nickname' => $profile->nickname));
$output->element('a', array('href' => $url, $output->element('a', array('href' => $url,
'class' => 'entity_remote_subscribe'), 'class' => 'entity_remote_subscribe'),
_m('OStatus')); _m('Subscribe'));
$output->elementEnd('li'); $output->elementEnd('li');
} }
return false;
} }
/** /**
@ -217,6 +218,9 @@ class OStatusPlugin extends Plugin
$count = preg_match_all('/(\w+\.)*\w+@(\w+\.)*\w+(\w+\-\w+)*\.\w+/', $notice->content, $matches); $count = preg_match_all('/(\w+\.)*\w+@(\w+\.)*\w+(\w+\-\w+)*\.\w+/', $notice->content, $matches);
if ($count) { if ($count) {
foreach ($matches[0] as $webfinger) { foreach ($matches[0] as $webfinger) {
// FIXME: look up locally first
// Check to see if we've got an actual webfinger // Check to see if we've got an actual webfinger
$w = new Webfinger; $w = new Webfinger;
@ -237,6 +241,8 @@ class OStatusPlugin extends Plugin
continue; continue;
} }
// FIXME: this needs to go out in a queue handler
$xml = '<?xml version="1.0" encoding="UTF-8" ?>'; $xml = '<?xml version="1.0" encoding="UTF-8" ?>';
$xml .= $notice->asAtomEntry(); $xml .= $notice->asAtomEntry();
@ -273,4 +279,14 @@ class OStatusPlugin extends Plugin
$schema->ensureTable('hubsub', HubSub::schemaDef()); $schema->ensureTable('hubsub', HubSub::schemaDef());
return true; return true;
} }
function onEndShowStatusNetStyles($action) {
$action->cssLink(common_path('plugins/OStatus/theme/base/css/ostatus.css'));
return true;
}
function onEndShowStatusNetScripts($action) {
$action->script(common_path('plugins/OStatus/js/ostatus.js'));
return true;
}
} }

View File

@ -68,8 +68,20 @@ class OStatusInitAction extends Action
function showForm($err = null) function showForm($err = null)
{ {
$this->err = $err; $this->err = $err;
if ($this->boolean('ajax')) {
header('Content-Type: text/xml;charset=utf-8');
$this->xw->startDocument('1.0', 'UTF-8');
$this->elementStart('html');
$this->elementStart('head');
$this->element('title', null, _('Subscribe to user'));
$this->elementEnd('head');
$this->elementStart('body');
$this->showContent();
$this->elementEnd('body');
$this->elementEnd('html');
} else {
$this->showPage(); $this->showPage();
}
} }
function showContent() function showContent()
@ -79,15 +91,15 @@ class OStatusInitAction extends Action
'class' => 'form_settings', 'class' => 'form_settings',
'action' => common_local_url('ostatusinit'))); 'action' => common_local_url('ostatusinit')));
$this->elementStart('fieldset'); $this->elementStart('fieldset');
$this->element('legend', _('Subscribe to a remote user')); $this->element('legend', null, sprintf(_('Subscribe to %s'), $this->nickname));
$this->hidden('token', common_session_token()); $this->hidden('token', common_session_token());
$this->elementStart('ul', 'form_data'); $this->elementStart('ul', 'form_data');
$this->elementStart('li'); $this->elementStart('li', array('id' => 'ostatus_nickname'));
$this->input('nickname', _('User nickname'), $this->nickname, $this->input('nickname', _('User nickname'), $this->nickname,
_('Nickname of the user you want to follow')); _('Nickname of the user you want to follow'));
$this->elementEnd('li'); $this->elementEnd('li');
$this->elementStart('li'); $this->elementStart('li', array('id' => 'ostatus_profile'));
$this->input('acct', _('Profile Account'), $this->acct, $this->input('acct', _('Profile Account'), $this->acct,
_('Your account id (i.e. user@identi.ca)')); _('Your account id (i.e. user@identi.ca)'));
$this->elementEnd('li'); $this->elementEnd('li');

View File

@ -76,7 +76,7 @@ class OStatusSubAction extends Action
$this->elementStart('fieldset', array('id' => 'settings_feeds')); $this->elementStart('fieldset', array('id' => 'settings_feeds'));
$this->elementStart('ul', 'form_data'); $this->elementStart('ul', 'form_data');
$this->elementStart('li', array('id' => 'settings_twitter_login_button')); $this->elementStart('li');
$this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed')); $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed'));
$this->elementEnd('li'); $this->elementEnd('li');
$this->elementEnd('ul'); $this->elementEnd('ul');

View File

@ -61,7 +61,7 @@ class SalmonAction extends Action
// XXX: check that document element is Atom entry // XXX: check that document element is Atom entry
// XXX: check the signature // XXX: check the signature
$this->act = Activity::fromAtomEntry($dom->documentElement); $this->act = new Activity($dom->documentElement);
} }
function handle($args) function handle($args)

View File

@ -75,7 +75,6 @@ class Ostatus_profile extends Memcached_DataObject
public $created; public $created;
public $lastupdate; public $lastupdate;
public /*static*/ function staticGet($k, $v=null) public /*static*/ function staticGet($k, $v=null)
{ {
return parent::staticGet(__CLASS__, $k, $v); return parent::staticGet(__CLASS__, $k, $v);
@ -598,9 +597,10 @@ class Ostatus_profile extends Memcached_DataObject
// Double-check for oldies // Double-check for oldies
// @fixme this could explode horribly for multiple feeds on a blog. sigh // @fixme this could explode horribly for multiple feeds on a blog. sigh
$dupe = new Notice();
$dupe->uri = $notice->uri; $dupe = Notice::staticGet('uri', $notice->uri);
if ($dupe->find(true)) {
if (!empty($dupe)) {
common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}"); common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
continue; continue;
} }

View File

@ -0,0 +1,60 @@
SN.U.DialogBox = {
Subscribe: function(a) {
var f = a.parent().find('#form_ostatus_connect');
if (f.length > 0) {
f.show();
}
else {
$.ajax({
type: 'GET',
dataType: 'xml',
url: a[0].href+'&ajax=1',
beforeSend: function(formData) {
a.addClass('processing');
},
error: function (xhr, textStatus, errorThrown) {
alert(errorThrown || textStatus);
},
success: function(data, textStatus, xhr) {
if (typeof($('form', data)[0]) != 'undefined') {
a.after(document._importNode($('form', data)[0], true));
var form = a.parent().find('#form_ostatus_connect');
form
.addClass('dialogbox')
.append('<button class="close">&#215;</button>');
form
.find('.submit')
.addClass('submit_dialogbox')
.removeClass('submit')
.bind('click', function() {
form.addClass('processing');
});
form.find('button.close').click(function(){
form.hide();
return false;
});
form.find('#acct').focus();
}
a.removeClass('processing');
}
});
}
}
};
SN.Init.Subscribe = function() {
$('.entity_subscribe a').live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; });
};
$(document).ready(function() {
if ($('.entity_subscribe .entity_remote_subscribe').length > 0) {
SN.Init.Subscribe();
}
});

View File

@ -31,7 +31,75 @@ if (!defined('STATUSNET')) {
exit(1); exit(1);
} }
class ActivityNoun /**
* Utilities for turning DOMish things into Activityish things
*
* Some common functions that I didn't have the bandwidth to try to factor
* into some kind of reasonable superclass, so just dumped here. Might
* be useful to have an ActivityObject parent class or something.
*
* @category OStatus
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
class ActivityUtils
{
const ATOM = 'http://www.w3.org/2005/Atom';
const LINK = 'link';
const REL = 'rel';
const TYPE = 'type';
const HREF = 'href';
/**
* Get the permalink for an Activity object
*
* @param DOMElement $element A DOM element
*
* @return string related link, if any
*/
static function getLink($element)
{
$links = $element->getElementsByTagnameNS(self::ATOM, self::LINK);
foreach ($links as $link) {
$rel = $link->getAttribute(self::REL);
$type = $link->getAttribute(self::TYPE);
if ($rel == 'alternate' && $type == 'text/html') {
return $link->getAttribute(self::HREF);
}
}
return null;
}
}
/**
* A noun-ish thing in the activity universe
*
* The activity streams spec talks about activity objects, while also having
* a tag activity:object, which is in fact an activity object. Aaaaaah!
*
* This is just a thing in the activity universe. Can be the subject, object,
* or indirect object (target!) of an activity verb. Rotten name, and I'm
* propagating it. *sigh*
*
* @category OStatus
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
class ActivityObject
{ {
const ARTICLE = 'http://activitystrea.ms/schema/1.0/article'; const ARTICLE = 'http://activitystrea.ms/schema/1.0/article';
const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry'; const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry';
@ -47,19 +115,114 @@ class ActivityNoun
const PERSON = 'http://activitystrea.ms/schema/1.0/person'; const PERSON = 'http://activitystrea.ms/schema/1.0/person';
const GROUP = 'http://activitystrea.ms/schema/1.0/group'; const GROUP = 'http://activitystrea.ms/schema/1.0/group';
const PLACE = 'http://activitystrea.ms/schema/1.0/place'; const PLACE = 'http://activitystrea.ms/schema/1.0/place';
const COMMENT = 'http://activitystrea.ms/schema/1.0/comment'; // tea const COMMENT = 'http://activitystrea.ms/schema/1.0/comment';
// ^^^^^^^^^^ tea!
// Atom elements we snarf
const TITLE = 'title';
const SUMMARY = 'summary';
const CONTENT = 'content';
const ID = 'id';
const SOURCE = 'source';
const NAME = 'name';
const URI = 'uri';
const EMAIL = 'email';
public $type; public $type;
public $id; public $id;
public $title; public $title;
public $summary; public $summary;
public $content; public $content;
public $link;
public $source;
/**
* Constructor
*
* This probably needs to be refactored
* to generate a local class (ActivityPerson, ActivityFile, ...)
* based on the object type.
*
* @param DOMElement $element DOM thing to turn into an Activity thing
*/
function __construct($element)
{
$this->source = $element;
if ($element->tagName == 'author') {
$this->type = self::PERSON; // XXX: is this fair?
$this->title = $this->_childContent($element, self::NAME);
$this->id = $this->_childContent($element, self::URI);
if (empty($this->id)) {
$email = $this->_childContent($element, self::EMAIL);
if (!empty($email)) {
// XXX: acct: ?
$this->id = 'mailto:'.$email;
}
} }
class Activity } else {
{
const NAMESPACE = 'http://activitystrea.ms/schema/1.0/';
$this->type = $this->_childContent($element, Activity::OBJECTTYPE,
Activity::SPEC);
if (empty($this->type)) {
$this->type = ActivityObject::NOTE;
}
$this->id = $this->_childContent($element, self::ID);
$this->title = $this->_childContent($element, self::TITLE);
$this->summary = $this->_childContent($element, self::SUMMARY);
$this->content = $this->_childContent($element, self::CONTENT);
$this->source = $this->_childContent($element, self::SOURCE);
$this->link = ActivityUtils::getLink($element);
// XXX: grab PoCo stuff
}
}
/**
* Grab the text content of a DOM element child of the current element
*
* @param DOMElement $element Element whose children we examine
* @param string $tag Tag to look up
* @param string $namespace Namespace to use, defaults to Atom
*
* @return string content of the child
*/
private function _childContent($element, $tag, $namespace=Activity::ATOM)
{
$els = $element->getElementsByTagnameNS($namespace, $tag);
if (empty($els) || $els->length == 0) {
return null;
} else {
$el = $els->item(0);
return $el->textContent;
}
}
}
/**
* Utility class to hold a bunch of constant defining default verb types
*
* @category OStatus
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
class ActivityVerb
{
const POST = 'http://activitystrea.ms/schema/1.0/post'; const POST = 'http://activitystrea.ms/schema/1.0/post';
const SHARE = 'http://activitystrea.ms/schema/1.0/share'; const SHARE = 'http://activitystrea.ms/schema/1.0/share';
const SAVE = 'http://activitystrea.ms/schema/1.0/save'; const SAVE = 'http://activitystrea.ms/schema/1.0/save';
@ -69,17 +232,162 @@ class Activity
const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend'; const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend';
const JOIN = 'http://activitystrea.ms/schema/1.0/join'; const JOIN = 'http://activitystrea.ms/schema/1.0/join';
const TAG = 'http://activitystrea.ms/schema/1.0/tag'; const TAG = 'http://activitystrea.ms/schema/1.0/tag';
public $actor; // an ActivityNoun
public $verb; // a string (the URL)
public $object; // an ActivityNoun
public $target; // an ActivityNoun
static function fromAtomEntry($domEntry)
{
} }
/**
* An activity in the ActivityStrea.ms world
*
* An activity is kind of like a sentence: someone did something
* to something else.
*
* 'someone' is the 'actor'; 'did something' is the verb;
* 'something else' is the object.
*
* @category OStatus
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
class Activity
{
const SPEC = 'http://activitystrea.ms/spec/1.0/';
const SCHEMA = 'http://activitystrea.ms/schema/1.0/';
const VERB = 'verb';
const OBJECT = 'object';
const ACTOR = 'actor';
const SUBJECT = 'subject';
const OBJECTTYPE = 'object-type';
const CONTEXT = 'context';
const TARGET = 'target';
const ATOM = 'http://www.w3.org/2005/Atom';
const AUTHOR = 'author';
const PUBLISHED = 'published';
const UPDATED = 'updated';
public $actor; // an ActivityObject
public $verb; // a string (the URL)
public $object; // an ActivityObject
public $target; // an ActivityObject
public $context; // an ActivityObject
public $time; // Time of the activity
public $link; // an ActivityObject
public $entry; // the source entry
public $feed; // the source feed
/**
* Turns a regular old Atom <entry> into a magical activity
*
* @param DOMElement $entry Atom entry to poke at
* @param DOMElement $feed Atom feed, for context
*/
function __construct($entry, $feed = null)
{
$this->entry = $entry;
$this->feed = $feed;
$pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM);
if (!empty($pubEl)) {
$this->time = strtotime($pubEl->textContent);
} else {
// XXX technically an error; being liberal. Good idea...?
$updateEl = $this->_child($entry, self::UPDATED, self::ATOM);
if (!empty($updateEl)) {
$this->time = strtotime($updateEl->textContent);
} else {
$this->time = null;
}
}
$this->link = ActivityUtils::getLink($entry);
$verbEl = $this->_child($entry, self::VERB);
if (!empty($verbEl)) {
$this->verb = trim($verbEl->textContent);
} else {
$this->verb = ActivityVerb::POST;
// XXX: do other implied stuff here
}
$objectEl = $this->_child($entry, self::OBJECT);
if (!empty($objectEl)) {
$this->object = new ActivityObject($objectEl);
} else {
$this->object = new ActivityObject($entry);
}
$actorEl = $this->_child($entry, self::ACTOR);
if (!empty($actorEl)) {
$this->actor = new ActivityObject($actorEl);
} else if (!empty($feed) &&
$subjectEl = $this->_child($feed, self::SUBJECT)) {
$this->actor = new ActivityObject($subjectEl);
} else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) {
$this->actor = new ActivityObject($authorEl);
} else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR,
self::ATOM)) {
$this->actor = new ActivityObject($authorEl);
}
$contextEl = $this->_child($entry, self::CONTEXT);
if (!empty($contextEl)) {
$this->context = new ActivityObject($contextEl);
}
$targetEl = $this->_child($entry, self::TARGET);
if (!empty($targetEl)) {
$this->target = new ActivityObject($targetEl);
}
}
/**
* Returns an Atom <entry> based on this activity
*
* @return DOMElement Atom entry
*/
function toAtomEntry() function toAtomEntry()
{ {
return null;
}
/**
* Gets the first child element 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 DOMElement found element or null
*/
private function _child($element, $tag, $namespace=self::SPEC)
{
$els = $element->getElementsByTagnameNS($namespace, $tag);
if (empty($els) || $els->length == 0) {
return null;
} else {
return $els->item(0);
}
} }
} }

View File

@ -0,0 +1,147 @@
<?php
if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
print "This script must be run from the command line\n";
exit();
}
// XXX: we should probably have some common source for this stuff
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
define('STATUSNET', true);
require_once INSTALLDIR . '/lib/common.php';
require_once INSTALLDIR . '/plugins/OStatus/lib/activity.php';
class ActivityParseTests extends PHPUnit_Framework_TestCase
{
public function testExample1()
{
global $_example1;
$dom = DOMDocument::loadXML($_example1);
$act = new Activity($dom->documentElement);
$this->assertFalse(empty($act));
$this->assertEquals($act->time, 1243860840);
$this->assertEquals($act->verb, ActivityVerb::POST);
}
public function testExample3()
{
global $_example3;
$dom = DOMDocument::loadXML($_example3);
$feed = $dom->documentElement;
$entries = $feed->getElementsByTagName('entry');
$entry = $entries->item(0);
$act = new Activity($entry, $feed);
$this->assertFalse(empty($act));
$this->assertEquals($act->time, 1071340202);
$this->assertEquals($act->link, 'http://example.org/2003/12/13/atom03.html');
$this->assertEquals($act->verb, ActivityVerb::POST);
$this->assertFalse(empty($act->actor));
$this->assertEquals($act->actor->type, ActivityObject::PERSON);
$this->assertEquals($act->actor->title, 'John Doe');
$this->assertEquals($act->actor->id, 'mailto:johndoe@example.com');
$this->assertFalse(empty($act->object));
$this->assertEquals($act->object->type, ActivityObject::NOTE);
$this->assertEquals($act->object->id, 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a');
$this->assertEquals($act->object->title, 'Atom-Powered Robots Run Amok');
$this->assertEquals($act->object->summary, 'Some text.');
$this->assertEquals($act->object->link, 'http://example.org/2003/12/13/atom03.html');
$this->assertTrue(empty($act->context));
$this->assertTrue(empty($act->target));
$this->assertEquals($act->entry, $entry);
$this->assertEquals($act->feed, $feed);
}
}
$_example1 = <<<EXAMPLE1
<?xml version='1.0' encoding='UTF-8'?>
<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
<id>tag:versioncentral.example.org,2009:/commit/1643245</id>
<published>2009-06-01T12:54:00Z</published>
<title>Geraldine committed a change to yate</title>
<content type="xhtml">Geraldine just committed a change to yate on VersionCentral</content>
<link rel="alternate" type="text/html"
href="http://versioncentral.example.org/geraldine/yate/commit/1643245" />
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<activity:verb>http://versioncentral.example.org/activity/commit</activity:verb>
<activity:object>
<activity:object-type>http://versioncentral.example.org/activity/changeset</activity:object-type>
<id>tag:versioncentral.example.org,2009:/change/1643245</id>
<title>Punctuation Changeset</title>
<summary>Fixing punctuation because it makes it more readable.</summary>
<link rel="alternate" type="text/html" href="..." />
</activity:object>
</entry>
EXAMPLE1;
$_example2 = <<<EXAMPLE2
<?xml version='1.0' encoding='UTF-8'?>
<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
<id>tag:photopanic.example.com,2008:activity01</id>
<title>Geraldine posted a Photo on PhotoPanic</title>
<published>2008-11-02T15:29:00Z</published>
<link rel="alternate" type="text/html" href="/geraldine/activities/1" />
<activity:verb>
http://activitystrea.ms/schema/1.0/post
</activity:verb>
<activity:object>
<id>tag:photopanic.example.com,2008:photo01</id>
<title>My Cat</title>
<published>2008-11-02T15:29:00Z</published>
<link rel="alternate" type="text/html" href="/geraldine/photos/1" />
<activity:object-type>
tag:atomactivity.example.com,2008:photo
</activity:object-type>
<source>
<title>Geraldine's Photos</title>
<link rel="self" type="application/atom+xml" href="/geraldine/photofeed.xml" />
<link rel="alternate" type="text/html" href="/geraldine/" />
</source>
</activity:object>
<content type="html">
&lt;p&gt;Geraldine posted a Photo on PhotoPanic&lt;/p&gt;
&lt;img src="/geraldine/photo1.jpg"&gt;
</content>
</entry>
EXAMPLE2;
$_example3 = <<<EXAMPLE3
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Example Feed</title>
<subtitle>A subtitle.</subtitle>
<link href="http://example.org/feed/" rel="self" />
<link href="http://example.org/" />
<id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
<updated>2003-12-13T18:30:02Z</updated>
<author>
<name>John Doe</name>
<email>johndoe@example.com</email>
</author>
<entry>
<title>Atom-Powered Robots Run Amok</title>
<link href="http://example.org/2003/12/13/atom03" />
<link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"/>
<link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<updated>2003-12-13T18:30:02Z</updated>
<summary>Some text.</summary>
</entry>
</feed>
EXAMPLE3;

View File

@ -0,0 +1,30 @@
/** theme: base for OStatus
*
* @package StatusNet
* @author Sarven Capadisli <csarven@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
#form_ostatus_connect.dialogbox {
width:70%;
background-image:none;
}
#form_ostatus_connect.dialogbox .form_data label {
width:34%;
}
#form_ostatus_connect.dialogbox .form_data input {
width:57%;
}
#form_ostatus_connect.dialogbox .form_data .form_guide {
margin-left:36%;
}
#form_ostatus_connect.dialogbox #ostatus_nickname {
display:none;
}
#form_ostatus_connect.dialogbox .submit_dialogbox {
min-width:96px;
}

View File

@ -438,49 +438,7 @@ class FinishopenidloginAction extends Action
function urlToNickname($openid) function urlToNickname($openid)
{ {
static $bad = array('query', 'user', 'password', 'port', 'fragment'); return common_url_to_nickname($openid);
$parts = parse_url($openid);
# If any of these parts exist, this won't work
foreach ($bad as $badpart) {
if (array_key_exists($badpart, $parts)) {
return null;
}
}
# We just have host and/or path
# If it's just a host...
if (array_key_exists('host', $parts) &&
(!array_key_exists('path', $parts) || strcmp($parts['path'], '/') == 0))
{
$hostparts = explode('.', $parts['host']);
# Try to catch common idiom of nickname.service.tld
if ((count($hostparts) > 2) &&
(strlen($hostparts[count($hostparts) - 2]) > 3) && # try to skip .co.uk, .com.au
(strcmp($hostparts[0], 'www') != 0))
{
return $this->nicknamize($hostparts[0]);
} else {
# Do the whole hostname
return $this->nicknamize($parts['host']);
}
} else {
if (array_key_exists('path', $parts)) {
# Strip starting, ending slashes
$path = preg_replace('@/$@', '', $parts['path']);
$path = preg_replace('@^/@', '', $path);
if (strpos($path, '/') === false) {
return $this->nicknamize($path);
}
}
}
return null;
} }
function xriToNickname($xri) function xriToNickname($xri)
@ -510,7 +468,6 @@ class FinishopenidloginAction extends Action
function nicknamize($str) function nicknamize($str)
{ {
$str = preg_replace('/\W/', '', $str); return common_nicknamize($str);
return strtolower($str);
} }
} }

View File

@ -45,6 +45,7 @@ class PoweredByStatusNetPlugin extends Plugin
{ {
function onEndAddressData($action) function onEndAddressData($action)
{ {
$action->text(' ');
$action->elementStart('span', 'poweredby'); $action->elementStart('span', 'poweredby');
$action->raw(sprintf(_m('powered by %s'), $action->raw(sprintf(_m('powered by %s'),
sprintf('<a href="http://status.net/">%s</a>', sprintf('<a href="http://status.net/">%s</a>',

View File

@ -20,13 +20,15 @@
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
$shortoptions = 'fi::'; $shortoptions = 'fi::a';
$longoptions = array('id::', 'foreground'); $longoptions = array('id::', 'foreground', 'all');
$helptext = <<<END_OF_IM_HELP $helptext = <<<END_OF_IM_HELP
Daemon script for receiving new notices from IM users. Daemon script for receiving new notices from IM users.
-i --id Identity (default none) -i --id Identity (default none)
-a --all Handle XMPP for all local sites
(requires Stomp queue handler, status_network setup)
-f --foreground Stay in the foreground (default background) -f --foreground Stay in the foreground (default background)
END_OF_IM_HELP; END_OF_IM_HELP;
@ -35,13 +37,16 @@ require_once INSTALLDIR.'/scripts/commandline.inc';
class ImDaemon extends SpawningDaemon class ImDaemon extends SpawningDaemon
{ {
function __construct($id=null, $daemonize=true, $threads=1) protected $allsites = false;
function __construct($id=null, $daemonize=true, $threads=1, $allsites=false)
{ {
if ($threads != 1) { if ($threads != 1) {
// This should never happen. :) // This should never happen. :)
throw new Exception("IMDaemon can must run single-threaded"); throw new Exception("IMDaemon can must run single-threaded");
} }
parent::__construct($id, $daemonize, $threads); parent::__construct($id, $daemonize, $threads);
$this->allsites = $allsites;
} }
function runThread() function runThread()
@ -49,7 +54,7 @@ class ImDaemon extends SpawningDaemon
common_log(LOG_INFO, 'Waiting to listen to IM connections and queues'); common_log(LOG_INFO, 'Waiting to listen to IM connections and queues');
$master = new ImMaster($this->get_id()); $master = new ImMaster($this->get_id());
$master->init(); $master->init($this->allsites);
$master->service(); $master->service();
common_log(LOG_INFO, 'terminating normally'); common_log(LOG_INFO, 'terminating normally');
@ -69,7 +74,9 @@ class ImMaster extends IoMaster
{ {
$classes = array(); $classes = array();
if (Event::handle('StartImDaemonIoManagers', array(&$classes))) { if (Event::handle('StartImDaemonIoManagers', array(&$classes))) {
$classes[] = 'QueueManager'; $qm = QueueManager::get();
$qm->setActiveGroup('im');
$classes[] = $qm;
} }
Event::handle('EndImDaemonIoManagers', array(&$classes)); Event::handle('EndImDaemonIoManagers', array(&$classes));
foreach ($classes as $class) { foreach ($classes as $class) {
@ -87,7 +94,8 @@ if (have_option('i', 'id')) {
} }
$foreground = have_option('f', 'foreground'); $foreground = have_option('f', 'foreground');
$all = have_option('a') || have_option('--all');
$daemon = new ImDaemon($id, !$foreground); $daemon = new ImDaemon($id, !$foreground, 1, $all);
$daemon->runOnce(); $daemon->runOnce();

View File

@ -74,8 +74,6 @@ require_once(INSTALLDIR.'/lib/daemon.php');
require_once(INSTALLDIR.'/classes/Queue_item.php'); require_once(INSTALLDIR.'/classes/Queue_item.php');
require_once(INSTALLDIR.'/classes/Notice.php'); require_once(INSTALLDIR.'/classes/Notice.php');
define('CLAIM_TIMEOUT', 1200);
/** /**
* Queue handling daemon... * Queue handling daemon...
* *
@ -92,7 +90,7 @@ class QueueDaemon extends SpawningDaemon
function __construct($id=null, $daemonize=true, $threads=1, $allsites=false) function __construct($id=null, $daemonize=true, $threads=1, $allsites=false)
{ {
parent::__construct($id, $daemonize, $threads); parent::__construct($id, $daemonize, $threads);
$this->all = $allsites; $this->allsites = $allsites;
} }
/** /**
@ -108,7 +106,7 @@ class QueueDaemon extends SpawningDaemon
$this->log(LOG_INFO, 'checking for queued notices'); $this->log(LOG_INFO, 'checking for queued notices');
$master = new QueueMaster($this->get_id()); $master = new QueueMaster($this->get_id());
$master->init($this->all); $master->init($this->allsites);
try { try {
$master->service(); $master->service();
} catch (Exception $e) { } catch (Exception $e) {
@ -133,14 +131,16 @@ class QueueMaster extends IoMaster
*/ */
function initManagers() function initManagers()
{ {
$classes = array(); $managers = array();
if (Event::handle('StartQueueDaemonIoManagers', array(&$classes))) { if (Event::handle('StartQueueDaemonIoManagers', array(&$managers))) {
$classes[] = 'QueueManager'; $qm = QueueManager::get();
$qm->setActiveGroup('main');
$managers[] = $qm;
} }
Event::handle('EndQueueDaemonIoManagers', array(&$classes)); Event::handle('EndQueueDaemonIoManagers', array(&$managers));
foreach ($classes as $class) { foreach ($managers as $manager) {
$this->instantiate($class); $this->instantiate($manager);
} }
} }
} }

View File

@ -288,7 +288,7 @@ margin-left:18px;
} }
#site_nav_global_primary li { #site_nav_global_primary li {
display:inline; display:inline;
margin-left:11px; margin-left:18px;
} }
.system_notice dt { .system_notice dt {
@ -370,7 +370,7 @@ margin-bottom:11px;
#site_nav_global_secondary ul li { #site_nav_global_secondary ul li {
display:inline; display:inline;
margin-right:11px; margin-right:18px;
} }
#export_data li a { #export_data li a {
padding-left:20px; padding-left:20px;
@ -383,15 +383,13 @@ padding-left:28px;
} }
#export_data ul { #export_data ul {
display:inline; width:100%;
float:left;
} }
#export_data li { #export_data li {
list-style-type:none; list-style-type:none;
display:inline; float:left;
margin-left:11px; margin-right:11px;
}
#export_data li:first-child {
margin-left:0;
} }
#licenses { #licenses {
@ -801,8 +799,8 @@ list-style-type:none;
display:inline; display:inline;
} }
.entity_tags li { .entity_tags li {
display:inline; float:left;
margin-right:4px; margin-right:11px;
} }
.aside .section { .aside .section {
@ -820,6 +818,7 @@ font-size:1em;
#entity_statistics dt, #entity_statistics dt,
#entity_statistics dd { #entity_statistics dd {
display:inline; display:inline;
margin-right:11px;
} }
#entity_statistics dt:after { #entity_statistics dt:after {
content: ":"; content: ":";
@ -1496,6 +1495,11 @@ display:inline;
margin-right:7px; margin-right:7px;
line-height:1.25; line-height:1.25;
} }
.tag-cloud li:before {
content:'\0009';
}
.aside .tag-cloud li { .aside .tag-cloud li {
line-height:1.5; line-height:1.5;
} }