diff --git a/src/Module/BaseAdmin.php b/src/Module/BaseAdmin.php index a7b38a503..01215dc8e 100644 --- a/src/Module/BaseAdmin.php +++ b/src/Module/BaseAdmin.php @@ -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 index 000000000..87a531d5b --- /dev/null +++ b/src/Module/Debug/ActivityPubConversion.php @@ -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; + } +} diff --git a/static/routes.config.php b/static/routes.config.php index ac7882693..8c3fba99b 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -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 index 000000000..dfc6d7367 --- /dev/null +++ b/view/templates/debug/activitypubconversion.tpl @@ -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