castopod/modules/Plugins/Core/BasePlugin.php
Yassine Doghri 94cea0ce91 feat: set min PHP version to 8.4
update CI4 to 4.6.0 + use codeigniter-vite and vite-plugin-codeigniter to load assets
2025-03-14 12:54:51 +00:00

387 lines
9.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Modules\Plugins\Core;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Libraries\HtmlHead;
use App\Libraries\RssFeed;
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\Field;
use Modules\Plugins\Manifest\Manifest;
use Modules\Plugins\Manifest\Person;
use Modules\Plugins\Manifest\Repository;
use Modules\Plugins\Manifest\Settings;
use Override;
/**
* @property string $key
* @property string $iconSrc
*/
abstract class BasePlugin implements PluginInterface
{
protected string $key;
protected string $iconSrc;
protected PluginStatus $status;
protected Manifest $manifest;
protected ?string $readmeHTML;
protected ?string $licenseHTML;
public function __construct(
protected string $vendor,
protected string $package,
protected string $directory,
) {
$this->key = sprintf('%s/%s', $vendor, $package);
// TODO: cache manifest data
$manifestPath = $directory . '/manifest.json';
$this->manifest = new Manifest($this->key);
$this->manifest->loadFromFile($manifestPath);
// check compatibility with Castopod version
if ($this->manifest->minCastopodVersion !== null && version_compare(
CP_VERSION,
$this->manifest->minCastopodVersion,
'<',
)) {
$this->status = PluginStatus::INCOMPATIBLE;
} else {
$this->status = get_plugin_setting($this->key, 'active') ? PluginStatus::ACTIVE : PluginStatus::INACTIVE;
}
$this->iconSrc = $this->loadIcon($directory . '/icon.svg');
$this->readmeHTML = $this->loadMarkdownAsHTML($directory . '/README.md');
$this->licenseHTML = $this->loadMarkdownAsHTML($directory . '/LICENSE.md');
}
/**
* @param list<string>|string $value
*/
public function __set(string $name, array|string $value): void
{
$this->{$name} = $value;
}
#[Override]
public function rssBeforeChannel(Podcast $podcast): void
{
}
#[Override]
public function rssAfterChannel(Podcast $podcast, RssFeed $channel): void
{
}
#[Override]
public function rssBeforeItem(Episode $episode): void
{
}
#[Override]
public function rssAfterItem(Episode $episode, RssFeed $item): void
{
}
#[Override]
public function siteHead(HtmlHead $head): void
{
}
final public function getGeneralSetting(string $key): mixed
{
return get_plugin_setting($this->key, $key);
}
final public function getPodcastSetting(int $podcastId, string $key): mixed
{
return get_plugin_setting($this->key, $key, ['podcast', $podcastId]);
}
final public function getEpisodeSetting(int $episodeId, string $key): mixed
{
return get_plugin_setting($this->key, $key, ['episode', $episodeId]);
}
final public function clearCache(): void
{
foreach ($this->getHooks() as $hook) {
foreach (Plugins::CACHE_MAP[$hook] ?? [] as $cacheGlob) {
cache()->deleteMatching($cacheGlob);
}
}
}
/**
* @return bool true on success, false on failure
*/
final public function activate(): bool
{
if ($this->status === PluginStatus::ACTIVE) {
return false;
}
$this->setStatus(PluginStatus::ACTIVE);
if ($this->status === PluginStatus::INACTIVE) {
return false;
}
set_plugin_setting($this->key, 'active', true);
return true;
}
final public function deactivate(): bool
{
if ($this->status !== PluginStatus::ACTIVE) {
return false;
}
$this->setStatus(PluginStatus::INACTIVE);
set_plugin_setting($this->key, 'active', false);
return true;
}
final public function getStatus(): PluginStatus
{
return $this->status;
}
final public function getDirectory(): string
{
return $this->directory;
}
/**
* @return array<string,string>
*/
final public function getManifestErrors(): array
{
return Manifest::getPluginErrors($this->key);
}
final public function isHookDeclared(string $name): bool
{
return in_array($name, $this->manifest->hooks, true);
}
final public function getVersion(): string
{
return $this->manifest->version;
}
final public function getHomepage(): ?URI
{
return $this->manifest->homepage;
}
final public function getRepository(): ?Repository
{
return $this->manifest->repository;
}
/**
* @return list<string>
*/
final public function getKeywords(): array
{
return $this->manifest->keywords;
}
/**
* @return Person[]
*/
final public function getAuthors(): array
{
return $this->manifest->authors;
}
final public function getIconSrc(): string
{
return $this->iconSrc;
}
/**
* @return Field[]
*/
final public function getSettingsFields(string $type): array
{
$settings = $this->manifest->settings;
if (! $settings instanceof Settings) {
return [];
}
return $settings->{$type};
}
final public function getMinCastopodVersion(): string
{
return $this->manifest->minCastopodVersion ?? '';
}
/**
* @return list<string>
*/
final public function getHooks(): array
{
return $this->manifest->hooks;
}
final public function getKey(): string
{
return $this->key;
}
final public function getVendor(): string
{
return $this->vendor;
}
final public function getPackage(): string
{
return $this->package;
}
final public function getTitle(): string
{
$key = sprintf('Plugin.%s.title', $this->key);
/** @var string $title */
$title = lang($key);
if ($title === $key) {
return $this->manifest->name;
}
return $title;
}
final public function getDescription(): string
{
$key = sprintf('Plugin.%s.description', $this->key);
/** @var string $description */
$description = lang($key);
if ($description === $key) {
return esc($this->manifest->description);
}
return $description;
}
final public function getReadmeHTML(): ?string
{
return $this->readmeHTML;
}
final public function getLicense(): string
{
return $this->manifest->license ?? 'UNLICENSED';
}
final public function getLicenseHTML(): ?string
{
return $this->licenseHTML;
}
/**
* @param PluginStatus::ACTIVE|PluginStatus::INACTIVE $value
*/
final protected function setStatus(PluginStatus $value): self
{
if ($this->getManifestErrors() !== []) {
$this->status = PluginStatus::INVALID;
return $this;
}
$this->status = $value;
return $this;
}
final protected function getOption(string $option): mixed
{
return get_plugin_setting($this->key, $option);
}
final protected function setOption(string $option, mixed $value = null): void
{
set_plugin_setting($this->key, $option, $value);
}
private function loadIcon(string $path): string
{
// TODO: cache icon
$svgIcon = @file_get_contents($path);
if (! $svgIcon) {
return "data:image/svg+xml;utf8,%3Csvg xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' viewBox='0 0 64 64'%3E%3Cpath fill='%2300564A' d='M0 0h64v64H0z'%2F%3E%3Cpath fill='%23E7F9E4' d='M25.3 18.7a5 5 0 1 1 9.7 1.6h7c1 0 1.7.8 1.7 1.7v7a5 5 0 1 1 0 9.4v7c0 .9-.8 1.6-1.7 1.6H18.7c-1 0-1.7-.7-1.7-1.7V22c0-1 .7-1.7 1.7-1.7h7a5 5 0 0 1-.4-1.6Z'%2F%3E%3C%2Fsvg%3E";
}
$encodedIcon = rawurlencode(str_replace(["\r", "\n"], ' ', $svgIcon));
return 'data:image/svg+xml;utf8,' . str_replace(
['%20', '%22', '%27', '%3D'],
[' ', "'", "'", '='],
$encodedIcon,
);
}
private function loadMarkdownAsHTML(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' => (new URI(base_url()))->getHost(),
]);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new GithubFlavoredMarkdownExtension());
$environment->addExtension(new SmartPunctExtension());
$environment->addEventListener(
DocumentParsedEvent::class,
static function (DocumentParsedEvent $event): void {
(new ExternalLinkProcessor())->onDocumentParsed($event);
},
);
$environment->addEventListener(
DocumentParsedEvent::class,
static function (DocumentParsedEvent $event): void {
(new ExternalImageProcessor())->onDocumentParsed($event);
},
);
$converter = new MarkdownConverter($environment);
return $converter->convert($readmeMD)
->getContent();
}
}