Merge branch 'testing' into 0.9.x

This commit is contained in:
Evan Prodromou 2010-02-27 17:07:57 -05:00
commit f00e8bbf47
13 changed files with 541 additions and 67 deletions

View File

@ -282,12 +282,6 @@ class Notice extends Memcached_DataObject
$notice->content = $final;
if (!empty($rendered)) {
$notice->rendered = $rendered;
} else {
$notice->rendered = common_render_content($final, $notice);
}
$notice->source = $source;
$notice->uri = $uri;
$notice->url = $url;
@ -315,6 +309,12 @@ class Notice extends Memcached_DataObject
$notice->location_ns = $location_ns;
}
if (!empty($rendered)) {
$notice->rendered = $rendered;
} else {
$notice->rendered = common_render_content($final, $notice);
}
if (Event::handle('StartNoticeSave', array(&$notice))) {
// XXX: some of these functions write to the DB
@ -944,6 +944,8 @@ class Notice extends Memcached_DataObject
$reply->profile_id = $user->id;
$id = $reply->insert();
self::blow('reply:stream:%d', $user->id);
}
}
@ -971,7 +973,10 @@ class Notice extends Memcached_DataObject
$sender = Profile::staticGet($this->profile_id);
$mentions = common_find_mentions($this->profile_id, $this->content);
// @todo ideally this parser information would only
// be calculated once.
$mentions = common_find_mentions($this->content, $this);
$replied = array();

View File

@ -22,7 +22,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
//exit with 200 response, if this is checking fancy from the installer
if (isset($_REQUEST['p']) && $_REQUEST['p'] == 'check-fancy') { exit; }
define('STATUSNET_VERSION', '0.9.0beta6');
define('STATUSNET_VERSION', '0.9.0beta6+bugfix1');
define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
define('STATUSNET_CODENAME', 'Stand');

View File

