diff --git a/.gitignore b/.gitignore index c6178a65..6884decd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /.idea/ /.mage/ /.vscode/ +/public/base-packages/ /public/p/ /public/p2/ /public/satis/ diff --git a/.mage.yml b/.mage.yml index c1609b5d..0e81427a 100644 --- a/.mage.yml +++ b/.mage.yml @@ -11,6 +11,7 @@ magephp: - ./public/packages.json - ./stubs - ./tests + - ./tmp - ./tools - ./var - ./LICENSE @@ -30,8 +31,10 @@ magephp: - exec: { cmd: "echo \"APP_ENV=prod\" >> .env.local", desc: "Create .env.local" } - composer/install: { flags: '--no-dev --no-progress --optimize-autoloader' } on-deploy: + - exec: { cmd: 'mkdir -p tmp/base-packages', desc: 'Create tmp/base-packages folder' } - exec: { cmd: 'mkdir -p var', desc: 'Create var folder' } - exec: { cmd: 'mkdir -p var/satis', desc: 'Create var/satis folder' } + - exec: { cmd: 'test -d ~/site/shared/base-packages || mkdir -p ~/site/shared/base-packages', desc: 'Create shared/base-packages folder' } - exec: { cmd: 'test -d ~/site/shared/public/satis || mkdir -p ~/site/shared/public/satis', desc: 'Create shared/public/satis folder' } - fs/link: { from: "../../../shared/.env.local", to: ".env.local" } - fs/link: { from: "../../../../shared/public/satis", to: "public/satis" } @@ -39,6 +42,7 @@ magephp: - fs/link: { from: "satis/p2", to: "public/p2" } - fs/link: { from: "satis/aliases.json", to: "public/aliases.json" } - fs/link: { from: "satis/packages.json", to: "public/packages.json" } + - fs/link: { from: "../../../../../shared/base-packages", to: "tmp/base-packages/packages" } - exec: { cmd: "sqlite3 ~/site/mage/current/var/gettr.db '.backup var/gettr.db'", desc: "Copy DB" } - exec: { cmd: "php-restart", desc: "Restart PHP and reset OPcache" } - exec: { cmd: "php ./bin/console doctrine:migrations:sync-metadata-storage --no-interaction", desc: "Synchronize DB Migrations" } diff --git a/cnf/nginx.conf b/cnf/nginx.conf index 327f2f4f..eea84713 100644 --- a/cnf/nginx.conf +++ b/cnf/nginx.conf @@ -54,6 +54,9 @@ location ~ (?!^\/\.well-known)\/\. { allow 213.144.157.127; allow 2001:1620:c7f:111::/64; +# Allow GitHub for webhook testing +allow 140.82.115.0/24; + # Block abusing IPs deny 212.95.122.212; diff --git a/composer.json b/composer.json index 1bfdd2ed..fb26dbb3 100644 --- a/composer.json +++ b/composer.json @@ -34,78 +34,84 @@ "ext-ctype": "*", "ext-iconv": "*", "ext-json": "*", + "ext-mbstring": "*", "ext-sqlite3": "*", + "ext-zip": "*", "ext-zlib": "*", - "composer/semver": "^3.3", - "doctrine/collections": "^1.6", - "doctrine/dbal": "^3.3", - "doctrine/doctrine-bundle": "^2.7", - "doctrine/doctrine-migrations-bundle": "^3.2", - "doctrine/inflector": "^2.0", - "doctrine/orm": "^2.12", - "doctrine/persistence": "^2.5", - "easycorp/easyadmin-bundle": "^4.3", - "erusev/parsedown": "^1.7", - "guzzlehttp/guzzle": "^7.4", - "guzzlehttp/promises": "^1.5", - "jms/serializer": "^3.17", - "jms/serializer-bundle": "^4.0.2", + "composer/composer": "^2.5.5", + "composer/semver": "^3.3.2", + "doctrine/collections": "^1.8", + "doctrine/dbal": "^3.6.2", + "doctrine/doctrine-bundle": "^2.9.1", + "doctrine/doctrine-migrations-bundle": "^3.2.2", + "doctrine/inflector": "^2.0.6", + "doctrine/orm": "^2.14.3", + "doctrine/persistence": "^2.5.7", + "easycorp/easyadmin-bundle": "^4.6.1", + "erusev/parsedown": "^1.7.4", + "guzzlehttp/guzzle": "^7.5.1", + "guzzlehttp/promises": "^1.5.2", + "jms/serializer": "^3.23", + "jms/serializer-bundle": "^4.2.0", "knplabs/knp-menu": "^3.3", - "nelmio/api-doc-bundle": "^4.9", - "nelmio/cors-bundle": "^2.2", - "nelmio/security-bundle": "^2.5", + "nelmio/api-doc-bundle": "^4.11.1", + "nelmio/cors-bundle": "^2.3.1", + "nelmio/security-bundle": "^2.12", "psr/log": "^2.0", - "symfony/asset": "^6.2", - "symfony/cache": "^6.2", - "symfony/cache-contracts": "^2.5", - "symfony/console": "^6.2", - "symfony/dependency-injection": "^6.2", - "symfony/dotenv": "^6.2", - "symfony/expression-language": "^6.2", - "symfony/flex": "^2.2", - "symfony/form": "^6.2", - "symfony/framework-bundle": "^6.2", - "symfony/http-client": "^6.2", - "symfony/http-foundation": "^6.2", - "symfony/http-kernel": "^6.2", - "symfony/intl": "^6.2", - "symfony/mailer": "^6.2", - "symfony/mime": "^6.2", - "symfony/monolog-bundle": "^3.1", - "symfony/notifier": "^6.2", - "symfony/process": "^6.2", - "symfony/property-access": "^6.2", - "symfony/property-info": "^6.2", - "symfony/proxy-manager-bridge": "^6.2", - "symfony/routing": "^6.2", - "symfony/runtime": "^6.2", - "symfony/security-bundle": "^6.2", - "symfony/security-http": "^6.2", - "symfony/serializer": "^6.2", - "symfony/translation": "^6.2", - "symfony/twig-bundle": "^6.2", - "symfony/validator": "^6.2", - "symfony/web-link": "^6.2", - "symfony/yaml": "^6.2", - "t3g/symfony-template-bundle": "^3.4", + "symfony/asset": "^6.2.7", + "symfony/cache": "^6.2.8", + "symfony/cache-contracts": "^2.5.2", + "symfony/console": "^6.2.8", + "symfony/dependency-injection": "^6.2.8", + "symfony/dotenv": "^6.2.8", + "symfony/expression-language": "^6.2.7", + "symfony/filesystem": "^6.2.7", + "symfony/finder": "^6.2.7", + "symfony/flex": "^2.2.5", + "symfony/form": "^6.2.8", + "symfony/framework-bundle": "^6.2.9", + "symfony/http-client": "^6.2.9", + "symfony/http-foundation": "^6.2.8", + "symfony/http-kernel": "^6.2.9", + "symfony/intl": "^6.2.9", + "symfony/mailer": "^6.2.8", + "symfony/mime": "^6.2.7", + "symfony/monolog-bundle": "^3.8", + "symfony/notifier": "^6.2.8", + "symfony/options-resolver": "^6.2.7", + "symfony/process": "^6.2.8", + "symfony/property-access": "^6.2.8", + "symfony/property-info": "^6.2.8", + "symfony/proxy-manager-bridge": "^6.2.7", + "symfony/routing": "^6.2.8", + "symfony/runtime": "^6.2.8", + "symfony/security-bundle": "^6.2.8", + "symfony/security-http": "^6.2.8", + "symfony/serializer": "^6.2.8", + "symfony/translation": "^6.2.8", + "symfony/twig-bundle": "^6.2.7", + "symfony/validator": "^6.2.8", + "symfony/web-link": "^6.2.7", + "symfony/yaml": "^6.2.7", + "t3g/symfony-template-bundle": "^3.5", "t3g/symfony-usercentrics-bundle": "^1.0.2 || dev-master", - "twig/extra-bundle": "^2.12 || ^3.0", - "twig/twig": "^2.12 || ^3.0" + "twig/extra-bundle": "^2.12 || ^3.5.1", + "twig/twig": "^2.12 || ^3.5.1" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8", - "doctrine/doctrine-fixtures-bundle": "^3.4", - "ergebnis/composer-normalize": "^2.28", - "fakerphp/faker": "^1.20", + "bamarni/composer-bin-plugin": "^1.8.2", + "doctrine/doctrine-fixtures-bundle": "^3.4.3", + "ergebnis/composer-normalize": "^2.30.2", + "fakerphp/faker": "^1.21", "roave/security-advisories": "dev-latest", - "symfony/browser-kit": "^6.2", - "symfony/css-selector": "^6.2", - "symfony/debug-bundle": "^6.2", - "symfony/maker-bundle": "^1.45", - "symfony/panther": "^2.0", - "symfony/stopwatch": "^6.2", - "symfony/var-dumper": "^6.2", - "symfony/web-profiler-bundle": "^6.2" + "symfony/browser-kit": "^6.2.7", + "symfony/css-selector": "^6.2.7", + "symfony/debug-bundle": "^6.2.7", + "symfony/maker-bundle": "^1.48", + "symfony/panther": "^2.0.1", + "symfony/stopwatch": "^6.2.7", + "symfony/var-dumper": "^6.2.8", + "symfony/web-profiler-bundle": "^6.2.7" }, "replace": { "paragonie/random_compat": "2.*", diff --git a/composer.lock b/composer.lock index 0ee4eacc..b4639770 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8e1b5f08cabf42768a73e907373eb415", + "content-hash": "1ddf83ba22b6b292386b8ac262f094da", "packages": [ { "name": "composer/ca-bundle", @@ -82,6 +82,332 @@ ], "time": "2023-01-11T08:27:00+00:00" }, + { + "name": "composer/class-map-generator", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/class-map-generator.git", + "reference": "1e1cb2b791facb2dfe32932a7718cf2571187513" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/1e1cb2b791facb2dfe32932a7718cf2571187513", + "reference": "1e1cb2b791facb2dfe32932a7718cf2571187513", + "shasum": "" + }, + "require": { + "composer/pcre": "^2 || ^3", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6" + }, + "require-dev": { + "phpstan/phpstan": "^1.6", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/filesystem": "^5.4 || ^6", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\ClassMapGenerator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Utilities to scan PHP code and generate class maps.", + "keywords": [ + "classmap" + ], + "support": { + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.0.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-06-19T11:31:27+00:00" + }, + { + "name": "composer/composer", + "version": "2.5.5", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "c7cffaad16a60636a776017eac5bd8cd0095c32f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/c7cffaad16a60636a776017eac5bd8cd0095c32f", + "reference": "c7cffaad16a60636a776017eac5bd8cd0095c32f", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "composer/class-map-generator": "^1.0", + "composer/metadata-minifier": "^1.0", + "composer/pcre": "^2.1 || ^3.1", + "composer/semver": "^3.0", + "composer/spdx-licenses": "^1.5.7", + "composer/xdebug-handler": "^2.0.2 || ^3.0.3", + "justinrainbow/json-schema": "^5.2.11", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "react/promise": "^2.8", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.2", + "seld/signal-handler": "^2.0", + "symfony/console": "^5.4.11 || ^6.0.11", + "symfony/filesystem": "^5.4 || ^6.0", + "symfony/finder": "^5.4 || ^6.0", + "symfony/polyfill-php73": "^1.24", + "symfony/polyfill-php80": "^1.24", + "symfony/polyfill-php81": "^1.24", + "symfony/process": "^5.4 || ^6.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.9.3", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1", + "phpstan/phpstan-symfony": "^1.2.10", + "symfony/phpunit-bridge": "^6.0" + }, + "suggest": { + "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "phpstan": { + "includes": [ + "phpstan/rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "https://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/composer/issues", + "source": "https://github.com/composer/composer/tree/2.5.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-03-21T10:50:05+00:00" + }, + { + "name": "composer/metadata-minifier", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/metadata-minifier.git", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/c549d23829536f0d0e984aaabbf02af91f443207", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2", + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\MetadataMinifier\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Small utility library that handles metadata minification and expansion.", + "keywords": [ + "composer", + "compression" + ], + "support": { + "issues": "https://github.com/composer/metadata-minifier/issues", + "source": "https://github.com/composer/metadata-minifier/tree/1.0.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-04-07T13:37:33+00:00" + }, + { + "name": "composer/pcre", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-11-17T09:50:14+00:00" + }, { "name": "composer/semver", "version": "3.3.2", @@ -163,6 +489,152 @@ ], "time": "2022-04-01T19:23:25+00:00" }, + { + "name": "composer/spdx-licenses", + "version": "1.5.7", + "source": { + "type": "git", + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "c848241796da2abf65837d51dce1fae55a960149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/c848241796da2abf65837d51dce1fae55a960149", + "reference": "c848241796da2abf65837d51dce1fae55a960149", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Spdx\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "SPDX licenses list and validation library.", + "keywords": [ + "license", + "spdx", + "validator" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/spdx-licenses/issues", + "source": "https://github.com/composer/spdx-licenses/tree/1.5.7" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-05-23T07:37:50+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T21:32:43+00:00" + }, { "name": "doctrine/annotations", "version": "2.0.1", @@ -2393,16 +2865,86 @@ "xml" ], "support": { - "issues": "https://github.com/schmittjoh/JMSSerializerBundle/issues", - "source": "https://github.com/schmittjoh/JMSSerializerBundle/tree/4.2.0" + "issues": "https://github.com/schmittjoh/JMSSerializerBundle/issues", + "source": "https://github.com/schmittjoh/JMSSerializerBundle/tree/4.2.0" + }, + "funding": [ + { + "url": "https://github.com/goetas", + "type": "github" + } + ], + "time": "2022-09-13T19:27:18+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "5.2.12", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "ad87d5a5ca981228e0e205c2bc7dfb8e24559b60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/ad87d5a5ca981228e0e205c2bc7dfb8e24559b60", + "reference": "ad87d5a5ca981228e0e205c2bc7dfb8e24559b60", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/5.2.12" }, - "funding": [ - { - "url": "https://github.com/goetas", - "type": "github" - } - ], - "time": "2022-09-13T19:27:18+00:00" + "time": "2022-04-13T08:02:27+00:00" }, { "name": "knplabs/knp-components", @@ -3780,6 +4322,255 @@ }, "time": "2019-03-08T08:55:37+00:00" }, + { + "name": "react/promise", + "version": "v2.9.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/234f8fd1023c9158e2314fa9d7d0e6a83db42910", + "reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v2.9.0" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-02-11T10:27:51+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "4211420d25eba80712bff236a98960ef68b866b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/4211420d25eba80712bff236a98960ef68b866b7", + "reference": "4211420d25eba80712bff236a98960ef68b866b7", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2022-04-01T13:37:23+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phar" + ], + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1" + }, + "time": "2022-08-31T10:31:18+00:00" + }, + { + "name": "seld/signal-handler", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/signal-handler.git", + "reference": "f69d119511dc0360440cdbdaa71829c149b7be75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/signal-handler/zipball/f69d119511dc0360440cdbdaa71829c149b7be75", + "reference": "f69d119511dc0360440cdbdaa71829c149b7be75", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^7.5.20 || ^8.5.23", + "psr/log": "^1 || ^2 || ^3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\Signal\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Simple unix signal handler that silently fails where signals are not supported for easy cross-platform development", + "keywords": [ + "posix", + "sigint", + "signal", + "sigterm", + "unix" + ], + "support": { + "issues": "https://github.com/Seldaek/signal-handler/issues", + "source": "https://github.com/Seldaek/signal-handler/tree/2.0.1" + }, + "time": "2022-07-20T18:31:45+00:00" + }, { "name": "setasign/fpdi", "version": "v2.3.7", @@ -9675,76 +10466,6 @@ }, "time": "2022-12-13T13:54:32+00:00" }, - { - "name": "justinrainbow/json-schema", - "version": "5.2.12", - "source": { - "type": "git", - "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "ad87d5a5ca981228e0e205c2bc7dfb8e24559b60" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/ad87d5a5ca981228e0e205c2bc7dfb8e24559b60", - "reference": "ad87d5a5ca981228e0e205c2bc7dfb8e24559b60", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", - "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" - }, - "bin": [ - "bin/validate-json" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "JsonSchema\\": "src/JsonSchema/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bruno Prieto Reis", - "email": "bruno.p.reis@gmail.com" - }, - { - "name": "Justin Rainbow", - "email": "justin.rainbow@gmail.com" - }, - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - }, - { - "name": "Robert Schönthal", - "email": "seroscho@googlemail.com" - } - ], - "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", - "keywords": [ - "json", - "schema" - ], - "support": { - "issues": "https://github.com/justinrainbow/json-schema/issues", - "source": "https://github.com/justinrainbow/json-schema/tree/5.2.12" - }, - "time": "2022-04-13T08:02:27+00:00" - }, { "name": "localheinz/diff", "version": "1.1.1", @@ -9807,26 +10528,24 @@ }, { "name": "masterminds/html5", - "version": "2.7.6", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "897eb517a343a2281f11bc5556d6548db7d93947" + "reference": "3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/897eb517a343a2281f11bc5556d6548db7d93947", - "reference": "897eb517a343a2281f11bc5556d6548db7d93947", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3", + "reference": "3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-dom": "*", - "ext-libxml": "*", "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7" + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8" }, "type": "library", "extra": { @@ -9870,9 +10589,9 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.7.6" + "source": "https://github.com/Masterminds/html5-php/tree/2.8.0" }, - "time": "2022-08-18T16:18:26+00:00" + "time": "2023-04-26T07:27:39+00:00" }, { "name": "nikic/php-parser", @@ -10002,12 +10721,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "d2832e4594571d458e36fb4622220915a3c3a8f4" + "reference": "5eb9b0587e4625150e4892e569bb223714c86256" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/d2832e4594571d458e36fb4622220915a3c3a8f4", - "reference": "d2832e4594571d458e36fb4622220915a3c3a8f4", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/5eb9b0587e4625150e4892e569bb223714c86256", + "reference": "5eb9b0587e4625150e4892e569bb223714c86256", "shasum": "" }, "conflict": { @@ -10343,7 +11062,7 @@ "prestashop/blockwishlist": ">=2,<2.1.1", "prestashop/contactform": ">=1.0.1,<4.3", "prestashop/gamification": "<2.3.2", - "prestashop/prestashop": "<8.0.1", + "prestashop/prestashop": "<8.0.4", "prestashop/productcomments": "<5.0.2", "prestashop/ps_emailsubscription": "<2.6.1", "prestashop/ps_facetedsearch": "<3.4.1", @@ -10599,7 +11318,7 @@ "type": "tidelift" } ], - "time": "2023-04-25T13:05:55+00:00" + "time": "2023-04-25T20:04:17+00:00" }, { "name": "symfony/browser-kit", @@ -11160,7 +11879,9 @@ "ext-ctype": "*", "ext-iconv": "*", "ext-json": "*", + "ext-mbstring": "*", "ext-sqlite3": "*", + "ext-zip": "*", "ext-zlib": "*" }, "platform-dev": [], diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 7853e9ed..6a7e85e0 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -17,6 +17,9 @@ framework: php_errors: log: true + form: + legacy_error_messages: false + when@test: framework: test: true diff --git a/config/packages/nelmio_api_doc.yaml b/config/packages/nelmio_api_doc.yaml index 0b825b43..7246deb6 100644 --- a/config/packages/nelmio_api_doc.yaml +++ b/config/packages/nelmio_api_doc.yaml @@ -3,7 +3,7 @@ nelmio_api_doc: info: title: get.typo3.org description: REST API for getting information about TYPO3 releases - version: 1.0.0 + version: 1.1.0 components: securitySchemes: Basic: diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 8f4c7e42..5439e3ac 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -5,3 +5,5 @@ twig: packagist: search: https://packagist.org/explore/?type=typo3-cms-extension submit: https://packagist.org/packages/submit + form_themes: + - 'form/custom_theme.html.twig' diff --git a/config/services.yaml b/config/services.yaml index cd4c0610..b158a746 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -5,6 +5,8 @@ # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration parameters: app.domain: '%env(APP_DOMAIN)%' + app.base_packages_project_dir: 'tmp/base-packages' + app.base_packages_assets_dir: 'base-packages' services: # default configuration for services in *this* file @@ -17,10 +19,14 @@ services: App\: resource: '../src/' exclude: + - '../src/DataFixtures/' - '../src/DependencyInjection/' - '../src/Entity/' - - '../src/Kernel.php' + - '../src/Enum/' + - '../src/Exception/' + - '../src/Package/' - '../src/Tests/' + - '../src/Kernel.php' # controllers are imported separately to make sure services can be injected # as action arguments even if you don't extend any base controller class @@ -34,6 +40,9 @@ services: arguments: $appDomain: '%env(APP_DOMAIN)%' + App\EventListener\BasePackageListener: + tags: ['doctrine.orm.entity_listener'] + App\EventListener\MajorVersionListener: tags: ['doctrine.orm.entity_listener'] @@ -43,6 +52,17 @@ services: App\EventListener\RequirementListener: tags: ['doctrine.orm.entity_listener'] + App\Security\GitHubRequestChecker: + arguments: + $signingSecret: '%env(APP_WEBHOOK_SECRET_GITHUB)%' + + App\Service\BasePackageService: + arguments: + $projectDir: '%app.base_packages_project_dir%' + $assetsDir: '%app.base_packages_assets_dir%' + App\Service\CacheWarmupService: arguments: $baseUrl: '%env(BASE_URL)%' + + Composer\Console\Application: diff --git a/migrations/Version20220818095608.php b/migrations/Version20220818095608.php index 4f41860e..55f8a83f 100644 --- a/migrations/Version20220818095608.php +++ b/migrations/Version20220818095608.php @@ -33,7 +33,7 @@ final class Version20220818095608 extends AbstractMigration { public function getDescription(): string { - return ''; + return 'Add ID to Requirements'; } public function up(Schema $schema): void diff --git a/migrations/Version20220818183005.php b/migrations/Version20220818183005.php new file mode 100644 index 00000000..ad578d6c --- /dev/null +++ b/migrations/Version20220818183005.php @@ -0,0 +1,53 @@ +addSql('CREATE TABLE base_packages (name VARCHAR(255) NOT NULL, active BOOLEAN NOT NULL, official BOOLEAN NOT NULL, PRIMARY KEY(name))'); + $this->addSql('INSERT INTO base_packages (name, active, official) VALUES (?, ?, ?)', ['1' => 'typo3/bootstrap-package', '2' => 1, '3' => 1], ['1' => 2, '2' => 5, '3' => 5]); + $this->addSql('INSERT INTO base_packages (name, active, official) VALUES (?, ?, ?)', ['1' => 'typo3/fluid-styled-content', '2' => 1, '3' => 1], ['1' => 2, '2' => 5, '3' => 5]); + $this->addSql('INSERT INTO base_packages (name, active, official) VALUES (?, ?, ?)', ['1' => 'typo3/introduction-package', '2' => 1, '3' => 0], ['1' => 2, '2' => 5, '3' => 5]); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE base_packages'); + } +} diff --git a/public/assets/Css/additions.css b/public/assets/Css/additions.css index 1d507832..7e21c392 100644 --- a/public/assets/Css/additions.css +++ b/public/assets/Css/additions.css @@ -81,3 +81,11 @@ pre { .frame .swagger-ui .auth-wrapper .authorize { margin-right: 0; } + + +/* BasePackage List */ +.basepackage-official-badge { + position: absolute; + right: 0.75em; + transform: translate(0, 25%); +} diff --git a/public/assets/Images/OfficialBadge.svg b/public/assets/Images/OfficialBadge.svg new file mode 100644 index 00000000..829a4ac8 --- /dev/null +++ b/public/assets/Images/OfficialBadge.svg @@ -0,0 +1,92 @@ + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Command/ExtensionsTerJsonCreateCommand.php b/src/Command/ExtensionsTerJsonCreateCommand.php index 7a6bcf4f..33340dc1 100644 --- a/src/Command/ExtensionsTerJsonCreateCommand.php +++ b/src/Command/ExtensionsTerJsonCreateCommand.php @@ -423,9 +423,12 @@ protected function getPackageArray(SimpleXMLElement $extension, SimpleXMLElement ], ]; if ($version->composerinfo !== null) { - $composerInfo = json_decode((string)$version->composerinfo, true, 512); - if (is_array($composerInfo) && is_array($composerInfo['autoload'] ?? null)) { - $autoload = $composerInfo['autoload']; + try { + $composerInfo = json_decode((string)$version->composerinfo, true, 512, JSON_THROW_ON_ERROR); + if (is_array($composerInfo) && is_array($composerInfo['autoload'] ?? null)) { + $autoload = $composerInfo['autoload']; + } + } catch (\Throwable) { } } diff --git a/src/Controller/Admin/BasePackageCrudController.php b/src/Controller/Admin/BasePackageCrudController.php new file mode 100644 index 00000000..9d9934e5 --- /dev/null +++ b/src/Controller/Admin/BasePackageCrudController.php @@ -0,0 +1,68 @@ +add(Crud::PAGE_INDEX, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::DETAIL) + ->remove(Crud::PAGE_INDEX, Action::DELETE) + ; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add('name') + ->add('active') + ->add('official') + ; + } + + public function configureFields(string $pageName): iterable + { + return [ + TextField::new('name', 'Composer Package Name'), + BooleanField::new('active', 'Active'), + BooleanField::new('official', 'Official'), + ]; + } +} diff --git a/src/Controller/Admin/DashboardController.php b/src/Controller/Admin/DashboardController.php index 96d0b9de..c82c6bf0 100644 --- a/src/Controller/Admin/DashboardController.php +++ b/src/Controller/Admin/DashboardController.php @@ -23,6 +23,7 @@ namespace App\Controller\Admin; +use App\Entity\BasePackage; use App\Entity\MajorVersion; use App\Entity\Release; use App\Entity\Requirement; @@ -32,7 +33,6 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -use Iterator; #[IsGranted('ROLE_ADMIN')] class DashboardController extends AbstractDashboardController @@ -49,14 +49,12 @@ public function configureDashboard(): Dashboard ->setTitle('Administration'); } - /** - * @return Iterator<\EasyCorp\Bundle\EasyAdminBundle\Config\Menu\CrudMenuItem|\EasyCorp\Bundle\EasyAdminBundle\Config\Menu\DashboardMenuItem> - */ public function configureMenuItems(): iterable { yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home'); yield MenuItem::linkToCrud('Major Versions', 'fas fa-list', MajorVersion::class); yield MenuItem::linkToCrud('Requirements', 'fas fa-list', Requirement::class); yield MenuItem::linkToCrud('Releases', 'fas fa-list', Release::class); + yield MenuItem::linkToCrud('Base Packages', 'fas fa-list', BasePackage::class); } } diff --git a/src/Controller/Api/AbstractController.php b/src/Controller/Api/AbstractController.php index 8e567041..83d47961 100644 --- a/src/Controller/Api/AbstractController.php +++ b/src/Controller/Api/AbstractController.php @@ -36,17 +36,16 @@ use JMS\Serializer\SerializerInterface; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; use DateTime; use DateTimeImmutable; -use function iterator_apply; -use function iterator_to_array; use function is_string; abstract class AbstractController extends \Symfony\Bundle\FrameworkBundle\Controller\AbstractController { + use ValidationTrait; + public function __construct( private \Symfony\Contracts\Cache\TagAwareCacheInterface $cache, private \App\Service\CacheService $cacheService, @@ -106,26 +105,6 @@ protected function findMajorVersion(string $version): MajorVersion return $majorVersion; } - protected function validateObject(object $object): void - { - $violations = $this->validator->validate($object); - - if ($violations->count() > 0) { - $messages = ''; - - iterator_apply( - $violations, - static function (ConstraintViolationInterface $violation) use (&$messages): bool { - $messages .= \sprintf("%s: %s\n", $violation->getPropertyPath(), $violation->getMessage()); - return true; - }, - iterator_to_array($violations) - ); - - throw new BadRequestHttpException(trim($messages)); - } - } - /** * @param array $data */ diff --git a/src/Controller/Api/MajorVersion/RequirementsController.php b/src/Controller/Api/MajorVersion/RequirementsController.php index fd4441b3..68feca2b 100644 --- a/src/Controller/Api/MajorVersion/RequirementsController.php +++ b/src/Controller/Api/MajorVersion/RequirementsController.php @@ -233,7 +233,7 @@ public function updateRequirement(string $version, Request $request): JsonRespon ] ); if (!$entity instanceof Requirement) { - throw new NotFoundHttpException('Requirement does not exists'); + throw new NotFoundHttpException('Requirement does not exists.'); } $requirement->setVersion($entity->getVersion()); diff --git a/src/Controller/Api/SitepackageController.php b/src/Controller/Api/SitepackageController.php new file mode 100644 index 00000000..4055e2b7 --- /dev/null +++ b/src/Controller/Api/SitepackageController.php @@ -0,0 +1,114 @@ + 'json'])] +class SitepackageController extends AbstractController +{ + use ValidationTrait; + + public function __construct( + private \App\Service\SitepackageGenerator $sitepackageGenerator, + private \JMS\Serializer\SerializerInterface $serializer, + private \Symfony\Component\Validator\Validator\ValidatorInterface $validator, + ) { + } + + /** + * @OA\RequestBody( + * + * @Model(type=Sitepackage::class), + * request="sitepackage", + * required=true + * ) + * + * @OA\Response( + * response=200, + * description="Successfully generated.", + * + * @OA\Schema(type="file") + * ) + * + * @OA\Response( + * response=400, + * description="Request malformed." + * ) + * + * @OA\Tag(name="sitepackage") + */ + /* + #[OA\RequestBody( + //new Model(type: Sitepackage::class) + //ref: new OA\Schema(type: Sitepackage::class), + request: 'sitepackage', + required: true, + //new Model(type: Sitepackage::class) + content: new Model(type: Sitepackage::class) + //content: new OA\Schema(ref: new Model(type: Sitepackage::class)) + )] + #[OA\RequestBody(new Model(type: Sitepackage::class))] + #[OA\Response( + response: 200, + description: 'Successfully generated.', + //content: new OA\Schema(type: 'file') + content: new OA\MediaType(mediaType: 'application/zip') + )] + #[OA\Response( + response: 400, + description: 'Request malformed.' + )] + #[OA\Tag(name: 'sitepackage')] + */ + #[Route(path: '/create', methods: ['POST'])] + public function createSitepackage(Request $request): BinaryFileResponse + { + if (!is_string($content = $request->getContent())) { + throw new BadRequestHttpException('Missing or invalid request body.'); + } + + /** @var Sitepackage $sitepackage */ + $sitepackage = $this->serializer->deserialize($content, Sitepackage::class, 'json'); + $this->validateObject($sitepackage); + $this->sitepackageGenerator->create($sitepackage); + $filename = $this->sitepackageGenerator->getFilename(); + BinaryFileResponse::trustXSendfileTypeHeader(); + return $this + ->file($this->sitepackageGenerator->getZipPath(), StringUtility::toASCII($filename)) + ->deleteFileAfterSend(true); + } +} diff --git a/src/Controller/Api/ValidationTrait.php b/src/Controller/Api/ValidationTrait.php new file mode 100644 index 00000000..19755071 --- /dev/null +++ b/src/Controller/Api/ValidationTrait.php @@ -0,0 +1,54 @@ +validator->validate($object); + + if ($violations->count() > 0) { + $messages = ''; + + iterator_apply( + $violations, + static function (ConstraintViolationInterface $violation) use (&$messages): bool { + $messages .= sprintf("%s: %s\n", $violation->getPropertyPath(), $violation->getMessage()); + return true; + }, + iterator_to_array($violations) + ); + + throw new BadRequestHttpException(trim($messages)); + } + } +} diff --git a/src/Controller/EntryPointController.php b/src/Controller/EntryPointController.php index ccff285d..d18a173f 100644 --- a/src/Controller/EntryPointController.php +++ b/src/Controller/EntryPointController.php @@ -36,6 +36,8 @@ class EntryPointController extends AbstractController private const ENTRY_POINTS = [ // 'slug' => 'route', 'composer-helper' => 'composer-helper', + 'sitepackage' => 'tools_sitepackage', + 'sitepackage-builder' => 'tools_sitepackage', ]; #[Route('/go/{slug}', requirements: ['slug' => '.+'])] diff --git a/src/Controller/Tools/SitepackageController.php b/src/Controller/Tools/SitepackageController.php new file mode 100644 index 00000000..32312bcf --- /dev/null +++ b/src/Controller/Tools/SitepackageController.php @@ -0,0 +1,269 @@ +render( + 'tools/sitepackage/index.html.twig' + ); + } + + #[Route(path: '/new', name: 'tools_sitepackage_new')] + #[Route(path: '/new/{vendor}/{project}', name: 'tools_sitepackage_new')] + public function new(string $vendor = '', string $project = ''): Response + { + $this->setSitepackageConfig(new SitepackageDto(), false); + + $filtered = false; + $basePackages = $this->basePackageService->getBasePackages(); + + if ($vendor !== '' && $project !== '') { + $basePackageName = sprintf('%s/%s', $vendor, $project); + + try { + $basePackage = $this->basePackageService->checkAndInstallMissingBasePackage($basePackageName); + $basePackages = [$basePackage]; + $filtered = true; + } catch (Throwable $throwable) { + $this->addFlash( + 'fatal', + $throwable->getMessage() + ); + return $this->redirectToRoute('tools_sitepackage_new'); + } + } + + return $this->render( + 'tools/sitepackage/new.html.twig', + [ + 'basePackages' => $basePackages, + 'filtered' => $filtered, + ] + ); + } + + #[Route(path: '/detail/{vendor}/{project}', name: 'tools_sitepackage_detail')] + #[Route(path: '/detail/{vendor}/{project}/{typo3Version}', name: 'tools_sitepackage_detail')] + public function detail(string $vendor, string $project, string $typo3Version = ''): Response + { + $this->setSitepackageConfig(new SitepackageDto(), false); + + $basePackageName = sprintf('%s/%s', $vendor, $project); + + try { + $basePackage = $this->basePackageService->checkAndInstallMissingBasePackage($basePackageName); + + if ($typo3Version === '') { + $typo3Version = $basePackage->getTypo3Versions()[0]; + } + + $basePackageTemplate = $basePackage->getTemplates()[$typo3Version]; + } catch (Throwable $throwable) { + $this->addFlash( + 'fatal', + $throwable->getMessage() + ); + return $this->redirectToRoute('tools_sitepackage_new'); + } + + return $this->render( + 'tools/sitepackage/detail.html.twig', + [ + 'basePackage' => $basePackage, + 'basePackageTemplate' => $basePackageTemplate, + 'typo3Version' => $typo3Version, + ] + ); + } + + #[Route(path: '/validate/{vendor}/{project}', name: 'tools_sitepackage_validate')] + public function validate(string $vendor, string $project): RedirectResponse + { + try { + $configuration = $this->getSitepackageConfig(); + } catch (UnexpectedValueException) { + return $this->redirectToRoute('tools_sitepackage_new'); + } + + $configuration->basePackage = sprintf('%s/%s', $vendor, $project); + + try { + $basePackage = $this->basePackageService->checkAndInstallMissingBasePackage($configuration->basePackage); + $configuration->typo3Version = VersionUtility::versionToInt($basePackage->getTypo3Versions()[0]); + } catch (Throwable $throwable) { + $this->addFlash( + 'fatal', + $throwable->getMessage() + ); + return $this->redirectToRoute('tools_sitepackage_new'); + } + + $this->setSitepackageConfig($configuration, $this->isAdvancedSitepackageConfig()); + + return $this->redirectToRoute('tools_sitepackage_configure'); + } + + #[Route(path: '/configure', name: 'tools_sitepackage_configure')] + public function configure(Request $request): Response + { + try { + $configuration = $this->getSitepackageConfig(); + } catch (UnexpectedValueException) { + return $this->redirectToRoute('tools_sitepackage_new'); + } + + $form = $this->createForm( + SitepackageType::class, + $configuration, + [ + 'action' => $this->generateUrl('tools_sitepackage_configure'), + 'advanced' => $this->isAdvancedSitepackageConfig(), + ] + ); + $form->handleRequest($request); + + if ($form->isSubmitted()) { + if ( + $form->has('simple') && + ($simple = $form->get('simple')) instanceof SubmitButton && $simple->isClicked() + ) { + $this->setSitepackageConfig($configuration, false); + + return $this->redirectToRoute('tools_sitepackage_configure'); + } + + if ( + $form->has('advanced') && + ($advanced = $form->get('advanced')) instanceof SubmitButton && $advanced->isClicked() + ) { + $this->setSitepackageConfig($configuration, true); + + return $this->redirectToRoute('tools_sitepackage_configure'); + } + + if ($form->isValid()) { + $this->setSitepackageConfig($configuration, $this->isAdvancedSitepackageConfig()); + + return $this->redirectToRoute('tools_sitepackage_success'); + } + } + + return $this->render( + 'tools/sitepackage/configure.html.twig', + [ + 'form' => $form->createView(), + ] + ); + } + + #[Route(path: '/success', name: 'tools_sitepackage_success')] + public function success(): Response + { + try { + $sitepackage = SitepackageFactory::fromDto($this->getSitepackageConfig()); + } catch (UnexpectedValueException) { + return $this->redirectToRoute('tools_sitepackage_new'); + } + + $this->setSitepackage($sitepackage); + + return $this->render( + 'tools/sitepackage/success.html.twig', + [ + 'base_package' => $this->basePackageService->getInstalledBasePackage($sitepackage->getBasePackage()), + 'sitepackage' => $sitepackage, + ] + ); + } + + #[Route(path: '/download', name: 'tools_sitepackage_download')] + public function download(): Response + { + try { + $sitepackage = $this->getSitepackage(); + } catch (UnexpectedValueException) { + return $this->redirectToRoute('tools_sitepackage_new'); + } + + try { + $this->sitepackageGenerator->create($sitepackage); + } catch (RuntimeError $runtimeError) { + $this->setSitepackageError($runtimeError->getMessage()); + + return $this->redirectToRoute('tools_sitepackage_error'); + } + + BinaryFileResponse::trustXSendfileTypeHeader(); + + return $this + ->file( + $this->sitepackageGenerator->getZipPath(), + StringUtility::toASCII($this->sitepackageGenerator->getFilename()) + ) + ->deleteFileAfterSend(true); + } + + #[Route(path: '/error', name: 'tools_sitepackage_error')] + public function error(): Response + { + return $this->render( + 'tools/sitepackage/error.html.twig', + [ + 'error' => $this->getSitepackageError(), + ] + ); + } +} diff --git a/src/Controller/Webhooks/GitHubWebhookController.php b/src/Controller/Webhooks/GitHubWebhookController.php new file mode 100644 index 00000000..6a45f3cd --- /dev/null +++ b/src/Controller/Webhooks/GitHubWebhookController.php @@ -0,0 +1,70 @@ + 'json'])] +class GitHubWebhookController extends AbstractController +{ + public function __construct( + private readonly GitHubEventFactory $gitHubEventFactory, + private readonly GitHubRequestChecker $gitHubRequestChecker, + private \App\Service\BasePackageService $basePackageService + ) { + } + + #[Route(path: '', name: 'github_endpoint', methods: ['POST'])] + public function endPoint(Request $request): Response + { + try { + $this->gitHubRequestChecker->checkRequestSanity($request); + } catch (Throwable $throwable) { + // GitHub requires a successful (2xx) status in every case see + // https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#authednotify + return new Response($throwable->getMessage(), Response::HTTP_ACCEPTED); + } + + $gitHubEvent = $this->gitHubEventFactory->buildFromRequest($request); + + switch ($gitHubEvent->getType()) { + case 'ping': + return new Response('Ping received.', Response::HTTP_OK); + + case 'push': + $this->basePackageService->updatePackageRepository(); + return new Response('Package repository updated.', Response::HTTP_OK); + + default: + throw new UnknownGitHubEventTypeException($request, $gitHubEvent->getType(), 1_661_157_800); + } + } +} diff --git a/src/Entity/BasePackage.php b/src/Entity/BasePackage.php new file mode 100644 index 00000000..9e02d348 --- /dev/null +++ b/src/Entity/BasePackage.php @@ -0,0 +1,114 @@ +name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function isActive(): bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } + + public function isOfficial(): bool + { + return $this->official; + } + + public function setOfficial(bool $official): self + { + $this->official = $official; + + return $this; + } + + /** + * @return array{name: string, active: bool, official: bool} + */ + public function jsonSerialize(): array + { + return [ + 'name' => $this->getName(), + 'active' => $this->isActive(), + 'official' => $this->isOfficial(), + ]; + } + + public function __toString(): string + { + return $this->getName(); + } +} diff --git a/src/Enum/ReleaseTypeEnum.php b/src/Enum/ReleaseTypeEnum.php index cd2d3815..b3ba2ce8 100644 --- a/src/Enum/ReleaseTypeEnum.php +++ b/src/Enum/ReleaseTypeEnum.php @@ -40,9 +40,6 @@ final class ReleaseTypeEnum extends AbstractEnum */ public const OPTION_SECURITY = 'security'; - /** - * @inheritDoc - */ protected static array $optionNames = [ self::OPTION_REGULAR => 'Regular', self::OPTION_DEVELOPMENT => 'Development', diff --git a/src/Enum/RequirementCategoryEnum.php b/src/Enum/RequirementCategoryEnum.php index bc1c9a99..a2e20cfc 100644 --- a/src/Enum/RequirementCategoryEnum.php +++ b/src/Enum/RequirementCategoryEnum.php @@ -50,9 +50,6 @@ final class RequirementCategoryEnum extends AbstractEnum */ public const OPTION_COMPOSER = 'composer'; - /** - * @inheritDoc - */ protected static array $optionNames = [ self::OPTION_PHP => 'PHP', self::OPTION_DATABASE => 'Database', diff --git a/src/Event/GitHubEvent.php b/src/Event/GitHubEvent.php new file mode 100644 index 00000000..1f449024 --- /dev/null +++ b/src/Event/GitHubEvent.php @@ -0,0 +1,47 @@ +type; + } + + /** + * @return mixed[] + */ + public function getPayload(): array + { + return $this->payload; + } +} diff --git a/src/EventListener/BasePackageListener.php b/src/EventListener/BasePackageListener.php new file mode 100644 index 00000000..8f5ea560 --- /dev/null +++ b/src/EventListener/BasePackageListener.php @@ -0,0 +1,49 @@ +basePackageService->resetCache(); + } + + public function postRemove(): void + { + $this->basePackageService->resetCache(); + } + + public function postPersist(): void + { + $this->basePackageService->resetCache(); + } +} diff --git a/src/Exception/GitHubWebhookException.php b/src/Exception/GitHubWebhookException.php new file mode 100644 index 00000000..4acde40e --- /dev/null +++ b/src/Exception/GitHubWebhookException.php @@ -0,0 +1,45 @@ +request; + } +} diff --git a/src/Exception/IncompatiblePackageException.php b/src/Exception/IncompatiblePackageException.php new file mode 100644 index 00000000..279d9610 --- /dev/null +++ b/src/Exception/IncompatiblePackageException.php @@ -0,0 +1,30 @@ +getRequest()->getContent(); + } +} diff --git a/src/Exception/InvalidGitHubRequestSignatureException.php b/src/Exception/InvalidGitHubRequestSignatureException.php new file mode 100644 index 00000000..ba725179 --- /dev/null +++ b/src/Exception/InvalidGitHubRequestSignatureException.php @@ -0,0 +1,49 @@ +signature; + } +} diff --git a/src/Exception/MissingGitHubEventTypeException.php b/src/Exception/MissingGitHubEventTypeException.php new file mode 100644 index 00000000..91a999ad --- /dev/null +++ b/src/Exception/MissingGitHubEventTypeException.php @@ -0,0 +1,43 @@ +eventType), + $code, + $previous + ); + } +} diff --git a/src/Factory/GitHubEventFactory.php b/src/Factory/GitHubEventFactory.php new file mode 100644 index 00000000..cb1fc5d2 --- /dev/null +++ b/src/Factory/GitHubEventFactory.php @@ -0,0 +1,60 @@ +headers->get('X-GitHub-Event')) === null) { + throw new MissingGitHubEventTypeException($request); + } + + $content = $request->getContent(); + + try { + /** @var array $payload */ + $payload = json_decode($content, true, 512, JSON_THROW_ON_ERROR); + } catch (Throwable $throwable) { + throw new InvalidGitHubRequestPayloadException($request, 1_661_155_479, $throwable); + } + + return $this->build($eventType, $payload); + } +} diff --git a/src/Factory/GitHubEventFactoryInterface.php b/src/Factory/GitHubEventFactoryInterface.php new file mode 100644 index 00000000..2e9c8b42 --- /dev/null +++ b/src/Factory/GitHubEventFactoryInterface.php @@ -0,0 +1,47 @@ + $payload + */ + public function build(string $type, array $payload): GitHubEvent; + + /** + * Build a GitHub web hook event object based on the incoming request. + * + * @throws InvalidGitHubRequestPayloadException If the request payload is not a valid JSON content + * @throws MissingGitHubEventTypeException If the GitHub event type could not be found in the request headers + */ + public function buildFromRequest(Request $request): GitHubEvent; +} diff --git a/src/Factory/SitepackageFactory.php b/src/Factory/SitepackageFactory.php new file mode 100644 index 00000000..f6d7b1c3 --- /dev/null +++ b/src/Factory/SitepackageFactory.php @@ -0,0 +1,57 @@ +setBasePackage($dto->basePackage) + ->setTypo3Version($dto->typo3Version) + ->setTitle($dto->title ?? '') + ->setDescription($dto->description ?? '') + ->setExtensionKey($dto->extensionKey ?? '') + ->setRepositoryUrl($dto->repositoryUrl ?? '') + ->setComposerName($dto->composerName ?? '') + ->setPsr4Namespace($dto->psr4Namespace ?? '') + ->getAuthor() + ->setName($dto->name ?? '') + ->setEmail($dto->email ?? '') + ->setCompany($dto->company ?? '') + ->setHomepage($dto->homepage ?? '') + ; + + return $sitepackage; + } + + public static function fromEntity(Sitepackage $sitepackage): SitepackageDto + { + return SitepackageDto::fromEntity($sitepackage); + } +} diff --git a/src/Form/Dto/SitepackageDto.php b/src/Form/Dto/SitepackageDto.php new file mode 100644 index 00000000..1c6c4d65 --- /dev/null +++ b/src/Form/Dto/SitepackageDto.php @@ -0,0 +1,112 @@ +basePackage = $entity->getBasePackage(); + $dto->typo3Version = $entity->getTypo3Version(); + $dto->title = $entity->getTitle(); + $dto->description = $entity->getDescription(); + $dto->extensionKey = $entity->getExtensionKey(); + $dto->repositoryUrl = $entity->getRepositoryUrl(); + $dto->composerName = $entity->getComposerName(); + $dto->psr4Namespace = $entity->getPsr4Namespace(); + $dto->name = $entity->getAuthor()->getName(); + $dto->email = $entity->getAuthor()->getEmail(); + $dto->company = $entity->getAuthor()->getCompany(); + $dto->homepage = $entity->getAuthor()->getHomepage(); + + return $dto; + } +} diff --git a/src/Form/Extension/AbstractIconExtension.php b/src/Form/Extension/AbstractIconExtension.php new file mode 100644 index 00000000..c9eb7ece --- /dev/null +++ b/src/Form/Extension/AbstractIconExtension.php @@ -0,0 +1,57 @@ + $options + */ + public function buildView(FormView $view, FormInterface $form, array $options): void + { + if (is_string($icon = $options['icon'])) { + $view->vars['label'] = $this->iconExtension->getIcon($icon) . $view->vars['label']; + $view->vars['label_html'] = true; + } + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults(['icon' => null]); + $resolver->setAllowedTypes('icon', ['null', 'string']); + } +} diff --git a/src/Form/Extension/ButtonTypeIconExtension.php b/src/Form/Extension/ButtonTypeIconExtension.php new file mode 100644 index 00000000..38cb921d --- /dev/null +++ b/src/Form/Extension/ButtonTypeIconExtension.php @@ -0,0 +1,34 @@ +basePackageService->getInstalledBasePackage($configuration->basePackage); + + if (!$basePackage->official) { + $notification = 'This is a third party base package, not provided by TYPO3. Please contact the author in case of problems, we do not provide support.'; + $notificationType = 'info'; + } else { + $notification = null; + $notificationType = ''; + } + + $builder + ->add('basePackage', BasePackageType::class, [ + 'label' => 'Base Package', + 'notification' => $notification, + 'notification_type' => $notificationType, + ]) + ->add('typo3Version', Typo3VersionType::class, [ + 'label' => 'TYPO3 Version', + 'choice_filter' => static function (int $version) use ($basePackage): bool { + foreach ($basePackage->getTypo3Versions() as $typo3Version) { + if ($version < VersionUtility::versionToInt($typo3Version)) { + continue; + } + + if ($version / 1_000_000 !== VersionUtility::versionToInt($typo3Version) / 1_000_000) { + continue; + } + + return true; + } + + return false; + }, + 'expanded' => true, + ]) + ->add('title', TextType::class, [ + 'attr' => [ + 'autocomplete' => 'off', + 'placeholder' => 'My Sitepackage', + ], + ]) + ->add('description', TextareaType::class, [ + 'required' => false, + 'attr' => [ + 'autocomplete' => 'off', + 'placeholder' => 'Optional description for the use of this sitepackage', + ], + ]) + ->add('repositoryUrl', TextType::class, [ + 'label' => 'Repository URL', + 'required' => false, + 'attr' => [ + 'autocomplete' => 'off', + 'placeholder' => 'https://github.com/username/my_sitepackage', + ], + ]) + ; + + if ($advanced) { + $builder + ->add('composerName', TextType::class, [ + 'label' => 'Composer Name', + 'required' => false, + 'attr' => [ + 'autocomplete' => 'off', + 'placeholder' => 'my-company/my-sitepackage', + ], + ]) + ->add('psr4Namespace', TextType::class, [ + 'label' => 'PSR-4 Namespace', + 'required' => false, + 'attr' => [ + 'autocomplete' => 'off', + 'placeholder' => 'MyCompany\MySitepackage', + ], + ]) + ->add('extensionKey', TextType::class, [ + 'label' => 'Extension Key', + 'required' => false, + 'attr' => [ + 'autocomplete' => 'off', + 'placeholder' => 'my_sitepackage', + ], + ]) + ; + } + + $builder + ->add('name', TextType::class, [ + 'attr' => [ + 'autocomplete' => 'on', + 'placeholder' => 'John Doe', + ], + ]) + ->add('email', EmailType::class, [ + 'attr' => [ + 'autocomplete' => 'on', + 'placeholder' => 'john.doe@example.com', + ], + ]) + ->add('company', TextType::class, [ + 'attr' => [ + 'autocomplete' => 'on', + 'placeholder' => 'My Company', + ], + ]) + ->add('homepage', TextType::class, [ + 'attr' => [ + 'autocomplete' => 'on', + 'placeholder' => 'https://www.example.com', + ], + ]) + ->add( + 'save', + SubmitType::class, + [ + 'label' => 'Create Sitepackage', + 'icon' => 'actions-save', + 'attr' => ['class' => 'btn-primary'], + 'row_attr' => ['class' => 'd-inline-flex'], + ] + ) + ; + + if ($advanced) { + $builder->add( + 'simple', + SubmitType::class, + [ + 'label' => 'Simple Configuration', + 'icon' => 'actions-toggle-off', + 'attr' => ['class' => 'btn-secondary'], + 'row_attr' => ['class' => 'd-inline-flex ms-1'], + 'validate' => false, + ] + ); + } else { + $builder->add( + 'advanced', + SubmitType::class, + [ + 'label' => 'Advanced Configuration', + 'icon' => 'actions-toggle-on', + 'attr' => ['class' => 'btn-secondary'], + 'row_attr' => ['class' => 'd-inline-flex ms-1'], + 'validate' => false, + ] + ); + } + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => SitepackageDto::class, + 'advanced' => false, + ]); + + $resolver->setAllowedTypes('advanced', 'bool'); + } +} diff --git a/src/Form/Type/BasePackageType.php b/src/Form/Type/BasePackageType.php new file mode 100644 index 00000000..0fa741b6 --- /dev/null +++ b/src/Form/Type/BasePackageType.php @@ -0,0 +1,71 @@ +basePackageService->getInstalledBasePackages() as $basePackage) { + $choices[sprintf('%s (%s)', $basePackage->getTitle(), $basePackage->getComposerPackageName())] = $basePackage->getComposerPackageName(); + } + + $resolver->setDefaults([ + 'choices' => $choices, + 'disabled' => true, + 'notification' => null, + 'notification_type' => 'danger', + ]); + + $resolver->setAllowedTypes('notification', ['null', 'string']); + $resolver->setAllowedTypes('notification_type', 'string'); + } + + public function buildView(FormView $view, FormInterface $form, array $options): void + { + $view->vars['notification'] = $options['notification']; + $view->vars['notification_type'] = $options['notification_type']; + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/src/Form/Type/Typo3VersionType.php b/src/Form/Type/Typo3VersionType.php new file mode 100644 index 00000000..7ef48e59 --- /dev/null +++ b/src/Form/Type/Typo3VersionType.php @@ -0,0 +1,62 @@ +majorVersions->findAllComposerSupported() as $majorVersion) { + if ($majorVersion->getLatestRelease() === null) { + continue; + } + + $choices[$majorVersion->getTitle()] = VersionUtility::versionToInt( + VersionUtility::normalize($majorVersion->getLatestRelease()->getVersion(), 2) + ); + } + + $resolver->setDefaults([ + 'choices' => $choices, + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/src/Menu/MenuBuilder.php b/src/Menu/MenuBuilder.php index c57d259a..868294b5 100644 --- a/src/Menu/MenuBuilder.php +++ b/src/Menu/MenuBuilder.php @@ -28,9 +28,6 @@ class MenuBuilder extends TemplateMenuBuider { - /** - * @inheritDoc - */ public function mainDefault(array $options): ItemInterface { $menu = parent::mainDefault($options); @@ -48,6 +45,20 @@ public function mainDefault(array $options): ItemInterface 'label' => 'Release Notes', ] ); + $tools = $menu->addChild( + 'tools', + [ + 'route' => 'tools_sitepackage', + 'label' => 'Tools', + ] + ); + $tools->addChild( + 'tools-sitepackage', + [ + 'route' => 'tools_sitepackage', + 'label' => 'Sitepackage Builder', + ] + ); $composer = $menu->addChild( 'composer', [ @@ -79,9 +90,6 @@ public function mainDefault(array $options): ItemInterface return $menu; } - /** - * @inheritDoc - */ public function mainProfile(array $options): ItemInterface { $menu = parent::mainProfile($options); diff --git a/src/Package/BasePackage.php b/src/Package/BasePackage.php new file mode 100644 index 00000000..6129f992 --- /dev/null +++ b/src/Package/BasePackage.php @@ -0,0 +1,138 @@ +getName(), + ($entity !== null) ? $entity->isActive() : true, + $entity !== null && $entity->isOfficial(), + ($entity === null) + ); + + return $basePackage; + } + + public function __construct( + private readonly string $composerPackageInstallPath, + private readonly string $composerPackageName, + public readonly bool $active, + public readonly bool $official, + public readonly bool $thirdParty, + ) { + $this->manifest = new BasePackageManifest($this->composerPackageInstallPath); + } + + public function getAssetsDir(): string + { + return str_replace('/', '-', strtolower($this->composerPackageName)); + } + + public function getAssetPreviewImage(): string + { + return \sprintf('%s/images/%s', $this->getAssetsDir(), $this->manifest->getPreviewImage()); + } + + public function getInstallPath(): string + { + return $this->composerPackageInstallPath; + } + + public function getPublicInstallPath(): string + { + return \sprintf('%s/public', $this->composerPackageInstallPath); + } + + public function getComposerPackageName(): string + { + return $this->composerPackageName; + } + + public function getComposerVendorName(): string + { + return explode('/', strtolower($this->composerPackageName))[0]; + } + + public function getComposerProjectName(): string + { + return explode('/', strtolower($this->composerPackageName))[1]; + } + + public function getTitle(): string + { + return $this->manifest->getTitle(); + } + + public function getDescription(): string + { + return $this->manifest->getDescription(); + } + + /** + * @return string[] + */ + public function getTypo3Versions(): array + { + return $this->manifest->getTypo3Versions(); + } + + public function getPreviewImage(): string + { + return $this->manifest->getPreviewImage(); + } + + /** + * @return BasePackageTemplate[] + */ + public function getTemplates(): array + { + if ($this->templates === []) { + foreach ($this->getTypo3Versions() as $typo3Version) { + $this->templates[$typo3Version] = new BasePackageTemplate($this->composerPackageInstallPath, $this, $typo3Version); + } + } + + return $this->templates; + } +} diff --git a/src/Package/BasePackageTemplate.php b/src/Package/BasePackageTemplate.php new file mode 100644 index 00000000..152f624f --- /dev/null +++ b/src/Package/BasePackageTemplate.php @@ -0,0 +1,95 @@ +manifest = new BasePackageTemplateManifest( + $this->composerPackageInstallPath, + $coreVersion + ); + } + + public function getAssetsDir(): string + { + return sprintf('%s/%s', $this->basePackage->getAssetsDir(), $this->coreVersion); + } + + /** + * @return array + */ + public function getAssetsGallery(): array + { + $gallery = []; + $assetsDir = $this->getAssetsDir(); + + foreach ($this->manifest->getGallery() as $imageFilename => $imageDescription) { + $gallery[sprintf('%s/images/%s', $assetsDir, $imageFilename)] = $imageDescription; + } + + return $gallery; + } + + public function getPublicInstallPath(): string + { + return \sprintf('%s/templates/%s/public', $this->composerPackageInstallPath, $this->coreVersion); + } + + public function getSkeletonInstallPath(): string + { + return \sprintf('%s/templates/%s/skeleton', $this->composerPackageInstallPath, $this->coreVersion); + } + + public function getDescription(): string + { + return $this->manifest->getDescription(); + } + + /** + * @return array + */ + public function getDependencies(): array + { + return $this->manifest->getDependencies(); + } + + /** + * @return array + */ + public function getGallery(): array + { + return $this->manifest->getGallery(); + } +} diff --git a/src/Package/Manifest/BasePackageManifest.php b/src/Package/Manifest/BasePackageManifest.php new file mode 100644 index 00000000..5259e922 --- /dev/null +++ b/src/Package/Manifest/BasePackageManifest.php @@ -0,0 +1,84 @@ +, + * images: array{ + * preview: string|null + * }|array{} + * } + */ + private array $manifest; + + public function __construct( + private readonly string $composerPackageInstallPath, + ) { + $this->load($this->getFilename(), self::SCHEMA_PATH); + } + + public function getTitle(): string + { + return $this->manifest['title']; + } + + public function getDescription(): string + { + return $this->manifest['description']; + } + + /** + * @return string[] + */ + public function getTypo3Versions(): array + { + $array = $this->manifest['typo3-versions']; + rsort($array, SORT_NUMERIC); + return $array; + } + + public function getPreviewImage(): string + { + return $this->manifest['images']['preview'] ?? 'preview.png'; + } + + private function getFilename(): string + { + return sprintf('%s/manifest.json', rtrim(trim($this->composerPackageInstallPath), '/\\')); + } +} diff --git a/src/Package/Manifest/BasePackageManifestTrait.php b/src/Package/Manifest/BasePackageManifestTrait.php new file mode 100644 index 00000000..6900de5f --- /dev/null +++ b/src/Package/Manifest/BasePackageManifestTrait.php @@ -0,0 +1,49 @@ +validateSchema(JsonFile::STRICT_SCHEMA, $manifestSchemaFilename); + + if (!is_array($manifest = $jsonFile->read())) { + throw new RuntimeException( + sprintf('Manifest "%s" could not be decoded.', $manifestFilename), + 1_658_926_618 + ); + } + + /** @phpstan-ignore-next-line because validation above makes this assignment safe */ + $this->manifest = $manifest; + } +} diff --git a/src/Package/Manifest/BasePackageTemplateManifest.php b/src/Package/Manifest/BasePackageTemplateManifest.php new file mode 100644 index 00000000..092e5e2d --- /dev/null +++ b/src/Package/Manifest/BasePackageTemplateManifest.php @@ -0,0 +1,77 @@ +, + * images: array{ + * gallery: array + * } + * } + */ + private array $manifest; + + public function __construct( + private readonly string $composerPackageInstallPath, + private readonly string $coreVersion, + ) { + $this->load($this->getFilename(), self::SCHEMA_PATH); + } + + public function getDescription(): string + { + return $this->manifest['description']; + } + + /** + * @return array + */ + public function getDependencies(): array + { + return $this->manifest['dependencies']; + } + + /** + * @return array + */ + public function getGallery(): array + { + return $this->manifest['images']['gallery']; + } + + public function getFilename(): string + { + return sprintf('%s/templates/%s/manifest.json', $this->composerPackageInstallPath, $this->coreVersion); + } +} diff --git a/src/Package/Package/Author.php b/src/Package/Package/Author.php new file mode 100644 index 00000000..71fdaa5c --- /dev/null +++ b/src/Package/Package/Author.php @@ -0,0 +1,121 @@ +name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } + + public function getCompany(): string + { + return $this->company; + } + + public function setCompany(string $company): static + { + $this->company = $company; + + return $this; + } + + public function getHomepage(): string + { + return $this->homepage; + } + + public function setHomepage(string $homepage): static + { + $this->homepage = $homepage; + + return $this; + } + + /** + * @return array{name: string, email: string, company: string, homepage: string} + */ + public function jsonSerialize(): array + { + return [ + 'name' => $this->getName(), + 'email' => $this->getEmail(), + 'company' => $this->getCompany(), + 'homepage' => $this->getHomepage(), + ]; + } +} diff --git a/src/Package/Sitepackage.php b/src/Package/Sitepackage.php new file mode 100644 index 00000000..80c0966a --- /dev/null +++ b/src/Package/Sitepackage.php @@ -0,0 +1,285 @@ +company and title if empty") + */ + #[Assert\Regex( + pattern: '/^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$/', + message: 'The name must be lowercased and consist of words separated by `-`, `.` or `_`.' + )] + #[Serializer\Type('string')] + private string $composerName = ''; + + /** + * @SWG\Property(type="string", example="MyCompany\MySitepackage", default="generated from author->company and title if empty") + */ + #[Assert\Regex( + pattern: '/^[A-Z][A-Za-z0-9]+\\\[A-Z][A-Za-z0-9]+$/', + message: 'Only letters and numbers are allowed.' + )] + #[Serializer\Type('string')] + private string $psr4Namespace = ''; + + #[Assert\Valid] + #[Serializer\Type(Author::class)] + private Author $author; + + public function __construct() + { + $this->author = new Author(); + } + + public function getBasePackage(): string + { + return $this->basePackage; + } + + public function setBasePackage(string $basePackage): self + { + $this->basePackage = $basePackage; + + return $this; + } + + public function getTypo3Version(): int + { + return $this->typo3Version; + } + + public function setTypo3Version(int $typo3Version): self + { + $this->typo3Version = $typo3Version; + + return $this; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + public function getDescription(): string + { + return $this->description; + } + + public function setDescription(string $description): self + { + $this->description = $description; + + return $this; + } + + public function getExtensionKey(): string + { + if ($this->extensionKey === '') { + return StringUtility::camelCaseToLowerCaseUnderscored($this->getTitle()); + } + + return $this->extensionKey; + } + + public function setExtensionKey(string $extensionKey): self + { + if ($this->getExtensionKey() !== $extensionKey) { + $this->extensionKey = StringUtility::camelCaseToLowerCaseUnderscored($extensionKey); + } + + return $this; + } + + public function getRepositoryUrl(): string + { + return $this->repositoryUrl; + } + + public function setRepositoryUrl(string $repositoryUrl): self + { + $this->repositoryUrl = $repositoryUrl; + + return $this; + } + + public function getComposerName(): string + { + if ($this->composerName === '') { + return StringUtility::camelCaseToLowerCaseDashed($this->getAuthor()->getCompany()) . '/' . + StringUtility::camelCaseToLowerCaseDashed($this->getTitle()); + } + + return $this->composerName; + } + + public function setComposerName(string $composerName): self + { + if ($this->getComposerName() !== $composerName) { + $this->composerName = $composerName; + } + + return $this; + } + + public function getComposerVendorName(): string + { + return explode('/', $this->getComposerName())[0]; + } + + public function getComposerProjectName(): string + { + return explode('/', $this->getComposerName())[1]; + } + + public function getPsr4Namespace(): string + { + if ($this->psr4Namespace === '') { + return StringUtility::stringToUpperCamelCase($this->getAuthor()->getCompany()) . '\\' . + StringUtility::stringToUpperCamelCase($this->getTitle()); + } + + return $this->psr4Namespace; + } + + public function setPsr4Namespace(string $psr4Namespace): self + { + if ($this->getPsr4Namespace() !== $psr4Namespace) { + $this->psr4Namespace = $psr4Namespace; + } + + return $this; + } + + public function getAuthor(): Author + { + return $this->author; + } + + public function setAuthor(Author $author): self + { + $this->author = $author; + + return $this; + } + + /** + * @return array{ + * basePackage: string, + * typo3Version: int, + * title: string, + * description: string, + * extensionKey: string, + * repositoryUrl: string, + * composerName: string, + * psr4Namespace: string, + * author: Author + * } + */ + public function jsonSerialize(): array + { + return [ + 'basePackage' => $this->getBasePackage(), + 'typo3Version' => $this->getTypo3Version(), + 'title' => $this->getTitle(), + 'description' => $this->getDescription(), + 'extensionKey' => $this->getExtensionKey(), + 'repositoryUrl' => $this->getRepositoryUrl(), + 'composerName' => $this->getComposerName(), + 'psr4Namespace' => $this->getPsr4Namespace(), + 'author' => $this->getAuthor(), + ]; + } +} diff --git a/src/Repository/BasePackageRepository.php b/src/Repository/BasePackageRepository.php new file mode 100644 index 00000000..9ab5bda1 --- /dev/null +++ b/src/Repository/BasePackageRepository.php @@ -0,0 +1,79 @@ + + */ +final class BasePackageRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, BasePackage::class); + } + + public function add(BasePackage $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(BasePackage $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * @return BasePackage[] + */ + public function findAllActive(): array + { + return $this->findBy( + ['active' => true], + ['name' => 'ASC'] + ); + } + + /** + * @return BasePackage[] + */ + public function findAllActiveGroupedByOfficial(): array + { + return $this->findBy( + ['active' => true], + ['official' => 'DESC', 'name' => 'ASC'] + ); + } +} diff --git a/src/Security/GitHubRequestChecker.php b/src/Security/GitHubRequestChecker.php new file mode 100644 index 00000000..e23e7841 --- /dev/null +++ b/src/Security/GitHubRequestChecker.php @@ -0,0 +1,57 @@ +headers->get('X-Hub-Signature-256')) === null) { + throw new MissingGitHubSignatureException($request, 1_661_156_488); + } + + $algo = explode('=', $signature)[0]; + $digest = hash_hmac('sha256', $request->getContent(), $this->signingSecret); + + if (!hash_equals(sprintf('%s=%s', $algo, $digest), $signature)) { + throw new InvalidGitHubRequestSignatureException($request, $signature, 1_661_156_489); + } + } +} diff --git a/src/Service/BasePackageService.php b/src/Service/BasePackageService.php new file mode 100644 index 00000000..05e4460a --- /dev/null +++ b/src/Service/BasePackageService.php @@ -0,0 +1,707 @@ +composerApplication->setAutoExit(false); + } + + private function getProjectDir(): string + { + return rtrim($this->kernel->getProjectDir() . '/' . $this->projectDir, '/'); + } + + private function getRepositoryDir(): string + { + return rtrim($this->getProjectDir() . '/packages', '/'); + } + + private function changeToProjectDir(): void + { + $oldWorkingDir = (string)getcwd(); + //$oldWorkingDir = Platform::getCwd(true); + + if ($oldWorkingDir !== ($projectDir = $this->getProjectDir())) { + if (!$this->filesystem->exists($projectDir)) { + $this->filesystem->mkdir($projectDir); + } + chdir($projectDir); + $this->oldWorkingDir = $oldWorkingDir; + } + } + + private function restoreToWorkingDir(): void + { + if ($this->oldWorkingDir !== '') { + chdir($this->oldWorkingDir); + $this->oldWorkingDir = ''; + } + } + + private function getPublicDir(): string + { + $defaultPublicDir = 'public'; + + $composerFilePath = $this->kernel->getProjectDir() . '/composer.json'; + + if (!file_exists($composerFilePath)) { + return $defaultPublicDir; + } + + if (!is_string($contents = file_get_contents($composerFilePath))) { + return $defaultPublicDir; + } + + if (!is_array($composerConfig = json_decode($contents, true, 512, JSON_THROW_ON_ERROR))) { + return $defaultPublicDir; + } + + return $composerConfig['extra']['public-dir'] ?? $defaultPublicDir; + } + + private function getPublicAssetsDir(): string + { + $publicDir = $this->kernel->getProjectDir() . '/' . $this->getPublicDir(); + + if (!is_dir($publicDir)) { + throw new RuntimeException(sprintf('The target directory "%s" does not exist.', $publicDir)); + } + + return rtrim($publicDir . '/' . $this->assetsDir, '/'); + } + + /** + * Creates symbolic link. + * + * @throws RuntimeException if link cannot be created + */ + private function symlink(string $originDir, string $targetDir, bool $relative = false): void + { + if ($relative) { + $this->filesystem->mkdir(dirname($targetDir)); + + if (!is_string($absoluteTargetDir = realpath(dirname($targetDir)))) { + throw new RuntimeException( + sprintf('Could not determine absolute path for "%s".', $targetDir), + 1_658_923_646 + ); + } + + $originDir = $this->filesystem->makePathRelative($originDir, $absoluteTargetDir); + } + + $this->filesystem->symlink($originDir, $targetDir); + + if (!file_exists($targetDir)) { + throw new RuntimeException( + sprintf('Symbolic link "%s" was created but appears to be broken.', $targetDir), + 1_658_923_653 + ); + } + } + + private function installAssets(): void + { + $publicAssetsDir = $this->getPublicAssetsDir() . '/'; + $validAssetDirs = []; + + foreach ($this->getInstalledBasePackages() as $basePackage) { + $assetDir = $basePackage->getAssetsDir(); + $targetDir = $publicAssetsDir . $assetDir; + + $this->filesystem->remove($targetDir); + + if (!is_dir($originDir = $basePackage->getPublicInstallPath())) { + continue; + } + + $this->symlink($originDir, $targetDir, true); + $validAssetDirs[] = $assetDir; + + foreach ($basePackage->getTemplates() as $template) { + $assetDir = $template->getAssetsDir(); + $targetDir = $publicAssetsDir . $assetDir; + + $this->filesystem->remove($targetDir); + + if (!is_dir($originDir = $template->getPublicInstallPath())) { + continue; + } + + $this->symlink($originDir, $targetDir, true); + $validAssetDirs[] = $assetDir; + } + } + + // remove the assets of the bundles that no longer exist + if (is_dir($publicAssetsDir)) { + $dirsToRemove = Finder::create()->depth(0)->directories()->exclude($validAssetDirs)->in($publicAssetsDir); + $this->filesystem->remove($dirsToRemove); + } + } + + private function getComposer(): Composer + { + $composer = $this->composerApplication->getComposer(); + + if (!$composer instanceof \Composer\Composer) { + throw new RuntimeException( + \sprintf('Composer project is not setup in "%s".', $this->getProjectDir()), + 1_658_915_927 + ); + } + + return $composer; + } + + private function cloneRepository(): void + { + $process = new Process( + ['git', 'clone', 'https://github.com/GsTYPO3/base-packages.git', 'packages'], + $this->getProjectDir() + ); + if ($process->run() !== 0) { + throw new RuntimeException(\sprintf( + "Error while cloning repository.\n\nOutput:\n%s\n\nError Output:\n%s\n\Command:\n%s\n\nProject Dir:\n%s", + $process->getOutput(), + $process->getErrorOutput(), + $process->getCommandLine(), + $this->getProjectDir() + ), 1_660_919_239); + } + } + + private function updateRepository(): void + { + $process = new Process( + ['git', 'pull'], + $this->getRepositoryDir() + ); + if ($process->run() !== 0) { + throw new RuntimeException(\sprintf( + "Error while cloning repository:\n%s\n\n%s", + $process->getOutput(), + $process->getErrorOutput() + ), 1_660_919_240); + } + } + + /** + * @return array + */ + private function getComposerProjectFiles(): array + { + return [ + $this->getProjectDir() . '/vendor', + $this->getProjectDir() . '/composer.json', + $this->getProjectDir() . '/composer.lock', + ]; + } + + private function initializeProject(bool $force = false): void + { + // Clone or update the base-packages repository + if (!$this->filesystem->exists($this->getRepositoryDir() . '/.git')) { + $this->cloneRepository(); + } else { + $this->updateRepository(); + } + + // Remove previous installation + if ($force) { + $this->filesystem->remove($this->getComposerProjectFiles()); + } + + // Early return if project already exists + if ($this->filesystem->exists($this->getComposerProjectFiles())) { + return; + } + + // Create project + $output = new BufferedOutput(); + $input = new ArrayInput([ + 'command' => 'init', + '--name' => 't3o/base-packages', + '--description' => 'Known base packages shown at the get.typo3.org Tools.', + '--type' => 'project', + '--stability' => 'dev', + '--repository' => ['{"type": "path", "url": "packages/*/*"}', '{"packagist.org": false}'], + '--ansi' => true, + '--no-interaction' => true, + '--working-dir' => $this->getProjectDir(), + ]); + + if (($exitCode = $this->composerApplication->run($input, $output)) !== 0) { + throw new RuntimeException(\sprintf('Composer failed:\n%s', $output->fetch()), $exitCode); + } + + $this->cache->invalidateTags(['installed-base-packages']); + } + + private function installBasePackages(): void + { + $this->initializeProject(); + + // Prepare packages to require + $requirements = []; + foreach ($this->basePackageRepository->findAllActive() as $basePackage) { + $requirements[] = \sprintf('%s:%s', $basePackage->getName(), '@dev'); + } + + // Require packages + $output = new BufferedOutput(); + $input = new ArrayInput([ + 'command' => 'require', + '--no-progress' => true, + //'--no-update' => true, + //'--no-install' => true, + '--update-with-all-dependencies' => true, + '--ansi' => true, + '--no-interaction' => true, + '--working-dir' => $this->getProjectDir(), + 'packages' => $requirements, + ]); + + if (($exitCode = $this->composerApplication->run($input, $output)) !== 0) { + throw new RuntimeException(\sprintf('Composer failed:\n%s', $output->fetch()), $exitCode); + } + + $this->cache->invalidateTags(['installed-base-packages']); + + $this->installAssets(); + } + + private function installBasePackage(string $packageName): void + { + $this->initializeProject(); + + // Require package + $output = new BufferedOutput(); + $input = new ArrayInput([ + 'command' => 'require', + '--no-progress' => true, + //'--no-update' => true, + //'--no-install' => true, + '--update-with-all-dependencies' => true, + '--ansi' => true, + '--no-interaction' => true, + '--working-dir' => $this->getProjectDir(), + 'packages' => [$packageName . ':@dev'], + ]); + + if (($exitCode = $this->composerApplication->run($input, $output)) !== 0) { + throw new RuntimeException(\sprintf('Composer failed:\n%s', $output->fetch()), $exitCode); + } + + $this->cache->invalidateTags(['installed-base-packages']); + + $this->installAssets(); + } + + public function updateBasePackages(): void + { + // Install or update packages + $output = new BufferedOutput(); + $input = new ArrayInput([ + 'command' => 'update', + '--no-progress' => true, + '--with-all-dependencies' => true, + '--ignore-platform-reqs' => true, + '--prefer-stable' => true, + '--ansi' => true, + '--no-interaction' => true, + '--working-dir' => $this->getProjectDir(), + ]); + + if (($exitCode = $this->composerApplication->run($input, $output)) !== 0) { + throw new RuntimeException(\sprintf('Composer failed:\n%s', $output->fetch()), $exitCode); + } + + $this->cache->invalidateTags(['installed-base-packages']); + + $this->installAssets(); + } + + private function removeBasePackage(string $packageName): void + { + $this->initializeProject(); + + // Require package + $output = new BufferedOutput(); + $input = new ArrayInput([ + 'command' => 'remove', + '--no-progress' => true, + //'--no-update' => true, + //'--no-install' => true, + '--update-with-all-dependencies' => true, + '--unused' => true, + '--ansi' => true, + '--no-interaction' => true, + '--working-dir' => $this->getProjectDir(), + 'packages' => [$packageName], + ]); + + if (($exitCode = $this->composerApplication->run($input, $output)) !== 0) { + throw new RuntimeException(\sprintf('Composer failed:\n%s', $output->fetch()), $exitCode); + } + + $this->cache->invalidateTags(['installed-base-packages']); + + $this->installAssets(); + } + + /** + * @return array + */ + public function getInstalledBasePackages(): array + { + /** @var array */ + return $this->cache->get('installed-base-packages-grouped-by-official', function (ItemInterface $item): array { + $item->tag(['installed-base-packages', 'base-packages']); + $installedBasePackages = []; + + $this->changeToProjectDir(); + + try { + try { + $composer = $this->getComposer(); + } catch (Throwable) { + return $installedBasePackages; + } + + foreach ($composer->getRepositoryManager()->getLocalRepository()->getPackages() as $package) { + if ($package->getType() === self::PACKAGE_TYPE) { + $basePackageDto = BasePackage::fromPackage( + $composer->getInstallationManager()->getInstallPath($package), + $package, + $this->basePackageRepository->findOneBy([ + 'name' => $package->getName(), + 'active' => true, + ]) + ); + $installedBasePackages[] = $basePackageDto; + } + } + + usort($installedBasePackages, static function (BasePackage $a, BasePackage $b): int { + if ($a->official !== $b->official) { + if ($a->official) { + return -1; + } + + return 1; + } + + return $a->getTitle() <=> $b->getTitle(); + }); + + return $installedBasePackages; + } finally { + $this->restoreToWorkingDir(); + } + }); + } + + public function getInstalledBasePackage(string $packageName): BasePackage + { + foreach ($this->getInstalledBasePackages() as $basePackage) { + if ($basePackage->getComposerPackageName() === $packageName) { + return $basePackage; + } + } + + throw new PackageNotInstalledException(\sprintf('Base package "%s" not found.', $packageName), 1_658_839_466); + } + + /** + * @return array + */ + public function getBasePackages(): array + { + /** @var array */ + return $this->cache->get('active-base-packages-grouped-by-official', function (ItemInterface $item): array { + $item->tag(['active-base-packages', 'base-packages']); + + $basePackages = []; + + $this->changeToProjectDir(); + + try { + $this->installBasePackages(); + + try { + $composer = $this->getComposer(); + } catch (Throwable) { + return $basePackages; + } + + foreach ($this->basePackageRepository->findAllActiveGroupedByOfficial() as $basePackage) { + $package = $composer->getRepositoryManager()->getLocalRepository()->findPackage( + $basePackage->getName(), + new MatchAllConstraint() + ); + + if (!$package instanceof PackageInterface) { + throw new RuntimeException( + \sprintf('Package "%s" not found.', $basePackage->getName()), + 1_658_944_953 + ); + } + + $basePackageDto = BasePackage::fromPackage( + $composer->getInstallationManager()->getInstallPath($package), + $package, + $basePackage + ); + + $basePackages[] = $basePackageDto; + } + + return $basePackages; + } finally { + $this->restoreToWorkingDir(); + } + }); + } + + public function checkAndInstallMissingBasePackage(string $packageName): BasePackage + { + $this->changeToProjectDir(); + + try { + try { + $basePackage = $this->getInstalledBasePackage($packageName); + } catch (PackageNotInstalledException) { + try { + $basePackage = $this->validate($packageName); + } catch (Throwable $throwable) { + $this->removeBasePackage($packageName); + + throw $throwable; + } + } + + return $basePackage; + } finally { + $this->restoreToWorkingDir(); + } + } + + public function validate(string $packageName): BasePackage + { + $this->changeToProjectDir(); + + try { + try { + $basePackage = $this->getInstalledBasePackage($packageName); + } catch (PackageNotInstalledException) { + $package = $this->getComposer()->getRepositoryManager()->findPackage( + $packageName, + new MatchAllConstraint() + ); + + if (!$package instanceof PackageInterface) { + throw new RuntimeException( + \sprintf('Package "%s" not found.', $packageName), + 1_658_944_953 + ); + } + + if ($package->getType() !== self::PACKAGE_TYPE) { + throw new IncompatiblePackageException( + \sprintf('Package "%s" is not of type "%s".', $packageName, self::PACKAGE_TYPE), + 1_658_786_562 + ); + } + + $this->installBasePackage($packageName); + + $basePackage = $this->getInstalledBasePackage($packageName); + } + } finally { + $this->restoreToWorkingDir(); + } + + //return $basePackage; + + try { + /* + if (strlen($basePackage->title) < 5) { + throw new IncompatiblePackageException( + \sprintf('Title "%s" must have 5 characters or more.', $basePackage->title), + 1_658_945_128 + ); + } + + if (strlen($basePackage->description) < 10) { + throw new IncompatiblePackageException( + \sprintf('Description "%s" must have 10 characters or more.', $basePackage->description), + 1_658_945_398 + ); + } + */ + + if ($basePackage->getTypo3Versions() === []) { + throw new IncompatiblePackageException( + 'A base package must define one or more supported TYPO3 core versions.', + 1_658_945_403 + ); + } + + /* + foreach ($basePackage->getTypo3Versions() as $typo3Version) { + if (!\file_exists($basePackage->getInstallPath() . '/templates/' . $typo3Version . '/public/' . $basePackage->previewImage)) { + throw new IncompatiblePackageException( + 'A base package must have a preview image.', + 1_658_946_257 + ); + } + } + + /* + foreach ($basePackage->typo3Versions as $typo3Version) { + if (!\file_exists($basePackage->getInstallPath() . '/templates/' . $typo3Version . '/docs/detail.md')) { + throw new IncompatiblePackageException( + 'A base package must have a detail.md.', + 1_658_946_258 + ); + } + } + */ + + $forbiddenFiles = Finder::create() + ->ignoreDotFiles(false) + ->files() + ->notName(self::ALLOWED_FILE_TYPES) + ->in($basePackage->getInstallPath()/* . '/templates/skeletons'*/) + ; + if ($forbiddenFiles->hasResults()) { + throw new IncompatiblePackageException( + \sprintf( + 'Package contains not allowed files: %s', + str_replace( + $basePackage->getInstallPath() . '/', + '', + implode( + ', ', + iterator_to_array($forbiddenFiles->getIterator()) + ) + ) + ), + 1_658_952_036 + ); + } + + return $basePackage; + } catch (Throwable $throwable) { + throw new IncompatiblePackageException( + \sprintf('Package "%s" is not a valid base package. %s', $packageName, $throwable->getMessage()), + 1_658_949_297 + ); + } + } + + public function resetCache(): void + { + $this->cache->invalidateTags(['base-packages']); + } + + public function updatePackageRepository(): void + { + $this->updateRepository(); + $this->updateBasePackages(); + $this->cache->invalidateTags(['base-packages']); + } +} diff --git a/src/Service/LegacyDataService.php b/src/Service/LegacyDataService.php index 022721fd..786972f7 100644 --- a/src/Service/LegacyDataService.php +++ b/src/Service/LegacyDataService.php @@ -37,6 +37,7 @@ public function __construct( public function getReleaseJson(): string { + /** @var string */ return $this->cache->get('releases.json', function (ItemInterface $item): string { $item->tag(['major-versions', 'major-version', 'releases', 'release']); $content = json_encode($this->majorVersionRepository->findAllPreparedForJson(), JSON_THROW_ON_ERROR); diff --git a/src/Service/SitepackageGenerator.php b/src/Service/SitepackageGenerator.php new file mode 100644 index 00000000..81dc6f85 --- /dev/null +++ b/src/Service/SitepackageGenerator.php @@ -0,0 +1,178 @@ + + */ + private array $basePackages = []; + + private string $zipPath; + + private string $filename; + + public function __construct( + private readonly BasePackageService $basePackageService, + ) { + } + + public function create(Sitepackage $package): void + { + $this->filename = $package->getExtensionKey() . '.zip'; + $this->zipPath = ($zipPath = tempnam(sys_get_temp_dir(), $this->filename)) !== false + ? $zipPath : $this->filename + ; + + $zipFile = new ZipArchive(); + $opened = $zipFile->open($this->zipPath, ZipArchive::CREATE); + if ($opened === true) { + $sourceDir = $this->getSourceDir($package); + $files = Finder::create() + ->ignoreDotFiles(false) + ->ignoreVCS(false) + ->in($sourceDir) + ; + $this->addFiles($zipFile, $files, $package, $sourceDir); + + $zipFile->close(); + } + } + + public function getZipPath(): string + { + return $this->zipPath; + } + + public function getFilename(): string + { + return $this->filename; + } + + private function getBasePackage(Sitepackage $package): BasePackage + { + if (!($this->basePackages[$package->getBasePackage()] ?? null) instanceof BasePackage) { + $this->basePackages[$package->getBasePackage()] = + $this->basePackageService->getInstalledBasePackage($package->getBasePackage()); + } + + return $this->basePackages[$package->getBasePackage()]; + } + + private function getSourceDir(Sitepackage $package): string + { + $basePackage = $this->getBasePackage($package); + $basePackageTemplate = null; + $version = $package->getTypo3Version(); + + foreach ($basePackage->getTypo3Versions() as $typo3Version) { + if ($version < VersionUtility::versionToInt($typo3Version)) { + continue; + } + + $basePackageTemplate = $basePackage->getTemplates()[$typo3Version]; + break; + } + + if ($basePackageTemplate === null) { + throw new RuntimeException( + sprintf('Template for version "%s" not found.', $version), + 1_658_939_427 + ); + } + + return $basePackageTemplate->getSkeletonInstallPath(); + } + + private function getFileContent(string $file, string $baseDir, Sitepackage $package): string + { + $content = file_get_contents($file); + $fileUniqueId = uniqid($this->createRelativeFilePath( + $file, + $baseDir + )); + $twig = new Environment(new ArrayLoader([$fileUniqueId => $content]), [ + 'strict_variables' => true, + ]); + + return $twig->render( + $fileUniqueId, + [ + 'package' => $package, + 'timestamp' => time(), + ] + ); + } + + private function createRelativeFilePath(string $file, string $sourceDir): string + { + return substr($file, strlen(rtrim($sourceDir, '/')) + 1); + } + + private function isTwigFile(string $extension): bool + { + return $extension === 'twig'; + } + + private function removeTwigExtension(string $baseFileName): string + { + return substr($baseFileName, 0, -5); + } + + private function addFiles( + ZipArchive $zipFile, + Finder $files, + Sitepackage $package, + string $sourceDir + ): void { + $baseDir = dirname($sourceDir, 5); + + foreach ($files as $file) { + $baseFileName = $this->createRelativeFilePath($file->getPathname(), $sourceDir); + if ($file->isDir()) { + $zipFile->addEmptyDir($baseFileName); + } elseif (!$this->isTwigFile($file->getExtension())) { + $zipFile->addFile($file->getPathname(), $baseFileName); + } else { + $content = $this->getFileContent($file->getPathname(), $baseDir, $package); + $nameInZip = $this->removeTwigExtension($baseFileName); + $zipFile->addFromString($nameInZip, $content); + } + } + } +} diff --git a/src/Session/ToolSessionTrait.php b/src/Session/ToolSessionTrait.php new file mode 100644 index 00000000..b97a36af --- /dev/null +++ b/src/Session/ToolSessionTrait.php @@ -0,0 +1,109 @@ +container->get('request_stack')) instanceof RequestStack) { + throw new RuntimeException('Invalid request stack.', 1_659_141_555); + } + + return $requestStack->getSession(); + } + + /** + * @throws UnexpectedValueException + */ + private function getSitepackageConfig(): SitepackageDto + { + $configuration = $this->getSession()->get('sitepackage_config'); + + if (!($configuration instanceof SitepackageDto)) { + $this->addFlash( + 'error', + 'Whoops, we could not find the package configuration. Please submit the configuration again.' + ); + + throw new UnexpectedValueException('Invalid or missing configuration.', 1_638_038_672); + } + + return $configuration; + } + + private function isAdvancedSitepackageConfig(): bool + { + return $this->getSession()->get('sitepackage_config_advanced') === true; + } + + private function setSitepackageConfig(SitepackageDto $configuration, bool $advanced): void + { + $this->getSession()->set('sitepackage_config', $configuration); + $this->getSession()->set('sitepackage_config_advanced', $advanced); + } + + /** + * @throws UnexpectedValueException + */ + private function getSitepackage(): Sitepackage + { + $sitepackage = $this->getSession()->get('sitepackage'); + + if (!($sitepackage instanceof Sitepackage)) { + $this->addFlash( + 'error', + 'Whoops, we could not find the Sitepackage. Please submit the configuration again.' + ); + + throw new UnexpectedValueException('Invalid or missing Sitepackage.', 1_638_038_673); + } + + return $sitepackage; + } + + private function setSitepackage(Sitepackage $sitepackage): void + { + $this->getSession()->set('sitepackage', $sitepackage); + } + + private function getSitepackageError(): string + { + return is_string($error = $this->getSession()->get('sitepackage_error')) ? $error : ''; + } + + private function setSitepackageError(string $error): void + { + $this->getSession()->set('sitepackage_error', $error); + } +} diff --git a/src/Twig/Extension/VersionNumberExtension.php b/src/Twig/Extension/VersionNumberExtension.php new file mode 100644 index 00000000..80c2721a --- /dev/null +++ b/src/Twig/Extension/VersionNumberExtension.php @@ -0,0 +1,56 @@ +versionFilter(...)), + ]; + } + + public function versionFilter(int $version, int $positions = 3): string + { + $versionString = str_pad((string)$version, 9, '0', STR_PAD_LEFT); + $parts = [ + substr($versionString, 0, 3), + substr($versionString, 3, 3), + substr($versionString, 6, 3), + ]; + + return match ($positions) { + 1 => (string)(int)$parts[0], + 2 => (int)$parts[0] . '.' . (int)$parts[1], + default => (int)$parts[0] . '.' . (int)$parts[1] . '.' . (int)$parts[2], + }; + } +} diff --git a/src/Twig/Filter/BasePackageFilter.php b/src/Twig/Filter/BasePackageFilter.php new file mode 100644 index 00000000..c56be474 --- /dev/null +++ b/src/Twig/Filter/BasePackageFilter.php @@ -0,0 +1,47 @@ + $this->parse($markdown, $publicPath)), + ]; + } + + public function parse(string $markdown, string $publicPath): string + { + return str_replace('../public', $publicPath, $markdown); + } +} diff --git a/src/Utility/StringUtility.php b/src/Utility/StringUtility.php new file mode 100644 index 00000000..90be037c --- /dev/null +++ b/src/Utility/StringUtility.php @@ -0,0 +1,98 @@ + {% if communityVersions|length > 0 %}Download TYPO3{% endif %} Try TYPO3 + Create a Sitepackage

{% endframe %} diff --git a/templates/form/custom_theme.html.twig b/templates/form/custom_theme.html.twig new file mode 100644 index 00000000..e2a7b269 --- /dev/null +++ b/templates/form/custom_theme.html.twig @@ -0,0 +1,13 @@ +{% extends "custom_theme.html.twig" %} + +{%- block base_package_widget -%} + + {% if notification is not empty %} + + {% endif %} + + {{- block('choice_widget') -}} + +{%- endblock base_package_widget -%} diff --git a/templates/layout.html.twig b/templates/layout.html.twig index 2c71dc0f..e1aeb490 100644 --- a/templates/layout.html.twig +++ b/templates/layout.html.twig @@ -14,6 +14,7 @@ gtag(\'config\', \'G-JTSKLNF4S0\'); ') }} {% endblock %} +{% block headline %}Try the new Sitepackage Builder now and create a Sitepackage in 2 easy steps.{% endblock %} {% block footer %} {% frame with { color: 'primary' } %}
diff --git a/templates/tools/layout.html.twig b/templates/tools/layout.html.twig new file mode 100644 index 00000000..b1e59e9d --- /dev/null +++ b/templates/tools/layout.html.twig @@ -0,0 +1,10 @@ +{% extends 'layout.html.twig' %} + +{% block headline %}{% endblock %} + +{% block footer %} + + {% include 'tools/partials/privacy-footer.html.twig' %} + {{ parent() }} + +{% endblock %} diff --git a/templates/tools/partials/privacy-footer.html.twig b/templates/tools/partials/privacy-footer.html.twig new file mode 100644 index 00000000..a47f9274 --- /dev/null +++ b/templates/tools/partials/privacy-footer.html.twig @@ -0,0 +1,13 @@ +{% frame with { id: 'api', color: 'dark', center: true, indent: true, title: 'Do you already know the Sitepackage Builder API?' } %} +

