This repository has been archived on 2024-09-05. You can view files and clone it, but cannot push or open issues or pull requests.
shimmie2/core/extension.php

455 lines
15 KiB
PHP
Raw Normal View History

2021-12-14 18:32:47 +00:00
<?php
declare(strict_types=1);
namespace Shimmie2;
2009-07-19 07:38:13 +00:00
/**
2014-04-29 05:33:03 +00:00
* Class Extension
*
2009-05-15 08:52:55 +00:00
* send_event(BlahEvent()) -> onBlah($event)
*
* Also loads the theme object into $this->theme if available
*
* The original concept came from Artanis's Extension extension
2020-03-25 11:47:00 +00:00
* --> https://github.com/Artanis/simple-extension/tree/master
2009-05-15 08:52:55 +00:00
* Then re-implemented by Shish after he broke the forum and couldn't
* find the thread where the original was posted >_<
*/
abstract class Extension
{
public string $key;
2023-06-27 14:56:49 +00:00
protected Themelet $theme;
public ExtensionInfo $info;
private static array $enabled_extensions = [];
public function __construct($class = null)
{
$class = $class ?? get_called_class();
$this->theme = $this->get_theme_object($class);
$this->info = ExtensionInfo::get_for_extension_class($class);
$this->key = $this->info->key;
}
/**
* Find the theme object for a given extension.
*/
2023-06-27 14:56:49 +00:00
private function get_theme_object(string $base): Themelet
{
$base = str_replace("Shimmie2\\", "", $base);
$custom = "Shimmie2\Custom{$base}Theme";
$normal = "Shimmie2\\{$base}Theme";
if (class_exists($custom)) {
return new $custom();
} elseif (class_exists($normal)) {
return new $normal();
} else {
2023-06-27 14:56:49 +00:00
return new Themelet();
}
}
/**
* Override this to change the priority of the extension,
* lower numbered ones will receive events first.
*/
public function get_priority(): int
{
return 50;
}
public static function determine_enabled_extensions(): void
{
self::$enabled_extensions = [];
2023-02-03 16:58:16 +00:00
$extras = defined("EXTRA_EXTS") ? explode(",", EXTRA_EXTS) : [];
2019-09-29 13:30:55 +00:00
foreach (array_merge(
ExtensionInfo::get_core_extensions(),
2023-02-03 16:58:16 +00:00
$extras
2019-09-29 13:30:55 +00:00
) as $key) {
$ext = ExtensionInfo::get_by_key($key);
2023-11-11 21:49:12 +00:00
if ($ext === null || !$ext->is_supported()) {
continue;
}
2019-09-29 13:50:31 +00:00
// FIXME: error if one of our dependencies isn't supported
self::$enabled_extensions[] = $ext->key;
2019-09-29 13:30:55 +00:00
if (!empty($ext->dependencies)) {
foreach ($ext->dependencies as $dep) {
self::$enabled_extensions[] = $dep;
}
}
}
}
public static function is_enabled(string $key): ?bool
{
return in_array($key, self::$enabled_extensions);
}
public static function get_enabled_extensions(): array
{
return self::$enabled_extensions;
}
public static function get_enabled_extensions_as_string(): string
{
2019-09-29 13:30:55 +00:00
return implode(",", self::$enabled_extensions);
}
2019-11-03 19:04:57 +00:00
protected function get_version(string $name): int
{
global $config;
return $config->get_int($name, 0);
}
protected function set_version(string $name, int $ver)
{
global $config;
$config->set_int($name, $ver);
log_info("upgrade", "Set version for $name to $ver");
}
}
class ExtensionNotFound extends SCoreException
{
}
2022-10-28 00:46:48 +00:00
enum ExtensionVisibility
{
2022-10-28 00:45:35 +00:00
case DEFAULT;
case ADMIN;
case HIDDEN;
}
abstract class ExtensionInfo
{
// Every credit you get costs us RAM. It stops now.
public const SHISH_NAME = "Shish";
public const SHISH_EMAIL = "webmaster@shishnet.org";
2020-03-25 11:47:00 +00:00
public const SHIMMIE_URL = "https://code.shishnet.org/shimmie2/";
2023-11-11 21:49:12 +00:00
public const SHISH_AUTHOR = [self::SHISH_NAME => self::SHISH_EMAIL];
public const LICENSE_GPLV2 = "GPLv2";
public const LICENSE_MIT = "MIT";
public const LICENSE_WTFPL = "WTFPL";
public string $key;
public bool $core = false;
public bool $beta = false;
public string $name;
public string $license;
public string $description;
public array $authors = [];
public array $dependencies = [];
public array $conflicts = [];
2022-10-28 00:45:35 +00:00
public ExtensionVisibility $visibility = ExtensionVisibility::DEFAULT;
public ?string $link = null;
public ?string $version = null;
public ?string $documentation = null;
2020-01-26 13:19:35 +00:00
2022-10-28 00:45:35 +00:00
/** @var DatabaseDriverID[] which DBs this ext supports (blank for 'all') */
public array $db_support = [];
private ?bool $supported = null;
private ?string $support_info = null;
public function is_supported(): bool
{
2023-11-11 21:49:12 +00:00
if ($this->supported === null) {
$this->check_support();
}
return $this->supported;
}
public function get_support_info(): string
{
2023-11-11 21:49:12 +00:00
if ($this->supported === null) {
$this->check_support();
}
return $this->support_info;
}
private static array $all_info_by_key = [];
private static array $all_info_by_class = [];
private static array $core_extensions = [];
protected function __construct()
{
2019-10-02 08:03:14 +00:00
assert(!empty($this->key), "key field is required");
assert(!empty($this->name), "name field is required for extension $this->key");
assert(is_array($this->db_support), "db_support has to be an array for extension $this->key");
assert(is_array($this->authors), "authors has to be an array for extension $this->key");
assert(is_array($this->dependencies), "dependencies has to be an array for extension $this->key");
}
public function is_enabled(): bool
{
return Extension::is_enabled($this->key);
}
private function check_support()
{
global $database;
$this->support_info = "";
2022-10-28 00:45:35 +00:00
if (!empty($this->db_support) && !in_array($database->get_driver_id(), $this->db_support)) {
$this->support_info .= "Database not supported. ";
}
if (!empty($this->conflicts)) {
$intersects = array_intersect($this->conflicts, Extension::get_enabled_extensions());
if (!empty($intersects)) {
$this->support_info .= "Conflicts with other extension(s): " . join(", ", $intersects);
}
}
// Additional checks here as needed
$this->supported = empty($this->support_info);
}
public static function get_all(): array
{
return array_values(self::$all_info_by_key);
}
public static function get_all_keys(): array
{
return array_keys(self::$all_info_by_key);
}
public static function get_core_extensions(): array
{
return self::$core_extensions;
}
public static function get_by_key(string $key): ?ExtensionInfo
{
2019-09-29 13:30:55 +00:00
if (array_key_exists($key, self::$all_info_by_key)) {
return self::$all_info_by_key[$key];
} else {
return null;
}
}
public static function get_for_extension_class(string $base): ExtensionInfo
{
$normal = "{$base}Info";
if (array_key_exists($normal, self::$all_info_by_class)) {
return self::$all_info_by_class[$normal];
} else {
$infos = print_r(array_keys(self::$all_info_by_class), true);
throw new ExtensionNotFound("$normal not found in {$infos}");
}
}
public static function load_all_extension_info()
{
foreach (get_subclasses_of("Shimmie2\ExtensionInfo") as $class) {
2020-02-23 16:39:55 +00:00
$extension_info = new $class();
if (array_key_exists($extension_info->key, self::$all_info_by_key)) {
2023-02-03 16:44:16 +00:00
throw new SCoreException("Extension Info $class with key $extension_info->key has already been loaded");
2020-02-23 16:39:55 +00:00
}
2020-02-23 16:39:55 +00:00
self::$all_info_by_key[$extension_info->key] = $extension_info;
self::$all_info_by_class[$class] = $extension_info;
2023-11-11 21:49:12 +00:00
if ($extension_info->core === true) {
2020-02-23 16:39:55 +00:00
self::$core_extensions[] = $extension_info->key;
}
}
}
}
2009-07-19 07:38:13 +00:00
/**
2014-04-29 05:33:03 +00:00
* Class FormatterExtension
*
* Several extensions have this in common, make a common API.
*/
abstract class FormatterExtension extends Extension
{
public function onTextFormatting(TextFormattingEvent $event)
{
$event->formatted = $this->format($event->formatted);
$event->stripped = $this->strip($event->stripped);
}
abstract public function format(string $text): string;
abstract public function strip(string $text): string;
}
2009-07-16 19:20:53 +00:00
2009-07-19 07:38:13 +00:00
/**
2014-04-29 05:33:03 +00:00
* Class DataHandlerExtension
*
2009-07-19 03:48:25 +00:00
* This too is a common class of extension with many methods in common,
2014-04-29 05:33:03 +00:00
* so we have a base class to extend from.
2009-07-19 03:48:25 +00:00
*/
abstract class DataHandlerExtension extends Extension
{
protected array $SUPPORTED_MIME = [];
2020-02-23 18:37:22 +00:00
protected function move_upload_to_archive(DataUploadEvent $event)
{
$target = warehouse_path(Image::IMAGE_DIR, $event->hash);
if (!@copy($event->tmpname, $target)) {
$errors = error_get_last();
throw new UploadException(
"Failed to copy file from uploads ({$event->tmpname}) to archive ($target): ".
"{$errors['type']} / {$errors['message']}"
);
}
}
public function onDataUpload(DataUploadEvent $event)
{
2020-06-14 16:05:55 +00:00
$supported_mime = $this->supported_mime($event->mime);
$check_contents = $this->check_contents($event->tmpname);
2020-06-14 16:05:55 +00:00
if ($supported_mime && $check_contents) {
$this->move_upload_to_archive($event);
2020-06-14 16:05:55 +00:00
send_event(new ThumbnailGenerationEvent($event->hash, $event->mime));
/* Check if we are replacing an image */
if (!is_null($event->replace_id)) {
/* hax: This seems like such a dirty way to do this.. */
/* Check to make sure the image exists. */
$existing = Image::by_id($event->replace_id);
if (is_null($existing)) {
2020-10-26 15:10:00 +00:00
throw new UploadException("Post to replace does not exist!");
}
if ($existing->hash === $event->hash) {
2020-10-26 15:10:00 +00:00
throw new UploadException("The uploaded post is the same as the one to replace.");
}
$replacement = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
send_event(new ImageReplaceEvent($existing, $replacement));
$event->images[] = $replacement;
if(!empty($event->metadata['source'])) {
send_event(new SourceSetEvent($existing, $event->metadata['source']));
}
} else {
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
$iae = send_event(new ImageAdditionEvent($image));
$event->images[] = $iae->image;
$event->merged = $iae->merged;
if(!empty($event->metadata['tags'])) {
if($iae->merged) {
$event->metadata['tags'] = array_merge($iae->image->get_tag_array(), $event->metadata['tags']);
}
send_event(new TagSetEvent($image, $event->metadata['tags']));
}
if(!empty($event->metadata['source'])) {
send_event(new SourceSetEvent($image, $event->metadata['source']));
}
if (!empty($event->metadata['rating'])) {
send_event(new RatingSetEvent($image, $event->metadata['rating']));
}
if (!empty($event->metadata['locked'])) {
send_event(new LockSetEvent($image, $event->metadata['locked']));
}
}
2020-06-14 16:05:55 +00:00
} elseif ($supported_mime && !$check_contents) {
2020-01-29 20:22:50 +00:00
// We DO support this extension - but the file looks corrupt
throw new UploadException("Invalid or corrupted file");
}
}
public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
{
$result = false;
2020-06-14 16:05:55 +00:00
if ($this->supported_mime($event->mime)) {
2019-06-14 12:47:50 +00:00
if ($event->force) {
2020-06-14 16:05:55 +00:00
$result = $this->create_thumb($event->hash, $event->mime);
} else {
$outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash);
2019-06-14 12:47:50 +00:00
if (file_exists($outname)) {
return;
}
2020-06-14 16:05:55 +00:00
$result = $this->create_thumb($event->hash, $event->mime);
}
}
2019-06-14 12:47:50 +00:00
if ($result) {
$event->generated = true;
}
}
public function onDisplayingImage(DisplayingImageEvent $event)
{
global $config, $page;
2020-06-14 16:05:55 +00:00
if ($this->supported_mime($event->image->get_mime())) {
2023-02-03 20:03:04 +00:00
// @phpstan-ignore-next-line
$this->theme->display_image($event->image);
if ($config->get_bool(ImageConfig::SHOW_META) && method_exists($this->theme, "display_metadata")) {
$this->theme->display_metadata($event->image);
}
}
}
2020-02-23 18:37:22 +00:00
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
if ($this->supported_mime($event->image->get_mime())) {
2020-02-23 18:37:22 +00:00
$this->media_check_properties($event);
}
}
2020-02-23 16:05:09 +00:00
protected function create_image_from_data(string $filename, array $metadata): Image
{
$image = new Image();
assert(is_readable($filename));
$image->tmp_file = $filename;
$image->filesize = filesize($filename);
$image->hash = md5_file($filename);
2020-10-27 01:55:48 +00:00
$image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
$image->set_mime(MimeType::get_for_file($filename, get_file_ext($metadata["filename"]) ?? null));
2020-02-23 16:05:09 +00:00
if (empty($image->get_mime())) {
throw new UploadException("Unable to determine MIME for $filename");
}
try {
send_event(new MediaCheckPropertiesEvent($image));
} catch (MediaException $e) {
throw new UploadException("Unable to scan media properties $filename / $image->filename / $image->hash: ".$e->getMessage());
}
2020-02-23 16:05:09 +00:00
return $image;
}
2020-02-23 18:37:22 +00:00
abstract protected function media_check_properties(MediaCheckPropertiesEvent $event): void;
abstract protected function check_contents(string $tmpname): bool;
2020-06-14 16:05:55 +00:00
abstract protected function create_thumb(string $hash, string $mime): bool;
2020-02-23 16:39:55 +00:00
2020-06-14 16:05:55 +00:00
protected function supported_mime(string $mime): bool
2020-02-23 18:37:22 +00:00
{
2020-06-14 16:05:55 +00:00
return MimeType::matches_array($mime, $this->SUPPORTED_MIME);
2020-02-23 18:37:22 +00:00
}
2020-06-14 16:05:55 +00:00
public static function get_all_supported_mimes(): array
2020-02-23 18:12:14 +00:00
{
2020-02-23 16:39:55 +00:00
$arr = [];
foreach (get_subclasses_of("Shimmie2\DataHandlerExtension") as $handler) {
$handler = (new $handler());
2020-06-14 16:05:55 +00:00
$arr = array_merge($arr, $handler->SUPPORTED_MIME);
}
2020-06-14 16:05:55 +00:00
// Not sure how to handle this otherwise, don't want to set up a whole other event for this one class
if (class_exists("Shimmie2\TranscodeImage")) {
2020-06-14 16:05:55 +00:00
$arr = array_merge($arr, TranscodeImage::get_enabled_mimes());
}
$arr = array_unique($arr);
return $arr;
}
public static function get_all_supported_exts(): array
{
$arr = [];
foreach (self::get_all_supported_mimes() as $mime) {
$arr = array_merge($arr, FileExtension::get_all_for_mime($mime));
2020-02-23 16:39:55 +00:00
}
$arr = array_unique($arr);
2020-02-23 16:39:55 +00:00
return $arr;
}
2009-07-16 19:20:53 +00:00
}