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;
{
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) {
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;
}
}
- $mainpage = common_local_url('showgroup', array('nickname' => $nickname));
-
$cur = common_current_user();
// Checked in prepare() above
'location' => $location,
'aliases' => $aliases,
'userid' => $cur->id,
- 'mainpage' => $mainpage,
'local' => true));
common_redirect($group->homeUrl(), 303);
}
}
- $subscribers->free();
-
$this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE,
$this->page, 'subscribers',
array('nickname' => $this->user->nickname));
}
}
- $subscriptions->free();
-
$this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE,
$this->page, 'subscriptions',
array('nickname' => $this->user->nickname));
}
// MAGICALLY put fields into current scope
+ // @fixme kill extract(); it makes debugging absurdly hard
extract($fields);
// fill in later...
$uri = null;
}
+ if (empty($mainpage)) {
+ $mainpage = common_local_url('showgroup', array('nickname' => $nickname));
+ }
$group->nickname = $nickname;
$group->fullname = $fullname;
$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);
$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);
{
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);
}
}
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);
}
}
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');
}
}
+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.
$domain = mail_domain();
- $notifyfrom = '"'.common_config('site', 'name') .'" <noreply@'.$domain.'>';
+ $notifyfrom = '"'. str_replace('"', '\\"', common_config('site', 'name')) .'" <noreply@'.$domain.'>';
}
return $notifyfrom;
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));
+ });
});
});
// 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;
}
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')
$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);
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();
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();
$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());
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();
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 .")");
}
}
+ /**
+ * 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) {
}
}
+ /**
+ * 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, '-_', '+/'));
* 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)
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();
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();
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)) {
}
}
+ /**
+ * 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') {
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);
);
}
}
+
+/**
+ * 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'];
+ }
+}
+
/**
* 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)
return false;
}
- 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/magic-envelope+xml');
+ 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;
+ }
- 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;
- }
- if ($response->getStatus() != 200) {
- common_log(LOG_ERR, "Salmon at $endpoint_uri returned status " .
- $response->getStatus() . ': ' . $response->getBody());
- return false;
+ // Success!
+ return true;
}
- return true;
+ return false;
+ }
+
+ /**
+ * 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');
}
- public function createMagicEnv($text, $actor)
+ /**
+ * 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) {
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);
+
+ if ($magic_env->verify($env)) {
+ return true;
+ }
+ }
- return $magic_env->verify($env);
+ return false;
}
}
--- /dev/null
+<?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'");
+ }
+
+}
$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()
$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
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?
--- /dev/null
+#!/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";
+ }
+}
}
}
+ 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
</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;