+ Instead of using the website, you can also create your Sitepackage using + the API. +

+{% endframe %} +{% frame with { id: 'privacy', color: 'light', center: true, indent: true, title: 'We respect your Privacy!' } %} +

+ We are not storing, sharing or doing any other crazy stuff with the + data you provide to generate your very own Sitepackage. Simple as + that, check the sources. +

+{% endframe %} diff --git a/templates/tools/sitepackage/configure.html.twig b/templates/tools/sitepackage/configure.html.twig new file mode 100644 index 00000000..dc5a8284 --- /dev/null +++ b/templates/tools/sitepackage/configure.html.twig @@ -0,0 +1,18 @@ +{% extends 'tools/layout.html.twig' %} + +{% block title %}Create your own TYPO3 Sitepackage{% endblock %} + +{% block body %} + + {% frame with { color: 'dark', center: true, title: 'Create your own Sitepackage', titleSize: 1, backgroundImage: asset("assets/Images/keyvisual.png") } %} +

+ Awesome you made it here! Just a few more details about your + project and your own Sitepackage is ready for download. +

+ {% endframe %} + + {% frame with { id: 'configuration', indent: true, title: 'Configuration' } %} + {{ form(form, {'attr': {'novalidate': 'novalidate'}}) }} + {% endframe %} + +{% endblock %} diff --git a/templates/tools/sitepackage/detail.html.twig b/templates/tools/sitepackage/detail.html.twig new file mode 100644 index 00000000..580a5a1b --- /dev/null +++ b/templates/tools/sitepackage/detail.html.twig @@ -0,0 +1,90 @@ +{% extends 'tools/layout.html.twig' %} + +{% block title %}Create your own TYPO3 Sitepackage{% endblock %} + +{% block body %} + + {% frame with { color: 'dark', center: true, title: 'Create your own Sitepackage', titleSize: 1, backgroundImage: asset("assets/Images/keyvisual.png") } %} +