@ -540,22 +540,40 @@ class NoticeListItem extends Widget
function showContext()
{
$hasConversation = false;
if( !empty($this->notice->conversation)
&& $this->notice->conversation != $this->notice->id){
$hasConversation = true;
}else{
$conversation = Notice::conversationStream($this->notice->id, 1, 1);
if($conversation->N > 0){
if (!empty($this->notice->conversation)) {
$conversation = Notice::conversationStream(
$this->notice->conversation,
1,
1
);
if ($conversation->N > 0) {
$hasConversation = true;
}
}
if ($hasConversation){
$this->out->text(' ');
$convurl = common_local_url('conversation',
array('id' => $this->notice->conversation));
$this->out->element('a', array('href' => $convurl.'#notice-'.$this->notice->id,
'class' => 'response'),
_('in context'));
if ($hasConversation) {
$conv = Conversation::staticGet(
'id',
$this->notice->conversation
);
$convurl = $conv->uri;
if (!empty($convurl)) {
$this->out->text(' ');
$this->out->element(
'a',
array(
'href' => $convurl.'#notice-'.$this->notice->id,
'class' => 'response'),
_('in context')
);
} else {
$msg = sprintf(
"Couldn't find Conversation ID %d to make 'in context'"
. "link for Notice ID %d",
$this->notice->conversation,
$this->notice->id
);
common_log(LOG_WARNING, $msg);
}
}
}

View File

@ -426,14 +426,14 @@ function common_render_content($text, $notice)
{
$r = common_render_text($text);
$id = $notice->profile_id;
$r = common_linkify_mentions($id, $r);
$r = common_linkify_mentions($r, $notice);
$r = preg_replace('/(^|[\s\.\,\:\;]+)!([A-Za-z0-9]{1,64})/e', "'\\1!'.common_group_link($id, '\\2')", $r);
return $r;
}
function common_linkify_mentions($profile_id, $text)
function common_linkify_mentions($text, $notice)
{
$mentions = common_find_mentions($profile_id, $text);
$mentions = common_find_mentions($text, $notice);
// We need to go through in reverse order by position,
// so our positions stay valid despite our fudging with the
@ -487,11 +487,11 @@ function common_linkify_mention($mention)
return $output;
}
function common_find_mentions($profile_id, $text)
function common_find_mentions($text, $notice)
{
$mentions = array();
$sender = Profile::staticGet('id', $profile_id);
$sender = Profile::staticGet('id', $notice->profile_id);
if (empty($sender)) {
return $mentions;
@ -499,6 +499,30 @@ function common_find_mentions($profile_id, $text)
if (Event::handle('StartFindMentions', array($sender, $text, &$mentions))) {
// Get the context of the original notice, if any
$originalAuthor = null;
$originalNotice = null;
$originalMentions = array();
// Is it a reply?
if (!empty($notice) && !empty($notice->reply_to)) {
$originalNotice = Notice::staticGet('id', $notice->reply_to);
if (!empty($originalNotice)) {
$originalAuthor = Profile::staticGet('id', $originalNotice->profile_id);
$ids = $originalNotice->getReplies();
foreach ($ids as $id) {
$repliedTo = Profile::staticGet('id', $id);
if (!empty($repliedTo)) {
$originalMentions[$repliedTo->nickname] = $repliedTo;
}
}
}
}
preg_match_all('/^T ([A-Z0-9]{1,64}) /',
$text,
$tmatches,
@ -514,7 +538,22 @@ function common_find_mentions($profile_id, $text)
foreach ($matches as $match) {
$nickname = common_canonical_nickname($match[0]);
$mentioned = common_relative_profile($sender, $nickname);
// Try to get a profile for this nickname.
// Start with conversation context, then go to
// sender context.
if (!empty($originalAuthor) && $originalAuthor->nickname == $nickname) {
$mentioned = $originalAuthor;
} else if (!empty($originalMentions) &&
array_key_exists($nickname, $originalMentions)) {
$mention = $originalMentions[$nickname];
} else {
$mentioned = common_relative_profile($sender, $nickname);
}
if (!empty($mentioned)) {
@ -849,7 +888,7 @@ function common_relative_profile($sender, $nickname, $dt=null)
return null;
}
function common_local_url($action, $args=null, $params=null, $fragment=null)
function common_local_url($action, $args=null, $params=null, $fragment=null, $addSession=true)
{
$r = Router::get();
$path = $r->build($action, $args, $params, $fragment);
@ -857,12 +896,12 @@ function common_local_url($action, $args=null, $params=null, $fragment=null)
$ssl = common_is_sensitive($action);
if (common_config('site','fancy')) {
$url = common_path(mb_substr($path, 1), $ssl);
$url = common_path(mb_substr($path, 1), $ssl, $addSession);
} else {
if (mb_strpos($path, '/index.php') === 0) {
$url = common_path(mb_substr($path, 1), $ssl);
$url = common_path(mb_substr($path, 1), $ssl, $addSession);
} else {
$url = common_path('index.php'.$path, $ssl);
$url = common_path('index.php'.$path, $ssl, $addSession);
}
}
return $url;
@ -881,7 +920,7 @@ function common_is_sensitive($action)
return $ssl;
}
function common_path($relative, $ssl=false)
function common_path($relative, $ssl=false, $addSession=true)
{
$pathpart = (common_config('site', 'path')) ? common_config('site', 'path')."/" : '';
@ -905,7 +944,9 @@ function common_path($relative, $ssl=false)
}
}
$relative = common_inject_session($relative, $serverpart);
if ($addSession) {
$relative = common_inject_session($relative, $serverpart);
}
return $proto.'://'.$serverpart.'/'.$pathpart.$relative;
}
@ -1127,14 +1168,15 @@ function common_broadcast_profile(Profile $profile)
function common_profile_url($nickname)
{
return common_local_url('showstream', array('nickname' => $nickname));
return common_local_url('showstream', array('nickname' => $nickname),
null, null, false);
}
// Should make up a reasonable root URL
function common_root_url($ssl=false)
{
$url = common_path('', $ssl);
$url = common_path('', $ssl, false);
$i = strpos($url, '?');
if ($i !== false) {
$url = substr($url, 0, $i);
@ -1419,7 +1461,8 @@ function common_remove_magic_from_request()
function common_user_uri(&$user)
{
return common_local_url('userbyid', array('id' => $user->id));
return common_local_url('userbyid', array('id' => $user->id),
null, null, false);
}
function common_notice_uri(&$notice)

View File

@ -99,7 +99,7 @@ class HubSub extends Memcached_DataObject
return array_keys($this->keyTypes());
}
function sequenceKeys()
function sequenceKey()
{
return array(false, false, false);
}

View File

@ -50,7 +50,11 @@ class Magicsig extends Memcached_DataObject
public /*static*/ function staticGet($k, $v=null)
{
$obj = parent::staticGet(__CLASS__, $k, $v);
return Magicsig::fromString($obj->keypair);
if (!empty($obj)) {
return Magicsig::fromString($obj->keypair);
}
return $obj;
}
@ -84,6 +88,10 @@ class Magicsig extends Memcached_DataObject
return array('user_id' => 'K');
}
function sequenceKey() {
return array(false, false, false);
}
function insert()
{
$this->keypair = $this->toString();
@ -173,14 +181,15 @@ class Magicsig extends Memcached_DataObject
switch ($this->alg) {
case 'RSA-SHA256':
return 'sha256';
return 'magicsig_sha256';
}
}
public function sign($bytes)
{
$sig = $this->_rsa->createSign($bytes, null, 'sha256');
$hash = $this->getHash();
$sig = $this->_rsa->createSign($bytes, null, $hash);
if ($this->_rsa->isError()) {
$error = $this->_rsa->getLastError();
common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
@ -192,7 +201,8 @@ class Magicsig extends Memcached_DataObject
public function verify($signed_bytes, $signature)
{
$result = $this->_rsa->validateSign($signed_bytes, $signature, null, 'sha256');
$hash = $this->getHash();
$result = $this->_rsa->validateSign($signed_bytes, $signature, null, $hash);
if ($this->_rsa->isError()) {
$error = $this->keypair->getLastError();
common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
@ -205,7 +215,7 @@ class Magicsig extends Memcached_DataObject
// Define a sha256 function for hashing
// (Crypt_RSA should really be updated to use hash() )
function sha256($bytes)
function magicsig_sha256($bytes)
{
return hash('sha256', $bytes);
}

View File

@ -1288,9 +1288,9 @@ class Ostatus_profile extends Memcached_DataObject
$disco = new Discovery();
$result = $disco->lookup($addr);
if (!$result) {
try {
$result = $disco->lookup($addr);
} catch (Exception $e) {
self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), null);
return null;
}

View File

@ -83,6 +83,28 @@ class MagicEnvelope
}
public function toXML($env) {
$dom = new DOMDocument();
$envelope = $dom->createElementNS(MagicEnvelope::NS, 'me:env');
$envelope->setAttribute('xmlns:me', MagicEnvelope::NS);
$data = $dom->createElementNS(MagicEnvelope::NS, 'me:data', $env['data']);
$data->setAttribute('type', $env['data_type']);
$envelope->appendChild($data);
$enc = $dom->createElementNS(MagicEnvelope::NS, 'me:encoding', $env['encoding']);
$envelope->appendChild($enc);
$alg = $dom->createElementNS(MagicEnvelope::NS, 'me:alg', $env['alg']);
$envelope->appendChild($alg);
$sig = $dom->createElementNS(MagicEnvelope::NS, 'me:sig', $env['sig']);
$envelope->appendChild($sig);
$dom->appendChild($envelope);
return $dom->saveXML();
}
public function unfold($env)
{
$dom = new DOMDocument();

View File

@ -48,11 +48,14 @@ class Salmon
return false;
}
if (!common_config('ostatus', 'skip_signatures')) {
try {
$xml = $this->createMagicEnv($xml, $actor);
} catch (Exception $e) {
common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage());
return false;
}
$headers = array('Content-Type: application/atom+xml');
$headers = array('Content-Type: application/magic-envelope+xml');
try {
$client = new HTTPClient();
@ -72,7 +75,6 @@ class Salmon
public function createMagicEnv($text, $actor)
{
common_log(LOG_DEBUG, "Got actor as : ". print_r($actor, true));
$magic_env = new MagicEnvelope();
$user = User::staticGet('id', $actor->id);
@ -84,7 +86,6 @@ class Salmon
$magickey = new Magicsig();
$magickey->generate($user->id);
}
common_log(LOG_DEBUG, "Salmon: Loaded key for ". $user->id);
} else {
throw new Exception("Salmon invalid actor for signing");
}
@ -92,18 +93,17 @@ class Salmon
try {
$env = $magic_env->signMessage($text, 'application/atom+xml', $magickey->toString());
} catch (Exception $e) {
common_log(LOG_ERR, "Salmon signing failed: ". $e->getMessage());
return $text;
}
return $magic_env->unfold($env);
return $magic_env->toXML($env);
}
public function verifyMagicEnv($dom)
public function verifyMagicEnv($text)
{
$magic_env = new MagicEnvelope();
$env = $magic_env->fromDom($dom);
$env = $magic_env->parse($text);
return $magic_env->verify($env);
}

View File

@ -41,29 +41,32 @@ class SalmonAction extends Action
$this->clientError(_m('This method requires a POST.'));
}
if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
$this->clientError(_m('Salmon requires application/atom+xml'));
if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/magic-envelope+xml') {
$this->clientError(_m('Salmon requires application/magic-envelope+xml'));
}
$xml = file_get_contents('php://input');
$dom = DOMDocument::loadXML($xml);
// Check the signature
$salmon = new Salmon;
if (!$salmon->verifyMagicEnv($xml)) {
common_log(LOG_DEBUG, "Salmon signature verification failed.");
$this->clientError(_m('Salmon signature verification failed.'));
} else {
$magic_env = new MagicEnvelope();
$env = $magic_env->parse($xml);
$xml = $magic_env->unfold($env);
}
$dom = DOMDocument::loadXML($xml);
if ($dom->documentElement->namespaceURI != Activity::ATOM ||
$dom->documentElement->localName != 'entry') {
common_log(LOG_DEBUG, "Got invalid Salmon post: $xml");
$this->clientError(_m('Salmon post must be an Atom entry.'));
}
// Check the signature
$salmon = new Salmon;
if (!common_config('ostatus', 'skip_signatures')) {
if (!$salmon->verifyMagicEnv($dom)) {
common_log(LOG_DEBUG, "Salmon signature verification failed.");
$this->clientError(_m('Salmon signature verification failed.'));
}
}
$this->act = new Activity($dom->documentElement);
return true;
}

View File

@ -0,0 +1,249 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* Throttle registration by IP address
*
* PHP version 5
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Spam
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Throttle registration by IP address
*
* We a) record IP address of registrants and b) throttle registrations.
*
* @category Spam
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
class RegisterThrottlePlugin extends Plugin
{
/**
* Array of time spans in seconds to limits.
*
* Default is 3 registrations per hour, 5 per day, 10 per week.
*/
public $regLimits = array(604800 => 10, // per week
86400 => 5, // per day
3600 => 3); // per hour
/**
* Database schema setup
*
* We store user registrations in a table registration_ip.
*
* @return boolean hook value; true means continue processing, false means stop.
*/
function onCheckSchema()
{
$schema = Schema::get();
// For storing user-submitted flags on profiles
$schema->ensureTable('registration_ip',
array(new ColumnDef('user_id', 'integer', null,
false, 'PRI'),
new ColumnDef('ipaddress', 'varchar', 15, false, 'MUL'),
new ColumnDef('created', 'timestamp', null, false, 'MUL')));
return true;
}
/**
* Load related modules when needed
*
* @param string $cls Name of the class to be loaded
*
* @return boolean hook value; true means continue processing, false means stop.
*/
function onAutoload($cls)
{
$dir = dirname(__FILE__);
switch ($cls)
{
case 'Registration_ip':
include_once $dir . '/'.$cls.'.php';
return false;
default:
return true;
}
}
/**
* Called when someone tries to register.
*
* We check the IP here to determine if it goes over any of our
* configured limits.
*
* @param Action $action Action that is being executed
*
* @return boolean hook value
*
*/
function onStartRegistrationTry($action)
{
$ipaddress = $this->_getIpAddress();
if (empty($ipaddress)) {
throw new ServerException(_m('Cannot find IP address.'));
}
foreach ($this->regLimits as $seconds => $limit) {
$this->debug("Checking $seconds ($limit)");
$reg = $this->_getNthReg($ipaddress, $limit);
if (!empty($reg)) {
$this->debug("Got a {$limit}th registration.");
$regtime = strtotime($reg->created);
$now = time();
$this->debug("Comparing {$regtime} to {$now}");
if ($now - $regtime < $seconds) {
throw new Exception(_("Too many registrations. Take a break and try again later."));
}
}
}
return true;
}
/**
* Called after someone registers.
*
* We record the successful registration and IP address.
*
* @param Action $action Action that is being executed
*
* @return boolean hook value
*
*/
function onEndRegistrationTry($action)
{
$ipaddress = $this->_getIpAddress();
if (empty($ipaddress)) {
throw new ServerException(_m('Cannot find IP address.'));
}
$user = common_current_user();
if (empty($user)) {
throw new ServerException(_m('Cannot find user after successful registration.'));
}
$reg = new Registration_ip();
$reg->user_id = $user->id;
$reg->ipaddress = $ipaddress;
$result = $reg->insert();
if (!$result) {
common_log_db_error($reg, 'INSERT', __FILE__);
// @todo throw an exception?
}
return true;
}
/**
* Check the version of the plugin.
*
* @param array &$versions Version array.
*
* @return boolean hook value
*/
function onPluginVersion(&$versions)
{
$versions[] = array('name' => 'RegisterThrottle',
'version' => STATUSNET_VERSION,
'author' => 'Evan Prodromou',
'homepage' => 'http://status.net/wiki/Plugin:RegisterThrottle',
'description' =>
_m('Throttles excessive registration from a single IP.'));
return true;
}
/**
* Gets the current IP address.
*
* @return string IP address or null if not found.
*/
private function _getIpAddress()
{
$keys = array('HTTP_X_FORWARDED_FOR',
'CLIENT-IP',
'REMOTE_ADDR');
foreach ($keys as $k) {
if (!empty($_SERVER[$k])) {
return $_SERVER[$k];
}
}
return null;
}
/**
* Gets the Nth registration with the given IP address.
*
* @param string $ipaddress Address to key on
* @param integer $n Nth address
*
* @return Registration_ip nth registration or null if not found.
*/
private function _getNthReg($ipaddress, $n)
{
$reg = new Registration_ip();
$reg->ipaddress = $ipaddress;
$reg->orderBy('created DESC');
$reg->limit($n - 1, 1);
if ($reg->find(true)) {
return $reg;
} else {
return null;
}
}
}

View File

@ -0,0 +1,124 @@
<?php
/**
* Data class for storing IP addresses of new registrants.
*
* PHP version 5
*
* @category Data
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*
* 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')) {
exit(1);
}
require_once INSTALLDIR . '/classes/Memcached_DataObject.php';
/**
* Data class for storing IP addresses of new registrants.
*
* @category Spam
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*/
class Registration_ip extends Memcached_DataObject
{
public $__table = 'registration_ip'; // table name
public $user_id; // int(4) primary_key not_null
public $ipaddress; // varchar(15)
public $created; // timestamp
/**
* Get an instance by key
*
* @param string $k Key to use to lookup (usually 'user_id' for this class)
* @param mixed $v Value to lookup
*
* @return User_greeting_count object found, or null for no hits
*
*/
function staticGet($k, $v=null)
{
return Memcached_DataObject::staticGet('Registration_ip', $k, $v);
}
/**
* return table definition for DB_DataObject
*
* @return array array of column definitions
*/
function table()
{
return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
'ipaddress' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
'created' => DB_DATAOBJECT_MYSQLTIMESTAMP + DB_DATAOBJECT_NOTNULL);
}
/**
* return key definitions for DB_DataObject
*
* DB_DataObject needs to know about keys that the table has; this function
* defines them.
*
* @return array key definitions
*/
function keys()
{
return array('user_id' => 'K');
}
/**
* return key definitions for Memcached_DataObject
*
* Our caching system uses the same key definitions, but uses a different
* method to get them.
*
* @return array key definitions
*/
function keyTypes()
{
return $this->keys();
}
/**
* Magic formula for non-autoincrementing integer primary keys
*
* If a table has a single integer column as its primary key, DB_DataObject
* assumes that the column is auto-incrementing and makes a sequence table
* to do this incrementation. Since we don't need this for our class, we
* overload this method and return the magic formula that DB_DataObject needs.
*
* @return array magic three-false array that stops auto-incrementing.
*/
function sequenceKey()
{
return array(false, false, false);
}
}

View File

@ -799,8 +799,8 @@ list-style-type:none;
display:inline;
}
.entity_tags li {
float:left;
margin-right:11px;
display:inline;
margin-right:7px;
}
.aside .section {