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 16*fe060d0dSAndreas Gohr public const PLUGIN_TYPES = ['auth', 'admin', 'syntax', 'action', 'renderer', 'helper', 'remote', 'cli']; 1770316b84SAndreas Gohr 18*fe060d0dSAndreas Gohr public const TYPE_PLUGIN = 'plugin'; 19*fe060d0dSAndreas Gohr public 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 */ 68*fe060d0dSAndreas Gohr public static 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); 79*fe060d0dSAndreas Gohr $data = array_map(fn($item) => array_map(trim(...), sexplode(' ', $item, 2, '')), $data); 8070316b84SAndreas Gohr $data = array_combine(array_column($data, 0), array_column($data, 1)); 8170316b84SAndreas Gohr 8270316b84SAndreas Gohr return new self($type, $data['base'], $data['desc'], $data['author'], $data['email'], $data['url']); 8370316b84SAndreas Gohr } 8470316b84SAndreas Gohr 8570316b84SAndreas Gohr /** 8670316b84SAndreas Gohr * Return the files to be created 8770316b84SAndreas Gohr * 8870316b84SAndreas Gohr * @return array [path => content] 8970316b84SAndreas Gohr */ 9070316b84SAndreas Gohr public function getFiles() 9170316b84SAndreas Gohr { 9270316b84SAndreas Gohr return $this->files; 9370316b84SAndreas Gohr } 9470316b84SAndreas Gohr 9570316b84SAndreas Gohr // region content creators 9670316b84SAndreas Gohr 9770316b84SAndreas Gohr /** 9870316b84SAndreas Gohr * Add the basic files to the plugin 9970316b84SAndreas Gohr */ 10070316b84SAndreas Gohr public function addBasics() 10170316b84SAndreas Gohr { 10270316b84SAndreas Gohr $this->loadSkeleton('info.txt', $this->type . '.info.txt'); 10370316b84SAndreas Gohr $this->loadSkeleton('README'); 10470316b84SAndreas Gohr $this->loadSkeleton('LICENSE'); 10501bb0631SAndreas Gohr $this->loadSkeleton('_gitattributes', '.gitattributes'); 10670316b84SAndreas Gohr } 10770316b84SAndreas Gohr 10870316b84SAndreas Gohr /** 10970316b84SAndreas Gohr * Add another component to the plugin 11070316b84SAndreas Gohr * 11170316b84SAndreas Gohr * @param string $type 11270316b84SAndreas Gohr * @param string $component 11370316b84SAndreas Gohr */ 11489e2f9d1SAndreas Gohr public function addComponent($type, $component = '', $options = []) 11570316b84SAndreas Gohr { 11670316b84SAndreas Gohr if ($this->type !== self::TYPE_PLUGIN) { 11770316b84SAndreas Gohr throw new RuntimeException('Components can only be added to plugins'); 11870316b84SAndreas Gohr } 11970316b84SAndreas Gohr 12070316b84SAndreas Gohr if (!in_array($type, self::PLUGIN_TYPES)) { 12170316b84SAndreas Gohr throw new RuntimeException('Invalid type ' . $type); 12270316b84SAndreas Gohr } 12370316b84SAndreas Gohr 12470316b84SAndreas Gohr $plugin = $this->base; 12570316b84SAndreas Gohr 12670316b84SAndreas Gohr if ($component) { 12770316b84SAndreas Gohr $path = $type . '/' . $component . '.php'; 12870316b84SAndreas Gohr $class = $type . '_plugin_' . $plugin . '_' . $component; 12970316b84SAndreas Gohr $self = 'plugin_' . $plugin . '_' . $component; 13070316b84SAndreas Gohr } else { 13170316b84SAndreas Gohr $path = $type . '.php'; 13270316b84SAndreas Gohr $class = $type . '_plugin_' . $plugin; 13370316b84SAndreas Gohr $self = 'plugin_' . $plugin; 13470316b84SAndreas Gohr } 13570316b84SAndreas Gohr 13689e2f9d1SAndreas Gohr if ($type === 'action') { 13789e2f9d1SAndreas Gohr $replacements = $this->actionReplacements($options); 13889e2f9d1SAndreas Gohr } 139cde324c2SAndreas Gohr if ($type === 'renderer' && isset($options[0]) && $options[0] === 'Doku_Renderer_xhtml') { 140cde324c2SAndreas Gohr $type = 'renderer_xhtml'; // different template then 141cde324c2SAndreas Gohr } 14289e2f9d1SAndreas Gohr 14370316b84SAndreas Gohr $replacements['@@PLUGIN_COMPONENT_NAME@@'] = $class; 14470316b84SAndreas Gohr $replacements['@@SYNTAX_COMPONENT_NAME@@'] = $self; 14570316b84SAndreas Gohr $this->loadSkeleton($type . '.php', $path, $replacements); 14670316b84SAndreas Gohr } 14770316b84SAndreas Gohr 14870316b84SAndreas Gohr /** 14970316b84SAndreas Gohr * Add test framework optionally with a specific test 15070316b84SAndreas Gohr * 15170316b84SAndreas Gohr * @param string $test Name of the Test to add 15270316b84SAndreas Gohr */ 15370316b84SAndreas Gohr public function addTest($test = '') 15470316b84SAndreas Gohr { 15553bec4caSAndreas Gohr // pick a random day and time for the cron job 15653bec4caSAndreas Gohr $cron = sprintf( 15753bec4caSAndreas Gohr '%d %d %d * *', 15853bec4caSAndreas Gohr random_int(0, 59), 15953bec4caSAndreas Gohr random_int(0, 23), 16053bec4caSAndreas Gohr random_int(1, 28) 16153bec4caSAndreas Gohr ); 16253bec4caSAndreas Gohr 16353bec4caSAndreas Gohr $test = ucfirst($test); 16453bec4caSAndreas Gohr $this->loadSkeleton('.github/workflows/dokuwiki.yml', '', ['@@CRON@@' => $cron]); 16570316b84SAndreas Gohr if ($test) { 16670316b84SAndreas Gohr $replacements = ['@@TEST@@' => $test]; 16770316b84SAndreas Gohr $this->loadSkeleton('_test/StandardTest.php', '_test/' . $test . 'Test.php', $replacements); 16870316b84SAndreas Gohr } else { 16970316b84SAndreas Gohr $this->loadSkeleton('_test/GeneralTest.php'); 17070316b84SAndreas Gohr } 17170316b84SAndreas Gohr } 17270316b84SAndreas Gohr 17370316b84SAndreas Gohr /** 17470316b84SAndreas Gohr * Add configuration 17570316b84SAndreas Gohr * 17670316b84SAndreas Gohr * @param bool $translate if true the settings language file will be be added, too 17770316b84SAndreas Gohr */ 17870316b84SAndreas Gohr public function addConf($translate = false) 17970316b84SAndreas Gohr { 18070316b84SAndreas Gohr $this->loadSkeleton('conf/default.php'); 18170316b84SAndreas Gohr $this->loadSkeleton('conf/metadata.php'); 18270316b84SAndreas Gohr 18370316b84SAndreas Gohr if ($translate) { 18470316b84SAndreas Gohr $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php'); 18570316b84SAndreas Gohr } 18670316b84SAndreas Gohr } 18770316b84SAndreas Gohr 18870316b84SAndreas Gohr /** 18970316b84SAndreas Gohr * Add language 19070316b84SAndreas Gohr * 19170316b84SAndreas Gohr * Currently only english is added, theoretically this could also copy over the keys from an 19270316b84SAndreas Gohr * existing english language file. 19370316b84SAndreas Gohr * 19470316b84SAndreas Gohr * @param bool $conf if true the settings language file will be be added, too 19570316b84SAndreas Gohr */ 19670316b84SAndreas Gohr public function addLang($conf = false) 19770316b84SAndreas Gohr { 19870316b84SAndreas Gohr $this->loadSkeleton('lang/lang.php', 'lang/en/lang.php'); 19970316b84SAndreas Gohr if ($conf) { 20070316b84SAndreas Gohr $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php'); 20170316b84SAndreas Gohr } 20270316b84SAndreas Gohr } 20370316b84SAndreas Gohr 204719b4841SAndreas Gohr /** 205719b4841SAndreas Gohr * Add the AGENTS.md file 206719b4841SAndreas Gohr * 207719b4841SAndreas Gohr * @return void 208719b4841SAndreas Gohr */ 209719b4841SAndreas Gohr public function addAgents() 210719b4841SAndreas Gohr { 211719b4841SAndreas Gohr $this->loadSkeleton('AGENTS.md'); 212719b4841SAndreas Gohr } 213719b4841SAndreas Gohr 21470316b84SAndreas Gohr // endregion 21570316b84SAndreas Gohr 21670316b84SAndreas Gohr 21770316b84SAndreas Gohr /** 21870316b84SAndreas Gohr * Prepare the string replacements 21970316b84SAndreas Gohr * 22070316b84SAndreas Gohr * @param array $replacements override defaults 22170316b84SAndreas Gohr * @return array 22270316b84SAndreas Gohr */ 22370316b84SAndreas Gohr protected function prepareReplacements($replacements = []) 22470316b84SAndreas Gohr { 22570316b84SAndreas Gohr // defaults 22670316b84SAndreas Gohr $data = [ 22770316b84SAndreas Gohr '@@AUTHOR_NAME@@' => $this->author, 22870316b84SAndreas Gohr '@@AUTHOR_MAIL@@' => $this->email, 22970316b84SAndreas Gohr '@@PLUGIN_NAME@@' => $this->base, // FIXME rename to @@PLUGIN_BASE@@ 23070316b84SAndreas Gohr '@@PLUGIN_DESC@@' => $this->desc, 23170316b84SAndreas Gohr '@@PLUGIN_URL@@' => $this->url, 23270316b84SAndreas Gohr '@@PLUGIN_TYPE@@' => $this->type, 23370316b84SAndreas Gohr '@@INSTALL_DIR@@' => ($this->type == self::TYPE_PLUGIN) ? 'plugins' : 'tpl', 23470316b84SAndreas Gohr '@@DATE@@' => date('Y-m-d'), 23570316b84SAndreas Gohr ]; 23670316b84SAndreas Gohr 23770316b84SAndreas Gohr // merge given overrides 23870316b84SAndreas Gohr return array_merge($data, $replacements); 23970316b84SAndreas Gohr } 24070316b84SAndreas Gohr 24170316b84SAndreas Gohr /** 24270316b84SAndreas Gohr * Replacements needed for action components. 24370316b84SAndreas Gohr * 24489e2f9d1SAndreas Gohr * @param string[] $event Event names to handle 24570316b84SAndreas Gohr * @return string[] 24670316b84SAndreas Gohr */ 24789e2f9d1SAndreas Gohr protected function actionReplacements($events = []) 24870316b84SAndreas Gohr { 24989e2f9d1SAndreas Gohr if (!$events) $events = ['EXAMPLE_EVENT']; 25089e2f9d1SAndreas Gohr 25189e2f9d1SAndreas Gohr $register = ''; 25289e2f9d1SAndreas Gohr $handler = ''; 25389e2f9d1SAndreas Gohr 25489e2f9d1SAndreas Gohr $template = file_get_contents(__DIR__ . '/skel/action_handler.php'); 25589e2f9d1SAndreas Gohr 25689e2f9d1SAndreas Gohr foreach ($events as $event) { 25770316b84SAndreas Gohr $event = strtoupper($event); 25870316b84SAndreas Gohr $fn = 'handle' . str_replace('_', '', ucwords(strtolower($event), '_')); 25989e2f9d1SAndreas Gohr 26089e2f9d1SAndreas Gohr $register .= ' $controller->register_hook(\'' . $event . 26189e2f9d1SAndreas Gohr '\', \'AFTER|BEFORE\', $this, \'' . $fn . '\');' . "\n"; 26289e2f9d1SAndreas Gohr 26389e2f9d1SAndreas Gohr $handler .= str_replace(['@@EVENT@@', '@@HANDLER@@'], [$event, $fn], $template); 26489e2f9d1SAndreas Gohr } 26570316b84SAndreas Gohr 26670316b84SAndreas Gohr return [ 26716cb46d5SAnna Dabrowska '@@REGISTER@@' => rtrim($register, "\n"), 26816cb46d5SAnna Dabrowska '@@HANDLERS@@' => rtrim($handler, "\n"), 26970316b84SAndreas Gohr ]; 27070316b84SAndreas Gohr } 27170316b84SAndreas Gohr 27270316b84SAndreas Gohr /** 27370316b84SAndreas Gohr * Load a skeleton file, do the replacements and add it to the list of files 27470316b84SAndreas Gohr * 27570316b84SAndreas Gohr * @param string $skel Skeleton relative to the skel dir 27670316b84SAndreas Gohr * @param string $target File name in the final plugin/template, empty for same as skeleton 27770316b84SAndreas Gohr * @param array $replacements Non-default replacements to use 27870316b84SAndreas Gohr */ 27970316b84SAndreas Gohr protected function loadSkeleton($skel, $target = '', $replacements = []) 28070316b84SAndreas Gohr { 28170316b84SAndreas Gohr $replacements = $this->prepareReplacements($replacements); 28270316b84SAndreas Gohr if (!$target) $target = $skel; 28370316b84SAndreas Gohr 28470316b84SAndreas Gohr 28570316b84SAndreas Gohr $file = __DIR__ . '/skel/' . $skel; 28670316b84SAndreas Gohr if (!file_exists($file)) { 28770316b84SAndreas Gohr throw new RuntimeException('Skeleton file not found: ' . $skel); 28870316b84SAndreas Gohr } 28970316b84SAndreas Gohr $content = file_get_contents($file); 29070316b84SAndreas Gohr $this->files[$target] = str_replace( 29170316b84SAndreas Gohr array_keys($replacements), 29270316b84SAndreas Gohr array_values($replacements), 29370316b84SAndreas Gohr $content 29470316b84SAndreas Gohr ); 29570316b84SAndreas Gohr } 29670316b84SAndreas Gohr} 297