mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-22 16:51:20 +00:00
387 lines
9.7 KiB
PHP
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();
|
|
}
|
|
}
|