]> git.mxchange.org Git - friendica.git/commitdiff
Merge pull request #8904 from MrPetovan/task/ap-conversion-admin-module
authorMichael Vogel <icarus@dabo.de>
Tue, 21 Jul 2020 19:32:06 +0000 (21:32 +0200)
committerGitHub <noreply@github.com>
Tue, 21 Jul 2020 19:32:06 +0000 (21:32 +0200)
Add new admin debug module for ActivityPub

src/Module/BaseAdmin.php
src/Module/Debug/ActivityPubConversion.php [new file with mode: 0644]
src/Protocol/ActivityPub/Processor.php
src/Protocol/ActivityPub/Receiver.php
static/routes.config.php
view/templates/debug/activitypubconversion.tpl [new file with mode: 0644]

index a7b38a50330bac94c2505927ff4f167b68d5eb76..01215dc8e868770c763bc5632ebe88c7f4321dc2 100644 (file)
@@ -121,6 +121,7 @@ abstract class BaseAdmin extends BaseModule
                                'webfinger'    => ['webfinger'         , DI::l10n()->t('check webfinger')         , 'webfinger'],
                                'itemsource'   => ['admin/item/source' , DI::l10n()->t('Item Source')             , 'itemsource'],
                                'babel'        => ['babel'             , DI::l10n()->t('Babel')                   , 'babel'],
+                               'debug/ap'     => ['debug/ap'          , DI::l10n()->t('ActivityPub Conversion')  , 'debug/ap'],
                        ]],
                ];
 
