DevToolsCommand.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. <?php
  2. namespace Grav\Plugin\Console;
  3. use Grav\Common\Grav;
  4. use Grav\Common\Filesystem\Folder;
  5. use Grav\Common\GPM\GPM;
  6. use Grav\Common\Inflector;
  7. use Grav\Common\Twig\Twig;
  8. use Grav\Common\Utils;
  9. use RocketTheme\Toolbox\File\File;
  10. use Grav\Console\ConsoleCommand;
  11. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  12. use Symfony\Component\Console\Input\InputInterface;
  13. use Symfony\Component\Console\Style\SymfonyStyle;
  14. /**
  15. * Class DevToolsCommand
  16. * @package Grav\Plugin\Console
  17. */
  18. class DevToolsCommand extends ConsoleCommand
  19. {
  20. /** @var array */
  21. protected $component = [];
  22. /** @var Inflector */
  23. protected $inflector;
  24. /** @var UniformResourceLocator */
  25. protected $locator;
  26. /** @var Twig */
  27. protected $twig;
  28. /** @var GPM */
  29. protected $gpm;
  30. /** @var array */
  31. protected $options = [];
  32. /** @var array */
  33. protected $reserved_keywords = ['__halt_compiler', 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor'];
  34. /**
  35. * Initializes the basic requirements for the developer tools
  36. *
  37. * @return void
  38. */
  39. protected function init(): void
  40. {
  41. if (!function_exists('curl_version')) {
  42. exit('FATAL: DEVTOOLS requires PHP Curl module to be installed');
  43. }
  44. $grav = Grav::instance();
  45. $grav['config']->init();
  46. $grav['uri']->init();
  47. $this->inflector = $grav['inflector'];
  48. $this->locator = $grav['locator'];
  49. $this->twig = $grav['twig'];
  50. $this->gpm = new GPM();
  51. //Add `theme://` to prevent fail
  52. $this->locator->addPath('theme', '', []);
  53. $this->locator->addPath('plugin', '', []);
  54. $this->locator->addPath('blueprint', '', []);
  55. // $this->config->set('theme', $config->get('themes.' . $name));
  56. }
  57. /**
  58. * Backwards compatibility to Grav 1.6.
  59. *
  60. * @return InputInterface
  61. */
  62. public function getInput(): InputInterface
  63. {
  64. return $this->input;
  65. }
  66. /**
  67. * Backwards compatibility to Grav 1.6.
  68. *
  69. * @return SymfonyStyle
  70. */
  71. public function getIO(): SymfonyStyle
  72. {
  73. $output = $this->output;
  74. if (!$output instanceof SymfonyStyle) {
  75. $this->output = $output = new SymfonyStyle($this->input, $this->output);
  76. }
  77. return $this->output;
  78. }
  79. /**
  80. * Copies the component type and renames accordingly
  81. *
  82. * @return bool
  83. */
  84. protected function createComponent(): bool
  85. {
  86. $name = $this->component['name'];
  87. $folder_name = strtolower($this->inflector::hyphenize($name));
  88. $new_theme = $folder_name;
  89. $type = $this->component['type'];
  90. $grav = Grav::instance();
  91. $config = $grav['config'];
  92. $current_theme = $config->get('system.pages.theme');
  93. $template = $this->component['template'];
  94. $source_theme = null;
  95. if (isset($this->component['copy'])) {
  96. $current_theme = $this->component['copy'];
  97. $source_theme = $this->locator->findResource('themes://' . $current_theme);
  98. $template_folder = $source_theme;
  99. } else {
  100. $template_folder = __DIR__ . "/../components/{$type}/{$template}";
  101. }
  102. if ($type === 'blueprint') {
  103. $component_folder = $this->locator->findResource('themes://' . $current_theme) . '/blueprints';
  104. } else {
  105. $component_folder = $this->locator->findResource($type . 's://') . DS . $folder_name;
  106. }
  107. if (false === $template_folder) {
  108. $this->output->writeln("<red>Theme {$current_theme} does not exist</red>");
  109. return false;
  110. }
  111. if ($template === 'inheritance') {
  112. $parent_theme = $this->component['extends'];
  113. $yaml_file = $this->locator->findResource('themes://' . $parent_theme) . '/' . $parent_theme . '.yaml';
  114. $this->component['config'] = file_get_contents($yaml_file);;
  115. }
  116. if (isset($source_theme)) {
  117. /**
  118. * Copy existing theme and regex-replace old stuff with new
  119. */
  120. // Get source if a symlink
  121. if (is_link($template_folder)) {
  122. $template_folder = readlink($template_folder);
  123. if (false === $template_folder) {
  124. $this->output->writeln("<red>Theme {$current_theme} is a bad symlink</red>");
  125. return false;
  126. }
  127. }
  128. //Copy All files to component folder
  129. try {
  130. Folder::copy($template_folder, $component_folder, '/.git|node_modules/');
  131. } catch (\Exception $e) {
  132. $this->output->writeln("<red>" . $e->getMessage() . "</red>");
  133. return false;
  134. }
  135. // Do some filename renaming
  136. $base_old_filename = $component_folder . '/' . $current_theme;
  137. $base_new_filename = $component_folder . '/' . $new_theme;
  138. @rename( $base_old_filename . '.php', $base_new_filename . '.php');
  139. @rename( $base_old_filename . '.yaml', $base_new_filename . '.yaml');
  140. $camelized_current = $this->inflector::camelize($current_theme);
  141. $camelized_new = $this->inflector::camelize($name);
  142. $hyphenized_current = $this->inflector::hyphenize($current_theme);
  143. $hyphenized_new = $this->inflector::hyphenize($name);
  144. $titleized_current = $this->inflector::titleize($current_theme);
  145. $titleized_new = $this->inflector::titleize($name);
  146. $underscoreized_current = $this->inflector::underscorize($current_theme);
  147. $underscoreized_new = $this->inflector::underscorize($name);
  148. $variations_regex = [
  149. ["/$camelized_current/", "/$hyphenized_current/"],
  150. [$camelized_new, $hyphenized_new]
  151. ];
  152. if (!in_array("/$titleized_current/", array_values($variations_regex[0]))) {
  153. $current_regex = $variations_regex[0];
  154. $new_regex = $variations_regex[1];
  155. $current_regex[] = "/$titleized_current/";
  156. $new_regex[] = $titleized_new;
  157. $variations_regex = [$current_regex, $new_regex];
  158. }
  159. if (!in_array("/$underscoreized_current/", array_values($variations_regex[0]))) {
  160. $current_regex = $variations_regex[0];
  161. $new_regex = $variations_regex[1];
  162. $current_regex[] = "/$underscoreized_current/";
  163. $new_regex[] = $underscoreized_new;
  164. $variations_regex = [$current_regex, $new_regex];
  165. }
  166. $regex_array = [
  167. $new_theme . '.php' => $variations_regex,
  168. 'blueprints.yaml' => $variations_regex,
  169. 'README.md' => $variations_regex,
  170. ];
  171. foreach ($regex_array as $filename => $data) {
  172. $filename = $component_folder . '/' . $filename;
  173. if (!file_exists($filename)) {
  174. continue;
  175. }
  176. $file = file_get_contents($filename);
  177. if ($file) {
  178. $file = preg_replace($data[0], $data[1], $file);
  179. }
  180. file_put_contents($filename, $file);
  181. }
  182. echo $source_theme;
  183. } else {
  184. /**
  185. * Use components folder and twig processing
  186. */
  187. //Copy All files to component folder
  188. try {
  189. Folder::copy($template_folder, $component_folder);
  190. } catch (\Exception $e) {
  191. $this->output->writeln("<red>" . $e->getMessage() . "</red>");
  192. return false;
  193. }
  194. //Add Twig vars and templates then initialize
  195. $this->twig->twig_vars['component'] = $this->component;
  196. $this->twig->twig_paths[] = $template_folder;
  197. $this->twig->init();
  198. //Get all templates of component then process each with twig and save
  199. $templates = Folder::all($component_folder);
  200. try {
  201. foreach($templates as $templateFile) {
  202. if (Utils::endsWith($templateFile, '.twig') && !Utils::endsWith($templateFile, '.html.twig')) {
  203. $content = $this->twig->processTemplate($templateFile);
  204. $file = File::instance($component_folder . DS . str_replace('.twig', '', $templateFile));
  205. $file->content($content);
  206. $file->save();
  207. //Delete twig template
  208. $file = File::instance($component_folder . DS . $templateFile);
  209. $file->delete();
  210. }
  211. }
  212. } catch (\Exception $e) {
  213. $this->output->writeln("<red>" . $e->getMessage() . "</red>");
  214. $this->output->writeln("Rolling back...");
  215. Folder::delete($component_folder);
  216. $this->output->writeln($type . "creation failed!");
  217. return false;
  218. }
  219. if ($type !== 'blueprint') {
  220. rename($component_folder . DS . $type . '.php', $component_folder . DS . $folder_name . '.php');
  221. rename($component_folder . DS . $type . '.yaml', $component_folder . DS . $folder_name . '.yaml');
  222. } else {
  223. $bpname = $this->inflector::hyphenize($this->component['bpname']);
  224. rename($component_folder . DS . $type . '.yaml', $component_folder . DS . $bpname . '.yaml');
  225. }
  226. }
  227. $this->output->writeln('');
  228. $this->output->writeln('<green>SUCCESS</green> ' . $type . ' <magenta>' . $name . '</magenta> -> Created Successfully');
  229. $this->output->writeln('');
  230. $this->output->writeln('Path: <cyan>' . $component_folder . '</cyan>');
  231. $this->output->writeln('');
  232. if ($type === 'plugin') {
  233. $this->output->writeln('<yellow>Please run `cd ' . $component_folder . '` and `composer update` to initialize the autoloader</yellow>');
  234. $this->output->writeln('');
  235. }
  236. return true;
  237. }
  238. /**
  239. * Iterate through all options and validate
  240. *
  241. * @return void
  242. */
  243. protected function validateOptions(): void
  244. {
  245. foreach (array_filter($this->options) as $type => $value) {
  246. $this->validate($type, $value);
  247. }
  248. }
  249. /**
  250. * @param string $type
  251. * @param mixed $value
  252. * @return mixed
  253. */
  254. protected function validate(string $type, $value)
  255. {
  256. switch ($type) {
  257. case 'name':
  258. // Check If name
  259. if ($value === null || trim($value) === '') {
  260. throw new \RuntimeException('Name cannot be empty');
  261. }
  262. if (!$this->options['offline']) {
  263. // Check for name collision with online gpm.
  264. if (false !== $this->gpm->findPackage($value)) {
  265. throw new \RuntimeException('Package name exists in GPM');
  266. }
  267. } else {
  268. $this->output->writeln('');
  269. $this->output->writeln(' <red>Warning</red>: Please note that by skipping the online check, your project\'s plugin or theme name may conflict with an existing plugin or theme.');
  270. }
  271. // Check if it's reserved
  272. if ($this->isReservedWord(strtolower($value))) {
  273. throw new \RuntimeException("\"" . $value . "\" is a reserved word and cannot be used as the name");
  274. }
  275. break;
  276. case 'description':
  277. if($value === null || trim($value) === '') {
  278. throw new \RuntimeException('Description cannot be empty');
  279. }
  280. break;
  281. case 'themename':
  282. if($value === null || trim($value) === '') {
  283. throw new \RuntimeException('Theme Name cannot be empty');
  284. }
  285. break;
  286. case 'developer':
  287. if ($value === null || trim($value) === '') {
  288. throw new \RuntimeException('Developer\'s Name cannot be empty');
  289. }
  290. break;
  291. case 'githubid':
  292. // GitHubID can be blank, so nothing here
  293. break;
  294. case 'email':
  295. if (!preg_match('/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/iD', $value)) {
  296. throw new \RuntimeException('Not a valid email address');
  297. }
  298. break;
  299. }
  300. return $value;
  301. }
  302. /**
  303. * @param string $word
  304. * @return bool
  305. */
  306. public function isReservedWord(string $word): bool
  307. {
  308. return in_array($word, $this->reserved_keywords, true);
  309. }
  310. }