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) { 8070316b84SAndreas Gohr return array_map('trim', explode(' ', $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'); 10770316b84SAndreas Gohr $this->loadSkeleton('.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 */ 116*89e2f9d1SAndreas 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 138*89e2f9d1SAndreas Gohr if($type === 'action') { 139*89e2f9d1SAndreas Gohr $replacements = $this->actionReplacements($options); 140*89e2f9d1SAndreas Gohr } 141*89e2f9d1SAndreas Gohr 14270316b84SAndreas Gohr $replacements['@@PLUGIN_COMPONENT_NAME@@'] = $class; 14370316b84SAndreas Gohr $replacements['@@SYNTAX_COMPONENT_NAME@@'] = $self; 14470316b84SAndreas Gohr $this->loadSkeleton($type . '.php', $path, $replacements); 14570316b84SAndreas Gohr } 14670316b84SAndreas Gohr 14770316b84SAndreas Gohr /** 14870316b84SAndreas Gohr * Add test framework optionally with a specific test 14970316b84SAndreas Gohr * 15070316b84SAndreas Gohr * @param string $test Name of the Test to add 15170316b84SAndreas Gohr */ 15270316b84SAndreas Gohr public function addTest($test = '') 15370316b84SAndreas Gohr { 15470316b84SAndreas Gohr $test = ucfirst(strtolower($test)); 15570316b84SAndreas Gohr $this->loadSkeleton('.github/workflows/phpTestLinux.yml'); 15670316b84SAndreas Gohr if ($test) { 15770316b84SAndreas Gohr $replacements = ['@@TEST@@' => $test]; 15870316b84SAndreas Gohr $this->loadSkeleton('_test/StandardTest.php', '_test/' . $test . 'Test.php', $replacements); 15970316b84SAndreas Gohr } else { 16070316b84SAndreas Gohr $this->loadSkeleton('_test/GeneralTest.php'); 16170316b84SAndreas Gohr } 16270316b84SAndreas Gohr } 16370316b84SAndreas Gohr 16470316b84SAndreas Gohr /** 16570316b84SAndreas Gohr * Add configuration 16670316b84SAndreas Gohr * 16770316b84SAndreas Gohr * @param bool $translate if true the settings language file will be be added, too 16870316b84SAndreas Gohr */ 16970316b84SAndreas Gohr public function addConf($translate = false) 17070316b84SAndreas Gohr { 17170316b84SAndreas Gohr $this->loadSkeleton('conf/default.php'); 17270316b84SAndreas Gohr $this->loadSkeleton('conf/metadata.php'); 17370316b84SAndreas Gohr 17470316b84SAndreas Gohr if ($translate) { 17570316b84SAndreas Gohr $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php'); 17670316b84SAndreas Gohr } 17770316b84SAndreas Gohr } 17870316b84SAndreas Gohr 17970316b84SAndreas Gohr /** 18070316b84SAndreas Gohr * Add language 18170316b84SAndreas Gohr * 18270316b84SAndreas Gohr * Currently only english is added, theoretically this could also copy over the keys from an 18370316b84SAndreas Gohr * existing english language file. 18470316b84SAndreas Gohr * 18570316b84SAndreas Gohr * @param bool $conf if true the settings language file will be be added, too 18670316b84SAndreas Gohr */ 18770316b84SAndreas Gohr public function addLang($conf = false) 18870316b84SAndreas Gohr { 18970316b84SAndreas Gohr $this->loadSkeleton('lang/lang.php', 'lang/en/lang.php'); 19070316b84SAndreas Gohr if ($conf) { 19170316b84SAndreas Gohr $this->loadSkeleton('lang/settings.php', 'lang/en/settings.php'); 19270316b84SAndreas Gohr } 19370316b84SAndreas Gohr } 19470316b84SAndreas Gohr 19570316b84SAndreas Gohr // endregion 19670316b84SAndreas Gohr 19770316b84SAndreas Gohr 19870316b84SAndreas Gohr /** 19970316b84SAndreas Gohr * Prepare the string replacements 20070316b84SAndreas Gohr * 20170316b84SAndreas Gohr * @param array $replacements override defaults 20270316b84SAndreas Gohr * @return array 20370316b84SAndreas Gohr */ 20470316b84SAndreas Gohr protected function prepareReplacements($replacements = []) 20570316b84SAndreas Gohr { 20670316b84SAndreas Gohr // defaults 20770316b84SAndreas Gohr $data = [ 20870316b84SAndreas Gohr '@@AUTHOR_NAME@@' => $this->author, 20970316b84SAndreas Gohr '@@AUTHOR_MAIL@@' => $this->email, 21070316b84SAndreas Gohr '@@PLUGIN_NAME@@' => $this->base, // FIXME rename to @@PLUGIN_BASE@@ 21170316b84SAndreas Gohr '@@PLUGIN_DESC@@' => $this->desc, 21270316b84SAndreas Gohr '@@PLUGIN_URL@@' => $this->url, 21370316b84SAndreas Gohr '@@PLUGIN_TYPE@@' => $this->type, 21470316b84SAndreas Gohr '@@INSTALL_DIR@@' => ($this->type == self::TYPE_PLUGIN) ? 'plugins' : 'tpl', 21570316b84SAndreas Gohr '@@DATE@@' => date('Y-m-d'), 21670316b84SAndreas Gohr ]; 21770316b84SAndreas Gohr 21870316b84SAndreas Gohr // merge given overrides 21970316b84SAndreas Gohr return array_merge($data, $replacements); 22070316b84SAndreas Gohr } 22170316b84SAndreas Gohr 22270316b84SAndreas Gohr /** 22370316b84SAndreas Gohr * Replacements needed for action components. 22470316b84SAndreas Gohr * 225*89e2f9d1SAndreas Gohr * @param string[] $event Event names to handle 22670316b84SAndreas Gohr * @return string[] 22770316b84SAndreas Gohr */ 228*89e2f9d1SAndreas Gohr protected function actionReplacements($events = []) 22970316b84SAndreas Gohr { 230*89e2f9d1SAndreas Gohr if (!$events) $events = ['EXAMPLE_EVENT']; 231*89e2f9d1SAndreas Gohr 232*89e2f9d1SAndreas Gohr $register = ''; 233*89e2f9d1SAndreas Gohr $handler = ''; 234*89e2f9d1SAndreas Gohr 235*89e2f9d1SAndreas Gohr $template = file_get_contents(__DIR__ . '/skel/action_handler.php'); 236*89e2f9d1SAndreas Gohr 237*89e2f9d1SAndreas Gohr foreach ($events as $event) { 23870316b84SAndreas Gohr $event = strtoupper($event); 23970316b84SAndreas Gohr $fn = 'handle' . str_replace('_', '', ucwords(strtolower($event), '_')); 240*89e2f9d1SAndreas Gohr 241*89e2f9d1SAndreas Gohr $register .= ' $controller->register_hook(\'' . $event . 242*89e2f9d1SAndreas Gohr '\', \'AFTER|BEFORE\', $this, \'' . $fn . '\');'. "\n"; 243*89e2f9d1SAndreas Gohr 244*89e2f9d1SAndreas Gohr $handler .= str_replace(['@@EVENT@@','@@HANDLER@@'], [$event, $fn], $template); 245*89e2f9d1SAndreas Gohr } 24670316b84SAndreas Gohr 24770316b84SAndreas Gohr return [ 248*89e2f9d1SAndreas Gohr '@@REGISTER@@' => $register, 24970316b84SAndreas Gohr '@@HANDLERS@@' => $handler, 25070316b84SAndreas Gohr ]; 25170316b84SAndreas Gohr } 25270316b84SAndreas Gohr 25370316b84SAndreas Gohr /** 25470316b84SAndreas Gohr * Load a skeleton file, do the replacements and add it to the list of files 25570316b84SAndreas Gohr * 25670316b84SAndreas Gohr * @param string $skel Skeleton relative to the skel dir 25770316b84SAndreas Gohr * @param string $target File name in the final plugin/template, empty for same as skeleton 25870316b84SAndreas Gohr * @param array $replacements Non-default replacements to use 25970316b84SAndreas Gohr */ 26070316b84SAndreas Gohr protected function loadSkeleton($skel, $target = '', $replacements = []) 26170316b84SAndreas Gohr { 26270316b84SAndreas Gohr $replacements = $this->prepareReplacements($replacements); 26370316b84SAndreas Gohr if (!$target) $target = $skel; 26470316b84SAndreas Gohr 26570316b84SAndreas Gohr 26670316b84SAndreas Gohr $file = __DIR__ . '/skel/' . $skel; 26770316b84SAndreas Gohr if (!file_exists($file)) { 26870316b84SAndreas Gohr throw new RuntimeException('Skeleton file not found: ' . $skel); 26970316b84SAndreas Gohr } 27070316b84SAndreas Gohr $content = file_get_contents($file); 27170316b84SAndreas Gohr $this->files[$target] = str_replace( 27270316b84SAndreas Gohr array_keys($replacements), 27370316b84SAndreas Gohr array_values($replacements), 27470316b84SAndreas Gohr $content 27570316b84SAndreas Gohr ); 27670316b84SAndreas Gohr } 27770316b84SAndreas Gohr} 278