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'); + } +} +