+ Awesome you made it here! Just a few more details about your + project and your own Sitepackage is ready for download. +

+ {% endframe %} + + {% frame %} +
+

+ {{ basePackage.title }} Base Package for TYPO3 {{ typo3Version }} + {% if basePackage.official %} + {{ basePackage.title }} + {% endif %} +

+
+
+
+
+
+ {{ basePackageTemplate.description|markdown_to_html }} +
+
+ {% for imageFile, imageDescription in basePackageTemplate.assetsGallery %} +
+ {{ imageDescription }} +
{{ imageDescription }}
+
+ {% endfor %} +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Last Update15 September 2020
ExtensionsBootstrap Package
Extension XY
FrameworkBootstrap
Columns3
LayoutResponsive
+
+
+
+
+
+ {% endframe %} + +{% endblock %} diff --git a/templates/tools/sitepackage/error.html.twig b/templates/tools/sitepackage/error.html.twig new file mode 100644 index 00000000..bb863ae9 --- /dev/null +++ b/templates/tools/sitepackage/error.html.twig @@ -0,0 +1,23 @@ +{% extends 'tools/layout.html.twig' %} + +{% block title %}Error{% endblock %} + +{% block body %} + + {% frame with { color: 'dark', center: true, title: 'Error', titleSize: 1, backgroundImage: asset("assets/Images/keyvisual.png") } %} +

