. /** * TheFreeNetwork, "automagic" migration of internal remote profiles * between different federation protocols. * * @package GNUsocial * @author Bruno Casteleiro * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later */ defined('GNUSOCIAL') || die(); /** * Class TheFreeNetworkModule * This module ensures that multiple protocols serving the same purpose won't result in duplicated data. * This class is not to be extended but a developer implementing a new protocol should be aware of it and notify the * StartTFNCensus event. * * @category Module * @package GNUsocial * @author Bruno Casteleiro * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later */ class TheFreeNetworkModule extends Module { const MODULE_VERSION = '0.1.0alpha0'; public $protocols = null; // protocols TFN should handle public $priority = []; // protocols preferences private $lrdd = false; // whether LRDD plugin is active or not /** * Initialize TFN * * @return bool hook value */ public function onInitializePlugin(): bool { // some protocol plugins can be unactivated, // require needed classes $plugin_dir = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins'; $p = 0; foreach ($this->protocols as $protocol => $class) { $this->priority[$class] = $p++; require_once $plugin_dir . DIRECTORY_SEPARATOR . $protocol . DIRECTORY_SEPARATOR . 'classes' . DIRECTORY_SEPARATOR . $class . '.php'; } unset($p); // $lrdd flag $this->lrdd = PluginList::isPluginActive("LRDD"); return true; } /** * A new remote profile is being added, check if we * already have someone with the same URI. * * @param string $uri * @param string $class profile class that triggered this event * @param int|null &$profile_id Profile:id associated with the remote entity found * @return bool hook flag false * @throws AlreadyHandledException Do not allow to create a profile if a preferred protocol already has one */ public function onStartTFNLookup(string $uri, string $class, int &$profile_id = null): bool { [$profile_id, $cls] = $this->lookup($uri, $class) ?? [null, null]; if (is_null($profile_id)) { $perf = common_config('performance', 'high'); if (!$perf && $this->lrdd) { // Force lookup with online resources [$profile_id, $cls] = $this->lookup($uri, $class, true); } } // $cls being lower then $class means it has a higher priority (comes first) @see $this->onInitializePlugin() if (!is_null($profile_id) && $this->priority[$cls] < $this->priority[$class]) { throw new AlreadyHandledException("TheFreeNetworkModule->AlreadyHandled: $cls is preferred over $class"); } return false; } /** * A new remote profile was successfully added, delete * other remotes associated with the same Profile entity. * * @param string $class profile class that triggered this event * @param int $profile_id Profile:id associated with the new remote profile * @return bool hook flag */ public function onEndTFNLookup(string $class, int $profile_id): bool { foreach ($this->protocols as $p => $cls) { if ($cls != $class) { $profile = $cls::getKV('profile_id', $profile_id); if ($profile instanceof $cls) { $this->log(LOG_INFO, 'Deleting remote ' . $cls . ' associated with Profile:' . $profile_id); $i = new $cls(); $i->profile_id = $profile_id; $i->delete(); break; } } } return false; } /** * Plugin version information * * @param array $versions * @return bool hook true */ public function onPluginVersion(array &$versions): bool { $versions[] = [ 'name' => 'TheFreeNetwork', 'version' => self::MODULE_VERSION, 'author' => 'Bruno Casteleiro', 'homepage' => 'https://notabug.org/diogo/gnu-social/src/nightly/plugins/TheFreeNetwork', // TRANS: Module description. 'rawdescription' => '"Automagically" migrate internal remote profiles between different federation protocols' ]; return true; } /** * Search remote profile tables to find someone by URI. * When set to search online, it will grab the remote * entity's aliases and search with each one. * The table associated with the class that triggered * this lookup process will be discarded in the search. * * @param string $uri * @param string $class * @param bool $online * @return null|array [Profile:id, class] associated with the remote entity found */ private function lookup(string $uri, string $class, bool $online = false): ?array { if ($online) { $this->log(LOG_INFO, 'Searching with online resources for a remote profile with URI: ' . $uri); $all_ids = LRDDPlugin::grab_profile_aliases($uri); } else { $this->log(LOG_INFO, 'Searching for a remote profile with URI: ' . $uri); $all_ids = [$uri]; } if ($all_ids == null) { $this->log(LOG_INFO, 'Unable to find a remote profile with URI: ' . $uri); return null; } foreach ($this->protocols as $p => $cls) { if ($cls != $class) { foreach ($all_ids as $alias) { $profile = $cls::getKV('uri', $alias); if ($profile instanceof $cls) { $this->log(LOG_INFO, 'Found a remote ' . $cls . ' associated with Profile:' . $profile->getID()); return [$profile->getID(), $cls]; } } } } $this->log(LOG_INFO, 'Unable to find a remote profile with URI: ' . $uri); return null; } }