From 69add504e66d4dd28da26d17b781829f2d175c5a Mon Sep 17 00:00:00 2001 From: tenma Date: Thu, 24 Oct 2019 02:08:54 +0100 Subject: [PATCH] [OStatus] Add script for profile deduplication and URI fixing --- .gitignore | 1 + plugins/OStatus/OStatusPlugin.php | 32 ++++ plugins/OStatus/classes/Ostatus_profile.php | 11 ++ plugins/OStatus/scripts/updateuris.php | 200 ++++++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100755 plugins/OStatus/scripts/updateuris.php diff --git a/.gitignore b/.gitignore index ff15346bcd..77f715fc62 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ config-*.php good-config.php *.mo /vendor/ +plugins/OStatus/.ostatus_version.txt \ No newline at end of file diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 40154669d6..941a67af52 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -1719,4 +1719,36 @@ class OStatusPlugin extends Plugin return is_null($profile); } + + /** + * To be consistent with the ActivityPub protocol, we need to make sure that + * all ostatus profiles are unique and use non-fancy URIs. This method executes + * an external script to ensure that conditions. + * + * @return bool hook flag + * @author Bruno Casteleiro + */ + public function onEndUpgrade(): bool + { + $flag = __DIR__ . DIRECTORY_SEPARATOR . '.ostatus_version.txt'; + $script = __DIR__ . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'updateuris.php'; + + if (!file_exists($flag)) { + // If our flag-file isn't set then we know OStatus hasn't upgraded yet, + // run external script and plant the flag + define('OSTATUS_UPGRADE', true); + require_once $script; + + $file = fopen($flag, "w"); + + if ($file === false) { + $this->log(LOG_ERR, "Failed to create the file: {$flag}\n"); + } else { + fwrite($file, self::PLUGIN_VERSION . PHP_EOL); + fclose($file); + } + } + + return true; + } } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 0dcfb3d95b..40edf55ba8 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -80,6 +80,17 @@ class Ostatus_profile extends Managed_DataObject return $this->uri; } + /** + * Getter for profile_id property + * + * @return int + * @author Bruno Casteleiro + */ + public function getID() + { + return $this->profile_id; + } + public function getFeedSub() { return FeedSub::getByUri($this->feeduri); diff --git a/plugins/OStatus/scripts/updateuris.php b/plugins/OStatus/scripts/updateuris.php new file mode 100755 index 0000000000..b28c502a31 --- /dev/null +++ b/plugins/OStatus/scripts/updateuris.php @@ -0,0 +1,200 @@ +#!/usr/bin/env php +. + +/** + * Update OStatus profile URIs to the non-fancy version + * (to be consistent with ActivityPub). Duplicated profiles + * found in the process are deleted. + * + * @package OStatus + * @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 + */ + +define('INSTALLDIR', dirname(__FILE__, 4)); +define('PUBLICDIR', INSTALLDIR . DIRECTORY_SEPARATOR . 'public'); + +$shortoptions = ''; +$longoptions = []; + +$helptext = <<selectAdd(); + $os->selectAdd('profile_id'); + $os->selectAdd('uri'); + $os->whereAdd('profile_id IS NOT NULL');// we want users, don't need group profiles + + if (!$os->find()) { + return; + } + + $seen = []; // profiles cache, for duplicates we keep the latest found + + while ($os->fetch()) { + $id = $os->profile_id; + $uri = $os->uri; + + printfv("Inspecting oprofile:{$id}..."); + + // known URI? + if (isset($seen[$uri])) { + // yes, keep current profile only + $delete[] = $seen[$uri]; + $seen[$uri] = $id; + printfv("DONE.\n"); + continue; + } + + $uri_parts = parse_url($uri); + $scheme = $uri_parts["scheme"]; + $domain = $uri_parts["host"]; + $path = $uri_parts["path"]; + + // find the type of domain we're dealing with + try { + HTTPClient::quickGet("{$scheme}://{$domain}/api/gnusocial/version.json"); + } catch (Exception $e) { + // not a GS domain, just skip it + $seen[$uri] = $id; + printfv("DONE.\n"); + continue; + } + + // GS domain, check URI + if (startsWith($path, '/index.php/user')) { + // non-fancy uri already, good to go + $seen[$uri] = $id; + } else { + // get profile's real id, build non-fancy uri + try { + $nick = explode('/', $path)[2]; + $resp = json_decode( + HTTPClient::quickGet("{$scheme}://{$domain}/api/users/show/{$nick}.json"), + true + ); + + $real_id = $resp['id']; + + } catch (Exception $e) { + if ($e->getCode() === 404) { + // User not found error, delete! + $delete[] = $id; + } else { + // unexpected, lets maintain the profile just in case.. + echo "\nError retrieving the real ID for oprofile:{$id}:" . $e->getMessage() . "\n"; + $seen[$uri] = $id; + } + + printfv("DONE.\n"); + continue; + } + + $defancy = "{$scheme}://{$domain}/index.php/user/{$real_id}"; + + // keep current profile if we know this non-fancy URI + if (isset($seen[$defancy])) { + $delete[] = $seen[$defancy]; + } + + $update[$id] = $defancy; + $seen[$defancy] = $id; + } + + printfv("DONE.\n"); + } + + $os->free(); + unset($os); +} + +function deleteProfiles(array $delete): void { + printfnq("Deleting duplicated OStatus profiles...\n"); + + $profile = Profile::multiGet('id', $delete); + while ($profile->fetch()) { + $id = $profile->getID(); + printfv("Deleting profile:{$id}..."); + + if (!$profile->delete()) { + echo "\nFailed to delete profile:{$id}\n"; + } + + printfv("DONE.\n"); + } + + unset($profile); +} + +function updateUris(array $update): void { + printfnq("Updating OStatus profiles...\n"); + + $oprofile = Ostatus_profile::multiGet('profile_id', array_keys($update)); + while ($oprofile->fetch()) { + $id = $oprofile->getID(); + printfv("Updating oprofile:{$id} URI..."); + + try { + updateUri($oprofile, $update[$id]); + } catch (Exception $e) { + echo "\nFailed to update oprofile:{$id}: " . $e->getMessage() . "\n"; + } + + printfv("DONE.\n"); + } + + unset($oprofile); +} + +function updateUri(Ostatus_profile $profile, string $new_uri): void { + $orig = clone($profile); + $profile->uri = $new_uri; + $profile->updateWithKeys($orig); +} + +function startsWith(string $str, string $key): bool +{ + $length = strlen($key); + return (substr($str, 0, $length) === $key); +} + +run();