+ Your sitepackage was not created, please check the error below. +

+ {% endframe %} + + {% frame with { id: 'error', indent: true, title: 'Error information' } %} +

{{ error }}

+

+ + {{ icon('actions-arrow-left-alt', 'auto') }} + Back + +

+ {% endframe %} + +{% endblock %} diff --git a/templates/tools/sitepackage/index.html.twig b/templates/tools/sitepackage/index.html.twig new file mode 100644 index 00000000..ab5b8bdd --- /dev/null +++ b/templates/tools/sitepackage/index.html.twig @@ -0,0 +1,38 @@ +{% extends 'tools/layout.html.twig' %} + +{% block title %}Kickstart your TYPO3 template development{% endblock %} + +{% block body %} + + {% frame with { color: 'dark', center: true, title: 'Sitepackage Builder', titleSize: 1, backgroundImage: asset("assets/Images/keyvisual.png") } %} +

+ Sitepackage-Builder is your kickstarter for modern TYPO3 Theme development. Learn more about TYPO3 templating + or start your own template right now. +

+

+ + {{ icon('actions-rocket', 'auto') }} + Create Sitepackage + +

+ {% endframe %} + + {% frame with { id: 'about', title: 'What is a Sitepackage?' } %} +

+ A Sitepackage is a TYPO3 Extension that containers all relevant configuration for a Website. + Having all configuration stored in a package keeps it protected from unauthorized access. + As Extension your Sitepackage will manage your dependencies to other Extensions and/or the TYPO3 Version. + This will ease your deployment and enables you to put the configuration of your Webiste under Version Control. +

