Loading app/Config/Autoload.php +1 −1 Original line number Diff line number Diff line Loading @@ -114,7 +114,7 @@ class Autoload extends AutoloadConfig public function __construct() { // load plugins namespaces $pluginsPaths = glob(ROOTPATH . '/plugins/*', GLOB_ONLYDIR); $pluginsPaths = glob(ROOTPATH . '/plugins/*', GLOB_ONLYDIR | GLOB_NOSORT); if (! $pluginsPaths) { $pluginsPaths = []; Loading modules/Plugins/BasePlugin.php +57 −8 Original line number Diff line number Diff line Loading @@ -20,7 +20,9 @@ use RuntimeException; * @property string $license * @property string $compatible * @property string[] $keywords * @property string[] $hooks * @property string $iconSrc * @property array{settings:array{key:string,name:string,description:string}[],podcast:array{key:string,name:string,description:string}[],episode:array{key:string,name:string,description:string}[]} $options */ abstract class BasePlugin implements PluginInterface { Loading Loading @@ -76,6 +78,11 @@ abstract class BasePlugin implements PluginInterface return $this->active; } final public function isHookDeclared(string $name): bool { return in_array($name, $this->hooks, true); } final public function getKey(): string { return $this->key; Loading Loading @@ -138,6 +145,31 @@ abstract class BasePlugin implements PluginInterface throw new RuntimeException('manifest.json is not a valid JSON', 1); } $validation = service('validation'); if (array_key_exists('options', $manifest)) { $optionRules = [ 'key' => 'required|alpha_numeric', 'name' => 'required|max_length[32]', 'description' => 'permit_empty|max_length[128]', ]; $defaultOption = [ 'key' => '', 'name' => '', 'description' => '', ]; $validation->setRules($optionRules); foreach ($manifest['options'] as $key => $options) { foreach ($options as $key2 => $option) { $manifest['options'][$key][$key2] = array_merge($defaultOption, $option); if (! $validation->run($manifest['options'][$key][$key2])) { dd($this->key, $manifest['options'][$key][$key2], $validation->getErrors()); } } } } $rules = [ 'name' => 'required|max_length[32]', 'version' => 'required|regex_match[/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/]', Loading @@ -145,22 +177,39 @@ abstract class BasePlugin implements PluginInterface 'description' => 'max_length[128]', 'releaseDate' => 'valid_date[Y-m-d]', 'license' => 'in_list[MIT]', 'author.name' => 'max_length[32]', 'author.email' => 'valid_email', 'author.url' => 'valid_url_strict', 'author.name' => 'permit_empty|max_length[32]', 'author.email' => 'permit_empty|valid_email', 'author.url' => 'permit_empty|valid_url_strict', 'website' => 'valid_url_strict', 'keywords.*' => 'in_list[seo,podcasting20,analytics]', 'keywords.*' => 'permit_empty|in_list[seo,podcasting20,analytics]', 'hooks.*' => 'permit_empty|in_list[' . implode(',', Plugins::HOOKS) . ']', 'options' => 'permit_empty', ]; $validation = service('validation'); $validation->setRules($rules); if (! $validation->run($manifest)) { dd($validation->getErrors()); } dd($this->key, $manifest, $validation->getErrors()); } $defaultAttributes = [ 'description' => '', 'releaseDate' => '', 'license' => '', 'author' => [], 'website' => '', 'hooks' => [], 'keywords' => [], 'options' => [ 'settings' => [], 'podcast' => [], 'episode' => [], ], ]; $validated = $validation->getValidated(); return $validation->getValidated(); return array_merge_recursive_distinct($defaultAttributes, $validated); } private function loadIcon(string $path): string Loading modules/Plugins/Config/Routes.php +8 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,14 @@ $routes->group( 'as' => 'plugins-installed', 'filter' => 'permission:plugins.manage', ]); $routes->get('(:segment)', 'PluginsController::settings/$1', [ 'as' => 'plugins-settings', 'filter' => 'permission:plugins.manage', ]); $routes->post('(:segment)', 'PluginsController::settingsAction/$1', [ 'as' => 'plugins-settings-action', 'filter' => 'permission:plugins.manage', ]); $routes->post('activate/(:segment)', 'PluginsController::activate/$1', [ 'as' => 'plugins-activate', 'filter' => 'permission:plugins.manage', Loading modules/Plugins/Controllers/PluginsController.php +38 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Modules\Plugins\Controllers; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\HTTP\RedirectResponse; use Modules\Admin\Controllers\BaseController; use Modules\Plugins\Plugins; Loading @@ -30,6 +31,43 @@ class PluginsController extends BaseController ]); } public function settings(string $pluginKey): string { /** @var Plugins $plugins */ $plugins = service('plugins'); $plugin = $plugins->getPlugin($pluginKey); if ($plugin === null) { throw PageNotFoundException::forPageNotFound(); } helper('form'); return view('plugins/settings', [ 'plugin' => $plugin, ]); } public function settingsAction(string $pluginKey): RedirectResponse { /** @var Plugins $plugins */ $plugins = service('plugins'); $plugin = $plugins->getPlugin($pluginKey); if ($plugin === null) { throw PageNotFoundException::forPageNotFound(); } foreach ($plugin->options['settings'] as $option) { $optionKey = $option['key']; $optionValue = $this->request->getPost($optionKey); $plugins->setOption($pluginKey, $optionKey, $optionValue); } return redirect()->back(); } public function activate(string $pluginKey): RedirectResponse { service('plugins')->activate($pluginKey); Loading modules/Plugins/Helpers/plugins_helper.php +42 −0 Original line number Diff line number Diff line Loading @@ -22,3 +22,45 @@ if (! function_exists('set_plugin_option')) { ->set($key, $value, $context); } } if (! function_exists('array_merge_recursive_distinct')) { /** * array_merge_recursive does indeed merge arrays, but it converts values with duplicate * keys to arrays rather than overwriting the value in the first array with the duplicate * value in the second array, as array_merge does. I.e., with array_merge_recursive, * this happens (documented behavior): * * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value')); * => array('key' => array('org value', 'new value')); * * array_merge_recursive_distinct does not change the datatypes of the values in the arrays. * Matching keys' values in the second array overwrite those in the first array, as is the * case with array_merge, i.e.: * * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value')); * => array('key' => array('new value')); * * Parameters are passed by reference, though only for performance reasons. They're not * altered by this function. * * from https://www.php.net/manual/en/function.array-merge-recursive.php#92195 * * @param array<mixed> $array1 * @param array<mixed> $array2 * @return array<mixed> */ function array_merge_recursive_distinct(array &$array1, array &$array2): array { $merged = $array1; foreach ($array2 as $key => &$value) { if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { $merged[$key] = array_merge_recursive_distinct($merged[$key], $value); } else { $merged[$key] = $value; } } return $merged; } } Loading
app/Config/Autoload.php +1 −1 Original line number Diff line number Diff line Loading @@ -114,7 +114,7 @@ class Autoload extends AutoloadConfig public function __construct() { // load plugins namespaces $pluginsPaths = glob(ROOTPATH . '/plugins/*', GLOB_ONLYDIR); $pluginsPaths = glob(ROOTPATH . '/plugins/*', GLOB_ONLYDIR | GLOB_NOSORT); if (! $pluginsPaths) { $pluginsPaths = []; Loading
modules/Plugins/BasePlugin.php +57 −8 Original line number Diff line number Diff line Loading @@ -20,7 +20,9 @@ use RuntimeException; * @property string $license * @property string $compatible * @property string[] $keywords * @property string[] $hooks * @property string $iconSrc * @property array{settings:array{key:string,name:string,description:string}[],podcast:array{key:string,name:string,description:string}[],episode:array{key:string,name:string,description:string}[]} $options */ abstract class BasePlugin implements PluginInterface { Loading Loading @@ -76,6 +78,11 @@ abstract class BasePlugin implements PluginInterface return $this->active; } final public function isHookDeclared(string $name): bool { return in_array($name, $this->hooks, true); } final public function getKey(): string { return $this->key; Loading Loading @@ -138,6 +145,31 @@ abstract class BasePlugin implements PluginInterface throw new RuntimeException('manifest.json is not a valid JSON', 1); } $validation = service('validation'); if (array_key_exists('options', $manifest)) { $optionRules = [ 'key' => 'required|alpha_numeric', 'name' => 'required|max_length[32]', 'description' => 'permit_empty|max_length[128]', ]; $defaultOption = [ 'key' => '', 'name' => '', 'description' => '', ]; $validation->setRules($optionRules); foreach ($manifest['options'] as $key => $options) { foreach ($options as $key2 => $option) { $manifest['options'][$key][$key2] = array_merge($defaultOption, $option); if (! $validation->run($manifest['options'][$key][$key2])) { dd($this->key, $manifest['options'][$key][$key2], $validation->getErrors()); } } } } $rules = [ 'name' => 'required|max_length[32]', 'version' => 'required|regex_match[/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/]', Loading @@ -145,22 +177,39 @@ abstract class BasePlugin implements PluginInterface 'description' => 'max_length[128]', 'releaseDate' => 'valid_date[Y-m-d]', 'license' => 'in_list[MIT]', 'author.name' => 'max_length[32]', 'author.email' => 'valid_email', 'author.url' => 'valid_url_strict', 'author.name' => 'permit_empty|max_length[32]', 'author.email' => 'permit_empty|valid_email', 'author.url' => 'permit_empty|valid_url_strict', 'website' => 'valid_url_strict', 'keywords.*' => 'in_list[seo,podcasting20,analytics]', 'keywords.*' => 'permit_empty|in_list[seo,podcasting20,analytics]', 'hooks.*' => 'permit_empty|in_list[' . implode(',', Plugins::HOOKS) . ']', 'options' => 'permit_empty', ]; $validation = service('validation'); $validation->setRules($rules); if (! $validation->run($manifest)) { dd($validation->getErrors()); } dd($this->key, $manifest, $validation->getErrors()); } $defaultAttributes = [ 'description' => '', 'releaseDate' => '', 'license' => '', 'author' => [], 'website' => '', 'hooks' => [], 'keywords' => [], 'options' => [ 'settings' => [], 'podcast' => [], 'episode' => [], ], ]; $validated = $validation->getValidated(); return $validation->getValidated(); return array_merge_recursive_distinct($defaultAttributes, $validated); } private function loadIcon(string $path): string Loading
modules/Plugins/Config/Routes.php +8 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,14 @@ $routes->group( 'as' => 'plugins-installed', 'filter' => 'permission:plugins.manage', ]); $routes->get('(:segment)', 'PluginsController::settings/$1', [ 'as' => 'plugins-settings', 'filter' => 'permission:plugins.manage', ]); $routes->post('(:segment)', 'PluginsController::settingsAction/$1', [ 'as' => 'plugins-settings-action', 'filter' => 'permission:plugins.manage', ]); $routes->post('activate/(:segment)', 'PluginsController::activate/$1', [ 'as' => 'plugins-activate', 'filter' => 'permission:plugins.manage', Loading
modules/Plugins/Controllers/PluginsController.php +38 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Modules\Plugins\Controllers; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\HTTP\RedirectResponse; use Modules\Admin\Controllers\BaseController; use Modules\Plugins\Plugins; Loading @@ -30,6 +31,43 @@ class PluginsController extends BaseController ]); } public function settings(string $pluginKey): string { /** @var Plugins $plugins */ $plugins = service('plugins'); $plugin = $plugins->getPlugin($pluginKey); if ($plugin === null) { throw PageNotFoundException::forPageNotFound(); } helper('form'); return view('plugins/settings', [ 'plugin' => $plugin, ]); } public function settingsAction(string $pluginKey): RedirectResponse { /** @var Plugins $plugins */ $plugins = service('plugins'); $plugin = $plugins->getPlugin($pluginKey); if ($plugin === null) { throw PageNotFoundException::forPageNotFound(); } foreach ($plugin->options['settings'] as $option) { $optionKey = $option['key']; $optionValue = $this->request->getPost($optionKey); $plugins->setOption($pluginKey, $optionKey, $optionValue); } return redirect()->back(); } public function activate(string $pluginKey): RedirectResponse { service('plugins')->activate($pluginKey); Loading
modules/Plugins/Helpers/plugins_helper.php +42 −0 Original line number Diff line number Diff line Loading @@ -22,3 +22,45 @@ if (! function_exists('set_plugin_option')) { ->set($key, $value, $context); } } if (! function_exists('array_merge_recursive_distinct')) { /** * array_merge_recursive does indeed merge arrays, but it converts values with duplicate * keys to arrays rather than overwriting the value in the first array with the duplicate * value in the second array, as array_merge does. I.e., with array_merge_recursive, * this happens (documented behavior): * * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value')); * => array('key' => array('org value', 'new value')); * * array_merge_recursive_distinct does not change the datatypes of the values in the arrays. * Matching keys' values in the second array overwrite those in the first array, as is the * case with array_merge, i.e.: * * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value')); * => array('key' => array('new value')); * * Parameters are passed by reference, though only for performance reasons. They're not * altered by this function. * * from https://www.php.net/manual/en/function.array-merge-recursive.php#92195 * * @param array<mixed> $array1 * @param array<mixed> $array2 * @return array<mixed> */ function array_merge_recursive_distinct(array &$array1, array &$array2): array { $merged = $array1; foreach ($array2 as $key => &$value) { if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { $merged[$key] = array_merge_recursive_distinct($merged[$key], $value); } else { $merged[$key] = $value; } } return $merged; } }