Merge branch 'testing' of git@gitorious.org:statusnet/mainline into 0.9.x

This commit is contained in:
Brion Vibber 2010-03-18 17:19:28 -07:00
commit 3e2e88b0df
14 changed files with 236 additions and 175 deletions

4
README
View File

@ -137,7 +137,9 @@ run correctly.
- PHP 5.2.3+. It may be possible to run this software on earlier - PHP 5.2.3+. It may be possible to run this software on earlier
versions of PHP, but many of the functions used are only available versions of PHP, but many of the functions used are only available
in PHP 5.2 or above. in PHP 5.2 or above. 5.2.6 or later is needed for XMPP background
daemons on 64-bit platforms. PHP 5.3.x should work but is known
to cause some failures for OpenID.
- MySQL 5.x. The StatusNet database is stored, by default, in a MySQL - MySQL 5.x. The StatusNet database is stored, by default, in a MySQL
server. It has been primarily tested on 5.x servers, although it may server. It has been primarily tested on 5.x servers, although it may
be possible to install on earlier (or later!) versions. The server be possible to install on earlier (or later!) versions. The server

View File

@ -244,11 +244,17 @@ class ApiStatusesUpdateAction extends ApiAuthAction
$options = array_merge($options, $locOptions); $options = array_merge($options, $locOptions);
} }
$this->notice = try {
Notice::saveNew($this->auth_user->id, $this->notice = Notice::saveNew(
$content, $this->auth_user->id,
$this->source, $content,
$options); $this->source,
$options
);
} catch (Exception $e) {
$this->clientError($e->getMessage());
return;
}
if (isset($upload)) { if (isset($upload)) {
$upload->attachToNotice($this->notice); $upload->attachToNotice($this->notice);

View File

@ -301,6 +301,10 @@ class AvatarsettingsAction extends AccountSettingsAction
$this->showForm($e->getMessage()); $this->showForm($e->getMessage());
return; return;
} }
if ($imagefile === null) {
$this->showForm(_('No file uploaded.'));
return;
}
$cur = common_current_user(); $cur = common_current_user();

View File