+

+ Learn more about the best practices recommended from the TYPO3 Core Team. +

+

+ + {{ icon('actions-notebook', 'auto') }} + Learn about Sitepackages + +

+ {% endframe %} + +{% endblock %} diff --git a/templates/tools/sitepackage/new.html.twig b/templates/tools/sitepackage/new.html.twig new file mode 100644 index 00000000..7a87e261 --- /dev/null +++ b/templates/tools/sitepackage/new.html.twig @@ -0,0 +1,88 @@ +{% extends 'tools/layout.html.twig' %} + +{% block title %}Create your own TYPO3 Sitepackage{% endblock %} + +{% block body %} + + {% frame with { color: 'dark', center: true, title: 'Create your own Sitepackage', titleSize: 1, backgroundImage: asset("assets/Images/keyvisual.png") } %} +

+ Awesome you made it here! Just a few more details about your + project and your own Sitepackage is ready for download. +

+ {% endframe %} + + {% if basePackages|length > 0 %} + {% if not filtered %} + {% frame with { id: 'base-packages', center: true, title: 'Choose a Base Package to continue with' } %}{% endframe %} + {% endif %} + + {% frame %} +
+ {% for basePackage in basePackages %} +
+
+
+

{{ basePackage.title }}

+
+ + {% if basePackage.official %} + {{ basePackage.title }} + {% endif %} +
+

