Merge branch 'testing' into 0.9.x

Conflicts:
	index.php
This commit is contained in:
Brion Vibber 2010-04-29 15:14:51 -07:00
commit 2260d6ec7c
15 changed files with 389 additions and 43 deletions

View File

@ -1007,8 +1007,6 @@ class Notice extends Memcached_DataObject
$reply->profile_id = $user->id; $reply->profile_id = $user->id;
$id = $reply->insert(); $id = $reply->insert();
self::blow('reply:stream:%d', $user->id);
} }
} }
@ -1074,6 +1072,7 @@ class Notice extends Memcached_DataObject
throw new ServerException("Couldn't save reply for {$this->id}, {$mentioned->id}"); throw new ServerException("Couldn't save reply for {$this->id}, {$mentioned->id}");
} else { } else {
$replied[$mentioned->id] = 1; $replied[$mentioned->id] = 1;
self::blow('reply:stream:%d', $mentioned->id);
} }
} }
} }
@ -1129,7 +1128,6 @@ class Notice extends Memcached_DataObject
foreach ($recipientIds as $recipientId) { foreach ($recipientIds as $recipientId) {
$user = User::staticGet('id', $recipientId); $user = User::staticGet('id', $recipientId);
if (!empty($user)) { if (!empty($user)) {
self::blow('reply:stream:%d', $recipientId);
mail_notify_attn($user, $this); mail_notify_attn($user, $this);
} }
} }

View File

