170316b84SAndreas Gohr<?php 270316b84SAndreas Gohr 370316b84SAndreas Gohrnamespace dokuwiki\plugin\dev; 470316b84SAndreas Gohr 570316b84SAndreas Gohruse RuntimeException; 670316b84SAndreas Gohr 770316b84SAndreas Gohr/** 870316b84SAndreas Gohr * This class holds basic information about a plugin or template and uses the skeleton files to 970316b84SAndreas Gohr * create new plugin or template specific versions of them. 1070316b84SAndreas Gohr * 1170316b84SAndreas Gohr * This class does not write any files, but only provides the data for the actual file creation. 1270316b84SAndreas Gohr */ 1370316b84SAndreas Gohrclass Skeletor 1470316b84SAndreas Gohr{ 1570316b84SAndreas Gohr // FIXME this may change upstream we may want to update it via github action 1670316b84SAndreas Gohr const PLUGIN_TYPES = ['auth', 'admin', 'syntax', 'action', 'renderer', 'helper', 'remote', 'cli']; 1770316b84SAndreas Gohr 1870316b84SAndreas Gohr const TYPE_PLUGIN = 'plugin'; 1970316b84SAndreas Gohr const TYPE_TEMPLATE = 'template'; 2070316b84SAndreas Gohr 2170316b84SAndreas Gohr protected $type; 2270316b84SAndreas Gohr protected $base; 2370316b84SAndreas Gohr protected $author; 2470316b84SAndreas Gohr protected $desc; 2570316b84SAndreas Gohr protected $name; 2670316b84SAndreas Gohr protected $email; 2770316b84SAndreas Gohr protected $url; 2870316b84SAndreas Gohr protected $dir; 2970316b84SAndreas Gohr 3070316b84SAndreas Gohr /** @var array The files to be created in the form of [path => content] */ 3170316b84SAndreas Gohr protected $files = []; 3270316b84SAndreas Gohr 3370316b84SAndreas Gohr /** 3470316b84SAndreas Gohr * Initialize the skeletor from provided data 3570316b84SAndreas Gohr * 3670316b84SAndreas Gohr * @param string $type 3770316b84SAndreas Gohr * @param string $base 3870316b84SAndreas Gohr * @param string $desc 3970316b84SAndreas Gohr * @param string $author 4070316b84SAndreas Gohr * @param string $email 4170316b84SAndreas Gohr * @param string $name 4270316b84SAndreas Gohr * @param string $url 4370316b84SAndreas Gohr */ 4470316b84SAndreas Gohr public function __construct($type, $base, $desc, $author, $email, $name = '', $url = '') 4570316b84SAndreas Gohr { 4670316b84SAndreas Gohr $this->type = $type; 4770316b84SAndreas Gohr $this->base = $base; 4870316b84SAndreas Gohr $this->desc = $desc; 4970316b84SAndreas Gohr $this->author = $author; 5070316b84SAndreas Gohr $this->email = $email; 5170316b84SAndreas Gohr $this->name = $name ?: ucfirst($base . ' ' . $type); 5270316b84SAndreas Gohr 5370316b84SAndreas Gohr if ($type == self::TYPE_PLUGIN) { 5470316b84SAndreas Gohr $this->url = $url ?: 'https://www.dokuwiki.org/plugin:' . $base; 5570316b84SAndreas Gohr $this->dir = 'lib/plugins/' . $base; 5670316b84SAndreas Gohr } else { 5770316b84SAndreas Gohr $this->url = $url ?: 'https://www.dokuwiki.org/template:' . $base; 5870316b84SAndreas Gohr $this->dir = 'lib/tpl/' . $base; 5970316b84SAndreas Gohr } 6070316b84SAndreas Gohr } 6170316b84SAndreas Gohr 6270316b84SAndreas Gohr /** 6370316b84SAndreas Gohr * Create an instance using an existing plugin or template directory 6470316b84SAndreas Gohr * 6570316b84SAndreas Gohr * @param string $dir 6670316b84SAndreas Gohr * @return Skeletor 6770316b84SAndreas Gohr */ 6870316b84SAndreas Gohr static public function fromDir($dir) 6970316b84SAndreas Gohr { 7070316b84SAndreas Gohr if (file_exists($dir . '/plugin.info.txt')) { 7170316b84SAndreas Gohr $type = self::TYPE_PLUGIN; 7270316b84SAndreas Gohr } elseif (file_exists($dir . '/template.info.txt')) { 7370316b84SAndreas Gohr $type = self::TYPE_TEMPLATE; 7470316b84SAndreas Gohr } else { 7570316b84SAndreas Gohr throw new RuntimeException('Not a plugin or template directory'); 7670316b84SAndreas Gohr } 7770316b84SAndreas Gohr 7870316b84SAndreas Gohr $data = file($dir . '/' . $type . '.info.txt', FILE_IGNORE_NEW_LINES); 7970316b84SAndreas Gohr $data = array_map(function ($item) { 80dd6f86a0SAndreas Gohr return array_map('trim', sexplode(' ', $item, 2, '')); 8170316b84SAndreas Gohr }, $data); 8270316b84SAndreas Gohr $data = array_combine(array_column($data, 0), array_column($data, 1)); 8370316b84SAndreas Gohr 8470316b84SAndreas Gohr return new self($type, $data['base'], $data['desc'], $data['author'], $data['email'], $data['url']); 8570316b84SAndreas Gohr } 8670316b84SAndreas Gohr 8770316b84SAndreas Gohr /** 8870316b84SAndreas Gohr * Return the files to be created 8970316b84SAndreas Gohr * 9070316b84SAndreas Gohr * @return array [path => content] 9170316b84SAndreas Gohr */ 9270316b84SAndreas Gohr public function getFiles() 9370316b84SAndreas Gohr { 9470316b84SAndreas Gohr return $this->files; 9570316b84SAndreas Gohr } 9670316b84SAndreas Gohr 9770316b84SAndreas Gohr // region content creators 9870316b84SAndreas Gohr 9970316b84SAndreas Gohr /** 10070316b84SAndreas Gohr * Add the basic files to the plugin 10170316b84SAndreas Gohr */ 10270316b84SAndreas Gohr public function addBasics() 10370316b84SAndreas Gohr { 10470316b84SAndreas Gohr $this->loadSkeleton('info.txt', $this->type . '.info.txt'); 10570316b84SAndreas Gohr $this->loadSkeleton('README'); 10670316b84SAndreas Gohr $this->loadSkeleton('LICENSE'); 10701bb0631SAndreas Gohr $this->loadSkeleton('_gitattributes', '.gitattributes'); 10870316b84SAndreas Gohr } 10970316b84SAndreas Gohr 11070316b84SAndreas Gohr /** 11170316b84SAndreas Gohr * Add another component to the plugin 11270316b84SAndreas Gohr * 11370316b84SAndreas Gohr * @param string $type 11470316b84SAndreas Gohr * @param string $component 11570316b84SAndreas Gohr */ 11689e2f9d1SAndreas Gohr public function addComponent($type, $component = '', $options = []) 11770316b84SAndreas Gohr { 11870316b84SAndreas Gohr if ($this->type !== self::TYPE_PLUGIN) { 11970316b84SAndreas Gohr throw new RuntimeException('Components can only be added to plugins'); 12070316b84SAndreas Gohr } 12170316b84SAndreas Gohr 12270316b84SAndreas Gohr if (!in_array($type, self::PLUGIN_TYPES)) { 12370316b84SAndreas Gohr throw new RuntimeException('Invalid type ' . $type); 12470316b84SAndreas Gohr } 12570316b84SAndreas Gohr 12670316b84SAndreas Gohr $plugin = $this->base; 12770316b84SAndreas Gohr 12870316b84SAndreas Gohr if ($component) { 12970316b84SAndreas Gohr $path = $type . '/' . $component . '.php'; 13070316b84SAndreas Gohr $class = $type . '_plugin_' . $plugin . '_' . $component; 13170316b84SAndreas Gohr $self = 'plugin_' . $plugin . '_' . $component; 13270316b84SAndreas Gohr } else { 13370316b84SAndreas Gohr $path = $type . '.php'; 13470316b84SAndreas Gohr $class = $type . '_plugin_' . $plugin; 13570316b84SAndreas Gohr $self = 'plugin_' . $plugin; 13670316b84SAndreas Gohr } 13770316b84SAndreas Gohr 13889e2f9d1SAndreas Gohr if ($type === 'action') { 13989e2f9d1SAndreas Gohr $replacements = $this->actionReplacements($options); 14089e2f9d1SAndreas Gohr } 141cde324c2SAndreas Gohr if ($type === 'renderer' && isset($options[0]) && $options[0] === 'Doku_Renderer_xhtml') { 142cde324c2SAndreas Gohr $type = 'renderer_xhtml'; // different template then 143cde324c2SAndreas Gohr } 14489e2f9d1SAndreas Gohr 14570316b84SAndreas Gohr $replacements['@@PLUGIN_COMPONENT_NAME@@'] = $class; 14670316b84SAndreas Gohr $replacements['@@SYNTAX_COMPONENT_NAME@@'] = $self; 14770316b84SAndreas Gohr $this->loadSkeleton($type . '.php', $path, $replacements); 14870316b84SAndreas Gohr } 14970316b84SAndreas Gohr 15070316b84SAndreas Gohr /** 15170316b84SAndreas Gohr * Add test framework optionally with a specific test 15270316b84SAndreas Gohr * 15370316b84SAndreas Gohr * @param string $test Name of the Test to add 15470316b84SAndreas Gohr */ 15570316b84SAndreas Gohr public function addTest($test = '') 15670316b84SAndreas Gohr { 15753bec4caSAndreas Gohr // pick a random day and time for the cron job 15853bec4caSAndreas Gohr $cron = sprintf( 15953bec4caSAndreas Gohr '%d %d %d * *', 16053bec4caSAndreas Gohr random_int(0, 59), 16153bec4caSAndreas Gohr random_int(0, 23), 16253bec4caSAndreas Gohr random_int(1, 28) 16353bec4caSAndreas Gohr ); 16453bec4caSAndreas Gohr 16553bec4caSAndreas Gohr $test = ucfirst($test); 16653bec4caSAndreas Gohr $this->loadSkeleton('.github/workflows/dokuwiki.yml', '', ['@@CRON@@' => $cron]); 16770316b84SAndreas Gohr if ($test) { 16870316b84SAndreas Gohr $replacements = ['@@TEST@@' => $test]; 16970316b84SAndreas Gohr $this->loadSkeleton('_test/StandardTest.php', '_test/' . $test . 'Test.php', $replacements); 17070316b84SAndreas Gohr } else { 17170316b84SAndreas Gohr $this->loadSkeleton('_test/GeneralTest.php'); 17270316b84SAndreas Gohr } 17370316b84SAndreas Gohr } 17470316b84SAndreas Gohr 17570316b84SAndreas Gohr /** 17670316b84SAndreas Gohr * Add configuration 17770316b84SAndreas Gohr * 17870316b84SAndreas Gohr * @param bool $translate if true the settings language file will be be added, too 17970316b84SAndreas Gohr */ 18070316b84SAndreas Gohr public function addConf($translate = false) 18170316b84SAndreas Gohr { 18270316b84SAndreas Gohr $this->loadSkeleton('conf/default.php'); 18370316b84SAndreas Gohr $this->loadSkeleton('conf/metadata.php'); 18470316b84SAndreas Gohr 18570316b84SAndreas Gohr if ($translate) { 18670316b84SAndreas Gohr $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php'); 18770316b84SAndreas Gohr } 18870316b84SAndreas Gohr } 18970316b84SAndreas Gohr 19070316b84SAndreas Gohr /** 19170316b84SAndreas Gohr * Add language 19270316b84SAndreas Gohr * 19370316b84SAndreas Gohr * Currently only english is added, theoretically this could also copy over the keys from an 19470316b84SAndreas Gohr * existing english language file. 19570316b84SAndreas Gohr * 19670316b84SAndreas Gohr * @param bool $conf if true the settings language file will be be added, too 19770316b84SAndreas Gohr */ 19870316b84SAndreas Gohr public function addLang($conf = false) 19970316b84SAndreas Gohr { 20070316b84SAndreas Gohr $this->loadSkeleton('lang/lang.php', 'lang/en/lang.php'); 20170316b84SAndreas Gohr if ($conf) { 20270316b84SAndreas Gohr $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php'); 20370316b84SAndreas Gohr } 20470316b84SAndreas Gohr } 20570316b84SAndreas Gohr 20670316b84SAndreas Gohr // endregion 20770316b84SAndreas Gohr 20870316b84SAndreas Gohr 20970316b84SAndreas Gohr /** 21070316b84SAndreas Gohr * Prepare the string replacements 21170316b84SAndreas Gohr * 21270316b84SAndreas Gohr * @param array $replacements override defaults 21370316b84SAndreas Gohr * @return array 21470316b84SAndreas Gohr */ 21570316b84SAndreas Gohr protected function prepareReplacements($replacements = []) 21670316b84SAndreas Gohr { 21770316b84SAndreas Gohr // defaults 21870316b84SAndreas Gohr $data = [ 21970316b84SAndreas Gohr '@@AUTHOR_NAME@@' => $this->author, 22070316b84SAndreas Gohr '@@AUTHOR_MAIL@@' => $this->email, 22170316b84SAndreas Gohr '@@PLUGIN_NAME@@' => $this->base, // FIXME rename to @@PLUGIN_BASE@@ 22270316b84SAndreas Gohr '@@PLUGIN_DESC@@' => $this->desc, 22370316b84SAndreas Gohr '@@PLUGIN_URL@@' => $this->url, 22470316b84SAndreas Gohr '@@PLUGIN_TYPE@@' => $this->type, 22570316b84SAndreas Gohr '@@INSTALL_DIR@@' => ($this->type == self::TYPE_PLUGIN) ? 'plugins' : 'tpl', 22670316b84SAndreas Gohr '@@DATE@@' => date('Y-m-d'), 22770316b84SAndreas Gohr ]; 22870316b84SAndreas Gohr 22970316b84SAndreas Gohr // merge given overrides 23070316b84SAndreas Gohr return array_merge($data, $replacements); 23170316b84SAndreas Gohr } 23270316b84SAndreas Gohr 23370316b84SAndreas Gohr /** 23470316b84SAndreas Gohr * Replacements needed for action components. 23570316b84SAndreas Gohr * 23689e2f9d1SAndreas Gohr * @param string[] $event Event names to handle 23770316b84SAndreas Gohr * @return string[] 23870316b84SAndreas Gohr */ 23989e2f9d1SAndreas Gohr protected function actionReplacements($events = []) 24070316b84SAndreas Gohr { 24189e2f9d1SAndreas Gohr if (!$events) $events = ['EXAMPLE_EVENT']; 24289e2f9d1SAndreas Gohr 24389e2f9d1SAndreas Gohr $register = ''; 24489e2f9d1SAndreas Gohr $handler = ''; 24589e2f9d1SAndreas Gohr 24689e2f9d1SAndreas Gohr $template = file_get_contents(__DIR__ . '/skel/action_handler.php'); 24789e2f9d1SAndreas Gohr 24889e2f9d1SAndreas Gohr foreach ($events as $event) { 24970316b84SAndreas Gohr $event = strtoupper($event); 25070316b84SAndreas Gohr $fn = 'handle' . str_replace('_', '', ucwords(strtolower($event), '_')); 25189e2f9d1SAndreas Gohr 25289e2f9d1SAndreas Gohr $register .= ' $controller->register_hook(\'' . $event . 25389e2f9d1SAndreas Gohr '\', \'AFTER|BEFORE\', $this, \'' . $fn . '\');' . "\n"; 25489e2f9d1SAndreas Gohr 25589e2f9d1SAndreas Gohr $handler .= str_replace(['@@EVENT@@', '@@HANDLER@@'], [$event, $fn], $template); 25689e2f9d1SAndreas Gohr } 25770316b84SAndreas Gohr 25870316b84SAndreas Gohr return [ 259*16cb46d5SAnna Dabrowska '@@REGISTER@@' => rtrim($register, "\n"), 260*16cb46d5SAnna Dabrowska '@@HANDLERS@@' => rtrim($handler, "\n"), 26170316b84SAndreas Gohr ]; 26270316b84SAndreas Gohr } 26370316b84SAndreas Gohr 26470316b84SAndreas Gohr /** 26570316b84SAndreas Gohr * Load a skeleton file, do the replacements and add it to the list of files 26670316b84SAndreas Gohr * 26770316b84SAndreas Gohr * @param string $skel Skeleton relative to the skel dir 26870316b84SAndreas Gohr * @param string $target File name in the final plugin/template, empty for same as skeleton 26970316b84SAndreas Gohr * @param array $replacements Non-default replacements to use 27070316b84SAndreas Gohr */ 27170316b84SAndreas Gohr protected function loadSkeleton($skel, $target = '', $replacements = []) 27270316b84SAndreas Gohr { 27370316b84SAndreas Gohr $replacements = $this->prepareReplacements($replacements); 27470316b84SAndreas Gohr if (!$target) $target = $skel; 27570316b84SAndreas Gohr 27670316b84SAndreas Gohr 27770316b84SAndreas Gohr $file = __DIR__ . '/skel/' . $skel; 27870316b84SAndreas Gohr if (!file_exists($file)) { 27970316b84SAndreas Gohr throw new RuntimeException('Skeleton file not found: ' . $skel); 28070316b84SAndreas Gohr } 28170316b84SAndreas Gohr $content = file_get_contents($file); 28270316b84SAndreas Gohr $this->files[$target] = str_replace( 28370316b84SAndreas Gohr array_keys($replacements), 28470316b84SAndreas Gohr array_values($replacements), 28570316b84SAndreas Gohr $content 28670316b84SAndreas Gohr ); 28770316b84SAndreas Gohr } 28870316b84SAndreas Gohr} 289