{{ basePackage.description }}

+
+ +
+
+ {% endfor %} +
+ {% endframe %} + + {% if filtered %} + {% frame with { center: true } %} +

+ + {{ icon('actions-rocket', 'auto') }} + Show all Base Packages + +

+ {% endframe %} + {% endif %} + {% else %} +
+

No BasePackages found.

+
+ {% endif %} + +{% endblock %} diff --git a/templates/tools/sitepackage/success.html.twig b/templates/tools/sitepackage/success.html.twig new file mode 100644 index 00000000..50afe07e --- /dev/null +++ b/templates/tools/sitepackage/success.html.twig @@ -0,0 +1,84 @@ +{% extends 'tools/layout.html.twig' %} + +{% block title %}Success{% endblock %} + +{% block body %} + + {% frame with { color: 'dark', center: true, title: 'Congratulations!', titleSize: 1, backgroundImage: asset("assets/Images/keyvisual.png") } %} +

+ Your Sitepackage has been successfully prepared. You can now check the + configuration or download the prepared Sitepackage. +

+ {% endframe %} + + {% frame with { id: 'configuration', indent: true, title: 'Your Sitepackage Configuration' } %} +
+
Base Package
+
{{ base_package.title }} ({{ base_package.composerPackageName }})
+
Source
+
{% if base_package.official %}TYPO3 official base package{% elseif base_package.thirdParty %}Unknown third party{% else %}Verified third party{% endif %}
+
TYPO3 Version
+
{{ sitepackage.typo3Version|version(2) }}
+
+