@ -62,6 +62,14 @@ class Subscription extends Memcached_DataObject
static function start($subscriber, $other) static function start($subscriber, $other)
{ {
// @fixme should we enforce this as profiles in callers instead?
if ($subscriber instanceof User) {
$subscriber = $subscriber->getProfile();
}
if ($other instanceof User) {
$other = $other->getProfile();
}
if (!$subscriber->hasRight(Right::SUBSCRIBE)) { if (!$subscriber->hasRight(Right::SUBSCRIBE)) {
throw new Exception(_('You have been banned from subscribing.')); throw new Exception(_('You have been banned from subscribing.'));
} }
@ -75,20 +83,7 @@ class Subscription extends Memcached_DataObject
} }
if (Event::handle('StartSubscribe', array($subscriber, $other))) { if (Event::handle('StartSubscribe', array($subscriber, $other))) {
$sub = self::saveNew($subscriber->id, $other->id);
$sub = new Subscription();
$sub->subscriber = $subscriber->id;
$sub->subscribed = $other->id;
$sub->created = common_sql_now();
$result = $sub->insert();
if (!$result) {
common_log_db_error($sub, 'INSERT', __FILE__);
throw new Exception(_('Could not save subscription.'));
}
$sub->notify(); $sub->notify();
self::blow('user:notices_with_friends:%d', $subscriber->id); self::blow('user:notices_with_friends:%d', $subscriber->id);
@ -103,20 +98,11 @@ class Subscription extends Memcached_DataObject
!self::exists($other, $subscriber) && !self::exists($other, $subscriber) &&
!$subscriber->hasBlocked($other)) { !$subscriber->hasBlocked($other)) {
$auto = new Subscription(); try {
self::start($other, $subscriber);
$auto->subscriber = $other->id; } catch (Exception $e) {
$auto->subscribed = $subscriber->id; common_log(LOG_ERR, "Exception during autosubscribe of {$other->nickname} to profile {$subscriber->id}: {$e->getMessage()}");
$auto->created = common_sql_now();
$result = $auto->insert();
if (!$result) {
common_log_db_error($auto, 'INSERT', __FILE__);
throw new Exception(_('Could not save subscription.'));
} }
$auto->notify();
} }
Event::handle('EndSubscribe', array($subscriber, $other)); Event::handle('EndSubscribe', array($subscriber, $other));
@ -125,6 +111,30 @@ class Subscription extends Memcached_DataObject
return true; return true;
} }
/**
* Low-level subscription save.
* Outside callers should use Subscription::start()
*/
protected function saveNew($subscriber_id, $other_id)
{
$sub = new Subscription();
$sub->subscriber = $subscriber_id;
$sub->subscribed = $other_id;
$sub->jabber = 1;
$sub->sms = 1;
$sub->created = common_sql_now();
$result = $sub->insert();
if (!$result) {
common_log_db_error($sub, 'INSERT', __FILE__);
throw new Exception(_('Could not save subscription.'));
}
return $sub;
}
function notify() function notify()
{ {
# XXX: add other notifications (Jabber, SMS) here # XXX: add other notifications (Jabber, SMS) here

View File

@ -75,7 +75,11 @@ class User extends Memcached_DataObject
function getProfile() function getProfile()
{ {
return Profile::staticGet('id', $this->id); $profile = Profile::staticGet('id', $this->id);
if (empty($profile)) {
throw new UserNoProfileException($this);
}
return $profile;
} }
function isSubscribed($other) function isSubscribed($other)
@ -141,9 +145,6 @@ class User extends Memcached_DataObject
function getCurrentNotice() function getCurrentNotice()
{ {
$profile = $this->getProfile(); $profile = $this->getProfile();
if (!$profile) {
return null;
}
return $profile->getCurrentNotice(); return $profile->getCurrentNotice();
} }
@ -152,19 +153,12 @@ class User extends Memcached_DataObject
return Sms_carrier::staticGet('id', $this->carrier); return Sms_carrier::staticGet('id', $this->carrier);
} }
/**
* @deprecated use Subscription::start($sub, $other);
*/
function subscribeTo($other) function subscribeTo($other)
{ {
$sub = new Subscription(); return Subscription::start($this->getProfile(), $other);
$sub->subscriber = $this->id;
$sub->subscribed = $other->id;
$sub->created = common_sql_now(); // current time
if (!$sub->insert()) {
return false;
}
return true;
} }
function hasBlocked($other) function hasBlocked($other)
@ -345,17 +339,7 @@ class User extends Memcached_DataObject
common_log(LOG_WARNING, sprintf("Default user %s does not exist.", $defnick), common_log(LOG_WARNING, sprintf("Default user %s does not exist.", $defnick),
__FILE__); __FILE__);
} else { } else {
$defsub = new Subscription(); Subscription::start($user, $defuser);
$defsub->subscriber = $user->id;
$defsub->subscribed = $defuser->id;
$defsub->created = $user->created;
$result = $defsub->insert();
if (!$result) {
common_log_db_error($defsub, 'INSERT', __FILE__);
return false;
}
} }
} }
@ -471,21 +455,13 @@ class User extends Memcached_DataObject
function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) {
$profile = $this->getProfile(); $profile = $this->getProfile();
if (!$profile) { return $profile->getTaggedNotices($tag, $offset, $limit, $since_id, $before_id);
return null;
} else {
return $profile->getTaggedNotices($tag, $offset, $limit, $since_id, $before_id);
}
} }
function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
{ {
$profile = $this->getProfile(); $profile = $this->getProfile();
if (!$profile) { return $profile->getNotices($offset, $limit, $since_id, $before_id);
return null;
} else {
return $profile->getNotices($offset, $limit, $since_id, $before_id);
}
} }
function favoriteNotices($offset=0, $limit=NOTICES_PER_PAGE, $own=false) function favoriteNotices($offset=0, $limit=NOTICES_PER_PAGE, $own=false)
@ -626,14 +602,12 @@ class User extends Memcached_DataObject
function getSubscriptions($offset=0, $limit=null) function getSubscriptions($offset=0, $limit=null)
{ {
$profile = $this->getProfile(); $profile = $this->getProfile();
assert(!empty($profile));
return $profile->getSubscriptions($offset, $limit); return $profile->getSubscriptions($offset, $limit);
} }
function getSubscribers($offset=0, $limit=null) function getSubscribers($offset=0, $limit=null)
{ {
$profile = $this->getProfile(); $profile = $this->getProfile();
assert(!empty($profile));
return $profile->getSubscribers($offset, $limit); return $profile->getSubscribers($offset, $limit);
} }
@ -697,9 +671,7 @@ class User extends Memcached_DataObject
function delete() function delete()
{ {
$profile = $this->getProfile(); $profile = $this->getProfile();
if ($profile) { $profile->delete();
$profile->delete();
}
$related = array('Fave', $related = array('Fave',
'Confirm_address', 'Confirm_address',

View File

@ -723,7 +723,7 @@ class ActivityObject
} }
} }
static function fromNotice($notice) static function fromNotice(Notice $notice)
{ {
$object = new ActivityObject(); $object = new ActivityObject();
@ -737,7 +737,7 @@ class ActivityObject
return $object; return $object;
} }
static function fromProfile($profile) static function fromProfile(Profile $profile)
{ {
$object = new ActivityObject(); $object = new ActivityObject();

View File

@ -0,0 +1,74 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* class for an exception when the user profile is missing
*
* 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 Exception
* @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/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Class for an exception when the user profile is missing
*
* @category Exception
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
class UserNoProfileException extends ServerException
{
var $user = null;
/**
* constructor
*
* @param User $user User that's missing a profile
*/
public function __construct($user)
{
$this->user = $user;
$message = sprintf(_("User %s (%d) has no profile record."),
$user->nickname, $user->id);
parent::__construct($message);
}
/**
* Accessor for user
*
* @return User the user that triggered this exception
*/
public function getUser()
{
return $this->user;
}
}

View File

@ -290,7 +290,7 @@ class OStatusPlugin extends Plugin
$url = "$scheme://$target"; $url = "$scheme://$target";
$this->log(LOG_INFO, "Checking profile address '$url'"); $this->log(LOG_INFO, "Checking profile address '$url'");
try { try {
$oprofile = Ostatus_profile::ensureProfile($url); $oprofile = Ostatus_profile::ensureProfileURL($url);
if ($oprofile && !$oprofile->isGroup()) { if ($oprofile && !$oprofile->isGroup()) {
$profile = $oprofile->localProfile(); $profile = $oprofile->localProfile();
$matches[$pos] = array('mentioned' => array($profile), $matches[$pos] = array('mentioned' => array($profile),
@ -392,7 +392,7 @@ class OStatusPlugin extends Plugin
foreach ($urls as $url) { foreach ($urls as $url) {
try { try {
return Ostatus_profile::ensureProfile($url); return Ostatus_profile::ensureProfileURL($url);
} catch (Exception $e) { } catch (Exception $e) {
common_log(LOG_ERR, 'Profile lookup failed for ' . common_log(LOG_ERR, 'Profile lookup failed for ' .
$arg . ': ' . $e->getMessage()); $arg . ': ' . $e->getMessage());

View File

@ -299,7 +299,7 @@ class OStatusSubAction extends Action
if ($user->isSubscribed($local)) { if ($user->isSubscribed($local)) {
// TRANS: OStatus remote subscription dialog error. // TRANS: OStatus remote subscription dialog error.
$this->showForm(_m('Already subscribed!')); $this->showForm(_m('Already subscribed!'));
} elseif ($this->oprofile->subscribeLocalToRemote($user)) { } elseif (Subscription::start($user, $local)) {
$this->success(); $this->success();
} else { } else {
// TRANS: OStatus remote subscription dialog error. // TRANS: OStatus remote subscription dialog error.

View File

@ -194,52 +194,6 @@ class Ostatus_profile extends Memcached_DataObject
} }
} }
/**
* Subscribe a local user to this remote user.
* PuSH subscription will be started if necessary, and we'll
* send a Salmon notification to the remote server if available
* notifying them of the sub.
*
* @param User $user
* @return boolean success
* @throws FeedException
*/
public function subscribeLocalToRemote(User $user)
{
if ($this->isGroup()) {
throw new ServerException("Can't subscribe to a remote group");
}
if ($this->subscribe()) {
if ($user->subscribeTo($this->localProfile())) {
$this->notify($user->getProfile(), ActivityVerb::FOLLOW, $this);
return true;
}
}
return false;
}
/**
* Mark this remote profile as subscribing to the given local user,
* and send appropriate notifications to the user.
*
* This will generally be in response to a subscription notification
* from a foreign site to our local Salmon response channel.
*
* @param User $user
* @return boolean success
*/
public function subscribeRemoteToLocal(User $user)
{
if ($this->isGroup()) {
throw new ServerException("Remote groups can't subscribe to local users");
}
Subscription::start($this->localProfile(), $user->getProfile());
return true;
}
/** /**
* Send a subscription request to the hub for this feed. * Send a subscription request to the hub for this feed.
* The hub will later send us a confirmation POST to /main/push/callback. * The hub will later send us a confirmation POST to /main/push/callback.
@ -1460,7 +1414,7 @@ class Ostatus_profile extends Memcached_DataObject
if (array_key_exists('feedurl', $hints)) { if (array_key_exists('feedurl', $hints)) {
try { try {
common_log(LOG_INFO, "Discovery on acct:$addr with feed URL $feedUrl"); common_log(LOG_INFO, "Discovery on acct:$addr with feed URL " . $hints['feedurl']);
$oprofile = self::ensureFeedURL($hints['feedurl'], $hints); $oprofile = self::ensureFeedURL($hints['feedurl'], $hints);
self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri); self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri);
return $oprofile; return $oprofile;
@ -1475,7 +1429,7 @@ class Ostatus_profile extends Memcached_DataObject
if (array_key_exists('profileurl', $hints)) { if (array_key_exists('profileurl', $hints)) {
try { try {
common_log(LOG_INFO, "Discovery on acct:$addr with profile URL $profileUrl"); common_log(LOG_INFO, "Discovery on acct:$addr with profile URL $profileUrl");
$oprofile = self::ensureProfile($hints['profileurl'], $hints); $oprofile = self::ensureProfileURL($hints['profileurl'], $hints);
self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri); self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri);
return $oprofile; return $oprofile;
} catch (Exception $e) { } catch (Exception $e) {

View File

@ -65,17 +65,22 @@ class DiscoveryHints {
{ {
common_debug("starting tidy"); common_debug("starting tidy");
$body = self::_tidy($body); $body = self::_tidy($body, $url);
common_debug("done with tidy"); common_debug("done with tidy");
set_include_path(get_include_path() . PATH_SEPARATOR . INSTALLDIR . '/plugins/OStatus/extlib/hkit/'); set_include_path(get_include_path() . PATH_SEPARATOR . INSTALLDIR . '/plugins/OStatus/extlib/hkit/');
require_once('hkit.class.php'); require_once('hkit.class.php');
$h = new hKit; // hKit code is not clean for notices and warnings
$old = error_reporting();
error_reporting($old & ~E_NOTICE & ~E_WARNING);
$h = new hKit;
$hcards = $h->getByString('hcard', $body); $hcards = $h->getByString('hcard', $body);
error_reporting($old);
if (empty($hcards)) { if (empty($hcards)) {
return array(); return array();
} }
@ -144,39 +149,61 @@ class DiscoveryHints {
return $hints; return $hints;
} }
private static function _tidy($body) /**
* hKit needs well-formed XML for its parsing.
* We'll take the HTML body here and normalize it to XML.
*
* @param string $body HTML document source, possibly not-well-formed
* @param string $url source URL
* @return string well-formed XML document source
* @throws Exception if HTML parsing failed.
*/
private static function _tidy($body, $url)
{ {
if (function_exists('tidy_parse_string')) { if (empty($body)) {
common_debug("Tidying with extension"); throw new Exception("Empty HTML could not be parsed.");
$text = tidy_parse_string($body);
$text = tidy_clean_repair($text);
return $body;
} else if ($fullpath = self::_findProgram('tidy')) {
common_debug("Tidying with program $fullpath");
$tempfile = tempnam('/tmp', 'snht'); // statusnet hcard tidy
file_put_contents($tempfile, $source);
exec("$fullpath -utf8 -indent -asxhtml -numeric -bare -quiet $tempfile", $tidy);
unlink($tempfile);
return implode("\n", $tidy);
} else {
common_debug("Not tidying.");
return $body;
} }
} $dom = new DOMDocument();
private static function _findProgram($name) // Some HTML errors will trigger warnings, but still work.
{ $old = error_reporting();
$path = $_ENV['PATH']; error_reporting($old & ~E_WARNING);
$ok = $dom->loadHTML($body);
$parts = explode(':', $path); error_reporting($old);
foreach ($parts as $part) { if ($ok) {
$fullpath = $part . '/' . $name; // hKit doesn't give us a chance to pass the source URL for
if (is_executable($fullpath)) { // resolving relative links, such as the avatar photo on a
return $fullpath; // Google profile. We'll slip it into a <base> tag if there's
// not already one present.
$bases = $dom->getElementsByTagName('base');
if ($bases && $bases->length >= 1) {
$base = $bases->item(0);
if ($base->hasAttribute('href')) {
$base->setAttribute('href', $url);
}
} else {
$base = $dom->createElement('base');
$base->setAttribute('href', $url);
$heads = $dom->getElementsByTagName('head');
if ($heads || $heads->length) {
$head = $heads->item(0);
} else {
$head = $dom->createElement('head');
$root = $dom->documentRoot;
if ($root->firstChild) {
$root->insertBefore($head, $root->firstChild);
} else {
$root->appendChild($head);
}
}
$head->appendChild($base);
} }
return $dom->saveXML();
} else {
throw new Exception("Invalid HTML could not be parsed.");
} }
return null;
} }
} }

View File

@ -43,21 +43,21 @@ class LinkHeader
static function getLink($response, $rel=null, $type=null) static function getLink($response, $rel=null, $type=null)
{ {
$headers = $response->getHeader('Link'); $headers = $response->getHeader('Link');
if ($headers) {
// Can get an array or string, so try to simplify the path
if (!is_array($headers)) {
$headers = array($headers);
}
// Can get an array or string, so try to simplify the path foreach ($headers as $header) {
if (!is_array($headers)) { $lh = new LinkHeader($header);
$headers = array($headers);
}
foreach ($headers as $header) { if ((is_null($rel) || $lh->rel == $rel) &&
$lh = new LinkHeader($header); (is_null($type) || $lh->type == $type)) {
return $lh->href;
if ((is_null($rel) || $lh->rel == $rel) && }
(is_null($type) || $lh->type == $type)) {
return $lh->href;
} }
} }
return null; return null;
} }
} }

View File

@ -56,7 +56,12 @@ try {
$user = new User(); $user = new User();
if ($user->find()) { if ($user->find()) {
while ($user->fetch()) { while ($user->fetch()) {
updateOStatus($user); try {
updateOStatus($user);
} catch (Exception $e) {
common_log(LOG_NOTICE, "Couldn't convert OMB subscriptions ".
"for {$user->nickname} to OStatus: " . $e->getMessage());
}
} }
} }
} else { } else {
@ -98,7 +103,7 @@ function updateOStatus($user)
echo "Checking {$rp->nickname}..."; echo "Checking {$rp->nickname}...";
} }
$op = Ostatus_profile::ensureProfile($rp->profileurl); $op = Ostatus_profile::ensureProfileURL($rp->profileurl);
if (empty($op)) { if (empty($op)) {
echo "can't convert.\n"; echo "can't convert.\n";
@ -107,8 +112,8 @@ function updateOStatus($user)
if (!have_option('q', 'quiet')) { if (!have_option('q', 'quiet')) {
echo "Converting..."; echo "Converting...";
} }
Subscription::cancel($up, $rp);
Subscription::start($up, $op->localProfile()); Subscription::start($up, $op->localProfile());
Subscription::cancel($up, $rp);
if (!have_option('q', 'quiet')) { if (!have_option('q', 'quiet')) {
echo "done.\n"; echo "done.\n";
} }
@ -118,8 +123,7 @@ function updateOStatus($user)
if (!have_option('q', 'quiet')) { if (!have_option('q', 'quiet')) {
echo "fail.\n"; echo "fail.\n";
} }
continue; common_log(LOG_NOTICE, "Couldn't convert OMB subscription (" . $up->nickname . ", " . $rp->nickname .
common_log(LOG_WARNING, "Couldn't convert OMB subscription (" . $up->nickname . ", " . $rp->nickname .
") to OStatus: " . $e->getMessage()); ") to OStatus: " . $e->getMessage());
continue; continue;
} }

View File

@ -98,7 +98,15 @@ class XmppMaster extends IoMaster
// don't have to find an XMPP site to start up when using --all mode. // don't have to find an XMPP site to start up when using --all mode.
if (common_config('xmpp','enabled')==false) { if (common_config('xmpp','enabled')==false) {
print "Aborting daemon - xmpp is disabled\n"; print "Aborting daemon - xmpp is disabled\n";
exit(); exit(1);
}
if (version_compare(PHP_VERSION, '5.2.6', '<')) {
$arch = php_uname('m');
if ($arch == 'x86_64' || $arch == 'amd64') {
print "Aborting daemon - 64-bit PHP prior to 5.2.6 has known bugs in stream_select; you are running " . PHP_VERSION . " on $arch.\n";
exit(1);
}
} }
if (have_option('i', 'id')) { if (have_option('i', 'id')) {