diff --git a/src/Module/Debug/ActivityPubConversion.php b/src/Module/Debug/ActivityPubConversion.php
new file mode 100644 (file)
index 0000000..87a531d
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+/**
+ * @copyright Copyright (C) 2020, Friendica
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Module\Debug;
+
+use Friendica\BaseModule;
+use Friendica\Content\Text;
+use Friendica\Core\Logger;
+use Friendica\Core\Renderer;
+use Friendica\DI;
+use Friendica\Model\Item;
+use Friendica\Model\Tag;
+use Friendica\Protocol\ActivityPub;
+use Friendica\Util\JsonLD;
+use Friendica\Util\XML;
+
+class ActivityPubConversion extends BaseModule
+{
+       public static function content(array $parameters = [])
+       {
+               function visible_whitespace($s)
+               {
+                       return '<pre>' . htmlspecialchars($s) . '</pre>';
+               }
+
+               $results = [];
+               if (!empty($_REQUEST['source'])) {
+                       try {
+                               $source = json_decode($_REQUEST['source'], true);
+                               $trust_source = true;
+                               $uid = local_user();
+                               $push = false;
+
+                               if (!$source) {
+                                       throw new \Exception('Failed to decode source JSON');
+                               }
+
+                               $formatted = json_encode($source, JSON_PRETTY_PRINT);
+                               $results[] = [
+                                       'title'   => DI::l10n()->t('Formatted'),
+                                       'content' => visible_whitespace(trim(var_export($formatted, true), "'")),
+                               ];
+                               $results[] = [
+                                       'title'   => DI::l10n()->t('Source'),
+                                       'content' => visible_whitespace(var_export($source, true))
+                               ];
+                               $activity = JsonLD::compact($source);
+                               if (!$activity) {
+                                       throw new \Exception('Failed to compact JSON');
+                               }
+                               $results[] = [
+                                       'title'   => DI::l10n()->t('Activity'),
+                                       'content' => visible_whitespace(var_export($activity, true))
+                               ];
+
+                               $type = JsonLD::fetchElement($activity, '@type');
+
+                               if (!$type) {
+                                       throw new \Exception('Empty type');
+                               }
+
+                               if (!JsonLD::fetchElement($activity, 'as:object', '@id')) {
+                                       throw new \Exception('Empty object');
+                               }
+
+                               if (!JsonLD::fetchElement($activity, 'as:actor', '@id')) {
+                                       throw new \Exception('Empty actor');
+                               }
+
+                               // Don't trust the source if "actor" differs from "attributedTo". The content could be forged.
+                               if ($trust_source && ($type == 'as:Create') && is_array($activity['as:object'])) {
+                                       $actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
+                                       $attributed_to = JsonLD::fetchElement($activity['as:object'], 'as:attributedTo', '@id');
+                                       $trust_source = ($actor == $attributed_to);
+                                       if (!$trust_source) {
+                                               throw new \Exception('Not trusting actor: ' . $actor . '. It differs from attributedTo: ' . $attributed_to);
+                                       }
+                               }
+
+                               // $trust_source is called by reference and is set to true if the content was retrieved successfully
+                               $object_data = ActivityPub\Receiver::prepareObjectData($activity, $uid, $push, $trust_source);
+                               if (empty($object_data)) {
+                                       throw new \Exception('No object data found');
+                               }
+
+                               if (!$trust_source) {
+                                       throw new \Exception('No trust for activity type "' . $type . '", so we quit now.');
+                               }
+
+                               if (!empty($body) && empty($object_data['raw'])) {
+                                       $object_data['raw'] = $body;
+                               }
+
+                               // Internal flag for thread completion. See Processor.php
+                               if (!empty($activity['thread-completion'])) {
+                                       $object_data['thread-completion'] = $activity['thread-completion'];
+                               }
+
+                               $results[] = [
+                                       'title'   => DI::l10n()->t('Object data'),
+                                       'content' => visible_whitespace(var_export($object_data, true))
+                               ];
+
+                               $item = ActivityPub\Processor::createItem($object_data);
+
+                               $results[] = [
+                                       'title'   => DI::l10n()->t('Result Item'),
+                                       'content' => visible_whitespace(var_export($item, true))
+                               ];
+                       } catch (\Throwable $e) {
+                               $results[] = [
+                                       'title'   => DI::l10n()->t('Error'),
+                                       'content' => $e->getMessage(),
+                               ];
+                       }
+               }
+
+               $tpl = Renderer::getMarkupTemplate('debug/activitypubconversion.tpl');
+               $o = Renderer::replaceMacros($tpl, [
+                       '$source'          => ['source', DI::l10n()->t('Source activity'), $_REQUEST['source'] ?? '', ''],
+                       '$results'       => $results
+               ]);
+
+               return $o;
+       }
+}
index 177813e32d55067988372da2a890db1feabbd2df..241907f62359885289ce88ef4c8238f906c6d22a 100644 (file)
@@ -170,7 +170,8 @@ class Processor
                $item = Item::selectFirst(['uri', 'uri-id', 'thr-parent', 'gravity'], ['uri' => $activity['id']]);
                if (!DBA::isResult($item)) {
                        Logger::warning('No existing item, item will be created', ['uri' => $activity['id']]);
-                       self::createItem($activity);
+                       $item = self::createItem($activity);
+                       self::postItem($activity, $item);
                        return;
                }
 
@@ -189,6 +190,7 @@ class Processor
         * Prepares data for a message
         *
         * @param array $activity Activity array
+        * @return array Internal item
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
@@ -216,7 +218,71 @@ class Processor
 
                $item['diaspora_signed_text'] = $activity['diaspora:comment'] ?? '';
 
-               self::postItem($activity, $item);
+               /// @todo What to do with $activity['context']?
+               if (empty($activity['directmessage']) && ($item['gravity'] != GRAVITY_PARENT) && !Item::exists(['uri' => $item['thr-parent']])) {
+                       Logger::info('Parent not found, message will be discarded.', ['thr-parent' => $item['thr-parent']]);
+                       return [];
+               }
+
+               $item['network'] = Protocol::ACTIVITYPUB;
+               $item['author-link'] = $activity['author'];
+               $item['author-id'] = Contact::getIdForURL($activity['author'], 0, false);
+               $item['owner-link'] = $activity['actor'];
+               $item['owner-id'] = Contact::getIdForURL($activity['actor'], 0, false);
+
+               if (in_array(0, $activity['receiver']) && !empty($activity['unlisted'])) {
+                       $item['private'] = Item::UNLISTED;
+               } elseif (in_array(0, $activity['receiver'])) {
+                       $item['private'] = Item::PUBLIC;
+               } else {
+                       $item['private'] = Item::PRIVATE;
+               }
+
+               if (!empty($activity['raw'])) {
+                       $item['source'] = $activity['raw'];
+                       $item['protocol'] = Conversation::PARCEL_ACTIVITYPUB;
+                       $item['conversation-href'] = $activity['context'] ?? '';
+                       $item['conversation-uri'] = $activity['conversation'] ?? '';
+
+                       if (isset($activity['push'])) {
+                               $item['direction'] = $activity['push'] ? Conversation::PUSH : Conversation::PULL;
+                       }
+               }
+
+               $item['isForum'] = false;
+
+               if (!empty($activity['thread-completion'])) {
+                       // Store the original actor in the "causer" fields to enable the check for ignored or blocked contacts
+                       $item['causer-link'] = $item['owner-link'];
+                       $item['causer-id'] = $item['owner-id'];
+
+                       Logger::info('Ignoring actor because of thread completion.', ['actor' => $item['owner-link']]);
+                       $item['owner-link'] = $item['author-link'];
+                       $item['owner-id'] = $item['author-id'];
+               } else {
+                       $actor = APContact::getByURL($item['owner-link'], false);
+                       $item['isForum'] = ($actor['type'] == 'Group');
+               }
+
+               $item['uri'] = $activity['id'];
+
+               $item['created'] = DateTimeFormat::utc($activity['published']);
+               $item['edited'] = DateTimeFormat::utc($activity['updated']);
+               $guid = $activity['sc:identifier'] ?: self::getGUIDByURL($item['uri']);
+               $item['guid'] = $activity['diaspora:guid'] ?: $guid;
+
+               $item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]);
+
+               $item = self::processContent($activity, $item);
+               if (empty($item)) {
+                       return [];
+               }
+
+               $item['plink'] = $activity['alternate-url'] ?? $item['uri'];
+
+               $item = self::constructAttachList($activity, $item);
+
+               return $item;
        }
 
        /**
@@ -303,7 +369,7 @@ class Processor
         */
        public static function createActivity($activity, $verb)
        {
-               $item = [];
+               $item = self::createItem($activity);
                $item['verb'] = $verb;
                $item['thr-parent'] = $activity['object_id'];
                $item['gravity'] = GRAVITY_ACTIVITY;
@@ -446,72 +512,8 @@ class Processor
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       private static function postItem($activity, $item)
+       public static function postItem(array $activity, array $item)
        {
-               /// @todo What to do with $activity['context']?
-               if (empty($activity['directmessage']) && ($item['gravity'] != GRAVITY_PARENT) && !Item::exists(['uri' => $item['thr-parent']])) {
-                       Logger::info('Parent not found, message will be discarded.', ['thr-parent' => $item['thr-parent']]);
-                       return;
-               }
-
-               $item['network'] = Protocol::ACTIVITYPUB;
-               $item['author-link'] = $activity['author'];
-               $item['author-id'] = Contact::getIdForURL($activity['author'], 0, false);
-               $item['owner-link'] = $activity['actor'];
-               $item['owner-id'] = Contact::getIdForURL($activity['actor'], 0, false);
-
-               if (in_array(0, $activity['receiver']) && !empty($activity['unlisted'])) {
-                       $item['private'] = Item::UNLISTED;
-               } elseif (in_array(0, $activity['receiver'])) {
-                       $item['private'] = Item::PUBLIC;
-               } else {
-                       $item['private'] = Item::PRIVATE;
-               }
-
-               if (!empty($activity['raw'])) {
-                       $item['source'] = $activity['raw'];
-                       $item['protocol'] = Conversation::PARCEL_ACTIVITYPUB;
-                       $item['conversation-href'] = $activity['context'] ?? '';
-                       $item['conversation-uri'] = $activity['conversation'] ?? '';
-
-                       if (isset($activity['push'])) {
-                               $item['direction'] = $activity['push'] ? Conversation::PUSH : Conversation::PULL;
-                       }
-               }
-
-               $isForum = false;
-
-               if (!empty($activity['thread-completion'])) {
-                       // Store the original actor in the "causer" fields to enable the check for ignored or blocked contacts
-                       $item['causer-link'] = $item['owner-link'];
-                       $item['causer-id'] = $item['owner-id'];
-
-                       Logger::info('Ignoring actor because of thread completion.', ['actor' => $item['owner-link']]);
-                       $item['owner-link'] = $item['author-link'];
-                       $item['owner-id'] = $item['author-id'];
-               } else {
-                       $actor = APContact::getByURL($item['owner-link'], false);
-                       $isForum = ($actor['type'] == 'Group');
-               }
-
-               $item['uri'] = $activity['id'];
-
-               $item['created'] = DateTimeFormat::utc($activity['published']);
-               $item['edited'] = DateTimeFormat::utc($activity['updated']);
-               $guid = $activity['sc:identifier'] ?: self::getGUIDByURL($item['uri']);
-               $item['guid'] = $activity['diaspora:guid'] ?: $guid;
-
-               $item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]);
-
-               $item = self::processContent($activity, $item);
-               if (empty($item)) {
-                       return;
-               }
-
-               $item['plink'] = $activity['alternate-url'] ?? $item['uri'];
-
-               $item = self::constructAttachList($activity, $item);
-
                $stored = false;
 
                foreach ($activity['receiver'] as $receiver) {
@@ -521,7 +523,7 @@ class Processor
 
                        $item['uid'] = $receiver;
 
-                       if ($isForum) {
+                       if ($item['isForum'] ?? false) {
                                $item['contact-id'] = Contact::getIdForURL($activity['actor'], $receiver, false);
                        } else {
                                $item['contact-id'] = Contact::getIdForURL($activity['author'], $receiver, false);
@@ -539,7 +541,7 @@ class Processor
                        if (DI::pConfig()->get($receiver, 'system', 'accept_only_sharer', false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT)) {
                                $skip = !Contact::isSharingByURL($activity['author'], $receiver);
 
-                               if ($skip && (($activity['type'] == 'as:Announce') || $isForum)) {
+                               if ($skip && (($activity['type'] == 'as:Announce') || ($item['isForum'] ?? false))) {
                                        $skip = !Contact::isSharingByURL($activity['actor'], $receiver);
                                }
 
index aa59b9eb9e722e0455adbe9017f026f2ecf1e8f9..226ad601044c68f112850985e8013a0a724886ed 100644 (file)
@@ -184,7 +184,7 @@ class Receiver
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       private static function prepareObjectData($activity, $uid, $push, &$trust_source)
+       public static function prepareObjectData($activity, $uid, $push, &$trust_source)
        {
                $actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
                if (empty($actor)) {
@@ -227,6 +227,7 @@ class Receiver
                        if ($type == 'as:Announce') {
                                $trust_source = false;
                        }
+
                        $object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source, $uid);
                        if (empty($object_data)) {
                                Logger::log("Object data couldn't be processed", Logger::DEBUG);
@@ -337,7 +338,6 @@ class Receiver
                if (!JsonLD::fetchElement($activity, 'as:actor', '@id')) {
                        Logger::log('Empty actor', Logger::DEBUG);
                        return;
-
                }
 
                // Don't trust the source if "actor" differs from "attributedTo". The content could be forged.
@@ -374,7 +374,8 @@ class Receiver
                switch ($type) {
                        case 'as:Create':
                                if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
-                                       ActivityPub\Processor::createItem($object_data);
+                                       $item = ActivityPub\Processor::createItem($object_data);
+                                       ActivityPub\Processor::postItem($object_data, $item);
                                }
                                break;
 
@@ -391,7 +392,8 @@ class Receiver
                                        // If this isn't set, then a single reshare appears on top. This is used for groups.
                                        $object_data['thread-completion'] = ($profile['type'] != 'Group');
 
-                                       ActivityPub\Processor::createItem($object_data);
+                                       $item = ActivityPub\Processor::createItem($object_data);
+                                       ActivityPub\Processor::postItem($object_data, $item);
 
                                        // Add the bottom reshare information only for persons
                                        if ($profile['type'] != 'Group') {
index ac78826939cf8ce72c5eeb1b523967947b2b37af..8c3fba99b7623ac2d416f7af648bf765949c447d 100644 (file)
@@ -107,6 +107,7 @@ return [
        '/apps'                => [Module\Apps::class,         [R::GET]],
        '/attach/{item:\d+}'   => [Module\Attach::class,       [R::GET]],
        '/babel'               => [Module\Debug\Babel::class,  [R::GET, R::POST]],
+       '/debug/ap'            => [Module\Debug\ActivityPubConversion::class,  [R::GET, R::POST]],
        '/bookmarklet'         => [Module\Bookmarklet::class,  [R::GET]],
 
        '/community[/{content}[/{accounttype}]]' => [Module\Conversation\Community::class, [R::GET]],
diff --git a/view/templates/debug/activitypubconversion.tpl b/view/templates/debug/activitypubconversion.tpl
new file mode 100644 (file)
index 0000000..dfc6d73
--- /dev/null
@@ -0,0 +1,24 @@
+<h2>ActivityPub Conversion</h2>
+<form action="debug/ap" method="post" class="panel panel-default">
+       <div class="panel-body">
+               <div class="form-group">
+                       {{include file="field_textarea.tpl" field=$source}}
+               </div>
+               <p><button type="submit" class="btn btn-primary">Submit</button></p>
+       </div>
+</form>
+
+{{if $results}}
+<div class="babel-results">
+       {{foreach $results as $result}}
+       <div class="panel panel-default">
+               <div class="panel-heading">
+                       <h3 class="panel-title">{{$result.title}}</h3>
+               </div>
+               <div class="panel-body">
+                       {{$result.content nofilter}}
+               </div>
+       </div>
+       {{/foreach}}
+</div>
+{{/if}}
\ No newline at end of file