Commit e6bfdfc3 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat(plugins): load README.md file to view plugin's instructions in UI

parent 1510e36c
Loading
Loading
Loading
Loading
+6 −2
Original line number Diff line number Diff line
@@ -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',
                ]);
+16 −0
Original line number Diff line number Diff line
@@ -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 */
+51 −0
Original line number Diff line number Diff line
@@ -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;
@@ -27,6 +35,8 @@ abstract class BasePlugin implements PluginInterface

    protected Manifest $manifest;

    protected string $readmeHTML;

    public function __construct(
        protected string $vendor,
        protected string $package,
@@ -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');
    }

    /**
@@ -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);
@@ -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();
    }
}
+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();
    }
}
+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