diff --git a/.concrete/index.maintenance.php b/.concrete/index.maintenance.php
new file mode 100644
index 0000000..5c4677c
--- /dev/null
+++ b/.concrete/index.maintenance.php
@@ -0,0 +1,18 @@
+
+
Site Maintenance
+
+
+
+ We’ll be back soon!
+
+
We’re performing some maintenance at the moment, please check back soon
+
+
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..42aa49d
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,15 @@
+# Path-based git attributes
+# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
+
+# Ignore unneeded stuff "export-ignore".
+/.editorconfig export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.phpstorm.meta.php export-ignore
+/.psalm export-ignore
+/.github export-ignore
+/ISSUE_TEMPLATE.md export-ignore
+/psalm.xml.dist export-ignore
+/phpunit.xml.dist export-ignore
+/tests export-ignore
+/docs export-ignore
diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml
new file mode 100644
index 0000000..6b1d1fe
--- /dev/null
+++ b/.github/workflows/phpcs.yml
@@ -0,0 +1,34 @@
+name: PHPCS
+
+on: [push, pull_request]
+
+jobs:
+ phpcs:
+ name: PHPCS
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@releases/v2
+ with:
+ php-version: 8.0
+ extensions: mbstring
+
+ - name: Get composer cache directory
+ id: composer-cache
+ run: echo "::set-output name=dir::$(composer config cache-files-dir)"
+
+ - name: Cache dependencies
+ uses: actions/cache@v2
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist
+
+ - name: Run PHPCS
+ run: ./vendor/bin/phpcs --ignore=fixtures src tests
diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
new file mode 100644
index 0000000..eda225a
--- /dev/null
+++ b/.github/workflows/phpunit.yml
@@ -0,0 +1,40 @@
+name: PHPUnit
+
+on: [push, pull_request]
+
+jobs:
+ phpunit:
+ name: PHPUnit
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ php: ['7.1', '8.0']
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup PHP with specific version of PECL extension
+ uses: shivammathur/setup-php@releases/v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: mbstring
+
+ - name: Get composer cache directory
+ id: composer-cache
+ run: echo "::set-output name=dir::$(composer config cache-files-dir)"
+
+ - name: Cache dependencies
+ uses: actions/cache@v2
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist
+
+ - name: Configure matchers
+ uses: mheap/phpunit-matcher-action@v1
+
+ - name: Run PHPUnit
+ run: ./vendor/bin/phpunit
diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml
new file mode 100644
index 0000000..c3b9875
--- /dev/null
+++ b/.github/workflows/psalm.yml
@@ -0,0 +1,22 @@
+name: Psalm
+
+on: [push, pull_request]
+
+jobs:
+ psalm:
+ name: Psalm
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Psalm
+ uses: docker://vimeo/psalm-github-actions
+ with:
+ security_analysis: true
+ report_file: results.sarif
+
+ - name: Upload Security Analysis results to GitHub
+ uses: github/codeql-action/upload-sarif@v1
+ with:
+ sarif_file: results.sarif
diff --git a/.gitignore b/.gitignore
index 7e9f2d9..8dfbb5b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
.DS_Store
.idea
-vendor
\ No newline at end of file
+vendor
+build
+composer.lock
+.phpunit.result.cache
diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php
new file mode 100755
index 0000000..4028a53
--- /dev/null
+++ b/.phpstorm.meta.php
@@ -0,0 +1,12 @@
+ '@',
+]));
+override(Application::make(0), map([
+ '' => '@',
+]));
diff --git a/.psalm/plugins/PsrContainerChecker.php b/.psalm/plugins/PsrContainerChecker.php
new file mode 100644
index 0000000..6556771
--- /dev/null
+++ b/.psalm/plugins/PsrContainerChecker.php
@@ -0,0 +1,68 @@
+classImplements($className, ContainerInterface::class)
+ ) {
+ return;
+ }
+
+ $arg = $expr->args[0] ?? null;
+ if ($arg === null || ! $arg->value instanceof ClassConstFetch) {
+ return;
+ }
+
+ $class = $arg->value->class;
+ if (! $class->hasAttribute('resolvedName')) {
+ return;
+ }
+
+ $return_type_candidate = new Union([
+ new TNamedObject(
+ (string) $class->getAttribute('resolvedName')
+ ),
+ ]);
+ }
+}
diff --git a/.psalm/stubs/Composer/Semver/Comparator.php.stub b/.psalm/stubs/Composer/Semver/Comparator.php.stub
new file mode 100644
index 0000000..c818edb
--- /dev/null
+++ b/.psalm/stubs/Composer/Semver/Comparator.php.stub
@@ -0,0 +1,107 @@
+ $version2.
+ *
+ * @param string $version1
+ * @param string $version2
+ *
+ * @return bool
+ *
+ * @psalm-pure
+ */
+ public static function greaterThan($version1, $version2)
+ {
+ }
+
+ /**
+ * Evaluates the expression: $version1 >= $version2.
+ *
+ * @param string $version1
+ * @param string $version2
+ *
+ * @return bool
+ *
+ * @psalm-pure
+ */
+ public static function greaterThanOrEqualTo($version1, $version2)
+ {
+ }
+
+ /**
+ * Evaluates the expression: $version1 < $version2.
+ *
+ * @param string $version1
+ * @param string $version2
+ *
+ * @return bool
+ *
+ * @psalm-pure
+ */
+ public static function lessThan($version1, $version2)
+ {
+ }
+
+ /**
+ * Evaluates the expression: $version1 <= $version2.
+ *
+ * @param string $version1
+ * @param string $version2
+ *
+ * @return bool
+ *
+ * @psalm-pure
+ */
+ public static function lessThanOrEqualTo($version1, $version2)
+ {
+ }
+
+ /**
+ * Evaluates the expression: $version1 == $version2.
+ *
+ * @param string $version1
+ * @param string $version2
+ *
+ * @return bool
+ *
+ * @psalm-pure
+ */
+ public static function equalTo($version1, $version2)
+ {
+ }
+
+ /**
+ * Evaluates the expression: $version1 != $version2.
+ *
+ * @param string $version1
+ * @param string $version2
+ *
+ * @return bool
+ *
+ * @psalm-pure
+ */
+ public static function notEqualTo($version1, $version2)
+ {
+ }
+
+ /**
+ * Evaluates the expression: $version1 $operator $version2.
+ *
+ * @param string $version1
+ * @param string $operator
+ * @param string $version2
+ *
+ * @return bool
+ *
+ * @psalm-pure
+ */
+ public static function compare($version1, $operator, $version2)
+ {
+ }
+}
diff --git a/.psalm/stubs/Concrete/Core/Application.php.stub b/.psalm/stubs/Concrete/Core/Application.php.stub
new file mode 100644
index 0000000..fe42b63
--- /dev/null
+++ b/.psalm/stubs/Concrete/Core/Application.php.stub
@@ -0,0 +1,6 @@
+ ncreteconcreteconcreteconcreteconcre
+ concreteconcreteconcreteconcreteconcrete
+ | concreteconcr--econc teconcreteconcrete
+ | concreteconc econ tecon.reteconcrete
+ | concreteconc. econ teco :eteconcrete
+ | concre- .oncr con tec. reteconcrete
+ | concret :ncr con. tec 'reteconcrete
+ | concret: ncr: conc .te. creteconcrete
+ | concrete. :cret conc ete ncreteconcrete
+ | concreteco re:. .:oncreteconcrete
+ | concretecon . . . .ncre..concrete
+ | concreteco .concreteconc. .oncrete
+ | concretec econcreteco. econcrete
+ | concretec: :concrete. :teconcrete
+ | concreteco: .ncr. :reteconcrete
+ | concreteconcr:. .oncreteconcrete
+ | concreteconcreteconcreteconcreteconcrete
+ \ oncreteconcreteconcreteconcreteconcre
+ ----------------------------------
diff --git a/art/concrete-symfony.txt b/art/concrete-symfony.txt
new file mode 100644
index 0000000..3841c97
--- /dev/null
+++ b/art/concrete-symfony.txt
@@ -0,0 +1,20 @@
+
+ ncreteconcreteconcreteconcreteconcre
+ concreteconcreteconcreteconcreteconcrete
+ |> concreteconcr--econc teconcreteconcrete
+ |> concreteconc econ tecon.reteconcrete
+ |> concreteconc. econ teco :eteconcrete
+ |> concre- .oncr con tec. reteconcrete
+ |> concret :ncr con. tec 'reteconcrete
+ |> concret: ncr: conc .te. creteconcrete
+ |> concrete. :cret conc ete ncreteconcrete
+ |> concreteco re:. .:oncreteconcrete
+ |> concretecon . . . .ncre..concrete
+ |> concreteco .concreteconc. .oncrete
+ |> concretec econcreteco. econcrete
+ |> concretec: :concrete. :teconcrete
+ |> concreteco: .ncr. :reteconcrete
+ |> concreteconcr:. .oncreteconcrete
+ |> concreteconcreteconcreteconcreteconcrete
+ \ >oncreteconcreteconcreteconcreteconcre
+ ---------------------------------->>
diff --git a/art/concrete.txt b/art/concrete.txt
new file mode 100644
index 0000000..89d9e3b
--- /dev/null
+++ b/art/concrete.txt
@@ -0,0 +1,20 @@
+
+ ncreteconcreteconcreteconcreteconcre
+ concreteconcreteconcreteconcreteconcrete
+ | concreteconcr--econc teconcreteconcrete
+ | concreteconc econ tecon.reteconcrete
+ | concreteconc. econ teco :eteconcrete
+ | concre- .oncr con tec. reteconcrete
+ | concret :ncr con. tec 'reteconcrete
+ | concret: ncr: conc .te. creteconcrete
+ | concrete. :cret conc ete ncreteconcrete
+ | concreteco re:. .:oncreteconcrete
+ | concretecon . . . .ncre..concrete
+ | concreteco .concreteconc. .oncrete
+ | concretec econcreteco. econcrete
+ | concretec: :concrete. :teconcrete
+ | concreteco: .ncr. :reteconcrete
+ | concreteconcr:. .oncreteconcrete
+ | concreteconcreteconcreteconcreteconcrete
+ \ oncreteconcreteconcreteconcreteconcre
+ ----------------------------------
diff --git a/bin/concrete.php b/bin/concrete.php
index 6a9fab1..1233271 100755
--- a/bin/concrete.php
+++ b/bin/concrete.php
@@ -1,16 +1,63 @@
register();
+
+// Set up DI container
+$container = new Container();
+$container->delegate(new ReflectionContainer());
+$container->addServiceProvider(ConcreteServiceProvider::class);
+$container->addServiceProvider(InstallationServiceProvider::class);
+
+// Set up an inflector for the container itself
+$container->inflector(\League\Container\ContainerAwareInterface::class)
+ ->invokeMethod('setContainer', [$container]);
+
+// Setup console application
+$application = new Application($container);
+
+// Set up an inflector for the console
+$container->inflector(Command\ConsoleAwareInterface::class)
+ ->invokeMethod('setConsole', [$application]);
+
+// Run
+try {
+ // Add commands
+ $application->run();
+} catch(Exception\Installation\InstallationNotFound $e) {
+ $output = $application->getOutputStyle();
+
+ if ($output->isVeryVerbose()) {
+ throw $e;
+ }
+ $output->error($e->getMessage());
+ die($e->getCode());
+} catch (\Throwable $e) {
+ $output = $application->getOutputStyle();
-$application = new Application();
-$application->addCommands(
- [
- new \Concrete\Console\Command\InfoCommand(),
- new \Concrete\Console\Command\Backup\BackupCommand(),
- new \Concrete\Console\Command\Database\DumpCommand(),
+ if ($output->isVeryVerbose()) {
+ throw $e;
+ }
- ]
-);
-$application->run();
+ $output->error($e->getMessage());
+ die($e->getCode());
+}
diff --git a/composer.json b/composer.json
index b1a93a8..b74f2db 100644
--- a/composer.json
+++ b/composer.json
@@ -4,12 +4,22 @@
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
- "symfony/console": "^4.1",
- "symfony/process": "^5.2"
+ "php": "^7.1|^8.0",
+ "ext-json": "*",
+ "ext-pdo": "*",
+ "symfony/process": "^3|^4|^5",
+ "league/container": "^3.3",
+ "mnapoli/silly": "^1.7",
+ "composer/semver": "^3.2",
+ "league/climate": "^3.5",
+ "league/flysystem": "^1.0"
},
"bin": [
"bin/concrete"
],
+ "conflict": {
+ "concrete5/core": "*"
+ },
"autoload": {
"psr-4": {
"Concrete\\Console\\": "src/"
@@ -17,5 +27,20 @@
"files": [
"helpers.php"
]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Concrete\\Console\\": "tests/"
+ },
+ "files": [
+ "helpers.php"
+ ]
+ },
+ "require-dev": {
+ "ext-simplexml": "*",
+ "nunomaduro/collision": "^3|^4|^5",
+ "phpunit/phpunit": "^7|^8|^9",
+ "mockery/mockery": "^1.3",
+ "squizlabs/php_codesniffer": "^3.5"
}
}
diff --git a/composer.lock b/composer.lock
deleted file mode 100644
index 5c05478..0000000
--- a/composer.lock
+++ /dev/null
@@ -1,517 +0,0 @@
-{
- "_readme": [
- "This file locks the dependencies of your project to a known state",
- "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
- "This file is @generated automatically"
- ],
- "content-hash": "19a720cc3fa5b97a87e97637d8abdf9a",
- "packages": [
- {
- "name": "psr/container",
- "version": "1.1.1",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/container.git",
- "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
- "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
- "shasum": ""
- },
- "require": {
- "php": ">=7.2.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Psr\\Container\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "Common Container Interface (PHP FIG PSR-11)",
- "homepage": "https://github.com/php-fig/container",
- "keywords": [
- "PSR-11",
- "container",
- "container-interface",
- "container-interop",
- "psr"
- ],
- "time": "2021-03-05T17:36:06+00:00"
- },
- {
- "name": "symfony/console",
- "version": "v4.4.20",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/console.git",
- "reference": "c98349bda966c70d6c08b4cd8658377c94166492"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/c98349bda966c70d6c08b4cd8658377c94166492",
- "reference": "c98349bda966c70d6c08b4cd8658377c94166492",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1.3",
- "symfony/polyfill-mbstring": "~1.0",
- "symfony/polyfill-php73": "^1.8",
- "symfony/polyfill-php80": "^1.15",
- "symfony/service-contracts": "^1.1|^2"
- },
- "conflict": {
- "symfony/dependency-injection": "<3.4",
- "symfony/event-dispatcher": "<4.3|>=5",
- "symfony/lock": "<4.4",
- "symfony/process": "<3.3"
- },
- "provide": {
- "psr/log-implementation": "1.0"
- },
- "require-dev": {
- "psr/log": "~1.0",
- "symfony/config": "^3.4|^4.0|^5.0",
- "symfony/dependency-injection": "^3.4|^4.0|^5.0",
- "symfony/event-dispatcher": "^4.3",
- "symfony/lock": "^4.4|^5.0",
- "symfony/process": "^3.4|^4.0|^5.0",
- "symfony/var-dumper": "^4.3|^5.0"
- },
- "suggest": {
- "psr/log": "For using the console logger",
- "symfony/event-dispatcher": "",
- "symfony/lock": "",
- "symfony/process": ""
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Console\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Eases the creation of beautiful and testable command line interfaces",
- "homepage": "https://symfony.com",
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2021-02-22T18:44:15+00:00"
- },
- {
- "name": "symfony/polyfill-mbstring",
- "version": "v1.22.1",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "5232de97ee3b75b0360528dae24e73db49566ab1"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1",
- "reference": "5232de97ee3b75b0360528dae24e73db49566ab1",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1"
- },
- "suggest": {
- "ext-mbstring": "For best performance"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "1.22-dev"
- },
- "thanks": {
- "name": "symfony/polyfill",
- "url": "https://github.com/symfony/polyfill"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Polyfill\\Mbstring\\": ""
- },
- "files": [
- "bootstrap.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony polyfill for the Mbstring extension",
- "homepage": "https://symfony.com",
- "keywords": [
- "compatibility",
- "mbstring",
- "polyfill",
- "portable",
- "shim"
- ],
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2021-01-22T09:19:47+00:00"
- },
- {
- "name": "symfony/polyfill-php73",
- "version": "v1.22.1",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/polyfill-php73.git",
- "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2",
- "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "1.22-dev"
- },
- "thanks": {
- "name": "symfony/polyfill",
- "url": "https://github.com/symfony/polyfill"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Polyfill\\Php73\\": ""
- },
- "files": [
- "bootstrap.php"
- ],
- "classmap": [
- "Resources/stubs"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
- "homepage": "https://symfony.com",
- "keywords": [
- "compatibility",
- "polyfill",
- "portable",
- "shim"
- ],
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2021-01-07T16:49:33+00:00"
- },
- {
- "name": "symfony/polyfill-php80",
- "version": "v1.22.1",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91",
- "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "1.22-dev"
- },
- "thanks": {
- "name": "symfony/polyfill",
- "url": "https://github.com/symfony/polyfill"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Polyfill\\Php80\\": ""
- },
- "files": [
- "bootstrap.php"
- ],
- "classmap": [
- "Resources/stubs"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Ion Bazan",
- "email": "ion.bazan@gmail.com"
- },
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
- "homepage": "https://symfony.com",
- "keywords": [
- "compatibility",
- "polyfill",
- "portable",
- "shim"
- ],
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2021-01-07T16:49:33+00:00"
- },
- {
- "name": "symfony/process",
- "version": "v5.2.4",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/process.git",
- "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/313a38f09c77fbcdc1d223e57d368cea76a2fd2f",
- "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f",
- "shasum": ""
- },
- "require": {
- "php": ">=7.2.5",
- "symfony/polyfill-php80": "^1.15"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Process\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Executes commands in sub-processes",
- "homepage": "https://symfony.com",
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2021-01-27T10:15:41+00:00"
- },
- {
- "name": "symfony/service-contracts",
- "version": "v2.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/service-contracts.git",
- "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1",
- "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1",
- "shasum": ""
- },
- "require": {
- "php": ">=7.2.5",
- "psr/container": "^1.0"
- },
- "suggest": {
- "symfony/service-implementation": ""
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.2-dev"
- },
- "thanks": {
- "name": "symfony/contracts",
- "url": "https://github.com/symfony/contracts"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Contracts\\Service\\": ""
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Generic abstractions related to writing services",
- "homepage": "https://symfony.com",
- "keywords": [
- "abstractions",
- "contracts",
- "decoupling",
- "interfaces",
- "interoperability",
- "standards"
- ],
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2020-09-07T11:33:47+00:00"
- }
- ],
- "packages-dev": [],
- "aliases": [],
- "minimum-stability": "dev",
- "stability-flags": [],
- "prefer-stable": true,
- "prefer-lowest": false,
- "platform": [],
- "platform-dev": [],
- "plugin-api-version": "1.1.0"
-}
diff --git a/helpers.php b/helpers.php
index e69de29..f2ba7cc 100755
--- a/helpers.php
+++ b/helpers.php
@@ -0,0 +1,121 @@
+setOptions($options);
+ }
+
+ return $process;
+}
+
+function stdoutToOutput(ConsoleOutputInterface $output): Closure
+{
+ return function($message, $channel) use ($output): void {
+ switch ($channel) {
+ case 'STDOUT':
+ $output->writeln($message);
+ break;
+ case 'STDERR':
+ $output->getErrorOutput()->writeln($message, OutputInterface::OUTPUT_RAW);
+ break;
+ default:
+ dd('Unknown message type: ' . $channel);
+ }
+ };
+}
+
+if (!function_exists('dot_get')) {
+ /**
+ * @template T
+ * @param array $array
+ * @param string $key
+ * @psalm-param T $default
+ * @return T|mixed
+ */
+ function dot_get(array $array, string $key, $default = null)
+ {
+ if (is_null($key)) {
+ return $array;
+ }
+
+ if (array_key_exists($key, $array)) {
+ return $array[$key];
+ }
+
+ if (strpos($key, '.') === false) {
+ return isset($array[$key]) ? $array[$key] : $default;
+ }
+
+ foreach (explode('.', $key) as $segment) {
+ if (is_array($array) && array_key_exists($segment, $array)) {
+ $array = $array[$segment];
+ } else {
+ return $default;
+ }
+ }
+
+ return $array;
+ }
+}
+
+if (!function_exists('snake_case')) {
+ function snake_case(string $original, string $delimiter = '_'): string
+ {
+ $result = $original;
+ if (! ctype_lower($result)) {
+ $result = preg_replace('/\s+/u', '', ucwords($result));
+
+ $result = mb_strtolower(preg_replace('/(.)(?=[A-Z])/u', '$1'.$delimiter, $result), 'UTF-8');
+ }
+
+ return $result;
+ }
+}
+
+if (!function_exists('class_basename')) {
+ function class_basename($classOrInstance): string {
+ if (!is_string($classOrInstance)) {
+ $classOrInstance = get_class($classOrInstance);
+ }
+
+ $segments = explode('\\', $classOrInstance);
+ return array_pop($segments);
+ }
+}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000..c2f9bc3
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,14 @@
+
+
+ The coding standard of concrete/console package
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..199119e
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,28 @@
+
+
+
+
+ tests
+
+
+
+
+ src/
+
+
+
+
+
+
+
+
+
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..1c884a0
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Application.php b/src/Application.php
index 39567f8..0bf0452 100644
--- a/src/Application.php
+++ b/src/Application.php
@@ -2,48 +2,113 @@
namespace Concrete\Console;
-use Concrete\Console\Bootstrap\Booter;
-use Concrete\Console\Command\InstallationAwareCommandInterface;
-use Concrete\Console\Installation\Factory;
-use Concrete\Console\Installation\Validator;
-use Symfony\Component\Console\Application as SymfonyApplication;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\ArgvInput;
+use Concrete\Console\Command\CommandProvider;
+use Concrete\Console\Command\Output;
+use Concrete\Console\Command\OutputStyle;
+use Concrete\Console\Command\OutputStyleAwareInterface;
+use League\Container\Container;
+use Silly\Application as SillyApplication;
+use Silly\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class Application extends SymfonyApplication
+/**
+ * @method Container getContainer()
+ */
+class Application extends SillyApplication
{
- public function __construct()
+ /**
+ * @var false
+ */
+ protected $registered = false;
+
+ /**
+ * @var InputInterface
+ */
+ protected $input;
+
+ /**
+ * @var OutputInterface
+ */
+ protected $output;
+
+ /**
+ * @var OutputStyle
+ */
+ protected $style;
+
+ public function __construct(Container $container)
{
- parent::__construct('Concrete Console', '0.8');
+ parent::__construct('Concrete Console', '0.1');
+ $this->useContainer($container, true, true);
}
- public function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
+ public function doRun(InputInterface $input, OutputInterface $output)
{
- if ($command instanceof InstallationAwareCommandInterface) {
- $command->mergeApplicationDefinition();
- $input->bind($command->getDefinition());
- $path = $command->getInstallation($input) ?? null;
-
- $installationFactory = new Factory();
- if ($path) {
- // Turn the path into something real in case it's relative
- $path = $installationFactory->normalizePath($path);
- $installation = $installationFactory->createFromPath($path);
- } else {
- $installation = $installationFactory->detectInstallation();
- }
-
- $validator = new Validator();
- if ($validator->isValid($installation)) {
- $booter = new Booter();
- $booter->boot($installation);
- }
+ $this->input = $input;
+ $this->output = $output;
+ $this->style = new OutputStyle($input, $output);
+
+ try {
+ // Loads in default stuff
+ $input->bind($this->getDefinition());
+ } catch (\Throwable $e) {
+ // Errors must be ignored, full binding/validation happens later when the command is known.
}
- return parent::doRunCommand($command, $input, $output); // TODO: Change the autogenerated stub
+
+ // Add input, output, and OutputStyle to container
+ $this->getContainer()->add(InputInterface::class, $input);
+ $this->getContainer()->add(OutputInterface::class, $output);
+ $this->getContainer()->add(ConsoleOutputInterface::class, $output);
+ $this->getContainer()->add(OutputStyle::class, $this->style);
+ $this->getContainer()->inflector(OutputStyleAwareInterface::class)
+ ->invokeMethod('setOutputStyle', [OutputStyle::class]);
+
+ // Add commands
+ CommandProvider::register($this->getContainer(), $this);
+
+ return parent::doRun($input, $output);
+ }
+
+ protected function getDefaultInputDefinition()
+ {
+ $definition = parent::getDefaultInputDefinition();
+
+ $definition->addOptions([
+ new InputOption('--instance', '-I', InputOption::VALUE_REQUIRED, 'Specify the concrete5 directory', '.'),
+ ]);
+
+ return $definition;
}
-}
\ No newline at end of file
+ public function getHelp()
+ {
+ $artWidth = 42;
+
+ // Get the width of the title, excluding the version on purpose
+ $titleWidth = strlen($this->getName());
+
+ // Make help a little fancy
+ return implode('', [
+ file_get_contents(__DIR__ . '/../art/concrete-symfony.txt'),
+ str_repeat(' ', (int) ceil(($artWidth - $titleWidth) / 2)),
+ "{$this->getName()} {$this->getVersion()}>>",
+ ]);
+ }
+
+ public function getInput(): InputInterface
+ {
+ return $this->input;
+ }
+ public function getOutput(): OutputInterface
+ {
+ return $this->output;
+ }
+
+ public function getOutputStyle(): OutputStyle
+ {
+ return $this->style;
+ }
+}
diff --git a/src/Bootstrap/Booter.php b/src/Bootstrap/Booter.php
deleted file mode 100644
index 8193434..0000000
--- a/src/Bootstrap/Booter.php
+++ /dev/null
@@ -1,24 +0,0 @@
-getPath() . '/application/config');
- define('DIR_BASE', $installation->getPath());
-
- // Include configuration
- require $installation->getPath() . '/concrete/bootstrap/configure.php';
-
- // Include autoloaders
- require $installation->getPath() . '/concrete/bootstrap/autoload.php';
-
- // Begin startup
- $app = require $installation->getPath() . '/concrete/bootstrap/start.php';
- }
-}
\ No newline at end of file
diff --git a/src/Command/AbstractInstallationCommand.php b/src/Command/AbstractInstallationCommand.php
deleted file mode 100644
index be55fa6..0000000
--- a/src/Command/AbstractInstallationCommand.php
+++ /dev/null
@@ -1,26 +0,0 @@
-addInstallationOption();
- }
-
- protected function addInstallationOption()
- {
- $this->addOption('installation', 'i', InputOption::VALUE_OPTIONAL, 'Concrete installation to run command against.');
- }
-
- public function getInstallation(InputInterface $input): ?string
- {
- return $input->hasOption('installation') ? $input->getOption('installation') : null;
- }
-}
\ No newline at end of file
diff --git a/src/Command/Backup/BackupCommand.php b/src/Command/Backup/BackupCommand.php
index 339ceae..9dce282 100644
--- a/src/Command/Backup/BackupCommand.php
+++ b/src/Command/Backup/BackupCommand.php
@@ -1,161 +1,301 @@
setName('backup:backup')
- ->setDescription('Generates a Concrete installation backup.')
- ->addArgument('file', InputArgument::OPTIONAL, 'Filename for the backup.')
- ->addOption('skip-core', null, InputOption::VALUE_NONE, 'Does not include the Concrete core in the archive.');
+ $this->skipCore = (bool)$input->getOption('skip-core');
+
+ // Make a new directory to back up in
+ $backupDirectory = Platform::tempDirectory(true);
+ /** @var string $directory */
+ $directory = $input->getOption('dir');
+
+ if (!file_exists($directory)) {
+ mkdir($directory, 0777, true);
+ }
+
+ $app = $this->getApplication();
+ /** @var Site $site */
+ $site = $app->make('site')->getSite();
+ $manifest = (new Manifest())
+ ->setHostName(gethostname())
+ ->setVersion($this->getInstallation()->getVersion()->getVersion())
+ ->setInstallationPath($this->getInstallation()->getPath())
+ ->setSiteName($site->getSiteName())
+ ->setUrl((string)$site->getSiteCanonicalURL());
+
+ $e = null;
+ try {
+ $manifest = $this->exportDatabase($manifest, $backupDirectory);
+ $manifest = $this->exportFiles($manifest, $backupDirectory);
+ $manifest = $this->exportApplication($manifest, $backupDirectory);
+ $manifest = $this->exportPackages($manifest, $backupDirectory);
+ $manifest = $this->exportCore($manifest, $backupDirectory);
+ $manifest = $this->exportIndexEntrypoint($manifest, $backupDirectory);
+ $this->writeManifest($manifest, $backupDirectory);
+
+ // Finalize the backup
+ $this->compressDirectory($backupDirectory, $directory, $filename);
+ } catch (Throwable $e) {
+ // Rethrow any errors
+ throw $e;
+ } finally {
+ $this->output->writeln('Removing temporary directory...');
+
+ $rmProcess = new Process(
+ [
+ 'rm',
+ '-r',
+ $backupDirectory
+ ]
+ );
+ $rmProcess->setTimeout(null);
+ $rmProcess->run();
+ }
+
+ $this->output->success('Backup complete!');
+ return 0;
}
- protected function exportDatabase(array &$manifest, string $directory, InputInterface $input, OutputInterface $output)
+ /**
+ * @return void
+ */
+ public static function register(Container $container, Application $console): void
+ {
+ $console
+ ->command(
+ 'backup:backup [filename] [--skip-core] [--temp] [--dir=]',
+ self::class,
+ ['backup']
+ )
+ ->descriptions(
+ 'Generate a Concrete installation backup',
+ [
+ 'filename' => 'Filename to use',
+ '--skip-core' => 'Does not include the Concrete core in the archive.',
+ '--temp' => 'Store relative to the concrete temp folder',
+ '--dir' => 'The directory to store the backup in'
+ ]
+ )->defaults(
+ [
+ 'dir' => Platform::configDirectory() . '/backups'
+ ]
+ );
+ }
+
+ protected function exportDatabase(Manifest $manifest, string $directory): Manifest
{
$dbOutput = $directory . DIRECTORY_SEPARATOR . 'db';
mkdir($dbOutput);
$dbOutput = $dbOutput . '/db.sql';
- $command = $this->getApplication()->find('database:dump');
- $arguments = ['file' => $dbOutput];
- $backupInput = new ArrayInput($arguments);
- $command->run($backupInput, $output);
+ $this->getConsole()->runCommand("database:dump '{$dbOutput}'");
- $manifest['contents']['database'] = true;;
+ return $manifest->setDatabase('db/db.sql');
}
- protected function exportIndexEntrypoint(array &$manifest, string $directory, InputInterface $input, OutputInterface $output)
+ protected function exportIndexEntrypoint(Manifest $manifest, string $directory): Manifest
{
- copy(DIR_BASE . DIRECTORY_SEPARATOR . DISPATCHER_FILENAME, $directory . DIRECTORY_SEPARATOR . DISPATCHER_FILENAME);
- $manifest['contents']['index'] = true;;
+ copy(
+ DIR_BASE . DIRECTORY_SEPARATOR . DISPATCHER_FILENAME,
+ $directory . DIRECTORY_SEPARATOR . DISPATCHER_FILENAME
+ );
+
+ return $manifest->setIncludeIndex(true);
}
- protected function exportFiles(array &$manifest, string $directory, InputInterface $input, OutputInterface $output)
+ protected function exportFiles(Manifest $manifest, string $directory): Manifest
{
$storageOutput = $directory . DIRECTORY_SEPARATOR . 'storage';
mkdir($storageOutput);
// loop through all storage locations
- $app = Facade::getFacadeApplication();
+ $app = $this->getApplication();
$fileStorageFactory = $app->make(StorageLocationFactory::class);
$storageLocations = $fileStorageFactory->fetchList();
- foreach($storageLocations as $storageLocation) {
+ /** @var StorageLocation $storageLocation */
+ foreach ($storageLocations as $storageLocation) {
$configuration = $storageLocation->getConfigurationObject();
if ($configuration instanceof LocalConfiguration) {
- $output->writeln(
- sprintf("Adding files from storage location: '%s' (%s)",
+ $this->output->writeln(
+ sprintf(
+ "Adding files from storage location: '%s' (%s)",
$storageLocation->getName(),
$configuration->getRootPath()
)
);
- $storageLocationOutput = $storageOutput . DIRECTORY_SEPARATOR . $storageLocation->getID();
+ $storageLocationOutput = $storageOutput . DIRECTORY_SEPARATOR . $storageLocation->getID() . '/';
mkdir($storageLocationOutput);
// Copy files from the storage location.
- $rsyncProcess = new Process([
- 'rsync',
- '-av',
- '-progress',
- $configuration->getRootPath(),
- $storageLocationOutput,
- '--exclude=tmp/',
- '--exclude=cache/',
- '--exclude=incoming/'
- ]);
+ $rsyncProcess = new Process(
+ [
+ 'rsync',
+ '-avL',
+ '--progress',
+ rtrim($configuration->getRootPath(), '/') . '/',
+ $storageLocationOutput,
+ '--exclude=tmp/',
+ '--exclude=cache/',
+ '--exclude=incoming/'
+ ]
+ );
$rsyncProcess->setTimeout(null);
$rsyncProcess->run();
if (!$rsyncProcess->isSuccessful()) {
- throw \Exception($rsyncProcess->getErrorOutput());
+ throw new Exception($rsyncProcess->getErrorOutput());
}
- $manifest['contents']['files'] = true;
-
+ $manifest = $manifest->addStorageLocation(
+ $storageLocation->getID(),
+ $storageLocation->getName(),
+ $storageLocation->isDefault(),
+ true
+ );
} else {
- $output->writeln(
- sprintf("** Alert! File storage location '%s' is not an instance of local configuration. It will not be included in this backup.",
- $storageLocation->getName()
+ $this->output->writeln(
+ sprintf(
+ "** Alert! File storage location '%s' is not an instance of local configuration." .
+ "It will not be included in this backup.",
+ $storageLocation->getName()
)
);
}
}
+ return $manifest;
}
- protected function writeManifest(array $manifest, string $directory)
+ protected function writeManifest(Manifest $manifest, string $directory): Manifest
{
file_put_contents($directory . DIRECTORY_SEPARATOR . 'manifest.json', json_encode($manifest));
+ return $manifest;
}
- protected function exportApplication(array &$manifest, string $directory, InputInterface $input, OutputInterface $output)
+ protected function exportApplication(Manifest $manifest, string $directory): Manifest
{
- $output->writeln('Exporting application/ directory...');
- $rsyncProcess = new Process([
- 'rsync',
- '-av',
- '-progress',
- DIR_APPLICATION,
- $directory,
- '--exclude=config/doctrine/',
- '--exclude=files/',
- ]);
+ $this->output->writeln('Exporting application/ directory...');
+ $rsyncProcess = new Process(
+ [
+ 'rsync',
+ '-avL',
+ '-progress',
+ DIR_APPLICATION,
+ $directory,
+ '--exclude=config/doctrine/',
+ '--exclude=config/*.database.php',
+ '--exclude=config/database.php',
+ '--exclude=files/',
+ ]
+ );
$rsyncProcess->setTimeout(null);
$rsyncProcess->run();
if (!$rsyncProcess->isSuccessful()) {
- throw new \Exception($rsyncProcess->getErrorOutput());
+ throw new Exception($rsyncProcess->getErrorOutput());
}
- $manifest['contents']['application'] = true;
- $manifest['contents']['applicationContents'] = [];
- foreach(['attributes', 'authentication', 'blocks', 'bootstrap', 'config', 'controllers', 'elements',
- 'jobs', 'languages', 'mail', 'page_templates', 'single_pages', 'src', 'themes', 'tools', 'views'] as $item) {
- $manifest['contents']['applicationContents'][] = $item;
- }
+ return $manifest->addApplicationItems(
+ [
+ 'attributes',
+ 'authentication',
+ 'blocks',
+ 'bootstrap',
+ 'config',
+ 'controllers',
+ 'elements',
+ 'jobs',
+ 'languages',
+ 'mail',
+ 'page_templates',
+ 'single_pages',
+ 'src',
+ 'themes',
+ 'tools',
+ 'views'
+ ]
+ );
}
- protected function exportPackages(array &$manifest, string $directory, InputInterface $input, OutputInterface $output)
+ protected function exportPackages(Manifest $manifest, string $directory): Manifest
{
- $output->writeln('Exporting packages/ directory...');
- $rsyncProcess = new Process([
- 'rsync',
- '-av',
- '-progress',
- DIR_PACKAGES,
- $directory
- ]);
+ $this->output->writeln('Exporting packages/ directory...');
+ /** @var PackageService $packages */
+ $packages = $this->getApplication()->make(PackageService::class);
+ $installed = $packages->getInstalledHandles();
+
+ foreach ($packages->getAvailablePackages() as $package) {
+ $handle = $package->getPackageHandle();
+ $manifest = $manifest->addPackage(
+ $handle,
+ in_array($handle, $installed),
+ file_exists($package->getPackagePath())
+ );
+ }
+
+ // Make sure we have installed missing packages as well
+ foreach ($installed as $packageHandle) {
+ if ($manifest->getPackage($packageHandle)) {
+ continue;
+ }
+
+ $manifest = $manifest->addPackage($packageHandle, true, false);
+ }
+
+ $rsyncProcess = new Process(
+ [
+ 'rsync',
+ '-avL',
+ '-progress',
+ DIR_PACKAGES,
+ $directory
+ ]
+ );
$rsyncProcess->setTimeout(null);
$rsyncProcess->run();
if (!$rsyncProcess->isSuccessful()) {
- throw new \Exception($rsyncProcess->getErrorOutput());
+ throw new Exception($rsyncProcess->getErrorOutput());
}
- $manifest['contents']['packages'] = true;
+ return $manifest;
}
- protected function exportCore(array &$manifest, string $directory, InputInterface $input, OutputInterface $output)
+ protected function exportCore(Manifest $manifest, string $directory): Manifest
{
- if (!$input->getOption('skip-core')) {
- $output->writeln('Exporting concrete/ directory...');
+ if (!$this->skipCore) {
+ $this->output->writeln('Exporting concrete/ directory...');
$rsyncProcess = new Process(
[
'rsync',
- '-av',
+ '-avL',
'-progress',
DIR_BASE_CORE,
$directory
@@ -165,83 +305,32 @@ protected function exportCore(array &$manifest, string $directory, InputInterfac
$rsyncProcess->run();
if (!$rsyncProcess->isSuccessful()) {
- throw new \Exception($rsyncProcess->getErrorOutput());
+ throw new Exception($rsyncProcess->getErrorOutput());
}
- $manifest['contents']['core'] = true;
- } else {
- $manifest['contents']['core'] = false;
+ return $manifest->setIncludeCore(true);
}
+
+ return $manifest->setIncludeCore(false);
}
- protected function compressDirectory(string $directory, InputInterface $input, OutputInterface $output)
+ protected function compressDirectory(string $directory, string $outputDirectory, string $outputFile = null): void
{
- $file = $input->getArgument('file');
- if (!$file) {
- $app = Facade::getFacadeApplication();
+ if (!$outputFile) {
+ $app = $this->getApplication();
$siteName = $app->make('site')->getSite()->getSiteName();
- $date = new \DateTime();
- $file = sprintf('backup_%s_%s', snake_case($siteName), $date->format('Y-m-d-H-i-s'));
+ $date = new DateTime();
+ /** @psalm-suppress UndefinedFunction */
+ $outputFile = sprintf('backup_%s_%s', snake_case($siteName), $date->format('Y-m-d-H-i-s'));
}
- rename($directory, $file);
+ $this->output->writeln(sprintf('Compressing directory: %s', $outputFile));
- $output->writeln(sprintf('Compressing directory: %s', $file));
-
- $tarProcess = new Process([
- 'tar',
- '-zcf',
- $file . '.tar.gz',
- $file
- ]);
- $tarProcess->setTimeout(null);
- $tarProcess->run();
-
- if (!$tarProcess->isSuccessful()) {
- throw new \Exception($tarProcess->getErrorOutput());
- }
+ $compressed = new PharData($outputDirectory . '/' . $outputFile . '.tar.gz', null, null, Phar::GZ);
+ $compressed->buildFromDirectory($directory);
- $output->writeln('Removing temporary directory...');
-
- $rmProcess = new Process([
- 'rm',
- '-r',
- $file
- ]);
- $rmProcess->setTimeout(null);
- $rmProcess->run();
-
- return 0;
- }
-
- public function execute(InputInterface $input, OutputInterface $output)
- {
- $backupDirectory = getcwd() . DIRECTORY_SEPARATOR . sprintf('tmp_%s', uniqid());
- mkdir($backupDirectory);
-
- $app = Facade::getFacadeApplication();
- $site = $app->make('site')->getSite();
- $manifest = [
- 'site' => $site->getSiteName(),
- 'url' => $site->getSiteCanonicalURL(),
- 'installationPath' => DIR_BASE,
- 'version' => '1.0',
- 'contents' => []
- ];
-
- $this->exportDatabase($manifest, $backupDirectory, $input, $output);
- $this->exportFiles($manifest, $backupDirectory, $input, $output);
- $this->exportApplication($manifest, $backupDirectory, $input, $output);
- $this->exportPackages($manifest, $backupDirectory, $input, $output);
- $this->exportCore($manifest, $backupDirectory, $input, $output);
- $this->exportIndexEntrypoint($manifest, $backupDirectory, $input, $output);
- $this->writeManifest($manifest, $backupDirectory);
- $this->compressDirectory($backupDirectory, $input, $output);
-
- $output->writeln('Backup complete!>');
-
- return 0;
+ $this->output->writeln(['', 'Successfully created backup file at:']);
+ $this->output->writeln($compressed->getPath(), self::VERBOSITY_QUIET);
+ $this->output->writeln('>');
}
-
-
-}
\ No newline at end of file
+}
diff --git a/src/Command/Backup/InspectCommand.php b/src/Command/Backup/InspectCommand.php
new file mode 100644
index 0000000..067767c
--- /dev/null
+++ b/src/Command/Backup/InspectCommand.php
@@ -0,0 +1,144 @@
+output->error('Backup file not found.');
+ return 111;
+ }
+
+ $manifest = $factory->forBackup($backupFile);
+
+ if (!$manifest) {
+ $this->output->error('Unable to load manifest from backup file.');
+ return 112;
+ }
+
+ if ($path = $input->getOption('ls')) {
+ /** @var string $path */
+ return $this->list($path, $backupFile);
+ }
+
+ if ($input->getOption('manifest-only')) {
+ $this->output->writeln(json_encode($manifest, JSON_PRETTY_PRINT));
+ return 0;
+ }
+ $testAdjective = function (string $affirmative, string $negative): callable {
+ return function (bool $test) use ($affirmative, $negative): string {
+ return $test ? '' . $affirmative . '>' : '' . $negative . '>';
+ };
+ };
+
+ $included = $testAdjective('Included', 'Excluded');
+ $installed = $testAdjective('Installed', 'Not Installed');
+
+ $this->output->table(['basics'], [
+ ['Version', $manifest->getVersion()],
+ ['Core', $included($manifest->includesCore())],
+ ['Index', $included($manifest->includesIndex())],
+ ]);
+
+ $packages = [];
+ foreach ($manifest->getPackages() as $package) {
+ $packages[] = [
+ dot_get($package, 'handle'),
+ $installed(!!dot_get($package, 'installed')),
+ $included(!!dot_get($package, 'included'))
+ ];
+ }
+ $this->output->table(['Packages'], $packages);
+
+ $locations = [];
+ foreach ($manifest->getStorageLocations() as $location) {
+ $locations[] = [
+ dot_get($location, 'name'),
+ dot_get($location, 'id'),
+ number_format($this->fileCount($backupFile, $location['id'])),
+ $included(!!dot_get($location, 'included')),
+ $testAdjective('Default', '')(!!dot_get($location, 'default')),
+ ];
+ }
+
+ $keys = ['Storage Locations | ', 'ID | ', 'File count | ', 'Is Included', 'Is Default'];
+ $locations = array_map(function ($columns) use ($keys) {
+ return array_combine($keys, $columns);
+ }, $locations);
+
+ $this->output->table(['Storage Locations', 'ID', 'File Count'], $locations);
+ return 0;
+ }
+
+ public static function register(Container $container, Application $console): void
+ {
+ $console->command('backup:inspect backupfile [-r|--manifest-only] [--ls=]', self::class)
+ ->descriptions('Inspects a Concrete installation backup', [
+ 'backupfile' => 'The path to the backup file to inspect',
+ '--manifest-only' => 'Output the raw manifest json, combine with jq> command.',
+ '--ls' => 'List out files at a given path'
+ ]);
+ }
+
+ protected function fileCount(string $backupFile, int $storageLocationId): int
+ {
+ $data = new PharData($backupFile . '/storage/' . $storageLocationId);
+
+ /** @psalm-suppress TooManyArguments Psalm has the wrong definition for PharData::count */
+ return $data->count(COUNT_RECURSIVE);
+ }
+
+ private function list(string $path, string $backupFile): int
+ {
+ $backup = new PharData($backupFile);
+ $path = ltrim($path, '/');
+ $headers = [
+ 'Size', 'Path'
+ ];
+ $table = [];
+ $isFile = false;
+
+ if ($path) {
+ if (!$backup->offsetExists($path)) {
+ $this->output->error('Cannot access "' . $path . '": No such file or directory');
+ return 1;
+ }
+
+ /** @var PharFileInfo $backupDir */
+ $backupDir = $backup->offsetGet($path);
+
+ if (!$backupDir->isDir()) {
+ $table[] = [$backupDir->getSize(), $backupDir->getFilename()];
+ $isFile = true;
+ } else {
+ $backup = new PharData($backupDir->getPathname());
+ }
+ }
+
+ if (!$isFile) {
+ /** @var PharFileInfo $child */
+ foreach ($backup as $child) {
+ $colors = $child->isDir() ? '' : '';
+ $close = $child->isDir() ? '/>' : '';
+ $table[] = [
+ $child->isDir() ? '' : $child->getSize(),
+ $colors . ltrim(implode('/', [$path, $child->getFilename()]), '/') . $close
+ ];
+ }
+ }
+
+ $this->output->table($headers, $table);
+ return 0;
+ }
+}
diff --git a/src/Command/Backup/RestoreCommand.php b/src/Command/Backup/RestoreCommand.php
new file mode 100644
index 0000000..ca396e6
--- /dev/null
+++ b/src/Command/Backup/RestoreCommand.php
@@ -0,0 +1,98 @@
+getInstallation();
+ $manifest = $factory->forBackup($backupFile);
+
+ if (!$this->output->confirm('Are you sure you want to restore this backup?')) {
+ return 1;
+ }
+
+ if (!$installation) {
+ throw new InstallationNotFound('No installation found.');
+ }
+
+ $restore
+ ->enableMaintenancePage()
+ ->restoreApplication(!!$input->getOption('skip-application'))
+ ->restoreCore(!!$input->getOption('skip-core') || !$manifest->includesCore())
+ ->restoreConfig(!!$input->getOption('skip-config'))
+ ->restoreIndex(!!$input->getOption('skip-index') || !$manifest->includesIndex())
+ ->restorePackages(!!$input->getOption('skip-packages') || !$manifest->getPackages())
+ ->restoreDatabase(!!$input->getOption('skip-database') || !$manifest->getDatabase())
+ ->restoreFiles(!!$input->getOption('skip-files'))
+ ->finalize();
+
+ // Handle various skips
+ if ($input->getOption('skip-application')) {
+ $restore->restoreApplication(true);
+ }
+ if ($input->getOption('skip-core')) {
+ $restore->restoreCore(true);
+ }
+
+ $job = Restoration::forBackup(
+ new \PharData($backupFile),
+ $manifest,
+ $installation,
+ Platform::tempDirectory(true),
+ !!$input->getOption('dryrun')
+ );
+
+ try {
+ $restore->resolve()->restore($job);
+ } finally {
+ $this->output->newLine();
+ $this->output->writeln('Cleaning up...');
+ $finalize = new Finalize();
+ $finalize->clean($job);
+ }
+ }
+
+ public static function register(Container $container, Application $console): void
+ {
+ $console
+ ->command(
+ 'backup:restore backupFile [-D|--dryrun]
+ [--skip-db] [--skip-core] [--skip-packages] [--skip-config] [--skip-files] [--skip-application]
+ [--skip-index] [--skip-database]',
+ self::class,
+ ['restore']
+ )
+ ->descriptions(
+ 'Restore a concrete5 site from a backup.',
+ [
+ 'backupFile' => 'The file to restore from',
+ '--dryrun' => 'Don\'t actually run restoration',
+ '--skip-core' => 'Skip the concrete5 core',
+ '--skip-packages' => 'Skip packages',
+ '--skip-config' => 'Skip config',
+ '--skip-files' => 'Skip any storage locations',
+ '--skip-application' => 'Skip restoring the full application directory',
+ '--skip-index' => 'Skip the index.php file',
+ '--skip-database' => 'Skip restoring the database',
+ ]
+ );
+ }
+}
diff --git a/src/Command/Command.php b/src/Command/Command.php
new file mode 100644
index 0000000..6f348b9
--- /dev/null
+++ b/src/Command/Command.php
@@ -0,0 +1,61 @@
+output = $outputStyle;
+ }
+
+ public function setConsole(Application $application): void
+ {
+ $this->console = $application;
+ }
+
+ protected function getConsole(): Application
+ {
+ return $this->console;
+ }
+
+ public function getApplication(): \Concrete\Core\Application\Application
+ {
+ $connection = $this->getConnection();
+ if (!$connection || !$connection instanceof ApplicationEnabledConnectionInterface) {
+ throw new VersionMismatch('This command can only run on Application Enabled concrete installs.');
+ }
+
+ return $connection->getApplication();
+ }
+}
diff --git a/src/Command/CommandGroupInterface.php b/src/Command/CommandGroupInterface.php
new file mode 100644
index 0000000..099c8f5
--- /dev/null
+++ b/src/Command/CommandGroupInterface.php
@@ -0,0 +1,12 @@
+setName('database:dump')
- ->setDescription('Dumps the Concrete database to a file.')
- ->addArgument('file', InputArgument::OPTIONAL, 'Filename for the dump file.')
- ->addOption(
- 'gz',
- null,
- InputOption::VALUE_NONE,
- 'Gzip the export'
- );
- }
-
- public function execute(InputInterface $input, OutputInterface $output)
+ public function __invoke(string $file, Input $input)
{
- $file = $input->getArgument('file');
- $app = Facade::getFacadeApplication();
+ $app = $this->getApplication();
$config = $app->make('config')->get('database');
$connection = $config['default-connection'];
- foreach($config['connections'] as $identifier => $connectionRow) {
+ foreach ($config['connections'] as $identifier => $connectionRow) {
if ($identifier == $connection) {
if (!$file) {
$date = new \DateTime();
@@ -44,7 +30,7 @@ public function execute(InputInterface $input, OutputInterface $output)
$mysqldump = sprintf(
"mysqldump --host='%s' --port='%s' --user='%s' --password='%s' '%s' > '%s'",
$connectionRow['server'],
- 3306,
+ isset($connectionRow['port']) ? $connectionRow['port'] : 3306,
$connectionRow['username'],
$connectionRow['password'],
$connectionRow['database'],
@@ -52,32 +38,39 @@ public function execute(InputInterface $input, OutputInterface $output)
);
$outputFile = $file;
- $output->writeln(sprintf('Exporting database: %s', $connectionRow['database']));
+ $this->output->writeln(sprintf('Exporting database: %s', $connectionRow['database']));
- $process = Process::fromShellCommandline($mysqldump);
+ $process = process($mysqldump);
$process->setTimeout(null);
$process->run();
if ($process->isSuccessful()) {
if ($input->getOption('gz')) {
$outputFile = $file . '.gz';
- $output->writeln('Compressing file with gzip...');
- $process = Process::fromShellCommandline(sprintf("gzip '%s'", $file));
+ $this->output->writeln('Compressing file with gzip...');
+ $process = process(['gzip', $file]);
$process->setTimeout(null);
$process->run();
}
- $output->writeln(sprintf('Database backed up to file: %s', $outputFile));
+ $this->output->writeln(sprintf('Database backed up to file: %s', $outputFile));
} else {
- $output->writeln('' . $process->getErrorOutput() . '');
+ $this->output->writeln('' . $process->getErrorOutput() . '');
}
return 0;
}
}
- $output->writeln('Unable to locate default Concrete database connection.');
+ $this->output->writeln('Unable to locate default Concrete database connection.');
return 1;
}
-
-}
\ No newline at end of file
+ public static function register(Container $container, Application $console): void
+ {
+ $console->command('database:dump [file] [-z|--gz]', self::class)
+ ->descriptions('Dumps the Concrete database to a file', [
+ 'file' => 'Filename for the dump file',
+ '--gz' => 'Flag to gzip',
+ ]);
+ }
+}
diff --git a/src/Command/InfoCommand.php b/src/Command/InfoCommand.php
deleted file mode 100644
index 98ebcfb..0000000
--- a/src/Command/InfoCommand.php
+++ /dev/null
@@ -1,29 +0,0 @@
-setName('info')
- ->setDescription('Get info about about the Concrete installation.');
- }
-
- public function execute(InputInterface $input, OutputInterface $output)
- {
- $info = new Info();
- $output->writeln('# Location');
- $output->writeln(sprintf('Path to installation: %s', DIR_BASE));
- $output->writeln('# concrete5 Version');
- $output->writeln('Installed - ' . ($info->isInstalled() ? 'Yes' : 'No'));
- $output->writeln($info->getCoreVersions());
- }
-
-
-}
\ No newline at end of file
diff --git a/src/Command/InstallationAwareCommandInterface.php b/src/Command/InstallationAwareCommandInterface.php
deleted file mode 100644
index 650275a..0000000
--- a/src/Command/InstallationAwareCommandInterface.php
+++ /dev/null
@@ -1,12 +0,0 @@
-newLine();
+ $this->write($prefix . rtrim($step, '. ') . $suffix);
+ }
+
+ public function outputDone(string $doneText = 'Done!', string $tag = ''): void
+ {
+ $this->write($tag . $doneText . '>');
+ }
+
+ public function outputDryrun(): void
+ {
+ $this->outputDone('Done! (Dry run)>');
+ }
+
+ public function outputFinal(): void
+ {
+ $this->outputStep('', '', ' -');
+ }
+}
diff --git a/src/Command/OutputStyleAwareInterface.php b/src/Command/OutputStyleAwareInterface.php
new file mode 100644
index 0000000..a5044d8
--- /dev/null
+++ b/src/Command/OutputStyleAwareInterface.php
@@ -0,0 +1,9 @@
+traitOutputStyle = $outputStyle;
+ }
+
+ protected function getOutputStyle(): OutputStyle
+ {
+ return $this->traitOutputStyle ?: new OutputStyle(new ArgvInput([]), new NullOutput());
+ }
+}
diff --git a/src/Command/Site/InfoCommand.php b/src/Command/Site/InfoCommand.php
new file mode 100644
index 0000000..6ca0d97
--- /dev/null
+++ b/src/Command/Site/InfoCommand.php
@@ -0,0 +1,35 @@
+getApplication();
+
+ $info = $app->make(Info::class);
+ $this->output->writeln('# Location');
+ $this->output->writeln(sprintf('Path to installation: %s', DIR_BASE));
+ $this->output->writeln('# concrete5 Version');
+ $this->output->writeln('Installed - ' . ($info->isInstalled() ? 'Yes' : 'No'));
+ $this->output->writeln($info->getCoreVersions());
+ }
+
+ /**
+ * @param Container $container
+ * @param Application $console
+ * @return void
+ */
+ public static function register(Container $container, Application $console): void
+ {
+ $console->command('site:info', self::class, ['info'])
+ ->descriptions('Get info about the current Concrete installation');
+ }
+}
diff --git a/src/Command/Site/SyncCommand.php b/src/Command/Site/SyncCommand.php
new file mode 100644
index 0000000..5989f7f
--- /dev/null
+++ b/src/Command/Site/SyncCommand.php
@@ -0,0 +1,65 @@
+getInstallation());
+
+ if (!$file) {
+ throw new RuntimeException('Unable to locate concrete.json config file.');
+ }
+
+ $data = json_decode(file_get_contents($file), true);
+
+ $instances = InstanceConfig::fromJson(dot_get($data, 'instances', []));
+ $fromInstance = dot_get($instances, $from);
+
+ if (!$fromInstance || !$fromInstance instanceof InstanceConfig) {
+ throw new RuntimeException('No instances found.');
+ }
+
+ $this->output->outputStep('Backing up remote site');
+ $process = $fromInstance->executeConsole('backup -q');
+ $process->mustRun();
+ $this->output->outputDone();
+
+ $this->output->outputStep('Syncing remote file');
+ $syncPath = Platform::tempDirectory(true) . '/backup.tar.gz';
+
+ $path = trim($process->getOutput());
+ $process = $fromInstance->downloadFile($path, $syncPath);
+ $process->mustRun();
+ $this->output->outputDone();
+
+ $this->output->outputStep('Restoring');
+ $result = $this->console->runCommand("backup:restore '{$syncPath}' --no-interaction", $this->output);
+ $this->output->outputDone();
+
+ return $result;
+ }
+
+ public static function register(Container $container, Application $console): void
+ {
+ $console->command('site:sync from [--config=]', self::class)
+ ->descriptions(
+ 'Sync a remote site into this site using backups.',
+ [
+ 'from' => 'The location to sync from @remote:/path/to/backup.tar.gz or @remote',
+ '--config' => 'Specify the config file to use',
+ ]
+ );
+ }
+}
diff --git a/src/Concrete/Adapter/AbstractApplicationEnabledAdapter.php b/src/Concrete/Adapter/AbstractApplicationEnabledAdapter.php
new file mode 100644
index 0000000..d6a4990
--- /dev/null
+++ b/src/Concrete/Adapter/AbstractApplicationEnabledAdapter.php
@@ -0,0 +1,95 @@
+createConnection();
+ $connection->connect($this->resolveApplication($path));
+
+ return $connection;
+ }
+
+ /**
+ * Get the connection to connect with
+ *
+ * @return ApplicationEnabledConnectionInterface
+ */
+ abstract protected function createConnection(): ApplicationEnabledConnectionInterface;
+
+ /**
+ * Resolve the application object from a concrete5 site
+ * @param string $path
+ * @return Application
+ */
+ private function resolveApplication($path): Application
+ {
+ chdir($path);
+
+ // Setup
+ $this->defineConstants($path);
+ $this->registerAutoload($path);
+
+ // Get the concrete5 application
+ $cms = $this->getApplicationInstance($path);
+
+ // Boot the runtime
+ $this->bootApplication($cms);
+
+ return $cms;
+ }
+
+ /**
+ * @param $path
+ */
+ protected function defineConstants(string $path): void
+ {
+ // Define some required constants
+ define('DIR_BASE', $path);
+ define('C5_ENVIRONMENT_ONLY', true);
+
+ // Load in the rest of them
+ require $path . '/concrete/bootstrap/configure.php';
+ }
+
+ /**
+ * @param $path
+ */
+ protected function registerAutoload(string $path): void
+ {
+ // Load in concrete5's autoloader
+ require $path . '/concrete/bootstrap/autoload.php';
+ }
+
+ /**
+ * @param $path
+ * @return Application
+ */
+ protected function getApplicationInstance(string $path): Application
+ {
+ return require $path . '/concrete/bootstrap/start.php';
+ }
+
+ /**
+ * @param $cms
+ */
+ protected function bootApplication(Application $cms): void
+ {
+ if (method_exists($cms, 'getRuntime')) {
+ $runtime = $cms->getRuntime();
+ $runtime->boot();
+ }
+ }
+}
diff --git a/src/Concrete/Adapter/AdapterFactory.php b/src/Concrete/Adapter/AdapterFactory.php
new file mode 100644
index 0000000..80602cf
--- /dev/null
+++ b/src/Concrete/Adapter/AdapterFactory.php
@@ -0,0 +1,35 @@
+ 5.6.0', $version);
+ }
+
+ $map = [
+ '5.6.9999' => Version6Adapter::class,
+ '9.9999999' => ApplicationEnabledAdapter::class,
+ ];
+
+ foreach ($map as $test => $adapterClass) {
+ if (Comparator::lessThanOrEqualTo($version, $test)) {
+ return $this->getContainer()->get($adapterClass);
+ }
+ }
+
+ throw VersionMismatch::expected('< 10.0.0', $version);
+ }
+}
diff --git a/src/Concrete/Adapter/AdapterInterface.php b/src/Concrete/Adapter/AdapterInterface.php
new file mode 100644
index 0000000..c57e66a
--- /dev/null
+++ b/src/Concrete/Adapter/AdapterInterface.php
@@ -0,0 +1,17 @@
+handleAttaching($path);
+ ob_end_clean();
+ return $result;
+ }
+
+ protected function handleAttaching(string $path): LegacyConnection
+ {
+ // Change the cwd to the site path
+ chdir($path);
+
+ // Define a couple things concrete5 expects
+ define('DIR_BASE', $path);
+ define('C5_ENVIRONMENT_ONLY', true);
+
+ // Set the error reporting low
+ error_reporting(E_ALL | ~E_NOTICE | ~E_WARNING | ~E_STRICT);
+
+ // Add 3rdparty to include path
+ set_include_path(get_include_path() . PATH_SEPARATOR . $path . '/concrete/libraries/3rdparty');
+
+ // Include Adodb first, not sure why this was needed
+ @require_once $path . '/concrete/libraries/3rdparty/adodb/adodb.inc.php';
+
+ // Include Loader explicitly
+ @require_once $path . '/concrete/libraries/loader.php';
+
+ // Load in legacy dispatcher
+ @require_once $path . '/concrete/dispatcher.php';
+
+ // Adodb Stuff
+ $GLOBALS['ADODB_ASSOC_CASE'] = 2;
+ $GLOBALS['ADODB_ACTIVE_CACHESECS'] = 300;
+ $GLOBALS['ADODB_CACHE_DIR'] = defined('DIR_FILES_CACHE_DB') ? DIR_FILES_CACHE_DB : '';
+
+ return new LegacyConnection();
+ }
+}
diff --git a/src/Concrete/Client.php b/src/Concrete/Client.php
new file mode 100644
index 0000000..d84be07
--- /dev/null
+++ b/src/Concrete/Client.php
@@ -0,0 +1,41 @@
+adapter = $adapter;
+ }
+
+ /**
+ * Attempt to disconnect
+ * (This isn't going to be fully supported for awhile)
+ *
+ * @param ConnectionInterface $connection
+ * @return bool
+ */
+ public function disconnect(ConnectionInterface $connection): bool
+ {
+ return $connection->disconnect();
+ }
+
+ /**
+ * Get a connection to a concrete5 site
+ * @param string $path The path to the site to connect to
+ * @return ConnectionInterface
+ */
+ public function connect($path): ConnectionInterface
+ {
+ return $this->adapter->attach($path);
+ }
+}
diff --git a/src/Concrete/ClientInterface.php b/src/Concrete/ClientInterface.php
new file mode 100644
index 0000000..98af7cc
--- /dev/null
+++ b/src/Concrete/ClientInterface.php
@@ -0,0 +1,25 @@
+getLeagueContainer();
+
+ $app->add(ClientInterface::class, function (): ClientInterface {
+ $installation = $this->getContainer()->get(Installation::class);
+ $adapter = $this->getContainer()->get(AdapterFactory::class)
+ ->forVersion($installation->getVersion()->getVersion());
+
+ return new Client($adapter);
+ });
+
+ $app->add(ConnectionInterface::class, function (): ConnectionInterface {
+ $client = $this->getContainer()->get(ClientInterface::class);
+ $installation = $this->getContainer()->get(Installation::class);
+
+ return $client->connect($installation->getPath());
+ }, true);
+ }
+
+ public function boot()
+ {
+ $app = $this->getLeagueContainer();
+
+ // Add inflector for version7 connection
+ $app->inflector(ConnectionAwareInterface::class)
+ ->invokeMethod('setConnection', [ConnectionInterface::class]);
+ }
+}
diff --git a/src/Concrete/Connection/ApplicationEnabledConnection.php b/src/Concrete/Connection/ApplicationEnabledConnection.php
new file mode 100644
index 0000000..9a66bc0
--- /dev/null
+++ b/src/Concrete/Connection/ApplicationEnabledConnection.php
@@ -0,0 +1,59 @@
+application = $application;
+ }
+
+ /**
+ * Determine if this connection is connected
+ *
+ * @return bool
+ */
+ public function isConnected(): bool
+ {
+ return $this->application !== null;
+ }
+
+ /**
+ * Disconnect a connection
+ *
+ * @return bool Success or failure
+ */
+ public function disconnect(): bool
+ {
+ if (!$this->isConnected()) {
+ return false;
+ }
+
+ $this->application = null;
+ return true;
+ }
+
+ /**
+ * @return Application
+ */
+ public function getApplication(): Application
+ {
+ if (!$this->application) {
+ throw new \RuntimeException('Accessing the application before it has been populated.');
+ }
+
+ return $this->application;
+ }
+}
diff --git a/src/Concrete/Connection/ApplicationEnabledConnectionInterface.php b/src/Concrete/Connection/ApplicationEnabledConnectionInterface.php
new file mode 100644
index 0000000..c87cab7
--- /dev/null
+++ b/src/Concrete/Connection/ApplicationEnabledConnectionInterface.php
@@ -0,0 +1,11 @@
+traitConnection = $connection;
+ }
+
+ protected function getConnection(): ?ConnectionInterface
+ {
+ return $this->traitConnection;
+ }
+}
diff --git a/src/Concrete/Connection/ConnectionInterface.php b/src/Concrete/Connection/ConnectionInterface.php
new file mode 100644
index 0000000..4a6d9a3
--- /dev/null
+++ b/src/Concrete/Connection/ConnectionInterface.php
@@ -0,0 +1,19 @@
+isConnected();
+ }
+}
diff --git a/src/Concrete/InstanceConfig.php b/src/Concrete/InstanceConfig.php
new file mode 100644
index 0000000..a926a86
--- /dev/null
+++ b/src/Concrete/InstanceConfig.php
@@ -0,0 +1,96 @@
+user, $this->host, (int) $this->port);
+ return $sshConfig ? $sshConfig($ssh) : $ssh;
+ }
+
+ public function executeConsole(string $command, callable $sshConfig = null): Process
+ {
+ return $this->execute($this->consolePath . ' ' . $command, $sshConfig);
+ }
+
+ public function executeAsyncConsole(string $command, callable $sshConfig = null): Process
+ {
+ return $this->executeAsync($this->consolePath . ' ' . $command, $sshConfig);
+ }
+
+ public function downloadFile(string $remotePath, string $localPath, callable $sshConfig = null): Process
+ {
+ if ($this->host === 'localhost') {
+ return $this->ssh($sshConfig)->run("cp '{$remotePath}' '{$localPath}'");
+ }
+
+ return $this->ssh($sshConfig)->download($remotePath, $localPath);
+ }
+
+ public function execute(string $command, callable $sshConfig = null): Process
+ {
+ if ($this->host === 'localhost') {
+ return $this->ssh($sshConfig)->run("cd {$this->path} && {$command}");
+ }
+
+ return $this->ssh($sshConfig)->execute('cd ' . $this->path . "\n" . $command);
+ }
+
+ public function executeAsync(string $command, callable $sshConfig = null): Process
+ {
+ if ($this->host === 'localhost') {
+ return $this->ssh($sshConfig)->run("cd {$this->path} && {$command}", 'start');
+ }
+
+ return $this->ssh($sshConfig)->executeAsync('cd ' . $this->path . "\n" . $command);
+ }
+
+ /**
+ * @param array $instances
+ * @return array
+ */
+ public static function fromJson(array $instances): array
+ {
+ $result = [];
+ foreach ($instances as $handle => $instance) {
+ $config = new InstanceConfig();
+
+ $config->host = dot_get($instance, 'host', '');
+ $config->user = dot_get($instance, 'user', '');
+ $config->path = dot_get($instance, 'path', '');
+ $config->consolePath = dot_get($instance, 'consolePath', 'concrete');
+ $config->port = dot_get($instance, 'port', '22');
+ $config->sshKey = dot_get($instance, 'sshKey', '');
+ $config->sshKeypassword = dot_get($instance, 'sshKeyPassword', '');
+
+ $result[$handle] = $config;
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Concrete/Loader.php b/src/Concrete/Loader.php
new file mode 100644
index 0000000..797deba
--- /dev/null
+++ b/src/Concrete/Loader.php
@@ -0,0 +1,148 @@
+callLoader('library', $library, $packageHandle);
+ }
+
+ /**
+ * @param string $model
+ * @param string|null $packageHandle
+ * @return mixed
+ */
+ public function model(string $model, string $packageHandle = null)
+ {
+ return $this->callLoader('model', $model, $packageHandle);
+ }
+
+ /**
+ * @param string $file
+ * @param string $packageHandle
+ * @param array|null $args
+ * @return mixed
+ */
+ public function packageElement(string $file, string $packageHandle, array $args = null)
+ {
+ return $this->callLoader('model', $file, $packageHandle, $args);
+ }
+
+ /**
+ * @param string $file
+ * @param array|null $args
+ * @return mixed
+ */
+ public function element(string $file, array $args = null)
+ {
+ return $this->callLoader('model', $file, $args);
+ }
+
+ /**
+ * @param string $file
+ * @param array|null $args
+ * @param string|null $packageHandle
+ * @return mixed
+ */
+ public function tool(string $file, array $args = null, string $packageHandle = null)
+ {
+ return $this->callLoader('model', $file, $args, $packageHandle);
+ }
+
+ /**
+ * @param string $block
+ * @return mixed
+ */
+ public function block(string $block)
+ {
+ return $this->callLoader('model', $block);
+ }
+
+ /**
+ * @return mixed
+ */
+ public function database()
+ {
+ return $this->callLoader('database');
+ }
+
+ /**
+ * @return mixed
+ */
+ public function db()
+ {
+ return $this->callLoader('db');
+ }
+
+ /**
+ * @param string $file
+ * @param string|null $pkgHandle
+ * @return mixed
+ */
+ public function helper(string $file, string $pkgHandle = null)
+ {
+ // Shim in false for pkgHandle default value
+ if ($pkgHandle === null) {
+ $pkgHandle = false;
+ }
+
+ return $this->callLoader('helper', $file, $pkgHandle);
+ }
+
+ /**
+ * @param string $package
+ * @return mixed
+ */
+ public function package(string $package)
+ {
+ return $this->callLoader('package', $package);
+ }
+
+ /**
+ * @param string $package
+ * @return mixed
+ */
+ public function startingPointPackage(string $package)
+ {
+ return $this->callLoader('startingPointPackage', $package);
+ }
+
+ /**
+ * @param string $collectionType
+ * @return mixed
+ */
+ public function pageTypeControllerPath(string $collectionType)
+ {
+ return $this->callLoader('pageTypeControllerPath', $collectionType);
+ }
+
+ /**
+ * @param string $path
+ * @return mixed
+ */
+ public function controller(string $path)
+ {
+ return $this->callLoader('controller', $path);
+ }
+}
diff --git a/src/Concrete/Restore/Restoration.php b/src/Concrete/Restore/Restoration.php
new file mode 100644
index 0000000..0b697e2
--- /dev/null
+++ b/src/Concrete/Restore/Restoration.php
@@ -0,0 +1,145 @@
+backup = $backup;
+ $self->manifest = $manifest;
+ $self->install = $install;
+ $self->temp = $temp;
+ $self->isDryRun = $dryrun;
+
+ return $self;
+ }
+
+ public function hasExtracted(): bool
+ {
+ return $this->extracted !== null;
+ }
+
+ public function extract(): string
+ {
+ if (!$this->extracted) {
+ do {
+ $extractDir = $this->tempDir(uniqid('extract'), false);
+ } while (file_exists($extractDir));
+
+ $this->getBackup()->extractTo($extractDir);
+ $this->extracted = $extractDir;
+ }
+
+ return $this->extracted;
+ }
+
+ public function tempDir(string $subpath = null, bool $create = true): string
+ {
+ $temp = rtrim($this->temp, '/');
+
+ if ($subpath) {
+ $temp .= '/' . trim($subpath, '/');
+ if ($create && !file_exists($temp)) {
+ mkdir($temp, 0777, true);
+ }
+ }
+
+ return $temp;
+ }
+
+ public function getBackup(): PharData
+ {
+ return $this->backup;
+ }
+
+ /**
+ * @return Installation
+ */
+ public function getInstallation(): Installation
+ {
+ return $this->install;
+ }
+
+ public function findConcretePath(string $subpath, string $path = null): array
+ {
+ if (!$path) {
+ $path = $this->getInstallation()->getPath();
+ }
+
+ $path = rtrim($path, '/');
+ $subpath = ltrim($subpath);
+
+ $results = [];
+ $check = [
+ '',
+ 'public',
+ 'www',
+ 'web',
+ ];
+
+ foreach ($check as $expectedPath) {
+ $fullPath = implode(
+ '/',
+ array_filter(
+ [
+ $path,
+ $expectedPath,
+ $subpath
+ ]
+ )
+ );
+
+ if (file_exists($fullPath)) {
+ $results[] = $fullPath;
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * @return Manifest
+ */
+ public function getManifest(): Manifest
+ {
+ return $this->manifest;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isDryRun(): bool
+ {
+ return $this->isDryRun;
+ }
+}
diff --git a/src/Concrete/Restore/RestorationManager.php b/src/Concrete/Restore/RestorationManager.php
new file mode 100644
index 0000000..1c53f16
--- /dev/null
+++ b/src/Concrete/Restore/RestorationManager.php
@@ -0,0 +1,72 @@
+ */
+ protected $strategies = [];
+
+ public function addStrategy(StrategyInterface $strategy, bool $skip = false): RestorationManager
+ {
+ $this->strategies[get_class($strategy)] = [$strategy, $skip];
+ return $this;
+ }
+
+ public function restoreGenerator(Restoration $job): \Generator
+ {
+ $failed = 0;
+ $succeeded = 0;
+ foreach ($this->strategies as $strategyArray) {
+ [$strategy, $skip] = $strategyArray;
+ $this->outputStyle->write(
+ ' Starting ' . snake_case(class_basename($strategy), ' ') . '> step...'
+ );
+
+ if (!$skip) {
+ if ($strategy->restore($job)) {
+ $this->outputStyle->writeln(' Success!>');
+ $succeeded++;
+ yield self::RESULT_SUCCESS;
+ } else {
+ $this->outputStyle->writeln(' Failed.>');
+ $failed++;
+ yield self::RESULT_FAILURE;
+ }
+ } else {
+ $this->outputStyle->writeln(' Skipped.>');
+ yield self::RESULT_SKIPPED;
+ }
+ }
+
+ $this->outputStyle->newLine();
+ $this->outputStyle->writeln('Finished restoration' . ($failed ? " with {$failed} failures." : '!'));
+
+ return $failed === 0;
+ }
+
+ public function restore(Restoration $job): bool
+ {
+ $generator = $this->restoreGenerator($job);
+ // Resolve the generator completely
+ iterator_to_array($generator);
+
+ return $generator->getReturn();
+ }
+
+ public function setOutputStyle(OutputStyle $outputStyle): void
+ {
+ $this->outputStyle = $outputStyle;
+ }
+}
diff --git a/src/Concrete/Restore/RestorationManagerBuilder.php b/src/Concrete/Restore/RestorationManagerBuilder.php
new file mode 100644
index 0000000..455725d
--- /dev/null
+++ b/src/Concrete/Restore/RestorationManagerBuilder.php
@@ -0,0 +1,74 @@
+manager = $manager;
+ }
+
+ public function restoreDatabase(bool $skip = false): RestorationManagerBuilder
+ {
+ return $this->addStrategy(Strategy\RestoreDatabase::class, $skip);
+ }
+
+ public function restoreCore(bool $skip = false): RestorationManagerBuilder
+ {
+ return $this->addStrategy(Strategy\RestoreCore::class, $skip);
+ }
+
+ public function restorePackages(bool $skip = false): RestorationManagerBuilder
+ {
+ return $this->addStrategy(Strategy\RestorePackages::class, $skip);
+ }
+
+ public function restoreApplication(bool $skip = false): RestorationManagerBuilder
+ {
+ return $this->addStrategy(Strategy\RestoreApplication::class, $skip);
+ }
+
+ public function restoreIndex(bool $skip = false): RestorationManagerBuilder
+ {
+ return $this->addStrategy(Strategy\RestoreIndex::class, $skip);
+ }
+
+ public function restoreConfig(bool $skip = false): RestorationManagerBuilder
+ {
+ return $this->addStrategy(Strategy\RestoreConfig::class, $skip);
+ }
+
+ public function restoreFiles(bool $skip = false): RestorationManagerBuilder
+ {
+ return $this->addStrategy(Strategy\RestoreStorageLocations::class, $skip);
+ }
+
+ public function enableMaintenancePage(bool $skip = false): RestorationManagerBuilder
+ {
+ return $this->addStrategy(Strategy\EnableMaintenanceMode::class, $skip);
+ }
+
+ public function finalize(bool $skip = false): RestorationManagerBuilder
+ {
+ return $this->addStrategy(Strategy\Finalize::class, $skip);
+ }
+
+ public function resolve(): RestorationManager
+ {
+ return $this->manager;
+ }
+
+ protected function addStrategy(string $strategy, bool $skip = false): RestorationManagerBuilder
+ {
+ $this->manager->addStrategy($this->container->get($strategy), $skip);
+ return $this;
+ }
+}
diff --git a/src/Concrete/Restore/Strategy/AbstractDirectoryExtractStrategy.php b/src/Concrete/Restore/Strategy/AbstractDirectoryExtractStrategy.php
new file mode 100644
index 0000000..16dd62e
--- /dev/null
+++ b/src/Concrete/Restore/Strategy/AbstractDirectoryExtractStrategy.php
@@ -0,0 +1,76 @@
+getOutputStyle();
+ $path = $this->getExtractDirectory();
+ $name = $this->getExtractName();
+
+ $output->outputStep("Locating {$name} directory");
+
+ // Locate the core we need to override
+ $coreDirs = $job->findConcretePath($path);
+
+ if (count($coreDirs) > 1) {
+ throw new \InvalidArgumentException("Unable to find {$name} directory, multiple detected.");
+ }
+
+ if (!$coreDirs) {
+ throw new \InvalidArgumentException("Unable to find {$name} directory, not detected.");
+ }
+
+ $file = array_shift($coreDirs);
+ $dir = dirname($file);
+ $output->outputDone($file);
+
+ // Clear old directory if needed
+ if ($this->shouldClear($job)) {
+ $output->outputStep('Clearing old ' . $name);
+
+ if (!$job->isDryRun()) {
+ $fs = new Filesystem(new Local($dir));
+ if (!$fs->deleteDir(basename($file))) {
+ return false;
+ }
+ $output->outputDone();
+ } else {
+ $output->outputDryrun();
+ }
+ }
+
+ // Extract all files to the path
+ $output->outputStep("Extracting {$name} directories... ");
+ $subpathCount = mb_substr_count($path, '/', 'utf8');
+ $extractPath = $dir;
+
+ while ($subpathCount-- && $extractPath) {
+ $extractPath = dirname($extractPath);
+ }
+
+ if (!$job->isDryRun()) {
+ $job->getBackup()->extractTo($extractPath, [$path . '/'], true);
+ $output->outputDone();
+ } else {
+ $output->outputDryrun();
+ }
+
+ // Output a string to make the success look nice
+ $output->outputFinal();
+
+ return true;
+ }
+}
diff --git a/src/Concrete/Restore/Strategy/AbstractOutputtingStrategy.php b/src/Concrete/Restore/Strategy/AbstractOutputtingStrategy.php
new file mode 100644
index 0000000..8704274
--- /dev/null
+++ b/src/Concrete/Restore/Strategy/AbstractOutputtingStrategy.php
@@ -0,0 +1,12 @@
+getOutputStyle();
+ $installation = $job->getInstallation();
+ $installPath = $installation->getPath();
+ $check = [
+ '/index.php',
+ '/public/index.php',
+ '/web/index.php',
+ ];
+
+ $maintenance = Config::maintenancePage($installation);
+ $cachePath = $job->tempDir('indexes');
+ $found = false;
+ foreach ($check as $item) {
+ if (!file_exists($installPath . $item)) {
+ continue;
+ }
+
+ $output->outputStep('Backing up and replacing ' . $item);
+
+ // cache the file and replace it with our maintenance page
+ $dirname = dirname($item);
+ if ($dirname !== '/') {
+ mkdir($cachePath . $dirname, 0777, true);
+ }
+
+ copy($installPath . $item, $cachePath . $item);
+ if ($job->isDryRun()) {
+ $output->outputDryrun();
+ } else {
+ file_put_contents($installPath . $item, $maintenance);
+ $output->outputDone();
+ }
+ $found = true;
+ }
+
+ $output->outputFinal();
+ return $found;
+ }
+}
diff --git a/src/Concrete/Restore/Strategy/Finalize.php b/src/Concrete/Restore/Strategy/Finalize.php
new file mode 100644
index 0000000..729d964
--- /dev/null
+++ b/src/Concrete/Restore/Strategy/Finalize.php
@@ -0,0 +1,120 @@
+findConcretePath('concrete/bin/concrete5');
+ if (!$cliPath) {
+ throw new \RuntimeException('Unable to locate concrete5 cli tool.');
+ } else {
+ $cliPath = array_shift($cliPath);
+ }
+
+ return $this->generateProxies($job, $cliPath)
+ && $this->clearCache($job, $cliPath)
+ && $this->restoreIndexes($job)
+ && $this->sync($job);
+ }
+
+ public function clean(Restoration $job): bool
+ {
+ return $this->clearTemp($job);
+ }
+
+ private function restoreIndexes(Restoration $job): bool
+ {
+ $output = $this->getOutputStyle();
+ $install = $job->getInstallation();
+ $installPath = $install->getPath();
+ $basePath = $job->tempDir('indexes');
+ $allFiles = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator(
+ $basePath,
+ \RecursiveDirectoryIterator::SKIP_DOTS
+ )
+ );
+ $dry = $job->isDryRun();
+
+ /** @var \SplFileInfo $file */
+ foreach ($allFiles as $file) {
+ $output->outputStep('Restoring index file ' . $file->getPathname());
+
+ $newPath = $installPath . '/' . trim(substr($file->getPathname(), strlen($basePath)), '/');
+ if (!$dry) {
+ copy($file->getPathname(), $newPath);
+ $output->outputDone();
+ } else {
+ $output->outputDryrun();
+ }
+ }
+
+ $output->outputFinal();
+ return true;
+ }
+
+ private function sync(Restoration $job): bool
+ {
+ $sync = $job->tempDir('sync');
+ //$sync = new Rsync();
+ //var_dump($sync->getCommand($sync, $job->getInstallation()));
+
+ return true;
+ }
+
+ private function clearTemp(Restoration $job): bool
+ {
+ $filesystem = new Filesystem(new Local('/'));
+ return $filesystem->deleteDir($job->tempDir());
+ }
+
+ private function generateProxies(Restoration $job, string $path): bool
+ {
+ $output = $this->getOutputStyle();
+ $output->outputStep('Regenerating database proxies');
+ if ($job->isDryRun()) {
+ $output->outputDryrun();
+ return true;
+ }
+
+ $command = process($path . ' orm:generate:proxies');
+ $command->mustRun();
+
+ if ($command->isSuccessful()) {
+ $output->outputDone();
+ return true;
+ }
+
+ $output->outputDone('Failed', '');
+ return false;
+ }
+
+ private function clearCache(Restoration $job, string $path): bool
+ {
+ $output = $this->getOutputStyle();
+ $output->outputStep('Clearing cache');
+
+ if ($job->isDryRun()) {
+ $output->outputDryrun();
+ return true;
+ }
+
+ $command = process($path . ' c5:clear-cache');
+ $command->mustRun();
+
+ if ($command->isSuccessful()) {
+ $output->outputDone();
+ return true;
+ }
+
+ $output->outputDone('Failed', '');
+ return false;
+ }
+}
diff --git a/src/Concrete/Restore/Strategy/RestoreApplication.php b/src/Concrete/Restore/Strategy/RestoreApplication.php
new file mode 100644
index 0000000..fbe746b
--- /dev/null
+++ b/src/Concrete/Restore/Strategy/RestoreApplication.php
@@ -0,0 +1,25 @@
+getExtractDirectory();
+ }
+
+ protected function shouldClear(Restoration $job): bool
+ {
+ return false;
+ }
+}
diff --git a/src/Concrete/Restore/Strategy/RestoreConfig.php b/src/Concrete/Restore/Strategy/RestoreConfig.php
new file mode 100644
index 0000000..1726745
--- /dev/null
+++ b/src/Concrete/Restore/Strategy/RestoreConfig.php
@@ -0,0 +1,27 @@
+getExtractDirectory();
+ }
+
+ protected function shouldClear(Restoration $job): bool
+ {
+ return false;
+ }
+}
diff --git a/src/Concrete/Restore/Strategy/RestoreDatabase.php b/src/Concrete/Restore/Strategy/RestoreDatabase.php
new file mode 100644
index 0000000..ab8d76f
--- /dev/null
+++ b/src/Concrete/Restore/Strategy/RestoreDatabase.php
@@ -0,0 +1,220 @@
+getOutputStyle();
+ $backup = $job->getBackup();
+ $manifest = $job->getManifest();
+ $database = $manifest->getDatabase();
+ $tempDir = $job->tempDir('database', true);
+
+ if ($database) {
+ $output->outputStep('Extracting SQL file');
+ $backup->extractTo($tempDir, $database);
+ $output->outputDone();
+ } else {
+ $this->getOutputStyle()->error('Database not included in backup.');
+ return false;
+ }
+
+ $output->outputStep('Determining database credentials');
+
+ $connection = $this->getDatabaseCredentials($job);
+ if (!($sql = $this->testCredentials($connection))) {
+ $output->outputDone('Error', '');
+ return false;
+ }
+ $output->outputDone();
+
+ $output->outputStep('Clearing database data');
+ $tables = $sql->query('show tables')->fetchAll(PDO::FETCH_COLUMN);
+ $sql->exec('set FOREIGN_KEY_CHECKS=0');
+
+ foreach ($tables as $table) {
+ if ($job->isDryRun()) {
+ continue;
+ }
+
+ if ($sql->exec("drop table `{$table}`") === false) {
+ throw new RuntimeException(
+ sprintf(
+ 'Unable to delete database table "%s" %s',
+ $table,
+ implode(', ', $sql->errorInfo())
+ )
+ );
+ }
+ }
+ $sql->exec('set FOREIGN_KEY_CHECKS=1');
+ if ($job->isDryRun()) {
+ $output->outputDryrun();
+ } else {
+ $output->outputDone();
+ }
+
+ $output->outputStep('Restoring');
+
+ $input = fopen($tempDir . '/' . $database, 'r+');
+ $process = process(
+ [
+ 'mysql',
+ '-u' . dot_get($connection, 'username', ''),
+ '-p' . dot_get($connection, 'password', ''),
+ '-h' . dot_get($connection, 'server', ''),
+ dot_get($connection, 'database', ''),
+ ],
+ null,
+ null,
+ $input,
+ 0
+ );
+
+ if ($job->isDryRun()) {
+ $output->outputDryrun();
+ $output->outputFinal();
+ return true;
+ } else {
+ $process->start(
+ function (string $channel, string $message): void {
+ if ($channel === 'err') {
+ throw new RuntimeException('Failed to restore database: ' . $message);
+ }
+ }
+ );
+
+ while ($process->isRunning()) {
+ sleep(1);
+ }
+
+ $output->outputDone($process->isSuccessful() ? 'Done!' : 'Failed.');
+ $output->outputFinal();
+ return $process->isSuccessful();
+ }
+ }
+
+ /**
+ * @param Restoration $job
+ * @return DatabaseCredentialsType
+ */
+ private function getDatabaseCredentials(Restoration $job): array
+ {
+ $extractConfigCredentials = function (array $config): array {
+ $default = dot_get($config, 'default-connection');
+ if ($default && $defaultConnection = dot_get($config, 'connections.' . $default)) {
+ return [
+ 'server' => (string)dot_get($defaultConnection, 'server', ''),
+ 'database' => (string)dot_get($defaultConnection, 'database', ''),
+ 'username' => (string)dot_get($defaultConnection, 'username', ''),
+ 'password' => (string)dot_get($defaultConnection, 'password', ''),
+ 'charset' => (string)dot_get($defaultConnection, 'character_set', ''),
+ 'collation' => (string)dot_get($defaultConnection, 'collation', ''),
+ 'cert' => (string)dot_get($defaultConnection, 'database', ''),
+ ];
+ }
+
+ throw new \InvalidArgumentException(
+ 'Invalid configuration provided. Couldn\'t resolve default connection.'
+ );
+ };
+
+ // First try loading directly using the connection if there is one
+ try {
+ $connection = $this->getConnection();
+ if ($connection && $connection instanceof ApplicationEnabledConnectionInterface) {
+ $config = $connection->getApplication()->make('config');
+ $result = $config->get('database');
+
+ return $extractConfigCredentials($result);
+ }
+ } catch (Throwable $e) {
+ // Ignore errors
+ }
+
+ // Next try calling the built in command line utility
+ try {
+ $paths = $job->findConcretePath('concrete/bin/concrete5');
+ $cliPath = array_shift($paths);
+ if ($cliPath) {
+ $process = process(
+ [
+ $cliPath,
+ 'c5:config',
+ 'get',
+ 'database'
+ ],
+ $job->getInstallation()->getPath()
+ );
+ $process->mustRun();
+
+ $result = json_decode($process->getOutput(), true);
+ return $extractConfigCredentials($result);
+ }
+ } catch (Throwable $e) {
+ // Ignore errors
+ }
+
+ // Finally try loading them directly, this is the least safe because these PHP files may refer to environment
+ // or declared variables
+ try {
+ $path = $job->findConcretePath('application/config/database.php');
+ $configPath = array_shift($path);
+
+ if ($configPath) {
+ return $extractConfigCredentials(include $configPath);
+ }
+ } catch (Throwable $e) {
+ // Ignore errors
+ }
+
+ throw new RuntimeException('Unable to determine database credentials.');
+ }
+
+ /**
+ * @param array|null $connection
+ * @return ?PDO
+ * @psalm-assert-if-true DatabaseCredentialsType $connection
+ */
+ private function testCredentials(?array $connection): ?PDO
+ {
+ if ($connection) {
+ // Test out database credentials
+ $sql = new PDO(
+ 'mysql:host=' . $connection['server'] . ';dbname=' . $connection['database'],
+ $connection['username'],
+ $connection['password']
+ );
+
+ $result = $sql->query('SHOW VARIABLES LIKE "%version%"');
+ if ($result->fetchAll()) {
+ return $sql;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/Concrete/Restore/Strategy/RestoreIndex.php b/src/Concrete/Restore/Strategy/RestoreIndex.php
new file mode 100644
index 0000000..450743c
--- /dev/null
+++ b/src/Concrete/Restore/Strategy/RestoreIndex.php
@@ -0,0 +1,47 @@
+getOutputStyle();
+ $backup = $job->getBackup();
+
+ $output->outputStep('Locating index');
+ if (!$job->getManifest()->includesIndex()) {
+ return false;
+ }
+ $output->outputDone();
+
+ $output->outputStep('Reading from backup');
+ $index = $backup->offsetGet('index.php');
+ if (!$index || !($data = $index->getContent())) {
+ return false;
+ }
+ $output->outputDone();
+
+ $configDir = new Filesystem(new Local($job->tempDir()));
+ $configDir->addPlugin(new ListPaths());
+
+ $output->outputStep('Replacing cached indexes');
+ foreach ($configDir->listPaths('indexes') as $file) {
+ $configDir->put($file, $data);
+ }
+ $output->outputDone();
+
+ $output->outputFinal();
+ return true;
+ }
+}
diff --git a/src/Concrete/Restore/Strategy/RestorePackages.php b/src/Concrete/Restore/Strategy/RestorePackages.php
new file mode 100644
index 0000000..5dfe00e
--- /dev/null
+++ b/src/Concrete/Restore/Strategy/RestorePackages.php
@@ -0,0 +1,24 @@
+getExtractDirectory();
+ }
+
+ protected function shouldClear(Restoration $job): bool
+ {
+ return true;
+ }
+}
diff --git a/src/Concrete/Restore/Strategy/RestoreStorageLocations.php b/src/Concrete/Restore/Strategy/RestoreStorageLocations.php
new file mode 100644
index 0000000..22a9a89
--- /dev/null
+++ b/src/Concrete/Restore/Strategy/RestoreStorageLocations.php
@@ -0,0 +1,75 @@
+getOutputStyle();
+ $output->outputStep('Validating site');
+ $install = $this->container->get(ConnectionInterface::class);
+ if (!$install instanceof ApplicationEnabledConnectionInterface) {
+ return false;
+ }
+
+ $app = $install->getApplication();
+ /** @var StorageLocationFactory $fileStorageFactory */
+ $fileStorageFactory = $app->make(StorageLocationFactory::class);
+
+ // Extract storage locations to a temp dir
+ $tmp = $job->tempDir('storage');
+ $job->getBackup()->extractTo($tmp, 'storage/');
+
+ $output->outputDone();
+
+ foreach ($job->getManifest()->getStorageLocations() as $location) {
+ $output->outputStep('Extracting storage location ' . $location['name']);
+
+ if ($job->isDryRun()) {
+ $output->outputDryrun();
+ continue;
+ }
+
+ $backupStorage = new Filesystem(new Local($tmp . '/storage/' . $location['id']));
+ $locationObject = $fileStorageFactory->fetchByID($location['id']);
+
+ /** @psalm-suppress DeprecatedClass Mountmanager is not removed in v2 */
+ $manager = new MountManager([
+ 'bu' => $backupStorage,
+ 'sl' => $locationObject->getFileSystemObject()
+ ]);
+
+ foreach ($manager->listContents('bu://', true) as $file) {
+ $path = $file['path'];
+ if ($file['type'] === 'dir') {
+ $manager->createDir('sl://' . $path);
+ continue;
+ }
+
+ try {
+ $manager->delete('sl://' . $path);
+ } catch (FileNotFoundException $e) {
+ // Ignore
+ }
+ $manager->copy('bu:///' . $path, 'sl://' . $path);
+ }
+ }
+
+ $this->getOutputStyle()->outputFinal();
+ return true;
+ }
+}
diff --git a/src/Concrete/Restore/StrategyInterface.php b/src/Concrete/Restore/StrategyInterface.php
new file mode 100644
index 0000000..87527ff
--- /dev/null
+++ b/src/Concrete/Restore/StrategyInterface.php
@@ -0,0 +1,9 @@
+inflatedDetectors();
+ foreach ($detectors as $detector) {
+ if ($version = $detector->versionAtPath($path)) {
+ return $version;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return DetectorInterface[]
+ */
+ protected function inflatedDetectors(): array
+ {
+ $detectors = $this->detectors;
+ if (!$this->mapped) {
+ $detectors = array_map(function ($detector) {
+ if (is_string($detector)) {
+ $detector = $this->getContainer()->get($detector);
+ }
+
+ return $detector;
+ }, $detectors);
+
+ $this->mapped = true;
+ $this->detectors = $detectors;
+ }
+
+ /** @psalm-var DetectorInterface[] */
+ return $detectors;
+ }
+}
diff --git a/src/Installation/Detector/DetectorInterface.php b/src/Installation/Detector/DetectorInterface.php
new file mode 100644
index 0000000..ed27543
--- /dev/null
+++ b/src/Installation/Detector/DetectorInterface.php
@@ -0,0 +1,11 @@
+loadVersion($versionFile)) {
+ return Version::fromVersionString($version);
+ }
+ }
+
+ return null;
+ }
+ protected function loadVersion(string $versionFile): ?string
+ {
+ $contents = file_get_contents($versionFile, false, null, 0, 1000);
+ $matches = [];
+ if ($contents && preg_match("/\\\$APP_VERSION = '(?.+?)';/", $contents, $matches)) {
+ return isset($matches['version']) ? $matches['version'] : null;
+ }
+
+ return null;
+ }
+}
diff --git a/src/Installation/Detector/Version7Detector.php b/src/Installation/Detector/Version7Detector.php
new file mode 100644
index 0000000..923a995
--- /dev/null
+++ b/src/Installation/Detector/Version7Detector.php
@@ -0,0 +1,41 @@
+loadVersion($versionFile)) {
+ return Version::fromVersionString($version);
+ }
+ }
+
+ return null;
+ }
+
+ protected function loadVersion(string $versionFile): ?string
+ {
+ $contents = file_get_contents($versionFile, false, null, 0, 1000);
+
+ $matches = [];
+ if ($contents && preg_match("/\s+'version'\s+=>\s+'(?.+?)',\n/m", $contents, $matches)) {
+ return isset($matches['version']) ? $matches['version'] : null;
+ }
+
+ return null;
+ }
+}
diff --git a/src/Installation/Factory.php b/src/Installation/Factory.php
deleted file mode 100644
index 145b58b..0000000
--- a/src/Installation/Factory.php
+++ /dev/null
@@ -1,48 +0,0 @@
-path = $path;
+ $this->version = $version;
}
/**
* @return string
*/
- public function getPath()
+ public function getPath(): string
{
return $this->path;
}
-
+ /**
+ * @return Version
+ */
+ public function getVersion(): Version
+ {
+ return $this->version;
+ }
-}
\ No newline at end of file
+ public function getFilesystem(): Filesystem
+ {
+ return new Filesystem(new Local($this->getPath()));
+ }
+}
diff --git a/src/Installation/InstallationAwareInterface.php b/src/Installation/InstallationAwareInterface.php
new file mode 100644
index 0000000..f494005
--- /dev/null
+++ b/src/Installation/InstallationAwareInterface.php
@@ -0,0 +1,12 @@
+traitInstallation = $connection;
+ }
+
+ protected function getInstallation(): Installation
+ {
+ return $this->traitInstallation;
+ }
+}
diff --git a/src/Installation/InstallationDetector.php b/src/Installation/InstallationDetector.php
new file mode 100644
index 0000000..624b71e
--- /dev/null
+++ b/src/Installation/InstallationDetector.php
@@ -0,0 +1,66 @@
+versionDetector = $detector;
+ }
+
+ /**
+ * Deals with a potential relative path.
+ *
+ * @param string $path
+ *
+ * @return null|string
+ */
+ protected function normalizePath(string $path): ?string
+ {
+ if (!$path) {
+ // Obtain the current working directory
+ // (have to do it this way because when of symlinks with local composer)
+ $path = dirname(getcwd() . DIRECTORY_SEPARATOR . $_SERVER['PHP_SELF']);
+
+ // Load autoload relative to this location
+ $path .= implode(self::DIRECTORY_SEPARATOR, ['..', '..', self::DIRNAME_COMPOSER_PUBLIC]);
+ }
+
+ if (substr($path, 0, 1) === '.') {
+ // This is a relative path
+ $path = getcwd() . DIRECTORY_SEPARATOR . $path;
+ }
+ return realpath($path) ?: null; // realpath just in case.
+ }
+
+ public function detect(string $path): Installation
+ {
+ $path = $this->normalizePath($path);
+ if (!$path) {
+ throw new InstallationNotFound('Unable to determine path.');
+ }
+
+ $version = $this->versionDetector->versionAtPath($path);
+
+ if (!$version) {
+ throw InstallationNotFound::atPath($path);
+ }
+
+ return new Installation($path, $version);
+ }
+}
diff --git a/src/Installation/InstallationServiceProvider.php b/src/Installation/InstallationServiceProvider.php
new file mode 100644
index 0000000..1dbea82
--- /dev/null
+++ b/src/Installation/InstallationServiceProvider.php
@@ -0,0 +1,45 @@
+getLeagueContainer()->add(Installation::class, function (): ?Installation {
+ $factory = $this->getContainer()->get(InstallationDetector::class);
+ $input = $this->getContainer()->get(InputInterface::class)->getOption('instance');
+ if (!is_string($input)) {
+ throw new \RuntimeException('Invalid instance value given, input must be a string.');
+ }
+
+ return $factory->detect($input);
+ }, true);
+
+ $this->getLeagueContainer()
+ ->add(InstallationDetector::class)
+ ->addArgument(BaseDetector::class);
+ }
+
+ public function boot()
+ {
+ $this->getLeagueContainer()->inflector(InstallationAwareInterface::class)
+ ->invokeMethod('setInstallation', [Installation::class]);
+ }
+}
diff --git a/src/Installation/Manifest.php b/src/Installation/Manifest.php
new file mode 100644
index 0000000..6d42f12
--- /dev/null
+++ b/src/Installation/Manifest.php
@@ -0,0 +1,411 @@
+created = new \DateTimeImmutable();
+ }
+
+ public function addPackage(string $handle, bool $installed, bool $included): self
+ {
+ $self = clone $this;
+ $self->packages[] = [
+ 'handle' => $handle,
+ 'included' => $included,
+ 'installed' => $installed,
+ ];
+
+ return $self;
+ }
+
+ /**
+ * @param string $handle
+ * @return array
+ * @psalm-return ?PackageType
+ */
+ public function getPackage(string $handle): ?array
+ {
+ foreach ($this->packages as $package) {
+ if ($package['handle'] === $handle) {
+ return $package;
+ }
+ }
+
+ return null;
+ }
+
+ public function addStorageLocation(int $id, string $name, bool $default, bool $included): self
+ {
+ $self = clone $this;
+ $self->storageLocations[] = [
+ 'id' => $id,
+ 'name' => $name,
+ 'default' => $default,
+ 'included' => $included,
+ ];
+
+ return $self;
+ }
+
+ public function addApplicationItem(string $item): self
+ {
+ $self = $this;
+ if (!in_array($item, $this->applicationContents)) {
+ $self = clone $this;
+ $self->applicationContents[] = $item;
+ sort($self->applicationContents);
+ }
+
+ return $self;
+ }
+
+ public function addApplicationItems(array $items): self
+ {
+ $self = $this;
+ foreach ($items as $item) {
+ $self = $self->addApplicationItem($item);
+ }
+
+ return $self;
+ }
+
+ /**
+ * @return array
+ * @psalm-return ManifestType
+ */
+ public function jsonSerialize()
+ {
+ return [
+ 'created' => $this->getDateCreated() ? $this->getDateCreated()->format(self::DATE_FORMAT) : null,
+ 'site' => $this->getSiteName(),
+ 'url' => $this->getUrl(),
+ 'path' => $this->getPath(),
+ 'host' => $this->getHostName(),
+ 'version' => $this->getVersion(),
+ 'contents' => [
+ 'application' => !!$this->getApplicationContents(),
+ 'applicationContents' => $this->getApplicationContents(),
+ 'core' => $this->includesCore(),
+ 'database' => $this->getDatabase(),
+ 'index' => $this->includesIndex(),
+ 'packages' => $this->getPackages(),
+ 'storageLocations' => $this->getStorageLocations(),
+ ]
+ ];
+ }
+
+ /**
+ * @psalm-suppress MismatchingDocblockParamType
+ * @psalm-param ManifestType $data
+ */
+ public static function jsonDeserialize(array $data): Manifest
+ {
+ $createdDate = dot_get($data, 'created');
+ $createdDateTime = $createdDate ? \DateTimeImmutable::createFromFormat(self::DATE_FORMAT, $createdDate) : null;
+
+ $self = new Manifest();
+ $self->created = $createdDateTime ?: null;
+ $self->siteName = dot_get($data, 'site');
+ $self->hostName = dot_get($data, 'host');
+ $self->url = dot_get($data, 'url');
+ $self->installationPath = dot_get($data, 'path');
+ $self->version = dot_get($data, 'version');
+ $contents = dot_get($data, 'contents', []);
+ $self->database = dot_get($contents, 'database');
+ $self->includeCore = (bool) dot_get($contents, 'core', false);
+ $self->includeIndex = (bool) dot_get($contents, 'index', false);
+ $self->applicationContents = (array) dot_get($contents, 'applicationContents');
+
+ // Load in packages
+ $packages = (array) dot_get($contents, 'packages');
+ foreach ($packages as $package) {
+ $package = (array) $package;
+ $self = $self->addPackage(
+ dot_get($package, 'handle', ''),
+ dot_get($package, 'installed', false),
+ dot_get($package, 'included', false)
+ );
+ }
+
+ // Load in storage locations
+ $locations = (array) dot_get($contents, 'storageLocations');
+ foreach ($locations as $location) {
+ $location = (array) $location;
+ $self = $self->addStorageLocation(
+ dot_get($location, 'id', 0),
+ dot_get($location, 'name', ''),
+ dot_get($location, 'default', false),
+ dot_get($location, 'included', false)
+ );
+ }
+
+ return $self;
+ }
+
+
+ /**
+ * @psalm-return PackageType[]
+ */
+ public function getPackages(): array
+ {
+ return $this->packages;
+ }
+
+ /**
+ * @psalm-return StorageLocationType[]
+ */
+ public function getStorageLocations(): array
+ {
+ return $this->storageLocations;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getApplicationContents(): array
+ {
+ return $this->applicationContents;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getDatabase(): ?string
+ {
+ return $this->database;
+ }
+
+ /**
+ * @return string
+ */
+ public function getVersion(): string
+ {
+ return $this->version;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getPath(): ?string
+ {
+ return $this->installationPath;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getUrl(): ?string
+ {
+ return $this->url;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getHostName(): ?string
+ {
+ return $this->hostName;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getSiteName(): ?string
+ {
+ return $this->siteName;
+ }
+
+ /**
+ * @return bool
+ */
+ public function includesCore(): bool
+ {
+ return $this->includeCore;
+ }
+
+ /**
+ * @return bool
+ */
+ public function includesIndex(): bool
+ {
+ return $this->includeIndex;
+ }
+
+ /**
+ * @param string|null $database
+ * @return Manifest
+ */
+ public function setDatabase(?string $database): Manifest
+ {
+ $self = clone $this;
+ $self->database = $database;
+ return $self;
+ }
+
+ /**
+ * @param string $version
+ * @return Manifest
+ */
+ public function setVersion(string $version): Manifest
+ {
+ $self = clone $this;
+ $self->version = $version;
+ return $self;
+ }
+
+ /**
+ * @param string|null $installationPath
+ * @return Manifest
+ */
+ public function setInstallationPath(?string $installationPath): Manifest
+ {
+ $self = clone $this;
+ $self->installationPath = $installationPath;
+ return $self;
+ }
+
+ /**
+ * @param string|null $url
+ * @return Manifest
+ */
+ public function setUrl(?string $url): Manifest
+ {
+ $self = clone $this;
+ $self->url = $url;
+ return $self;
+ }
+
+ /**
+ * @param string|null $hostName
+ * @return Manifest
+ */
+ public function setHostName(?string $hostName): Manifest
+ {
+ $self = clone $this;
+ $self->hostName = $hostName;
+ return $self;
+ }
+
+ /**
+ * @param string|null $siteName
+ * @return Manifest
+ */
+ public function setSiteName(?string $siteName): Manifest
+ {
+ $self = clone $this;
+ $self->siteName = $siteName;
+ return $self;
+ }
+
+ /**
+ * @param bool $includeCore
+ * @return Manifest
+ */
+ public function setIncludeCore(bool $includeCore): Manifest
+ {
+ $self = clone $this;
+ $self->includeCore = $includeCore;
+ return $self;
+ }
+
+ /**
+ * @param bool $includeIndex
+ * @return Manifest
+ */
+ public function setIncludeIndex(bool $includeIndex): Manifest
+ {
+ $self = clone $this;
+ $self->includeIndex = $includeIndex;
+ return $self;
+ }
+
+ /**
+ * @param \DateTimeInterface|null $created
+ */
+ public function setDateCreated(?\DateTimeInterface $created): Manifest
+ {
+ if ($created instanceof \DateTime) {
+ $created = \DateTimeImmutable::createFromMutable($created);
+ } elseif (!$created instanceof \DateTimeImmutable) {
+ throw new \RuntimeException('Unknown datetime type provided.');
+ }
+
+ $self = clone $this;
+ $self->created = $created;
+
+ return $self;
+ }
+
+ /**
+ * @return \DateTimeImmutable|null
+ */
+ public function getDateCreated(): ?\DateTimeImmutable
+ {
+ return $this->created;
+ }
+}
diff --git a/src/Installation/ManifestFactory.php b/src/Installation/ManifestFactory.php
new file mode 100644
index 0000000..1af9c70
--- /dev/null
+++ b/src/Installation/ManifestFactory.php
@@ -0,0 +1,38 @@
+offsetGet('manifest.json')->getContent(), true);
+
+ if (!$contents) {
+ throw InvalidManifest::atPath($path);
+ }
+
+ return Manifest::jsonDeserialize($contents);
+ }
+
+ public function forConnection(ConnectionInterface $connection): ?Manifest
+ {
+ if (!$connection instanceof ApplicationEnabledConnectionInterface) {
+ return null;
+ }
+
+ // @TODO Implement this factory method
+ return new Manifest();
+ }
+}
diff --git a/src/Installation/Validator.php b/src/Installation/Validator.php
index 2b7963c..94de459 100644
--- a/src/Installation/Validator.php
+++ b/src/Installation/Validator.php
@@ -10,23 +10,34 @@
class Validator
{
- const DIRNAME_CONCRETE = 'concrete';
- const FILENAME_DISPATCHER = 'dispatcher.php';
+ public const DIRNAME_CONCRETE = 'concrete';
+ public const FILENAME_DISPATCHER = 'dispatcher.php';
public function isValid(Installation $installation): bool
{
if (!is_dir($installation->getPath())) {
- throw new \RuntimeException(sprintf('Unable to locate installation directory: %s', $installation->getPath()));
+ throw new \RuntimeException(
+ sprintf(
+ 'Unable to locate installation directory: %s',
+ $installation->getPath()
+ )
+ );
}
- if (!file_exists(
- $installation->getPath() . DIRECTORY_SEPARATOR . self::DIRNAME_CONCRETE . DIRECTORY_SEPARATOR .
- self::FILENAME_DISPATCHER)) {
- throw new \RuntimeException(sprintf('Installation directory %s does not appear to be a valid Concrete installation', $installation->getPath()));
+ $path = implode(
+ DIRECTORY_SEPARATOR,
+ [$installation->getPath(), self::DIRNAME_CONCRETE, self::FILENAME_DISPATCHER]
+ );
+
+ if (!file_exists($path)) {
+ throw new \RuntimeException(
+ sprintf(
+ 'Installation directory %s does not appear to be a valid Concrete installation',
+ $installation->getPath()
+ )
+ );
}
return true;
}
-
-
-}
\ No newline at end of file
+}
diff --git a/src/Installation/Version.php b/src/Installation/Version.php
new file mode 100644
index 0000000..3f56d38
--- /dev/null
+++ b/src/Installation/Version.php
@@ -0,0 +1,171 @@
+versionString = $version;
+ }
+
+ public function getVersion(): string
+ {
+ return $this->versionString;
+ }
+
+ public static function fromVersionString(string $version): Version
+ {
+ $version = self::normalizeVersionString($version);
+ return new self($version);
+ }
+
+ /**
+ * Normalize a given version into something that works well with the comparator.
+ *
+ * @param string $version
+ * @param int $segments
+ * @param bool $validate
+ * @param bool $keepNoun
+ * @return string
+ *
+ * @psalm-pure
+ */
+ public static function normalizeVersionString(
+ string $version,
+ int $segments = 5,
+ bool $validate = true,
+ bool $keepNoun = true
+ ): string {
+ $version = explode('.', trim($version));
+ $finalSegment = array_pop($version);
+ $withoutNoun = (string)intval($finalSegment);
+ $noun = substr($finalSegment, strlen($withoutNoun));
+ $version[] = $withoutNoun;
+
+ if ($validate && count($version) > $segments) {
+ throw new InvalidArgumentException('Invalid version number provided, too many version segments.');
+ }
+
+ if (count($version) > $segments) {
+ // Delete segments
+ $version = array_slice($version, 0, $segments);
+ } else {
+ // Add segments
+ $version = array_pad($version, $segments, '0');
+ }
+
+ return implode('.', $version) . ($keepNoun ? $noun : '');
+ }
+
+ /**
+ * @return string
+ * @psalm-return '5.5'|'5.6'|'5.7'|'8'|'9'|'10'
+ */
+ public function getMajorVersion(): string
+ {
+ $version = $this->getVersion();
+ if (substr($version, 0, 1) === '5') {
+ $version = self::normalizeVersionString($version, 2, false, false);
+ } else {
+ $version = self::normalizeVersionString($version, 1, false, false);
+ }
+
+ /** @var '5.6'|'5.7'|'8'|'9' $version */
+ return $version;
+ }
+
+ /**
+ * Evaluates the expression: $version1 > $version2.
+ *
+ * @param string $version
+ *
+ * @return bool
+ */
+ public function greaterThan(string $version): bool
+ {
+ return Comparator::greaterThan($this->versionString, self::normalizeVersionString($version));
+ }
+
+ /**
+ * Evaluates the expression: $version1 >= $version2.
+ *
+ * @param string $version
+ *
+ * @return bool
+ */
+ public function greaterThanOrEqualTo(string $version): bool
+ {
+ return Comparator::greaterThanOrEqualTo($this->versionString, self::normalizeVersionString($version));
+ }
+
+ /**
+ * Evaluates the expression: $version1 < $version2.
+ *
+ * @param string $version
+ *
+ * @return bool
+ */
+ public function lessThan(string $version): bool
+ {
+ /** @psalm-suppress */
+ return Comparator::lessThan($this->versionString, self::normalizeVersionString($version));
+ }
+
+ /**
+ * Evaluates the expression: $version1 <= $version2.
+ *t
+ * @param string $version
+ *
+ * @return bool
+ */
+ public function lessThanOrEqualTo(string $version): bool
+ {
+ return Comparator::lessThanOrEqualTo($this->versionString, self::normalizeVersionString($version));
+ }
+
+ /**
+ * Evaluates the expression: $version1 == $version2.
+ *
+ * @param string $version
+ *
+ * @return bool
+ */
+ public function equalTo(string $version): bool
+ {
+ return Comparator::equalTo($this->versionString, self::normalizeVersionString($version));
+ }
+
+ /**
+ * Evaluates the expression: $version1 != $version2.
+ *
+ * @param string $version
+ *
+ * @return bool
+ */
+ public function notEqualTo(string $version): bool
+ {
+ return Comparator::notEqualTo($this->versionString, self::normalizeVersionString($version));
+ }
+}
diff --git a/src/Util/Config.php b/src/Util/Config.php
new file mode 100644
index 0000000..2f39efc
--- /dev/null
+++ b/src/Util/Config.php
@@ -0,0 +1,39 @@
+getPath() . '/.concrete/' . $fileName;
+ }
+
+ $check[] = Platform::configDirectory() . '/' . $fileName;
+ $check[] = __DIR__ . '/../../.concrete/' . $fileName;
+
+ foreach ($check as $path) {
+ if (file_exists($path)) {
+ return $path;
+ }
+ }
+
+ return null;
+ }
+
+ public static function maintenancePage(Installation $intallation = null): string
+ {
+ $path = self::findFile('index.maintenance.php', $intallation);
+ if ($path) {
+ return file_get_contents($path);
+ }
+
+ return 'MAINTENANCE MODE';
+ }
+}
diff --git a/src/Util/Platform.php b/src/Util/Platform.php
new file mode 100644
index 0000000..56b206f
--- /dev/null
+++ b/src/Util/Platform.php
@@ -0,0 +1,148 @@
+
+ */
+class Platform
+{
+ /**
+ * Parses tildes and environment variables in paths.
+ *
+ * @param string $path
+ * @return string
+ */
+ public static function expandPath(string $path): string
+ {
+ if (preg_match('#^~[\\/]#', $path)) {
+ return self::getUserDirectory() . substr($path, 1);
+ }
+
+ return preg_replace_callback(
+ '#^(\$|(?P%))(?P\w++)(?(percent)%)(?P.*)#',
+ function (array $matches) {
+ // Treat HOME as an alias for USERPROFILE on Windows for legacy reasons
+ if (Platform::isWindows() && $matches['var'] == 'HOME') {
+ return (getenv('HOME') ?: getenv('USERPROFILE')) . $matches['path'];
+ }
+
+ return getenv($matches['var']) . $matches['path'];
+ },
+ $path
+ );
+ }
+
+ /**
+ * @return string The formal user home as detected from environment parameters
+ * @throws RuntimeException If the user home could not reliably be determined
+ */
+ public static function getUserDirectory(): string
+ {
+ if (false !== ($home = getenv('HOME'))) {
+ return $home;
+ }
+
+ if (self::isWindows() && false !== ($home = getenv('USERPROFILE'))) {
+ return $home;
+ }
+
+ if (function_exists('posix_getuid') && function_exists('posix_getpwuid')) {
+ $info = posix_getpwuid(posix_getuid());
+
+ return $info['dir'];
+ }
+
+ throw new RuntimeException('Could not determine user directory');
+ }
+
+ public static function configDirectory(): string
+ {
+ $configDir = self::getUserDirectory() . '/.config/concrete';
+ if (!file_exists($configDir)) {
+ mkdir($configDir, 0777, true);
+ }
+
+ return $configDir;
+ }
+
+ public static function tempDirectory(bool $createSubpath = false): string
+ {
+ $tempDir = self::configDirectory() . '/tmp';
+ if (!file_exists($tempDir)) {
+ mkdir($tempDir, 0777, true);
+ }
+
+ if ($createSubpath) {
+ do {
+ $subpath = $tempDir . '/' . uniqid('tmpdir_');
+ } while (file_exists($subpath));
+
+ mkdir($subpath, 0777, true);
+
+ return $subpath;
+ }
+
+ return $tempDir;
+ }
+
+ /**
+ * @return bool Whether the host machine is running a Windows OS
+ */
+ public static function isWindows()
+ {
+ return defined('PHP_WINDOWS_VERSION_BUILD');
+ }
+
+ /**
+ * @param string $str
+ * @return int return a guaranteed binary length of the string, regardless of silly mbstring configs
+ */
+ public static function strlen(string $str): int
+ {
+ static $useMbString = null;
+ if (null === $useMbString) {
+ $useMbString = function_exists('mb_strlen') && ini_get('mbstring.func_overload');
+ }
+
+ if ($useMbString) {
+ return mb_strlen($str, '8bit');
+ }
+
+ return strlen($str);
+ }
+
+ /**
+ * @param ?resource $fd
+ * @return bool
+ */
+ public static function isTty($fd = null): bool
+ {
+ if ($fd === null) {
+ $fd = defined('STDOUT') ? STDOUT : fopen('php://stdout', 'w');
+ }
+
+ // modern cross-platform function, includes the fstat
+ // fallback so if it is present we trust it
+ if (function_exists('stream_isatty')) {
+ return stream_isatty($fd);
+ }
+
+ // only trusting this if it is positive, otherwise prefer fstat fallback
+ if (function_exists('posix_isatty') && posix_isatty($fd)) {
+ return true;
+ }
+
+ $stat = @fstat($fd);
+ // Check if formatted mode is S_IFCHR
+ return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
+ }
+}
diff --git a/src/Util/Ssh.php b/src/Util/Ssh.php
new file mode 100644
index 0000000..ffcffe4
--- /dev/null
+++ b/src/Util/Ssh.php
@@ -0,0 +1,304 @@
+user = $user;
+ $this->host = $host;
+ $this->port = $port;
+ $this->processConfigurationClosure = function (Process $process): void {
+ };
+ $this->onOutput = function (string $type, string $line): void {
+ };
+ }
+
+ /**
+ * @param string $user
+ * @param string $host
+ * @param int|null $port
+ * @return static
+ */
+ public static function create(string $user, string $host, int $port = null): self
+ {
+ return new Ssh($user, $host, $port);
+ }
+
+ public function withPrivateKey(string $pathToPrivateKey): self
+ {
+ $self = clone $this;
+ $self->pathToPrivateKey = $pathToPrivateKey;
+
+ return $self;
+ }
+
+ /**
+ * @param int $port
+ * @return $this
+ * @throws Exception If the port is negative
+ */
+ public function withPort(int $port): self
+ {
+ if ($port < 0) {
+ throw new Exception('Port must be a positive integer.');
+ }
+ $self = clone $this;
+ $self->port = $port;
+
+ return $self;
+ }
+
+ public function withProcessConfiguration(Closure $processConfigurationClosure): self
+ {
+ $self = clone $this;
+ $self->processConfigurationClosure = $processConfigurationClosure;
+
+ return $self;
+ }
+
+ public function withOnOutput(Closure $onOutput): self
+ {
+ $self = clone $this;
+ $self->onOutput = $onOutput;
+
+ return $self;
+ }
+
+ public function withStrictHostKeyChecking(): self
+ {
+ $self = clone $this;
+ $self->enableStrictHostChecking = true;
+
+ return $self;
+ }
+
+ public function withoutStrictHostKeyChecking(): self
+ {
+ $self = clone $this;
+ $self->enableStrictHostChecking = false;
+
+ return $self;
+ }
+
+ public function withQuietMode(): self
+ {
+ $self = clone $this;
+ $self->quietMode = true;
+
+ return $self;
+ }
+
+ public function withoutQuietMode(): self
+ {
+ $self = clone $this;
+ $self->quietMode = false;
+
+ return $self;
+ }
+
+ public function withoutPasswordAuthentication(): self
+ {
+ $self = clone $this;
+ $self->enablePasswordAuthentication = false;
+
+ return $self;
+ }
+
+ public function withPasswordAuthentication(): self
+ {
+ $self = clone $this;
+ $self->enablePasswordAuthentication = true;
+
+ return $self;
+ }
+
+ /**
+ * @param string|array $command
+ * @return string
+ */
+ public function getExecuteCommand($command): string
+ {
+ $commands = $this->wrapArray($command);
+ $extraOptions = $this->getExtraSshOptions();
+ $commandString = implode(PHP_EOL, $commands);
+ $delimiter = 'EOF-SPATIE-SSH';
+ $target = $this->getTarget();
+
+ return "{$this->sshExecutable} {$extraOptions} {$target} 'bash -se' << \\$delimiter" . PHP_EOL
+ . $commandString . PHP_EOL
+ . $delimiter;
+ }
+
+ /**
+ * @param string|array $command
+ *
+ * @return Process
+ */
+ public function execute($command): Process
+ {
+ $sshCommand = $this->getExecuteCommand($command);
+
+ return $this->run($sshCommand);
+ }
+
+ /**
+ * @param string|array $command
+ *
+ * @return Process
+ */
+ public function executeAsync($command): Process
+ {
+ $sshCommand = $this->getExecuteCommand($command);
+
+ return $this->run($sshCommand, 'start');
+ }
+
+ /**
+ * @param string $sourcePath
+ * @param string $destinationPath
+ * @return string
+ */
+ public function getDownloadCommand(string $sourcePath, string $destinationPath): string
+ {
+ return "{$this->scpExecutable} {$this->getExtraScpOptions()} {$this->getTarget()}:$sourcePath $destinationPath";
+ }
+
+ public function download(string $sourcePath, string $destinationPath): Process
+ {
+ $downloadCommand = $this->getDownloadCommand($sourcePath, $destinationPath);
+
+ return $this->run($downloadCommand);
+ }
+
+ public function getUploadCommand(string $sourcePath, string $destinationPath): string
+ {
+ return "{$this->scpExecutable} {$this->getExtraScpOptions()} $sourcePath {$this->getTarget()}:$destinationPath";
+ }
+
+ public function upload(string $sourcePath, string $destinationPath): Process
+ {
+ $uploadCommand = $this->getUploadCommand($sourcePath, $destinationPath);
+
+ return $this->run($uploadCommand);
+ }
+
+ protected function getExtraSshOptions(): string
+ {
+ $extraOptions = $this->getExtraOptions();
+
+ if (!is_null($this->port)) {
+ $extraOptions[] = "-p {$this->port}";
+ }
+
+ return implode(' ', $extraOptions);
+ }
+
+ /**
+ * @return string
+ */
+ protected function getExtraScpOptions(): string
+ {
+ $extraOptions = $this->getExtraOptions();
+
+ $extraOptions[] = '-r';
+
+ if (!is_null($this->port)) {
+ $extraOptions[] = "-P {$this->port}";
+ }
+
+ return implode(' ', $extraOptions);
+ }
+
+ /**
+ * @return array
+ */
+ private function getExtraOptions(): array
+ {
+ $extraOptions = [];
+
+ if ($this->pathToPrivateKey) {
+ $extraOptions[] = "-i {$this->pathToPrivateKey}";
+ }
+
+ if (!$this->enableStrictHostChecking) {
+ $extraOptions[] = '-o StrictHostKeyChecking=no';
+ $extraOptions[] = '-o UserKnownHostsFile=/dev/null';
+ }
+
+ if (!$this->enablePasswordAuthentication) {
+ $extraOptions[] = '-o PasswordAuthentication=no';
+ }
+
+ if ($this->quietMode) {
+ $extraOptions[] = '-q';
+ }
+
+ return $extraOptions;
+ }
+
+ /**
+ * @param array|string $arrayOrString
+ * @return array
+ */
+ protected function wrapArray($arrayOrString): array
+ {
+ return (array)$arrayOrString;
+ }
+
+ public function run(string $command, string $method = 'run'): Process
+ {
+ $process = \process($command, null, null, null, 0);
+ ($this->processConfigurationClosure)($process);
+ $process->{$method}($this->onOutput);
+
+ return $process;
+ }
+
+ protected function getTarget(): string
+ {
+ return "{$this->user}@{$this->host}";
+ }
+}
diff --git a/tests/Installation/Detector/AbstractVersionDetectorTest.php b/tests/Installation/Detector/AbstractVersionDetectorTest.php
new file mode 100644
index 0000000..80adde9
--- /dev/null
+++ b/tests/Installation/Detector/AbstractVersionDetectorTest.php
@@ -0,0 +1,66 @@
+ [],
+ 'invalid' => [],
+ ];
+
+ /**
+ * @dataProvider validPaths
+ */
+ public function testDetectsValidPaths(string $path, string $expected)
+ {
+ $detector = new $this->class();
+ $version = $detector->versionAtPath($path);
+
+ $this->assertNotNull($version, 'Expected version not matched at path ' . $path);
+ $this->assertEquals(
+ Version::normalizeVersionString($expected),
+ $version->getVersion(),
+ 'Invalid version matched for path.'
+ );
+ }
+
+ /**
+ * @dataProvider invalidPaths
+ */
+ public function testSkipsInvalidPaths(string $path)
+ {
+ $detector = new $this->class();
+ $this->assertNull($detector->versionAtPath($path), 'Found version where none should be found.');
+ }
+
+ public function validPaths()
+ {
+ $base = realpath(__DIR__ . '/../../../');
+ return array_map(
+ function ($path) use ($base) {
+ return [$base . '/' . ltrim($path[0], '/'), $path[1]];
+ },
+ $this->paths['valid']
+ );
+ }
+
+ public function invalidPaths()
+ {
+ $base = realpath(__DIR__ . '/../../../');
+ return array_map(
+ function ($path) use ($base) {
+ return [$base . $path[0]];
+ },
+ $this->paths['invalid']
+ );
+ }
+}
diff --git a/tests/Installation/Detector/Version6DetectorTest.php b/tests/Installation/Detector/Version6DetectorTest.php
new file mode 100644
index 0000000..f55208e
--- /dev/null
+++ b/tests/Installation/Detector/Version6DetectorTest.php
@@ -0,0 +1,19 @@
+ [
+ ['tests/fixtures/adapter/v6/Installed', '5.6.4'],
+ ],
+ 'invalid' => [
+ ['tests/fixtures/adapter/v6/NotInstalled'],
+ ['tests/fixtures/adapter/v7'],
+ ['tests/fixtures/adapter/v8'],
+ ]
+ ];
+}
diff --git a/tests/Installation/Detector/Version7DetectorTest.php b/tests/Installation/Detector/Version7DetectorTest.php
new file mode 100644
index 0000000..b3f0c95
--- /dev/null
+++ b/tests/Installation/Detector/Version7DetectorTest.php
@@ -0,0 +1,21 @@
+ [
+ ['tests/fixtures/adapter/v7', '5.7.5.13'],
+ ['tests/fixtures/adapter/v8', '8.6.0a2'],
+ ],
+ 'invalid' => [
+ ['tests/fixtures/adapter/v6/NotInstalled'],
+ ['tests/fixtures/adapter/v6/Installed'],
+ ]
+ ];
+}
diff --git a/tests/Installation/InstallationDetectorTest.php b/tests/Installation/InstallationDetectorTest.php
new file mode 100644
index 0000000..fd45170
--- /dev/null
+++ b/tests/Installation/InstallationDetectorTest.php
@@ -0,0 +1,13 @@
+setDateCreated(\DateTime::createFromFormat(Manifest::DATE_FORMAT, '2021-03-10T20:40:50+0000'))
+ ->setSiteName('concrete5 Site')
+ ->setUrl('https://some.url')
+ ->setInstallationPath('/home/foo/concrete5')
+ ->setHostName('foo.local')
+ ->setVersion('1.0')
+ ->setDatabase('db.sql')
+ ->setIncludeCore(true)
+ ->setIncludeIndex(true)
+
+ // Add application stuff
+ ->addApplicationItems([
+ 'views',
+ 'tools',
+ 'themes',
+ 'src',
+ 'single_pages',
+ 'page_templates',
+ 'mail',
+ 'languages',
+ 'attributes',
+ 'authentication',
+ 'blocks',
+ 'bootstrap',
+ 'config',
+ 'controllers',
+ 'elements',
+ 'jobs',
+ ])
+
+ // Add storage locations
+ ->addStorageLocation(1, 'red', true, true)
+ ->addStorageLocation(2, 'blue', true, false)
+ ->addStorageLocation(3, 'green', false, true)
+ ->addStorageLocation(4, 'yellow', false, false)
+
+ // Add Packages
+ ->addPackage('foo', true, true)
+ ->addPackage('baz', false, true)
+ ->addPackage('zab', true, false)
+ ->addPackage('bar', false, false)
+ ;
+
+ $contents = json_decode(file_get_contents(__DIR__ . '/../fixtures/manifest/complete.json'), true);
+ $this->assertSame($contents, $manifest->jsonSerialize());
+ }
+
+ /**
+ * @dataProvider filesToTest
+ */
+ public function testCompleteJson(string $file): void
+ {
+ $contents = json_decode(file_get_contents(__DIR__ . '/../fixtures/manifest/' . $file), true);
+ $manifest = Manifest::jsonDeserialize($contents);
+
+ // Validate the manifest against expected values
+ $this->assertEquals($contents, $manifest->jsonSerialize());
+ }
+
+ public function filesToTest(): iterable
+ {
+ yield ['complete.json'];
+ yield ['opposite.json'];
+ }
+}
diff --git a/tests/Installation/VersionTest.php b/tests/Installation/VersionTest.php
new file mode 100644
index 0000000..8277de4
--- /dev/null
+++ b/tests/Installation/VersionTest.php
@@ -0,0 +1,90 @@
+ $expected) {
+ $this->assertEquals($expected, $version->{$method}($compare), "{$method} {$errorString}");
+ }
+ }
+
+ public function versionComparisons(): array
+ {
+ $greater = [true, true, false, false, false, true];
+ $equal = [false, true, false, true, true, false];
+ $less = [false, false, true, true, false, true];
+
+ return [
+ // Long version chains vs short version numbers
+ ['5.7.0', '5.7', $equal],
+ ['5.7.0.0.1', '5.7', $greater],
+ ['5.6.0.0.1', '5.7', $less],
+ ['5.7.5.2', '5', $greater],
+ ['5.7.5.2', '5.7.5.2', $equal],
+ ['5.7.5.2', '5.7.5.3', $less],
+
+ // Broad spectrum
+ ['5.6', '9', $less],
+ ['9', '5.6', $greater],
+
+ // Versions with nouns
+ ['8.5.0RC1', '8.5.0', $less],
+ ['8.5.0RC1', '8.5.0RC2', $less],
+ ['8.5.0RC3', '8.5.0RC2', $greater],
+ ['8.5.0-alpha', '8.5.0-beta', $less],
+ ['8.5.1-alpha', '8.5.0', $greater],
+ ['5.5.2.2a1', '5.5.2.2', $less],
+ ['5.5.2.2a1', '5.5.2.1', $greater],
+ ];
+ }
+
+ /**
+ * @dataProvider majorVersions
+ * @param string $version
+ * @param string $expected
+ */
+ public function testMajorVersion(string $version, string $expected)
+ {
+ $version = Version::fromVersionString($version);
+ $this->assertEquals($expected, $version->getMajorVersion());
+ }
+
+ public function majorVersions(): array
+ {
+ return [
+ ['5.5.2.2a1', '5.5'],
+ ['5.6.5.5.1RC2', '5.6'],
+ ['5.7.5.5.1', '5.7'],
+ ['8.5.5.1', '8'],
+ ['9', '9'],
+ ['9.0.0.0.0beta', '9'],
+ ['10.1.1', '10'],
+ ];
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..5f8d156
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,26 @@
+version;
+ }
+}
diff --git a/tests/fixtures/adapter/v6/Installed/concrete/config/version.php b/tests/fixtures/adapter/v6/Installed/concrete/config/version.php
new file mode 100644
index 0000000..c277d19
--- /dev/null
+++ b/tests/fixtures/adapter/v6/Installed/concrete/config/version.php
@@ -0,0 +1,3 @@
+ '5.7.5.13',
+ 'version_installed' => '5.7.5.13',
+ 'version_db' => '20160615000000', // the key of the latest database migration
+
+ /**
+ * Installation status
+ *
+ * @var bool
+ */
+ 'installed' => true,
+
+ /**
+ * The current Site Name
+ *
+ * @var string concrete.core.site
+ */
+ 'site' => 'concrete5',
+
+ /**
+ * The current Locale
+ */
+ 'locale' => 'en_US',
+
+ /**
+ * The current Charset
+ */
+ 'charset' => 'UTF-8',
+
+ /**
+ * Maintenance mode
+ */
+ 'maintenance_mode' => false,
+
+ /**
+ * ------------------------------------------------------------------------
+ * Debug settings
+ * ------------------------------------------------------------------------
+ */
+ 'debug' => array(
+ /**
+ * Display errors
+ *
+ * @var bool
+ */
+ 'display_errors' => true,
+
+ /**
+ * Site debug level
+ *
+ * @var string (message|debug)
+ */
+ 'detail' => 'message'
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Proxy Settings
+ * ------------------------------------------------------------------------
+ */
+ 'proxy' => array(
+ 'host' => null,
+ 'port' => null,
+ 'user' => null,
+ 'password' => null
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * File upload settings
+ * ------------------------------------------------------------------------
+ */
+ 'upload' => array(
+
+ /**
+ * Allowed file extensions
+ *
+ * @var string semi-colon separated.
+ */
+ 'extensions' => '*.flv;*.jpg;*.gif;*.jpeg;*.ico;*.docx;*.xla;*.png;*.psd;*.swf;*.doc;*.txt;*.xls;*.xlsx;' .
+ '*.csv;*.pdf;*.tiff;*.rtf;*.m4a;*.mov;*.wmv;*.mpeg;*.mpg;*.wav;*.3gp;*.avi;*.m4v;*.mp4;*.mp3;*.qt;*.ppt;' .
+ '*.pptx;*.kml;*.xml;*.svg;*.webm;*.ogg;*.ogv'
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Mail settings
+ * ------------------------------------------------------------------------
+ */
+ 'mail' => array(
+ 'method' => 'PHP_MAIL',
+ 'methods' => array(
+ 'smtp' => array(
+ 'server' => '',
+ 'port' => '',
+ 'username' => '',
+ 'password' => '',
+ 'encryption' => ''
+ )
+ )
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Cache settings
+ * ------------------------------------------------------------------------
+ */
+ 'cache' => array(
+
+ /**
+ * Enabled
+ *
+ * @var bool
+ */
+ 'enabled' => true,
+
+ /**
+ * Lifetime
+ *
+ * @var int Seconds
+ */
+ 'lifetime' => 21600,
+
+ /**
+ * Cache overrides
+ *
+ * @var bool
+ */
+ 'overrides' => true,
+
+ /**
+ * Cache Blocks
+ *
+ * @var bool
+ */
+ 'blocks' => true,
+
+ /**
+ * Cache Assets
+ *
+ * @var bool
+ */
+ 'assets' => false,
+
+ /**
+ * Cache Theme CSS/JS
+ *
+ * @var bool
+ */
+ 'theme_css' => true,
+
+ /**
+ * Cache full page
+ *
+ * @var bool|string (block|all)
+ */
+ 'pages' => false,
+
+ /**
+ * Use Doctrine development mode
+ *
+ * @var bool
+ */
+ 'doctrine_dev_mode' => false,
+
+ /**
+ * How long to cache full page
+ *
+ * @var string
+ */
+ 'full_page_lifetime' => 'default',
+
+ /**
+ * Custom lifetime value, only used if concrete.cache.full_page_lifetime is 'custom'
+ *
+ * @var int
+ */
+ 'full_page_lifetime_value' => null,
+
+ /**
+ * Calculate the cache key reading the assets contents (true) of the assets modification time (false).
+ *
+ * @var bool
+ */
+ 'full_contents_assets_hash' => false,
+
+ 'directory' => DIR_FILES_UPLOADED_STANDARD . '/cache',
+ /**
+ * Relative path to the cache directory. If empty it'll be calculated from concrete.cache.directory
+ * @var string|null
+ */
+ 'directory_relative' => null,
+ 'page' => array(
+ 'directory' => DIR_FILES_UPLOADED_STANDARD . '/cache/pages',
+ 'adapter' => 'file',
+ ),
+ 'environment' => array(
+ 'file' => 'environment.cache'
+ ),
+
+ 'levels' => array(
+ 'expensive' => array(
+ 'drivers' => array(
+ 'core_ephemeral' => array(
+ 'class' => '\Stash\Driver\Ephemeral',
+ 'options' => array()
+ ),
+
+ 'core_filesystem' => array(
+ 'class' => '\Stash\Driver\FileSystem',
+ 'options' => array(
+ 'path' => DIR_FILES_UPLOADED_STANDARD . '/cache',
+ 'dirPermissions' => DIRECTORY_PERMISSIONS_MODE_COMPUTED,
+ 'filePermissions' => FILE_PERMISSIONS_MODE_COMPUTED
+ )
+ ),
+ )
+ ),
+ 'object' => array(
+ 'drivers' => array(
+ 'core_ephemeral' => array(
+ 'class' => '\Stash\Driver\Ephemeral',
+ 'options' => array()
+ )
+ )
+ )
+ )
+
+ ),
+
+ 'multilingual' => array(
+ 'redirect_home_to_default_locale' => false,
+ 'use_browser_detected_locale' => false,
+ 'default_locale' => false,
+ 'default_source_locale' => 'en_US'
+ ),
+
+ 'design' => array(
+ 'enable_custom' => true,
+ 'enable_layouts' => true
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Logging settings
+ * ------------------------------------------------------------------------
+ */
+ 'log' => array(
+
+ /**
+ * Log emails
+ *
+ * @var bool
+ */
+ 'emails' => true,
+
+ /**
+ * Log Errors
+ *
+ * @var bool
+ */
+ 'errors' => true,
+
+ /**
+ * Log Spam
+ *
+ * @var bool
+ */
+ 'spam' => false,
+
+
+ 'queries' => array(
+
+ /**
+ * Whether to log database queries or not.
+ *
+ * @var bool
+ */
+ 'log' => false,
+
+
+ 'clear_on_reload' => false
+
+
+
+ )
+ ),
+ 'jobs' => array(
+
+ 'enable_scheduling' => true
+
+ ),
+
+ 'filesystem' => array(
+ /** Temporary directory.
+ * @link \Concrete\Core\File\Service\File::getTemporaryDirectory
+ */
+ 'temp_directory' => null,
+ 'permissions' => array(
+ 'file' => FILE_PERMISSIONS_MODE_COMPUTED,
+ 'directory' => DIRECTORY_PERMISSIONS_MODE_COMPUTED
+ )
+ ),
+
+ 'editor' => array(
+ 'concrete' => array(
+ 'enable_filemanager' => true,
+ 'enable_sitemap' => true
+ ),
+ 'plugins' => array(
+ 'selected' => array(
+ 'concrete5lightbox',
+ 'undoredo',
+ 'specialcharacters',
+ 'table'
+ )
+ )
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Email settings
+ * ------------------------------------------------------------------------
+ */
+ 'email' => array(
+
+ /**
+ * Enable emails
+ *
+ * @var bool
+ */
+ 'enabled' => true,
+ 'default' => array(
+ 'address' => 'concrete5-noreply@' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost'),
+ 'name' => ''
+ ),
+ 'form_block' => array(
+ 'address' => false
+ ),
+ 'forgot_password' => array(
+ 'address' => null,
+ 'name' => null,
+ ),
+ 'validate_registration' => array(
+ 'address' => null,
+ 'name' => null,
+ ),
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Marketplace settings
+ * ------------------------------------------------------------------------
+ */
+ 'marketplace' => array(
+ /**
+ * Enable marketplace integration
+ *
+ * @var bool concrete.marketplace.enabled
+ */
+ 'enabled' => true,
+
+ /**
+ * Time it takes for a request to timeout
+ *
+ * @var int concrete.marketplace.request_timeout
+ */
+ 'request_timeout' => 30,
+
+ /**
+ * Marketplace Token
+ *
+ * @var null|string concrete.marketplace.token
+ */
+ 'token' => null,
+
+ /**
+ * Marketplace Site url Token
+ *
+ * @var null|string concrete.marketplace.site_token
+ */
+ 'site_token' => null,
+
+ /**
+ * Enable intelligent search integration
+ *
+ * @var bool concrete.marketplace.intelligent_search
+ */
+ 'intelligent_search' => true,
+
+ /**
+ * Log requests
+ *
+ * @var bool concrete.marketplace.log_requests
+ */
+ 'log_requests' => false
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Getting external news and help from concrete5.org
+ * ------------------------------------------------------------------------
+ */
+ 'external' => array(
+
+ /**
+ * Provide help within the intelligent search
+ *
+ * @var bool concrete.external.intelligent_search_help
+ */
+ 'intelligent_search_help' => true,
+
+ /**
+ * Display an overlay with up-to-date news from concrete5
+ *
+ * @var bool concrete.external.news_overlay
+ */
+ 'news_overlay' => true,
+
+ /**
+ * Enable concrete5 news within your site
+ *
+ * @var bool concrete.external.news
+ */
+ 'news' => true,
+ ),
+
+ /**
+ * --------------------------------------------------------------------
+ * Miscellaneous settings
+ * --------------------------------------------------------------------
+ */
+ 'misc' => array(
+ 'user_timezones' => false,
+ 'package_backup_directory' => DIR_FILES_UPLOADED_STANDARD . '/trash',
+ 'enable_progressive_page_reindex' => true,
+ 'mobile_theme_id' => 0,
+ 'sitemap_approve_immediately' => true,
+ 'enable_translate_locale_en_us' => false,
+ 'page_search_index_lifetime' => 259200,
+ 'enable_trash_can' => true,
+ 'app_version_display_in_header' => true,
+ 'default_jpeg_image_compression' => 80,
+ 'help_overlay' => true,
+ 'require_version_comments' => false,
+ ),
+
+ 'theme' => array(
+
+ 'compress_preprocessor_output' => true,
+ 'generate_less_sourcemap' => false,
+ ),
+
+ 'updates' => array(
+
+ 'enable_auto_update_core' => false,
+ 'enable_auto_update_packages' => false,
+ 'enable_permissions_protection' => true,
+ 'check_threshold' => 172800,
+ 'services' => array(
+ 'get_available_updates' => 'http://www.concrete5.org/tools/update_core',
+ 'inspect_update' => 'http://www.concrete5.org/tools/inspect_update'
+ )
+ ),
+ 'paths' => array(
+ 'trash' => '/!trash',
+ 'drafts' => '/!drafts'
+ ),
+ 'icons' => array(
+ 'page_template' => array(
+ 'width' => 120,
+ 'height' => 90
+ ),
+ 'theme_thumbnail' => array(
+ 'width' => 120,
+ 'height' => 90
+ ),
+ 'file_manager_listing' => array(
+ 'handle' => 'file_manager_listing',
+ 'width' => 60,
+ 'height' => 60
+ ),
+ 'file_manager_detail' => array(
+ 'handle' => 'file_manager_detail',
+ 'width' => 400
+ ),
+ 'user_avatar' => array(
+ 'width' => 80,
+ 'height' => 80,
+ 'default' => ASSETS_URL_IMAGES . '/avatar_none.png'
+ )
+ ),
+
+ 'file_manager' => array(
+ 'images' => array(
+ 'use_exim_data_to_rotate_images' => false,
+ 'manipulation_library' => 'gd'
+ ),
+ 'results' => 10
+ ),
+
+ 'sitemap_xml' => array(
+ 'file' => 'sitemap.xml',
+ 'frequency' => 'weekly',
+ 'priority' => 0.5
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Accessibility
+ * ------------------------------------------------------------------------
+ */
+ 'accessibility' => array(
+ /**
+ * Show titles in the concrete5 toolbars
+ *
+ * @var bool
+ */
+ 'toolbar_titles' => false,
+
+ /**
+ * Increase the font size in the concrete5 toolbars
+ *
+ * @var bool
+ */
+ 'toolbar_large_font' => false,
+
+ /**
+ * Show help system
+ *
+ * @var bool
+ */
+ 'display_help_system' => true
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Internationalization
+ * ------------------------------------------------------------------------
+ */
+ 'i18n' => array(
+
+ /**
+ * Allow users to choose language on login
+ *
+ * @var bool
+ */
+ 'choose_language_login' => false
+
+ ),
+ 'urls' => array(
+ 'concrete5' => 'http://www.concrete5.org',
+ 'concrete5_secure' => 'https://www.concrete5.org',
+ 'newsflow' => 'http://newsflow.concrete5.org',
+ 'background_feed' => '//backgroundimages.concrete5.org/wallpaper',
+ 'background_feed_secure' => 'https://backgroundimages.concrete5.org/wallpaper',
+ 'background_info' => 'http://backgroundimages.concrete5.org/get_image_data.php',
+ 'help' => array(
+ 'developer' => 'http://www.concrete5.org/documentation/developers/5.7/',
+ 'user' => 'http://www.concrete5.org/documentation/using-concrete5-7',
+ 'forum' => 'http://www.concrete5.org/community/forums'
+ ),
+ 'paths' => array(
+ 'menu_help_service' => '/tools/get_remote_help_list/',
+ 'site_page' => '/private/sites',
+ 'newsflow_slot_content' => '/tools/slot_content/',
+ 'marketplace' => array(
+ 'connect' => '/marketplace/connect',
+ 'connect_success' => '/marketplace/connect/-/connected',
+ 'connect_validate' => '/marketplace/connect/-/validate',
+ 'connect_new_token' => '/marketplace/connect/-/generate_token',
+ 'checkout' => '/cart/-/add/',
+ 'purchases' => '/marketplace/connect/-/get_available_licenses',
+ 'item_information' => '/marketplace/connect/-/get_item_information',
+ 'item_free_license' => '/marketplace/connect/-/enable_free_license',
+ 'remote_item_list' => '/marketplace/'
+ )
+ )
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * White labeling.
+ * ------------------------------------------------------------------------
+ */
+ 'white_label' => array(
+
+ /**
+ * Custom Logo source path relative to the public directory.
+ *
+ * @var bool|string The logo path
+ */
+ 'logo' => false,
+
+ /**
+ * Custom Name
+ *
+ * @var bool|string The name
+ */
+ 'name' => false,
+
+ /**
+ * Dashboard background image url
+ *
+ * @var null|string
+ */
+ 'dashboard_background' => null
+ ),
+ 'session' => array(
+
+ 'name' => 'CONCRETE5',
+ 'handler' => 'file',
+ 'save_path' => null,
+ 'max_lifetime' => 7200,
+ 'cookie' => array(
+ 'cookie_path' => false, // set a specific path here if you know it, otherwise it'll default to relative
+ 'cookie_lifetime' => 0,
+ 'cookie_domain' => false,
+ 'cookie_secure' => false,
+ 'cookie_httponly' => true
+ )
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * User information and registration settings.
+ * ------------------------------------------------------------------------
+ */
+ 'user' => array(
+ /**
+ * --------------------------------------------------------------------
+ * Registration settings.
+ * --------------------------------------------------------------------
+ */
+ 'registration' => array(
+
+ /**
+ * Registration
+ *
+ * @var bool
+ */
+ 'enabled' => false,
+
+ /**
+ * Registration type
+ *
+ * @var string The type (disabled|enabled|validate_email|manual_approve)
+ */
+ 'type' => 'disabled',
+
+ /**
+ * Enable Registration Captcha
+ *
+ * @var bool
+ */
+ 'captcha' => true,
+
+ /**
+ * Use emails instead of usernames to log in
+ *
+ * @var bool
+ */
+ 'email_registration' => false,
+
+ /**
+ * Validate emails during registration
+ *
+ * @var bool
+ */
+ 'validate_email' => false,
+
+ /**
+ * Admins approve each registration
+ *
+ * @var bool
+ */
+ 'approval' => false,
+
+ /**
+ * Send notifications after successful registration.
+ *
+ * @var bool|string Email to notify
+ */
+ 'notification' => false
+ ),
+
+ /**
+ * --------------------------------------------------------------------
+ * Gravatar Settings
+ * --------------------------------------------------------------------
+ */
+ 'gravatar' => array(
+ 'enabled' => false,
+ 'max_level' => 0,
+ 'image_set' => 0
+ ),
+ 'group' => array(
+
+ 'badge' => array(
+
+ 'default_point_value' => 50
+ )
+
+ ),
+
+ /**
+ * Enable public user profiles
+ *
+ * @var bool
+ */
+ 'profiles_enabled' => false,
+
+ 'username' => array(
+ 'maximum' => 64,
+ 'minimum' => 3,
+ 'allow_spaces' => false
+
+ ),
+ 'password' => array(
+ 'maximum' => 128,
+ 'minimum' => 5,
+ 'hash_portable' => false,
+ 'hash_cost_log2' => 12,
+ 'legacy_salt' => '',
+ ),
+ 'private_messages' => array(
+ 'throttle_max' => 20,
+ 'throttle_max_timespan' => 15 // minutes
+ )
+
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Spam
+ * ------------------------------------------------------------------------
+ */
+ 'spam' => array(
+ /**
+ * Whitelist group ID
+ *
+ * @var int
+ */
+ 'whitelist_group' => 0,
+
+ /**
+ * Notification email
+ *
+ * @var string
+ */
+ 'notify_email' => ''
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Security
+ * ------------------------------------------------------------------------
+ */
+ 'security' => array(
+ 'session' => array(
+
+ 'invalidate_on_user_agent_mismatch' => true,
+
+ 'invalidate_on_ip_mismatch' => true
+
+ ),
+ 'ban' => array(
+ 'ip' => array(
+
+ 'enabled' => true,
+
+ /**
+ * Maximum attempts
+ */
+ 'attempts' => 5,
+
+ /**
+ * Threshold time
+ */
+ 'time' => 300,
+
+ /**
+ * Ban length in minutes
+ */
+ 'length' => 10
+ )
+ ),
+ 'misc' => array(
+
+ /**
+ * Defence Click Jacking.
+ *
+ * @var bool|string DENY, SAMEORIGIN, ALLOW-FROM uri
+ */
+ 'x_frame_options' => 'SAMEORIGIN'
+ )
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Permissions and behaviors toggles.
+ * ------------------------------------------------------------------------
+ */
+ 'permissions' => array(
+ /**
+ * Forward to login if access is denied
+ *
+ * @var bool
+ */
+ 'forward_to_login' => true,
+
+ /**
+ * Permission model
+ *
+ * @var string The permission model (simple|advanced)
+ */
+ 'model' => 'simple',
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * SEO Settings
+ * ------------------------------------------------------------------------
+ */
+ 'seo' => array(
+
+ 'tracking' => array(
+ /**
+ * User defined tracking code
+ *
+ * @var string
+ */
+ 'code' => '',
+
+ /**
+ * Tracking code position
+ *
+ * @var string (top|bottom)
+ */
+ 'code_position' => 'bottom'
+
+ ),
+ 'exclude_words' => 'a, an, as, at, before, but, by, for, from, is, in, into, like, of, off, on, onto, per, ' .
+ 'since, than, the, this, that, to, up, via, with',
+
+ /**
+ * URL rewriting
+ *
+ * Doesn't impact concrete.seo.url_rewriting_all which is set at a lower level and
+ * controls whether ALL items will be rewritten.
+ *
+ * @var bool
+ */
+ 'url_rewriting' => false,
+ 'url_rewriting_all' => false,
+ 'redirect_to_canonical_url' => false,
+ 'canonical_url' => null,
+ 'canonical_ssl_url' => null,
+ 'trailing_slash' => false,
+ 'title_format' => '%1$s :: %2$s',
+ 'title_segment_separator' => ' :: ',
+ 'page_path_separator' => '-',
+ 'group_name_separator' => ' / ',
+ 'segment_max_length' => 128,
+ 'paging_string' => 'ccm_paging_p'
+ ),
+
+ /**
+ * ------------------------------------------------------------------------
+ * Statistics Settings
+ * ------------------------------------------------------------------------
+ */
+ 'statistics' => array(
+ 'track_downloads' => true
+ ),
+ 'limits' => array(
+ 'sitemap_pages' => 100,
+ 'delete_pages' => 100,
+ 'copy_pages' => 10,
+ 'page_search_index_batch' => 200,
+ 'job_queue_batch' => 10,
+ 'style_customizer' => array(
+ 'size_min' => -50,
+ 'size_max' => 200,
+ )
+ ),
+
+ 'page' => array(
+ 'search' => array(
+ // Always reindex pages (usually it isn't performed when approving workflows)
+ 'always_reindex' => false,
+ )
+ ),
+);
diff --git a/tests/fixtures/adapter/v8/concrete/bootstrap/autoload.php b/tests/fixtures/adapter/v8/concrete/bootstrap/autoload.php
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/adapter/v8/concrete/bootstrap/configure.php b/tests/fixtures/adapter/v8/concrete/bootstrap/configure.php
new file mode 100644
index 0000000..e69de29
diff --git a/tests/fixtures/adapter/v8/concrete/bootstrap/start.php b/tests/fixtures/adapter/v8/concrete/bootstrap/start.php
new file mode 100644
index 0000000..2e7723c
--- /dev/null
+++ b/tests/fixtures/adapter/v8/concrete/bootstrap/start.php
@@ -0,0 +1,34 @@
+app = $app;
+ }
+
+ public function boot()
+ {
+ $this->app->booted = true;
+ }
+ }
+}
+
+namespace Concrete\Core\Application {
+
+ class Application
+ {
+ public $booted = false;
+
+ public function getRuntime()
+ {
+ return new \Concrete\Core\Foundation\Runtime\Runtime($this);
+ }
+ }
+
+ return new Application();
+}
diff --git a/tests/fixtures/adapter/v8/concrete/config/concrete.php b/tests/fixtures/adapter/v8/concrete/config/concrete.php
new file mode 100644
index 0000000..5f1c8a1
--- /dev/null
+++ b/tests/fixtures/adapter/v8/concrete/config/concrete.php
@@ -0,0 +1,1190 @@
+ '8.6.0a2',
+ 'version_installed' => '8.6.0a2',
+ 'version_db' => '20200501000000', // the key of the latest database migration
+
+ /*
+ * Installation status
+ *
+ * @var bool
+ */
+ 'installed' => true,
+
+ /*
+ * The current Locale
+ */
+ 'locale' => 'en_US',
+
+ /*
+ * The current Charset
+ */
+ 'charset' => 'UTF-8',
+
+ /*
+ * The byte-order-mark for the current charset
+ */
+ 'charset_bom' => "\xEF\xBB\xBF",
+
+ /*
+ * Maintenance mode
+ */
+ 'maintenance_mode' => false,
+
+ /*
+ * ------------------------------------------------------------------------
+ * Debug settings
+ * ------------------------------------------------------------------------
+ */
+ 'debug' => [
+ /*
+ * Display errors
+ *
+ * @var bool
+ */
+ 'display_errors' => true,
+
+ /*
+ * Site debug level
+ *
+ * @var string (message|debug)
+ */
+ 'detail' => 'debug',
+
+ /*
+ * Error reporting level
+ *
+ * @var int|null
+ */
+ 'error_reporting' => null,
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Proxy Settings
+ * ------------------------------------------------------------------------
+ */
+ 'proxy' => [
+ 'host' => null,
+ 'port' => null,
+ 'user' => null,
+ 'password' => null,
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * File upload settings
+ * ------------------------------------------------------------------------
+ */
+ 'upload' => [
+ /*
+ * Allowed file extensions
+ *
+ * @var string semi-colon separated.
+ */
+ 'extensions' => '*.flv;*.jpg;*.gif;*.jpeg;*.ico;*.docx;*.xla;*.png;*.psd;*.swf;*.doc;*.txt;*.xls;*.xlsx;' .
+ '*.csv;*.pdf;*.tiff;*.rtf;*.m4a;*.mov;*.wmv;*.mpeg;*.mpg;*.wav;*.3gp;*.avi;*.m4v;*.mp4;*.mp3;*.qt;*.ppt;' .
+ '*.pptx;*.kml;*.xml;*.svg;*.webm;*.ogg;*.ogv',
+ /*
+ * Disallowed file extension list (takes the precedence over the extensions whitelist).
+ *
+ * @var string semi-colon separated.
+ */
+ 'extensions_blacklist' => '*.php;*.php2;*.php3;*.php4;*.php5;*.php7;*.php8;*.phtml;*.phar;*.htaccess;*.pl;*.phpsh;*.pht;*.shtml;*.cgi',
+
+ 'chunking' => [
+ // Enable uploading files in chunks?
+ 'enabled' => true,
+ // The chunk size (if empty we'll automatically determine it)
+ 'chunkSize' => null,
+ ],
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Export settings
+ * ------------------------------------------------------------------------
+ */
+ 'export' => [
+ 'csv' => [
+ // Include the BOM (byte-order mark) in generated CSV files?
+ // @var bool
+ 'include_bom' => false,
+ 'datetime_format' => 'ATOM',
+ ],
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Interface settings
+ * ------------------------------------------------------------------------
+ */
+ 'interface' => [
+ 'panel' => [
+ /*
+ * Enable the page relations panel
+ */
+ 'page_relations' => false,
+ ],
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Mail settings
+ * ------------------------------------------------------------------------
+ */
+ 'mail' => [
+ 'method' => 'PHP_MAIL',
+ 'methods' => [
+ 'smtp' => [
+ 'server' => '',
+ 'port' => '',
+ 'username' => '',
+ 'password' => '',
+ 'encryption' => '',
+ 'messages_per_connection' => null,
+ ],
+ ],
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Cache settings
+ * ------------------------------------------------------------------------
+ */
+ 'cache' => [
+ /*
+ * Enabled
+ *
+ * @var bool
+ */
+ 'enabled' => true,
+
+ /*
+ * Lifetime
+ *
+ * @var int Seconds
+ */
+ 'lifetime' => 21600,
+
+ /*
+ * Cache overrides
+ *
+ * @var bool
+ */
+ 'overrides' => true,
+
+ /*
+ * Cache Blocks
+ *
+ * @var bool
+ */
+ 'blocks' => true,
+
+ /*
+ * Cache Assets
+ *
+ * @var bool
+ */
+ 'assets' => false,
+
+ /*
+ * Cache Theme CSS/JS
+ *
+ * @var bool
+ */
+ 'theme_css' => true,
+
+ /*
+ * Cache full page
+ *
+ * @var bool|string (blocks|all)
+ */
+ 'pages' => false,
+
+ /*
+ * Use Doctrine development mode
+ *
+ * @var bool
+ */
+ 'doctrine_dev_mode' => false,
+
+ /*
+ * How long to cache full page
+ *
+ * @var string
+ */
+ 'full_page_lifetime' => 'default',
+
+ /*
+ * Custom lifetime value, only used if concrete.cache.full_page_lifetime is 'custom'
+ *
+ * @var int
+ */
+ 'full_page_lifetime_value' => null,
+
+ /*
+ * Calculate the cache key reading the assets contents (true) of the assets modification time (false).
+ *
+ * @var bool
+ */
+ 'full_contents_assets_hash' => false,
+
+ 'directory' => DIR_FILES_UPLOADED_STANDARD . '/cache',
+ /*
+ * Relative path to the cache directory. If empty it'll be calculated from concrete.cache.directory
+ * @var string|null
+ */
+ 'directory_relative' => null,
+ 'page' => [
+ 'directory' => DIR_FILES_UPLOADED_STANDARD . '/cache/pages',
+ 'adapter' => 'file',
+ ],
+
+ 'levels' => [
+ 'overrides' => [
+ 'drivers' => [
+ 'core_ephemeral' => [
+ 'class' => '\Stash\Driver\Ephemeral',
+ 'options' => [],
+ ],
+
+ 'core_filesystem' => [
+ 'class' => \Concrete\Core\Cache\Driver\FileSystemStashDriver::class,
+ 'options' => [
+ 'path' => DIR_FILES_UPLOADED_STANDARD . '/cache/overrides',
+ 'dirPermissions' => DIRECTORY_PERMISSIONS_MODE_COMPUTED,
+ 'filePermissions' => FILE_PERMISSIONS_MODE_COMPUTED,
+ ],
+ ],
+ 'redis' => [
+ 'class' => \Concrete\Core\Cache\Driver\RedisStashDriver::class,
+ 'options' => [
+ /* Example configuration for servers
+ 'servers' => [
+ [
+ 'server' => 'localhost',
+ 'port' => 6379,
+ 'ttl' => 10 //Connection Timeout - not TTL for objects
+ ],
+ [
+ 'server' => 'outside.server',
+ 'port' => 6379,
+ 'ttl' => 10
+ ],
+ ],*/
+ 'prefix' => 'c5_overrides',
+ 'database' => 0, // Use different Redis Databases - optional
+ ],
+ ],
+ ],
+ 'preferred_driver' => 'core_filesystem', // Use this to specify a preferred driver
+ ],
+ 'expensive' => [
+ 'drivers' => [
+ 'core_ephemeral' => [
+ 'class' => '\Stash\Driver\Ephemeral',
+ 'options' => [],
+ ],
+ 'core_filesystem' => [
+ 'class' => \Concrete\Core\Cache\Driver\FileSystemStashDriver::class,
+ 'options' => [
+ 'path' => DIR_FILES_UPLOADED_STANDARD . '/cache/expensive',
+ 'dirPermissions' => DIRECTORY_PERMISSIONS_MODE_COMPUTED,
+ 'filePermissions' => FILE_PERMISSIONS_MODE_COMPUTED,
+ ],
+ ],
+ 'redis' => [
+ 'class' => \Concrete\Core\Cache\Driver\RedisStashDriver::class,
+ 'options' => [
+ 'prefix' => 'c5_expensive',
+ 'database' => 0, // Use different Redis Databases - optional
+ ],
+ ],
+ ],
+ 'preferred_driver' => 'core_filesystem', // Use this to specify a preferred driver
+ ],
+ 'object' => [
+ 'drivers' => [
+ 'core_ephemeral' => [
+ 'class' => '\Stash\Driver\Ephemeral',
+ 'options' => [],
+ ],
+ 'redis' => [
+ 'class' => \Concrete\Core\Cache\Driver\RedisStashDriver::class,
+ 'options' => [
+ 'prefix' => 'c5_object',
+ 'database' => 0, // Use different Redis Databases - optional
+ ],
+ ],
+ ],
+ 'preferred_driver' => 'core_ephemeral', // Use this to specify a preferred driver
+ ],
+ ],
+
+ 'clear' => [
+ 'thumbnails' => false,
+ ],
+ ],
+
+ 'design' => [
+ 'enable_custom' => true,
+ 'enable_layouts' => true,
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Logging settings
+ * ------------------------------------------------------------------------
+ */
+ 'log' => [
+ /*
+ * Whether to log emails
+ *
+ * @var bool
+ */
+ 'emails' => true,
+
+ /*
+ * Whether to log Errors
+ *
+ * @var bool
+ */
+ 'errors' => true,
+
+ /*
+ * Whether to log Spam
+ *
+ * @var bool
+ */
+ 'spam' => false,
+
+ /*
+ * Whether to log REST API requests headers
+ *
+ * @var bool
+ */
+ 'api' => false,
+
+ 'enable_dashboard_report' => true,
+
+ 'configuration' => [
+ /*
+ * Configuration mode
+ *
+ * @var string simple|advanced
+ */
+ 'mode' => 'simple',
+ 'simple' => [
+ /*
+ * What log level to store core logs in the database
+ * @var string
+ */
+ 'core_logging_level' => 'NOTICE',
+
+ /*
+ * Which handle to use
+ *
+ * @var string (database|file)
+ */
+ 'handler' => 'database',
+
+ 'file' => [
+ /*
+ * File path to store logs
+ *
+ * @var string
+ */
+ 'file' => '',
+ ],
+ ],
+
+ 'advanced' => [
+ 'configuration' => [],
+ ],
+ ],
+ ],
+ 'jobs' => [
+ 'enable_scheduling' => true,
+ ],
+
+ 'filesystem' => [
+ /* Temporary directory.
+ * @link \Concrete\Core\File\Service\File::getTemporaryDirectory
+ */
+ 'temp_directory' => null,
+ 'permissions' => [
+ 'file' => FILE_PERMISSIONS_MODE_COMPUTED,
+ 'directory' => DIRECTORY_PERMISSIONS_MODE_COMPUTED,
+ ],
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Email settings
+ * ------------------------------------------------------------------------
+ */
+ 'email' => [
+ /*
+ * Enable emails
+ *
+ * @var bool
+ */
+ 'enabled' => true,
+ 'default' => [
+ 'address' => 'concrete5-noreply@concrete5',
+ 'name' => '',
+ ],
+ 'form_block' => [
+ 'address' => false,
+ ],
+ 'forgot_password' => [
+ 'address' => null,
+ 'name' => null,
+ ],
+ 'validate_registration' => [
+ 'address' => null,
+ 'name' => null,
+ ],
+ 'workflow_notification' => [
+ 'address' => null,
+ 'name' => null,
+ ],
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Form settings
+ * ------------------------------------------------------------------------
+ */
+ 'form' => [
+ /*
+ * Whether to store form submissions. Auto means form submissions will be stored, but the block
+ * will offer an option to disable on a per-block basis. True means they will always be stored,
+ * and false means they will never be stored.
+ *
+ * @var string "auto", true or false
+ */
+ 'store_form_submissions' => 'auto',
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Marketplace settings
+ * ------------------------------------------------------------------------
+ */
+ 'marketplace' => [
+ /*
+ * Enable marketplace integration
+ *
+ * @var bool concrete.marketplace.enabled
+ */
+ 'enabled' => true,
+
+ /*
+ * Time it takes for a request to timeout
+ *
+ * @var int concrete.marketplace.request_timeout
+ */
+ 'request_timeout' => 30,
+
+ /*
+ * Marketplace Token
+ *
+ * @var null|string concrete.marketplace.token
+ */
+ 'token' => null,
+
+ /*
+ * Marketplace Site url Token
+ *
+ * @var null|string concrete.marketplace.site_token
+ */
+ 'site_token' => null,
+
+ /*
+ * Enable intelligent search integration
+ *
+ * @var bool concrete.marketplace.intelligent_search
+ */
+ 'intelligent_search' => true,
+
+ /*
+ * Log requests
+ *
+ * @var bool concrete.marketplace.log_requests
+ */
+ 'log_requests' => false,
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Getting external news and help from concrete5.org
+ * ------------------------------------------------------------------------
+ */
+ 'external' => [
+ /*
+ * Provide help within the intelligent search
+ *
+ * @var bool concrete.external.intelligent_search_help
+ */
+ 'intelligent_search_help' => true,
+
+ /*
+ * Enable concrete5 news within your site
+ *
+ * @var bool concrete.external.news
+ */
+ 'news' => true,
+ ],
+
+ /*
+ * --------------------------------------------------------------------
+ * Miscellaneous settings
+ * --------------------------------------------------------------------
+ */
+ 'misc' => [
+ 'user_timezones' => false,
+ 'package_backup_directory' => DIR_FILES_UPLOADED_STANDARD . '/trash',
+ 'enable_progressive_page_reindex' => true,
+ 'mobile_theme_id' => 0,
+ 'sitemap_approve_immediately' => true,
+ 'enable_translate_locale_en_us' => false,
+ 'page_search_index_lifetime' => 259200,
+ 'enable_trash_can' => true,
+ 'app_version_display_in_header' => true,
+ /*
+ * The JPEG compression level (in range 0... 100)
+ */
+ 'default_jpeg_image_compression' => 80,
+ /*
+ * The PNG compression level (in range 0... 9)
+ */
+ 'default_png_image_compression' => 9,
+ /*
+ * The default thumbnail format: jpeg, png, auto (if auto: we'll create a jpeg if the source image is jpeg, we'll create a png otherwise).
+ */
+ 'default_thumbnail_format' => 'auto',
+ /*
+ * The threshold (total number of pixels - width x height x number of frames)
+ * after which we'll reload images instead of creating in-memory clones.
+ * If empty: unlimited
+ */
+ 'inplace_image_operations_limit' => 4194304,
+ /*
+ * @var string (now|async)
+ */
+ 'basic_thumbnailer_generation_strategy' => 'now',
+ 'help_overlay' => true,
+ 'require_version_comments' => false,
+ /*
+ * Control whether a block type can me moved to different block type sets
+ *
+ * @var bool
+ */
+ 'enable_move_blocktypes_across_sets' => false,
+ /*
+ * Control whether or not the image editor should add crossOrigin when loading images from external sources (s3, etc)
+ */
+ 'image_editor_cors_policy' => [
+ 'enable_cross_origin' => false,
+ 'anonymous_request' => true,
+ ],
+ /*
+ * Check whether to add a "generator" tag with the concrete5 version to the site pages
+ *
+ * @var bool
+ */
+ 'generator_tag_display_in_header' => true,
+ ],
+
+ 'theme' => [
+ 'compress_preprocessor_output' => true,
+ 'generate_less_sourcemap' => false,
+ ],
+
+ 'updates' => [
+ 'enable_auto_update_packages' => false,
+ 'enable_permissions_protection' => true,
+ 'check_threshold' => 172800,
+ 'services' => [
+ 'get_available_updates' => 'http://www.concrete5.org/tools/update_core',
+ 'inspect_update' => 'http://www.concrete5.org/tools/inspect_update',
+ ],
+ // Set to true to skip checking if there's a newer core version available (useful for example if the core is upgraded via composer)
+ 'skip_core' => false,
+ // List of package handles that shouldn't be checked for new versions in marketplace (useful for example if the core is upgraded via composer)
+ // Set to true to skip all the packages
+ 'skip_packages' => [],
+ ],
+ 'paths' => [
+ 'trash' => '/!trash',
+ 'drafts' => '/!drafts',
+ ],
+ 'icons' => [
+ 'page_template' => [
+ 'width' => 120,
+ 'height' => 90,
+ ],
+ 'theme_thumbnail' => [
+ 'width' => 120,
+ 'height' => 90,
+ ],
+ 'file_manager_listing' => [
+ 'handle' => 'file_manager_listing',
+ 'width' => 60,
+ 'height' => 60,
+ ],
+ 'file_manager_detail' => [
+ 'handle' => 'file_manager_detail',
+ 'width' => 400,
+ 'height' => 400,
+ ],
+ 'user_avatar' => [
+ 'width' => 80,
+ 'height' => 80,
+ 'default' => ASSETS_URL_IMAGES . '/avatar_none.png',
+ ],
+ ],
+
+ 'file_manager' => [
+ 'images' => [
+ 'use_exif_data_to_rotate_images' => false,
+ 'manipulation_library' => 'gd',
+ 'create_high_dpi_thumbnails' => true,
+ /*
+ * The style of preview image used in the file_manager
+ *
+ * @var string 'small'(default,30x30), 'large(60x60)' or 'full(size of file_manager_listing)'
+ */
+ 'preview_image_size' => 'small',
+ /*
+ * Show file_manager_detail thumbnail as preview image in popover
+ *
+ * @var boolean
+ */
+ 'preview_image_popover' => true,
+ // SVG sanitization
+ 'svg_sanitization' => [
+ // The operation that the SVG sanitizer should perform.
+ // This must be value of one of the Concrete\Core\File\Import\Processor\SvgProcessor::ACTION_... constants
+ 'action' => 'sanitize',
+ // Space-separated list of tags to be kept
+ 'allowed_tags' => '',
+ // Space-separated list of attributes to be kept
+ 'allowed_attributes' => '',
+ ],
+ /*
+ * Background color of the Image Editor saveArea
+ * Leave empty to use a transparent background
+ *
+ * @var string
+ */
+ 'image_editor_save_area_background_color' => '',
+ ],
+ /*
+ * Options for the results per page dropdown
+ *
+ * @var array
+ */
+ 'items_per_page_options' => [10, 25, 50, 100, 250],
+ /*
+ * Default number of results per page
+ *
+ * @var int
+ */
+ 'results' => 10,
+ ],
+
+ 'search_users' => [
+ 'results' => 10,
+ ],
+
+ 'sitemap_xml' => [
+ 'file' => 'sitemap.xml',
+ 'frequency' => 'weekly',
+ 'priority' => 0.5,
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Accessibility
+ * ------------------------------------------------------------------------
+ */
+ 'accessibility' => [
+ /*
+ * Show titles in the concrete5 toolbars
+ *
+ * @var bool
+ */
+ 'toolbar_titles' => false,
+
+ /*
+ * Increase the font size in the concrete5 toolbars
+ *
+ * @var bool
+ */
+ 'toolbar_large_font' => false,
+
+ /*
+ * Show help system
+ *
+ * @var bool
+ */
+ 'display_help_system' => true,
+
+ /*
+ * Show tooltips in the concrete5 toolbars
+ *
+ * @var bool
+ */
+ 'toolbar_tooltips' => true,
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Internationalization
+ * ------------------------------------------------------------------------
+ */
+ 'i18n' => [
+ /*
+ * Allow users to choose language on login
+ *
+ * @var bool
+ */
+ 'choose_language_login' => false,
+ // Fetch language files when installing a package connected to the marketplace [boolean]
+ 'auto_install_package_languages' => true,
+ // Community Translation instance offering concrete5 translations
+ 'community_translation' => [
+ // API entry point of the Community Translation instance
+ 'entry_point' => 'http://translate.concrete5.org/api',
+ // API Token to be used for the Community Translation instance
+ 'api_token' => '',
+ // Languages below this translation progress won't be considered
+ 'progress_limit' => 60,
+ // Lifetime (in seconds) of the cache items associated to downloaded data
+ 'cache_lifetime' => 3600, // 1 hour
+ // Base URI for package details
+ 'package_url' => 'https://translate.concrete5.org/translate/package',
+ ],
+ ],
+ 'urls' => [
+ 'concrete5' => 'http://www.concrete5.org',
+ 'concrete5_secure' => 'https://www.concrete5.org',
+ 'newsflow' => 'http://newsflow.concrete5.org',
+ 'background_feed' => '//backgroundimages.concrete5.org/wallpaper',
+ 'privacy_policy' => '//www.concrete5.org/legal/privacy-policy',
+ 'background_feed_secure' => 'https://backgroundimages.concrete5.org/wallpaper',
+ 'background_info' => 'http://backgroundimages.concrete5.org/get_image_data.php',
+ 'videos' => 'https://www.youtube.com/user/concrete5cms/videos',
+ 'help' => [
+ 'developer' => 'http://documentation.concrete5.org/developers',
+ 'user' => 'http://documentation.concrete5.org/editors',
+ 'forum' => 'http://www.concrete5.org/community/forums',
+ 'slack' => 'https://www.concrete5.org/slack',
+ ],
+ 'paths' => [
+ 'menu_help_service' => '/tools/get_remote_help_list/',
+ 'site_page' => '/private/sites',
+ 'newsflow_slot_content' => '/tools/slot_content/',
+ 'marketplace' => [
+ 'connect' => '/marketplace/connect',
+ 'connect_success' => '/marketplace/connect/-/connected',
+ 'connect_validate' => '/marketplace/connect/-/validate',
+ 'connect_new_token' => '/marketplace/connect/-/generate_token',
+ 'checkout' => '/cart/-/add',
+ 'purchases' => '/marketplace/connect/-/get_available_licenses',
+ 'item_information' => '/marketplace/connect/-/get_item_information',
+ 'item_free_license' => '/marketplace/connect/-/enable_free_license',
+ 'remote_item_list' => '/marketplace/',
+ ],
+ ],
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * White labeling.
+ * ------------------------------------------------------------------------
+ */
+ 'white_label' => [
+ /*
+ * Custom Logo source path relative to the public directory.
+ *
+ * @var bool|string The logo path
+ */
+ 'logo' => false,
+
+ /*
+ * Custom Name
+ *
+ * @var bool|string The name
+ */
+ 'name' => false,
+
+ /*
+ * Background image url
+ *
+ * @var null|string
+ */
+ 'background_image' => null,
+ ],
+ 'session' => [
+ 'name' => 'CONCRETE5',
+ 'handler' => 'file',
+ 'redis' => [
+ 'database' => 1, // Use different Redis Databases - optional
+ ],
+ 'save_path' => null,
+ // Minimum duration (in seconds) of an "unoutched" session
+ 'max_lifetime' => 7200,
+ // gc_probability and gc_divisor together define the probability to
+ // cleanup expided sessions ("garbage collection").
+ // Example: if gc_probability is 1 and gc_divisor is 100, on average we'll have 1 GC every 100 requests (1%)
+ // Example: if gc_probability is 5 and gc_divisor is 20, on average we'll have 1 GC every 20 requests (25%)
+ 'gc_probability' => 1,
+ 'gc_divisor' => 100,
+ 'cookie' => [
+ 'cookie_path' => false, // set a specific path here if you know it, otherwise it'll default to relative
+ 'cookie_lifetime' => 0,
+ 'cookie_domain' => false,
+ 'cookie_secure' => false,
+ 'cookie_httponly' => true,
+ 'cookie_raw' => false,
+ 'cookie_samesite' => null,
+ ],
+ 'remember_me' => [
+ 'lifetime' => 1209600, // 2 weeks in seconds
+ ],
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * User information and registration settings.
+ * ------------------------------------------------------------------------
+ */
+ 'user' => [
+ /*
+ * --------------------------------------------------------------------
+ * Registration settings.
+ * --------------------------------------------------------------------
+ */
+ 'registration' => [
+ /*
+ * Registration
+ *
+ * @var bool
+ */
+ 'enabled' => false,
+
+ /*
+ * Registration type
+ *
+ * @var string The type (disabled|enabled|validate_email)
+ */
+ 'type' => 'disabled',
+
+ /*
+ * Enable Registration Captcha
+ *
+ * @var bool
+ */
+ 'captcha' => true,
+
+ /*
+ * Use emails instead of usernames to log in
+ *
+ * @var bool
+ */
+ 'email_registration' => false,
+
+ /*
+ * Determines whether the username field is displayed when registering
+ */
+ 'display_username_field' => true,
+
+ /*
+ * Determines whether the confirm password field is displayed when registering
+ */
+ 'display_confirm_password_field' => true,
+
+ /*
+ * Validate emails during registration
+ *
+ * @var bool
+ */
+ 'validate_email' => false,
+
+ /*
+ * Admins approve each registration
+ *
+ * @var bool
+ */
+ 'approval' => false,
+
+ /*
+ * Send notifications after successful registration.
+ *
+ * @var bool|string Email to notify
+ */
+ 'notification' => false,
+ ],
+
+ /*
+ * --------------------------------------------------------------------
+ * Gravatar Settings
+ * --------------------------------------------------------------------
+ */
+ 'group' => [
+ 'badge' => [
+ 'default_point_value' => 50,
+ ],
+ ],
+
+ 'username' => [
+ 'maximum' => 64,
+ 'minimum' => 3,
+ 'allowed_characters' => [
+ 'boundary' => 'A-Za-z0-9',
+ 'middle' => 'A-Za-z0-9_\.',
+ 'requirement_string' => 'A username may only contain letters, numbers, dots (not at the beginning/end), and underscores (not at the beginning/end).',
+ 'error_string' => 'A username may only contain letters, numbers, dots (not at the beginning/end), and underscores (not at the beginning/end).',
+ ],
+ ],
+ 'password' => [
+ 'maximum' => 128,
+ 'minimum' => 5,
+ 'required_special_characters' => 0,
+ 'required_lower_case' => 0,
+ 'required_upper_case' => 0,
+ 'reuse' => 0,
+ 'custom_regex' => [],
+ 'hash_portable' => false,
+ 'hash_cost_log2' => 12,
+ 'legacy_salt' => '',
+ ],
+ 'email' => [
+ 'test_mx_record' => false,
+ 'strict' => true,
+ ],
+ 'private_messages' => [
+ 'throttle_max' => 20,
+ 'throttle_max_timespan' => 15, // minutes
+ ],
+
+ 'deactivation' => [
+ 'enable_login_threshold_deactivation' => false,
+ 'login' => [
+ 'threshold' => 120, // in days
+ ],
+ 'authentication_failure' => [
+ 'enabled' => false,
+ 'amount' => 5, // The number of failures
+ 'duration' => 300, // In so many seconds
+ ],
+ 'message' => 'This user is inactive. Please contact us regarding this account.',
+ ],
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Spam
+ * ------------------------------------------------------------------------
+ */
+ 'spam' => [
+ /*
+ * Whitelist group ID
+ *
+ * @var int
+ */
+ 'whitelist_group' => 0,
+
+ /*
+ * Notification email
+ *
+ * @var string
+ */
+ 'notify_email' => '',
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Calendar
+ * ------------------------------------------------------------------------
+ */
+ 'calendar' => [
+ 'colors' => [
+ 'text' => '#ffffff',
+ 'background' => '#3A87AD',
+ ],
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Security
+ * ------------------------------------------------------------------------
+ */
+ 'security' => [
+ 'session' => [
+ 'invalidate_on_user_agent_mismatch' => true,
+
+ 'invalidate_on_ip_mismatch' => true,
+
+ 'invalidate_inactive_users' => [
+ // Is the automatically logout inactive users setting enabled?
+ 'enabled' => false,
+ // Time window (in seconds) for inactive users to be automatically logout
+ 'time' => 300,
+ ],
+ ],
+ 'misc' => [
+ /*
+ * Defence Click Jacking.
+ *
+ * @var bool|string DENY, SAMEORIGIN, ALLOW-FROM uri
+ */
+ 'x_frame_options' => 'SAMEORIGIN',
+ ],
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Permissions and behaviors toggles.
+ * ------------------------------------------------------------------------
+ */
+ 'permissions' => [
+ /*
+ * Forward to login if access is denied
+ *
+ * @var bool
+ */
+ 'forward_to_login' => true,
+
+ /*
+ * Permission model
+ *
+ * @var string The permission model (simple|advanced)
+ */
+ 'model' => 'simple',
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * SEO Settings
+ * ------------------------------------------------------------------------
+ */
+ 'seo' => [
+ 'exclude_words' => 'a, an, as, at, before, but, by, for, from, is, in, into, like, of, off, on, onto, per, ' .
+ 'since, than, the, this, that, to, up, via, with',
+
+ /*
+ * URL rewriting
+ *
+ * Doesn't impact concrete.seo.url_rewriting_all which is set at a lower level and
+ * controls whether ALL items will be rewritten.
+ *
+ * @var bool
+ */
+ 'url_rewriting' => false,
+ 'url_rewriting_all' => false,
+ 'redirect_to_canonical_url' => false,
+ 'canonical_url' => null,
+ 'canonical_url_alternative' => null,
+ 'trailing_slash' => false,
+ 'title_format' => '%2$s :: %1$s',
+ 'title_segment_separator' => ' :: ',
+ 'page_path_separator' => '-',
+ 'group_name_separator' => ' / ',
+ 'segment_max_length' => 128,
+ 'paging_string' => 'ccm_paging_p',
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * Statistics Settings
+ * ------------------------------------------------------------------------
+ */
+ 'statistics' => [
+ 'track_downloads' => true,
+ ],
+ 'limits' => [
+ 'sitemap_pages' => 100,
+ 'delete_pages' => 100,
+ 'copy_pages' => 10,
+ 'page_search_index_batch' => 200,
+ 'job_queue_batch' => 10,
+ 'style_customizer' => [
+ 'size_min' => -50,
+ 'size_max' => 200,
+ ],
+ ],
+
+ 'page' => [
+ 'search' => [
+ // Always reindex pages (usually it isn't performed when approving workflows)
+ 'always_reindex' => false,
+ ],
+ ],
+
+ 'editor' => [
+ 'plugins' => [
+ 'selected' => [],
+ ],
+ ],
+
+ 'composer' => [
+ // [float] The time in seconds until idle triggers a save (set to 0 to disable autosave)
+ 'idle_timeout' => 1,
+ ],
+
+ /*
+ * ------------------------------------------------------------------------
+ * API settings
+ * ------------------------------------------------------------------------
+ */
+ 'api' => [
+ /*
+ * Enabled
+ *
+ * @var bool
+ */
+ 'enabled' => false,
+
+ /**
+ * Which grant types do we allow to connect to the API.
+ *
+ * @var array
+ */
+ 'grant_types' => [
+ 'client_credentials' => true,
+ 'authorization_code' => true,
+ 'password_credentials' => false,
+ 'refresh_token' => true,
+ ],
+ ],
+
+ 'mutex' => [
+ 'semaphore' => [
+ 'priority' => 100,
+ 'class' => Concrete\Core\System\Mutex\SemaphoreMutex::class,
+ ],
+ 'file_lock' => [
+ 'priority' => 50,
+ 'class' => Concrete\Core\System\Mutex\FileLockMutex::class,
+ ],
+ ],
+
+ 'social' => [
+ 'additional_services' => [
+ // Add here a list of arrays like this:
+ // ['service_handle', 'Service Name', 'icon']
+ // Where 'icon' is the handle of a FontAwesome 4 icon (see https://fontawesome.com/v4.7.0/icons/ )
+ ],
+ ],
+];
diff --git a/tests/fixtures/manifest/complete.json b/tests/fixtures/manifest/complete.json
new file mode 100644
index 0000000..17df986
--- /dev/null
+++ b/tests/fixtures/manifest/complete.json
@@ -0,0 +1,80 @@
+{
+ "created": "2021-03-10T20:40:50+0000",
+ "site": "concrete5 Site",
+ "url": "https://some.url",
+ "path": "\/home\/foo\/concrete5",
+ "host": "foo.local",
+ "version": "1.0",
+ "contents": {
+ "application": true,
+ "applicationContents": [
+ "attributes",
+ "authentication",
+ "blocks",
+ "bootstrap",
+ "config",
+ "controllers",
+ "elements",
+ "jobs",
+ "languages",
+ "mail",
+ "page_templates",
+ "single_pages",
+ "src",
+ "themes",
+ "tools",
+ "views"
+ ],
+ "core": true,
+ "database": "db.sql",
+ "index": true,
+ "packages": [
+ {
+ "handle": "foo",
+ "included": true,
+ "installed": true
+ },
+ {
+ "handle": "baz",
+ "included": true,
+ "installed": false
+ },
+ {
+ "handle": "zab",
+ "included": false,
+ "installed": true
+ },
+ {
+ "handle": "bar",
+ "included": false,
+ "installed": false
+ }
+ ],
+ "storageLocations": [
+ {
+ "id": 1,
+ "name": "red",
+ "default": true,
+ "included": true
+ },
+ {
+ "id": 2,
+ "name": "blue",
+ "default": true,
+ "included": false
+ },
+ {
+ "id": 3,
+ "name": "green",
+ "default": false,
+ "included": true
+ },
+ {
+ "id": 4,
+ "name": "yellow",
+ "default": false,
+ "included": false
+ }
+ ]
+ }
+}
diff --git a/tests/fixtures/manifest/opposite.json b/tests/fixtures/manifest/opposite.json
new file mode 100644
index 0000000..7ee0975
--- /dev/null
+++ b/tests/fixtures/manifest/opposite.json
@@ -0,0 +1,58 @@
+{
+ "created": "2020-02-10T20:40:50+0000",
+ "site": "",
+ "url": "",
+ "path": "",
+ "host": "",
+ "version": "",
+ "contents": {
+ "database": "",
+ "storageLocations": [
+ {
+ "id": 1,
+ "name": "red",
+ "default": false,
+ "included": false
+ },
+ {
+ "id": 2,
+ "name": "blue",
+ "default": false,
+ "included": true
+ },
+ {
+ "id": 3,
+ "name": "green",
+ "default": true,
+ "included": false
+ },
+ {
+ "id": 4,
+ "name": "yellow",
+ "default": true,
+ "included": true
+ }
+ ],
+ "application": false,
+ "applicationContents": [],
+ "packages": [
+ {
+ "handle": "foo",
+ "installed": false,
+ "included": false
+ },
+ {
+ "handle": "baz",
+ "installed": true,
+ "included": false
+ },
+ {
+ "handle": "bar",
+ "installed": true,
+ "included": true
+ }
+ ],
+ "core": false,
+ "index": false
+ }
+}