Sitepackage Extension

+
+
Titel
+
{{ sitepackage.title }}
+ {% if sitepackage.description %} +
Description
+
{{ sitepackage.description }}
+ {% endif %} +
Extension Key
+
{{ sitepackage.extensionKey }}
+ {% if sitepackage.repositoryUrl %} +
Repository
+
{{ sitepackage.repositoryUrl }}
+ {% endif %} +
+

PHP

+
+
Composer Name
+
{{ sitepackage.composerName }}
+
PSR-4 Namespace
+
{{ sitepackage.psr4Namespace }}
+
+

Author

+
+
Name
+
{{ sitepackage.author.name }}
+
E-Mail
+
{{ sitepackage.author.email }}
+
Company
+
{{ sitepackage.author.company }}
+
Homepage
+
{{ sitepackage.author.homepage }}
+
+

+ + {{ icon('actions-undo', 'auto') }} + Restart + + + {{ icon('actions-open', 'auto') }} + Edit Configuration + + + {{ icon('actions-download', 'auto') }} + Download + +

+ {% endframe %} + + {% frame with { color: 'dark', size: 'default', center: true } %} +

+ Your Sitepackage has been successfully created and is ready for download. +

+

+ + {{ icon('actions-download', 'auto') }} + Download + +

+ {% endframe %} + +{% endblock %} diff --git a/tests/Functional/Controller/Web/DefaultControllerTest.php b/tests/Functional/Controller/Web/DefaultControllerTest.php index c75f4e1b..5c6d5679 100644 --- a/tests/Functional/Controller/Web/DefaultControllerTest.php +++ b/tests/Functional/Controller/Web/DefaultControllerTest.php @@ -46,7 +46,7 @@ public function webDefault(): void { $this->client->request('GET', '/'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', 'Build Blazingly'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', 'Build Blazingly'); self::assertSelectorTextContains('#download-community-1 .btn', 'Get version 10'); self::assertSelectorTextContains('#download-community-2 .btn', 'Get version 9'); self::assertSelectorTextContains('#download-elts-1 .btn-primary', 'Buy ELTS'); @@ -70,7 +70,7 @@ public function webVersionSprint(): void { $this->client->request('GET', '/version/10'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', 'TYPO3 10'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', 'TYPO3 10'); } /** @@ -80,7 +80,7 @@ public function webVersionSpecific(): void { $this->client->request('GET', '/version/10.0.0'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', '(10.0.0)'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', '(10.0.0)'); self::assertSelectorNotExists('#notice-elts'); self::assertSelectorExists('#accordion-download'); } @@ -92,7 +92,7 @@ public function webVersionElts(): void { $this->client->request('GET', '/version/6.2'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', '(6.2.23 ELTS)'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', '(6.2.23 ELTS)'); self::assertSelectorExists('#notice-elts'); self::assertSelectorNotExists('#accordion-download'); } @@ -104,7 +104,7 @@ public function webVersionBeforeElts(): void { $this->client->request('GET', '/version/6.2.0'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', '(6.2.0)'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', '(6.2.0)'); self::assertSelectorExists('#notice-elts'); self::assertSelectorExists('#accordion-download'); } @@ -116,7 +116,7 @@ public function webVersionOutdated(): void { $this->client->request('GET', '/version/4.5.0'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', '(4.5.0)'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', '(4.5.0)'); self::assertSelectorExists('#notice-outdated'); self::assertSelectorExists('#accordion-download'); } @@ -128,7 +128,7 @@ public function webVersionOutdatedElts(): void { $this->client->request('GET', '/version/4.5'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', '(4.5.23 ELTS)'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', '(4.5.23 ELTS)'); self::assertSelectorExists('#notice-outdated'); self::assertSelectorNotExists('#accordion-download'); } @@ -150,7 +150,7 @@ public function weReleaseNotesSprint(): void { $this->client->request('GET', '/release-notes/10'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', '10.0.5'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', '10.0.5'); } /** @@ -160,7 +160,7 @@ public function webReleaseNotesSpecific(): void { $this->client->request('GET', '/release-notes/10.0.0'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', '10.0.0'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', '10.0.0'); self::assertSelectorNotExists('#notice-elts'); } @@ -171,7 +171,7 @@ public function webReleaseNotesElts(): void { $this->client->request('GET', '/release-notes/6.2'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', '6.2.23 ELTS'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', '6.2.23 ELTS'); self::assertSelectorExists('#notice-elts'); } @@ -182,7 +182,7 @@ public function webReleaseNotesBeforeElts(): void { $this->client->request('GET', '/release-notes/6.2.0'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', '6.2.0'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', '6.2.0'); self::assertSelectorExists('#notice-elts'); } @@ -193,7 +193,7 @@ public function webReleaseNotesOutdated(): void { $this->client->request('GET', '/release-notes/4.5.0'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', '4.5.0'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', '4.5.0'); self::assertSelectorExists('#notice-outdated'); } @@ -204,7 +204,7 @@ public function webReleaseNotesOutdatedElts(): void { $this->client->request('GET', '/release-notes/4.5'); self::assertResponseIsSuccessful(); - self::assertSelectorTextContains('h1', '4.5.23 ELTS'); + self::assertSelectorTextContains('div.frame-container:nth-child(2) > div:nth-child(1) > h1:nth-child(1)', '4.5.23 ELTS'); self::assertSelectorExists('#notice-outdated'); } } diff --git a/tmp/.gitkeep b/tmp/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/tmp/base-packages/.gitignore b/tmp/base-packages/.gitignore new file mode 100644 index 00000000..a68d087b --- /dev/null +++ b/tmp/base-packages/.gitignore @@ -0,0 +1,2 @@ +/* +!/.gitignore