Merge remote branch 'gitorious/testing' into testing
This commit is contained in:
		| @@ -66,6 +66,12 @@ class ApiGroupMembershipAction extends ApiPrivateAuthAction | ||||
|         parent::prepare($args); | ||||
|  | ||||
|         $this->group    = $this->getTargetGroup($this->arg('id')); | ||||
|         if (empty($this->group)) { | ||||
|             // TRANS: Client error displayed trying to show group membership on a non-existing group. | ||||
|             $this->clientError(_('Group not found.'), 404, $this->format); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         $this->profiles = $this->getProfiles(); | ||||
|  | ||||
|         return true; | ||||
| @@ -84,12 +90,6 @@ class ApiGroupMembershipAction extends ApiPrivateAuthAction | ||||
|     { | ||||
|         parent::handle($args); | ||||
|  | ||||
|         if (empty($this->group)) { | ||||
|             // TRANS: Client error displayed trying to show group membership on a non-existing group. | ||||
|             $this->clientError(_('Group not found.'), 404, $this->format); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // XXX: RSS and Atom | ||||
|  | ||||
|         switch($this->format) { | ||||
|   | ||||
| @@ -377,7 +377,7 @@ class ApiStatusesUpdateAction extends ApiAuthAction | ||||
|     function supported($cmd) | ||||
|     { | ||||
|         static $cmdlist = array('MessageCommand', 'SubCommand', 'UnsubCommand', | ||||
|             'FavCommand', 'OnCommand', 'OffCommand'); | ||||
|             'FavCommand', 'OnCommand', 'OffCommand', 'JoinCommand', 'LeaveCommand'); | ||||
|  | ||||
|         if (in_array(get_class($cmd), $cmdlist)) { | ||||
|             return true; | ||||
|   | ||||
| @@ -200,8 +200,6 @@ class NewgroupAction extends Action | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         $mainpage = common_local_url('showgroup', array('nickname' => $nickname)); | ||||
|  | ||||
|         $cur = common_current_user(); | ||||
|  | ||||
|         // Checked in prepare() above | ||||
| @@ -215,7 +213,6 @@ class NewgroupAction extends Action | ||||
|                                             'location' => $location, | ||||
|                                             'aliases'  => $aliases, | ||||
|                                             'userid'   => $cur->id, | ||||
|                                             'mainpage' => $mainpage, | ||||
|                                             'local'    => true)); | ||||
|  | ||||
|         common_redirect($group->homeUrl(), 303); | ||||
|   | ||||
| @@ -100,8 +100,6 @@ class SubscribersAction extends GalleryAction | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         $subscribers->free(); | ||||
|  | ||||
|         $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, | ||||
|                           $this->page, 'subscribers', | ||||
|                           array('nickname' => $this->user->nickname)); | ||||
|   | ||||
| @@ -106,8 +106,6 @@ class SubscriptionsAction extends GalleryAction | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             $subscriptions->free(); | ||||
|  | ||||
|             $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, | ||||
|                               $this->page, 'subscriptions', | ||||
|                               array('nickname' => $this->user->nickname)); | ||||
|   | ||||
| @@ -487,6 +487,7 @@ class User_group extends Memcached_DataObject | ||||
|         } | ||||
|  | ||||
|         // MAGICALLY put fields into current scope | ||||
|         // @fixme kill extract(); it makes debugging absurdly hard | ||||
|  | ||||
|         extract($fields); | ||||
|  | ||||
| @@ -498,6 +499,9 @@ class User_group extends Memcached_DataObject | ||||
|             // fill in later... | ||||
|             $uri = null; | ||||
|         } | ||||
|         if (empty($mainpage)) { | ||||
|             $mainpage = common_local_url('showgroup', array('nickname' => $nickname)); | ||||
|         } | ||||
|  | ||||
|         $group->nickname    = $nickname; | ||||
|         $group->fullname    = $fullname; | ||||
|   | ||||
| @@ -182,6 +182,9 @@ class Activity | ||||
|         $actorEl = $this->_child($entry, self::ACTOR); | ||||
|  | ||||
|         if (!empty($actorEl)) { | ||||
|             // Standalone <activity:actor> elements are a holdover from older | ||||
|             // versions of ActivityStreams. Newer feeds should have this data | ||||
|             // integrated straight into <atom:author>. | ||||
|  | ||||
|             $this->actor = new ActivityObject($actorEl); | ||||
|  | ||||
| @@ -196,19 +199,24 @@ class Activity | ||||
|                     $this->actor->id = $authorObj->id; | ||||
|                 } | ||||
|             } | ||||
|         } 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)) { | ||||
|  | ||||
|             // An <atom:author> in the entry overrides any author info on | ||||
|             // the surrounding feed. | ||||
|             $this->actor = new ActivityObject($authorEl); | ||||
|  | ||||
|         } else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR, | ||||
|                                                               self::ATOM)) { | ||||
|  | ||||
|             // If there's no <atom:author> on the entry, it's safe to assume | ||||
|             // the containing feed's authorship info applies. | ||||
|             $this->actor = new ActivityObject($authorEl); | ||||
|         } else if (!empty($feed) && | ||||
|                    $subjectEl = $this->_child($feed, self::SUBJECT)) { | ||||
|  | ||||
|             // Feed subject is used for things like groups. | ||||
|             // Should actually possibly not be interpreted as an actor...? | ||||
|             $this->actor = new ActivityObject($subjectEl); | ||||
|         } | ||||
|  | ||||
|         $contextEl = $this->_child($entry, self::CONTEXT); | ||||
|   | ||||
| @@ -1437,41 +1437,23 @@ class ApiAction extends Action | ||||
|     { | ||||
|         if (empty($id)) { | ||||
|             if (self::is_decimal($this->arg('id'))) { | ||||
|                 return User_group::staticGet($this->arg('id')); | ||||
|                 return User_group::staticGet('id', $this->arg('id')); | ||||
|             } else if ($this->arg('id')) { | ||||
|                 $nickname = common_canonical_nickname($this->arg('id')); | ||||
|                 $local = Local_group::staticGet('nickname', $nickname); | ||||
|                 if (empty($local)) { | ||||
|                     return null; | ||||
|                 } else { | ||||
|                     return User_group::staticGet('id', $local->id); | ||||
|                 } | ||||
|                 return User_group::getForNickname($this->arg('id')); | ||||
|             } else if ($this->arg('group_id')) { | ||||
|                 // This is to ensure that a non-numeric user_id still | ||||
|                 // overrides screen_name even if it doesn't get used | ||||
|                 // This is to ensure that a non-numeric group_id still | ||||
|                 // overrides group_name even if it doesn't get used | ||||
|                 if (self::is_decimal($this->arg('group_id'))) { | ||||
|                     return User_group::staticGet('id', $this->arg('group_id')); | ||||
|                 } | ||||
|             } else if ($this->arg('group_name')) { | ||||
|                 $nickname = common_canonical_nickname($this->arg('group_name')); | ||||
|                 $local = Local_group::staticGet('nickname', $nickname); | ||||
|                 if (empty($local)) { | ||||
|                     return null; | ||||
|                 } else { | ||||
|                     return User_group::staticGet('id', $local->group_id); | ||||
|                 } | ||||
|                 return User_group::getForNickname($this->arg('group_name')); | ||||
|             } | ||||
|  | ||||
|         } else if (self::is_decimal($id)) { | ||||
|             return User_group::staticGet($id); | ||||
|             return User_group::staticGet('id', $id); | ||||
|         } else { | ||||
|             $nickname = common_canonical_nickname($id); | ||||
|             $local = Local_group::staticGet('nickname', $nickname); | ||||
|             if (empty($local)) { | ||||
|                 return null; | ||||
|             } else { | ||||
|                 return User_group::staticGet('id', $local->group_id); | ||||
|             } | ||||
|             return User_group::getForNickname($id); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -76,6 +76,10 @@ class ArrayWrapper | ||||
|     function __call($name, $args) | ||||
|     { | ||||
|         $item =& $this->_items[$this->_i]; | ||||
|         if (!is_object($item)) { | ||||
|             common_log(LOG_ERR, "Invalid entry " . var_export($item, true) . " at index $this->_i of $this->N; calling $name()"); | ||||
|             throw new ServerException("Internal error: bad entry in array wrapper list."); | ||||
|         } | ||||
|         return call_user_func_array(array($item, $name), $args); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -95,7 +95,11 @@ function _have_config() | ||||
|     return StatusNet::haveConfig(); | ||||
| } | ||||
|  | ||||
| function __autoload($cls) | ||||
| /** | ||||
|  * Wrapper for class autoloaders. | ||||
|  * This used to be the special function name __autoload(), but that causes bugs with PHPUnit 3.5+ | ||||
|  */ | ||||
| function autoload_sn($cls) | ||||
| { | ||||
|     if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) { | ||||
|         require_once(INSTALLDIR.'/classes/' . $cls . '.php'); | ||||
| @@ -111,6 +115,8 @@ function __autoload($cls) | ||||
|     } | ||||
| } | ||||
|  | ||||
| spl_autoload_register('autoload_sn'); | ||||
|  | ||||
| // XXX: how many of these could be auto-loaded on use? | ||||
| // XXX: note that these files should not use config options | ||||
| // at compile time since DB config options are not yet loaded. | ||||
|   | ||||
| @@ -121,7 +121,7 @@ function mail_notify_from() | ||||
|  | ||||
|         $domain = mail_domain(); | ||||
|  | ||||
|         $notifyfrom = '"'.common_config('site', 'name') .'" <noreply@'.$domain.'>'; | ||||
|         $notifyfrom = '"'. str_replace('"', '\\"', common_config('site', 'name')) .'" <noreply@'.$domain.'>'; | ||||
|     } | ||||
|  | ||||
|     return $notifyfrom; | ||||
|   | ||||
| @@ -16,6 +16,12 @@ jQuery(document).ready(function($){ | ||||
|     contentSelector : "#notices_primary ol.notices", | ||||
|     itemSelector    : "#notices_primary ol.notices li" | ||||
|     },function(){ | ||||
|         SN.Init.Notices(); | ||||
|         // Reply button and attachment magic need to be set up | ||||
|         // for each new notice. | ||||
|         // DO NOT run SN.Init.Notices() which will duplicate stuff. | ||||
|         $(this).find('.notice').each(function() { | ||||
|             SN.U.NoticeReplyTo($(this)); | ||||
|             SN.U.NoticeWithAttachment($(this)); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -21,7 +21,7 @@ | ||||
|     // grab each selector option and see if any fail. | ||||
|     function areSelectorsValid(opts){ | ||||
|       for (var key in opts){ | ||||
|         if (key.indexOf && key.indexOf('Selector') && $(opts[key]).length === 0){ | ||||
|         if (key.indexOf && (key.indexOf('Selector') != -1) && $(opts[key]).length === 0){ | ||||
|             debug('Your ' + key + ' found no elements.');     | ||||
|             return false; | ||||
|         }  | ||||
|   | ||||
| @@ -39,11 +39,41 @@ class Magicsig extends Memcached_DataObject | ||||
|  | ||||
|     public $__table = 'magicsig'; | ||||
|  | ||||
|     /** | ||||
|      * Key to user.id/profile.id for the local user whose key we're storing. | ||||
|      * | ||||
|      * @var int | ||||
|      */ | ||||
|     public $user_id; | ||||
|  | ||||
|     /** | ||||
|      * Flattened string representation of the key pair; callers should | ||||
|      * usually use $this->publicKey and $this->privateKey directly, | ||||
|      * which hold live Crypt_RSA key objects. | ||||
|      * | ||||
|      * @var string | ||||
|      */ | ||||
|     public $keypair; | ||||
|  | ||||
|     /** | ||||
|      * Crypto algorithm used for this key; currently only RSA-SHA256 is supported. | ||||
|      * | ||||
|      * @var string | ||||
|      */ | ||||
|     public $alg; | ||||
|  | ||||
|     /** | ||||
|      * Public RSA key; gets serialized in/out via $this->keypair string. | ||||
|      * | ||||
|      * @var Crypt_RSA | ||||
|      */ | ||||
|     public $publicKey; | ||||
|  | ||||
|     /** | ||||
|      * PrivateRSA key; gets serialized in/out via $this->keypair string. | ||||
|      * | ||||
|      * @var Crypt_RSA | ||||
|      */ | ||||
|     public $privateKey; | ||||
|  | ||||
|     public function __construct($alg = 'RSA-SHA256') | ||||
| @@ -51,6 +81,13 @@ class Magicsig extends Memcached_DataObject | ||||
|         $this->alg = $alg; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Fetch a Magicsig object from the cache or database on a field match. | ||||
|      * | ||||
|      * @param string $k | ||||
|      * @param mixed $v | ||||
|      * @return Magicsig | ||||
|      */ | ||||
|     public /*static*/ function staticGet($k, $v=null) | ||||
|     { | ||||
|         $obj =  parent::staticGet(__CLASS__, $k, $v); | ||||
| @@ -103,6 +140,14 @@ class Magicsig extends Memcached_DataObject | ||||
|         return array(false, false, false); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Save this keypair into the database. | ||||
|      * | ||||
|      * Overloads default insert behavior to encode the live key objects | ||||
|      * as a flat string for storage. | ||||
|      * | ||||
|      * @return mixed | ||||
|      */ | ||||
|     function insert() | ||||
|     { | ||||
|         $this->keypair = $this->toString(); | ||||
| @@ -110,6 +155,14 @@ class Magicsig extends Memcached_DataObject | ||||
|         return parent::insert(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Generate a new keypair for a local user and store in the database. | ||||
|      * | ||||
|      * Warning: this can be very slow on systems without the GMP module. | ||||
|      * Runtimes of 20-30 seconds are not unheard-of. | ||||
|      * | ||||
|      * @param int $user_id id of local user we're creating a key for | ||||
|      */ | ||||
|     public function generate($user_id) | ||||
|     { | ||||
|         $rsa = new Crypt_RSA(); | ||||
| @@ -128,6 +181,12 @@ class Magicsig extends Memcached_DataObject | ||||
|         $this->insert(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Encode the keypair or public key as a string. | ||||
|      * | ||||
|      * @param boolean $full_pair set to false to leave out the private key. | ||||
|      * @return string | ||||
|      */ | ||||
|     public function toString($full_pair = true) | ||||
|     { | ||||
|         $mod = Magicsig::base64_url_encode($this->publicKey->modulus->toBytes()); | ||||
| @@ -140,6 +199,13 @@ class Magicsig extends Memcached_DataObject | ||||
|         return 'RSA.' . $mod . '.' . $exp . $private_exp; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Decode a string representation of an RSA public key or keypair | ||||
|      * as a Magicsig object which can be used to sign or verify. | ||||
|      * | ||||
|      * @param string $text | ||||
|      * @return Magicsig | ||||
|      */ | ||||
|     public static function fromString($text) | ||||
|     { | ||||
|         $magic_sig = new Magicsig(); | ||||
| @@ -168,6 +234,14 @@ class Magicsig extends Memcached_DataObject | ||||
|         return $magic_sig; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Fill out $this->privateKey or $this->publicKey with a Crypt_RSA object | ||||
|      * representing the give key (as mod/exponent pair). | ||||
|      * | ||||
|      * @param string $mod base64-encoded | ||||
|      * @param string $exp base64-encoded exponent | ||||
|      * @param string $type one of 'public' or 'private' | ||||
|      */ | ||||
|     public function loadKey($mod, $exp, $type = 'public') | ||||
|     { | ||||
|         common_log(LOG_DEBUG, "Adding ".$type." key: (".$mod .', '. $exp .")"); | ||||
| @@ -186,11 +260,22 @@ class Magicsig extends Memcached_DataObject | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the name of the crypto algorithm used for this key. | ||||
|      * | ||||
|      * @return string | ||||
|      */ | ||||
|     public function getName() | ||||
|     { | ||||
|         return $this->alg; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the name of a hash function to use for signing with this key. | ||||
|      * | ||||
|      * @return string | ||||
|      * @fixme is this used? doesn't seem to be called by name. | ||||
|      */ | ||||
|     public function getHash() | ||||
|     { | ||||
|         switch ($this->alg) { | ||||
| @@ -200,24 +285,48 @@ class Magicsig extends Memcached_DataObject | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Generate base64-encoded signature for the given byte string | ||||
|      * using our private key. | ||||
|      * | ||||
|      * @param string $bytes as raw byte string | ||||
|      * @return string base64-encoded signature | ||||
|      */ | ||||
|     public function sign($bytes) | ||||
|     { | ||||
|         $sig = $this->privateKey->sign($bytes); | ||||
|         return Magicsig::base64_url_encode($sig); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param string $signed_bytes as raw byte string | ||||
|      * @param string $signature as base64 | ||||
|      * @return boolean | ||||
|      */ | ||||
|     public function verify($signed_bytes, $signature) | ||||
|     { | ||||
|         $signature = Magicsig::base64_url_decode($signature); | ||||
|         return $this->publicKey->verify($signed_bytes, $signature); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * URL-encoding-friendly base64 variant encoding. | ||||
|      * | ||||
|      * @param string $input | ||||
|      * @return string | ||||
|      */ | ||||
|     public static function base64_url_encode($input) | ||||
|     { | ||||
|         return strtr(base64_encode($input), '+/', '-_'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * URL-encoding-friendly base64 variant decoding. | ||||
|      * | ||||
|      * @param string $input | ||||
|      * @return string | ||||
|      */ | ||||
|     public static function base64_url_decode($input) | ||||
|     { | ||||
|         return base64_decode(strtr($input, '-_', '+/')); | ||||
|   | ||||
| @@ -331,6 +331,7 @@ class Ostatus_profile extends Memcached_DataObject | ||||
|      * an acceptable response from the remote site. | ||||
|      * | ||||
|      * @param mixed $entry XML string, Notice, or Activity | ||||
|      * @param Profile $actor | ||||
|      * @return boolean success | ||||
|      */ | ||||
|     public function notifyActivity($entry, $actor) | ||||
|   | ||||
| @@ -80,21 +80,53 @@ class MagicEnvelope | ||||
|         throw new Exception(_m('Unable to locate signer public key.')); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * The current MagicEnvelope spec as used in StatusNet 0.9.7 and later | ||||
|      * includes both the original data and some signing metadata fields as | ||||
|      * the input plaintext for the signature hash. | ||||
|      * | ||||
|      * @param array $env | ||||
|      * @return string | ||||
|      */ | ||||
|     public function signingText($env) { | ||||
|         return implode('.', array($env['data'], // this field is pre-base64'd | ||||
|                             Magicsig::base64_url_encode($env['data_type']), | ||||
|                             Magicsig::base64_url_encode($env['encoding']), | ||||
|                             Magicsig::base64_url_encode($env['alg']))); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param <type> $text | ||||
|      * @param <type> $mimetype | ||||
|      * @param <type> $keypair | ||||
|      * @return array: associative array of envelope properties | ||||
|      * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around | ||||
|      */ | ||||
|     public function signMessage($text, $mimetype, $keypair) | ||||
|     { | ||||
|         $signature_alg = Magicsig::fromString($keypair); | ||||
|         $armored_text = Magicsig::base64_url_encode($text); | ||||
|  | ||||
|         return array( | ||||
|         $env = array( | ||||
|             'data' => $armored_text, | ||||
|             'encoding' => MagicEnvelope::ENCODING, | ||||
|             'data_type' => $mimetype, | ||||
|             'sig' => $signature_alg->sign($armored_text), | ||||
|             'sig' => '', | ||||
|             'alg' => $signature_alg->getName() | ||||
|         ); | ||||
|  | ||||
|         $env['sig'] = $signature_alg->sign($this->signingText($env)); | ||||
|  | ||||
|         return $env; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create an <me:env> XML representation of the envelope. | ||||
|      * | ||||
|      * @param array $env associative array with envelope data | ||||
|      * @return string representation of XML document | ||||
|      * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around | ||||
|      */ | ||||
|     public function toXML($env) { | ||||
|         $xs = new XMLStringer(); | ||||
|         $xs->startXML(); | ||||
| @@ -110,6 +142,16 @@ class MagicEnvelope | ||||
|         return $string; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Extract the contained XML payload, and insert a copy of the envelope | ||||
|      * signature data as an <me:provenance> section. | ||||
|      * | ||||
|      * @param array $env associative array with envelope data | ||||
|      * @return string representation of modified XML document | ||||
|      * | ||||
|      * @fixme in case of XML parsing errors, this will spew to the error log or output | ||||
|      * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around | ||||
|      */ | ||||
|     public function unfold($env) | ||||
|     { | ||||
|         $dom = new DOMDocument(); | ||||
| @@ -136,6 +178,14 @@ class MagicEnvelope | ||||
|         return $dom->saveXML(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Find the author URI referenced in the given Atom entry. | ||||
|      * | ||||
|      * @param string $text string containing Atom entry XML | ||||
|      * @return mixed URI string or false if XML parsing fails, or null if no author URI can be found | ||||
|      * | ||||
|      * @fixme XML parsing failures will spew to error logs/output | ||||
|      */ | ||||
|     public function getAuthor($text) { | ||||
|         $doc = new DOMDocument(); | ||||
|         if (!$doc->loadXML($text)) { | ||||
| @@ -153,11 +203,30 @@ class MagicEnvelope | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check if the author in the Atom entry fragment claims to match | ||||
|      * the given identifier URI. | ||||
|      * | ||||
|      * @param string $text string containing Atom entry XML | ||||
|      * @param string $signer_uri | ||||
|      * @return boolean | ||||
|      */ | ||||
|     public function checkAuthor($text, $signer_uri) | ||||
|     { | ||||
|         return ($this->getAuthor($text) == $signer_uri); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Attempt to verify cryptographic signing for parsed envelope data. | ||||
|      * Requires network access to retrieve public key referenced by the envelope signer. | ||||
|      * | ||||
|      * Details of failure conditions are dumped to output log and not exposed to caller. | ||||
|      * | ||||
|      * @param array $env array representation of magic envelope data, as returned from MagicEnvelope::parse() | ||||
|      * @return boolean | ||||
|      * | ||||
|      * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around | ||||
|      */ | ||||
|     public function verify($env) | ||||
|     { | ||||
|         if ($env['alg'] != 'RSA-SHA256') { | ||||
| @@ -187,15 +256,35 @@ class MagicEnvelope | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return $verifier->verify($env['data'], $env['sig']); | ||||
|         return $verifier->verify($this->signingText($env), $env['sig']); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Extract envelope data from an XML document containing an <me:env> or <me:provenance> element. | ||||
|      * | ||||
|      * @param string XML source | ||||
|      * @return mixed associative array of envelope data, or false on unrecognized input | ||||
|      * | ||||
|      * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around | ||||
|      * @fixme will spew errors to logs or output in case of XML parse errors | ||||
|      * @fixme may give fatal errors if some elements are missing or invalid XML | ||||
|      * @fixme calling DOMDocument::loadXML statically triggers warnings in strict mode | ||||
|      */ | ||||
|     public function parse($text) | ||||
|     { | ||||
|         $dom = DOMDocument::loadXML($text); | ||||
|         return $this->fromDom($dom); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Extract envelope data from an XML document containing an <me:env> or <me:provenance> element. | ||||
|      * | ||||
|      * @param DOMDocument $dom | ||||
|      * @return mixed associative array of envelope data, or false on unrecognized input | ||||
|      * | ||||
|      * @fixme it might be easier to work with storing envelope data these in the object instead of passing arrays around | ||||
|      * @fixme may give fatal errors if some elements are missing | ||||
|      */ | ||||
|     public function fromDom($dom) | ||||
|     { | ||||
|         $env_element = $dom->getElementsByTagNameNS(MagicEnvelope::NS, 'env')->item(0); | ||||
| @@ -218,3 +307,24 @@ class MagicEnvelope | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Variant of MagicEnvelope using the earlier signature form listed in the MagicEnvelope | ||||
|  * spec in early 2010; this was used in StatusNet up through 0.9.6, so for backwards compatiblity | ||||
|  * we still need to accept and sometimes send this format. | ||||
|  */ | ||||
| class MagicEnvelopeCompat extends MagicEnvelope { | ||||
|  | ||||
|     /** | ||||
|      * StatusNet through 0.9.6 used an earlier version of the MagicEnvelope spec | ||||
|      * which used only the input data, without the additional fields, as the plaintext | ||||
|      * for signing. | ||||
|      * | ||||
|      * @param array $env | ||||
|      * @return string | ||||
|      */ | ||||
|     public function signingText($env) { | ||||
|         return $env['data']; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -38,10 +38,12 @@ class Salmon | ||||
|     /** | ||||
|      * Sign and post the given Atom entry as a Salmon message. | ||||
|      * | ||||
|      * @fixme pass through the actor for signing? | ||||
|      * Side effects: may generate a keypair on-demand for the given user, | ||||
|      * which can be very slow on some systems. | ||||
|      * | ||||
|      * @param string $endpoint_uri | ||||
|      * @param string $xml | ||||
|      * @param string $xml string representation of payload | ||||
|      * @param Profile $actor local user profile whose keys to sign with | ||||
|      * @return boolean success | ||||
|      */ | ||||
|     public function post($endpoint_uri, $xml, $actor) | ||||
| @@ -50,34 +52,65 @@ class Salmon | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             $xml = $this->createMagicEnv($xml, $actor); | ||||
|         } catch (Exception $e) { | ||||
|             common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage()); | ||||
|             return false; | ||||
|         } | ||||
|         foreach ($this->formatClasses() as $class) { | ||||
|             try { | ||||
|                 $envelope = $this->createMagicEnv($xml, $actor, $class); | ||||
|             } catch (Exception $e) { | ||||
|                 common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage()); | ||||
|                 return false; | ||||
|             } | ||||
|      | ||||
|             $headers = array('Content-Type: application/magic-envelope+xml'); | ||||
|      | ||||
|             try { | ||||
|                 $client = new HTTPClient(); | ||||
|                 $client->setBody($envelope); | ||||
|                 $response = $client->post($endpoint_uri, $headers); | ||||
|             } catch (HTTP_Request2_Exception $e) { | ||||
|                 common_log(LOG_ERR, "Salmon ($class) post to $endpoint_uri failed: " . $e->getMessage()); | ||||
|                 continue; | ||||
|             } | ||||
|             if ($response->getStatus() != 200) { | ||||
|                 common_log(LOG_ERR, "Salmon ($class) at $endpoint_uri returned status " . | ||||
|                     $response->getStatus() . ': ' . $response->getBody()); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|         $headers = array('Content-Type: application/magic-envelope+xml'); | ||||
|  | ||||
|         try { | ||||
|             $client = new HTTPClient(); | ||||
|             $client->setBody($xml); | ||||
|             $response = $client->post($endpoint_uri, $headers); | ||||
|         } catch (HTTP_Request2_Exception $e) { | ||||
|             common_log(LOG_ERR, "Salmon post to $endpoint_uri failed: " . $e->getMessage()); | ||||
|             return false; | ||||
|             // Success! | ||||
|             return true; | ||||
|         } | ||||
|         if ($response->getStatus() != 200) { | ||||
|             common_log(LOG_ERR, "Salmon at $endpoint_uri returned status " . | ||||
|                 $response->getStatus() . ': ' . $response->getBody()); | ||||
|             return false; | ||||
|         } | ||||
|         return true; | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     public function createMagicEnv($text, $actor) | ||||
|     /** | ||||
|      * List the magic envelope signature class variants in the order we try them. | ||||
|      * Multiples are needed for backwards-compat with StatusNet prior to 0.9.7, | ||||
|      * which used a draft version of the magic envelope spec. | ||||
|      */ | ||||
|     protected function formatClasses() { | ||||
|         return array('MagicEnvelope', 'MagicEnvelopeCompat'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Encode the given string as a signed MagicEnvelope XML document, | ||||
|      * using the keypair for the given local user profile. | ||||
|      * | ||||
|      * Side effects: will create and store a keypair on-demand if one | ||||
|      * hasn't already been generated for this user. This can be very slow | ||||
|      * on some systems. | ||||
|      * | ||||
|      * @param string $text XML fragment to sign, assumed to be Atom | ||||
|      * @param Profile $actor Profile of a local user to use as signer | ||||
|      * @param string $class to override the magic envelope signature version, pass a MagicEnvelope subclass here | ||||
|      * | ||||
|      * @return string XML string representation of magic envelope | ||||
|      * | ||||
|      * @throws Exception on bad profile input or key generation problems | ||||
|      * @fixme if signing fails, this seems to return the original text without warning. Is there a reason for this? | ||||
|      */ | ||||
|     public function createMagicEnv($text, $actor, $class='MagicEnvelope') | ||||
|     { | ||||
|         $magic_env = new MagicEnvelope(); | ||||
|         $magic_env = new $class(); | ||||
|  | ||||
|         $user = User::staticGet('id', $actor->id); | ||||
|         if ($user->id) { | ||||
| @@ -101,12 +134,32 @@ class Salmon | ||||
|         return $magic_env->toXML($env); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check if the given magic envelope is well-formed and correctly signed. | ||||
|      * Needs to have network access to fetch public keys over the web. | ||||
|      * Both current and back-compat signature formats will be checked. | ||||
|      * | ||||
|      * Side effects: exceptions and caching updates may occur during network | ||||
|      * fetches. | ||||
|      * | ||||
|      * @param string $text XML fragment of magic envelope | ||||
|      * @return boolean | ||||
|      * | ||||
|      * @throws Exception on bad profile input or key generation problems | ||||
|      * @fixme could hit fatal errors or spew output on invalid XML | ||||
|      */ | ||||
|     public function verifyMagicEnv($text) | ||||
|     { | ||||
|         $magic_env = new MagicEnvelope(); | ||||
|         foreach ($this->formatClasses() as $class) { | ||||
|             $magic_env = new $class(); | ||||
|  | ||||
|         $env = $magic_env->parse($text); | ||||
|             $env = $magic_env->parse($text); | ||||
|  | ||||
|         return $magic_env->verify($env); | ||||
|             if ($magic_env->verify($env)) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										60
									
								
								plugins/OStatus/tests/MagicEnvelopeTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								plugins/OStatus/tests/MagicEnvelopeTest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| <?php | ||||
|  | ||||
| if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { | ||||
|     print "This script must be run from the command line\n"; | ||||
|     exit(); | ||||
| } | ||||
|  | ||||
| define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..')); | ||||
| define('STATUSNET', true); | ||||
|  | ||||
| require_once INSTALLDIR . '/lib/common.php'; | ||||
|  | ||||
| class MagicEnvelopeTest extends PHPUnit_Framework_TestCase | ||||
| { | ||||
|     /** | ||||
|      * Test that MagicEnvelope builds the correct plaintext for signing. | ||||
|      * @dataProvider provider | ||||
|      */ | ||||
|     public function testSignatureText($env, $expected) | ||||
|     { | ||||
|         $magic = new MagicEnvelope; | ||||
|         $text = $magic->signingText($env); | ||||
|  | ||||
|         $this->assertEquals($expected, $text, "'$text' should be '$expected'"); | ||||
|     } | ||||
|  | ||||
|     static public function provider() | ||||
|     { | ||||
|         return array( | ||||
|             array( | ||||
|                 // Sample case given in spec: | ||||
|                 // http://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-magicsig-00.html#signing | ||||
|                 array( | ||||
|                     'data' => 'Tm90IHJlYWxseSBBdG9t', | ||||
|                     'data_type' => 'application/atom+xml', | ||||
|                     'encoding' => 'base64url', | ||||
|                     'alg' => 'RSA-SHA256' | ||||
|                 ), | ||||
|                 'Tm90IHJlYWxseSBBdG9t.YXBwbGljYXRpb24vYXRvbSt4bWw=.YmFzZTY0dXJs.UlNBLVNIQTI1Ng==' | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Test that MagicEnvelope builds the correct plaintext for signing. | ||||
|      * @dataProvider provider | ||||
|      */ | ||||
|     public function testSignatureTextCompat($env, $expected) | ||||
|     { | ||||
|         // Our old code didn't add the extra fields, just used the armored text. | ||||
|         $alt = $env['data']; | ||||
|  | ||||
|         $magic = new MagicEnvelopeCompat; | ||||
|         $text = $magic->signingText($env); | ||||
|  | ||||
|         $this->assertEquals($alt, $text, "'$text' should be '$alt'"); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -72,6 +72,8 @@ class OStatusTester extends TestBase | ||||
|         $base = 'test' . mt_rand(1, 1000000); | ||||
|         $this->pub = new SNTestClient($this->a, 'pub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout); | ||||
|         $this->sub = new SNTestClient($this->b, 'sub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout); | ||||
|  | ||||
|         $this->group = 'group' . $base; | ||||
|     } | ||||
|  | ||||
|     function run() | ||||
| @@ -163,6 +165,39 @@ class OStatusTester extends TestBase | ||||
|         $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri())); | ||||
|     } | ||||
|  | ||||
|     function testCreateGroup() | ||||
|     { | ||||
|         $this->groupUrl = $this->pub->createGroup($this->group); | ||||
|         $this->assertTrue(!empty($this->groupUrl)); | ||||
|     } | ||||
|  | ||||
|     function testJoinGroup() | ||||
|     { | ||||
|         #$this->assertFalse($this->sub->inGroup($this->groupUrl)); | ||||
|         $this->sub->joinGroup($this->groupUrl); | ||||
|         #$this->assertTrue($this->sub->inGroup($this->groupUrl)); | ||||
|     } | ||||
|  | ||||
|     function testLocalGroupPost() | ||||
|     { | ||||
|         $post = $this->pub->post("Group post from local to !{$this->group}, should go out over push."); | ||||
|         $this->assertNotEqual('', $post); | ||||
|         $this->sub->assertReceived($post); | ||||
|     } | ||||
|  | ||||
|     function testRemoteGroupPost() | ||||
|     { | ||||
|         $post = $this->sub->post("Group post from remote to !{$this->group}, should come in over salmon."); | ||||
|         $this->assertNotEqual('', $post); | ||||
|         $this->pub->assertReceived($post); | ||||
|     } | ||||
|  | ||||
|     function testLeaveGroup() | ||||
|     { | ||||
|         #$this->assertTrue($this->sub->inGroup($this->groupUrl)); | ||||
|         $this->sub->leaveGroup($this->groupUrl); | ||||
|         #$this->assertFalse($this->sub->inGroup($this->groupUrl)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| class SNTestClient extends TestBase | ||||
| @@ -534,6 +569,63 @@ class SNTestClient extends TestBase | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create a group on this site. | ||||
|      * | ||||
|      * @param string $nickname | ||||
|      * @param array $options | ||||
|      * @return string: profile URL for the group | ||||
|      */ | ||||
|     function createGroup($nickname, $options=array()) { | ||||
|         $this->log("Creating group as %s on %s: %s", | ||||
|                    $this->username, | ||||
|                    $this->basepath, | ||||
|                    $nickname); | ||||
|  | ||||
|         $data = $this->api('statusnet/groups/create', 'json', | ||||
|             array_merge(array('nickname' => $nickname), $options)); | ||||
|         $url = $data['url']; | ||||
|  | ||||
|         if ($url) { | ||||
|             $this->log('  created as %s', $url); | ||||
|         } else { | ||||
|             $this->log('  failed? %s', var_export($data, true)); | ||||
|         } | ||||
|         return $url; | ||||
|     } | ||||
|  | ||||
|     function groupInfo($nickname) { | ||||
|         $data = $this->api('statusnet/groups/show', 'json', array( | ||||
|             'id' => $nickname | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Join a group. | ||||
|      * | ||||
|      * @param string $group nickname or URL | ||||
|      */ | ||||
|     function joinGroup($group) { | ||||
|         $this->post('join ' . $group); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Leave a group. | ||||
|      * | ||||
|      * @param string $group nickname or URL | ||||
|      */ | ||||
|     function leaveGroup($group) { | ||||
|         $this->post('drop ' . $group); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param string $nickname | ||||
|      * @return | ||||
|      */ | ||||
|     function inGroup($nickname) { | ||||
|         // @todo | ||||
|     } | ||||
| } | ||||
|  | ||||
| // @fixme switch to commandline.inc? | ||||
|   | ||||
							
								
								
									
										92
									
								
								plugins/OStatus/tests/slap.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								plugins/OStatus/tests/slap.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| #!/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__) . '/../../..')); | ||||
|  | ||||
| $longoptions = array('verify', 'slap=', 'notice='); | ||||
|  | ||||
| $helptext = <<<END_OF_HELP | ||||
| slap.php [options] | ||||
|  | ||||
| Test generation and sending of magic envelopes for Salmon slaps. | ||||
|  | ||||
|      --notice=N   generate entry for this notice number | ||||
|      --verify     send signed magic envelope to Tuomas Koski's test service | ||||
|      --slap=<url> send signed Salmon slap to the destination endpoint | ||||
|  | ||||
|  | ||||
| END_OF_HELP; | ||||
|  | ||||
| require_once INSTALLDIR.'/scripts/commandline.inc'; | ||||
|  | ||||
| if (!have_option('--notice')) { | ||||
|     print "$helptext"; | ||||
|     exit(1); | ||||
| } | ||||
|  | ||||
| $notice_id = get_option_value('--notice'); | ||||
|  | ||||
| $notice = Notice::staticGet('id', $notice_id); | ||||
| $profile = $notice->getProfile(); | ||||
| $entry = $notice->asAtomEntry(true); | ||||
|  | ||||
| echo "== Original entry ==\n\n"; | ||||
| print $entry; | ||||
| print "\n\n"; | ||||
|  | ||||
| $salmon = new Salmon(); | ||||
| $envelope = $salmon->createMagicEnv($entry, $profile); | ||||
|  | ||||
| echo "== Signed envelope ==\n\n"; | ||||
| print $envelope; | ||||
| print "\n\n"; | ||||
|  | ||||
| echo "== Testing local verification ==\n\n"; | ||||
| $ok = $salmon->verifyMagicEnv($envelope); | ||||
| if ($ok) { | ||||
|     print "OK\n\n"; | ||||
| } else { | ||||
|     print "FAIL\n\n"; | ||||
| } | ||||
|  | ||||
| if (have_option('--verify')) { | ||||
|     $url = 'http://www.madebymonsieur.com/ostatus_discovery/magic_env/validate/'; | ||||
|     echo "== Testing remote verification ==\n\n"; | ||||
|     print "Sending for verification to $url ...\n"; | ||||
|  | ||||
|     $client = new HTTPClient(); | ||||
|     $response = $client->post($url, array(), array('magic_env' => $envelope)); | ||||
|  | ||||
|     print $response->getStatus() . "\n\n"; | ||||
|     print $response->getBody() . "\n\n"; | ||||
| } | ||||
|  | ||||
| if (have_option('--slap')) { | ||||
|     $url = get_option_value('--slap'); | ||||
|     echo "== Remote salmon slap ==\n\n"; | ||||
|     print "Sending signed Salmon slap to $url ...\n"; | ||||
|  | ||||
|     $ok = $salmon->post($url, $entry, $profile); | ||||
|     if ($ok) { | ||||
|         print "OK\n\n"; | ||||
|     } else { | ||||
|         print "FAIL\n\n"; | ||||
|     } | ||||
| } | ||||
| @@ -382,6 +382,29 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function testExample10() | ||||
|     { | ||||
|         global $_example10; | ||||
|         $dom = new DOMDocument(); | ||||
|         $dom->loadXML($_example10); | ||||
|  | ||||
|         // example 10 is a PuSH item of a post on a group feed, as generated | ||||
|         // by 0.9.7 code after migration away from <activity:actor> to <author> | ||||
|         $feed = $dom->documentElement; | ||||
|         $entry = $dom->getElementsByTagName('entry')->item(0); | ||||
|         $expected = 'http://lazarus.local/mublog/user/557'; | ||||
|  | ||||
|         // Reading just the entry alone should pick up its own <author> | ||||
|         // as the actor. | ||||
|         $act = new Activity($entry); | ||||
|         $this->assertEquals($act->actor->id, $expected); | ||||
|  | ||||
|         // Reading the entry in feed context used to be buggy, picking up | ||||
|         // the feed's <activity:subject> which referred to the group. | ||||
|         // It should now be returning the expected author entry... | ||||
|         $act = new Activity($entry, $feed); | ||||
|         $this->assertEquals($act->actor->id, $expected); | ||||
|     } | ||||
| } | ||||
|  | ||||
| $_example1 = <<<EXAMPLE1 | ||||
| @@ -792,3 +815,93 @@ $_example9 = <<<EXAMPLE9 | ||||
|     </entry> | ||||
| </feed> | ||||
| EXAMPLE9; | ||||
|  | ||||
| // Sample PuSH entry from a group feed in 0.9.7 | ||||
| // Old <activity:actor> has been removed from entries in this version. | ||||
| // A bug in the order of input processing meant that we were incorrectly | ||||
| // reading the feed's <activity:subject> instead of the entry's <author>, | ||||
| // causing the entry to get rejected as malformed (groups can't post on | ||||
| // their own; we want to see the actual author's info here). | ||||
| $_example10 = <<<EXAMPLE10 | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/"> | ||||
|  <generator uri="http://status.net" version="0.9.7alpha1">StatusNet</generator> | ||||
|  <id>http://lazarus.local/mublog/api/statusnet/groups/timeline/22.atom</id> | ||||
|  <title>grouptest316173 timeline</title> | ||||
|  <subtitle>Updates from grouptest316173 on Blaguette!</subtitle> | ||||
|  <logo>http://lazarus.local/mublog/theme/default/default-avatar-profile.png</logo> | ||||
|  <updated>2011-01-06T22:44:18+00:00</updated> | ||||
| <author> | ||||
|  <activity:object-type>http://activitystrea.ms/schema/1.0/group</activity:object-type> | ||||
|  <uri>http://lazarus.local/mublog/group/22/id</uri> | ||||
|  <name>grouptest316173</name> | ||||
|  <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/group/22/id"/> | ||||
|  <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://lazarus.local/mublog/theme/default/default-avatar-profile.png"/> | ||||
|  <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://lazarus.local/mublog/theme/default/default-avatar-stream.png"/> | ||||
|  <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://lazarus.local/mublog/theme/default/default-avatar-mini.png"/> | ||||
|  <poco:preferredUsername>grouptest316173</poco:preferredUsername> | ||||
|  <poco:displayName>grouptest316173</poco:displayName> | ||||
| </author> | ||||
| <activity:subject> | ||||
|  <activity:object-type>http://activitystrea.ms/schema/1.0/group</activity:object-type> | ||||
|  <id>http://lazarus.local/mublog/group/22/id</id> | ||||
|  <title>grouptest316173</title> | ||||
|  <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/group/22/id"/> | ||||
|  <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://lazarus.local/mublog/theme/default/default-avatar-profile.png"/> | ||||
|  <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://lazarus.local/mublog/theme/default/default-avatar-stream.png"/> | ||||
|  <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://lazarus.local/mublog/theme/default/default-avatar-mini.png"/> | ||||
|  <poco:preferredUsername>grouptest316173</poco:preferredUsername> | ||||
|  <poco:displayName>grouptest316173</poco:displayName> | ||||
| </activity:subject> | ||||
|  <link href="http://lazarus.local/mublog/group/grouptest316173" rel="alternate" type="text/html"/> | ||||
|  <link href="http://lazarus.local/mublog/main/push/hub" rel="hub"/> | ||||
|  <link href="http://lazarus.local/mublog/main/salmon/group/22" rel="salmon"/> | ||||
|  <link href="http://lazarus.local/mublog/main/salmon/group/22" rel="http://salmon-protocol.org/ns/salmon-replies"/> | ||||
|  <link href="http://lazarus.local/mublog/main/salmon/group/22" rel="http://salmon-protocol.org/ns/salmon-mention"/> | ||||
|  <link href="http://lazarus.local/mublog/api/statusnet/groups/timeline/22.atom" rel="self" type="application/atom+xml"/> | ||||
|  <statusnet:group_info member_count="2"></statusnet:group_info> | ||||
| <entry> | ||||
|  <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> | ||||
|  <id>http://lazarus.local/mublog/notice/1243</id> | ||||
|  <title>Group post from local to !grouptest316173, should go out over push.</title> | ||||
|  <content type="html">Group post from local to !<span class="vcard"><a href="http://lazarus.local/mublog/group/22/id" class="url"><span class="fn nickname">grouptest316173</span></a></span>, should go out over push.</content> | ||||
|  <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/notice/1243"/> | ||||
|  <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> | ||||
|  <published>2011-01-06T22:44:18+00:00</published> | ||||
|  <updated>2011-01-06T22:44:18+00:00</updated> | ||||
|  <author> | ||||
|   <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type> | ||||
|   <uri>http://lazarus.local/mublog/user/557</uri> | ||||
|   <name>Pubtest316173 Smith</name> | ||||
|   <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/pubtest316173"/> | ||||
|   <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://lazarus.local/mublog/theme/default/default-avatar-profile.png"/> | ||||
|   <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://lazarus.local/mublog/theme/default/default-avatar-stream.png"/> | ||||
|   <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://lazarus.local/mublog/theme/default/default-avatar-mini.png"/> | ||||
|   <poco:preferredUsername>pubtest316173</poco:preferredUsername> | ||||
|   <poco:displayName>Pubtest316173 Smith</poco:displayName> | ||||
|   <poco:note>Stub account for OStatus tests.</poco:note> | ||||
|   <poco:urls> | ||||
|    <poco:type>homepage</poco:type> | ||||
|    <poco:value>http://example.org/pubtest316173</poco:value> | ||||
|    <poco:primary>true</poco:primary> | ||||
|   </poco:urls> | ||||
|  </author> | ||||
|  <link rel="ostatus:conversation" href="http://lazarus.local/mublog/conversation/1131"/> | ||||
|  <link rel="ostatus:attention" href="http://lazarus.local/mublog/group/22/id"/> | ||||
|  <link rel="mentioned" href="http://lazarus.local/mublog/group/22/id"/> | ||||
|  <category term="grouptest316173"></category> | ||||
|  <source> | ||||
|   <id>http://lazarus.local/mublog/api/statuses/user_timeline/557.atom</id> | ||||
|   <title>Pubtest316173 Smith</title> | ||||
|   <link rel="alternate" type="text/html" href="http://lazarus.local/mublog/pubtest316173"/> | ||||
|   <link rel="self" type="application/atom+xml" href="http://lazarus.local/mublog/api/statuses/user_timeline/557.atom"/> | ||||
|   <link rel="license" href="http://creativecommons.org/licenses/by/3.0/"/> | ||||
|   <icon>http://lazarus.local/mublog/theme/default/default-avatar-profile.png</icon> | ||||
|   <updated>2011-01-06T22:44:18+00:00</updated> | ||||
|  </source> | ||||
|  <link rel="self" type="application/atom+xml" href="http://lazarus.local/mublog/api/statuses/show/1243.atom"/> | ||||
|  <link rel="edit" type="application/atom+xml" href="http://lazarus.local/mublog/api/statuses/show/1243.atom"/> | ||||
|  <statusnet:notice_info local_id="1243" source="api"></statusnet:notice_info> | ||||
| </entry> | ||||
| </feed> | ||||
| EXAMPLE10; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user