2009-11-20 17:42:19 +00:00
< ? php
/*
2010-02-08 19:06:03 +00:00
* StatusNet - the distributed open - source microblogging tool
* Copyright ( C ) 2009 - 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 />.
*/
2010-10-08 18:42:59 +01:00
if ( ! defined ( 'STATUSNET' )) {
exit ( 1 );
}
2010-02-08 19:06:03 +00:00
/**
2010-02-18 21:22:21 +00:00
* @ package OStatusPlugin
2010-02-08 19:06:03 +00:00
* @ maintainer Brion Vibber < brion @ status . net >
*/
2010-02-12 00:22:16 +00:00
class Ostatus_profile extends Memcached_DataObject
2009-11-20 17:42:19 +00:00
{
2010-02-12 00:22:16 +00:00
public $__table = 'ostatus_profile' ;
2009-11-20 17:42:19 +00:00
2010-02-18 21:22:21 +00:00
public $uri ;
2009-11-20 17:42:19 +00:00
public $profile_id ;
2010-02-12 00:22:16 +00:00
public $group_id ;
2009-11-20 17:42:19 +00:00
public $feeduri ;
2010-02-12 00:22:16 +00:00
public $salmonuri ;
2010-02-24 19:06:10 +00:00
public $avatar ; // remote URL of the last avatar we saved
2010-02-12 00:22:16 +00:00
2009-11-20 17:42:19 +00:00
public $created ;
2010-02-18 21:22:21 +00:00
public $modified ;
2009-11-20 17:42:19 +00:00
public /*static*/ function staticGet ( $k , $v = null )
{
return parent :: staticGet ( __CLASS__ , $k , $v );
}
2010-01-04 18:30:19 +00:00
/**
* return table definition for DB_DataObject
*
* DB_DataObject needs to know something about the table to manipulate
* instances . This method provides all the DB_DataObject needs to know .
*
* @ return array array of column definitions
*/
function table ()
{
2010-02-18 21:22:21 +00:00
return array ( 'uri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL ,
2010-02-10 02:32:52 +00:00
'profile_id' => DB_DATAOBJECT_INT ,
'group_id' => DB_DATAOBJECT_INT ,
2010-02-20 00:21:17 +00:00
'feeduri' => DB_DATAOBJECT_STR ,
2010-02-11 20:12:48 +00:00
'salmonuri' => DB_DATAOBJECT_STR ,
2010-02-24 19:06:10 +00:00
'avatar' => DB_DATAOBJECT_STR ,
2010-01-04 18:30:19 +00:00
'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL ,
2010-02-18 21:22:21 +00:00
'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL );
2010-01-04 18:30:19 +00:00
}
2010-02-16 14:58:33 +00:00
2010-01-04 18:30:19 +00:00
static function schemaDef ()
{
2010-02-18 21:22:21 +00:00
return array ( new ColumnDef ( 'uri' , 'varchar' ,
255 , false , 'PRI' ),
2010-01-04 18:30:19 +00:00
new ColumnDef ( 'profile_id' , 'integer' ,
2010-02-11 00:09:20 +00:00
null , true , 'UNI' ),
2010-02-10 02:32:52 +00:00
new ColumnDef ( 'group_id' , 'integer' ,
2010-02-11 00:09:20 +00:00
null , true , 'UNI' ),
2010-01-04 18:30:19 +00:00
new ColumnDef ( 'feeduri' , 'varchar' ,
2010-02-20 00:21:17 +00:00
255 , true , 'UNI' ),
2010-02-11 20:12:48 +00:00
new ColumnDef ( 'salmonuri' , 'text' ,
null , true ),
2010-02-24 19:06:10 +00:00
new ColumnDef ( 'avatar' , 'text' ,
null , true ),
2010-01-04 18:30:19 +00:00
new ColumnDef ( 'created' , 'datetime' ,
null , false ),
2010-02-18 21:22:21 +00:00
new ColumnDef ( 'modified' , 'datetime' ,
2010-01-04 18:30:19 +00:00
null , false ));
}
/**
* return key definitions for DB_DataObject
*
* DB_DataObject needs to know about keys that the table has ; this function
* defines them .
*
* @ return array key definitions
*/
function keys ()
{
2010-02-08 19:06:03 +00:00
return array_keys ( $this -> keyTypes ());
2010-01-04 18:30:19 +00:00
}
/**
* return key definitions for Memcached_DataObject
*
* Our caching system uses the same key definitions , but uses a different
* method to get them .
*
* @ return array key definitions
*/
function keyTypes ()
2009-11-20 17:42:19 +00:00
{
2010-02-18 21:22:21 +00:00
return array ( 'uri' => 'K' , 'profile_id' => 'U' , 'group_id' => 'U' , 'feeduri' => 'U' );
2010-02-08 19:06:03 +00:00
}
function sequenceKey ()
{
2010-02-18 21:22:21 +00:00
return array ( false , false , false );
2009-11-20 17:42:19 +00:00
}
2010-01-04 18:30:19 +00:00
/**
* Fetch the StatusNet - side profile for this feed
* @ return Profile
*/
2010-02-12 01:11:46 +00:00
public function localProfile ()
2009-11-20 17:42:19 +00:00
{
2010-02-12 01:11:46 +00:00
if ( $this -> profile_id ) {
return Profile :: staticGet ( 'id' , $this -> profile_id );
}
return null ;
}
/**
* Fetch the StatusNet - side profile for this feed
* @ return Profile
*/
public function localGroup ()
{
if ( $this -> group_id ) {
return User_group :: staticGet ( 'id' , $this -> group_id );
}
return null ;
2009-11-20 17:42:19 +00:00
}
2010-02-22 17:43:27 +00:00
/**
* Returns an ActivityObject describing this remote user or group profile .
* Can then be used to generate Atom chunks .
*
* @ return ActivityObject
*/
function asActivityObject ()
{
if ( $this -> isGroup ()) {
2010-02-26 02:55:11 +00:00
return ActivityObject :: fromGroup ( $this -> localGroup ());
2010-02-22 17:43:27 +00:00
} else {
return ActivityObject :: fromProfile ( $this -> localProfile ());
}
}
2010-02-12 18:54:48 +00:00
/**
* Returns an XML string fragment with profile information as an
* Activity Streams noun object with the given element type .
*
* Assumes that 'activity' namespace has been previously defined .
*
2010-02-22 17:43:27 +00:00
* @ fixme replace with wrappers on asActivityObject when it ' s got everything .
*
2010-02-12 18:54:48 +00:00
* @ param string $element one of 'actor' , 'subject' , 'object' , 'target'
* @ return string
*/
function asActivityNoun ( $element )
{
if ( $this -> isGroup ()) {
2010-02-26 02:55:11 +00:00
$noun = ActivityObject :: fromGroup ( $this -> localGroup ());
return $noun -> asString ( 'activity:' . $element );
2010-02-12 18:54:48 +00:00
} else {
2010-02-26 02:55:11 +00:00
$noun = ActivityObject :: fromProfile ( $this -> localProfile ());
return $noun -> asString ( 'activity:' . $element );
2010-02-12 18:54:48 +00:00
}
}
2010-02-10 02:32:52 +00:00
/**
2010-02-22 17:43:27 +00:00
* @ return boolean true if this is a remote group
2010-02-10 02:32:52 +00:00
*/
function isGroup ()
{
2010-02-22 17:43:27 +00:00
if ( $this -> profile_id && ! $this -> group_id ) {
return false ;
} else if ( $this -> group_id && ! $this -> profile_id ) {
return true ;
} else if ( $this -> group_id && $this -> profile_id ) {
2010-09-19 14:17:36 +01:00
// TRANS: Server exception.
throw new ServerException ( sprintf ( _m ( 'Invalid ostatus_profile state: both group and profile IDs set for %s.' ), $this -> uri ));
2010-02-22 17:43:27 +00:00
} else {
2010-09-19 14:17:36 +01:00
// TRANS: Server exception.
throw new ServerException ( sprintf ( _m ( 'Invalid ostatus_profile state: both group and profile IDs empty for %s.' ), $this -> uri ));
2010-02-22 17:43:27 +00:00
}
2010-02-10 02:32:52 +00:00
}
2010-02-12 01:11:46 +00:00
/**
2010-02-18 21:22:21 +00:00
* Send a subscription request to the hub for this feed .
* The hub will later send us a confirmation POST to / main / push / callback .
2010-02-12 01:11:46 +00:00
*
2010-02-18 21:22:21 +00:00
* @ return bool true on success , false on failure
* @ throws ServerException if feed state is not valid
2010-02-12 01:11:46 +00:00
*/
2010-02-20 00:21:17 +00:00
public function subscribe ()
2010-02-12 01:11:46 +00:00
{
2010-02-18 21:22:21 +00:00
$feedsub = FeedSub :: ensureFeed ( $this -> feeduri );
2010-03-19 22:47:43 +00:00
if ( $feedsub -> sub_state == 'active' ) {
// Active subscription, we don't need to do anything.
2010-02-18 21:22:21 +00:00
return true ;
2010-03-19 22:47:43 +00:00
} else {
// Inactive or we got left in an inconsistent state.
// Run a subscription request to make sure we're current!
2010-02-18 21:22:21 +00:00
return $feedsub -> subscribe ();
2010-02-12 01:11:46 +00:00
}
}
/**
2010-08-06 18:56:18 +01:00
* Check if this remote profile has any active local subscriptions , and
* if not drop the PuSH subscription feed .
2010-02-18 21:22:21 +00:00
*
* @ return bool true on success , false on failure
2010-02-12 01:11:46 +00:00
*/
2010-02-18 21:22:21 +00:00
public function unsubscribe () {
2010-08-06 18:56:18 +01:00
$this -> garbageCollect ();
2010-02-12 01:11:46 +00:00
}
2010-02-23 20:44:27 +00:00
/**
* Check if this remote profile has any active local subscriptions , and
* if not drop the PuSH subscription feed .
*
* @ return boolean
*/
public function garbageCollect ()
2010-08-06 18:56:18 +01:00
{
$feedsub = FeedSub :: staticGet ( 'uri' , $this -> feeduri );
return $feedsub -> garbageCollect ();
}
/**
* Check if this remote profile has any active local subscriptions , so the
* PuSH subscription layer can decide if it can drop the feed .
*
* This gets called via the FeedSubSubscriberCount event when running
* FeedSub :: garbageCollect () .
*
* @ return int
*/
public function subscriberCount ()
2010-02-23 20:44:27 +00:00
{
if ( $this -> isGroup ()) {
$members = $this -> localGroup () -> getMembers ( 0 , 1 );
$count = $members -> N ;
} else {
$count = $this -> localProfile () -> subscriberCount ();
}
2010-08-06 18:56:18 +01:00
common_log ( LOG_INFO , __METHOD__ . " SUB COUNT BEFORE: $count " );
// Other plugins may be piggybacking on OStatus without having
// an active group or user-to-user subscription we know about.
Event :: handle ( 'Ostatus_profileSubscriberCount' , array ( $this , & $count ));
common_log ( LOG_INFO , __METHOD__ . " SUB COUNT AFTER: $count " );
return $count ;
2010-02-23 20:44:27 +00:00
}
2010-02-12 18:54:48 +00:00
/**
* Send an Activity Streams notification to the remote Salmon endpoint ,
* if so configured .
*
2010-02-21 15:53:11 +00:00
* @ param Profile $actor Actor who did the activity
* @ param string $verb Activity :: SUBSCRIBE or Activity :: JOIN
* @ param Object $object object of the action ; must define asActivityNoun ( $tag )
2010-02-12 18:54:48 +00:00
*/
2010-02-19 20:08:07 +00:00
public function notify ( $actor , $verb , $object = null )
2010-02-12 18:54:48 +00:00
{
2010-02-19 20:08:07 +00:00
if ( ! ( $actor instanceof Profile )) {
$type = gettype ( $actor );
if ( $type == 'object' ) {
$type = get_class ( $actor );
}
2010-09-19 14:17:36 +01:00
// TRANS: Server exception.
// TRANS: %1$s is the method name the exception occured in, %2$s is the actor type.
throw new ServerException ( sprintf ( _m ( 'Invalid actor passed to %1$s: %2$s.' ), __METHOD__ , $type ));
2010-02-19 20:08:07 +00:00
}
2010-02-12 18:54:48 +00:00
if ( $object == null ) {
2010-02-21 02:25:40 +00:00
$object = $this ;
2010-02-12 18:54:48 +00:00
}
if ( $this -> salmonuri ) {
2010-02-21 15:53:11 +00:00
$text = 'update' ;
$id = TagURI :: mint ( '%s:%s:%s' ,
$verb ,
$actor -> getURI (),
2010-02-21 18:31:58 +00:00
common_date_iso8601 ( time ()));
2010-02-12 18:54:48 +00:00
2010-02-20 01:01:38 +00:00
// @fixme consolidate all these NS settings somewhere
$attributes = array ( 'xmlns' => Activity :: ATOM ,
'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/' ,
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0' ,
'xmlns:georss' => 'http://www.georss.org/georss' ,
2010-02-23 20:44:27 +00:00
'xmlns:ostatus' => 'http://ostatus.org/schema/1.0' ,
2010-02-26 00:06:49 +00:00
'xmlns:poco' => 'http://portablecontacts.net/spec/1.0' ,
'xmlns:media' => 'http://purl.org/syndication/atommedia' );
2010-02-20 01:01:38 +00:00
2010-02-18 18:20:48 +00:00
$entry = new XMLStringer ();
2010-02-20 01:01:38 +00:00
$entry -> elementStart ( 'entry' , $attributes );
2010-02-12 18:54:48 +00:00
$entry -> element ( 'id' , null , $id );
$entry -> element ( 'title' , null , $text );
$entry -> element ( 'summary' , null , $text );
2010-02-19 18:29:06 +00:00
$entry -> element ( 'published' , null , common_date_w3dtf ( common_sql_now ()));
2010-02-12 18:54:48 +00:00
$entry -> element ( 'activity:verb' , null , $verb );
2010-02-18 18:20:48 +00:00
$entry -> raw ( $actor -> asAtomAuthor ());
$entry -> raw ( $actor -> asActivityActor ());
2010-02-21 02:25:40 +00:00
$entry -> raw ( $object -> asActivityNoun ( 'object' ));
2010-02-18 18:20:48 +00:00
$entry -> elementEnd ( 'entry' );
2010-02-12 18:54:48 +00:00
2010-02-20 01:01:38 +00:00
$xml = $entry -> getString ();
2010-02-19 20:08:07 +00:00
common_log ( LOG_INFO , " Posting to Salmon endpoint $this->salmonuri : $xml " );
2010-02-12 18:54:48 +00:00
$salmon = new Salmon (); // ?
2010-02-26 19:21:21 +00:00
return $salmon -> post ( $this -> salmonuri , $xml , $actor );
2010-02-12 18:54:48 +00:00
}
2010-02-22 17:43:27 +00:00
return false ;
2010-02-12 18:54:48 +00:00
}
2010-02-24 20:36:36 +00:00
/**
* Send a Salmon notification ping immediately , and confirm that we got
* an acceptable response from the remote site .
*
* @ param mixed $entry XML string , Notice , or Activity
* @ return boolean success
*/
2010-02-26 19:21:21 +00:00
public function notifyActivity ( $entry , $actor )
2010-02-21 15:53:11 +00:00
{
if ( $this -> salmonuri ) {
2010-02-24 20:36:36 +00:00
$salmon = new Salmon ();
2010-02-26 19:21:21 +00:00
return $salmon -> post ( $this -> salmonuri , $this -> notifyPrepXml ( $entry ), $actor );
2010-02-24 20:36:36 +00:00
}
2010-02-21 15:53:11 +00:00
2010-02-24 20:36:36 +00:00
return false ;
}
2010-02-21 15:53:11 +00:00
2010-02-24 20:36:36 +00:00
/**
* Queue a Salmon notification for later . If queues are disabled we ' ll
* send immediately but won ' t get the return value .
*
* @ param mixed $entry XML string , Notice , or Activity
* @ return boolean success
*/
2010-02-26 19:21:21 +00:00
public function notifyDeferred ( $entry , $actor )
2010-02-24 20:36:36 +00:00
{
if ( $this -> salmonuri ) {
$data = array ( 'salmonuri' => $this -> salmonuri ,
2010-02-26 19:21:21 +00:00
'entry' => $this -> notifyPrepXml ( $entry ),
'actor' => $actor -> id );
2010-02-21 15:53:11 +00:00
2010-02-24 20:36:36 +00:00
$qm = QueueManager :: get ();
return $qm -> enqueue ( $data , 'salmon' );
2010-02-21 15:53:11 +00:00
}
2010-02-22 17:43:27 +00:00
return false ;
2010-02-21 15:53:11 +00:00
}
2010-02-24 20:36:36 +00:00
protected function notifyPrepXml ( $entry )
{
$preamble = '<?xml version="1.0" encoding="UTF-8" ?' . '>' ;
if ( is_string ( $entry )) {
return $entry ;
} else if ( $entry instanceof Activity ) {
return $preamble . $entry -> asString ( true );
} else if ( $entry instanceof Notice ) {
return $preamble . $entry -> asAtomEntry ( true , true );
} else {
2010-09-19 14:17:36 +01:00
// TRANS: Server exception.
throw new ServerException ( _m ( 'Invalid type passed to Ostatus_profile::notify. It must be XML string or Activity entry.' ));
2010-02-24 20:36:36 +00:00
}
}
2010-02-12 18:54:48 +00:00
function getBestName ()
{
if ( $this -> isGroup ()) {
return $this -> localGroup () -> getBestName ();
} else {
return $this -> localProfile () -> getBestName ();
}
}
2009-11-20 17:42:19 +00:00
/**
* Read and post notices for updates from the feed .
* Currently assumes that all items in the feed are new ,
* coming from a PuSH hub .
*
2010-03-05 18:55:07 +00:00
* @ param DOMDocument $doc
* @ param string $source identifier ( " push " )
2009-11-20 17:42:19 +00:00
*/
2010-03-05 18:55:07 +00:00
public function processFeed ( DOMDocument $doc , $source )
2009-11-20 17:42:19 +00:00
{
2010-03-05 18:55:07 +00:00
$feed = $doc -> documentElement ;
2010-03-20 12:23:13 +00:00
if ( $feed -> localName == 'feed' && $feed -> namespaceURI == Activity :: ATOM ) {
$this -> processAtomFeed ( $feed , $source );
} else if ( $feed -> localName == 'rss' ) { // @fixme check namespace
$this -> processRssFeed ( $feed , $source );
} else {
2010-09-19 14:17:36 +01:00
throw new Exception ( _m ( 'Unknown feed format.' ));
2010-03-05 18:55:07 +00:00
}
2010-03-20 12:23:13 +00:00
}
2010-03-05 18:55:07 +00:00
2010-03-20 12:23:13 +00:00
public function processAtomFeed ( DOMElement $feed , $source )
{
2010-02-16 23:31:11 +00:00
$entries = $feed -> getElementsByTagNameNS ( Activity :: ATOM , 'entry' );
if ( $entries -> length == 0 ) {
common_log ( LOG_ERR , __METHOD__ . " : no entries in feed update, ignoring " );
return ;
}
for ( $i = 0 ; $i < $entries -> length ; $i ++ ) {
$entry = $entries -> item ( $i );
2010-02-24 01:09:52 +00:00
$this -> processEntry ( $entry , $feed , $source );
2010-02-16 23:31:11 +00:00
}
}
2010-03-20 12:23:13 +00:00
public function processRssFeed ( DOMElement $rss , $source )
{
$channels = $rss -> getElementsByTagName ( 'channel' );
if ( $channels -> length == 0 ) {
2010-09-19 14:17:36 +01:00
throw new Exception ( _m ( 'RSS feed without a channel.' ));
2010-03-20 12:23:13 +00:00
} else if ( $channels -> length > 1 ) {
common_log ( LOG_WARNING , __METHOD__ . " : more than one channel in an RSS feed " );
}
$channel = $channels -> item ( 0 );
$items = $channel -> getElementsByTagName ( 'item' );
for ( $i = 0 ; $i < $items -> length ; $i ++ ) {
$item = $items -> item ( $i );
$this -> processEntry ( $item , $channel , $source );
}
}
2010-02-16 23:31:11 +00:00
/**
* Process a posted entry from this feed source .
*
* @ param DOMElement $entry
* @ param DOMElement $feed for context
2010-03-05 18:55:07 +00:00
* @ param string $source identifier ( " push " or " salmon " )
2010-02-16 23:31:11 +00:00
*/
2010-08-13 21:14:47 +01:00
2010-02-24 01:09:52 +00:00
public function processEntry ( $entry , $feed , $source )
2010-02-16 23:31:11 +00:00
{
$activity = new Activity ( $entry , $feed );
2010-08-13 21:14:47 +01:00
if ( Event :: handle ( 'StartHandleFeedEntry' , array ( $activity ))) {
// @todo process all activity objects
switch ( $activity -> objects [ 0 ] -> type ) {
case ActivityObject :: ARTICLE :
case ActivityObject :: BLOGENTRY :
case ActivityObject :: NOTE :
case ActivityObject :: STATUS :
case ActivityObject :: COMMENT :
2010-08-13 22:35:49 +01:00
case null :
2010-08-13 21:14:47 +01:00
if ( $activity -> verb == ActivityVerb :: POST ) {
$this -> processPost ( $activity , $source );
} else {
common_log ( LOG_INFO , " Ignoring activity with unrecognized verb $activity->verb " );
}
break ;
default :
2010-09-19 14:17:36 +01:00
// TRANS: Client exception.
throw new ClientException ( _m ( 'Can\'t handle that kind of post.' ));
2010-08-13 21:14:47 +01:00
}
2010-03-22 12:17:14 +00:00
2010-08-13 21:14:47 +01:00
Event :: handle ( 'EndHandleFeedEntry' , array ( $activity ));
2010-02-16 23:31:11 +00:00
}
}
/**
* Process an incoming post activity from this remote feed .
* @ param Activity $activity
2010-02-24 01:09:52 +00:00
* @ param string $method 'push' or 'salmon'
* @ return mixed saved Notice or false
2010-02-23 00:44:45 +00:00
* @ fixme break up this function , it ' s getting nasty long
2010-02-16 23:31:11 +00:00
*/
2010-02-24 01:09:52 +00:00
public function processPost ( $activity , $method )
2010-02-16 23:31:11 +00:00
{
2010-02-17 01:49:49 +00:00
if ( $this -> isGroup ()) {
2010-02-24 01:09:52 +00:00
// A group feed will contain posts from multiple authors.
2010-02-17 01:49:49 +00:00
// @fixme validate these profiles in some way!
2010-02-18 21:22:21 +00:00
$oprofile = self :: ensureActorProfile ( $activity );
2010-02-24 01:09:52 +00:00
if ( $oprofile -> isGroup ()) {
// Groups can't post notices in StatusNet.
common_log ( LOG_WARNING , " OStatus: skipping post with group listed as author: $oprofile->uri in feed from $this->uri " );
return false ;
}
2010-02-17 01:49:49 +00:00
} else {
2010-03-20 13:25:56 +00:00
$actor = $activity -> actor ;
if ( empty ( $actor )) {
// OK here! assume the default
} else if ( $actor -> id == $this -> uri || $actor -> link == $this -> uri ) {
$this -> updateFromActivityObject ( $actor );
2010-08-03 00:08:54 +01:00
} else if ( $actor -> id ) {
// We have an ActivityStreams actor with an explicit ID that doesn't match the feed owner.
// This isn't what we expect from mainline OStatus person feeds!
2010-08-13 19:41:44 +01:00
// Group feeds go down another path, with different validation...
// Most likely this is a plain ol' blog feed of some kind which
// doesn't match our expectations. We'll take the entry, but ignore
// the <author> info.
common_log ( LOG_WARNING , " Got an actor ' { $actor -> title } ' ( { $actor -> id } ) on single-user feed for { $this -> uri } " );
2010-08-03 00:08:54 +01:00
} else {
// Plain <author> without ActivityStreams actor info.
// We'll just ignore this info for now and save the update under the feed's identity.
2010-02-17 01:49:49 +00:00
}
2010-03-20 13:25:56 +00:00
2010-02-17 01:49:49 +00:00
$oprofile = $this ;
2010-02-16 23:31:11 +00:00
}
2010-02-10 21:18:53 +00:00
2010-03-20 14:30:57 +00:00
// It's not always an ActivityObject::NOTE, but... let's just say it is.
2010-03-23 01:53:09 +00:00
$note = $activity -> objects [ 0 ];
2010-03-20 14:30:57 +00:00
2010-02-24 01:09:52 +00:00
// The id URI will be used as a unique identifier for for the notice,
// protecting against duplicate saves. It isn't required to be a URL;
// tag: URIs for instance are found in Google Buzz feeds.
2010-03-20 14:30:57 +00:00
$sourceUri = $note -> id ;
2010-02-16 23:31:11 +00:00
$dupe = Notice :: staticGet ( 'uri' , $sourceUri );
if ( $dupe ) {
2010-02-20 16:38:05 +00:00
common_log ( LOG_INFO , " OStatus: ignoring duplicate post: $sourceUri " );
2010-02-24 01:09:52 +00:00
return false ;
2010-02-16 23:31:11 +00:00
}
2010-02-10 21:18:53 +00:00
2010-02-24 01:09:52 +00:00
// We'll also want to save a web link to the original notice, if provided.
2010-02-20 16:38:05 +00:00
$sourceUrl = null ;
2010-03-20 14:30:57 +00:00
if ( $note -> link ) {
$sourceUrl = $note -> link ;
2010-02-23 21:47:14 +00:00
} else if ( $activity -> link ) {
$sourceUrl = $activity -> link ;
2010-03-20 14:30:57 +00:00
} else if ( preg_match ( '!^https?://!' , $note -> id )) {
$sourceUrl = $note -> id ;
}
// Use summary as fallback for content
if ( ! empty ( $note -> content )) {
$sourceContent = $note -> content ;
} else if ( ! empty ( $note -> summary )) {
$sourceContent = $note -> summary ;
} else if ( ! empty ( $note -> title )) {
$sourceContent = $note -> title ;
} else {
// @fixme fetch from $sourceUrl?
2010-09-19 14:17:36 +01:00
// TRANS: Client exception. %s is a source URL.
throw new ClientException ( sprintf ( _m ( 'No content for notice %s.' ), $sourceUri ));
2010-02-20 16:38:05 +00:00
}
2010-02-23 21:47:14 +00:00
// Get (safe!) HTML and text versions of the content
2010-03-20 14:30:57 +00:00
$rendered = $this -> purify ( $sourceContent );
2010-09-30 19:29:31 +01:00
$content = html_entity_decode ( strip_tags ( $rendered ), ENT_QUOTES , 'UTF-8' );
2010-02-16 14:58:33 +00:00
2010-02-25 03:22:42 +00:00
$shortened = common_shorten_links ( $content );
2010-02-25 03:02:43 +00:00
// If it's too long, try using the summary, and make the
// HTML an attachment.
$attachment = null ;
if ( Notice :: contentTooLong ( $shortened )) {
2010-03-20 14:30:57 +00:00
$attachment = $this -> saveHTMLFile ( $note -> title , $rendered );
2010-09-30 19:29:31 +01:00
$summary = html_entity_decode ( strip_tags ( $note -> summary ), ENT_QUOTES , 'UTF-8' );
2010-02-25 03:02:43 +00:00
if ( empty ( $summary )) {
$summary = $content ;
}
2010-02-25 03:22:42 +00:00
$shortSummary = common_shorten_links ( $summary );
2010-02-25 03:02:43 +00:00
if ( Notice :: contentTooLong ( $shortSummary )) {
2010-03-25 23:08:09 +00:00
$url = common_shorten_url ( $sourceUrl );
2010-02-25 03:02:43 +00:00
$shortSummary = substr ( $shortSummary ,
0 ,
Notice :: maxContent () - ( mb_strlen ( $url ) + 2 ));
2010-03-25 21:15:54 +00:00
$content = $shortSummary . ' ' . $url ;
// We mark up the attachment link specially for the HTML output
// so we can fold-out the full version inline.
2010-09-19 14:17:36 +01:00
2010-11-02 20:05:16 +00:00
// @fixme I18N this tooltip will be saved with the site's default language
// TRANS: Shown when a notice is longer than supported and/or when attachments are present. At runtime this will usually be replaced with localized text from StatusNet core messages.
2010-09-19 14:17:36 +01:00
$showMoreText = _m ( 'Show more' );
2010-03-25 21:15:54 +00:00
$attachUrl = common_local_url ( 'attachment' ,
array ( 'attachment' => $attachment -> id ));
$rendered = common_render_text ( $shortSummary ) .
2010-03-18 13:26:30 +00:00
'<a href="' . htmlspecialchars ( $attachUrl ) . '"' .
' class="attachment more"' .
2010-09-19 14:17:36 +01:00
' title="' . htmlspecialchars ( $showMoreText ) . '">' .
2010-03-18 13:26:30 +00:00
'…' .
2010-09-19 14:17:36 +01:00
'</a>' ;
2010-02-25 03:02:43 +00:00
}
}
2010-02-24 01:09:52 +00:00
$options = array ( 'is_local' => Notice :: REMOTE_OMB ,
2010-02-20 16:38:05 +00:00
'url' => $sourceUrl ,
2010-02-23 21:47:14 +00:00
'uri' => $sourceUri ,
2010-02-24 01:09:52 +00:00
'rendered' => $rendered ,
'replies' => array (),
2010-02-25 19:26:33 +00:00
'groups' => array (),
2010-03-03 00:30:09 +00:00
'tags' => array (),
'urls' => array ());
2010-02-25 19:26:33 +00:00
2010-02-24 01:09:52 +00:00
// Check for optional attributes...
2010-02-20 16:38:05 +00:00
2010-02-24 01:09:52 +00:00
if ( ! empty ( $activity -> time )) {
$options [ 'created' ] = common_sql_date ( $activity -> time );
2010-02-16 23:31:11 +00:00
}
2010-02-23 00:44:45 +00:00
if ( $activity -> context ) {
2010-02-24 01:09:52 +00:00
// Any individual or group attn: targets?
$replies = $activity -> context -> attention ;
$options [ 'groups' ] = $this -> filterReplies ( $oprofile , $replies );
$options [ 'replies' ] = $replies ;
// Maintain direct reply associations
// @fixme what about conversation ID?
if ( ! empty ( $activity -> context -> replyToID )) {
$orig = Notice :: staticGet ( 'uri' ,
$activity -> context -> replyToID );
if ( ! empty ( $orig )) {
$options [ 'reply_to' ] = $orig -> id ;
2010-02-23 00:44:45 +00:00
}
2010-02-24 01:09:52 +00:00
}
$location = $activity -> context -> location ;
if ( $location ) {
$options [ 'lat' ] = $location -> lat ;
$options [ 'lon' ] = $location -> lon ;
if ( $location -> location_id ) {
$options [ 'location_ns' ] = $location -> location_ns ;
$options [ 'location_id' ] = $location -> location_id ;
2010-02-23 00:44:45 +00:00
}
}
}
2010-02-10 02:32:52 +00:00
2010-02-25 19:26:33 +00:00
// Atom categories <-> hashtags
foreach ( $activity -> categories as $cat ) {
if ( $cat -> term ) {
$term = common_canonical_tag ( $cat -> term );
if ( $term ) {
$options [ 'tags' ][] = $term ;
}
}
}
2010-03-03 00:30:09 +00:00
// Atom enclosures -> attachment URLs
foreach ( $activity -> enclosures as $href ) {
// @fixme save these locally or....?
$options [ 'urls' ][] = $href ;
}
2010-02-23 00:44:45 +00:00
try {
2010-02-24 01:09:52 +00:00
$saved = Notice :: saveNew ( $oprofile -> profile_id ,
2010-02-23 00:44:45 +00:00
$content ,
'ostatus' ,
2010-02-24 01:09:52 +00:00
$options );
if ( $saved ) {
Ostatus_source :: saveNew ( $saved , $this , $method );
2010-02-25 03:02:43 +00:00
if ( ! empty ( $attachment )) {
File_to_post :: processNew ( $attachment -> id , $saved -> id );
}
2010-02-24 01:09:52 +00:00
}
2010-02-23 00:44:45 +00:00
} catch ( Exception $e ) {
2010-02-24 01:09:52 +00:00
common_log ( LOG_ERR , " OStatus save of remote message $sourceUri failed: " . $e -> getMessage ());
throw $e ;
2010-02-23 00:44:45 +00:00
}
2010-02-24 01:09:52 +00:00
common_log ( LOG_INFO , " OStatus saved remote message $sourceUri as notice id $saved->id " );
return $saved ;
}
2010-02-22 03:51:11 +00:00
2010-02-24 01:09:52 +00:00
/**
* Clean up HTML
*/
protected function purify ( $html )
{
2010-02-24 23:39:40 +00:00
require_once INSTALLDIR . '/extlib/htmLawed/htmLawed.php' ;
2010-03-03 00:30:09 +00:00
$config = array ( 'safe' => 1 ,
'deny_attribute' => 'id,style,on*' );
2010-02-24 23:39:40 +00:00
return htmLawed ( $html , $config );
2010-02-24 01:09:52 +00:00
}
/**
* Filters a list of recipient ID URIs to just those for local delivery .
* @ param Ostatus_profile local profile of sender
* @ param array in / out & $attention_uris set of URIs , will be pruned on output
* @ return array of group IDs
*/
protected function filterReplies ( $sender , & $attention_uris )
{
2010-02-24 02:19:13 +00:00
common_log ( LOG_DEBUG , " Original reply recipients: " . implode ( ', ' , $attention_uris ));
2010-02-24 01:09:52 +00:00
$groups = array ();
$replies = array ();
2010-08-10 21:36:38 +01:00
foreach ( array_unique ( $attention_uris ) as $recipient ) {
2010-02-24 01:09:52 +00:00
// Is the recipient a local user?
$user = User :: staticGet ( 'uri' , $recipient );
if ( $user ) {
// @fixme sender verification, spam etc?
$replies [] = $recipient ;
continue ;
}
// Is the recipient a local group?
// $group = User_group::staticGet('uri', $recipient);
2010-03-11 01:00:05 +00:00
$id = OStatusPlugin :: localGroupFromUrl ( $recipient );
if ( $id ) {
2010-02-24 01:09:52 +00:00
$group = User_group :: staticGet ( 'id' , $id );
if ( $group ) {
// Deliver to all members of this local group if allowed.
2010-02-24 02:19:13 +00:00
$profile = $sender -> localProfile ();
if ( $profile -> isMember ( $group )) {
2010-02-24 01:09:52 +00:00
$groups [] = $group -> id ;
2010-02-24 02:19:13 +00:00
} else {
common_log ( LOG_DEBUG , " Skipping reply to local group $group->nickname as sender $profile->id is not a member " );
2010-02-24 01:09:52 +00:00
}
continue ;
2010-02-24 02:19:13 +00:00
} else {
common_log ( LOG_DEBUG , " Skipping reply to bogus group $recipient " );
2010-02-24 01:09:52 +00:00
}
}
2010-02-24 02:19:13 +00:00
2010-09-16 22:43:27 +01:00
// Is the recipient a remote user or group?
try {
$oprofile = Ostatus_profile :: ensureProfileURI ( $recipient );
if ( $oprofile -> isGroup ()) {
// Deliver to local members of this remote group.
// @fixme sender verification?
$groups [] = $oprofile -> group_id ;
} else {
// may be canonicalized or something
$replies [] = $oprofile -> uri ;
}
continue ;
} catch ( Exception $e ) {
// Neither a recognizable local nor remote user!
common_log ( LOG_DEBUG , " Skipping reply to unrecognized profile $recipient : " . $e -> getMessage ());
}
2010-02-24 02:19:13 +00:00
2010-02-23 00:44:45 +00:00
}
2010-02-24 01:09:52 +00:00
$attention_uris = $replies ;
2010-02-24 02:19:13 +00:00
common_log ( LOG_DEBUG , " Local reply recipients: " . implode ( ', ' , $replies ));
common_log ( LOG_DEBUG , " Local group recipients: " . implode ( ', ' , $groups ));
2010-02-24 01:09:52 +00:00
return $groups ;
2010-02-16 23:31:11 +00:00
}
2010-02-18 21:22:21 +00:00
/**
2010-03-21 22:18:37 +00:00
* Look up and if necessary create an Ostatus_profile for the remote entity
* with the given profile page URL . This should never return null -- you
* will either get an object or an exception will be thrown .
*
2010-02-18 21:22:21 +00:00
* @ param string $profile_url
* @ return Ostatus_profile
2010-04-07 00:32:04 +01:00
* @ throws Exception on various error conditions
* @ throws OStatusShadowException if this reference would obscure a local user / group
2010-02-18 21:22:21 +00:00
*/
2010-03-16 16:25:18 +00:00
public static function ensureProfileURL ( $profile_url , $hints = array ())
2010-02-18 21:22:21 +00:00
{
2010-03-16 16:25:18 +00:00
$oprofile = self :: getFromProfileURL ( $profile_url );
if ( ! empty ( $oprofile )) {
return $oprofile ;
}
$hints [ 'profileurl' ] = $profile_url ;
// Fetch the URL
// XXX: HTTP caching
$client = new HTTPClient ();
$client -> setHeader ( 'Accept' , 'text/html,application/xhtml+xml' );
$response = $client -> get ( $profile_url );
if ( ! $response -> isOk ()) {
2010-09-19 14:17:36 +01:00
// TRANS: Exception. %s is a profile URL.
throw new Exception ( sprintf ( _m ( 'Could not reach profile page %s.' ), $profile_url ));
2010-03-16 16:25:18 +00:00
}
// Check if we have a non-canonical URL
$finalUrl = $response -> getUrl ();
if ( $finalUrl != $profile_url ) {
$hints [ 'profileurl' ] = $finalUrl ;
$oprofile = self :: getFromProfileURL ( $finalUrl );
if ( ! empty ( $oprofile )) {
return $oprofile ;
}
}
// Try to get some hCard data
$body = $response -> getBody ();
$hcardHints = DiscoveryHints :: hcardHints ( $body , $finalUrl );
if ( ! empty ( $hcardHints )) {
$hints = array_merge ( $hints , $hcardHints );
}
// Check if they've got an LRDD header
$lrdd = LinkHeader :: getLink ( $response , 'lrdd' , 'application/xrd+xml' );
if ( ! empty ( $lrdd )) {
$xrd = Discovery :: fetchXrd ( $lrdd );
$xrdHints = DiscoveryHints :: fromXRD ( $xrd );
$hints = array_merge ( $hints , $xrdHints );
}
// If discovery found a feedurl (probably from LRDD), use it.
if ( array_key_exists ( 'feedurl' , $hints )) {
return self :: ensureFeedURL ( $hints [ 'feedurl' ], $hints );
}
// Get the feed URL from HTML
2010-02-18 21:22:21 +00:00
$discover = new FeedDiscovery ();
2010-03-16 16:25:18 +00:00
$feedurl = $discover -> discoverFromHTML ( $finalUrl , $body );
if ( ! empty ( $feedurl )) {
$hints [ 'feedurl' ] = $feedurl ;
return self :: ensureFeedURL ( $feedurl , $hints );
}
2010-03-21 22:18:37 +00:00
2010-09-19 14:17:36 +01:00
// TRANS: Exception.
throw new Exception ( sprintf ( _m ( 'Could not find a feed URL for profile page %s.' ), $finalUrl ));
2010-03-16 16:25:18 +00:00
}
2010-03-21 22:18:37 +00:00
/**
* Look up the Ostatus_profile , if present , for a remote entity with the
* given profile page URL . Will return null for both unknown and invalid
* remote profiles .
*
* @ return mixed Ostatus_profile or null
2010-04-07 00:32:04 +01:00
* @ throws OStatusShadowException for local profiles
2010-03-21 22:18:37 +00:00
*/
2010-03-16 16:25:18 +00:00
static function getFromProfileURL ( $profile_url )
{
$profile = Profile :: staticGet ( 'profileurl' , $profile_url );
if ( empty ( $profile )) {
return null ;
}
// Is it a known Ostatus profile?
$oprofile = Ostatus_profile :: staticGet ( 'profile_id' , $profile -> id );
if ( ! empty ( $oprofile )) {
return $oprofile ;
2010-02-26 03:48:28 +00:00
}
2010-02-18 21:22:21 +00:00
2010-03-16 16:25:18 +00:00
// Is it a local user?
$user = User :: staticGet ( 'id' , $profile -> id );
if ( ! empty ( $user )) {
2010-09-19 14:17:36 +01:00
// @todo i18n FIXME: use sprintf and add i18n (?)
2010-04-07 00:32:04 +01:00
throw new OStatusShadowException ( $profile , " ' $profile_url ' is the profile for local user ' { $user -> nickname } '. " );
2010-03-16 16:25:18 +00:00
}
// Continue discovery; it's a remote profile
// for OMB or some other protocol, may also
// support OStatus
return null ;
}
2010-03-21 22:18:37 +00:00
/**
* Look up and if necessary create an Ostatus_profile for remote entity
* with the given update feed . This should never return null -- you will
* either get an object or an exception will be thrown .
*
* @ return Ostatus_profile
* @ throws Exception
*/
2010-03-16 16:25:18 +00:00
public static function ensureFeedURL ( $feed_url , $hints = array ())
{
$discover = new FeedDiscovery ();
$feeduri = $discover -> discoverFromFeedURL ( $feed_url );
$hints [ 'feedurl' ] = $feeduri ;
2010-08-03 00:08:54 +01:00
$huburi = $discover -> getHubLink ();
2010-02-26 03:48:28 +00:00
$hints [ 'hub' ] = $huburi ;
2010-02-26 18:17:24 +00:00
$salmonuri = $discover -> getAtomLink ( Salmon :: NS_REPLIES );
2010-02-26 03:48:28 +00:00
$hints [ 'salmon' ] = $salmonuri ;
2010-02-18 21:22:21 +00:00
2010-08-03 00:08:54 +01:00
if ( ! $huburi && ! common_config ( 'feedsub' , 'fallback_hub' )) {
2010-02-18 21:22:21 +00:00
// We can only deal with folks with a PuSH hub
throw new FeedSubNoHubException ();
}
2010-03-19 20:50:06 +00:00
$feedEl = $discover -> root ;
if ( $feedEl -> tagName == 'feed' ) {
return self :: ensureAtomFeed ( $feedEl , $hints );
} else if ( $feedEl -> tagName == 'channel' ) {
return self :: ensureRssChannel ( $feedEl , $hints );
} else {
throw new FeedSubBadXmlException ( $feeduri );
}
}
2010-02-21 17:56:46 +00:00
2010-03-21 22:18:37 +00:00
/**
* Look up and , if necessary , create an Ostatus_profile for the remote
* profile with the given Atom feed - actually loaded from the feed .
* This should never return null -- you will either get an object or
* an exception will be thrown .
*
* @ param DOMElement $feedEl root element of a loaded Atom feed
* @ param array $hints additional discovery information passed from higher levels
* @ fixme should this be marked public ?
* @ return Ostatus_profile
* @ throws Exception
*/
2010-03-19 20:50:06 +00:00
public static function ensureAtomFeed ( $feedEl , $hints )
{
// Try to get a profile from the feed activity:subject
2010-02-21 17:56:46 +00:00
$subject = ActivityUtils :: child ( $feedEl , Activity :: SUBJECT , Activity :: SPEC );
if ( ! empty ( $subject )) {
$subjObject = new ActivityObject ( $subject );
2010-02-26 03:48:28 +00:00
return self :: ensureActivityObjectProfile ( $subjObject , $hints );
2010-02-21 17:56:46 +00:00
}
// Otherwise, try the feed author
$author = ActivityUtils :: child ( $feedEl , Activity :: AUTHOR , Activity :: ATOM );
if ( ! empty ( $author )) {
$authorObject = new ActivityObject ( $author );
2010-02-26 03:48:28 +00:00
return self :: ensureActivityObjectProfile ( $authorObject , $hints );
2010-02-21 17:56:46 +00:00
}
// Sheesh. Not a very nice feed! Let's try fingerpoken in the
// entries.
2010-03-20 14:46:22 +00:00
$entries = $feedEl -> getElementsByTagNameNS ( Activity :: ATOM , 'entry' );
2010-02-21 17:56:46 +00:00
if ( ! empty ( $entries ) && $entries -> length > 0 ) {
$entry = $entries -> item ( 0 );
$actor = ActivityUtils :: child ( $entry , Activity :: ACTOR , Activity :: SPEC );
if ( ! empty ( $actor )) {
$actorObject = new ActivityObject ( $actor );
2010-02-26 03:48:28 +00:00
return self :: ensureActivityObjectProfile ( $actorObject , $hints );
2010-02-21 17:56:46 +00:00
}
$author = ActivityUtils :: child ( $entry , Activity :: AUTHOR , Activity :: ATOM );
if ( ! empty ( $author )) {
$authorObject = new ActivityObject ( $author );
2010-02-26 03:48:28 +00:00
return self :: ensureActivityObjectProfile ( $authorObject , $hints );
2010-02-21 17:56:46 +00:00
}
2010-02-18 21:22:21 +00:00
}
2010-02-21 17:56:46 +00:00
// XXX: make some educated guesses here
2010-09-19 14:17:36 +01:00
throw new FeedSubException ( _m ( 'Can\'t find enough profile information to make a feed.' ));
2010-02-18 21:22:21 +00:00
}
2010-03-21 22:18:37 +00:00
/**
* Look up and , if necessary , create an Ostatus_profile for the remote
* profile with the given RSS feed - actually loaded from the feed .
* This should never return null -- you will either get an object or
* an exception will be thrown .
*
* @ param DOMElement $feedEl root element of a loaded RSS feed
* @ param array $hints additional discovery information passed from higher levels
* @ fixme should this be marked public ?
* @ return Ostatus_profile
* @ throws Exception
*/
2010-03-19 20:50:06 +00:00
public static function ensureRssChannel ( $feedEl , $hints )
{
2010-03-20 22:18:55 +00:00
// Special-case for Posterous. They have some nice metadata in their
// posterous:author elements. We should use them instead of the channel.
$items = $feedEl -> getElementsByTagName ( 'item' );
if ( $items -> length > 0 ) {
$item = $items -> item ( 0 );
$authorEl = ActivityUtils :: child ( $item , ActivityObject :: AUTHOR , ActivityObject :: POSTEROUS );
if ( ! empty ( $authorEl )) {
$obj = ActivityObject :: fromPosterousAuthor ( $authorEl );
2010-03-21 12:37:58 +00:00
// Posterous has multiple authors per feed, and multiple feeds
// per author. We check if this is the "main" feed for this author.
if ( array_key_exists ( 'profileurl' , $hints ) &&
! empty ( $obj -> poco ) &&
common_url_to_nickname ( $hints [ 'profileurl' ]) == $obj -> poco -> preferredUsername ) {
return self :: ensureActivityObjectProfile ( $obj , $hints );
}
2010-03-20 22:18:55 +00:00
}
}
2010-03-19 20:50:06 +00:00
// @fixme we should check whether this feed has elements
// with different <author> or <dc:creator> elements, and... I dunno.
// Do something about that.
$obj = ActivityObject :: fromRssChannel ( $feedEl );
return self :: ensureActivityObjectProfile ( $obj , $hints );
}
2010-02-18 21:22:21 +00:00
/**
* Download and update given avatar image
2010-03-19 17:15:00 +00:00
*
2010-02-18 21:22:21 +00:00
* @ param string $url
* @ throws Exception in various failure cases
*/
protected function updateAvatar ( $url )
{
2010-02-24 19:06:10 +00:00
if ( $url == $this -> avatar ) {
// We've already got this one.
return ;
}
2010-03-19 17:15:00 +00:00
if ( ! common_valid_http_url ( $url )) {
2010-09-03 00:35:04 +01:00
throw new ServerException ( sprintf ( _m ( " Invalid avatar URL %s. " ), $url ));
2010-03-19 17:15:00 +00:00
}
2010-02-24 19:06:10 +00:00
2010-02-22 17:43:27 +00:00
if ( $this -> isGroup ()) {
$self = $this -> localGroup ();
} else {
$self = $this -> localProfile ();
}
if ( ! $self ) {
throw new ServerException ( sprintf (
2010-09-03 00:35:04 +01:00
_m ( " Tried to update avatar for unsaved remote profile %s. " ),
2010-02-22 17:43:27 +00:00
$this -> uri ));
}
2010-02-18 21:22:21 +00:00
// @fixme this should be better encapsulated
// ripped from oauthstore.php (for old OMB client)
$temp_filename = tempnam ( sys_get_temp_dir (), 'listener_avatar' );
2010-10-13 19:04:41 +01:00
try {
if ( ! copy ( $url , $temp_filename )) {
throw new ServerException ( sprintf ( _m ( " Unable to fetch avatar from %s. " ), $url ));
}
2010-02-19 03:18:14 +00:00
2010-10-13 19:04:41 +01:00
if ( $this -> isGroup ()) {
$id = $this -> group_id ;
} else {
$id = $this -> profile_id ;
}
// @fixme should we be using different ids?
$imagefile = new ImageFile ( $id , $temp_filename );
$filename = Avatar :: filename ( $id ,
image_type_to_extension ( $imagefile -> type ),
null ,
common_timestamp ());
rename ( $temp_filename , Avatar :: path ( $filename ));
} catch ( Exception $e ) {
unlink ( $temp_filename );
throw $e ;
2010-02-19 20:08:07 +00:00
}
2010-09-08 00:49:05 +01:00
// @fixme hardcoded chmod is lame, but seems to be necessary to
// keep from accidentally saving images from command-line (queues)
// that can't be read from web server, which causes hard-to-notice
// problems later on:
//
// http://status.net/open-source/issues/2663
chmod ( Avatar :: path ( $filename ), 0644 );
2010-02-22 17:43:27 +00:00
$self -> setOriginal ( $filename );
2010-02-24 19:06:10 +00:00
$orig = clone ( $this );
$this -> avatar = $url ;
$this -> update ( $orig );
2010-02-18 21:22:21 +00:00
}
2010-02-24 19:06:10 +00:00
/**
* Pull avatar URL from ActivityObject or profile hints
*
* @ param ActivityObject $object
* @ param array $hints
* @ return mixed URL string or false
*/
2010-09-21 11:13:19 +01:00
public static function getActivityObjectAvatar ( $object , $hints = array ())
2010-02-21 17:56:46 +00:00
{
2010-02-26 02:51:44 +00:00
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 ;
2010-02-24 19:06:10 +00:00
} else if ( array_key_exists ( 'avatar' , $hints )) {
return $hints [ 'avatar' ];
}
return false ;
2010-02-21 17:56:46 +00:00
}
2010-02-17 01:49:49 +00:00
/**
* Get an appropriate avatar image source URL , if available .
*
* @ param ActivityObject $actor
* @ param DOMElement $feed
* @ return string
*/
2010-02-21 17:56:46 +00:00
2010-02-18 21:22:21 +00:00
protected static function getAvatar ( $actor , $feed )
2010-02-17 01:49:49 +00:00
{
$url = '' ;
$icon = '' ;
if ( $actor -> avatar ) {
$url = trim ( $actor -> avatar );
}
if ( ! $url ) {
// Check <atom:logo> and <atom:icon> on the feed
$els = $feed -> childNodes ();
if ( $els && $els -> length ) {
for ( $i = 0 ; $i < $els -> length ; $i ++ ) {
$el = $els -> item ( $i );
if ( $el -> namespaceURI == Activity :: ATOM ) {
if ( empty ( $url ) && $el -> localName == 'logo' ) {
$url = trim ( $el -> textContent );
break ;
}
if ( empty ( $icon ) && $el -> localName == 'icon' ) {
// Use as a fallback
$icon = trim ( $el -> textContent );
}
}
}
}
if ( $icon && ! $url ) {
$url = $icon ;
}
}
if ( $url ) {
$opts = array ( 'allowed_schemes' => array ( 'http' , 'https' ));
if ( Validate :: uri ( $url , $opts )) {
return $url ;
}
}
return common_path ( 'plugins/OStatus/images/96px-Feed-icon.svg.png' );
}
/**
2010-02-18 21:22:21 +00:00
* Fetch , or build if necessary , an Ostatus_profile for the actor
* in a given Activity Streams activity .
2010-03-21 22:18:37 +00:00
* This should never return null -- you will either get an object or
* an exception will be thrown .
2010-02-18 21:22:21 +00:00
*
* @ param Activity $activity
* @ param string $feeduri if we already know the canonical feed URI !
2010-02-19 18:29:06 +00:00
* @ param string $salmonuri if we already know the salmon return channel URI
2010-02-18 21:22:21 +00:00
* @ return Ostatus_profile
2010-03-21 22:18:37 +00:00
* @ throws Exception
2010-02-17 01:49:49 +00:00
*/
2010-02-21 17:56:46 +00:00
2010-02-26 03:48:28 +00:00
public static function ensureActorProfile ( $activity , $hints = array ())
2010-02-17 01:49:49 +00:00
{
2010-02-26 03:48:28 +00:00
return self :: ensureActivityObjectProfile ( $activity -> actor , $hints );
2010-02-21 17:56:46 +00:00
}
2010-03-21 22:18:37 +00:00
/**
* Fetch , or build if necessary , an Ostatus_profile for the profile
* in a given Activity Streams object ( can be subject , actor , or object ) .
* This should never return null -- you will either get an object or
* an exception will be thrown .
*
* @ param ActivityObject $object
* @ param array $hints additional discovery information passed from higher levels
* @ return Ostatus_profile
* @ throws Exception
*/
2010-02-26 03:48:28 +00:00
public static function ensureActivityObjectProfile ( $object , $hints = array ())
2010-02-21 17:56:46 +00:00
{
$profile = self :: getActivityObjectProfile ( $object );
2010-02-24 19:06:10 +00:00
if ( $profile ) {
$profile -> updateFromActivityObject ( $object , $hints );
} else {
2010-02-26 03:48:28 +00:00
$profile = self :: createActivityObjectProfile ( $object , $hints );
2010-02-17 01:49:49 +00:00
}
return $profile ;
}
/**
* @ param Activity $activity
* @ return mixed matching Ostatus_profile or false if none known
2010-03-21 22:46:28 +00:00
* @ throws ServerException if feed info invalid
2010-02-17 01:49:49 +00:00
*/
2010-02-24 23:28:01 +00:00
public static function getActorProfile ( $activity )
2010-02-17 01:49:49 +00:00
{
2010-02-21 17:56:46 +00:00
return self :: getActivityObjectProfile ( $activity -> actor );
}
2010-03-21 22:46:28 +00:00
/**
* @ param ActivityObject $activity
* @ return mixed matching Ostatus_profile or false if none known
* @ throws ServerException if feed info invalid
*/
2010-02-21 17:56:46 +00:00
protected static function getActivityObjectProfile ( $object )
{
$uri = self :: getActivityObjectProfileURI ( $object );
2010-02-21 19:14:45 +00:00
return Ostatus_profile :: staticGet ( 'uri' , $uri );
2010-02-21 17:56:46 +00:00
}
2010-02-17 01:49:49 +00:00
/**
2010-03-21 22:46:28 +00:00
* Get the identifier URI for the remote entity described
* by this ActivityObject . This URI is * not * guaranteed to be
* a resolvable HTTP / HTTPS URL .
*
* @ param ActivityObject $object
2010-02-17 01:49:49 +00:00
* @ return string
2010-03-21 22:46:28 +00:00
* @ throws ServerException if feed info invalid
2010-02-17 01:49:49 +00:00
*/
2010-02-21 17:56:46 +00:00
protected static function getActivityObjectProfileURI ( $object )
2010-02-17 01:49:49 +00:00
{
2010-03-21 22:46:28 +00:00
if ( $object -> id ) {
2010-03-21 23:25:12 +00:00
if ( ActivityUtils :: validateUri ( $object -> id )) {
2010-03-21 22:46:28 +00:00
return $object -> id ;
}
2010-02-17 01:49:49 +00:00
}
2010-03-21 22:46:28 +00:00
// If the id is missing or invalid (we've seen feeds mistakenly listing
// things like local usernames in that field) then we'll use the profile
// page link, if valid.
if ( $object -> link && common_valid_http_url ( $object -> link )) {
2010-02-21 17:56:46 +00:00
return $object -> link ;
2010-02-17 01:49:49 +00:00
}
2010-09-03 00:35:04 +01:00
throw new ServerException ( " No author ID URI found. " );
2010-02-17 01:49:49 +00:00
}
/**
2010-02-18 21:22:21 +00:00
* @ fixme validate stuff somewhere
2010-02-17 01:49:49 +00:00
*/
2010-02-21 17:56:46 +00:00
2010-02-22 17:43:27 +00:00
/**
* Create local ostatus_profile and profile / user_group entries for
* the provided remote user or group .
2010-03-21 22:18:37 +00:00
* This should never return null -- you will either get an object or
* an exception will be thrown .
2010-02-22 17:43:27 +00:00
*
* @ param ActivityObject $object
* @ param array $hints
*
* @ return Ostatus_profile
*/
2010-02-26 03:48:28 +00:00
protected static function createActivityObjectProfile ( $object , $hints = array ())
2010-02-21 17:56:46 +00:00
{
2010-02-26 03:48:28 +00:00
$homeuri = $object -> id ;
$discover = false ;
2010-02-18 21:22:21 +00:00
if ( ! $homeuri ) {
common_log ( LOG_DEBUG , __METHOD__ . " empty actor profile URI: " . var_export ( $activity , true ));
2010-03-11 01:00:05 +00:00
throw new Exception ( " No profile URI " );
}
2010-03-21 22:18:37 +00:00
$user = User :: staticGet ( 'uri' , $homeuri );
if ( $user ) {
2010-09-19 14:17:36 +01:00
// TRANS: Exception.
throw new Exception ( _m ( 'Local user can\'t be referenced as remote.' ));
2010-03-11 01:00:05 +00:00
}
if ( OStatusPlugin :: localGroupFromUrl ( $homeuri )) {
2010-09-19 14:17:36 +01:00
// TRANS: Exception.
throw new Exception ( _m ( 'Local group can\'t be referenced as remote.' ));
2010-02-18 21:22:21 +00:00
}
2010-02-17 01:49:49 +00:00
2010-02-26 03:48:28 +00:00
if ( array_key_exists ( 'feedurl' , $hints )) {
$feeduri = $hints [ 'feedurl' ];
} else {
$discover = new FeedDiscovery ();
$feeduri = $discover -> discoverFromURL ( $homeuri );
2010-02-22 02:57:09 +00:00
}
2010-02-26 03:48:28 +00:00
if ( array_key_exists ( 'salmon' , $hints )) {
$salmonuri = $hints [ 'salmon' ];
} else {
if ( ! $discover ) {
$discover = new FeedDiscovery ();
$discover -> discoverFromFeedURL ( $hints [ 'feedurl' ]);
2010-02-22 02:57:09 +00:00
}
2010-02-26 18:17:24 +00:00
$salmonuri = $discover -> getAtomLink ( Salmon :: NS_REPLIES );
2010-02-22 02:57:09 +00:00
}
2010-02-26 03:48:28 +00:00
if ( array_key_exists ( 'hub' , $hints )) {
$huburi = $hints [ 'hub' ];
} else {
if ( ! $discover ) {
$discover = new FeedDiscovery ();
$discover -> discoverFromFeedURL ( $hints [ 'feedurl' ]);
}
2010-08-03 00:08:54 +01:00
$huburi = $discover -> getHubLink ();
2010-02-26 03:48:28 +00:00
}
2010-02-20 16:17:54 +00:00
2010-08-03 00:08:54 +01:00
if ( ! $huburi && ! common_config ( 'feedsub' , 'fallback_hub' )) {
2010-02-26 03:48:28 +00:00
// We can only deal with folks with a PuSH hub
throw new FeedSubNoHubException ();
2010-02-20 00:21:17 +00:00
}
2010-02-17 01:49:49 +00:00
$oprofile = new Ostatus_profile ();
2010-02-21 17:56:46 +00:00
$oprofile -> uri = $homeuri ;
$oprofile -> feeduri = $feeduri ;
$oprofile -> salmonuri = $salmonuri ;
2010-02-17 01:49:49 +00:00
2010-02-21 17:56:46 +00:00
$oprofile -> created = common_sql_now ();
$oprofile -> modified = common_sql_now ();
2010-02-19 18:29:06 +00:00
2010-02-22 17:43:27 +00:00
if ( $object -> type == ActivityObject :: PERSON ) {
$profile = new Profile ();
2010-02-25 21:02:08 +00:00
$profile -> created = common_sql_now ();
2010-02-24 19:06:10 +00:00
self :: updateProfile ( $profile , $object , $hints );
2010-02-22 17:43:27 +00:00
$oprofile -> profile_id = $profile -> insert ();
if ( ! $oprofile -> profile_id ) {
2010-09-19 14:17:36 +01:00
// TRANS: Exception.
throw new ServerException ( _m ( 'Can\'t save local profile.' ));
2010-02-22 17:43:27 +00:00
}
} else {
$group = new User_group ();
2010-02-25 21:02:08 +00:00
$group -> uri = $homeuri ;
2010-02-22 17:43:27 +00:00
$group -> created = common_sql_now ();
2010-02-24 19:06:10 +00:00
self :: updateGroup ( $group , $object , $hints );
2010-02-22 17:43:27 +00:00
$oprofile -> group_id = $group -> insert ();
if ( ! $oprofile -> group_id ) {
2010-09-19 14:17:36 +01:00
// TRANS: Exception.
throw new ServerException ( _m ( 'Can\'t save local profile.' ));
2010-02-22 17:43:27 +00:00
}
}
2010-02-17 01:49:49 +00:00
$ok = $oprofile -> insert ();
2010-02-21 17:56:46 +00:00
2010-04-22 02:19:16 +01:00
if ( ! $ok ) {
2010-09-19 14:17:36 +01:00
// TRANS: Exception.
throw new ServerException ( _m ( 'Can\'t save OStatus profile.' ));
2010-04-22 02:19:16 +01:00
}
$avatar = self :: getActivityObjectAvatar ( $object , $hints );
if ( $avatar ) {
try {
2010-02-22 17:43:27 +00:00
$oprofile -> updateAvatar ( $avatar );
2010-04-22 02:19:16 +01:00
} catch ( Exception $ex ) {
// Profile is saved, but Avatar is messed up. We're
// just going to continue.
common_log ( LOG_WARNING , " Exception saving OStatus profile avatar: " . $ex -> getMessage ());
2010-02-22 17:43:27 +00:00
}
2010-02-17 01:49:49 +00:00
}
2010-04-22 02:19:16 +01:00
return $oprofile ;
2010-02-17 01:49:49 +00:00
}
2010-02-24 19:06:10 +00:00
/**
* Save any updated profile information to our local copy .
* @ param ActivityObject $object
* @ param array $hints
*/
2010-02-24 23:28:01 +00:00
public function updateFromActivityObject ( $object , $hints = array ())
2010-02-24 19:06:10 +00:00
{
if ( $this -> isGroup ()) {
$group = $this -> localGroup ();
self :: updateGroup ( $group , $object , $hints );
} else {
$profile = $this -> localProfile ();
self :: updateProfile ( $profile , $object , $hints );
}
$avatar = self :: getActivityObjectAvatar ( $object , $hints );
if ( $avatar ) {
2010-04-22 02:19:16 +01:00
try {
$this -> updateAvatar ( $avatar );
} catch ( Exception $ex ) {
common_log ( LOG_WARNING , " Exception saving OStatus profile avatar: " . $ex -> getMessage ());
}
2010-02-24 19:06:10 +00:00
}
}
2010-09-21 11:13:19 +01:00
public static function updateProfile ( $profile , $object , $hints = array ())
2010-02-24 19:06:10 +00:00
{
$orig = clone ( $profile );
$profile -> nickname = self :: getActivityObjectNickname ( $object , $hints );
2010-02-26 00:58:51 +00:00
if ( ! empty ( $object -> title )) {
$profile -> fullname = $object -> title ;
} else if ( array_key_exists ( 'fullname' , $hints )) {
$profile -> fullname = $hints [ 'fullname' ];
}
2010-02-24 19:06:10 +00:00
if ( ! empty ( $object -> link )) {
$profile -> profileurl = $object -> link ;
} else if ( array_key_exists ( 'profileurl' , $hints )) {
$profile -> profileurl = $hints [ 'profileurl' ];
2010-02-26 00:01:22 +00:00
} else if ( Validate :: uri ( $object -> id , array ( 'allowed_schemes' => array ( 'http' , 'https' )))) {
$profile -> profileurl = $object -> id ;
2010-02-24 19:06:10 +00:00
}
2010-02-25 00:51:24 +00:00
$profile -> bio = self :: getActivityObjectBio ( $object , $hints );
$profile -> location = self :: getActivityObjectLocation ( $object , $hints );
$profile -> homepage = self :: getActivityObjectHomepage ( $object , $hints );
if ( ! empty ( $object -> geopoint )) {
$location = ActivityContext :: locationFromPoint ( $object -> geopoint );
if ( ! empty ( $location )) {
$profile -> lat = $location -> lat ;
$profile -> lon = $location -> lon ;
}
}
2010-02-24 19:06:10 +00:00
// @fixme tags/categories
// @todo tags from categories
if ( $profile -> id ) {
common_log ( LOG_DEBUG , " Updating OStatus profile $profile->id from remote info $object->id : " . var_export ( $object , true ) . var_export ( $hints , true ));
$profile -> update ( $orig );
}
}
protected static function updateGroup ( $group , $object , $hints = array ())
{
$orig = clone ( $group );
$group -> nickname = self :: getActivityObjectNickname ( $object , $hints );
$group -> fullname = $object -> title ;
2010-02-25 21:02:08 +00:00
if ( ! empty ( $object -> link )) {
$group -> mainpage = $object -> link ;
} else if ( array_key_exists ( 'profileurl' , $hints )) {
$group -> mainpage = $hints [ 'profileurl' ];
}
2010-02-24 19:06:10 +00:00
// @todo tags from categories
2010-02-25 21:02:08 +00:00
$group -> description = self :: getActivityObjectBio ( $object , $hints );
$group -> location = self :: getActivityObjectLocation ( $object , $hints );
$group -> homepage = self :: getActivityObjectHomepage ( $object , $hints );
2010-02-24 19:06:10 +00:00
if ( $group -> id ) {
common_log ( LOG_DEBUG , " Updating OStatus group $group->id from remote info $object->id : " . var_export ( $object , true ) . var_export ( $hints , true ));
$group -> update ( $orig );
}
}
2010-02-25 00:51:24 +00:00
protected static function getActivityObjectHomepage ( $object , $hints = array ())
{
$homepage = null ;
$poco = $object -> poco ;
if ( ! empty ( $poco )) {
$url = $poco -> getPrimaryURL ();
2010-03-02 00:35:36 +00:00
if ( $url && $url -> type == 'homepage' ) {
2010-02-25 00:51:24 +00:00
$homepage = $url -> value ;
}
}
// @todo Try for a another PoCo URL?
return $homepage ;
}
protected static function getActivityObjectLocation ( $object , $hints = array ())
{
$location = null ;
2010-02-26 00:58:51 +00:00
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 ) . ' … ' ;
2010-02-25 00:51:24 +00:00
}
}
// @todo Try to find location some othe way? Via goerss point?
return $location ;
}
protected static function getActivityObjectBio ( $object , $hints = array ())
{
$bio = null ;
if ( ! empty ( $object -> poco )) {
$note = $object -> poco -> note ;
2010-02-26 00:58:51 +00:00
} 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 ;
2010-02-25 00:51:24 +00:00
}
}
// @todo Try to get bio info some other way?
return $bio ;
}
2010-09-21 11:13:19 +01:00
public static function getActivityObjectNickname ( $object , $hints = array ())
2010-02-21 17:56:46 +00:00
{
2010-02-24 19:06:10 +00:00
if ( $object -> poco ) {
if ( ! empty ( $object -> poco -> preferredUsername )) {
return common_nicknamize ( $object -> poco -> preferredUsername );
}
}
2010-02-26 00:58:51 +00:00
2010-02-22 12:57:44 +00:00
if ( ! empty ( $object -> nickname )) {
return common_nicknamize ( $object -> nickname );
}
2010-02-21 17:56:46 +00:00
2010-02-26 00:58:51 +00:00
if ( array_key_exists ( 'nickname' , $hints )) {
return $hints [ 'nickname' ];
}
2010-03-20 11:44:38 +00:00
// Try the profile url (like foo.example.com or example.com/user/foo)
2010-02-22 02:57:09 +00:00
2010-03-20 11:44:38 +00:00
$profileUrl = ( $object -> link ) ? $object -> link : $hints [ 'profileurl' ];
if ( ! empty ( $profileUrl )) {
$nickname = self :: nicknameFromURI ( $profileUrl );
}
// Try the URI (may be a tag:, http:, acct:, ...
if ( empty ( $nickname )) {
$nickname = self :: nicknameFromURI ( $object -> id );
}
2010-02-21 17:56:46 +00:00
2010-02-22 02:57:09 +00:00
// Try a Webfinger if one was passed (way) down
if ( empty ( $nickname )) {
if ( array_key_exists ( 'webfinger' , $hints )) {
$nickname = self :: nicknameFromURI ( $hints [ 'webfinger' ]);
}
}
// Try the name
2010-02-21 17:56:46 +00:00
if ( empty ( $nickname )) {
$nickname = common_nicknamize ( $object -> title );
2010-02-17 01:49:49 +00:00
}
2010-02-21 17:56:46 +00:00
return $nickname ;
2010-02-17 01:49:49 +00:00
}
2010-02-21 17:56:46 +00:00
protected static function nicknameFromURI ( $uri )
{
preg_match ( '/(\w+):/' , $uri , $matches );
$protocol = $matches [ 1 ];
switch ( $protocol ) {
case 'acct' :
case 'mailto' :
if ( preg_match ( " /^ $protocol :(.*)?@.* \$ / " , $uri , $matches )) {
return common_canonical_nickname ( $matches [ 1 ]);
}
return null ;
case 'http' :
return common_url_to_nickname ( $uri );
break ;
default :
return null ;
}
}
2010-02-22 02:37:12 +00:00
2010-03-04 02:23:28 +00:00
/**
2010-03-21 22:18:37 +00:00
* Look up , and if necessary create , an Ostatus_profile for the remote
* entity with the given webfinger address .
* This should never return null -- you will either get an object or
* an exception will be thrown .
*
2010-03-04 02:23:28 +00:00
* @ param string $addr webfinger address
* @ return Ostatus_profile
* @ throws Exception on error conditions
2010-04-07 00:32:04 +01:00
* @ throws OStatusShadowException if this reference would obscure a local user / group
2010-03-04 02:23:28 +00:00
*/
2010-02-22 02:37:12 +00:00
public static function ensureWebfinger ( $addr )
{
2010-02-26 01:29:52 +00:00
// First, try the cache
$uri = self :: cacheGet ( sprintf ( 'ostatus_profile:webfinger:%s' , $addr ));
if ( $uri !== false ) {
if ( is_null ( $uri )) {
2010-03-04 02:23:28 +00:00
// Negative cache entry
2010-09-19 14:17:36 +01:00
// TRANS: Exception.
throw new Exception ( _m ( 'Not a valid webfinger address.' ));
2010-02-26 01:29:52 +00:00
}
$oprofile = Ostatus_profile :: staticGet ( 'uri' , $uri );
if ( ! empty ( $oprofile )) {
return $oprofile ;
}
}
2010-03-16 16:25:18 +00:00
// Try looking it up
2010-02-22 02:37:12 +00:00
$oprofile = Ostatus_profile :: staticGet ( 'uri' , 'acct:' . $addr );
if ( ! empty ( $oprofile )) {
2010-02-26 01:29:52 +00:00
self :: cacheSet ( sprintf ( 'ostatus_profile:webfinger:%s' , $addr ), $oprofile -> uri );
2010-02-22 02:37:12 +00:00
return $oprofile ;
}
// Now, try some discovery
2010-02-25 22:34:56 +00:00
$disco = new Discovery ();
2010-02-22 02:37:12 +00:00
2010-02-26 20:38:48 +00:00
try {
2010-03-16 16:25:18 +00:00
$xrd = $disco -> lookup ( $addr );
2010-02-26 20:38:48 +00:00
} catch ( Exception $e ) {
2010-03-04 02:23:28 +00:00
// Save negative cache entry so we don't waste time looking it up again.
2010-03-04 02:28:39 +00:00
// @fixme distinguish temporary failures?
2010-02-26 01:29:52 +00:00
self :: cacheSet ( sprintf ( 'ostatus_profile:webfinger:%s' , $addr ), null );
2010-09-19 14:17:36 +01:00
// TRANS: Exception.
throw new Exception ( _m ( 'Not a valid webfinger address.' ));
2010-02-22 02:37:12 +00:00
}
2010-03-04 02:28:39 +00:00
$hints = array ( 'webfinger' => $addr );
2010-03-16 16:25:18 +00:00
$dhints = DiscoveryHints :: fromXRD ( $xrd );
$hints = array_merge ( $hints , $dhints );
// If there's an Hcard, let's grab its info
2010-02-22 02:37:12 +00:00
2010-03-16 16:25:18 +00:00
if ( array_key_exists ( 'hcard' , $hints )) {
if ( ! array_key_exists ( 'profileurl' , $hints ) ||
$hints [ 'hcard' ] != $hints [ 'profileurl' ]) {
$hcardHints = DiscoveryHints :: fromHcardUrl ( $hints [ 'hcard' ]);
$hints = array_merge ( $hcardHints , $hints );
}
2010-02-26 00:58:51 +00:00
}
2010-02-22 02:37:12 +00:00
// If we got a feed URL, try that
2010-03-16 16:25:18 +00:00
if ( array_key_exists ( 'feedurl' , $hints )) {
2010-02-22 02:37:12 +00:00
try {
2010-03-18 22:11:25 +00:00
common_log ( LOG_INFO , " Discovery on acct: $addr with feed URL " . $hints [ 'feedurl' ]);
2010-03-16 16:25:18 +00:00
$oprofile = self :: ensureFeedURL ( $hints [ 'feedurl' ], $hints );
2010-02-26 01:29:52 +00:00
self :: cacheSet ( sprintf ( 'ostatus_profile:webfinger:%s' , $addr ), $oprofile -> uri );
2010-02-22 02:37:12 +00:00
return $oprofile ;
} catch ( Exception $e ) {
common_log ( LOG_WARNING , " Failed creating profile from feed URL ' $feedUrl ': " . $e -> getMessage ());
// keep looking
}
}
// If we got a profile page, try that!
2010-03-16 16:25:18 +00:00
if ( array_key_exists ( 'profileurl' , $hints )) {
2010-02-22 02:37:12 +00:00
try {
2010-02-26 02:07:52 +00:00
common_log ( LOG_INFO , " Discovery on acct: $addr with profile URL $profileUrl " );
2010-03-18 20:13:57 +00:00
$oprofile = self :: ensureProfileURL ( $hints [ 'profileurl' ], $hints );
2010-02-26 01:29:52 +00:00
self :: cacheSet ( sprintf ( 'ostatus_profile:webfinger:%s' , $addr ), $oprofile -> uri );
2010-02-22 02:37:12 +00:00
return $oprofile ;
2010-04-07 00:32:04 +01:00
} catch ( OStatusShadowException $e ) {
// We've ended up with a remote reference to a local user or group.
// @fixme ideally we should be able to say who it was so we can
// go back and refer to it the regular way
throw $e ;
2010-02-22 02:37:12 +00:00
} catch ( Exception $e ) {
common_log ( LOG_WARNING , " Failed creating profile from profile URL ' $profileUrl ': " . $e -> getMessage ());
// keep looking
2010-04-07 00:32:04 +01:00
//
// @fixme this means an error discovering from profile page
// may give us a corrupt entry using the webfinger URI, which
// will obscure the correct page-keyed profile later on.
2010-02-22 02:37:12 +00:00
}
}
// XXX: try hcard
// XXX: try FOAF
2010-03-16 16:25:18 +00:00
if ( array_key_exists ( 'salmon' , $hints )) {
$salmonEndpoint = $hints [ 'salmon' ];
2010-02-22 02:37:12 +00:00
// An account URL, a salmon endpoint, and a dream? Not much to go
// on, but let's give it a try
$uri = 'acct:' . $addr ;
$profile = new Profile ();
$profile -> nickname = self :: nicknameFromUri ( $uri );
$profile -> created = common_sql_now ();
2010-02-22 02:57:09 +00:00
if ( isset ( $profileUrl )) {
$profile -> profileurl = $profileUrl ;
}
2010-02-22 02:37:12 +00:00
$profile_id = $profile -> insert ();
if ( ! $profile_id ) {
common_log_db_error ( $profile , 'INSERT' , __FILE__ );
2010-09-19 14:17:36 +01:00
// TRANS: Exception. %s is a webfinger address.
throw new Exception ( sprintf ( _m ( 'Couldn\'t save profile for "%s".' ), $addr ));
2010-02-22 02:37:12 +00:00
}
$oprofile = new Ostatus_profile ();
$oprofile -> uri = $uri ;
$oprofile -> salmonuri = $salmonEndpoint ;
$oprofile -> profile_id = $profile_id ;
$oprofile -> created = common_sql_now ();
2010-02-22 02:57:09 +00:00
if ( isset ( $feedUrl )) {
$profile -> feeduri = $feedUrl ;
}
2010-02-22 02:37:12 +00:00
$result = $oprofile -> insert ();
if ( ! $result ) {
common_log_db_error ( $oprofile , 'INSERT' , __FILE__ );
2010-09-19 14:17:36 +01:00
// TRANS: Exception. %s is a webfinger address.
throw new Exception ( sprintf ( _m ( 'Couldn\'t save ostatus_profile for "%s".' ), $addr ));
2010-02-22 02:37:12 +00:00
}
2010-02-26 01:29:52 +00:00
self :: cacheSet ( sprintf ( 'ostatus_profile:webfinger:%s' , $addr ), $oprofile -> uri );
2010-02-22 02:37:12 +00:00
return $oprofile ;
}
2010-09-19 14:17:36 +01:00
// TRANS: Exception. %s is a webfinger address.
throw new Exception ( sprintf ( _m ( 'Couldn\'t find a valid profile for "%s".' ), $addr ));
2010-02-22 02:37:12 +00:00
}
2010-02-25 03:02:43 +00:00
2010-03-17 22:49:10 +00:00
/**
* Store the full - length scrubbed HTML of a remote notice to an attachment
* file on our server . We ' ll link to this at the end of the cropped version .
*
* @ param string $title plaintext for HTML page ' s title
* @ param string $rendered HTML fragment for HTML page ' s body
* @ return File
*/
2010-02-25 03:02:43 +00:00
function saveHTMLFile ( $title , $rendered )
{
2010-03-30 00:27:50 +01:00
$final = sprintf ( " <!DOCTYPE html> \n " .
'<html><head>' .
'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">' .
'<title>%s</title>' .
'</head>' .
2010-03-17 22:49:10 +00:00
'<body>%s</body></html>' ,
2010-02-25 03:02:43 +00:00
htmlspecialchars ( $title ),
$rendered );
$filename = File :: filename ( $this -> localProfile (),
'ostatus' , // ignored?
'text/html' );
$filepath = File :: path ( $filename );
file_put_contents ( $filepath , $final );
$file = new File ;
$file -> filename = $filename ;
$file -> url = File :: url ( $filename );
$file -> size = filesize ( $filepath );
$file -> date = time ();
$file -> mimetype = 'text/html' ;
$file_id = $file -> insert ();
if ( $file_id === false ) {
common_log_db_error ( $file , " INSERT " , __FILE__ );
2010-09-19 14:17:36 +01:00
throw new ServerException ( _m ( 'Could not store HTML content of long post as file.' ));
2010-02-25 03:02:43 +00:00
}
2010-02-25 03:26:40 +00:00
return $file ;
2010-02-25 03:02:43 +00:00
}
2010-09-01 21:17:18 +01:00
static function ensureProfileURI ( $uri )
{
$oprofile = null ;
2010-09-01 22:05:11 +01:00
// First, try to query it
$oprofile = Ostatus_profile :: staticGet ( 'uri' , $uri );
// If unfound, do discovery stuff
if ( empty ( $oprofile )) {
if ( preg_match ( " /^( \ w+) \ :(.*)/ " , $uri , $match )) {
$protocol = $match [ 1 ];
switch ( $protocol ) {
case 'http' :
case 'https' :
$oprofile = Ostatus_profile :: ensureProfileURL ( $uri );
break ;
case 'acct' :
case 'mailto' :
$rest = $match [ 2 ];
$oprofile = Ostatus_profile :: ensureWebfinger ( $rest );
default :
common_log ( " Unrecognized URI protocol for profile: $protocol ( $uri ) " );
break ;
}
2010-09-01 21:17:18 +01:00
}
}
2010-09-01 22:05:11 +01:00
return $oprofile ;
2010-09-01 21:17:18 +01:00
}
2009-11-20 17:42:19 +00:00
}
2010-04-07 00:32:04 +01:00
/**
* Exception indicating we ' ve got a remote reference to a local user ,
* not a remote user !
*
* If we can ue a local profile after all , it ' s available as $e -> profile .
*/
class OStatusShadowException extends Exception
{
public $profile ;
/**
* @ param Profile $profile
* @ param string $message
*/
function __construct ( $profile , $message ) {
$this -> profile = $profile ;
parent :: __construct ( $message );
}
}