]> git.mxchange.org Git - quix0rs-gnu-social.git/blobdiff - plugins/OStatus/classes/Ostatus_profile.php
Merge remote-tracking branch 'upstream/master' into social-master
[quix0rs-gnu-social.git] / plugins / OStatus / classes / Ostatus_profile.php
index 37d3dafa18bd4e63716c537c6c4091d3b1e88ce8..c63bf84dd155d5d1bc15eb901cc2b407bb8cbcab 100644 (file)
@@ -51,12 +51,12 @@ class Ostatus_profile extends Managed_DataObject
     {
         return array(
             'fields' => array(
-                'uri' => array('type' => 'varchar', 'length' => 255, 'not null' => true),
+                'uri' => array('type' => 'varchar', 'length' => 191, 'not null' => true),
                 'profile_id' => array('type' => 'integer'),
                 'group_id' => array('type' => 'integer'),
                 'peopletag_id' => array('type' => 'integer'),
-                'feeduri' => array('type' => 'varchar', 'length' => 255),
-                'salmonuri' => array('type' => 'varchar', 'length' => 255),
+                'feeduri' => array('type' => 'varchar', 'length' => 191),
+                'salmonuri' => array('type' => 'varchar', 'length' => 191),
                 'avatar' => array('type' => 'text'),
                 'created' => array('type' => 'datetime', 'not null' => true),
                 'modified' => array('type' => 'datetime', 'not null' => true),
@@ -113,10 +113,13 @@ class Ostatus_profile extends Managed_DataObject
      */
     public function localGroup()
     {
-        if ($this->group_id) {
-            return User_group::getKV('id', $this->group_id);
+        $group = User_group::getKV('id', $this->group_id);
+
+        if (!$group instanceof User_group) {
+            throw new NoSuchGroupException(array('id'=>$this->group_id));
         }
-        return null;
+
+        return $group;
     }
 
     /**
@@ -369,9 +372,10 @@ class Ostatus_profile extends Managed_DataObject
      * send immediately but won't get the return value.
      *
      * @param mixed $entry XML string, Notice, or Activity
+     * @param Profile $actor Acting profile
      * @return boolean success
      */
-    public function notifyDeferred($entry, $actor)
+    public function notifyDeferred($entry, Profile $actor)
     {
         if ($this->salmonuri) {
             $data = array('salmonuri' => $this->salmonuri,
@@ -477,14 +481,14 @@ class Ostatus_profile extends Managed_DataObject
      *
      * @return Notice Notice representing the new (or existing) activity
      */
-    public function processEntry($entry, $feed, $source)
+    public function processEntry(DOMElement $entry, DOMElement $feed, $source)
     {
         $activity = new Activity($entry, $feed);
         return $this->processActivity($activity, $source);
     }
 
     // TODO: Make this throw an exception
-    public function processActivity($activity, $source)
+    public function processActivity(Activity $activity, $source)
     {
         $notice = null;
 
@@ -510,9 +514,6 @@ class Ostatus_profile extends Managed_DataObject
                     throw new ClientException(_m('Cannot handle that kind of post.'));
                 }
                 break;
-            case ActivityVerb::SHARE:
-                $notice = $this->processShare($activity, $source);
-                break;
             default:
                 common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb");
             }
@@ -524,214 +525,6 @@ class Ostatus_profile extends Managed_DataObject
         return $notice;
     }
 
-    public function processShare($activity, $method)
-    {
-        $notice = null;
-
-        try {
-            $profile = ActivityUtils::checkAuthorship($activity, $this->localProfile());
-        } catch (ServerException $e) {
-            return null;
-        }
-
-        // The id URI will be used as a unique identifier 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.
-        $dupe = Notice::getKV('uri', $activity->id);
-        if ($dupe instanceof Notice) {
-            common_log(LOG_INFO, "OStatus: ignoring duplicate post: {$activity->id}");
-            return $dupe;
-        }
-
-        if (count($activity->objects) != 1) {
-            // TRANS: Client exception thrown when trying to share multiple activities at once.
-            throw new ClientException(_m('Can only handle share activities with exactly one object.'));
-        }
-
-        $shared = $activity->objects[0];
-
-        if (!$shared instanceof Activity) {
-            // TRANS: Client exception thrown when trying to share a non-activity object.
-            throw new ClientException(_m('Can only handle shared activities.'));
-        }
-
-        $sharedId = $shared->id;
-        if (!empty($shared->objects[0]->id)) {
-            // Because StatusNet since commit 8cc4660 sets $shared->id to a TagURI which
-            // fucks up federation, because the URI is no longer recognised by the origin.
-            // So we set it to the object ID if it exists, otherwise we trust $shared->id
-            $sharedId = $shared->objects[0]->id;
-        }
-        if (empty($sharedId)) {
-            throw new ClientException(_m('Shared activity does not have an id'));
-        }
-
-        // First check if we have the shared activity. This has to be done first, because
-        // we can't use these functions to "ensureActivityObjectProfile" of a local user,
-        // who might be the creator of the shared activity in question.
-        $sharedNotice = Notice::getKV('uri', $sharedId);
-        if (!$sharedNotice instanceof Notice) {
-            // If no locally stored notice is found, process it!
-            // TODO: Remember to check Deleted_notice!
-            // TODO: If a post is shared that we can't retrieve - what to do?
-            try {
-                $other = self::ensureActivityObjectProfile($shared->actor);
-                $sharedNotice = $other->processActivity($shared, $method);
-                if (!$sharedNotice instanceof Notice) {
-                    // And if we apparently can't get the shared notice, we'll abort the whole thing.
-                    // TRANS: Client exception thrown when saving an activity share fails.
-                    // TRANS: %s is a share ID.
-                    throw new ClientException(sprintf(_m('Failed to save activity %s.'), $sharedId));
-                }
-            } catch (FeedSubException $e) {
-                // Remote feed could not be found or verified, should we
-                // transform this into an "RT @user Blah, blah, blah..."?
-                common_log(LOG_INFO, __METHOD__ . ' got a ' . get_class($e) . ': ' . $e->getMessage());
-                return null;
-            }
-        }
-
-        // We'll want to save a web link to the original notice, if provided.
-
-        $sourceUrl = null;
-        if ($activity->link) {
-            $sourceUrl = $activity->link;
-        } else if ($activity->link) {
-            $sourceUrl = $activity->link;
-        } else if (preg_match('!^https?://!', $activity->id)) {
-            $sourceUrl = $activity->id;
-        }
-
-        // Use summary as fallback for content
-
-        if (!empty($activity->content)) {
-            $sourceContent = $activity->content;
-        } else if (!empty($activity->summary)) {
-            $sourceContent = $activity->summary;
-        } else if (!empty($activity->title)) {
-            $sourceContent = $activity->title;
-        } else {
-            // @todo FIXME: Fetch from $sourceUrl?
-            // TRANS: Client exception. %s is a source URI.
-            throw new ClientException(sprintf(_m('No content for notice %s.'), $activity->id));
-        }
-
-        // Get (safe!) HTML and text versions of the content
-
-        $rendered = $this->purify($sourceContent);
-        $content = common_strip_html($rendered);
-
-        $shortened = common_shorten_links($content);
-
-        // If it's too long, try using the summary, and make the
-        // HTML an attachment.
-
-        $attachment = null;
-
-        if (Notice::contentTooLong($shortened)) {
-            $attachment = $this->saveHTMLFile($activity->title, $rendered);
-            $summary = common_strip_html($activity->summary);
-            if (empty($summary)) {
-                $summary = $content;
-            }
-            $shortSummary = common_shorten_links($summary);
-            if (Notice::contentTooLong($shortSummary)) {
-                $url = common_shorten_url($sourceUrl);
-                $shortSummary = substr($shortSummary,
-                                       0,
-                                       Notice::maxContent() - (mb_strlen($url) + 2));
-                $content = $shortSummary . ' ' . $url;
-
-                // We mark up the attachment link specially for the HTML output
-                // so we can fold-out the full version inline.
-
-                // @todo 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
-                // TRANS: this will usually be replaced with localised text from StatusNet core messages.
-                $showMoreText = _m('Show more');
-                $attachUrl = common_local_url('attachment',
-                                              array('attachment' => $attachment->id));
-                $rendered = common_render_text($shortSummary) .
-                            '<a href="' . htmlspecialchars($attachUrl) .'"'.
-                            ' class="attachment more"' .
-                            ' title="'. htmlspecialchars($showMoreText) . '">' .
-                            '&#8230;' .
-                            '</a>';
-            }
-        }
-
-        $options = array('is_local' => Notice::REMOTE,
-                         'url' => $sourceUrl,
-                         'uri' => $activity->id,
-                         'rendered' => $rendered,
-                         'replies' => array(),
-                         'groups' => array(),
-                         'peopletags' => array(),
-                         'tags' => array(),
-                         'urls' => array(),
-                         'repeat_of' => $sharedNotice->id,
-                         'scope' => $sharedNotice->scope);
-
-        // Check for optional attributes...
-
-        if (!empty($activity->time)) {
-            $options['created'] = common_sql_date($activity->time);
-        }
-
-        if ($activity->context) {
-            // TODO: context->attention
-            list($options['groups'], $options['replies'])
-                = self::filterAttention($profile, $activity->context->attention);
-
-            // Maintain direct reply associations
-            // @todo FIXME: What about conversation ID?
-            if (!empty($activity->context->replyToID)) {
-                $orig = Notice::getKV('uri',
-                                          $activity->context->replyToID);
-                if ($orig instanceof Notice) {
-                    $options['reply_to'] = $orig->id;
-                }
-            }
-
-            $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;
-                }
-            }
-        }
-
-        if ($this->isPeopletag()) {
-            $options['peopletags'][] = $this->localPeopletag();
-        }
-
-        // Atom categories <-> hashtags
-        foreach ($activity->categories as $cat) {
-            if ($cat->term) {
-                $term = common_canonical_tag($cat->term);
-                if ($term) {
-                    $options['tags'][] = $term;
-                }
-            }
-        }
-
-        // Atom enclosures -> attachment URLs
-        foreach ($activity->enclosures as $href) {
-            // @todo FIXME: Save these locally or....?
-            $options['urls'][] = $href;
-        }
-
-        $notice = Notice::saveNew($profile->id,
-                                  $content,
-                                  'ostatus',
-                                  $options);
-
-        return $notice;
-    }
-
     /**
      * Process an incoming post activity from this remote feed.
      * @param Activity $activity
@@ -739,7 +532,7 @@ class Ostatus_profile extends Managed_DataObject
      * @return mixed saved Notice or false
      * @todo FIXME: Break up this function, it's getting nasty long
      */
-    public function processPost($activity, $method)
+    public function processPost(Activity $activity, $method)
     {
         $notice = null;
 
@@ -785,7 +578,7 @@ class Ostatus_profile extends Managed_DataObject
 
         // Get (safe!) HTML and text versions of the content
 
-        $rendered = $this->purify($sourceContent);
+        $rendered = common_purify($sourceContent);
         $content = common_strip_html($rendered);
 
         $shortened = common_shorten_links($content);
@@ -911,17 +704,6 @@ class Ostatus_profile extends Managed_DataObject
         return $saved;
     }
 
-    /**
-     * Clean up HTML
-     */
-    protected function purify($html)
-    {
-        require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
-        $config = array('safe' => 1,
-                        'deny_attribute' => 'id,style,on*');
-        return htmLawed($html, $config);
-    }
-
     /**
      * Filters a list of recipient ID URIs to just those for local delivery.
      * @param Profile local profile of sender
@@ -930,7 +712,7 @@ class Ostatus_profile extends Managed_DataObject
      */
     static public function filterAttention(Profile $sender, array $attention)
     {
-        common_log(LOG_DEBUG, "Original reply recipients: " . implode(', ', array_keys($attention)));
+        common_debug("Original reply recipients: " . implode(', ', array_keys($attention)));
         $groups = array();
         $replies = array();
         foreach ($attention as $recipient=>$type) {
@@ -952,11 +734,11 @@ class Ostatus_profile extends Managed_DataObject
                     if ($sender->isMember($group)) {
                         $groups[] = $group->id;
                     } else {
-                        common_log(LOG_DEBUG, sprintf('Skipping reply to local group %s as sender %d is not a member', $group->getNickname(), $sender->id));
+                        common_debug(sprintf('Skipping reply to local group %s as sender %d is not a member', $group->getNickname(), $sender->id));
                     }
                     continue;
                 } else {
-                    common_log(LOG_DEBUG, "Skipping reply to bogus group $recipient");
+                    common_debug("Skipping reply to bogus group $recipient");
                 }
             }
 
@@ -974,12 +756,12 @@ class Ostatus_profile extends Managed_DataObject
                 continue;
             } catch (Exception $e) {
                 // Neither a recognizable local nor remote user!
-                common_log(LOG_DEBUG, "Skipping reply to unrecognized profile $recipient: " . $e->getMessage());
+                common_debug("Skipping reply to unrecognized profile $recipient: " . $e->getMessage());
             }
 
         }
-        common_log(LOG_DEBUG, "Local reply recipients: " . implode(', ', $replies));
-        common_log(LOG_DEBUG, "Local group recipients: " . implode(', ', $groups));
+        common_debug("Local reply recipients: " . implode(', ', $replies));
+        common_debug("Local group recipients: " . implode(', ', $groups));
         return array($groups, $replies);
     }
 
