diff options
author | Maxence Lange <maxence@artificial-owl.com> | 2020-09-24 11:52:22 -0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-09-24 11:52:22 -0100 |
commit | a4bd006e362a04115868fc935702302238a3d9be (patch) | |
tree | 257337822c9f3c073577c139a005f58ad0b7c8ff | |
parent | 8043004d62e55f353c64ed8f5d22594f70bbfee3 (diff) | |
parent | 67e4dcf8a0798a3f8f3cd6a6fe1977245e122705 (diff) |
Merge pull request #954 from nextcloud/enh/noid/iwebfinger
using IWebfingerManager
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Makefile | 12 | ||||
-rw-r--r-- | appinfo/app.php | 41 | ||||
-rw-r--r-- | appinfo/autoload.php | 37 | ||||
-rw-r--r-- | appinfo/info.xml | 2 | ||||
-rw-r--r-- | composer/composer.json (renamed from composer.json) | 9 | ||||
-rw-r--r-- | composer/composer.lock (renamed from composer.lock) | 58 | ||||
-rw-r--r-- | lib/AppInfo/Application.php | 65 | ||||
-rw-r--r-- | lib/Db/StreamActionsRequest.php | 4 | ||||
-rw-r--r-- | lib/Listeners/WellKnownListener.php | 83 | ||||
-rw-r--r-- | lib/Model/WebfingerLink.php | 148 | ||||
-rw-r--r-- | lib/Service/ActorService.php | 8 | ||||
-rw-r--r-- | lib/Service/CacheActorService.php | 18 | ||||
-rw-r--r-- | lib/Service/WellKnownService.php | 132 |
14 files changed, 486 insertions, 134 deletions
@@ -3,3 +3,6 @@ js/ node_modules/ vendor/ img/twemoji/ +composer/* +!composer/composer.json +!composer/composer.lock @@ -64,9 +64,9 @@ clean: clean-dev: rm -rf node_modules -# composer packages -composer: - composer install --prefer-dist +build-composer: + composer install --prefer-dist --working-dir composer + composer update --prefer-dist --working-dir composer # releasing to github release: appstore github-release github-upload @@ -88,7 +88,7 @@ github-upload: --file $(build_dir)/$(app_name)-$(version).tar.gz # creating .tar.gz + signature -appstore: dev-setup lint build-js-production composer +appstore: dev-setup lint build-js-production build-composer mkdir -p $(sign_dir) rsync -a \ --exclude=/build \ @@ -101,8 +101,8 @@ appstore: dev-setup lint build-js-production composer --exclude=/.babelrc.js \ --exclude=/.drone.yml \ --exclude=/.eslintrc.js \ - --exclude=/composer.json \ - --exclude=/composer.lock \ + --exclude=/composer/composer.json \ + --exclude=/composer/composer.lock \ --exclude=/src \ --exclude=/node_modules \ --exclude=/webpack.*.js \ diff --git a/appinfo/app.php b/appinfo/app.php deleted file mode 100644 index 5a8c2071..00000000 --- a/appinfo/app.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php -declare(strict_types=1); - - -/** - * Nextcloud - Social Support - * - * This file is licensed under the Affero General Public License version 3 or - * later. See the COPYING file. - * - * @author Maxence Lange <maxence@artificial-owl.com> - * @copyright 2018, Maxence Lange <maxence@artificial-owl.com> - * @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 <http://www.gnu.org/licenses/>. - * - */ - -namespace OCA\Social\AppInfo; - - -require_once __DIR__ . '/autoload.php'; - - -/** @var Application $app */ -$app = \OC::$server->query(Application::class); - -$app->checkUpgradeStatus(); - - diff --git a/appinfo/autoload.php b/appinfo/autoload.php deleted file mode 100644 index 3427325f..00000000 --- a/appinfo/autoload.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php -declare(strict_types=1); - - -/** - * Nextcloud - Social Support - * - * This file is licensed under the Affero General Public License version 3 or - * later. See the COPYING file. - * - * @author Maxence Lange <maxence@artificial-owl.com> - * @copyright 2018, Maxence Lange <maxence@artificial-owl.com> - * @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 <http://www.gnu.org/licenses/>. - * - */ - -namespace OCA\Social\AppInfo; - -$composerDir = __DIR__ . '/../vendor/'; - -if (is_dir($composerDir) && file_exists($composerDir . 'autoload.php')) { - require_once $composerDir . 'autoload.php'; -} - diff --git a/appinfo/info.xml b/appinfo/info.xml index 70e44372..84a72e5b 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -35,7 +35,7 @@ <database>pgsql</database> <database>sqlite</database> <database>mysql</database> - <nextcloud min-version="17" max-version="19"/> + <nextcloud min-version="20" max-version="21"/> </dependencies> <background-jobs> diff --git a/composer.json b/composer/composer.json index f009f3b3..2e394046 100644 --- a/composer.json +++ b/composer/composer.json @@ -12,6 +12,15 @@ "config": { "platform": { "php": "7.0.0" + }, + "vendor-dir": ".", + "optimize-autoloader": true, + "classmap-authoritative": true, + "autoloader-suffix": "Social" + }, + "autoload": { + "psr-4": { + "OCA\\Social\\": "../lib/" } }, "require": { diff --git a/composer.lock b/composer/composer.lock index 758e0955..c03b7b1b 100644 --- a/composer.lock +++ b/composer/composer.lock @@ -1,7 +1,7 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], "content-hash": "f93a783c86bad53b0b8486db3fc61380", @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/daita/my-small-php-tools.git", - "reference": "d8778803612af20699c7efb0637bfe62478e596c" + "reference": "4e602526c3afbba7255ae4764037562075ef030f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/daita/my-small-php-tools/zipball/d8778803612af20699c7efb0637bfe62478e596c", - "reference": "d8778803612af20699c7efb0637bfe62478e596c", + "url": "https://api.github.com/repos/daita/my-small-php-tools/zipball/4e602526c3afbba7255ae4764037562075ef030f", + "reference": "4e602526c3afbba7255ae4764037562075ef030f", "shasum": "" }, "require": { @@ -40,7 +40,7 @@ } ], "description": "My small PHP Tools", - "time": "2020-03-18T23:09:45+00:00" + "time": "2020-08-06T13:26:28+00:00" }, { "name": "friendica/json-ld", @@ -243,6 +243,7 @@ ], "description": "This tool check syntax of PHP files about 20x faster than serial check.", "homepage": "https://github.com/JakubOnderka/PHP-Parallel-Lint", + "abandoned": "php-parallel-lint/php-parallel-lint", "time": "2018-02-24T15:31:20+00:00" }, { @@ -853,6 +854,7 @@ "keywords": [ "tokenizer" ], + "abandoned": true, "time": "2017-11-27T05:48:46+00:00" }, { @@ -1560,16 +1562,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.17.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9" + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9", - "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", "shasum": "" }, "require": { @@ -1581,7 +1583,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -1614,7 +1620,21 @@ "polyfill", "portable" ], - "time": "2020-05-12T16:14:59+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" }, { "name": "theseer/tokenizer", @@ -1658,23 +1678,24 @@ }, { "name": "webmozart/assert", - "version": "1.8.0", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6" + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6", - "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0", + "php": "^5.3.3 || ^7.0 || ^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { + "phpstan/phpstan": "<0.12.20", "vimeo/psalm": "<3.9.1" }, "require-dev": { @@ -1702,7 +1723,7 @@ "check", "validate" ], - "time": "2020-04-18T12:12:48+00:00" + "time": "2020-07-08T17:02:28+00:00" } ], "aliases": [], @@ -1716,5 +1737,6 @@ "platform-dev": [], "platform-overrides": { "php": "7.0.0" - } + }, + "plugin-api-version": "1.1.0" } diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 18996bac..87301824 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -31,13 +31,20 @@ declare(strict_types=1); namespace OCA\Social\AppInfo; +use Closure; use OC\DB\SchemaWrapper; +use OC\WellKnown\Event\WellKnownEvent; +use OCA\Social\Listeners\WellKnownListener; use OCA\Social\Notification\Notifier; use OCA\Social\Service\ConfigService; use OCA\Social\Service\UpdateService; use OCP\AppFramework\App; -use OCP\AppFramework\IAppContainer; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\QueryException; +use OCP\IServerContainer; +use Throwable; /** @@ -45,22 +52,12 @@ use OCP\AppFramework\QueryException; * * @package OCA\Social\AppInfo */ -class Application extends App { +class Application extends App implements IBootstrap { const APP_NAME = 'social'; - /** @var ConfigService */ - private $configService; - - /** @var UpdateService */ - private $updateService; - - /** @var IAppContainer */ - private $container; - - /** * Application constructor. * @@ -68,41 +65,59 @@ class Application extends App { */ public function __construct(array $params = []) { parent::__construct(self::APP_NAME, $params); + } - $this->container = $this->getContainer(); - $manager = $this->container->getServer() - ->getNotificationManager(); + /** + * @param IRegistrationContext $context + */ + public function register(IRegistrationContext $context): void { + // TODO: nc21, uncomment + // $context->registerEventListener(WellKnownEvent::class, WellKnownListener::class); + } + + + /** + * @param IBootContext $context + */ + public function boot(IBootContext $context): void { + $manager = $context->getServerContainer() + ->getNotificationManager(); $manager->registerNotifierService(Notifier::class); + + try { + $context->injectFn(Closure::fromCallable([$this, 'checkUpgradeStatus'])); + } catch (Throwable $e) { + } } /** + * Register Navigation Tab * + * @param IServerContainer $container */ - public function checkUpgradeStatus() { - $upgradeChecked = $this->container->getServer() - ->getConfig() - ->getAppValue(Application::APP_NAME, 'update_checked', ''); + protected function checkUpgradeStatus(IServerContainer $container) { + $upgradeChecked = $container->getConfig() + ->getAppValue(Application::APP_NAME, 'update_checked', ''); if ($upgradeChecked === '0.3') { return; } try { - $this->configService = $this->container->query(ConfigService::class); - $this->updateService = $this->container->query(UpdateService::class); + $configService = $container->query(ConfigService::class); + $updateService = $container->query(UpdateService::class); } catch (QueryException $e) { return; } - $server = $this->container->getServer(); - $schema = new SchemaWrapper($server->getDatabaseConnection()); + $schema = new SchemaWrapper($container->getDatabaseConnection()); if ($schema->hasTable('social_a2_stream')) { - $this->updateService->checkUpdateStatus(); + $updateService->checkUpdateStatus(); } - $this->configService->setAppValue('update_checked', '0.3'); + $configService->setAppValue('update_checked', '0.3'); } } diff --git a/lib/Db/StreamActionsRequest.php b/lib/Db/StreamActionsRequest.php index 99cf906d..2bacf14b 100644 --- a/lib/Db/StreamActionsRequest.php +++ b/lib/Db/StreamActionsRequest.php @@ -96,9 +96,7 @@ class StreamActionsRequest extends StreamActionsRequestBuilder { $this->limitToActorId($qb, $action->getActorId()); $this->limitToStreamId($qb, $action->getStreamId()); - $count = $qb->execute(); - - return $count; + return $qb->execute(); } diff --git a/lib/Listeners/WellKnownListener.php b/lib/Listeners/WellKnownListener.php new file mode 100644 index 00000000..8eb18c64 --- /dev/null +++ b/lib/Listeners/WellKnownListener.php @@ -0,0 +1,83 @@ +<?php +declare(strict_types=1); + + +/** + * Nextcloud - Social Support + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Maxence Lange <maxence@artificial-owl.com> + * @copyright 2020, Maxence Lange <maxence@artificial-owl.com> + * @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 <http://www.gnu.org/licenses/>. + * + */ + + +namespace OCA\Social\Listeners; + + +use OC\WellKnown\Event\WellKnownEvent; +use OCA\Social\Exceptions\CacheActorDoesNotExistException; +use OCA\Social\Exceptions\SocialAppConfigException; +use OCA\Social\Exceptions\UnauthorizedFediverseException; +use OCA\Social\Service\WellKnownService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\WellKnown\IWellKnownManager; + + +/** + * Class WellKnownListener + * + * @package OCA\Social\Listeners + */ +class WellKnownListener implements IEventListener { + + + private $wellKnownService; + + + /** + * WellKnownListener constructor. + * + * @param WellKnownService $wellKnownService + */ + public function __construct(WellKnownService $wellKnownService) { + $this->wellKnownService = $wellKnownService; + } + + + /** + * @param Event $event + */ + public function handle(Event $event): void { + if (!$event instanceof WellKnownEvent) { + return; + } + + $wellKnown = $event->getWellKnown(); + if ($wellKnown->getService() === IWellKnownManager::WEBFINGER) { + try { + $this->wellKnownService->webfinger($wellKnown); + } catch (CacheActorDoesNotExistException | SocialAppConfigException | UnauthorizedFediverseException $e) { + } + } + } + +} + diff --git a/lib/Model/WebfingerLink.php b/lib/Model/WebfingerLink.php new file mode 100644 index 00000000..5e44f593 --- /dev/null +++ b/lib/Model/WebfingerLink.php @@ -0,0 +1,148 @@ +<?php +declare(strict_types=1); + + +/** + * Nextcloud - Social Support + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Maxence Lange <maxence@artificial-owl.com> + * @copyright 2018, Maxence Lange <maxence@artificial-owl.com> + * @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 <http://www.gnu.org/licenses/>. + * + */ + + +namespace OCA\Social\Model; + +use JsonSerializable; + + +/** + * Class WebfingerLink + * + * @package OCA\Social\Model + */ +class WebfingerLink implements JsonSerializable { + + + /** @var string */ + private $href = ''; + + /** @var string */ + private $rel = ''; + + /** @var string */ + private $template = ''; + + /** @var string */ + private $type = ''; + + + /** + * @return string + */ + public function getHref(): string { + return $this->href; + } + + /** + * @param string $value + * + * @return WebfingerLink + */ + public function setHref(string $value): self { + $this->href = $value; + + return $this; + } + + + /** + * @return string + */ + public function getType(): string { + return $this->type; + } + + /** + * @param string $value + * + * @return WebfingerLink + */ + public function setType(string $value): self { + $this->type = $value; + + return $this; + } + + + /** + * @return string + */ + public function getRel(): string { + return $this->rel; + } + + /** + * @param string $value + * + * @return WebfingerLink + */ + public function setRel(string $value): self { + $this->rel = $value; + + return $this; + } + + + /** + * @return string + */ + public function getTemplate(): string { + return $this->template; + } + + /** + * @param string $value + * + * @return WebfingerLink + */ + public function setTemplate(string $value): self { + $this->template = $value; + + return $this; + } + + + /** + * @return array + */ + public function jsonSerialize(): array { + $data = [ + 'rel' => $this->getRel(), + 'type' => $this->getType(), + 'template' => $this->getTemplate(), + 'href' => $this->getHref() + ]; + + return array_filter($data); + } + +} + diff --git a/lib/Service/ActorService.php b/lib/Service/ActorService.php index 429ebf88..a8fe47d1 100644 --- a/lib/Service/ActorService.php +++ b/lib/Service/ActorService.php @@ -36,6 +36,7 @@ use OCA\Social\Db\CacheActorsRequest; use OCA\Social\Db\CacheDocumentsRequest; use OCA\Social\Exceptions\CacheActorDoesNotExistException; use OCA\Social\Exceptions\CacheDocumentDoesNotExistException; +use OCA\Social\Exceptions\ItemAlreadyExistsException; use OCA\Social\Exceptions\ItemUnknownException; use OCA\Social\Model\ActivityPub\Actor\Person; @@ -92,6 +93,8 @@ class ActorService { /** * @param Person $actor + * + * @throws ItemAlreadyExistsException */ public function cacheLocalActor(Person $actor) { $actor->setLocal(true); @@ -108,6 +111,8 @@ class ActorService { /** * @param Person $actor + * + * @throws ItemAlreadyExistsException */ public function save(Person $actor) { $this->cacheDocumentIfNeeded($actor); @@ -119,6 +124,7 @@ class ActorService { * @param Person $actor * * @return int + * @throws ItemAlreadyExistsException */ public function update(Person $actor): int { $this->cacheDocumentIfNeeded($actor); @@ -129,6 +135,8 @@ class ActorService { /** * @param Person $actor + * + * @throws ItemAlreadyExistsException */ private function cacheDocumentIfNeeded(Person $actor) { if ($actor->hasIcon()) { diff --git a/lib/Service/CacheActorService.php b/lib/Service/CacheActorService.php index f049c99a..b448a682 100644 --- a/lib/Service/CacheActorService.php +++ b/lib/Service/CacheActorService.php @@ -50,6 +50,7 @@ use OCA\Social\Exceptions\RetrieveAccountFormatException; use OCA\Social\Exceptions\SocialAppConfigException; use OCA\Social\Exceptions\UnauthorizedFediverseException; use OCA\Social\Model\ActivityPub\Actor\Person; +use OCP\IURLGenerator; class CacheActorService { @@ -58,12 +59,18 @@ class CacheActorService { use TArrayTools; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var CacheActorsRequest */ private $cacheActorsRequest; /** @var CurlService */ private $curlService; + /** @var FediverseService */ + private $fediverseService; + /** @var ConfigService */ private $configService; @@ -74,17 +81,21 @@ class CacheActorService { /** * CacheService constructor. * + * @param IUrlGenerator $urlGenerator * @param CacheActorsRequest $cacheActorsRequest * @param CurlService $curlService + * @param FediverseService $fediverseService * @param ConfigService $configService * @param MiscService $miscService */ public function __construct( - CacheActorsRequest $cacheActorsRequest, CurlService $curlService, - ConfigService $configService, MiscService $miscService + IUrlGenerator $urlGenerator, CacheActorsRequest $cacheActorsRequest, CurlService $curlService, + FediverseService $fediverseService, ConfigService $configService, MiscService $miscService ) { + $this->urlGenerator = $urlGenerator; $this->cacheActorsRequest = $cacheActorsRequest; $this->curlService = $curlService; + $this->fediverseService = $fediverseService; $this->configService = $configService; $this->miscService = $miscService; } @@ -166,8 +177,9 @@ class CacheActorService { */ public function getFromLocalAccount(string $account): Person { $instance = ''; + $account = ltrim($account, '@'); if (strrpos($account, '@')) { - list($account, $instance) = explode('@', $account); + list($account, $instance) = explode('@', $account, 2); } if ($instance === '' diff --git a/lib/Service/WellKnownService.php b/lib/Service/WellKnownService.php new file mode 100644 index 00000000..2ce5342f --- /dev/null +++ b/lib/Service/WellKnownService.php @@ -0,0 +1,132 @@ +<?php +declare(strict_types=1); + + +/** + * Nextcloud - Social Support + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Maxence Lange <maxence@artificial-owl.com> + * @copyright 2018, Maxence Lange <maxence@artificial-owl.com> + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under th |