diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a38075aa..25ac99705 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
* Added new `Security::sanitizeSVG()` function
1. [](#improved)
* Several FlexObject loading improvements
+ * Added `bin/grav page-system-validator [-r|--record] [-c|--check]` to test Flex Pages
1. [](#bugfix)
* Regression: Fixed language fallback
* Regression: Fixed translations when language code is used for non-language purposes
diff --git a/bin/grav b/bin/grav
index 28eddfef2..7d365cbfc 100755
--- a/bin/grav
+++ b/bin/grav
@@ -3,7 +3,7 @@
use Grav\Common\Composer;
use Grav\Common\Grav;
-use League\CLImate\CLImate;
+use Grav\Console\Cli;
use Symfony\Component\Console\Application;
\define('GRAV_CLI', true);
@@ -52,17 +52,18 @@ if (!file_exists(GRAV_ROOT . '/index.php')) {
$app = new Application('Grav CLI Application', GRAV_VERSION);
$app->addCommands(array(
- new \Grav\Console\Cli\InstallCommand(),
- new \Grav\Console\Cli\ComposerCommand(),
- new \Grav\Console\Cli\SandboxCommand(),
- new \Grav\Console\Cli\CleanCommand(),
- new \Grav\Console\Cli\ClearCacheCommand(),
- new \Grav\Console\Cli\BackupCommand(),
- new \Grav\Console\Cli\NewProjectCommand(),
- new \Grav\Console\Cli\SchedulerCommand(),
- new \Grav\Console\Cli\SecurityCommand(),
- new \Grav\Console\Cli\LogViewerCommand(),
- new \Grav\Console\Cli\YamlLinterCommand(),
- new \Grav\Console\Cli\ServerCommand(),
+ new Cli\InstallCommand(),
+ new Cli\ComposerCommand(),
+ new Cli\SandboxCommand(),
+ new Cli\CleanCommand(),
+ new Cli\ClearCacheCommand(),
+ new Cli\BackupCommand(),
+ new Cli\NewProjectCommand(),
+ new Cli\SchedulerCommand(),
+ new Cli\SecurityCommand(),
+ new Cli\LogViewerCommand(),
+ new Cli\YamlLinterCommand(),
+ new Cli\ServerCommand(),
+ new Cli\PageSystemValidatorCommand(),
));
$app->run();
diff --git a/system/src/Grav/Console/Cli/PageSystemValidatorCommand.php b/system/src/Grav/Console/Cli/PageSystemValidatorCommand.php
new file mode 100644
index 000000000..5ca625fca
--- /dev/null
+++ b/system/src/Grav/Console/Cli/PageSystemValidatorCommand.php
@@ -0,0 +1,276 @@
+ [[]],
+ 'summary' => [[], [200], [200, true]],
+ 'content' => [[]],
+ 'getRawContent' => [[]],
+ 'rawMarkdown' => [[]],
+ 'value' => [['content'], ['route'], ['order'], ['ordering'], ['folder'], ['slug'], ['name'], /*['frontmatter'],*/ ['header.menu'], ['header.slug']],
+ 'title' => [[]],
+ 'menu' => [[]],
+ 'visible' => [[]],
+ 'published' => [[]],
+ 'publishDate' => [[]],
+ 'unpublishDate' => [[]],
+ 'process' => [[]],
+ 'slug' => [[]],
+ 'order' => [[]],
+ //'id' => [[]],
+ 'modified' => [[]],
+ 'lastModified' => [[]],
+ 'folder' => [[]],
+ 'date' => [[]],
+ 'dateformat' => [[]],
+ 'taxonomy' => [[]],
+ 'shouldProcess' => [['twig'], ['markdown']],
+ 'isPage' => [[]],
+ 'isDir' => [[]],
+ 'exists' => [[]],
+
+ // Forms
+ 'forms' => [[]],
+
+ // Routing
+ 'urlExtension' => [[]],
+ 'routable' => [[]],
+ 'link' => [[], [false], [true]],
+ 'permalink' => [[]],
+ 'canonical' => [[], [false], [true]],
+ 'url' => [[], [true], [true, true], [true, true, false], [false, false, true, false]],
+ 'route' => [[]],
+ 'rawRoute' => [[]],
+ 'routeAliases' => [[]],
+ 'routeCanonical' => [[]],
+ 'redirect' => [[]],
+ 'relativePagePath' => [[]],
+ 'path' => [[]],
+ //'folder' => [[]],
+ 'parent' => [[]],
+ 'topParent' => [[]],
+ 'currentPosition' => [[]],
+ 'active' => [[]],
+ 'activeChild' => [[]],
+ 'home' => [[]],
+ 'root' => [[]],
+
+ // Translations
+ //'translatedLanguages' => [[], [false], [true]],
+ //'untranslatedLanguages' => [[], [false], [true]],
+ //'language' => [[]],
+
+ // Legacy
+ 'raw' => [[]],
+ 'frontmatter' => [[]],
+ 'httpResponseCode' => [[]],
+ 'httpHeaders' => [[]],
+ 'blueprintName' => [[]],
+ 'name' => [[]],
+ 'childType' => [[]],
+ 'template' => [[]],
+ 'templateFormat' => [[]],
+ 'extension' => [[]],
+ 'expires' => [[]],
+ 'cacheControl' => [[]],
+ 'ssl' => [[]],
+ 'metadata' => [[]],
+ 'eTag' => [[]],
+ 'filePath' => [[]],
+ 'filePathClean' => [[]],
+ 'orderDir' => [[]],
+ 'orderBy' => [[]],
+ 'orderManual' => [[]],
+ 'maxCount' => [[]],
+ 'modular' => [[]],
+ 'modularTwig' => [[]],
+ //'children' => [[]],
+ 'isFirst' => [[]],
+ 'isLast' => [[]],
+ 'prevSibling' => [[]],
+ 'nextSibling' => [[]],
+ 'adjacentSibling' => [[]],
+ 'ancestor' => [[]],
+ //'inherited' => [[]],
+ //'inheritedField' => [[]],
+ 'find' => [['/']],
+ //'collection' => [[]],
+ //'evaluate' => [[]],
+ 'folderExists' => [[]],
+ //'getOriginal' => [[]],
+ //'getAction' => [[]],
+ ];
+
+ protected function configure()
+ {
+ $this
+ ->setName('page-system-validator')
+ ->setDescription('Page validator can be used to compare site before/after update and when migrating to Flex Pages.')
+ ->addOption('record', 'r', InputOption::VALUE_NONE, 'Record results')
+ ->addOption('check', 'c', InputOption::VALUE_NONE, 'Compare site against previously recorded results')
+ ->setHelp('The page-system-validator command can be used to test the pages before and after upgrade');
+ }
+
+ protected function serve()
+ {
+ $this->output->writeln('');
+
+ $this->grav = $grav = Grav::instance();
+ $grav->setup();
+
+ // Initialize
+ $grav['uri']->init();
+ /** @var Config $config */
+ $config = $grav['config'];
+ $config->init();
+ $grav['plugins']->setup();
+ $grav['debugger']->init();
+ // Initialize the timezone.
+ $timezone = $config->get('system.timezone');
+ if ($timezone) {
+ date_default_timezone_set($timezone);
+ }
+ /** @var Uri $uri */
+ $uri = $grav['uri'];
+ $uri->init();
+ $grav->setLocale();
+ $grav['language']->setActive('en');
+
+ // Plugins
+ $grav['accounts'];
+ $grav['plugins']->init();
+ $grav->fireEvent('onPluginsInitialized');
+
+ // Themes
+ $grav['themes']->init();
+
+ // Twig
+ $grav['twig']->init();
+
+ // Pages
+ $grav['pages']->init();
+ $grav->fireEvent('onPagesInitialized', new Event(['pages' => $grav['pages']]));
+
+ if ($this->input->getOption('record')) {
+ $this->output->writeln('Pages: ' . $config->get('system.pages.type', 'page'));
+
+ $this->output->writeln('Record tests');
+ $this->output->writeln('');
+
+ $results = $this->record();
+ $file = $this->getFile('pages-old');
+ $file->save($results);
+
+ $this->output->writeln('Recorded tests to ' . $file->filename());
+ } elseif ($this->input->getOption('check')) {
+ $this->output->writeln('Pages: ' . $config->get('system.pages.type', 'page'));
+
+ $this->output->writeln('Run tests');
+ $this->output->writeln('');
+
+ $new = $this->record();
+ $file = $this->getFile('pages-new');
+ $file->save($new);
+
+ $file = $this->getFile('pages-old');
+ $old = $file->content();
+
+ $results = $this->check($old, $new);
+ if (empty($results)) {
+ $this->output->writeln('Success!');
+ } else {
+ $file = $this->getFile('diff');
+ $file->save($results);
+ $this->output->writeln('Recorded results to ' . $file->filename());
+ }
+ } else {
+ $this->output->writeln('page-system-validator [-r|--record] [-c|--check]');
+ }
+ $this->output->writeln('');
+ }
+
+ private function record()
+ {
+ $pages = $this->grav['pages'];
+ $all = $pages->all();
+
+ $results = [];
+ foreach ($all as $page) {
+ foreach ($this->tests as $method => $params) {
+ $params = $params ?: [[]];
+ foreach ($params as $p) {
+ $result = $page->$method(...$p);
+ if (in_array($method, ['summary', 'content', 'getRawContent'], true)) {
+ $result = preg_replace('/name="(form-nonce|__unique_form_id__)" value="[^"]+"/', 'name="\\1" value="DYNAMIC"', $result);
+ $result = preg_replace('`src=("|\'|")/images/./././././[^"]+\\1`', 'src="\\1images/GENERATED\\1', $result);
+ $result = preg_replace('/\?\d{10}/', '?1234567890', $result);
+ } elseif ($method === 'httpHeaders' && isset($result['Expires'])) {
+ $result['Expires'] = 'Thu, 19 Sep 2019 13:10:24 GMT (REPLACED AS DYNAMIC)';
+ } elseif ($result instanceof PageInterface) {
+ $result = $result->rawRoute();
+ } elseif (is_object($result)) {
+ $result = json_decode(json_encode($result), true);
+ }
+
+ $ps = [];
+ foreach ($p as $val) {
+ $ps[] = (string)var_export($val, true);
+ }
+ $pstr = implode(', ', $ps);
+ $call = "->{$method}({$pstr})";
+ $results[$page->rawRoute()][$call] = $result;
+ }
+ }
+ }
+
+ return json_decode(json_encode($results), true);
+ }
+
+ private function check(array $old, array $new)
+ {
+ $errors = [];
+ foreach ($old as $path => $page) {
+ if (!isset($new[$path])) {
+ $errors[$path] = 'PAGE REMOVED';
+ continue;
+ }
+ foreach ($page as $method => $test) {
+ if (($new[$path][$method] ?? null) !== $test) {
+ $errors[$path][$method] = ['old' => $test, 'new' => $new[$path][$method]];
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * @param string $name
+ * @return CompiledYamlFile
+ */
+ private function getFile(string $name)
+ {
+ return CompiledYamlFile::instance('cache://tests/' . $name . '.yaml');
+ }
+}
+