diff --git a/services/packagist/packagist-base.js b/services/packagist/packagist-base.js index 298105e40997f..2e50a0c010896 100644 --- a/services/packagist/packagist-base.js +++ b/services/packagist/packagist-base.js @@ -6,9 +6,7 @@ const packageSchema = Joi.array().items( Joi.object({ version: Joi.string().required(), require: Joi.alternatives( - Joi.object({ - php: Joi.string(), - }).required(), + Joi.object().pattern(Joi.string(), Joi.string()).required(), Joi.string().valid('__unset') ), }) @@ -23,7 +21,7 @@ class BasePackagistService extends BaseJsonService { /** * Default fetch method. * - * This method utilize composer metadata API which + * This method utilizes composer metadata API which * "... is the preferred way to access the data as it is always up to date, * and dumped to static files so it is very efficient on our end." (comment from official documentation). * For more information please refer to https://packagist.org/apidoc#get-package-data. @@ -47,7 +45,7 @@ class BasePackagistService extends BaseJsonService { /** * Fetch dev releases method. * - * This method utilize composer metadata API which + * This method utilizes composer metadata API which * "... is the preferred way to access the data as it is always up to date, * and dumped to static files so it is very efficient on our end." (comment from official documentation). * For more information please refer to https://packagist.org/apidoc#get-package-data. @@ -166,7 +164,6 @@ class BasePackagistService extends BaseJsonService { return versions.filter(version => version.version === release)[0] } } - const customServerDocumentationFragment = `

Note that only network-accessible packagist.org and other self-hosted Packagist instances are supported. diff --git a/services/packagist/packagist-dependency-version.service.js b/services/packagist/packagist-dependency-version.service.js new file mode 100644 index 0000000000000..832f9c46a985b --- /dev/null +++ b/services/packagist/packagist-dependency-version.service.js @@ -0,0 +1,200 @@ +import Joi from 'joi' +import { optionalUrl } from '../validators.js' +import { NotFound } from '../index.js' +import { + allVersionsSchema, + BasePackagistService, + customServerDocumentationFragment, +} from './packagist-base.js' + +const queryParamSchema = Joi.object({ + server: optionalUrl, + version: Joi.string(), +}).required() + +export default class PackagistDependencyVersion extends BasePackagistService { + static category = 'platform-support' + + static route = { + base: 'packagist/dependency-v', + pattern: ':user/:repo/:dependency+', + queryParamSchema, + } + + static examples = [ + { + title: 'Packagist Dependency Version', + namedParams: { + user: 'symfony', + repo: 'symfony', + dependency: 'twig/twig', + }, + staticPreview: this.render({ + dependency: 'twig/twig', + dependencyVersion: '2.13|^3.0.4', + }), + }, + { + title: 'Packagist Dependency Version (specify version)', + namedParams: { + user: 'symfony', + repo: 'symfony', + dependency: 'twig/twig', + }, + queryParams: { + version: 'v2.8.0', + }, + staticPreview: this.render({ + dependency: 'twig/twig', + dependencyVersion: '1.12', + }), + }, + { + title: 'Packagist Dependency Version (custom server)', + namedParams: { + user: 'symfony', + repo: 'symfony', + dependency: 'twig/twig', + }, + queryParams: { + server: 'https://packagist.org', + }, + staticPreview: this.render({ + dependency: 'twig/twig', + dependencyVersion: '2.13|^3.0.4', + }), + documentation: customServerDocumentationFragment, + }, + { + title: 'Packagist PHP Version', + namedParams: { + user: 'symfony', + repo: 'symfony', + dependency: 'php', + }, + staticPreview: this.render({ + dependency: 'php', + dependencyVersion: '^7.1.3', + }), + }, + ] + + static defaultBadgeData = { + label: 'dependency version', + color: 'blue', + } + + static render({ dependency, dependencyVersion }) { + return { + label: dependency, + message: dependencyVersion, + } + } + + async getDependencyVersion({ + json, + user, + repo, + dependency, + version = '', + server, + }) { + let packageVersion + const versions = BasePackagistService.expandPackageVersions( + json, + this.getPackageName(user, repo) + ) + + if (version === '') { + packageVersion = this.findLatestRelease(versions) + } else { + try { + packageVersion = await this.findSpecifiedVersion( + versions, + user, + repo, + version, + server + ) + } catch (e) { + packageVersion = null + } + } + + if (!packageVersion) { + throw new NotFound({ prettyMessage: 'invalid version' }) + } + + if (!packageVersion.require) { + throw new NotFound({ prettyMessage: 'version requirement not found' }) + } + + // All dependencies' names in the 'require' section from the response should be lowercase, + // so that we can compare lowercase name of the dependency given via url by the user. + Object.keys(packageVersion.require).forEach(dependency => { + packageVersion.require[dependency.toLowerCase()] = + packageVersion.require[dependency] + }) + + const depLowerCase = dependency.toLowerCase() + + if (!packageVersion.require[depLowerCase]) { + throw new NotFound({ prettyMessage: 'version requirement not found' }) + } + + return { dependencyVersion: packageVersion.require[depLowerCase] } + } + + async handle({ user, repo, dependency }, { server, version = '' }) { + const allData = await this.fetch({ + user, + repo, + schema: allVersionsSchema, + server, + }) + + const { dependencyVersion } = await this.getDependencyVersion({ + json: allData, + user, + repo, + dependency, + version, + server, + }) + + return this.constructor.render({ + dependency, + dependencyVersion, + }) + } + + findVersionIndex(json, version) { + return json.findIndex(v => v.version === version) + } + + async findSpecifiedVersion(json, user, repo, version, server) { + let release + + if ((release = json[this.findVersionIndex(json, version)])) { + return release + } else { + try { + const allData = await this.fetchDev({ + user, + repo, + schema: allVersionsSchema, + server, + }) + + const versions = BasePackagistService.expandPackageVersions( + allData, + this.getPackageName(user, repo) + ) + + return versions[this.findVersionIndex(versions, version)] + } catch (e) { + return release + } + } + } +} diff --git a/services/packagist/packagist-dependency-version.spec.js b/services/packagist/packagist-dependency-version.spec.js new file mode 100644 index 0000000000000..b03b2e39e6ee5 --- /dev/null +++ b/services/packagist/packagist-dependency-version.spec.js @@ -0,0 +1,93 @@ +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import PackagistDependencyVersion from './packagist-dependency-version.service.js' +const { expect } = chai +chai.use(chaiAsPromised) + +describe('PackagistDependencyVersion', function () { + const fullPackagistJson = { + packages: { + 'frodo/the-one-package': [ + { + version: 'v3.0.0', + require: { php: '^7.4 || 8', 'twig/twig': '~1.28|~2.0' }, + }, + { + version: 'v2.5.0', + require: '__unset', + }, + { + version: 'v2.4.0', + }, + { + version: 'v2.0.0', + require: { php: '^7.2', 'twig/twig': '~1.20|~1.30' }, + }, + { + version: 'v1.0.0', + require: { php: '^5.6 || ^7', 'twig/twig': '~1.10|~1.0' }, + }, + ], + }, + } + + it('should throw NotFound when package version is missing in the response', async function () { + await expect( + PackagistDependencyVersion.prototype.getDependencyVersion({ + json: fullPackagistJson, + user: 'frodo', + repo: 'the-one-package', + version: 'v4.0.0', + }) + ).to.be.rejectedWith('invalid version') + }) + + it('should throw NotFound when `require` section is missing in the response', async function () { + await expect( + PackagistDependencyVersion.prototype.getDependencyVersion({ + json: fullPackagistJson, + user: 'frodo', + repo: 'the-one-package', + version: 'v2.4.0', + }) + ).to.be.rejectedWith('version requirement not found') + }) + + it('should throw NotFound when `require` section in the response has the value of __unset (thank you, Packagist API :p)', async function () { + await expect( + PackagistDependencyVersion.prototype.getDependencyVersion({ + json: fullPackagistJson, + user: 'frodo', + repo: 'the-one-package', + version: 'v2.5.0', + }) + ).to.be.rejectedWith('version requirement not found') + }) + + it('should return dependency version for the default release', async function () { + expect( + await PackagistDependencyVersion.prototype.getDependencyVersion({ + json: fullPackagistJson, + user: 'frodo', + repo: 'the-one-package', + dependency: 'twig/twig', + }) + ) + .to.have.property('dependencyVersion') + .that.equals('~1.28|~2.0') + }) + + it('should return dependency version for the specified release', async function () { + expect( + await PackagistDependencyVersion.prototype.getDependencyVersion({ + json: fullPackagistJson, + user: 'frodo', + repo: 'the-one-package', + version: 'v2.0.0', + dependency: 'twig/twig', + }) + ) + .to.have.property('dependencyVersion') + .that.equals('~1.20|~1.30') + }) +}) diff --git a/services/packagist/packagist-dependency-version.tester.js b/services/packagist/packagist-dependency-version.tester.js new file mode 100644 index 0000000000000..3509ae484b5fa --- /dev/null +++ b/services/packagist/packagist-dependency-version.tester.js @@ -0,0 +1,61 @@ +import { isComposerVersion } from '../test-validators.js' +import { createServiceTester } from '../tester.js' +export const t = await createServiceTester() + +t.create('gets the package version') + .get('/symfony/symfony/twig/twig.json') + .expectBadge({ label: 'twig/twig', message: isComposerVersion }) + +t.create('incorrect dependency name') + .get('/symfony/symfony/twig/twiiiiiiig.json') + .expectBadge({ + label: 'dependency version', + message: 'version requirement not found', + }) + +t.create('missing vendor of dependency') + .get('/symfony/symfony/twig.json') + .expectBadge({ + label: 'dependency version', + message: 'version requirement not found', + }) + +t.create('gets the package version + specified symfony version') + .get('/symfony/symfony/twig/twig.json?version=v3.2.8') + .expectBadge({ label: 'twig/twig', message: isComposerVersion }) + +t.create('gets the package version + valid custom server') + .get('/symfony/symfony/twig/twig.json?server=https://packagist.org') + .expectBadge({ label: 'twig/twig', message: isComposerVersion }) + +t.create('invalid custom server') + .get('/symfony/symfony/twig/twig.json?server=https://packagisttttttt.org') + .expectBadge({ + label: 'dependency version', + message: 'inaccessible', + }) + +t.create('incorrect symfony version') + .get('/symfony/symfony/twig/twig.json?version=v3.2.80000') + .expectBadge({ + label: 'dependency version', + message: 'invalid version', + }) + +t.create('gets the package version - dependency does not need the vendor') + .get('/symfony/symfony/ext-xml.json') + .expectBadge({ label: 'ext-xml', message: isComposerVersion }) + +t.create('package with no requirements') + .get('/bpampuch/pdfmake/twig/twig.json') + .expectBadge({ + label: 'dependency version', + message: 'version requirement not found', + }) + +t.create('package with no twig/twig version requirement') + .get('/raulfraile/ladybug-theme-modern/twig/twig.json') + .expectBadge({ + label: 'dependency version', + message: 'version requirement not found', + }) diff --git a/services/packagist/packagist-php-version.service.js b/services/packagist/packagist-php-version.service.js index 11ef271899ed6..177d94bc94f17 100644 --- a/services/packagist/packagist-php-version.service.js +++ b/services/packagist/packagist-php-version.service.js @@ -1,149 +1,21 @@ import Joi from 'joi' +import { redirector } from '../index.js' import { optionalUrl } from '../validators.js' -import { NotFound } from '../index.js' -import { - allVersionsSchema, - BasePackagistService, - customServerDocumentationFragment, -} from './packagist-base.js' const queryParamSchema = Joi.object({ server: optionalUrl, }).required() -export default class PackagistPhpVersion extends BasePackagistService { - static category = 'platform-support' - - static route = { +export default redirector({ + category: 'platform-support', + route: { base: 'packagist/php-v', pattern: ':user/:repo/:version?', queryParamSchema, - } - - static examples = [ - { - title: 'Packagist PHP Version Support', - pattern: ':user/:repo', - namedParams: { - user: 'symfony', - repo: 'symfony', - }, - staticPreview: this.render({ php: '^7.1.3' }), - }, - { - title: 'Packagist PHP Version Support (specify version)', - pattern: ':user/:repo/:version', - namedParams: { - user: 'symfony', - repo: 'symfony', - version: 'v2.8.0', - }, - staticPreview: this.render({ php: '>=5.3.9' }), - }, - { - title: 'Packagist PHP Version Support (custom server)', - pattern: ':user/:repo', - namedParams: { - user: 'symfony', - repo: 'symfony', - }, - queryParams: { - server: 'https://packagist.org', - }, - staticPreview: this.render({ php: '^7.1.3' }), - documentation: customServerDocumentationFragment, - }, - ] - - static defaultBadgeData = { - label: 'php', - color: 'blue', - } - - static render({ php }) { - return { - message: php, - } - } - - findVersionIndex(json, version) { - return json.findIndex(v => v.version === version) - } - - async findSpecifiedVersion(json, user, repo, version, server) { - let release - - if ((release = json[this.findVersionIndex(json, version)])) { - return release - } else { - try { - const allData = await this.fetchDev({ - user, - repo, - schema: allVersionsSchema, - server, - }) - - const versions = BasePackagistService.expandPackageVersions( - allData, - this.getPackageName(user, repo) - ) - - return versions[this.findVersionIndex(versions, version)] - } catch (e) { - return release - } - } - } - - async getPhpVersion({ json, user, repo, version = '', server }) { - let packageVersion - const versions = BasePackagistService.expandPackageVersions( - json, - this.getPackageName(user, repo) - ) - - if (version === '') { - packageVersion = this.findLatestRelease(versions) - } else { - try { - packageVersion = await this.findSpecifiedVersion( - versions, - user, - repo, - version, - server - ) - } catch (e) { - packageVersion = null - } - } - - if (!packageVersion) { - throw new NotFound({ prettyMessage: 'invalid version' }) - } - - if (!packageVersion.require || !packageVersion.require.php) { - throw new NotFound({ prettyMessage: 'version requirement not found' }) - } - - return { phpVersion: packageVersion.require.php } - } - - async handle({ user, repo, version = '' }, { server }) { - const allData = await this.fetch({ - user, - repo, - schema: allVersionsSchema, - server, - }) - const { phpVersion } = await this.getPhpVersion({ - json: allData, - user, - repo, - version, - server, - }) - return this.constructor.render({ php: phpVersion }) - } -} + }, + transformPath: ({ user, repo }) => + `/packagist/dependency-v/${user}/${repo}/php`, + transformQueryParams: ({ version, server }) => ({ version, server }), + overrideTransformedQueryParams: true, + dateAdded: new Date('2022-09-07'), +}) diff --git a/services/packagist/packagist-php-version.spec.js b/services/packagist/packagist-php-version.spec.js deleted file mode 100644 index 9fc1bc150060c..0000000000000 --- a/services/packagist/packagist-php-version.spec.js +++ /dev/null @@ -1,115 +0,0 @@ -import { expect } from 'chai' -import PackagistPhpVersion from './packagist-php-version.service.js' - -describe('PackagistPhpVersion', function () { - const json = { - packages: { - 'frodo/the-one-package': [ - { - version: '3.0.0', - require: { php: '^7.4 || 8' }, - }, - { - version: '2.0.0', - require: { php: '^7.2' }, - }, - { - version: '1.0.0', - require: { php: '^5.6 || ^7' }, - }, - ], - }, - } - - it('should throw NotFound when package version is missing', async function () { - await expect( - PackagistPhpVersion.prototype.getPhpVersion({ - json, - user: 'frodo', - repo: 'the-one-package', - version: '4.0.0', - }) - ).to.be.rejectedWith('invalid version') - }) - - it('should throw NotFound when PHP version not found on package when using default release', async function () { - const specJson = { - packages: { - 'frodo/the-one-package': [ - { - version: '3.0.0', - }, - { - version: '2.0.0', - require: { php: '^7.2' }, - }, - { - version: '1.0.0', - require: { php: '^5.6 || ^7' }, - }, - ], - }, - } - await expect( - PackagistPhpVersion.prototype.getPhpVersion({ - json: specJson, - user: 'frodo', - repo: 'the-one-package', - }) - ).to.be.rejectedWith('version requirement not found') - }) - - it('should throw NotFound when PHP version not found on package when using specified release', async function () { - const specJson = { - packages: { - 'frodo/the-one-package': [ - { - version: '3.0.0', - require: { php: '^7.4 || 8' }, - }, - { - version: '2.0.0', - require: { php: '^7.2' }, - }, - { - version: '1.0.0', - require: '__unset', - }, - ], - }, - } - await expect( - PackagistPhpVersion.prototype.getPhpVersion({ - json: specJson, - user: 'frodo', - repo: 'the-one-package', - version: '1.0.0', - }) - ).to.be.rejectedWith('version requirement not found') - }) - - it('should return PHP version for the default release', async function () { - expect( - await PackagistPhpVersion.prototype.getPhpVersion({ - json, - user: 'frodo', - repo: 'the-one-package', - }) - ) - .to.have.property('phpVersion') - .that.equals('^7.4 || 8') - }) - - it('should return PHP version for the specified release', async function () { - expect( - await PackagistPhpVersion.prototype.getPhpVersion({ - json, - user: 'frodo', - repo: 'the-one-package', - version: '2.0.0', - }) - ) - .to.have.property('phpVersion') - .that.equals('^7.2') - }) -}) diff --git a/services/packagist/packagist-php-version.tester.js b/services/packagist/packagist-php-version.tester.js index d552985228ad9..24d4c73e9f671 100644 --- a/services/packagist/packagist-php-version.tester.js +++ b/services/packagist/packagist-php-version.tester.js @@ -1,35 +1,24 @@ -import { isComposerVersion } from '../test-validators.js' import { createServiceTester } from '../tester.js' export const t = await createServiceTester() -t.create('gets the package version of symfony') +t.create( + 'redirect getting required php version for the dependency from packagist (valid, package version not specified in request)' +) .get('/symfony/symfony.json') - .expectBadge({ label: 'php', message: isComposerVersion }) - -t.create('gets the package version of symfony 5.2.3') - .get('/symfony/symfony/v5.2.3.json') - .expectBadge({ label: 'php', message: isComposerVersion }) - -t.create('package with no requirements') - .get('/bpampuch/pdfmake.json') - .expectBadge({ label: 'php', message: 'version requirement not found' }) - -t.create('package with no php version requirement') - .get('/raulfraile/ladybug-theme-modern.json') - .expectBadge({ label: 'php', message: 'version requirement not found' }) - -t.create('invalid package name') - .get('/frodo/is-not-a-package.json') - .expectBadge({ label: 'php', message: 'not found' }) - -t.create('invalid version') - .get('/symfony/symfony/invalid.json') - .expectBadge({ label: 'php', message: 'invalid version' }) - -t.create('custom server') - .get('/symfony/symfony.json?server=https%3A%2F%2Fpackagist.org') - .expectBadge({ label: 'php', message: isComposerVersion }) - -t.create('invalid custom server') - .get('/symfony/symfony.json?server=https%3A%2F%2Fpackagist.com') - .expectBadge({ label: 'php', message: 'not found' }) + .expectRedirect('/packagist/dependency-v/symfony/symfony/php.json?') + +t.create( + 'redirect getting required php version for the dependency from packagist (valid, package version specified in request)' +) + .get('/symfony/symfony/v3.2.8.json') + .expectRedirect( + '/packagist/dependency-v/symfony/symfony/php.json?version=v3.2.8' + ) + +t.create( + 'redirect getting required php version for the dependency from packagist (valid, package version and server specified in request)' +) + .get('/symfony/symfony/v3.2.8.json?server=https://packagist.org') + .expectRedirect( + '/packagist/dependency-v/symfony/symfony/php.json?server=https%3A%2F%2Fpackagist.org&version=v3.2.8' + ) diff --git a/services/test-validators.js b/services/test-validators.js index ad1c131204f0a..148f471ba254c 100644 --- a/services/test-validators.js +++ b/services/test-validators.js @@ -43,7 +43,7 @@ const isVPlusDottedVersionNClausesWithOptionalSuffixAndEpoch = withRegex( // https://getcomposer.org/doc/04-schema.md#package-links // https://getcomposer.org/doc/04-schema.md#minimum-stability const isComposerVersion = withRegex( - /^\s*(>=|>|<|<=|!=|\^|~)?\d+(\.(\*|(\d+(\.(\d+|\*))?)))?((\s*\|\|)?\s*(>=|>|<|<=|!=|\^|~)?\d+(\.(\*|(\d+(\.(\d+|\*))?)))?)*\s*$/ + /^\*|(\s*(>=|>|<|<=|!=|\^|~)?\d+(\.(\*|(\d+(\.(\d+|\*))?)))?((\s*\|*)?\s*(>=|>|<|<=|!=|\^|~)?\d+(\.(\*|(\d+(\.(\d+|\*))?)))?)*\s*)$/ ) // Regex for validate php-version.versionReduction()