Loading modules/Plugins/Config/Routes.php +6 −2 Original line number Diff line number Diff line Loading @@ -25,11 +25,15 @@ $routes->group( 'filter' => 'permission:plugins.manage', ]); $routes->group('(:pluginKey)', static function ($routes): void { $routes->get('/', 'PluginController::generalSettings/$1/$2', [ $routes->get('/', 'PluginController::view/$1/$2', [ 'as' => 'plugins-view', 'filter' => 'permission:plugins.manage', ]); $routes->get('settings', 'PluginController::generalSettings/$1/$2', [ 'as' => 'plugins-general-settings', 'filter' => 'permission:plugins.manage', ]); $routes->post('/', 'PluginController::generalSettingsAction/$1/$2', [ $routes->post('settings', 'PluginController::generalSettingsAction/$1/$2', [ 'as' => 'plugins-general-settings-action', 'filter' => 'permission:plugins.manage', ]); Loading modules/Plugins/Controllers/PluginController.php +16 −0 Original line number Diff line number Diff line Loading @@ -48,6 +48,22 @@ class PluginController extends BaseController ]); } public function view(string $vendor, string $package): string { /** @var Plugins $plugins */ $plugins = service('plugins'); $plugin = $plugins->getPlugin($vendor, $package); if ($plugin === null) { throw PageNotFoundException::forPageNotFound(); } return view('plugins/view', [ 'plugin' => $plugin, ]); } public function generalSettings(string $vendor, string $package): string { /** @var Plugins $plugins */ Loading modules/Plugins/Core/BasePlugin.php +51 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,14 @@ use App\Entities\Episode; use App\Entities\Podcast; use App\Libraries\SimpleRSSElement; use CodeIgniter\HTTP\URI; use League\CommonMark\Environment\Environment; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; use League\CommonMark\Extension\GithubFlavoredMarkdownExtension; use League\CommonMark\Extension\SmartPunct\SmartPunctExtension; use League\CommonMark\MarkdownConverter; use Modules\Plugins\ExternalImageProcessor; use Modules\Plugins\ExternalLinkProcessor; use Modules\Plugins\Manifest\Manifest; use Modules\Plugins\Manifest\Settings; use Modules\Plugins\Manifest\SettingsField; Loading @@ -27,6 +35,8 @@ abstract class BasePlugin implements PluginInterface protected Manifest $manifest; protected string $readmeHTML; public function __construct( protected string $vendor, protected string $package, Loading Loading @@ -55,6 +65,8 @@ abstract class BasePlugin implements PluginInterface $this->active = get_plugin_option($this->key, 'active') ?? false; $this->iconSrc = $this->loadIcon($directory . '/icon.svg'); $this->readmeHTML = $this->loadReadme($directory . '/README.md'); } /** Loading Loading @@ -182,6 +194,11 @@ abstract class BasePlugin implements PluginInterface return $description; } final public function getReadmeHTML(): string { return $this->readmeHTML; } final protected function getOption(string $option): mixed { return get_plugin_option($this->key, $option); Loading @@ -208,4 +225,38 @@ abstract class BasePlugin implements PluginInterface $encodedIcon ); } private function loadReadme(string $path): ?string { // TODO: cache readme $readmeMD = @file_get_contents($path); if (! $readmeMD) { return null; } $environment = new Environment([ 'html_input' => 'escape', 'allow_unsafe_links' => false, 'host' => 'hello', ]); $environment->addExtension(new CommonMarkCoreExtension()); $environment->addExtension(new GithubFlavoredMarkdownExtension()); $environment->addExtension(new SmartPunctExtension()); $environment->addEventListener( DocumentParsedEvent::class, [new ExternalLinkProcessor($environment), 'onDocumentParsed'] ); $environment->addEventListener( DocumentParsedEvent::class, [new ExternalImageProcessor($environment), 'onDocumentParsed'] ); $converter = new MarkdownConverter($environment); return $converter->convert($readmeMD) ->getContent(); } } modules/Plugins/ExternalImageProcessor.php 0 → 100644 +53 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Modules\Plugins; use CodeIgniter\HTTP\URI; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\Node\Inline\Image; class ExternalImageProcessor { private EnvironmentInterface $environment; public function __construct(EnvironmentInterface $environment) { $this->environment = $environment; } public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); $walker = $document->walker(); while ($event = $walker->next()) { $node = $event->getNode(); // Only stop at Link nodes when we first encounter them if (! ($node instanceof Image) || ! $event->isEntering()) { continue; } $url = $node->getUrl(); if ($this->isUrlExternal($url)) { $node->detach(); } } } private function isUrlExternal(string $url): bool { // Only look at http and https URLs if (! preg_match('/^https?:\/\//', $url)) { return false; } $host = parse_url($url, PHP_URL_HOST); // TODO: load from environment's config // return $host != $this->environment->getConfiguration()->get('host'); return $host !== (new URI(base_url()))->getHost(); } } modules/Plugins/ExternalLinkProcessor.php 0 → 100644 +54 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Modules\Plugins; use CodeIgniter\HTTP\URI; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; class ExternalLinkProcessor { private EnvironmentInterface $environment; public function __construct(EnvironmentInterface $environment) { $this->environment = $environment; } public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); $walker = $document->walker(); while ($event = $walker->next()) { $node = $event->getNode(); // Only stop at Link nodes when we first encounter them if (! ($node instanceof Link) || ! $event->isEntering()) { continue; } $url = $node->getUrl(); if ($this->isUrlExternal($url)) { $node->data->append('attributes/target', '_blank'); $node->data->append('attributes/rel', 'noopener noreferrer'); } } } private function isUrlExternal(string $url): bool { // Only look at http and https URLs if (! preg_match('/^https?:\/\//', $url)) { return false; } $host = parse_url($url, PHP_URL_HOST); // TODO: load from environment's config // return $host != $this->environment->getConfiguration()->get('host'); return $host !== (new URI(base_url()))->getHost(); } } Loading
modules/Plugins/Config/Routes.php +6 −2 Original line number Diff line number Diff line Loading @@ -25,11 +25,15 @@ $routes->group( 'filter' => 'permission:plugins.manage', ]); $routes->group('(:pluginKey)', static function ($routes): void { $routes->get('/', 'PluginController::generalSettings/$1/$2', [ $routes->get('/', 'PluginController::view/$1/$2', [ 'as' => 'plugins-view', 'filter' => 'permission:plugins.manage', ]); $routes->get('settings', 'PluginController::generalSettings/$1/$2', [ 'as' => 'plugins-general-settings', 'filter' => 'permission:plugins.manage', ]); $routes->post('/', 'PluginController::generalSettingsAction/$1/$2', [ $routes->post('settings', 'PluginController::generalSettingsAction/$1/$2', [ 'as' => 'plugins-general-settings-action', 'filter' => 'permission:plugins.manage', ]); Loading
modules/Plugins/Controllers/PluginController.php +16 −0 Original line number Diff line number Diff line Loading @@ -48,6 +48,22 @@ class PluginController extends BaseController ]); } public function view(string $vendor, string $package): string { /** @var Plugins $plugins */ $plugins = service('plugins'); $plugin = $plugins->getPlugin($vendor, $package); if ($plugin === null) { throw PageNotFoundException::forPageNotFound(); } return view('plugins/view', [ 'plugin' => $plugin, ]); } public function generalSettings(string $vendor, string $package): string { /** @var Plugins $plugins */ Loading
modules/Plugins/Core/BasePlugin.php +51 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,14 @@ use App\Entities\Episode; use App\Entities\Podcast; use App\Libraries\SimpleRSSElement; use CodeIgniter\HTTP\URI; use League\CommonMark\Environment\Environment; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; use League\CommonMark\Extension\GithubFlavoredMarkdownExtension; use League\CommonMark\Extension\SmartPunct\SmartPunctExtension; use League\CommonMark\MarkdownConverter; use Modules\Plugins\ExternalImageProcessor; use Modules\Plugins\ExternalLinkProcessor; use Modules\Plugins\Manifest\Manifest; use Modules\Plugins\Manifest\Settings; use Modules\Plugins\Manifest\SettingsField; Loading @@ -27,6 +35,8 @@ abstract class BasePlugin implements PluginInterface protected Manifest $manifest; protected string $readmeHTML; public function __construct( protected string $vendor, protected string $package, Loading Loading @@ -55,6 +65,8 @@ abstract class BasePlugin implements PluginInterface $this->active = get_plugin_option($this->key, 'active') ?? false; $this->iconSrc = $this->loadIcon($directory . '/icon.svg'); $this->readmeHTML = $this->loadReadme($directory . '/README.md'); } /** Loading Loading @@ -182,6 +194,11 @@ abstract class BasePlugin implements PluginInterface return $description; } final public function getReadmeHTML(): string { return $this->readmeHTML; } final protected function getOption(string $option): mixed { return get_plugin_option($this->key, $option); Loading @@ -208,4 +225,38 @@ abstract class BasePlugin implements PluginInterface $encodedIcon ); } private function loadReadme(string $path): ?string { // TODO: cache readme $readmeMD = @file_get_contents($path); if (! $readmeMD) { return null; } $environment = new Environment([ 'html_input' => 'escape', 'allow_unsafe_links' => false, 'host' => 'hello', ]); $environment->addExtension(new CommonMarkCoreExtension()); $environment->addExtension(new GithubFlavoredMarkdownExtension()); $environment->addExtension(new SmartPunctExtension()); $environment->addEventListener( DocumentParsedEvent::class, [new ExternalLinkProcessor($environment), 'onDocumentParsed'] ); $environment->addEventListener( DocumentParsedEvent::class, [new ExternalImageProcessor($environment), 'onDocumentParsed'] ); $converter = new MarkdownConverter($environment); return $converter->convert($readmeMD) ->getContent(); } }
modules/Plugins/ExternalImageProcessor.php 0 → 100644 +53 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Modules\Plugins; use CodeIgniter\HTTP\URI; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\Node\Inline\Image; class ExternalImageProcessor { private EnvironmentInterface $environment; public function __construct(EnvironmentInterface $environment) { $this->environment = $environment; } public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); $walker = $document->walker(); while ($event = $walker->next()) { $node = $event->getNode(); // Only stop at Link nodes when we first encounter them if (! ($node instanceof Image) || ! $event->isEntering()) { continue; } $url = $node->getUrl(); if ($this->isUrlExternal($url)) { $node->detach(); } } } private function isUrlExternal(string $url): bool { // Only look at http and https URLs if (! preg_match('/^https?:\/\//', $url)) { return false; } $host = parse_url($url, PHP_URL_HOST); // TODO: load from environment's config // return $host != $this->environment->getConfiguration()->get('host'); return $host !== (new URI(base_url()))->getHost(); } }
modules/Plugins/ExternalLinkProcessor.php 0 → 100644 +54 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Modules\Plugins; use CodeIgniter\HTTP\URI; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; class ExternalLinkProcessor { private EnvironmentInterface $environment; public function __construct(EnvironmentInterface $environment) { $this->environment = $environment; } public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); $walker = $document->walker(); while ($event = $walker->next()) { $node = $event->getNode(); // Only stop at Link nodes when we first encounter them if (! ($node instanceof Link) || ! $event->isEntering()) { continue; } $url = $node->getUrl(); if ($this->isUrlExternal($url)) { $node->data->append('attributes/target', '_blank'); $node->data->append('attributes/rel', 'noopener noreferrer'); } } } private function isUrlExternal(string $url): bool { // Only look at http and https URLs if (! preg_match('/^https?:\/\//', $url)) { return false; } $host = parse_url($url, PHP_URL_HOST); // TODO: load from environment's config // return $host != $this->environment->getConfiguration()->get('host'); return $host !== (new URI(base_url()))->getHost(); } }