diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index 0bb4860ea7..04456ffea4 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -107,8 +107,6 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction $sitename = common_config('site', 'name'); $avatar = $this->group->homepage_logo; $title = sprintf(_("%s timeline"), $this->group->nickname); - $taguribase = TagURI::base(); - $id = "tag:$taguribase:GroupTimeline:" . $this->group->id; $subtitle = sprintf( _('Updates from %1$s on %2$s!'), @@ -138,19 +136,9 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction try { - // If this was called using an integer ID, i.e.: using the canonical - // URL for this group's feed, then pass the Group object into the feed, - // so the OStatus plugin, and possibly other plugins, can access it. - // Feels sorta hacky. -- Z + $atom = new AtomGroupNoticeFeed($this->group); - $atom = null; - $id = $this->arg('id'); - - if (strval(intval($id)) === strval($id)) { - $atom = new AtomGroupNoticeFeed($this->group); - } else { - $atom = new AtomGroupNoticeFeed(); - } + // @todo set all this Atom junk up inside the feed class $atom->setId($id); $atom->setTitle($title); @@ -169,6 +157,8 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction $aargs['id'] = $id; } + $atom->setId($this->getSelfUri('ApiTimelineGroup', $aargs)); + $atom->addLink( $this->getSelfUri('ApiTimelineGroup', $aargs), array('rel' => 'self', 'type' => 'application/atom+xml') diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 3e849cc786..b3ded97c0f 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -116,8 +116,6 @@ class ApiTimelineUserAction extends ApiBareAuthAction $sitename = common_config('site', 'name'); $title = sprintf(_("%s timeline"), $this->user->nickname); - $taguribase = TagURI::base(); - $id = "tag:$taguribase:UserTimeline:" . $this->user->id; $link = common_local_url( 'showstream', array('nickname' => $this->user->nickname) @@ -148,21 +146,10 @@ class ApiTimelineUserAction extends ApiBareAuthAction header('Content-Type: application/atom+xml; charset=utf-8'); - // If this was called using an integer ID, i.e.: using the canonical - // URL for this user's feed, then pass the User object into the feed, - // so the OStatus plugin, and possibly other plugins, can access it. - // Feels sorta hacky. -- Z + // @todo set all this Atom junk up inside the feed class - $atom = null; - $id = $this->arg('id'); + $atom = new AtomUserNoticeFeed($this->user); - if (strval(intval($id)) === strval($id)) { - $atom = new AtomUserNoticeFeed($this->user); - } else { - $atom = new AtomUserNoticeFeed(); - } - - $atom->setId($id); $atom->setTitle($title); $atom->setSubtitle($subtitle); $atom->setLogo($logo); @@ -181,6 +168,8 @@ class ApiTimelineUserAction extends ApiBareAuthAction $aargs['id'] = $id; } + $atom->setId($this->getSelfUri('ApiTimelineUser', $aargs)); + $atom->addLink( $this->getSelfUri('ApiTimelineUser', $aargs), array('rel' => 'self', 'type' => 'application/atom+xml') diff --git a/actions/hcard.php b/actions/hcard.php new file mode 100644 index 0000000000..55d0f65c8f --- /dev/null +++ b/actions/hcard.php @@ -0,0 +1,120 @@ +. + * + * @category Personal + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * User profile page + * + * @category Personal + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class HcardAction extends Action +{ + var $user; + var $profile; + + function prepare($args) + { + parent::prepare($args); + + $nickname_arg = $this->arg('nickname'); + $nickname = common_canonical_nickname($nickname_arg); + + // Permanent redirect on non-canonical nickname + + if ($nickname_arg != $nickname) { + $args = array('nickname' => $nickname); + common_redirect(common_local_url('hcard', $args), 301); + return false; + } + + $this->user = User::staticGet('nickname', $nickname); + + if (!$this->user) { + $this->clientError(_('No such user.'), 404); + return false; + } + + $this->profile = $this->user->getProfile(); + + if (!$this->profile) { + $this->serverError(_('User has no profile.')); + return false; + } + + return true; + } + + function handle($args) + { + parent::handle($args); + $this->showPage(); + } + + function title() + { + return $this->profile->getBestName(); + } + + function showContent() + { + $up = new ShortUserProfile($this, $this->user, $this->profile); + $up->show(); + } + + function showHeader() + { + return; + } + + function showAside() + { + return; + } + + function showSecondaryNav() + { + return; + } +} + +class ShortUserProfile extends UserProfile +{ + function showEntityActions() + { + return; + } +} \ No newline at end of file diff --git a/actions/subscribe.php b/actions/subscribe.php index 3745311b66..b1243f3933 100644 --- a/actions/subscribe.php +++ b/actions/subscribe.php @@ -145,7 +145,7 @@ class SubscribeAction extends Action $this->element('title', null, _('Subscribed')); $this->elementEnd('head'); $this->elementStart('body'); - $unsubscribe = new UnsubscribeForm($this, $this->other->getProfile()); + $unsubscribe = new UnsubscribeForm($this, $this->other); $unsubscribe->show(); $this->elementEnd('body'); $this->elementEnd('html'); diff --git a/classes/Notice.php b/classes/Notice.php index 46c5ebb37c..ac4640534c 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1096,6 +1096,7 @@ class Notice extends Memcached_DataObject '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'); } else { diff --git a/lib/activity.php b/lib/activity.php index 30cdc5a563..b201532138 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -360,6 +360,25 @@ class ActivityUtils return null; } + static function getLinks(DOMNode $element, $rel, $type=null) + { + $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK); + $out = array(); + + foreach ($links as $link) { + + $linkRel = $link->getAttribute(self::REL); + $linkType = $link->getAttribute(self::TYPE); + + if ($linkRel == $rel && + (is_null($type) || $linkType == $type)) { + $out[] = $link; + } + } + + return $out; + } + /** * Gets the first child element with the given tag * @@ -465,6 +484,75 @@ class ActivityUtils } } +// XXX: Arg! This wouldn't be necessary if we used Avatars conistently +class AvatarLink +{ + public $url; + public $type; + public $size; + public $width; + public $height; + + function __construct($element=null) + { + if ($element) { + // @fixme use correct namespaces + $this->url = $element->getAttribute('href'); + $this->type = $element->getAttribute('type'); + $width = $element->getAttribute('media:width'); + if ($width != null) { + $this->width = intval($width); + } + $height = $element->getAttribute('media:height'); + if ($height != null) { + $this->height = intval($height); + } + } + } + + static function fromAvatar($avatar) + { + if (empty($avatar)) { + return null; + } + $alink = new AvatarLink(); + $alink->type = $avatar->mediatype; + $alink->height = $avatar->height; + $alink->width = $avatar->width; + $alink->url = $avatar->displayUrl(); + return $alink; + } + + static function fromFilename($filename, $size) + { + $alink = new AvatarLink(); + $alink->url = $filename; + $alink->height = $size; + if (!empty($filename)) { + $alink->width = $size; + $alink->type = self::mediatype($filename); + } else { + $alink->url = User_group::defaultLogo($size); + $alink->type = 'image/png'; + } + return $alink; + } + + // yuck! + static function mediatype($filename) { + $ext = strtolower(end(explode('.', $filename))); + if ($ext == 'jpeg') { + $ext = 'jpg'; + } + // hope we don't support any others + $types = array('png', 'gif', 'jpg', 'jpeg'); + if (in_array($ext, $types)) { + return 'image/' . $ext; + } + return null; + } +} + /** * A noun-ish thing in the activity universe * @@ -521,7 +609,7 @@ class ActivityObject public $content; public $link; public $source; - public $avatar; + public $avatarLinks = array(); public $geopoint; public $poco; public $displayName; @@ -589,8 +677,10 @@ class ActivityObject if ($this->type == self::PERSON || $this->type == self::GROUP) { $this->displayName = $this->title; - // @fixme we may have multiple avatars with different resolutions specified - $this->avatar = ActivityUtils::getLink($element, 'avatar'); + $avatars = ActivityUtils::getLinks($element, 'avatar'); + foreach ($avatars as $link) { + $this->avatarLinks[] = new AvatarLink($link); + } $this->poco = new PoCo($element); } @@ -641,13 +731,40 @@ class ActivityObject $object->id = $profile->getUri(); $object->title = $profile->getBestName(); $object->link = $profile->profileurl; - $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); - if ($avatar) { - $object->avatar = $avatar->displayUrl(); + + $orig = $profile->getOriginalAvatar(); + + if (!empty($orig)) { + $object->avatarLinks[] = AvatarLink::fromAvatar($orig); + } + + $sizes = array( + AVATAR_PROFILE_SIZE, + AVATAR_STREAM_SIZE, + AVATAR_MINI_SIZE + ); + + foreach ($sizes as $size) { + + $alink = null; + $avatar = $profile->getAvatar($size); + + if (!empty($avatar)) { + $alink = AvatarLink::fromAvatar($avatar); + } else { + $alink = new AvatarLink(); + $alink->type = 'image/png'; + $alink->height = $size; + $alink->width = $size; + $alink->url = Avatar::defaultImage($size); + } + + $object->avatarLinks[] = $alink; } if (isset($profile->lat) && isset($profile->lon)) { - $object->geopoint = (float)$profile->lat . ' ' . (float)$profile->lon; + $object->geopoint = (float)$profile->lat + . ' ' . (float)$profile->lon; } $object->poco = PoCo::fromProfile($profile); @@ -663,13 +780,28 @@ class ActivityObject $object->id = $group->getUri(); $object->title = $group->getBestName(); $object->link = $group->getUri(); - $object->avatar = $group->getAvatar(); + + $object->avatarLinks[] = AvatarLink::fromFilename( + $group->homepage_logo, + AVATAR_PROFILE_SIZE + ); + + $object->avatarLinks[] = AvatarLink::fromFilename( + $group->stream_logo, + AVATAR_STREAM_SIZE + ); + + $object->avatarLinks[] = AvatarLink::fromFilename( + $group->mini_logo, + AVATAR_MINI_SIZE + ); $object->poco = PoCo::fromGroup($group); return $object; } + function asString($tag='activity:object') { $xs = new XMLStringer(true); @@ -705,29 +837,21 @@ class ActivityObject ); } - if ($this->type == ActivityObject::PERSON) { - $xs->element( - 'link', array( - 'type' => empty($this->avatar) ? 'image/png' : $this->avatar->mediatype, - 'rel' => 'avatar', - 'href' => empty($this->avatar) - ? Avatar::defaultImage(AVATAR_PROFILE_SIZE) - : $this->avatar - ), - null - ); - } + if ($this->type == ActivityObject::PERSON + || $this->type == ActivityObject::GROUP) { - // XXX: Gotta figure out mime-type! Gar. - - if ($this->type == ActivityObject::GROUP) { - $xs->element( - 'link', array( - 'rel' => 'avatar', - 'href' => $this->avatar - ), - null - ); + foreach ($this->avatarLinks as $avatar) { + $xs->element( + 'link', array( + 'rel' => 'avatar', + 'type' => $avatar->type, + 'media:width' => $avatar->width, + 'media:height' => $avatar->height, + 'href' => $avatar->url + ), + null + ); + } } if (!empty($this->geopoint)) { @@ -1038,7 +1162,8 @@ class Activity 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', 'xmlns:georss' => 'http://www.georss.org/georss', 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0', - 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0'); + 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0', + 'xmlns:media' => 'http://purl.org/syndication/atommedia'); } else { $attrs = array(); } diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php index d2bf2a416f..3c3556cb95 100644 --- a/lib/atomnoticefeed.php +++ b/lib/atomnoticefeed.php @@ -64,6 +64,11 @@ class AtomNoticeFeed extends Atom10Feed 'http://activitystrea.ms/spec/1.0/' ); + $this->addNamespace( + 'media', + 'http://purl.org/syndication/atommedia' + ); + $this->addNamespace( 'poco', 'http://portablecontacts.net/spec/1.0' diff --git a/lib/router.php b/lib/router.php index 0e15d83b9a..abbce041db 100644 --- a/lib/router.php +++ b/lib/router.php @@ -671,7 +671,7 @@ class Router foreach (array('subscriptions', 'subscribers', 'all', 'foaf', 'xrds', - 'replies', 'microsummary') as $a) { + 'replies', 'microsummary', 'hcard') as $a) { $m->connect($a, array('action' => $a, 'nickname' => $nickname)); @@ -737,7 +737,7 @@ class Router foreach (array('subscriptions', 'subscribers', 'nudge', 'all', 'foaf', 'xrds', - 'replies', 'inbox', 'outbox', 'microsummary') as $a) { + 'replies', 'inbox', 'outbox', 'microsummary', 'hcard') as $a) { $m->connect(':nickname/'.$a, array('action' => $a), array('nickname' => '[a-zA-Z0-9]{1,64}')); diff --git a/lib/util.php b/lib/util.php index d1c78f7d07..8381bc63c0 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1698,7 +1698,8 @@ function common_url_to_nickname($url) # Strip starting, ending slashes $path = preg_replace('@/$@', '', $parts['path']); $path = preg_replace('@^/@', '', $path); - if (strpos($path, '/') === false) { + $path = basename($path); + if ($path) { return common_nicknamize($path); } } diff --git a/plugins/OStatus/actions/xrd.php b/plugins/OStatus/actions/xrd.php index cc5c70b08e..2a754dcfef 100644 --- a/plugins/OStatus/actions/xrd.php +++ b/plugins/OStatus/actions/xrd.php @@ -66,9 +66,9 @@ class XrdAction extends Action 'type' => 'application/atom+xml'); // hCard - $xrd->links[] = array('rel' => 'http://microformats.org/profile/hcard', + $xrd->links[] = array('rel' => Webfinger::HCARD, 'type' => 'text/html', - 'href' => common_profile_url($nick)); + 'href' => common_local_url('hcard', array('nickname' => $nick))); // XFN $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11', @@ -78,8 +78,8 @@ class XrdAction extends Action $xrd->links[] = array('rel' => 'describedby', 'type' => 'application/rdf+xml', 'href' => common_local_url('foaf', - array('nickname' => $nick))); - + array('nickname' => $nick))); + $salmon_url = common_local_url('salmon', array('id' => $this->user->id)); @@ -93,10 +93,10 @@ class XrdAction extends Action $magickey = new Magicsig(); $magickey->generate($this->user->id); } - + $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, 'href' => 'data:application/magic-public-key;'. $magickey->keypair); - + // TODO - finalize where the redirect should go on the publisher $url = common_local_url('ostatussub') . '?profile={uri}'; $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 0fec8fc43d..4a9aafce1e 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -150,27 +150,7 @@ class Ostatus_profile extends Memcached_DataObject function asActivityObject() { if ($this->isGroup()) { - $object = new ActivityObject(); - $object->type = 'http://activitystrea.ms/schema/1.0/group'; - $object->id = $this->uri; - $self = $this->localGroup(); - - // @fixme put a standard getAvatar() interface on groups too - if ($self->homepage_logo) { - $object->avatar = $self->homepage_logo; - $map = array('png' => 'image/png', - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'gif' => 'image/gif'); - $extension = pathinfo(parse_url($object->avatar, PHP_URL_PATH), PATHINFO_EXTENSION); - if (isset($map[$extension])) { - // @fixme this ain't used/saved yet - $object->avatarType = $map[$extension]; - } - } - - $object->link = $this->uri; // @fixme accurate? - return $object; + return ActivityObject::fromGroup($this->localGroup()); } else { return ActivityObject::fromProfile($this->localProfile()); } @@ -189,57 +169,13 @@ class Ostatus_profile extends Memcached_DataObject */ function asActivityNoun($element) { - $xs = new XMLStringer(true); - $avatarHref = Avatar::defaultImage(AVATAR_PROFILE_SIZE); - $avatarType = 'image/png'; if ($this->isGroup()) { - $type = 'http://activitystrea.ms/schema/1.0/group'; - $self = $this->localGroup(); - - // @fixme put a standard getAvatar() interface on groups too - if ($self->homepage_logo) { - $avatarHref = $self->homepage_logo; - $map = array('png' => 'image/png', - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'gif' => 'image/gif'); - $extension = pathinfo(parse_url($avatarHref, PHP_URL_PATH), PATHINFO_EXTENSION); - if (isset($map[$extension])) { - $avatarType = $map[$extension]; - } - } + $noun = ActivityObject::fromGroup($this->localGroup()); + return $noun->asString('activity:' . $element); } else { - $type = 'http://activitystrea.ms/schema/1.0/person'; - $self = $this->localProfile(); - $avatar = $self->getAvatar(AVATAR_PROFILE_SIZE); - if ($avatar) { - $avatarHref = $avatar->url; - $avatarType = $avatar->mediatype; - } + $noun = ActivityObject::fromProfile($this->localProfile()); + return $noun->asString('activity:' . $element); } - $xs->elementStart('activity:' . $element); - $xs->element( - 'activity:object-type', - null, - $type - ); - $xs->element( - 'id', - null, - $this->uri); // ? - $xs->element('title', null, $self->getBestName()); - - $xs->element( - 'link', array( - 'type' => $avatarType, - 'href' => $avatarHref - ), - '' - ); - - $xs->elementEnd('activity:' . $element); - - return $xs->getString(); } /** @@ -401,7 +337,8 @@ class Ostatus_profile extends Memcached_DataObject 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', 'xmlns:georss' => 'http://www.georss.org/georss', 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0', - 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0'); + 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0', + 'xmlns:media' => 'http://purl.org/syndication/atommedia'); $entry = new XMLStringer(); $entry->elementStart('entry', $attributes); @@ -485,36 +422,6 @@ class Ostatus_profile extends Memcached_DataObject } } - function atomFeed($actor) - { - $feed = new Atom10Feed(); - // @fixme should these be set up somewhere else? - $feed->addNamespace('activity', 'http://activitystrea.ms/spec/1.0/'); - $feed->addNamespace('thr', 'http://purl.org/syndication/thread/1.0'); - $feed->addNamespace('georss', 'http://www.georss.org/georss'); - $feed->addNamespace('ostatus', 'http://ostatus.org/schema/1.0'); - - $taguribase = common_config('integration', 'taguri'); - $feed->setId("tag:{$taguribase}:UserTimeline:{$actor->id}"); // ??? - - $feed->setTitle($actor->getBestName() . ' timeline'); // @fixme - $feed->setUpdated(time()); - $feed->setPublished(time()); - - $feed->addLink(common_local_url('ApiTimelineUser', - array('id' => $actor->id, - 'type' => 'atom')), - array('rel' => 'self', - 'type' => 'application/atom+xml')); - - $feed->addLink(common_local_url('userbyid', - array('id' => $actor->id)), - array('rel' => 'alternate', - 'type' => 'text/html')); - - return $feed; - } - /** * Read and post notices for updates from the feed. * Currently assumes that all items in the feed are new, @@ -644,7 +551,6 @@ class Ostatus_profile extends Memcached_DataObject 'groups' => array(), 'tags' => array()); - // Check for optional attributes... if (!empty($activity->time)) { @@ -791,11 +697,18 @@ class Ostatus_profile extends Memcached_DataObject { // Get the canonical feed URI and check it $discover = new FeedDiscovery(); - $feeduri = $discover->discoverFromURL($profile_uri); + if ($hints['feedurl']) { + $feeduri = $hints['feedurl']; + $feeduri = $discover->discoverFromFeedURL($feeduri); + } else { + $feeduri = $discover->discoverFromURL($profile_uri); + $hints['feedurl'] = $feeduri; + } - //$feedsub = FeedSub::ensureFeed($feeduri, $discover->feed); $huburi = $discover->getAtomLink('hub'); + $hints['hub'] = $huburi; $salmonuri = $discover->getAtomLink('salmon'); + $hints['salmon'] = $salmonuri; if (!$huburi) { // We can only deal with folks with a PuSH hub @@ -810,7 +723,7 @@ class Ostatus_profile extends Memcached_DataObject if (!empty($subject)) { $subjObject = new ActivityObject($subject); - return self::ensureActivityObjectProfile($subjObject, $feeduri, $salmonuri, $hints); + return self::ensureActivityObjectProfile($subjObject, $hints); } // Otherwise, try the feed author @@ -819,7 +732,7 @@ class Ostatus_profile extends Memcached_DataObject if (!empty($author)) { $authorObject = new ActivityObject($author); - return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri, $hints); + return self::ensureActivityObjectProfile($authorObject, $hints); } // Sheesh. Not a very nice feed! Let's try fingerpoken in the @@ -835,7 +748,7 @@ class Ostatus_profile extends Memcached_DataObject if (!empty($actor)) { $actorObject = new ActivityObject($actor); - return self::ensureActivityObjectProfile($actorObject, $feeduri, $salmonuri, $hints); + return self::ensureActivityObjectProfile($actorObject, $hints); } @@ -843,7 +756,7 @@ class Ostatus_profile extends Memcached_DataObject if (!empty($author)) { $authorObject = new ActivityObject($author); - return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri, $hints); + return self::ensureActivityObjectProfile($authorObject, $hints); } } @@ -912,8 +825,20 @@ class Ostatus_profile extends Memcached_DataObject protected static function getActivityObjectAvatar($object, $hints=array()) { - if ($object->avatar) { - return $object->avatar; + if ($object->avatarLinks) { + $best = false; + // Take the exact-size avatar, or the largest avatar, or the first avatar if all sizeless + foreach ($object->avatarLinks as $avatar) { + if ($avatar->width == AVATAR_PROFILE_SIZE && $avatar->height = AVATAR_PROFILE_SIZE) { + // Exact match! + $best = $avatar; + break; + } + if (!$best || $avatar->width > $best->width) { + $best = $avatar; + } + } + return $best->url; } else if (array_key_exists('avatar', $hints)) { return $hints['avatar']; } @@ -976,18 +901,18 @@ class Ostatus_profile extends Memcached_DataObject * @return Ostatus_profile */ - public static function ensureActorProfile($activity, $feeduri=null, $salmonuri=null) + public static function ensureActorProfile($activity, $hints=array()) { - return self::ensureActivityObjectProfile($activity->actor, $feeduri, $salmonuri); + return self::ensureActivityObjectProfile($activity->actor, $hints); } - public static function ensureActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array()) + public static function ensureActivityObjectProfile($object, $hints=array()) { $profile = self::getActivityObjectProfile($object); if ($profile) { $profile->updateFromActivityObject($object, $hints); } else { - $profile = self::createActivityObjectProfile($object, $feeduri, $salmonuri, $hints); + $profile = self::createActivityObjectProfile($object, $hints); } return $profile; } @@ -1033,58 +958,55 @@ class Ostatus_profile extends Memcached_DataObject * @fixme validate stuff somewhere */ - protected static function createActorProfile($activity, $feeduri=null, $salmonuri=null) - { - $actor = $activity->actor; - - self::createActivityObjectProfile($actor, $feeduri, $salmonuri); - } - /** * Create local ostatus_profile and profile/user_group entries for * the provided remote user or group. * * @param ActivityObject $object - * @param string $feeduri - * @param string $salmonuri * @param array $hints * - * @fixme fold $feeduri/$salmonuri into $hints * @return Ostatus_profile */ - protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array()) + protected static function createActivityObjectProfile($object, $hints=array()) { - $homeuri = $object->id; + $homeuri = $object->id; + $discover = false; if (!$homeuri) { common_log(LOG_DEBUG, __METHOD__ . " empty actor profile URI: " . var_export($activity, true)); throw new ServerException("No profile URI"); } - if (empty($feeduri)) { - if (array_key_exists('feedurl', $hints)) { - $feeduri = $hints['feedurl']; - } - } - - if (empty($salmonuri)) { - if (array_key_exists('salmon', $hints)) { - $salmonuri = $hints['salmon']; - } - } - - if (!$feeduri || !$salmonuri) { - // Get the canonical feed URI and check it + if (array_key_exists('feedurl', $hints)) { + $feeduri = $hints['feedurl']; + } else { $discover = new FeedDiscovery(); $feeduri = $discover->discoverFromURL($homeuri); + } - $huburi = $discover->getAtomLink('hub'); - $salmonuri = $discover->getAtomLink('salmon'); - - if (!$huburi) { - // We can only deal with folks with a PuSH hub - throw new FeedSubNoHubException(); + if (array_key_exists('salmon', $hints)) { + $salmonuri = $hints['salmon']; + } else { + if (!$discover) { + $discover = new FeedDiscovery(); + $discover->discoverFromFeedURL($hints['feedurl']); } + $salmonuri = $discover->getAtomLink('salmon'); + } + + if (array_key_exists('hub', $hints)) { + $huburi = $hints['hub']; + } else { + if (!$discover) { + $discover = new FeedDiscovery(); + $discover->discoverFromFeedURL($hints['feedurl']); + } + $huburi = $discover->getAtomLink('hub'); + } + + if (!$huburi) { + // We can only deal with folks with a PuSH hub + throw new FeedSubNoHubException(); } $oprofile = new Ostatus_profile(); @@ -1155,11 +1077,19 @@ class Ostatus_profile extends Memcached_DataObject $orig = clone($profile); $profile->nickname = self::getActivityObjectNickname($object, $hints); - $profile->fullname = $object->title; + + if (!empty($object->title)) { + $profile->fullname = $object->title; + } else if (array_key_exists('fullname', $hints)) { + $profile->fullname = $hints['fullname']; + } + if (!empty($object->link)) { $profile->profileurl = $object->link; } else if (array_key_exists('profileurl', $hints)) { $profile->profileurl = $hints['profileurl']; + } else if (Validate::uri($object->id, array('allowed_schemes' => array('http', 'https')))) { + $profile->profileurl = $object->id; } $profile->bio = self::getActivityObjectBio($object, $hints); @@ -1228,12 +1158,16 @@ class Ostatus_profile extends Memcached_DataObject { $location = null; - if (!empty($object->poco)) { - if (isset($object->poco->address->formatted)) { - $location = $object->poco->address->formatted; - if (mb_strlen($location) > 255) { - $location = mb_substr($note, 0, 255 - 3) . ' … '; - } + if (!empty($object->poco) && + isset($object->poco->address->formatted)) { + $location = $object->poco->address->formatted; + } else if (array_key_exists('location', $hints)) { + $location = $hints['location']; + } + + if (!empty($location)) { + if (mb_strlen($location) > 255) { + $location = mb_substr($note, 0, 255 - 3) . ' … '; } } @@ -1248,13 +1182,16 @@ class Ostatus_profile extends Memcached_DataObject if (!empty($object->poco)) { $note = $object->poco->note; - if (!empty($note)) { - if (mb_strlen($note) > Profile::maxBio()) { - // XXX: truncate ok? - $bio = mb_substr($note, 0, Profile::maxBio() - 3) . ' … '; - } else { - $bio = $note; - } + } else if (array_key_exists('bio', $hints)) { + $note = $hints['bio']; + } + + if (!empty($note)) { + if (Profile::bioTooLong($note)) { + // XXX: truncate ok? + $bio = mb_substr($note, 0, Profile::maxBio() - 3) . ' … '; + } else { + $bio = $note; } } @@ -1270,10 +1207,15 @@ class Ostatus_profile extends Memcached_DataObject return common_nicknamize($object->poco->preferredUsername); } } + if (!empty($object->nickname)) { return common_nicknamize($object->nickname); } + if (array_key_exists('nickname', $hints)) { + return $hints['nickname']; + } + // Try the definitive ID $nickname = self::nicknameFromURI($object->id); @@ -1318,11 +1260,26 @@ class Ostatus_profile extends Memcached_DataObject public static function ensureWebfinger($addr) { + // First, try the cache + + $uri = self::cacheGet(sprintf('ostatus_profile:webfinger:%s', $addr)); + + if ($uri !== false) { + if (is_null($uri)) { + return null; + } + $oprofile = Ostatus_profile::staticGet('uri', $uri); + if (!empty($oprofile)) { + return $oprofile; + } + } + // First, look it up $oprofile = Ostatus_profile::staticGet('uri', 'acct:'.$addr); if (!empty($oprofile)) { + self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri); return $oprofile; } @@ -1333,6 +1290,7 @@ class Ostatus_profile extends Memcached_DataObject $result = $disco->lookup($addr); if (!$result) { + self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), null); return null; } @@ -1347,6 +1305,9 @@ class Ostatus_profile extends Memcached_DataObject case Discovery::UPDATESFROM: $feedUrl = $link['href']; break; + case Webfinger::HCARD: + $hcardUrl = $link['href']; + break; default: common_log(LOG_NOTICE, "Don't know what to do with rel = '{$link['rel']}'"); break; @@ -1358,11 +1319,19 @@ class Ostatus_profile extends Memcached_DataObject 'feedurl' => $feedUrl, 'salmon' => $salmonEndpoint); + if (isset($hcardUrl)) { + $hcardHints = self::slurpHcard($hcardUrl); + // Note: Webfinger > hcard + $hints = array_merge($hcardHints, $hints); + } + // If we got a feed URL, try that if (isset($feedUrl)) { try { + common_log(LOG_INFO, "Discovery on acct:$addr with feed URL $feedUrl"); $oprofile = self::ensureProfile($feedUrl, $hints); + self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri); return $oprofile; } catch (Exception $e) { common_log(LOG_WARNING, "Failed creating profile from feed URL '$feedUrl': " . $e->getMessage()); @@ -1374,7 +1343,9 @@ class Ostatus_profile extends Memcached_DataObject if (isset($profileUrl)) { try { + common_log(LOG_INFO, "Discovery on acct:$addr with profile URL $profileUrl"); $oprofile = self::ensureProfile($profileUrl, $hints); + self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri); return $oprofile; } catch (Exception $e) { common_log(LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage()); @@ -1426,6 +1397,7 @@ class Ostatus_profile extends Memcached_DataObject throw new Exception("Couldn't save ostatus_profile for '$addr'"); } + self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri); return $oprofile; } @@ -1464,4 +1436,67 @@ class Ostatus_profile extends Memcached_DataObject return $file; } + + protected static function slurpHcard($url) + { + set_include_path(get_include_path() . PATH_SEPARATOR . INSTALLDIR . '/plugins/OStatus/extlib/hkit/'); + require_once('hkit.class.php'); + + $h = new hKit; + + // Google Buzz hcards need to be tidied. Probably others too. + + $h->tidy_mode = 'proxy'; // 'proxy', 'exec', 'php' or 'none' + + // Get by URL + $hcards = $h->getByURL('hcard', $url); + + if (empty($hcards)) { + return array(); + } + + // @fixme more intelligent guess on multi-hcard pages + $hcard = $hcards[0]; + + $hints = array(); + + $hints['profileurl'] = $url; + + if (array_key_exists('nickname', $hcard)) { + $hints['nickname'] = $hcard['nickname']; + } + + if (array_key_exists('fn', $hcard)) { + $hints['fullname'] = $hcard['fn']; + } else if (array_key_exists('n', $hcard)) { + $hints['fullname'] = implode(' ', $hcard['n']); + } + + if (array_key_exists('photo', $hcard)) { + $hints['avatar'] = $hcard['photo']; + } + + if (array_key_exists('note', $hcard)) { + $hints['bio'] = $hcard['note']; + } + + if (array_key_exists('adr', $hcard)) { + if (is_string($hcard['adr'])) { + $hints['location'] = $hcard['adr']; + } else if (is_array($hcard['adr'])) { + $hints['location'] = implode(' ', $hcard['adr']); + } + } + + if (array_key_exists('url', $hcard)) { + if (is_string($hcard['url'])) { + $hints['homepage'] = $hcard['url']; + } else if (is_array($hcard['adr'])) { + // HACK get the last one; that's how our hcards look + $hints['homepage'] = $hcard['url'][count($hcard['url'])-1]; + } + } + + return $hints; + } } diff --git a/plugins/OStatus/extlib/hkit/hcard.profile.php b/plugins/OStatus/extlib/hkit/hcard.profile.php new file mode 100644 index 0000000000..6ec0dc8906 --- /dev/null +++ b/plugins/OStatus/extlib/hkit/hcard.profile.php @@ -0,0 +1,105 @@ +root_class = 'vcard'; + + $this->classes = array( + 'fn', array('honorific-prefix', 'given-name', 'additional-name', 'family-name', 'honorific-suffix'), + 'n', array('honorific-prefix', 'given-name', 'additional-name', 'family-name', 'honorific-suffix'), + 'adr', array('post-office-box', 'extended-address', 'street-address', 'postal-code', 'country-name', 'type', 'region', 'locality'), + 'label', 'bday', 'agent', 'nickname', 'photo', 'class', + 'email', array('type', 'value'), + 'category', 'key', 'logo', 'mailer', 'note', + 'org', array('organization-name', 'organization-unit'), + 'tel', array('type', 'value'), + 'geo', array('latitude', 'longitude'), + 'tz', 'uid', 'url', 'rev', 'role', 'sort-string', 'sound', 'title' + ); + + // classes that must only appear once per card + $this->singles = array( + 'fn' + ); + + // classes that are required (not strictly enforced - give at least one!) + $this->required = array( + 'fn' + ); + + $this->att_map = array( + 'fn' => array('IMG|alt'), + 'url' => array('A|href', 'IMG|src', 'AREA|href'), + 'photo' => array('IMG|src'), + 'bday' => array('ABBR|title'), + 'logo' => array('IMG|src'), + 'email' => array('A|href'), + 'geo' => array('ABBR|title') + ); + + + $this->callbacks = array( + 'url' => array($this, 'resolvePath'), + 'photo' => array($this, 'resolvePath'), + 'logo' => array($this, 'resolvePath'), + 'email' => array($this, 'resolveEmail') + ); + + + + function hKit_hcard_post($a) + { + + foreach ($a as &$vcard){ + + hKit_implied_n_optimization($vcard); + hKit_implied_n_from_fn($vcard); + + } + + return $a; + + } + + + function hKit_implied_n_optimization(&$vcard) + { + if (array_key_exists('fn', $vcard) && !is_array($vcard['fn']) && + !array_key_exists('n', $vcard) && (!array_key_exists('org', $vcard) || $vcard['fn'] != $vcard['org'])){ + + if (sizeof(explode(' ', $vcard['fn'])) == 2){ + $patterns = array(); + $patterns[] = array('/^(\S+),\s*(\S{1})$/', 2, 1); // Lastname, Initial + $patterns[] = array('/^(\S+)\s*(\S{1})\.*$/', 2, 1); // Lastname Initial(.) + $patterns[] = array('/^(\S+),\s*(\S+)$/', 2, 1); // Lastname, Firstname + $patterns[] = array('/^(\S+)\s*(\S+)$/', 1, 2); // Firstname Lastname + + foreach ($patterns as $pattern){ + if (preg_match($pattern[0], $vcard['fn'], $matches) === 1){ + $n = array(); + $n['given-name'] = $matches[$pattern[1]]; + $n['family-name'] = $matches[$pattern[2]]; + $vcard['n'] = $n; + + + break; + } + } + } + } + } + + + function hKit_implied_n_from_fn(&$vcard) + { + if (array_key_exists('fn', $vcard) && is_array($vcard['fn']) + && !array_key_exists('n', $vcard) && (!array_key_exists('org', $vcard) || $vcard['fn'] != $vcard['org'])){ + + $vcard['n'] = $vcard['fn']; + } + + if (array_key_exists('fn', $vcard) && is_array($vcard['fn'])){ + $vcard['fn'] = $vcard['fn']['text']; + } + } + +?> \ No newline at end of file diff --git a/plugins/OStatus/extlib/hkit/hkit.class.php b/plugins/OStatus/extlib/hkit/hkit.class.php new file mode 100644 index 0000000000..c3a54cff65 --- /dev/null +++ b/plugins/OStatus/extlib/hkit/hkit.class.php @@ -0,0 +1,475 @@ +' . implode(', ', $missing) . ''); + + } + + + public function getByURL($profile='', $url='') + { + + if ($profile=='' || $url == '') return false; + + $this->loadProfile($profile); + + $source = $this->loadURL($url); + + if ($source){ + $tidy_xhtml = $this->tidyThis($source); + + $fragment = false; + + if (strrchr($url, '#')) + $fragment = array_pop(explode('#', $url)); + + $doc = $this->loadDoc($tidy_xhtml, $fragment); + $s = $this->processNodes($doc, $this->classes); + $s = $this->postProcess($profile, $s); + + return $s; + }else{ + return false; + } + } + + public function getByString($profile='', $input_xml='') + { + if ($profile=='' || $input_xml == '') return false; + + $this->loadProfile($profile); + + $doc = $this->loadDoc($input_xml); + $s = $this->processNodes($doc, $this->classes); + $s = $this->postProcess($profile, $s); + + return $s; + + } + + private function processNodes($items, $classes, $allow_includes=true){ + + $out = array(); + + foreach($items as $item){ + $data = array(); + + for ($i=0; $ixpath($xpath); + + if ($results){ + foreach ($results as $result){ + if (isset($classes[$i+1]) && is_array($classes[$i+1])){ + $nodes = $this->processNodes($results, $classes[$i+1]); + if (sizeof($nodes) > 0){ + $nodes = array_merge(array('text'=>$this->getNodeValue($result, $classes[$i])), $nodes); + $data[$classes[$i]] = $nodes; + }else{ + $data[$classes[$i]] = $this->getNodeValue($result, $classes[$i]); + } + + }else{ + if (isset($data[$classes[$i]])){ + if (is_array($data[$classes[$i]])){ + // is already an array - append + $data[$classes[$i]][] = $this->getNodeValue($result, $classes[$i]); + + }else{ + // make it an array + if ($classes[$i] == 'value'){ // unless it's the 'value' of a type/value pattern + $data[$classes[$i]] .= $this->getNodeValue($result, $classes[$i]); + }else{ + $old_val = $data[$classes[$i]]; + $data[$classes[$i]] = array($old_val, $this->getNodeValue($result, $classes[$i])); + $old_val = false; + } + } + }else{ + // set as normal value + $data[$classes[$i]] = $this->getNodeValue($result, $classes[$i]); + + } + } + + // td@headers pattern + if (strtoupper(dom_import_simplexml($result)->tagName)== "TD" && $result['headers']){ + $include_ids = explode(' ', $result['headers']); + $doc = $this->doc; + foreach ($include_ids as $id){ + $xpath = "//*[@id='$id']/.."; + $includes = $doc->xpath($xpath); + foreach ($includes as $include){ + $tmp = $this->processNodes($include, $this->classes); + if (is_array($tmp)) $data = array_merge($data, $tmp); + } + } + } + } + } + } + $result = false; + } + + // include-pattern + if ($allow_includes){ + $xpath = ".//*[contains(concat(' ',normalize-space(@class),' '),' include ')]"; + $results = $item->xpath($xpath); + + if ($results){ + foreach ($results as $result){ + $tagName = strtoupper(dom_import_simplexml($result)->tagName); + if ((($tagName == "OBJECT" && $result['data']) || ($tagName == "A" && $result['href'])) + && preg_match('/\binclude\b/', $result['class'])){ + $att = ($tagName == "OBJECT" ? 'data' : 'href'); + $id = str_replace('#', '', $result[$att]); + $doc = $this->doc; + $xpath = "//*[@id='$id']"; + $includes = $doc->xpath($xpath); + foreach ($includes as $include){ + $include = simplexml_load_string(''.$include->asXML().''); // don't ask. + $tmp = $this->processNodes($include, $this->classes, false); + if (is_array($tmp)) $data = array_merge($data, $tmp); + } + } + } + } + } + $out[] = $data; + } + + if (sizeof($out) > 1){ + return $out; + }else if (isset($data)){ + return $data; + }else{ + return array(); + } + } + + + private function getNodeValue($node, $className) + { + + $tag_name = strtoupper(dom_import_simplexml($node)->tagName); + $s = false; + + // ignore DEL tags + if ($tag_name == 'DEL') return $s; + + // look up att map values + if (array_key_exists($className, $this->att_map)){ + + foreach ($this->att_map[$className] as $map){ + if (preg_match("/$tag_name\|/", $map)){ + $s = ''.$node[array_pop($foo = explode('|', $map))]; + } + } + } + + // if nothing and OBJ, try data. + if (!$s && $tag_name=='OBJECT' && $node['data']) $s = ''.$node['data']; + + // if nothing and IMG, try alt. + if (!$s && $tag_name=='IMG' && $node['alt']) $s = ''.$node['alt']; + + // if nothing and AREA, try alt. + if (!$s && $tag_name=='AREA' && $node['alt']) $s = ''.$node['alt']; + + //if nothing and not A, try title. + if (!$s && $tag_name!='A' && $node['title']) $s = ''.$node['title']; + + + // if nothing found, go with node text + $s = ($s ? $s : implode(array_filter($node->xpath('child::node()'), array(&$this, "filterBlankValues")), ' ')); + + // callbacks + if (array_key_exists($className, $this->callbacks)){ + $s = preg_replace_callback('/.*/', $this->callbacks[$className], $s, 1); + } + + // trim and remove line breaks + if ($tag_name != 'PRE'){ + $s = trim(preg_replace('/[\r\n\t]+/', '', $s)); + $s = trim(preg_replace('/(\s{2})+/', ' ', $s)); + } + + return $s; + } + + private function filterBlankValues($s){ + return preg_match("/\w+/", $s); + } + + + private function tidyThis($source) + { + switch ( $this->tidy_mode ) + { + case 'exec': + $tmp_file = $this->tmp_dir.md5($source).'.txt'; + file_put_contents($tmp_file, $source); + exec("tidy -utf8 -indent -asxhtml -numeric -bare -quiet $tmp_file", $tidy); + unlink($tmp_file); + return implode("\n", $tidy); + break; + + case 'php': + $tidy = tidy_parse_string($source); + return tidy_clean_repair($tidy); + break; + + default: + return $source; + break; + } + + } + + + private function loadProfile($profile) + { + require_once("$profile.profile.php"); + } + + + private function loadDoc($input_xml, $fragment=false) + { + $xml = simplexml_load_string($input_xml); + + $this->doc = $xml; + + if ($fragment){ + $doc = $xml->xpath("//*[@id='$fragment']"); + $xml = simplexml_load_string($doc[0]->asXML()); + $doc = null; + } + + // base tag + if ($xml->head->base['href']) $this->base = $xml->head->base['href']; + + // xml:base attribute - PITA with SimpleXML + preg_match('/xml:base="(.*)"/', $xml->asXML(), $matches); + if (is_array($matches) && sizeof($matches)>1) $this->base = $matches[1]; + + return $xml->xpath("//*[contains(concat(' ',normalize-space(@class),' '),' $this->root_class ')]"); + + } + + + private function loadURL($url) + { + $this->url = $url; + + if ($this->tidy_mode == 'proxy' && $this->tidy_proxy != ''){ + $url = $this->tidy_proxy . $url; + } + + return @file_get_contents($url); + + } + + + private function postProcess($profile, $s) + { + $required = $this->required; + + if (is_array($s) && array_key_exists($required[0], $s)){ + $s = array($s); + } + + $s = $this->dedupeSingles($s); + + if (function_exists('hKit_'.$profile.'_post')){ + $s = call_user_func('hKit_'.$profile.'_post', $s); + } + + $s = $this->removeTextVals($s); + + return $s; + } + + + private function resolvePath($filepath) + { // ugly code ahoy: needs a serious tidy up + + $filepath = $filepath[0]; + + $base = $this->base; + $url = $this->url; + + if ($base != '' && strpos($base, '://') !== false) + $url = $base; + + $r = parse_url($url); + $domain = $r['scheme'] . '://' . $r['host']; + + if (!isset($r['path'])) $r['path'] = '/'; + $path = explode('/', $r['path']); + $file = explode('/', $filepath); + $new = array(''); + + if (strpos($filepath, '://') !== false || strpos($filepath, 'data:') !== false){ + return $filepath; + } + + if ($file[0] == ''){ + // absolute path + return ''.$domain . implode('/', $file); + }else{ + // relative path + if ($path[sizeof($path)-1] == '') array_pop($path); + if (strpos($path[sizeof($path)-1], '.') !== false) array_pop($path); + + foreach ($file as $segment){ + if ($segment == '..'){ + array_pop($path); + }else{ + $new[] = $segment; + } + } + return ''.$domain . implode('/', $path) . implode('/', $new); + } + } + + private function resolveEmail($v) + { + $parts = parse_url($v[0]); + return ($parts['path']); + } + + + private function dedupeSingles($s) + { + $singles = $this->singles; + + foreach ($s as &$item){ + foreach ($singles as $classname){ + if (array_key_exists($classname, $item) && is_array($item[$classname])){ + if (isset($item[$classname][0])) $item[$classname] = $item[$classname][0]; + } + } + } + + return $s; + } + + private function removeTextVals($s) + { + foreach ($s as $key => &$val){ + if ($key){ + $k = $key; + }else{ + $k = ''; + } + + if (is_array($val)){ + $val = $this->removeTextVals($val); + }else{ + if ($k == 'text'){ + $val = ''; + } + } + } + + return array_filter($s); + } + + } + + +?> \ No newline at end of file diff --git a/plugins/OStatus/lib/webfinger.php b/plugins/OStatus/lib/webfinger.php new file mode 100644 index 0000000000..4b777c9a0a --- /dev/null +++ b/plugins/OStatus/lib/webfinger.php @@ -0,0 +1,164 @@ +. + * + * @package StatusNet + * @author James Walker + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +define('WEBFINGER_SERVICE_REL_VALUE', 'lrdd'); + +/** + * Implement the webfinger protocol. + */ + +class Webfinger +{ + const PROFILEPAGE = 'http://webfinger.net/rel/profile-page'; + const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from'; + const HCARD = 'http://microformats.org/profile/hcard'; + + /** + * Perform a webfinger lookup given an account. + */ + + public function lookup($id) + { + $id = $this->normalize($id); + list($name, $domain) = explode('@', $id); + + $links = $this->getServiceLinks($domain); + if (!$links) { + return false; + } + + $services = array(); + foreach ($links as $link) { + if ($link['template']) { + return $this->getServiceDescription($link['template'], $id); + } + if ($link['href']) { + return $this->getServiceDescription($link['href'], $id); + } + } + } + + /** + * Normalize an account ID + */ + function normalize($id) + { + if (substr($id, 0, 7) == 'acct://') { + return substr($id, 7); + } else if (substr($id, 0, 5) == 'acct:') { + return substr($id, 5); + } + + return $id; + } + + function getServiceLinks($domain) + { + $url = 'http://'. $domain .'/.well-known/host-meta'; + + $content = $this->fetchURL($url); + + if (empty($content)) { + common_log(LOG_DEBUG, 'Error fetching host-meta'); + return false; + } + + $result = XRD::parse($content); + + // Ensure that the host == domain (spec may include signing later) + if ($result->host != $domain) { + return false; + } + + $links = array(); + foreach ($result->links as $link) { + if ($link['rel'] == WEBFINGER_SERVICE_REL_VALUE) { + $links[] = $link; + } + + } + return $links; + } + + function getServiceDescription($template, $id) + { + $url = $this->applyTemplate($template, 'acct:' . $id); + + $content = $this->fetchURL($url); + + if (!$content) { + return false; + } + + return XRD::parse($content); + } + + function fetchURL($url) + { + try { + $c = Cache::instance(); + $content = $c->get('webfinger:url:'.$url); + if ($content !== false) { + return $content; + } + $client = new HTTPClient(); + $response = $client->get($url); + } catch (HTTP_Request2_Exception $e) { + return false; + } + + if ($response->getStatus() != 200) { + return false; + } + + $body = $response->getBody(); + + $c->set('webfinger:url:'.$url, $body); + + return $body; + } + + function applyTemplate($template, $id) + { + $template = str_replace('{uri}', urlencode($id), $template); + + return $template; + } + + function getHostMeta($domain, $template) { + $xrd = new XRD(); + $xrd->host = $domain; + $xrd->links[] = array('rel' => 'lrdd', + 'template' => $template, + 'title' => array('Resource Descriptor')); + + return $xrd->toXML(); + } +} + diff --git a/scripts/init_conversation.php b/scripts/init_conversation.php new file mode 100755 index 0000000000..675e7cabdd --- /dev/null +++ b/scripts/init_conversation.php @@ -0,0 +1,49 @@ +#!/usr/bin/env php +. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); + +require_once INSTALLDIR.'/scripts/commandline.inc'; + +common_log(LOG_INFO, 'Initializing conversation table...'); + +$notice = new Notice(); +$notice->query('select distinct conversation from notice'); + +while ($notice->fetch()) { + $id = $notice->conversation; + + if ($id) { + $uri = common_local_url('conversation', array('id' => $id)); + + // @fixme db_dataobject won't save our value for an autoincrement + // so we're bypassing the insert wrappers + $conv = new Conversation(); + $sql = "insert into conversation (id,uri,created) values(%d,'%s','%s')"; + $sql = sprintf($sql, + $id, + $conv->escape($uri), + $conv->escape(common_sql_now())); + echo "$id "; + $conv->query($sql); + print "... "; + } +} +print "done.\n"; diff --git a/tests/ActivityParseTests.php b/tests/ActivityParseTests.php index d1d8717343..7bf9cec7c4 100644 --- a/tests/ActivityParseTests.php +++ b/tests/ActivityParseTests.php @@ -121,10 +121,14 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase $this->assertEquals($act->actor->title, 'Test User'); $this->assertEquals($act->actor->id, 'http://example.net/mysite/user/3'); $this->assertEquals($act->actor->link, 'http://example.net/mysite/testuser'); + + $avatars = $act->actor->avatarLinks; + $this->assertEquals( - $act->actor->avatar, - 'http://example.net/mysite/avatar/3-96-20100224004207.jpeg' + $avatars[0]->url, + 'http://example.net/mysite/avatar/3-96-20100224004207.jpeg' ); + $this->assertEquals($act->actor->displayName, 'Test User'); $poco = $act->actor->poco;