PageSystemValidatorCommand.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. <?php
  2. /**
  3. * @package Grav\Console\Cli
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Console\Cli;
  9. use Grav\Common\Config\Config;
  10. use Grav\Common\File\CompiledYamlFile;
  11. use Grav\Common\Grav;
  12. use Grav\Common\Page\Interfaces\PageInterface;
  13. use Grav\Common\Page\Pages;
  14. use Grav\Console\GravCommand;
  15. use RocketTheme\Toolbox\Event\Event;
  16. use Symfony\Component\Console\Input\InputOption;
  17. use function in_array;
  18. use function is_object;
  19. /**
  20. * Class PageSystemValidatorCommand
  21. * @package Grav\Console\Cli
  22. */
  23. class PageSystemValidatorCommand extends GravCommand
  24. {
  25. /** @var array */
  26. protected $tests = [
  27. // Content
  28. 'header' => [[]],
  29. 'summary' => [[], [200], [200, true]],
  30. 'content' => [[]],
  31. 'getRawContent' => [[]],
  32. 'rawMarkdown' => [[]],
  33. 'value' => [['content'], ['route'], ['order'], ['ordering'], ['folder'], ['slug'], ['name'], /*['frontmatter'],*/ ['header.menu'], ['header.slug']],
  34. 'title' => [[]],
  35. 'menu' => [[]],
  36. 'visible' => [[]],
  37. 'published' => [[]],
  38. 'publishDate' => [[]],
  39. 'unpublishDate' => [[]],
  40. 'process' => [[]],
  41. 'slug' => [[]],
  42. 'order' => [[]],
  43. //'id' => [[]],
  44. 'modified' => [[]],
  45. 'lastModified' => [[]],
  46. 'folder' => [[]],
  47. 'date' => [[]],
  48. 'dateformat' => [[]],
  49. 'taxonomy' => [[]],
  50. 'shouldProcess' => [['twig'], ['markdown']],
  51. 'isPage' => [[]],
  52. 'isDir' => [[]],
  53. 'exists' => [[]],
  54. // Forms
  55. 'forms' => [[]],
  56. // Routing
  57. 'urlExtension' => [[]],
  58. 'routable' => [[]],
  59. 'link' => [[], [false], [true]],
  60. 'permalink' => [[]],
  61. 'canonical' => [[], [false], [true]],
  62. 'url' => [[], [true], [true, true], [true, true, false], [false, false, true, false]],
  63. 'route' => [[]],
  64. 'rawRoute' => [[]],
  65. 'routeAliases' => [[]],
  66. 'routeCanonical' => [[]],
  67. 'redirect' => [[]],
  68. 'relativePagePath' => [[]],
  69. 'path' => [[]],
  70. //'folder' => [[]],
  71. 'parent' => [[]],
  72. 'topParent' => [[]],
  73. 'currentPosition' => [[]],
  74. 'active' => [[]],
  75. 'activeChild' => [[]],
  76. 'home' => [[]],
  77. 'root' => [[]],
  78. // Translations
  79. 'translatedLanguages' => [[], [false], [true]],
  80. 'untranslatedLanguages' => [[], [false], [true]],
  81. 'language' => [[]],
  82. // Legacy
  83. 'raw' => [[]],
  84. 'frontmatter' => [[]],
  85. 'httpResponseCode' => [[]],
  86. 'httpHeaders' => [[]],
  87. 'blueprintName' => [[]],
  88. 'name' => [[]],
  89. 'childType' => [[]],
  90. 'template' => [[]],
  91. 'templateFormat' => [[]],
  92. 'extension' => [[]],
  93. 'expires' => [[]],
  94. 'cacheControl' => [[]],
  95. 'ssl' => [[]],
  96. 'metadata' => [[]],
  97. 'eTag' => [[]],
  98. 'filePath' => [[]],
  99. 'filePathClean' => [[]],
  100. 'orderDir' => [[]],
  101. 'orderBy' => [[]],
  102. 'orderManual' => [[]],
  103. 'maxCount' => [[]],
  104. 'modular' => [[]],
  105. 'modularTwig' => [[]],
  106. //'children' => [[]],
  107. 'isFirst' => [[]],
  108. 'isLast' => [[]],
  109. 'prevSibling' => [[]],
  110. 'nextSibling' => [[]],
  111. 'adjacentSibling' => [[]],
  112. 'ancestor' => [[]],
  113. //'inherited' => [[]],
  114. //'inheritedField' => [[]],
  115. 'find' => [['/']],
  116. //'collection' => [[]],
  117. //'evaluate' => [[]],
  118. 'folderExists' => [[]],
  119. //'getOriginal' => [[]],
  120. //'getAction' => [[]],
  121. ];
  122. /** @var Grav */
  123. protected $grav;
  124. /**
  125. * @return void
  126. */
  127. protected function configure(): void
  128. {
  129. $this
  130. ->setName('page-system-validator')
  131. ->setDescription('Page validator can be used to compare site before/after update and when migrating to Flex Pages.')
  132. ->addOption('record', 'r', InputOption::VALUE_NONE, 'Record results')
  133. ->addOption('check', 'c', InputOption::VALUE_NONE, 'Compare site against previously recorded results')
  134. ->setHelp('The <info>page-system-validator</info> command can be used to test the pages before and after upgrade');
  135. }
  136. /**
  137. * @return int
  138. */
  139. protected function serve(): int
  140. {
  141. $input = $this->getInput();
  142. $io = $this->getIO();
  143. $this->setLanguage('en');
  144. $this->initializePages();
  145. $io->newLine();
  146. $this->grav = $grav = Grav::instance();
  147. $grav->fireEvent('onPageInitialized', new Event(['page' => $grav['page']]));
  148. /** @var Config $config */
  149. $config = $grav['config'];
  150. if ($input->getOption('record')) {
  151. $io->writeln('Pages: ' . $config->get('system.pages.type', 'page'));
  152. $io->writeln('<magenta>Record tests</magenta>');
  153. $io->newLine();
  154. $results = $this->record();
  155. $file = $this->getFile('pages-old');
  156. $file->save($results);
  157. $io->writeln('Recorded tests to ' . $file->filename());
  158. } elseif ($input->getOption('check')) {
  159. $io->writeln('Pages: ' . $config->get('system.pages.type', 'page'));
  160. $io->writeln('<magenta>Run tests</magenta>');
  161. $io->newLine();
  162. $new = $this->record();
  163. $file = $this->getFile('pages-new');
  164. $file->save($new);
  165. $io->writeln('Recorded tests to ' . $file->filename());
  166. $file = $this->getFile('pages-old');
  167. $old = $file->content();
  168. $results = $this->check($old, $new);
  169. $file = $this->getFile('diff');
  170. $file->save($results);
  171. $io->writeln('Recorded results to ' . $file->filename());
  172. } else {
  173. $io->writeln('<green>page-system-validator [-r|--record] [-c|--check]</green>');
  174. }
  175. $io->newLine();
  176. return 0;
  177. }
  178. /**
  179. * @return array
  180. */
  181. private function record(): array
  182. {
  183. $io = $this->getIO();
  184. /** @var Pages $pages */
  185. $pages = $this->grav['pages'];
  186. $all = $pages->all();
  187. $results = [];
  188. $results[''] = $this->recordRow($pages->root());
  189. foreach ($all as $path => $page) {
  190. if (null === $page) {
  191. $io->writeln('<red>Error on page ' . $path . '</red>');
  192. continue;
  193. }
  194. $results[$page->rawRoute()] = $this->recordRow($page);
  195. }
  196. return json_decode(json_encode($results), true);
  197. }
  198. /**
  199. * @param PageInterface $page
  200. * @return array
  201. */
  202. private function recordRow(PageInterface $page): array
  203. {
  204. $results = [];
  205. foreach ($this->tests as $method => $params) {
  206. $params = $params ?: [[]];
  207. foreach ($params as $p) {
  208. $result = $page->$method(...$p);
  209. if (in_array($method, ['summary', 'content', 'getRawContent'], true)) {
  210. $result = preg_replace('/name="(form-nonce|__unique_form_id__)" value="[^"]+"/',
  211. 'name="\\1" value="DYNAMIC"', $result);
  212. $result = preg_replace('`src=("|\'|&quot;)/images/./././././[^"]+\\1`',
  213. 'src="\\1images/GENERATED\\1', $result);
  214. $result = preg_replace('/\?\d{10}/', '?1234567890', $result);
  215. } elseif ($method === 'httpHeaders' && isset($result['Expires'])) {
  216. $result['Expires'] = 'Thu, 19 Sep 2019 13:10:24 GMT (REPLACED AS DYNAMIC)';
  217. } elseif ($result instanceof PageInterface) {
  218. $result = $result->rawRoute();
  219. } elseif (is_object($result)) {
  220. $result = json_decode(json_encode($result), true);
  221. }
  222. $ps = [];
  223. foreach ($p as $val) {
  224. $ps[] = (string)var_export($val, true);
  225. }
  226. $pstr = implode(', ', $ps);
  227. $call = "->{$method}({$pstr})";
  228. $results[$call] = $result;
  229. }
  230. }
  231. return $results;
  232. }
  233. /**
  234. * @param array $old
  235. * @param array $new
  236. * @return array
  237. */
  238. private function check(array $old, array $new): array
  239. {
  240. $errors = [];
  241. foreach ($old as $path => $page) {
  242. if (!isset($new[$path])) {
  243. $errors[$path] = 'PAGE REMOVED';
  244. continue;
  245. }
  246. foreach ($page as $method => $test) {
  247. if (($new[$path][$method] ?? null) !== $test) {
  248. $errors[$path][$method] = ['old' => $test, 'new' => $new[$path][$method]];
  249. }
  250. }
  251. }
  252. return $errors;
  253. }
  254. /**
  255. * @param string $name
  256. * @return CompiledYamlFile
  257. */
  258. private function getFile(string $name): CompiledYamlFile
  259. {
  260. return CompiledYamlFile::instance('cache://tests/' . $name . '.yaml');
  261. }
  262. }