From 00755899520d6b32e053eb0488f45ad6ef50623a Mon Sep 17 00:00:00 2001 From: Christian Leucht Date: Wed, 12 Apr 2023 11:23:59 +0200 Subject: [PATCH 1/3] Make use of HttpDownloader and JsonFile for downloading the TranslatePackage translations. Previously the JSON data was downloaded via file_get_contents() which did not allow to make use of the stored credentials in composer config file. This implementation will make use of the Composer HttpDownloader which uses internally the Composer Config and the stored credentials to send alongside with the Request API Tokens or Basic Auth credentials. --- src/Package/TranslatablePackage.php | 54 ++-------------- src/Package/TranslatablePackageFactory.php | 63 +++++++++++++++---- src/Plugin.php | 7 ++- tests/Unit/Package/ProjectTranslationTest.php | 22 +++---- .../TranslatablePackageFactoryTest.php | 37 +++++++++-- .../Unit/Package/TranslatablePackageTest.php | 9 ++- 6 files changed, 110 insertions(+), 82 deletions(-) diff --git a/src/Package/TranslatablePackage.php b/src/Package/TranslatablePackage.php index febe892..2d2bdc0 100644 --- a/src/Package/TranslatablePackage.php +++ b/src/Package/TranslatablePackage.php @@ -42,11 +42,6 @@ class TranslatablePackage extends Package implements TranslatablePackageInterfac */ protected $endpoint; - /** - * @var bool - */ - private $translationLoaded = false; - /** * @var string|null */ @@ -57,12 +52,14 @@ class TranslatablePackage extends Package implements TranslatablePackageInterfac * @param string $directory * @param string $endpoint * @param string|null $endpointFileType + * @param array $translations */ public function __construct( PackageInterface $package, string $directory, string $endpoint, - ?string $endpointFileType = null + ?string $endpointFileType, + array $translations ) { parent::__construct( @@ -78,6 +75,7 @@ public function __construct( $this->endpoint = $endpoint; $this->languageDirectory = $directory; $this->endpointFileType = $endpointFileType; + $this->translations = $this->parseTranslations($translations); } /** @@ -85,8 +83,6 @@ public function __construct( */ public function translations(array $allowedLanguages = []): array { - $this->loadTranslations(); - if (count($allowedLanguages) === 0) { return $this->translations; } @@ -101,32 +97,6 @@ public function translations(array $allowedLanguages = []): array return $filtered; } - /** - * @return bool - */ - protected function loadTranslations(): bool - { - if ($this->translationLoaded) { - return false; - } - $this->translationLoaded = true; - - $apiUrl = $this->apiEndpoint(); - if ($apiUrl === '') { - return false; - } - - $result = $this->readEndpointContent($apiUrl); - $translations = is_array($result) ? ($result['translations'] ?? null) : null; - if (!is_array($translations) || (count($translations) < 1)) { - return false; - } - - $this->translations = $this->parseTranslations($translations); - - return true; - } - /** * {@inheritDoc} */ @@ -156,22 +126,6 @@ public function languageDirectory(): string return $this->languageDirectory; } - /** - * @param string $apiUrl - * @return array|null - */ - protected function readEndpointContent(string $apiUrl): ?array - { - $result = @file_get_contents($apiUrl); - if (!$result) { - return null; - } - - $result = json_decode($result, true); - - return is_array($result) ? $result : null; - } - /** * @param array $translationsData * @return list diff --git a/src/Package/TranslatablePackageFactory.php b/src/Package/TranslatablePackageFactory.php index 9016711..56d88b8 100644 --- a/src/Package/TranslatablePackageFactory.php +++ b/src/Package/TranslatablePackageFactory.php @@ -17,8 +17,10 @@ use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\UpdateOperation; +use Composer\IO\IOInterface; +use Composer\Json\JsonFile; use Composer\Package\PackageInterface; -use Composer\Package\Version\VersionParser; +use Composer\Util\HttpDownloader; use Inpsyde\WpTranslationDownloader\Config\PluginConfiguration; use Inpsyde\WpTranslationDownloader\Util\FnMatcher; @@ -31,16 +33,33 @@ class TranslatablePackageFactory */ protected $pluginConfiguration; + /** + * @var HttpDownloader + */ + protected $downloader; + + /** + * @var IOInterface + */ + protected $io; + /** * @param PluginConfiguration $pluginConfiguration */ - public function __construct(PluginConfiguration $pluginConfiguration) - { + public function __construct( + PluginConfiguration $pluginConfiguration, + HttpDownloader $downloader, + IOInterface $io + ) { + $this->pluginConfiguration = $pluginConfiguration; + $this->downloader = $downloader; + $this->io = $io; } /** * @param UninstallOperation|UpdateOperation|InstallOperation|OperationInterface $operation + * * @return null|TranslatablePackageInterface * @throws \InvalidArgumentException */ @@ -65,19 +84,37 @@ public function createFromOperation( */ public function create(PackageInterface $package): ?TranslatablePackageInterface { - $directory = $this->resolveDirectory($package); - if (!$directory) { - return null; - } + try { + $directory = $this->resolveDirectory($package); + if (! $directory) { + return null; + } - $endpointData = $this->resolveEndpoint($package); - if ($endpointData === null) { - return null; - } + $endpointData = $this->resolveEndpoint($package); + if ($endpointData === null) { + return null; + } - [$endpoint, $endpointType] = $endpointData; + [$endpoint, $endpointType] = $endpointData; - return new TranslatablePackage($package, $directory, $endpoint, $endpointType); + $jsonFile = new JsonFile($endpoint, $this->downloader, $this->io); + $translations = $jsonFile->read(); + if (! $translations) { + return null; + } + + return new TranslatablePackage( + $package, + $directory, + $endpoint, + $endpointType, + (array) ($translations['translations'] ?? []) + ); + } catch (\Throwable $exception) { + $this->io->error($exception->getMessage()); + + return null; + } } /** diff --git a/src/Plugin.php b/src/Plugin.php index 39493d9..c9bf061 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -16,6 +16,7 @@ use Composer\Command\BaseCommand; use Composer\Composer; use Composer\EventDispatcher\EventSubscriberInterface; +use Composer\Factory; use Composer\Installer\PackageEvent; use Composer\IO\IOInterface; use Composer\Package\CompletePackage; @@ -147,7 +148,11 @@ public function activate(Composer $composer, IOInterface $io) return; } - $this->translatablePackageFactory = new TranslatablePackageFactory($this->pluginConfig); + $this->translatablePackageFactory = new TranslatablePackageFactory( + $this->pluginConfig, + Factory::createHttpDownloader($io, $composer->getConfig()), + $this->io + ); $this->translationsDownloader = new TranslationPackageDownloader( $composer->getLoop(), diff --git a/tests/Unit/Package/ProjectTranslationTest.php b/tests/Unit/Package/ProjectTranslationTest.php index 410bc1c..d8a9c7f 100644 --- a/tests/Unit/Package/ProjectTranslationTest.php +++ b/tests/Unit/Package/ProjectTranslationTest.php @@ -6,7 +6,6 @@ use Composer\Package\CompletePackage; use Composer\Package\Package; -use Inpsyde\WpTranslationDownloader\Package\ProjectTranslation; use Inpsyde\WpTranslationDownloader\Package\TranslatablePackage; use PHPUnit\Framework\TestCase; @@ -30,16 +29,10 @@ public function __construct(?string $endpointFileType, array $data) new CompletePackage('test/test', '1.0.0.0', '1.0'), __DIR__, 'https://example.com/test/test/translations', - $endpointFileType + $endpointFileType, + [$this->data] ); } - - protected function loadTranslations(): bool - { - $this->translations = $this->parseTranslations([$this->data]); - - return true; - } }; $translation = $translatable->translations()[0]; @@ -215,6 +208,7 @@ public function testGenerationFromParsedJson(): void ] } JSON; + $translatablePackage = new class ($json) extends TranslatablePackage { private $json; @@ -224,15 +218,19 @@ public function __construct(string $json) parent::__construct( new Package('test/test', '2.0.5.0', '1.2.0.5'), __DIR__, - 'https://example.com' + 'https://example.com', + null, + $this->readEndpointContent('')['translations'] ?? [] ); } - protected function readEndpointContent(string $apiUrl): ?array + protected function readEndpointContent(string $apiUrl): array { $result = json_decode($this->json, true); - return is_array($result) ? $result : null; + return is_array($result) + ? $result + : []; } }; diff --git a/tests/Unit/Package/TranslatablePackageFactoryTest.php b/tests/Unit/Package/TranslatablePackageFactoryTest.php index 7a03d3c..b347575 100644 --- a/tests/Unit/Package/TranslatablePackageFactoryTest.php +++ b/tests/Unit/Package/TranslatablePackageFactoryTest.php @@ -5,8 +5,11 @@ namespace Inpsyde\WpTranslationDownloader\Tests\Unit\Package; use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\IO\IOInterface; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Package; +use Composer\Util\Http\Response; +use Composer\Util\HttpDownloader; use Inpsyde\WpTranslationDownloader\Config\PluginConfiguration; use Inpsyde\WpTranslationDownloader\Package\TranslatablePackageFactory; use Inpsyde\WpTranslationDownloader\Package\TranslatablePackageInterface; @@ -20,7 +23,21 @@ class TranslatablePackageFactoryTest extends TestCase public function testCreateFromOperation(): void { $pluginConfiguration = new PluginConfiguration([]); - $packageFactory = new TranslatablePackageFactory($pluginConfiguration); + + $responseStub = \Mockery::mock(Response::class); + $responseStub->expects('getBody')->andReturn('{"translations":{}}'); + + $downloaderStub = \Mockery::mock(HttpDownloader::class); + $downloaderStub->expects('get')->andReturn($responseStub); + + $ioStub = \Mockery::mock(IOInterface::class); + $ioStub->allows('error'); + + $packageFactory = new TranslatablePackageFactory( + $pluginConfiguration, + $downloaderStub, + $ioStub + ); $expectedName = 'inpsyde/google-tag-manager'; $expectedType = 'wordpress-plugin'; @@ -49,7 +66,11 @@ public function testResolveEndpoint( $loader = new ArrayLoader(); $pluginConfiguration = new PluginConfiguration($expectedApi); - $packageFactory = new TranslatablePackageFactory($pluginConfiguration); + $packageFactory = new TranslatablePackageFactory( + $pluginConfiguration, + \Mockery::mock(HttpDownloader::class), + \Mockery::mock(IOInterface::class) + ); $package = $loader->load($packageData); static::assertSame($expected, $packageFactory->resolveEndpoint($package)); @@ -217,7 +238,11 @@ public function testReplacingPlaceholders(): void $pluginConfiguration = new PluginConfiguration($api); - $packageFactory = new TranslatablePackageFactory($pluginConfiguration); + $packageFactory = new TranslatablePackageFactory( + $pluginConfiguration, + \Mockery::mock(HttpDownloader::class), + \Mockery::mock(IOInterface::class) + ); static::assertSame([$expectedUrl, null], $packageFactory->resolveEndpoint($package)); } @@ -230,7 +255,11 @@ public function testResolveDirectory(array $input, array $packages): void { $pluginConfiguration = new PluginConfiguration($input); - $packageFactory = new TranslatablePackageFactory($pluginConfiguration); + $packageFactory = new TranslatablePackageFactory( + $pluginConfiguration, + \Mockery::mock(HttpDownloader::class), + \Mockery::mock(IOInterface::class) + ); foreach ($packages as $packageData) { $version = $packageData['version'] ?? '1.0'; $package = new Package($packageData['name'], $version, $version); diff --git a/tests/Unit/Package/TranslatablePackageTest.php b/tests/Unit/Package/TranslatablePackageTest.php index 004d4b9..20879f1 100644 --- a/tests/Unit/Package/TranslatablePackageTest.php +++ b/tests/Unit/Package/TranslatablePackageTest.php @@ -5,7 +5,6 @@ namespace Inpsyde\WpTranslationDownloader\Tests\Unit\Package; use Composer\Package\Package; -use Composer\Package\PackageInterface; use Inpsyde\WpTranslationDownloader\Package\TranslatablePackage; use PHPUnit\Framework\TestCase; @@ -18,7 +17,13 @@ class TranslatablePackageTest extends TestCase public function testProjectName(string $input, string $expected): void { $package = new Package($input, '1.0.0.0', '1.0.0'); - $translatablePackage = new TranslatablePackage($package, __DIR__, 'https://example.com'); + $translatablePackage = new TranslatablePackage( + $package, + __DIR__, + 'https://example.com', + null, + [] + ); static::assertSame($expected, $translatablePackage->projectName()); } From 16db260c2951370839727ffd4227e7923411d8c7 Mon Sep 17 00:00:00 2001 From: Christian Leucht Date: Wed, 12 Apr 2023 11:29:12 +0200 Subject: [PATCH 2/3] psalm. --- src/Package/TranslatablePackageFactory.php | 5 ++++- src/Plugin.php | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Package/TranslatablePackageFactory.php b/src/Package/TranslatablePackageFactory.php index 56d88b8..0235e3e 100644 --- a/src/Package/TranslatablePackageFactory.php +++ b/src/Package/TranslatablePackageFactory.php @@ -98,17 +98,20 @@ public function create(PackageInterface $package): ?TranslatablePackageInterface [$endpoint, $endpointType] = $endpointData; $jsonFile = new JsonFile($endpoint, $this->downloader, $this->io); + /** @var array|null $translations */ $translations = $jsonFile->read(); if (! $translations) { return null; } + $translations = (array) ($translations['translations'] ?? []); + return new TranslatablePackage( $package, $directory, $endpoint, $endpointType, - (array) ($translations['translations'] ?? []) + $translations ); } catch (\Throwable $exception) { $this->io->error($exception->getMessage()); diff --git a/src/Plugin.php b/src/Plugin.php index c9bf061..dd2f91f 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -31,7 +31,6 @@ use Inpsyde\WpTranslationDownloader\Command\DownloadCommand; use Inpsyde\WpTranslationDownloader\Config\PluginConfiguration; use Inpsyde\WpTranslationDownloader\Config\PluginConfigurationBuilder; -use Inpsyde\WpTranslationDownloader\Util\ArchiveDownloaderFactory; use Inpsyde\WpTranslationDownloader\Util\Downloader; use Inpsyde\WpTranslationDownloader\Package\TranslatablePackageFactory; use Inpsyde\WpTranslationDownloader\Util\Locker; From cfd33989eb9563b2c2021fc69fdc5061a077a490 Mon Sep 17 00:00:00 2001 From: Christian Leucht Date: Wed, 12 Apr 2023 13:20:06 +0200 Subject: [PATCH 3/3] Configuration.md // add new section for "Authentication for privately hosted translations" --- docs/Configuration.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 0079bfc..f1489df 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -249,8 +249,6 @@ By using the above configuration, the package `my/package-name` will have transl The `directories` configuration, both by name and by type, supports [dynamic resolving via placeholders](./Dynamic%20resolving%20api%20and%20directories.md). - - ## Virtual Packages It might be desirable to install translations for packages that are *not* required in Composer. @@ -291,3 +289,11 @@ passed, assuming latest version, if none is passed. In any case, all three properties (if defined and not empty), will be used when building the API endpoint (no matter if default or customized by name/type). + +## Authentication for privately hosted translations + +Your private GlotPress system is probably secured with one or more authentication options. In order to allow your project to have access to these endpoints and translations file archives you will have to tell Composer how to authenticate with the server that hosts them. + +`inpsyde/wp-translation-downloader` makes use of the full Composer PHP API and therefor uses the `Composer\Util\HttpDownloader`. This implementation accesses internally the configured Composer defined credentials. + +Credentials can be stored on 4 different places; in an `auth.json` for the project, a global `auth.json`, in the `composer.json` itself or in the `COMPOSER_AUTH` environment variable. Read more about configuration of authentication in Composer here: https://getcomposer.org/doc/articles/authentication-for-private-packages.md \ No newline at end of file