@@ -1128,6 +910,11 @@ class Ostatus_profile extends Managed_DataObject
      */
     public static function ensureFeedURL($feed_url, array $hints=array())
     {
+        $oprofile = Ostatus_profile::getKV('feeduri', $feed_url);
+        if ($oprofile instanceof Ostatus_profile) {
+            return $oprofile;
+        }
+
         $discover = new FeedDiscovery();
 
         $feeduri = $discover->discoverFromFeedURL($feed_url);
@@ -1231,54 +1018,47 @@ class Ostatus_profile extends Managed_DataObject
      * Download and update given avatar image
      *
      * @param string $url
+     * @return Avatar    The Avatar we have on disk. (seldom used)
      * @throws Exception in various failure cases
      */
-    public function updateAvatar($url)
+    public function updateAvatar($url, $force=false)
     {
-        if ($url == $this->avatar) {
-            // We've already got this one.
-            return;
+        try {
+            // If avatar URL differs: update. If URLs were identical but we're forced: update.
+            if ($url == $this->avatar && !$force) {
+                // If there's no locally stored avatar, throw an exception and continue fetching below.
+                $avatar = Avatar::getUploaded($this->localProfile()) instanceof Avatar;
+                return $avatar;
+            }
+        } catch (NoAvatarException $e) {
+            // No avatar available, let's fetch it.
         }
+
         if (!common_valid_http_url($url)) {
             // TRANS: Server exception. %s is a URL.
             throw new ServerException(sprintf(_m('Invalid avatar URL %s.'), $url));
         }
 
-        if ($this->isGroup()) {
-            // FIXME: throw exception for localGroup
-            $self = $this->localGroup();
-        } else {
-            // this throws an exception already
-            $self = $this->localProfile();
-        }
-        if (!$self) {
-            throw new ServerException(sprintf(
-                // TRANS: Server exception. %s is a URI.
-                _m('Tried to update avatar for unsaved remote profile %s.'),
-                $this->getUri()));
-        }
+        $self = $this->localProfile();
 
         // @todo FIXME: This should be better encapsulated
         // ripped from oauthstore.php (for old OMB client)
-        $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
+        $temp_filename = tempnam(common_get_temp_dir(), 'listener_avatar');
         try {
-            $client = new HTTPClient();
-            $response = $client->get($url);
-
-            if (!$response->isOk()) {
-                // TRANS: Server exception. %s is a URL.
-                throw new ServerException(sprintf(_m('Unable to fetch avatar from %s.'), $url));
+            $imgData = HTTPClient::quickGet($url);
+            // Make sure it's at least an image file. ImageFile can do the rest.
+            if (false === getimagesizefromstring($imgData)) {
+                throw new UnsupportedMediaException(_('Downloaded group avatar was not an image.'));
             }
-            // FIXME: make sure it's an image here instead of _after_ writing to a file?
-            file_put_contents($response->getBody(), $temp_filename);
+            file_put_contents($temp_filename, $imgData);
+            unset($imgData);    // No need to carry this in memory.
 
             if ($this->isGroup()) {
                 $id = $this->group_id;
             } else {
                 $id = $this->profile_id;
             }
-            // @todo FIXME: Should we be using different ids?
-            $imagefile = new ImageFile($id, $temp_filename);
+            $imagefile = new ImageFile(null, $temp_filename);
             $filename = Avatar::filename($id,
                                          image_type_to_extension($imagefile->type),
                                          null,
@@ -1301,6 +1081,8 @@ class Ostatus_profile extends Managed_DataObject
         $orig = clone($this);
         $this->avatar = $url;
         $this->update($orig);
+
+        return Avatar::getUploaded($self);
     }
 
     /**
@@ -1486,7 +1268,7 @@ class Ostatus_profile extends Managed_DataObject
         $discover = false;
 
         if (!$homeuri) {
-            common_log(LOG_DEBUG, __METHOD__ . " empty actor profile URI: " . var_export($activity, true));
+            common_debug(__METHOD__ . " empty actor profile URI: " . var_export($activity, true));
             // TRANS: Exception.
             throw new Exception(_m('No profile URI.'));
         }
@@ -1728,7 +1510,7 @@ class Ostatus_profile extends Managed_DataObject
         // @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));
+            common_debug("Updating OStatus profile $profile->id from remote info $object->id: " . var_export($object, true) . var_export($hints, true));
             $profile->update($orig);
         }
     }
@@ -1752,12 +1534,12 @@ class Ostatus_profile extends Managed_DataObject
         $group->homepage = self::getActivityObjectHomepage($object, $hints);
 
         if ($group->id) {   // If no id, we haven't called insert() yet, so don't run update()
-            common_log(LOG_DEBUG, "Updating OStatus group $group->id from remote info $object->id: " . var_export($object, true) . var_export($hints, true));
+            common_debug("Updating OStatus group $group->id from remote info $object->id: " . var_export($object, true) . var_export($hints, true));
             $group->update($orig);
         }
     }
 
-    protected static function updatePeopletag($tag, ActivityObject $object, array $hints=array()) {
+    protected static function updatePeopletag(Peopletag $tag, ActivityObject $object, array $hints=array()) {
         $orig = clone($tag);
 
         $tag->tag = $object->title;
@@ -1773,7 +1555,7 @@ class Ostatus_profile extends Managed_DataObject
         $tag->tagger = $tagger->profile_id;
 
         if ($tag->id) {
-            common_log(LOG_DEBUG, "Updating OStatus peopletag $tag->id from remote info $object->id: " . var_export($object, true) . var_export($hints, true));
+            common_debug("Updating OStatus peopletag $tag->id from remote info $object->id: " . var_export($object, true) . var_export($hints, true));
             $tag->update($orig);
         }
     }
@@ -1807,8 +1589,8 @@ class Ostatus_profile extends Managed_DataObject
         }
 
         if (!empty($location)) {
-            if (mb_strlen($location) > 255) {
-                $location = mb_substr($note, 0, 255 - 3) . ' … ';
+            if (mb_strlen($location) > 191) {   // not 255 because utf8mb4 takes more space
+                $location = mb_substr($note, 0, 191 - 3) . ' … ';
             }
         }
 
@@ -2099,13 +1881,15 @@ class Ostatus_profile extends Managed_DataObject
                                    'text/html');
 
         $filepath = File::path($filename);
+        $fileurl = File::url($filename);
 
         file_put_contents($filepath, $final);
 
         $file = new File;
 
         $file->filename = $filename;
-        $file->url      = File::url($filename);
+        $file->urlhash  = File::hashurl($fileurl);
+        $file->url      = $fileurl;
         $file->size     = filesize($filepath);
         $file->date     = time();
         $file->mimetype = 'text/html';
@@ -2152,7 +1936,6 @@ class Ostatus_profile extends Managed_DataObject
                 throw new ServerException(sprintf(_m('Unrecognized URI protocol for profile: %1$s (%2$s).'),
                                                   $protocol,
                                                   $uri));
-                break;
             }
         } else {
             // TRANS: Server exception. %s is a URI.
@@ -2177,7 +1960,7 @@ class Ostatus_profile extends Managed_DataObject
         } else {
             $actor = $activity->actor;
 
-            if (empty($actor)) {
+            if (!$actor instanceof Profile) {
                 // OK here! assume the default
             } else if ($actor->id == $this->getUri() || $actor->link == $this->getUri()) {
                 $this->updateFromActivityObject($actor);