@ -22,6 +22,20 @@ class Reply extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */ /* the code above is auto generated do not remove the tag below */
###END_AUTOCODE ###END_AUTOCODE
/**
* Wrapper for record insertion to update related caches
*/
function insert()
{
$result = parent::insert();
if ($result) {
self::blow('reply:stream:%d', $this->profile_id);
}
return $result;
}
function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
{ {
$ids = Notice::stream(array('Reply', '_streamDirect'), $ids = Notice::stream(array('Reply', '_streamDirect'),

View File

@ -83,6 +83,7 @@ class Activity
const CREATOR = 'creator'; const CREATOR = 'creator';
const CONTENTNS = 'http://purl.org/rss/1.0/modules/content/'; const CONTENTNS = 'http://purl.org/rss/1.0/modules/content/';
const ENCODED = 'encoded';
public $actor; // an ActivityObject public $actor; // an ActivityObject
public $verb; // a string (the URL) public $verb; // a string (the URL)
@ -269,14 +270,21 @@ class Activity
$this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, self::RSS); $this->title = ActivityUtils::childContent($item, ActivityObject::TITLE, self::RSS);
$contentEl = ActivityUtils::child($item, ActivityUtils::CONTENT, self::CONTENTNS); $contentEl = ActivityUtils::child($item, self::ENCODED, self::CONTENTNS);
if (!empty($contentEl)) { if (!empty($contentEl)) {
$this->content = htmlspecialchars_decode($contentEl->textContent, ENT_QUOTES); // <content:encoded> XML node's text content is HTML; no further processing needed.
$this->content = $contentEl->textContent;
} else { } else {
$descriptionEl = ActivityUtils::child($item, self::DESCRIPTION, self::RSS); $descriptionEl = ActivityUtils::child($item, self::DESCRIPTION, self::RSS);
if (!empty($descriptionEl)) { if (!empty($descriptionEl)) {
$this->content = htmlspecialchars_decode($descriptionEl->textContent, ENT_QUOTES); // Per spec, <description> must be plaintext.
// In practice, often there's HTML... but these days good
// feeds are using <content:encoded> which is explicitly
// real HTML.
// We'll treat this following spec, and do HTML escaping
// to convert from plaintext to HTML.
$this->content = htmlspecialchars($descriptionEl->textContent);
} }
} }

View File

@ -213,11 +213,19 @@ class ActivityUtils
// slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3 // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3
if (empty($type) || $type == 'text') { if (empty($type) || $type == 'text') {
return $el->textContent; // We have plaintext saved as the XML text content.
// Since we want HTML, we need to escape any special chars.
return htmlspecialchars($el->textContent);
} else if ($type == 'html') { } else if ($type == 'html') {
// We have HTML saved as the XML text content.
// No additional processing required once we've got it.
$text = $el->textContent; $text = $el->textContent;
return htmlspecialchars_decode($text, ENT_QUOTES); return $text;
} else if ($type == 'xhtml') { } else if ($type == 'xhtml') {
// Per spec, the <content type="xhtml"> contains a single
// HTML <div> with XHTML namespace on it as a child node.
// We need to pull all of that <div>'s child nodes and
// serialize them back to an (X)HTML source fragment.
$divEl = ActivityUtils::child($el, 'div', 'http://www.w3.org/1999/xhtml'); $divEl = ActivityUtils::child($el, 'div', 'http://www.w3.org/1999/xhtml');
if (empty($divEl)) { if (empty($divEl)) {
return null; return null;

View File

@ -91,6 +91,7 @@ class Plugin
$path = INSTALLDIR . "/plugins/$name/locale"; $path = INSTALLDIR . "/plugins/$name/locale";
if (file_exists($path) && is_dir($path)) { if (file_exists($path) && is_dir($path)) {
bindtextdomain($name, $path); bindtextdomain($name, $path);
bind_textdomain_codeset($name, 'UTF-8');
} }
} }
} }

View File

@ -262,7 +262,7 @@ class BlacklistPlugin extends Plugin
$patterns = $this->_getUrlPatterns(); $patterns = $this->_getUrlPatterns();
foreach ($patterns as $pattern) { foreach ($patterns as $pattern) {
if (preg_match("/$pattern/", $url)) { if ($pattern != '' && preg_match("/$pattern/", $url)) {
return false; return false;
} }
} }
@ -285,7 +285,7 @@ class BlacklistPlugin extends Plugin
$patterns = $this->_getNicknamePatterns(); $patterns = $this->_getNicknamePatterns();
foreach ($patterns as $pattern) { foreach ($patterns as $pattern) {
if (preg_match("/$pattern/", $nickname)) { if ($pattern != '' && preg_match("/$pattern/", $nickname)) {
return false; return false;
} }
} }

View File

@ -94,7 +94,7 @@ class Homepage_blacklist extends Memcached_DataObject
function keys() function keys()
{ {
return array('pattern' => 'K'); return array_keys($this->keyTypes());
} }
/** /**
@ -108,7 +108,7 @@ class Homepage_blacklist extends Memcached_DataObject
function keyTypes() function keyTypes()
{ {
return $this->keys(); return array('pattern' => 'K');
} }
/** /**

View File

@ -88,7 +88,7 @@ class Nickname_blacklist extends Memcached_DataObject
function keys() function keys()
{ {
return array('pattern' => 'K'); return array_keys($this->keyTypes());
} }
/** /**
@ -99,7 +99,7 @@ class Nickname_blacklist extends Memcached_DataObject
function keyTypes() function keyTypes()
{ {
return $this->keys(); return array('pattern' => 'K');
} }
/** /**

View File

@ -88,28 +88,27 @@ class BlacklistadminpanelAction extends AdminPanelAction
function saveSettings() function saveSettings()
{ {
$nickPatterns = array(); $nickPatterns = $this->splitPatterns($this->trimmed('blacklist-nicknames'));
$rawNickPatterns = explode("\n", $this->trimmed('blacklist-nicknames'));
foreach ($rawNickPatterns as $raw) {
$nickPatterns[] = trim($raw);
}
Nickname_blacklist::saveNew($nickPatterns); Nickname_blacklist::saveNew($nickPatterns);
$rawUrlPatterns = explode("\n", $this->trimmed('blacklist-urls')); $urlPatterns = $this->splitPatterns($this->trimmed('blacklist-urls'));
$urlPatterns = array();
foreach ($rawUrlPatterns as $raw) {
$urlPatterns[] = trim($raw);
}
Homepage_blacklist::saveNew($urlPatterns); Homepage_blacklist::saveNew($urlPatterns);
return; return;
} }
protected function splitPatterns($text)
{
$patterns = array();
foreach (explode("\n", $text) as $raw) {
$trimmed = trim($raw);
if ($trimmed != '') {
$patterns[] = $trimmed;
}
}
return $patterns;
}
/** /**
* Validate the values * Validate the values
* *

View File

@ -1001,7 +1001,7 @@ class Ostatus_profile extends Memcached_DataObject
return; return;
} }
if (!common_valid_http_url($url)) { if (!common_valid_http_url($url)) {
throw new ServerException(_m("Invalid avatar URL %s"), $url); throw new ServerException(sprintf(_m("Invalid avatar URL %s"), $url));
} }
if ($this->isGroup()) { if ($this->isGroup()) {
@ -1303,15 +1303,23 @@ class Ostatus_profile extends Memcached_DataObject
$ok = $oprofile->insert(); $ok = $oprofile->insert();
if ($ok) { if (!$ok) {
$avatar = self::getActivityObjectAvatar($object, $hints);
if ($avatar) {
$oprofile->updateAvatar($avatar);
}
return $oprofile;
} else {
throw new ServerException("Can't save OStatus profile"); throw new ServerException("Can't save OStatus profile");
} }
$avatar = self::getActivityObjectAvatar($object, $hints);
if ($avatar) {
try {
$oprofile->updateAvatar($avatar);
} catch (Exception $ex) {
// Profile is saved, but Avatar is messed up. We're
// just going to continue.
common_log(LOG_WARNING, "Exception saving OStatus profile avatar: ". $ex->getMessage());
}
}
return $oprofile;
} }
/** /**
@ -1330,7 +1338,11 @@ class Ostatus_profile extends Memcached_DataObject
} }
$avatar = self::getActivityObjectAvatar($object, $hints); $avatar = self::getActivityObjectAvatar($object, $hints);
if ($avatar) { if ($avatar) {
$this->updateAvatar($avatar); try {
$this->updateAvatar($avatar);
} catch (Exception $ex) {
common_log(LOG_WARNING, "Exception saving OStatus profile avatar: " . $ex->getMessage());
}
} }
} }

View File

@ -0,0 +1,74 @@
#!/usr/bin/env php
<?php
/*
* StatusNet - a distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
$helptext = <<<END_OF_HELP
resub-feed.php [options] http://example.com/atom-feed-url
Reinitialize the PuSH subscription for the given feed. This may help get
things restarted if we and the hub have gotten our states out of sync.
END_OF_HELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
if (empty($args[0]) || !Validate::uri($args[0])) {
print "$helptext";
exit(1);
}
$feedurl = $args[0];
$sub = FeedSub::staticGet('topic', $feedurl);
if (!$sub) {
print "Feed $feedurl is not subscribed.\n";
exit(1);
}
print "Old state:\n";
showSub($sub);
print "\n";
print "Pinging hub $sub->huburi with new subscription for $sub->uri\n";
$ok = $sub->subscribe();
if ($ok) {
print "ok\n";
} else {
print "Could not confirm.\n";
}
$sub2 = FeedSub::staticGet('topic', $feedurl);
print "\n";
print "New state:\n";
showSub($sub2);
function showSub($sub)
{
print " Subscription state: $sub->sub_state\n";
print " Verify token: $sub->verify_token\n";
print " Signature secret: $sub->secret\n";
print " Sub start date: $sub->sub_start\n";
print " Record created: $sub->created\n";
print " Record modified: $sub->modified\n";
}

View File

@ -0,0 +1,147 @@
#!/usr/bin/env php
<?php
/*
* StatusNet - a distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
$helptext = <<<END_OF_HELP
update-profile.php [options] http://example.com/profile/url
Rerun profile and feed info discovery for the given OStatus remote profile,
and reinitialize its PuSH subscription for the given feed. This may help get
things restarted if the hub or feed URLs have changed for the profile.
END_OF_HELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
if (empty($args[0]) || !Validate::uri($args[0])) {
print "$helptext";
exit(1);
}
$uri = $args[0];
$oprofile = Ostatus_profile::staticGet('uri', $uri);
if (!$oprofile) {
print "No OStatus remote profile known for URI $uri\n";
exit(1);
}
print "Old profile state for $oprofile->uri\n";
showProfile($oprofile);
print "\n";
print "Re-running feed discovery for profile URL $oprofile->uri\n";
// @fixme will bork where the URI isn't the profile URL for now
$discover = new FeedDiscovery();
$feedurl = $discover->discoverFromURL($oprofile->uri);
$huburi = $discover->getAtomLink('hub');
$salmonuri = $discover->getAtomLink(Salmon::NS_REPLIES);
print " Feed URL: $feedurl\n";
print " Hub URL: $huburi\n";
print " Salmon URL: $salmonuri\n";
if ($feedurl != $oprofile->feeduri || $salmonuri != $oprofile->salmonuri) {
print "\n";
print "Updating...\n";
// @fixme update keys :P
#$orig = clone($oprofile);
#$oprofile->feeduri = $feedurl;
#$oprofile->salmonuri = $salmonuri;
#$ok = $oprofile->update($orig);
$ok = $oprofile->query('UPDATE ostatus_profile SET ' .
'feeduri=\'' . $oprofile->escape($feedurl) . '\',' .
'salmonuri=\'' . $oprofile->escape($salmonuri) . '\' ' .
'WHERE uri=\'' . $oprofile->escape($uri) . '\'');
if (!$ok) {
print "Failed to update profile record...\n";
exit(1);
}
$oprofile->decache();
} else {
print "\n";
print "Ok, ostatus_profile record unchanged.\n\n";
}
$sub = FeedSub::ensureFeed($feedurl);
if ($huburi != $sub->huburi) {
print "\n";
print "Updating hub record for feed; was $sub->huburi\n";
$orig = clone($sub);
$sub->huburi = $huburi;
$ok = $sub->update($orig);
if (!$ok) {
print "Failed to update sub record...\n";
exit(1);
}
} else {
print "\n";
print "Feed record ok, not changing.\n\n";
}
print "\n";
print "Pinging hub $sub->huburi with new subscription for $sub->uri\n";
$ok = $sub->subscribe();
if ($ok) {
print "ok\n";
} else {
print "Could not confirm.\n";
}
$o2 = Ostatus_profile::staticGet('uri', $uri);
print "\n";
print "New profile state:\n";
showProfile($o2);
print "\n";
print "New feed state:\n";
$sub2 = FeedSub::ensureFeed($feedurl);
showSub($sub2);
function showProfile($oprofile)
{
print " Feed URL: $oprofile->feeduri\n";
print " Salmon URL: $oprofile->salmonuri\n";
print " Avatar URL: $oprofile->avatar\n";
print " Profile ID: $oprofile->profile_id\n";
print " Group ID: $oprofile->group_id\n";
print " Record created: $oprofile->created\n";
print " Record modified: $oprofile->modified\n";
}
function showSub($sub)
{
print " Subscription state: $sub->sub_state\n";
print " Verify token: $sub->verify_token\n";
print " Signature secret: $sub->secret\n";
print " Sub start date: $sub->sub_start\n";
print " Record created: $sub->created\n";
print " Record modified: $sub->modified\n";
}

View File

@ -299,11 +299,21 @@ class AutosubmitAction extends Action
function title() function title()
{ {
return _m('OpenID Auto-Submit'); return _m('OpenID Login Submission');
} }
function showContent() function showContent()
{ {
$this->raw('<p style="margin: 20px 80px">');
// @fixme this would be better using standard CSS class, but the present theme's a bit scary.
$this->element('img', array('src' => Theme::path('images/icons/icon_processing.gif', 'base'),
// for some reason the base CSS sets <img>s as block display?!
'style' => 'display: inline'));
$this->text(_m('Requesting authorization from your login provider...'));
$this->raw('</p>');
$this->raw('<p style="margin-top: 60px; font-style: italic">');
$this->text(_m('If you are not redirected to your login provider in a few seconds, try pushing the button below.'));
$this->raw('</p>');
$this->raw($this->form_html); $this->raw($this->form_html);
} }
@ -311,8 +321,6 @@ class AutosubmitAction extends Action
{ {
parent::showScripts(); parent::showScripts();
$this->element('script', null, $this->element('script', null,
'$(document).ready(function() { ' . 'document.getElementById(\'' . $this->form_id . '\').submit();');
' $(\'#'. $this->form_id .'\').submit(); '.
'});');
} }
} }

View File

@ -100,7 +100,7 @@ class RSSCloudPlugin extends Plugin
* *
* Hook for RouterInitialized event. * Hook for RouterInitialized event.
* *
* @param Mapper &$m URL parser and mapper * @param Mapper $m URL parser and mapper
* *
* @return boolean hook return * @return boolean hook return
*/ */

View File

@ -32,6 +32,18 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase
$this->assertEquals('tag:versioncentral.example.org,2009:/change/1643245', $act->objects[0]->id); $this->assertEquals('tag:versioncentral.example.org,2009:/change/1643245', $act->objects[0]->id);
} }
public function testExample2()
{
global $_example2;
$dom = DOMDocument::loadXML($_example2);
$act = new Activity($dom->documentElement);
$this->assertFalse(empty($act));
// Did we handle <content type="html"> correctly with a typical payload?
$this->assertEquals("<p>Geraldine posted a Photo on PhotoPanic</p>\n " .
"<img src=\"/geraldine/photo1.jpg\">", trim($act->content));
}
public function testExample3() public function testExample3()
{ {
global $_example3; global $_example3;
@ -305,6 +317,71 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase
} }
public function testAtomContent()
{
$tests = array(array("<content>Some regular plain text.</content>",
"Some regular plain text."),
array("<content>&lt;b&gt;this is not HTML&lt;/b&gt;</content>",
"&lt;b&gt;this is not HTML&lt;/b&gt;"),
array("<content type='html'>Some regular plain HTML.</content>",
"Some regular plain HTML."),
array("<content type='html'>&lt;b&gt;this is too HTML&lt;/b&gt;</content>",
"<b>this is too HTML</b>"),
array("<content type='html'>&amp;lt;b&amp;gt;but this is not HTML!&amp;lt;/b&amp;gt;</content>",
"&lt;b&gt;but this is not HTML!&lt;/b&gt;"),
array("<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>Some regular plain XHTML.</div></content>",
"Some regular plain XHTML."),
array("<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'><b>This is some XHTML!</b></div></content>",
"<b>This is some XHTML!</b>"),
array("<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>&lt;b&gt;This is not some XHTML!&lt;/b&gt;</div></content>",
"&lt;b&gt;This is not some XHTML!&lt;/b&gt;"),
array("<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>&amp;lt;b&amp;gt;This is not some XHTML either!&amp;lt;/b&amp;gt;</div></content>",
"&amp;lt;b&amp;gt;This is not some XHTML either!&amp;lt;/b&amp;gt;"));
foreach ($tests as $data) {
list($source, $output) = $data;
$xml = "<entry xmlns='http://www.w3.org/2005/Atom'>" .
"<id>http://example.com/fakeid</id>" .
"<author><name>Test</name></author>" .
"<title>Atom content tests</title>" .
$source .
"</entry>";
$dom = DOMDocument::loadXML($xml);
$act = new Activity($dom->documentElement);
$this->assertFalse(empty($act));
$this->assertEquals($output, trim($act->content));
}
}
public function testRssContent()
{
$tests = array(array("<content:encoded>Some regular plain HTML.</content:encoded>",
"Some regular plain HTML."),
array("<content:encoded>Some &lt;b&gt;exciting bold HTML&lt;/b&gt;</content:encoded>",
"Some <b>exciting bold HTML</b>"),
array("<content:encoded>Some &amp;lt;b&amp;gt;escaped non-HTML.&amp;lt;/b&amp;gt;</content:encoded>",
"Some &lt;b&gt;escaped non-HTML.&lt;/b&gt;"),
array("<description>Some plain text.</description>",
"Some plain text."),
array("<description>Some &lt;b&gt;non-HTML text&lt;/b&gt;</description>",
"Some &lt;b&gt;non-HTML text&lt;/b&gt;"),
array("<description>Some &amp;lt;b&amp;gt;double-escaped text&amp;lt;/b&amp;gt;</description>",
"Some &amp;lt;b&amp;gt;double-escaped text&amp;lt;/b&amp;gt;"));
foreach ($tests as $data) {
list($source, $output) = $data;
$xml = "<item xmlns:content='http://purl.org/rss/1.0/modules/content/'>" .
"<guid>http://example.com/fakeid</guid>" .
"<title>RSS content tests</title>" .
$source .
"</item>";
$dom = DOMDocument::loadXML($xml);
$act = new Activity($dom->documentElement);
$this->assertFalse(empty($act));
$this->assertEquals($output, trim($act->content));
}
}
} }
$_example1 = <<<EXAMPLE1 $_example1 = <<<EXAMPLE1