-
-
-
- ';
- $sent = mail($this->to, $this->subject, $message, $headers);
- if($sent){
- log_info("mail", "Sent message '$this->subject' to '$this->to'");
- }
- else{
- log_info("mail", "Error sending message '$this->subject' to '$this->to'");
- }
-
- return $sent;
- }
-}
-
diff --git a/core/event.class.php b/core/event.class.php
deleted file mode 100644
index 37511e29..00000000
--- a/core/event.class.php
+++ /dev/null
@@ -1,325 +0,0 @@
- an event is generated with $args = array("view",
- * "42"); when an event handler asks $event->page_matches("view"), it returns
- * true and ignores the matched part, such that $event->count_args() = 1 and
- * $event->get_arg(0) = "42"
- */
-class PageRequestEvent extends Event {
- /**
- * @var array
- */
- public $args;
-
- /**
- * @var int
- */
- public $arg_count;
-
- /**
- * @var int
- */
- public $part_count;
-
- /**
- * @param string $path
- */
- public function __construct($path) {
- global $config;
-
- // trim starting slashes
- $path = ltrim($path, "/");
-
- // if path is not specified, use the default front page
- if(empty($path)) { /* empty is faster than strlen */
- $path = $config->get_string('front_page');
- }
-
- // break the path into parts
- $args = explode('/', $path);
-
- // voodoo so that an arg can contain a slash; is
- // this still needed?
- if(strpos($path, "^") !== FALSE) {
- $unescaped = array();
- foreach($args as $part) {
- $unescaped[] = _decaret($part);
- }
- $args = $unescaped;
- }
-
- $this->args = $args;
- $this->arg_count = count($args);
- }
-
- /**
- * Test if the requested path matches a given pattern.
- *
- * If it matches, store the remaining path elements in $args
- *
- * @param string $name
- * @return bool
- */
- public function page_matches(/*string*/ $name) {
- $parts = explode("/", $name);
- $this->part_count = count($parts);
-
- if($this->part_count > $this->arg_count) {
- return false;
- }
-
- for($i=0; $i<$this->part_count; $i++) {
- if($parts[$i] != $this->args[$i]) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Get the n th argument of the page request (if it exists.)
- *
- * @param int $n
- * @return string|null The argument (string) or NULL
- */
- public function get_arg(/*int*/ $n) {
- $offset = $this->part_count + $n;
- if($offset >= 0 && $offset < $this->arg_count) {
- return $this->args[$offset];
- }
- else {
- return null;
- }
- }
-
- /**
- * Returns the number of arguments the page request has.
- * @return int
- */
- public function count_args() {
- return int_escape($this->arg_count - $this->part_count);
- }
-
- /*
- * Many things use these functions
- */
-
- /**
- * @return array
- */
- public function get_search_terms() {
- $search_terms = array();
- if($this->count_args() === 2) {
- $search_terms = Tag::explode($this->get_arg(0));
- }
- return $search_terms;
- }
-
- /**
- * @return int
- */
- public function get_page_number() {
- $page_number = 1;
- if($this->count_args() === 1) {
- $page_number = int_escape($this->get_arg(0));
- }
- else if($this->count_args() === 2) {
- $page_number = int_escape($this->get_arg(1));
- }
- if($page_number === 0) $page_number = 1; // invalid -> 0
- return $page_number;
- }
-
- /**
- * @return int
- */
- public function get_page_size() {
- global $config;
- return $config->get_int('index_images');
- }
-}
-
-
-/**
- * Sent when index.php is called from the command line
- */
-class CommandEvent extends Event {
- /**
- * @var string
- */
- public $cmd = "help";
-
- /**
- * @var array
- */
- public $args = array();
-
- /**
- * @param string[] $args
- */
- public function __construct(/*array(string)*/ $args) {
- global $user;
-
- $opts = array();
- $log_level = SCORE_LOG_WARNING;
- $arg_count = count($args);
-
- for($i=1; $i<$arg_count; $i++) {
- switch($args[$i]) {
- case '-u':
- $user = User::by_name($args[++$i]);
- if(is_null($user)) {
- die("Unknown user");
- }
- break;
- case '-q':
- $log_level += 10;
- break;
- case '-v':
- $log_level -= 10;
- break;
- default:
- $opts[] = $args[$i];
- break;
- }
- }
-
- define("CLI_LOG_LEVEL", $log_level);
-
- if(count($opts) > 0) {
- $this->cmd = $opts[0];
- $this->args = array_slice($opts, 1);
- }
- else {
- print "\n";
- print "Usage: php {$args[0]} [flags] [command]\n";
- print "\n";
- print "Flags:\n";
- print " -u [username]\n";
- print " Log in as the specified user\n";
- print " -q / -v\n";
- print " Be quieter / more verbose\n";
- print " Scale is debug - info - warning - error - critical\n";
- print " Default is to show warnings and above\n";
- print " \n";
- print "Currently known commands:\n";
- }
- }
-}
-
-
-/**
- * A signal that some text needs formatting, the event carries
- * both the text and the result
- */
-class TextFormattingEvent extends Event {
- /**
- * For reference
- *
- * @var string
- */
- public $original;
-
- /**
- * with formatting applied
- *
- * @var string
- */
- public $formatted;
-
- /**
- * with formatting removed
- *
- * @var string
- */
- public $stripped;
-
- /**
- * @param string $text
- */
- public function __construct(/*string*/ $text) {
- $h_text = html_escape(trim($text));
- $this->original = $h_text;
- $this->formatted = $h_text;
- $this->stripped = $h_text;
- }
-}
-
-
-/**
- * A signal that something needs logging
- */
-class LogEvent extends Event {
- /**
- * a category, normally the extension name
- *
- * @var string
- */
- public $section;
-
- /**
- * See python...
- *
- * @var int
- */
- public $priority = 0;
-
- /**
- * Free text to be logged
- *
- * @var string
- */
- public $message;
-
- /**
- * The time that the event was created
- *
- * @var int
- */
- public $time;
-
- /**
- * Extra data to be held separate
- *
- * @var array
- */
- public $args;
-
- /**
- * @param string $section
- * @param int $priority
- * @param string $message
- * @param array $args
- */
- public function __construct($section, $priority, $message, $args) {
- $this->section = $section;
- $this->priority = $priority;
- $this->message = $message;
- $this->args = $args;
- $this->time = time();
- }
-}
-
diff --git a/core/event.php b/core/event.php
new file mode 100644
index 00000000..57f9c2dc
--- /dev/null
+++ b/core/event.php
@@ -0,0 +1,339 @@
+ an event is generated with $args = array("view",
+ * "42"); when an event handler asks $event->page_matches("view"), it returns
+ * true and ignores the matched part, such that $event->count_args() = 1 and
+ * $event->get_arg(0) = "42"
+ */
+class PageRequestEvent extends Event
+{
+ /**
+ * @var array
+ */
+ public $args;
+
+ /**
+ * @var int
+ */
+ public $arg_count;
+
+ /**
+ * @var int
+ */
+ public $part_count;
+
+ public function __construct(string $path)
+ {
+ parent::__construct();
+ global $config;
+
+ // trim starting slashes
+ $path = ltrim($path, "/");
+
+ // if path is not specified, use the default front page
+ if (empty($path)) { /* empty is faster than strlen */
+ $path = $config->get_string(SetupConfig::FRONT_PAGE);
+ }
+
+ // break the path into parts
+ $args = explode('/', $path);
+
+ $this->args = $args;
+ $this->arg_count = count($args);
+ }
+
+ /**
+ * Test if the requested path matches a given pattern.
+ *
+ * If it matches, store the remaining path elements in $args
+ */
+ public function page_matches(string $name): bool
+ {
+ $parts = explode("/", $name);
+ $this->part_count = count($parts);
+
+ if ($this->part_count > $this->arg_count) {
+ return false;
+ }
+
+ for ($i=0; $i<$this->part_count; $i++) {
+ if ($parts[$i] != $this->args[$i]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the n th argument of the page request (if it exists.)
+ */
+ public function get_arg(int $n): string
+ {
+ $offset = $this->part_count + $n;
+ if ($offset >= 0 && $offset < $this->arg_count) {
+ return $this->args[$offset];
+ } else {
+ throw new SCoreException("Requested an invalid argument #$n");
+ }
+ }
+
+ public function try_page_num(int $n): int
+ {
+ if ($this->count_args() > $n) {
+ $i = $this->get_arg($n);
+ if (is_numeric($i) && int_escape($i) > 0) {
+ return int_escape($i);
+ } else {
+ return 1;
+ }
+ } else {
+ return 1;
+ }
+ }
+
+ /**
+ * Returns the number of arguments the page request has.
+ */
+ public function count_args(): int
+ {
+ return $this->arg_count - $this->part_count;
+ }
+
+ /*
+ * Many things use these functions
+ */
+
+ public function get_search_terms(): array
+ {
+ $search_terms = [];
+ if ($this->count_args() === 2) {
+ $search_terms = Tag::explode(Tag::decaret($this->get_arg(0)));
+ }
+ return $search_terms;
+ }
+
+ public function get_page_number(): int
+ {
+ $page_number = 1;
+ if ($this->count_args() === 1) {
+ $page_number = int_escape($this->get_arg(0));
+ } elseif ($this->count_args() === 2) {
+ $page_number = int_escape($this->get_arg(1));
+ }
+ if ($page_number === 0) {
+ $page_number = 1;
+ } // invalid -> 0
+ return $page_number;
+ }
+
+ public function get_page_size(): int
+ {
+ global $config;
+ return $config->get_int(IndexConfig::IMAGES);
+ }
+}
+
+
+/**
+ * Sent when index.php is called from the command line
+ */
+class CommandEvent extends Event
+{
+ /**
+ * @var string
+ */
+ public $cmd = "help";
+
+ /**
+ * @var array
+ */
+ public $args = [];
+
+ /**
+ * #param string[] $args
+ */
+ public function __construct(array $args)
+ {
+ parent::__construct();
+ global $user;
+
+ $opts = [];
+ $log_level = SCORE_LOG_WARNING;
+ $arg_count = count($args);
+
+ for ($i=1; $i<$arg_count; $i++) {
+ switch ($args[$i]) {
+ case '-u':
+ $user = User::by_name($args[++$i]);
+ if (is_null($user)) {
+ die("Unknown user");
+ } else {
+ send_event(new UserLoginEvent($user));
+ }
+ break;
+ case '-q':
+ $log_level += 10;
+ break;
+ case '-v':
+ $log_level -= 10;
+ break;
+ default:
+ $opts[] = $args[$i];
+ break;
+ }
+ }
+
+ if (!defined("CLI_LOG_LEVEL")) {
+ define("CLI_LOG_LEVEL", $log_level);
+ }
+
+ if (count($opts) > 0) {
+ $this->cmd = $opts[0];
+ $this->args = array_slice($opts, 1);
+ } else {
+ print "\n";
+ print "Usage: php {$args[0]} [flags] [command]\n";
+ print "\n";
+ print "Flags:\n";
+ print "\t-u [username]\n";
+ print "\t\tLog in as the specified user\n";
+ print "\t-q / -v\n";
+ print "\t\tBe quieter / more verbose\n";
+ print "\t\tScale is debug - info - warning - error - critical\n";
+ print "\t\tDefault is to show warnings and above\n";
+ print "\n";
+ print "Currently known commands:\n";
+ }
+ }
+}
+
+
+/**
+ * A signal that some text needs formatting, the event carries
+ * both the text and the result
+ */
+class TextFormattingEvent extends Event
+{
+ /**
+ * For reference
+ *
+ * @var string
+ */
+ public $original;
+
+ /**
+ * with formatting applied
+ *
+ * @var string
+ */
+ public $formatted;
+
+ /**
+ * with formatting removed
+ *
+ * @var string
+ */
+ public $stripped;
+
+ public function __construct(string $text)
+ {
+ parent::__construct();
+ // We need to escape before formatting, instead of at display time,
+ // because formatters will add their own HTML tags into the mix and
+ // we don't want to escape those.
+ $h_text = html_escape(trim($text));
+ $this->original = $h_text;
+ $this->formatted = $h_text;
+ $this->stripped = $h_text;
+ }
+}
+
+
+/**
+ * A signal that something needs logging
+ */
+class LogEvent extends Event
+{
+ /**
+ * a category, normally the extension name
+ *
+ * @var string
+ */
+ public $section;
+
+ /**
+ * See python...
+ *
+ * @var int
+ */
+ public $priority = 0;
+
+ /**
+ * Free text to be logged
+ *
+ * @var string
+ */
+ public $message;
+
+ /**
+ * The time that the event was created
+ *
+ * @var int
+ */
+ public $time;
+
+ /**
+ * Extra data to be held separate
+ *
+ * @var array
+ */
+ public $args;
+
+ public function __construct(string $section, int $priority, string $message)
+ {
+ parent::__construct();
+ $this->section = $section;
+ $this->priority = $priority;
+ $this->message = $message;
+ $this->time = time();
+ }
+}
+
+class DatabaseUpgradeEvent extends Event
+{
+}
diff --git a/core/exceptions.class.php b/core/exceptions.class.php
deleted file mode 100644
index d2400893..00000000
--- a/core/exceptions.class.php
+++ /dev/null
@@ -1,29 +0,0 @@
-error = $msg;
+ $this->query = $query;
+ }
+}
+
+class InstallerException extends RuntimeException
+{
+ /** @var string */
+ public $title;
+
+ /** @var string */
+ public $body;
+
+ /** @var int */
+ public $code;
+
+ public function __construct(string $title, string $body, int $code)
+ {
+ parent::__construct($body);
+ $this->title = $title;
+ $this->body = $body;
+ $this->code = $code;
+ }
+}
+
+/**
+ * Class PermissionDeniedException
+ *
+ * A fairly common, generic exception.
+ */
+class PermissionDeniedException extends SCoreException
+{
+}
+
+/**
+ * Class ImageDoesNotExist
+ *
+ * This exception is used when an Image cannot be found by ID.
+ *
+ * Example: Image::by_id(-1) returns null
+ */
+class ImageDoesNotExist extends SCoreException
+{
+}
+
+/*
+ * For validate_input()
+ */
+class InvalidInput extends SCoreException
+{
+}
+
+/*
+ * This is used by the image resizing code when there is not enough memory to perform a resize.
+ */
+class InsufficientMemoryException extends SCoreException
+{
+}
+
+/*
+ * This is used by the image resizing code when there is an error while resizing
+ */
+class ImageResizeException extends SCoreException
+{
+}
diff --git a/core/extension.class.php b/core/extension.class.php
deleted file mode 100644
index dc8a1ccd..00000000
--- a/core/extension.class.php
+++ /dev/null
@@ -1,297 +0,0 @@
-formatted;
- * \endcode
- *
- * An extension is something which is capable of reacting to events.
- *
- *
- * \page hello The Hello World Extension
- *
- * \code
- * // ext/hello/main.php
- * public class HelloEvent extends Event {
- * public function __construct($username) {
- * $this->username = $username;
- * }
- * }
- *
- * public class Hello extends Extension {
- * public function onPageRequest(PageRequestEvent $event) { // Every time a page request is sent
- * global $user; // Look at the global "currently logged in user" object
- * send_event(new HelloEvent($user->name)); // Broadcast a signal saying hello to that user
- * }
- * public function onHello(HelloEvent $event) { // When the "Hello" signal is recieved
- * $this->theme->display_hello($event->username); // Display a message on the web page
- * }
- * }
- *
- * // ext/hello/theme.php
- * public class HelloTheme extends Themelet {
- * public function display_hello($username) {
- * global $page;
- * $h_user = html_escape($username); // Escape the data before adding it to the page
- * $block = new Block("Hello!", "Hello there $h_user"); // HTML-safe variables start with "h_"
- * $page->add_block($block); // Add the block to the page
- * }
- * }
- *
- * // ext/hello/test.php
- * public class HelloTest extends SCorePHPUnitTestCase {
- * public function testHello() {
- * $this->get_page("post/list"); // View a page, any page
- * $this->assert_text("Hello there"); // Check that the specified text is in that page
- * }
- * }
- *
- * // themes/mytheme/hello.theme.php
- * public class CustomHelloTheme extends HelloTheme { // CustomHelloTheme overrides HelloTheme
- * public function display_hello($username) { // the display_hello() function is customised
- * global $page;
- * $h_user = html_escape($username);
- * $page->add_block(new Block(
- * "Hello!",
- * "Hello there $h_user, look at my snazzy custom theme!"
- * );
- * }
- * }
- * \endcode
- *
- */
-
-/**
- * Class Extension
- *
- * send_event(BlahEvent()) -> onBlah($event)
- *
- * Also loads the theme object into $this->theme if available
- *
- * The original concept came from Artanis's Extension extension
- * --> http://github.com/Artanis/simple-extension/tree/master
- * Then re-implemented by Shish after he broke the forum and couldn't
- * find the thread where the original was posted >_<
- */
-abstract class Extension {
- /** @var array which DBs this ext supports (blank for 'all') */
- protected $db_support = [];
-
- /** @var Themelet this theme's Themelet object */
- public $theme;
-
- public function __construct() {
- $this->theme = $this->get_theme_object(get_called_class());
- }
-
- /**
- * @return boolean
- */
- public function is_live() {
- global $database;
- return (
- empty($this->db_support) ||
- in_array($database->get_driver_name(), $this->db_support)
- );
- }
-
- /**
- * Find the theme object for a given extension.
- *
- * @param string $base
- * @return Themelet
- */
- private function get_theme_object($base) {
- $custom = 'Custom'.$base.'Theme';
- $normal = $base.'Theme';
-
- if(class_exists($custom)) {
- return new $custom();
- }
- elseif(class_exists($normal)) {
- return new $normal();
- }
- else {
- return null;
- }
- }
-
- /**
- * Override this to change the priority of the extension,
- * lower numbered ones will recieve events first.
- *
- * @return int
- */
- public function get_priority() {
- return 50;
- }
-}
-
-/**
- * Class FormatterExtension
- *
- * Several extensions have this in common, make a common API.
- */
-abstract class FormatterExtension extends Extension {
- /**
- * @param TextFormattingEvent $event
- */
- public function onTextFormatting(TextFormattingEvent $event) {
- $event->formatted = $this->format($event->formatted);
- $event->stripped = $this->strip($event->stripped);
- }
-
- /**
- * @param string $text
- * @return string
- */
- abstract public function format(/*string*/ $text);
-
- /**
- * @param string $text
- * @return string
- */
- abstract public function strip(/*string*/ $text);
-}
-
-/**
- * Class DataHandlerExtension
- *
- * This too is a common class of extension with many methods in common,
- * so we have a base class to extend from.
- */
-abstract class DataHandlerExtension extends Extension {
- /**
- * @param DataUploadEvent $event
- * @throws UploadException
- */
- public function onDataUpload(DataUploadEvent $event) {
- $supported_ext = $this->supported_ext($event->type);
- $check_contents = $this->check_contents($event->tmpname);
- if($supported_ext && $check_contents) {
- move_upload_to_archive($event);
- send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
-
- /* Check if we are replacing an image */
- if(array_key_exists('replace', $event->metadata) && isset($event->metadata['replace'])) {
- /* hax: This seems like such a dirty way to do this.. */
-
- /* Validate things */
- $image_id = int_escape($event->metadata['replace']);
-
- /* Check to make sure the image exists. */
- $existing = Image::by_id($image_id);
-
- if(is_null($existing)) {
- throw new UploadException("Image to replace does not exist!");
- }
- if ($existing->hash === $event->metadata['hash']) {
- throw new UploadException("The uploaded image is the same as the one to replace.");
- }
-
- // even more hax..
- $event->metadata['tags'] = $existing->get_tag_list();
- $image = $this->create_image_from_data(warehouse_path("images", $event->metadata['hash']), $event->metadata);
-
- if(is_null($image)) {
- throw new UploadException("Data handler failed to create image object from data");
- }
-
- $ire = new ImageReplaceEvent($image_id, $image);
- send_event($ire);
- $event->image_id = $image_id;
- }
- else {
- $image = $this->create_image_from_data(warehouse_path("images", $event->hash), $event->metadata);
- if(is_null($image)) {
- throw new UploadException("Data handler failed to create image object from data");
- }
- $iae = new ImageAdditionEvent($image);
- send_event($iae);
- $event->image_id = $iae->image->id;
-
- // Rating Stuff.
- if(!empty($event->metadata['rating'])){
- $rating = $event->metadata['rating'];
- send_event(new RatingSetEvent($image, $rating));
- }
-
- // Locked Stuff.
- if(!empty($event->metadata['locked'])){
- $locked = $event->metadata['locked'];
- send_event(new LockSetEvent($image, !empty($locked)));
- }
- }
- }
- elseif($supported_ext && !$check_contents){
- throw new UploadException("Invalid or corrupted file");
- }
- }
-
- /**
- * @param ThumbnailGenerationEvent $event
- */
- public function onThumbnailGeneration(ThumbnailGenerationEvent $event) {
- if($this->supported_ext($event->type)) {
- if (method_exists($this, 'create_thumb_force') && $event->force == true) {
- $this->create_thumb_force($event->hash);
- }
- else {
- $this->create_thumb($event->hash);
- }
- }
- }
-
- /**
- * @param DisplayingImageEvent $event
- */
- public function onDisplayingImage(DisplayingImageEvent $event) {
- global $page;
- if($this->supported_ext($event->image->ext)) {
- $this->theme->display_image($page, $event->image);
- }
- }
-
- /*
- public function onSetupBuilding(SetupBuildingEvent $event) {
- $sb = $this->setup();
- if($sb) $event->panel->add_block($sb);
- }
-
- protected function setup() {}
- */
-
- /**
- * @param string $ext
- * @return bool
- */
- abstract protected function supported_ext($ext);
-
- /**
- * @param string $tmpname
- * @return bool
- */
- abstract protected function check_contents($tmpname);
-
- /**
- * @param string $filename
- * @param array $metadata
- * @return Image|null
- */
- abstract protected function create_image_from_data($filename, $metadata);
-
- /**
- * @param string $hash
- * @return bool
- */
- abstract protected function create_thumb($hash);
-}
-
diff --git a/core/extension.php b/core/extension.php
new file mode 100644
index 00000000..d464b578
--- /dev/null
+++ b/core/extension.php
@@ -0,0 +1,505 @@
+formatted;
+ * \endcode
+ *
+ * An extension is something which is capable of reacting to events.
+ *
+ *
+ * \page hello The Hello World Extension
+ *
+ * \code
+ * // ext/hello/main.php
+ * public class HelloEvent extends Event {
+ * public function __construct($username) {
+ * $this->username = $username;
+ * }
+ * }
+ *
+ * public class Hello extends Extension {
+ * public function onPageRequest(PageRequestEvent $event) { // Every time a page request is sent
+ * global $user; // Look at the global "currently logged in user" object
+ * send_event(new HelloEvent($user->name)); // Broadcast a signal saying hello to that user
+ * }
+ * public function onHello(HelloEvent $event) { // When the "Hello" signal is recieved
+ * $this->theme->display_hello($event->username); // Display a message on the web page
+ * }
+ * }
+ *
+ * // ext/hello/theme.php
+ * public class HelloTheme extends Themelet {
+ * public function display_hello($username) {
+ * global $page;
+ * $h_user = html_escape($username); // Escape the data before adding it to the page
+ * $block = new Block("Hello!", "Hello there $h_user"); // HTML-safe variables start with "h_"
+ * $page->add_block($block); // Add the block to the page
+ * }
+ * }
+ *
+ * // ext/hello/test.php
+ * public class HelloTest extends SCorePHPUnitTestCase {
+ * public function testHello() {
+ * $this->get_page("post/list"); // View a page, any page
+ * $this->assert_text("Hello there"); // Check that the specified text is in that page
+ * }
+ * }
+ *
+ * // themes/mytheme/hello.theme.php
+ * public class CustomHelloTheme extends HelloTheme { // CustomHelloTheme overrides HelloTheme
+ * public function display_hello($username) { // the display_hello() function is customised
+ * global $page;
+ * $h_user = html_escape($username);
+ * $page->add_block(new Block(
+ * "Hello!",
+ * "Hello there $h_user, look at my snazzy custom theme!"
+ * );
+ * }
+ * }
+ * \endcode
+ *
+ */
+
+/**
+ * Class Extension
+ *
+ * send_event(BlahEvent()) -> onBlah($event)
+ *
+ * Also loads the theme object into $this->theme if available
+ *
+ * The original concept came from Artanis's Extension extension
+ * --> http://github.com/Artanis/simple-extension/tree/master
+ * Then re-implemented by Shish after he broke the forum and couldn't
+ * find the thread where the original was posted >_<
+ */
+abstract class Extension
+{
+ /** @var string */
+ public $key;
+
+ /** @var Themelet */
+ protected $theme;
+
+ /** @var ExtensionInfo */
+ public $info;
+
+ private static $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);
+ if ($this->info===null) {
+ throw new ScoreException("Info class not found for extension $class");
+ }
+ $this->key = $this->info->key;
+ }
+
+ /**
+ * Find the theme object for a given extension.
+ */
+ private function get_theme_object(string $base): ?Themelet
+ {
+ $custom = 'Custom'.$base.'Theme';
+ $normal = $base.'Theme';
+
+ if (class_exists($custom)) {
+ return new $custom();
+ } elseif (class_exists($normal)) {
+ return new $normal();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * 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()
+ {
+ self::$enabled_extensions = [];
+ foreach (array_merge(
+ ExtensionInfo::get_core_extensions(),
+ explode(",", EXTRA_EXTS)
+ ) as $key) {
+ $ext = ExtensionInfo::get_by_key($key);
+ if ($ext===null || !$ext->is_supported()) {
+ continue;
+ }
+ // FIXME: error if one of our dependencies isn't supported
+ self::$enabled_extensions[] = $ext->key;
+ 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
+ {
+ return implode(",", self::$enabled_extensions);
+ }
+
+ 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");
+ }
+}
+
+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";
+ public const SHIMMIE_URL = "http://code.shishnet.org/shimmie2/";
+ 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 const VISIBLE_ADMIN = "admin";
+ public const VISIBLE_HIDDEN = "hidden";
+ private const VALID_VISIBILITY = [self::VISIBLE_ADMIN, self::VISIBLE_HIDDEN];
+
+ public $key;
+
+ public $core = false;
+
+ public $beta = false;
+
+ public $name;
+ public $authors = [];
+ public $link;
+ public $license;
+ public $version;
+ public $dependencies = [];
+ public $visibility;
+ public $description;
+ public $documentation;
+
+ /** @var array which DBs this ext supports (blank for 'all') */
+ public $db_support = [];
+
+ /** @var bool */
+ private $supported = null;
+
+ /** @var string */
+ private $support_info = null;
+
+ public function is_supported(): bool
+ {
+ if ($this->supported===null) {
+ $this->check_support();
+ }
+ return $this->supported;
+ }
+
+ public function get_support_info(): string
+ {
+ if ($this->supported===null) {
+ $this->check_support();
+ }
+ return $this->support_info;
+ }
+
+ private static $all_info_by_key = [];
+ private static $all_info_by_class = [];
+ private static $core_extensions = [];
+
+ protected function __construct()
+ {
+ assert(!empty($this->key), "key field is required");
+ assert(!empty($this->name), "name field is required for extension $this->key");
+ assert(empty($this->visibility) || in_array($this->visibility, self::VALID_VISIBILITY), "Invalid visibility 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 = "";
+ if (!empty($this->db_support) && !in_array($database->get_driver_name(), $this->db_support)) {
+ $this->support_info .= "Database not supported. ";
+ }
+ // 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
+ {
+ 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 {
+ return null;
+ }
+ }
+
+ public static function load_all_extension_info()
+ {
+ foreach (getSubclassesOf("ExtensionInfo") as $class) {
+ $extension_info = new $class();
+ if (array_key_exists($extension_info->key, self::$all_info_by_key)) {
+ throw new ScoreException("Extension Info $class with key $extension_info->key has already been loaded");
+ }
+
+ self::$all_info_by_key[$extension_info->key] = $extension_info;
+ self::$all_info_by_class[$class] = $extension_info;
+ if ($extension_info->core===true) {
+ self::$core_extensions[] = $extension_info->key;
+ }
+ }
+ }
+}
+
+/**
+ * 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;
+}
+
+/**
+ * Class DataHandlerExtension
+ *
+ * This too is a common class of extension with many methods in common,
+ * so we have a base class to extend from.
+ */
+abstract class DataHandlerExtension extends Extension
+{
+ protected $SUPPORTED_EXT = [];
+
+ 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)
+ {
+ $supported_ext = $this->supported_ext($event->type);
+ $check_contents = $this->check_contents($event->tmpname);
+ if ($supported_ext && $check_contents) {
+ $this->move_upload_to_archive($event);
+ send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
+
+ /* Check if we are replacing an image */
+ if (array_key_exists('replace', $event->metadata) && isset($event->metadata['replace'])) {
+ /* hax: This seems like such a dirty way to do this.. */
+
+ /* Validate things */
+ $image_id = int_escape($event->metadata['replace']);
+
+ /* Check to make sure the image exists. */
+ $existing = Image::by_id($image_id);
+
+ if (is_null($existing)) {
+ throw new UploadException("Image to replace does not exist!");
+ }
+ if ($existing->hash === $event->metadata['hash']) {
+ throw new UploadException("The uploaded image is the same as the one to replace.");
+ }
+
+ // even more hax..
+ $event->metadata['tags'] = $existing->get_tag_list();
+ $image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->metadata['hash']), $event->metadata);
+ if (is_null($image)) {
+ throw new UploadException("Data handler failed to create image object from data");
+ }
+ try {
+ send_event(new MediaCheckPropertiesEvent($image));
+ } catch (MediaException $e) {
+ throw new UploadException("Unable to scan media properties: ".$e->getMessage());
+ }
+
+ send_event(new ImageReplaceEvent($image_id, $image));
+ $event->image_id = $image_id;
+ } else {
+ $image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
+ if (is_null($image)) {
+ throw new UploadException("Data handler failed to create image object from data");
+ }
+ try {
+ send_event(new MediaCheckPropertiesEvent($image));
+ } catch (MediaException $e) {
+ throw new UploadException("Unable to scan media properties: ".$e->getMessage());
+ }
+
+ $iae = send_event(new ImageAdditionEvent($image));
+ $event->image_id = $iae->image->id;
+ $event->merged = $iae->merged;
+
+ // Rating Stuff.
+ if (!empty($event->metadata['rating'])) {
+ $rating = $event->metadata['rating'];
+ send_event(new RatingSetEvent($image, $rating));
+ }
+
+ // Locked Stuff.
+ if (!empty($event->metadata['locked'])) {
+ $locked = $event->metadata['locked'];
+ send_event(new LockSetEvent($image, !empty($locked)));
+ }
+ }
+ } elseif ($supported_ext && !$check_contents) {
+ // We DO support this extension - but the file looks corrupt
+ throw new UploadException("Invalid or corrupted file");
+ }
+ }
+
+ public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
+ {
+ $result = false;
+ if ($this->supported_ext($event->type)) {
+ if ($event->force) {
+ $result = $this->create_thumb($event->hash, $event->type);
+ } else {
+ $outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash);
+ if (file_exists($outname)) {
+ return;
+ }
+ $result = $this->create_thumb($event->hash, $event->type);
+ }
+ }
+ if ($result) {
+ $event->generated = true;
+ }
+ }
+
+ public function onDisplayingImage(DisplayingImageEvent $event)
+ {
+ global $page;
+ if ($this->supported_ext($event->image->ext)) {
+ /** @noinspection PhpPossiblePolymorphicInvocationInspection */
+ $this->theme->display_image($page, $event->image);
+ }
+ }
+
+ public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
+ {
+ if ($this->supported_ext($event->ext)) {
+ $this->media_check_properties($event);
+ }
+ }
+
+ protected function create_image_from_data(string $filename, array $metadata): Image
+ {
+ global $config;
+
+ $image = new Image();
+
+ $image->filesize = $metadata['size'];
+ $image->hash = $metadata['hash'];
+ $image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
+ if ($config->get_bool("upload_use_mime")) {
+ $image->ext = get_extension(getMimeType($filename));
+ } else {
+ $image->ext = (($pos = strpos($metadata['extension'], '?')) !== false) ? substr($metadata['extension'], 0, $pos) : $metadata['extension'];
+ }
+ $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
+ $image->source = $metadata['source'];
+
+ return $image;
+ }
+
+ abstract protected function media_check_properties(MediaCheckPropertiesEvent $event): void;
+ abstract protected function check_contents(string $tmpname): bool;
+ abstract protected function create_thumb(string $hash, string $type): bool;
+
+ protected function supported_ext(string $ext): bool
+ {
+ return in_array(strtolower($ext), $this->SUPPORTED_EXT);
+ }
+
+ public static function get_all_supported_exts(): array
+ {
+ $arr = [];
+ foreach (getSubclassesOf("DataHandlerExtension") as $handler) {
+ $arr = array_merge($arr, (new $handler())->SUPPORTED_EXT);
+ }
+ return $arr;
+ }
+}
diff --git a/core/imageboard.pack.php b/core/imageboard.pack.php
deleted file mode 100644
index 0ee89a09..00000000
--- a/core/imageboard.pack.php
+++ /dev/null
@@ -1,1292 +0,0 @@
- image ID list
- * translators, eg:
- *
- * \li the item "fred" will search the image_tags table to find image IDs with the fred tag
- * \li the item "size=640x480" will search the images table to find image IDs of 640x480 images
- *
- * So the search "fred size=640x480" will calculate two lists and take the
- * intersection. (There are some optimisations in there making it more
- * complicated behind the scenes, but as long as you can turn a single word
- * into a list of image IDs, making a search plugin should be simple)
- */
-
-/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
-* Classes *
-\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
-/**
- * Class Image
- *
- * An object representing an entry in the images table.
- *
- * As of 2.2, this no longer necessarily represents an
- * image per se, but could be a video, sound file, or any
- * other supported upload type.
- */
-class Image {
- private static $tag_n = 0; // temp hack
- public static $order_sql = null; // this feels ugly
-
- /** @var null|int */
- public $id = null;
-
- /** @var int */
- public $height;
-
- /** @var int */
- public $width;
-
- /** @var string */
- public $hash;
-
- public $filesize;
-
- /** @var string */
- public $filename;
-
- /** @var string */
- public $ext;
-
- /** @var string[]|null */
- public $tag_array;
-
- /** @var int */
- public $owner_id;
-
- /** @var string */
- public $owner_ip;
-
- /** @var string */
- public $posted;
-
- /** @var string */
- public $source;
-
- /** @var boolean */
- public $locked;
-
- /**
- * One will very rarely construct an image directly, more common
- * would be to use Image::by_id, Image::by_hash, etc.
- *
- * @param null|mixed $row
- */
- public function __construct($row=null) {
- assert('is_null($row) || is_array($row)');
-
- if(!is_null($row)) {
- foreach($row as $name => $value) {
- // some databases use table.name rather than name
- $name = str_replace("images.", "", $name);
- $this->$name = $value; // hax, this is likely the cause of much scrutinizer-ci complaints.
- }
- $this->locked = bool_escape($this->locked);
-
- assert(is_numeric($this->id));
- assert(is_numeric($this->height));
- assert(is_numeric($this->width));
- }
- }
-
- /**
- * Find an image by ID.
- *
- * @param int $id
- * @return Image
- */
- public static function by_id(/*int*/ $id) {
- assert('is_numeric($id)');
- global $database;
- $row = $database->get_row("SELECT * FROM images WHERE images.id=:id", array("id"=>$id));
- return ($row ? new Image($row) : null);
- }
-
- /**
- * Find an image by hash.
- *
- * @param string $hash
- * @return Image
- */
- public static function by_hash(/*string*/ $hash) {
- assert('is_string($hash)');
- global $database;
- $row = $database->get_row("SELECT images.* FROM images WHERE hash=:hash", array("hash"=>$hash));
- return ($row ? new Image($row) : null);
- }
-
- /**
- * Pick a random image out of a set.
- *
- * @param string[] $tags
- * @return Image
- */
- public static function by_random($tags=array()) {
- assert('is_array($tags)');
- $max = Image::count_images($tags);
- if ($max < 1) return null; // From Issue #22 - opened by HungryFeline on May 30, 2011.
- $rand = mt_rand(0, $max-1);
- $set = Image::find_images($rand, 1, $tags);
- if(count($set) > 0) return $set[0];
- else return null;
- }
-
- /**
- * Search for an array of images
- *
- * @param int $start
- * @param int $limit
- * @param string[] $tags
- * @throws SCoreException
- * @return Image[]
- */
- public static function find_images(/*int*/ $start, /*int*/ $limit, $tags=array()) {
- assert('is_numeric($start)');
- assert('is_numeric($limit)');
- assert('is_array($tags)');
- global $database, $user, $config;
-
- $images = array();
-
- if($start < 0) $start = 0;
- if($limit < 1) $limit = 1;
-
- if(SPEED_HAX) {
- if(!$user->can("big_search") and count($tags) > 3) {
- throw new SCoreException("Anonymous users may only search for up to 3 tags at a time");
- }
- }
-
- $result = null;
- if(SEARCH_ACCEL) {
- $result = Image::get_accelerated_result($tags, $start, $limit);
- }
-
- if(!$result) {
- $querylet = Image::build_search_querylet($tags);
- $querylet->append(new Querylet(" ORDER BY ".(Image::$order_sql ?: "images.".$config->get_string("index_order"))));
- $querylet->append(new Querylet(" LIMIT :limit OFFSET :offset", array("limit"=>$limit, "offset"=>$start)));
- #var_dump($querylet->sql); var_dump($querylet->variables);
- $result = $database->execute($querylet->sql, $querylet->variables);
- }
-
- while($row = $result->fetch()) {
- $images[] = new Image($row);
- }
- Image::$order_sql = null;
- return $images;
- }
-
- /**
- * @param string[] $tags
- * @return boolean
- */
- public static function validate_accel($tags) {
- $yays = 0;
- $nays = 0;
- foreach($tags as $tag) {
- if(!preg_match("/^-?[a-zA-Z0-9_]+$/", $tag)) {
- return false;
- }
- if($tag[0] == "-") $nays++;
- else $yays++;
- }
- return ($yays > 1 || $nays > 0);
- }
-
- /**
- * @param string[] $tags
- * @param int $offset
- * @param int $limit
- * @return null|PDOStatement
- * @throws SCoreException
- */
- public static function get_accelerated_result($tags, $offset, $limit) {
- global $database;
-
- if(!Image::validate_accel($tags)) {
- return null;
- }
-
- $yays = array();
- $nays = array();
- foreach($tags as $tag) {
- if($tag[0] == "-") {
- $nays[] = substr($tag, 1);
- }
- else {
- $yays[] = $tag;
- }
- }
- $req = array(
- "yays" => $yays,
- "nays" => $nays,
- "offset" => $offset,
- "limit" => $limit,
- );
-
- $fp = fsockopen("127.0.0.1", 21212);
- if (!$fp) {
- return null;
- }
- fwrite($fp, json_encode($req));
- $data = fgets($fp, 1024);
- fclose($fp);
-
- $response = json_decode($data);
- $list = implode(",", $response);
- if($list) {
- $result = $database->execute("SELECT * FROM images WHERE id IN ($list) ORDER BY images.id DESC");
- }
- else {
- $result = $database->execute("SELECT * FROM images WHERE 1=0 ORDER BY images.id DESC");
- }
- return $result;
- }
-
- /*
- * Image-related utility functions
- */
-
- /**
- * Count the number of image results for a given search
- *
- * @param string[] $tags
- * @return int
- */
- public static function count_images($tags=array()) {
- assert('is_array($tags)');
- global $database;
- $tag_count = count($tags);
-
- if($tag_count === 0) {
- $total = $database->cache->get("image-count");
- if(!$total) {
- $total = $database->get_one("SELECT COUNT(*) FROM images");
- $database->cache->set("image-count", $total, 600);
- }
- return $total;
- }
- else if($tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
- return $database->get_one(
- $database->scoreql_to_sql("SELECT count FROM tags WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag)"),
- array("tag"=>$tags[0]));
- }
- else {
- $querylet = Image::build_search_querylet($tags);
- return $database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables);
- }
- }
-
- /**
- * Count the number of pages for a given search
- *
- * @param string[] $tags
- * @return float
- */
- public static function count_pages($tags=array()) {
- assert('is_array($tags)');
- global $config;
- return ceil(Image::count_images($tags) / $config->get_int('index_images'));
- }
-
- /*
- * Accessors & mutators
- */
-
- /**
- * Find the next image in the sequence.
- *
- * Rather than simply $this_id + 1, one must take into account
- * deleted images and search queries
- *
- * @param string[] $tags
- * @param bool $next
- * @return Image
- */
- public function get_next($tags=array(), $next=true) {
- assert('is_array($tags)');
- assert('is_bool($next)');
- global $database;
-
- if($next) {
- $gtlt = "<";
- $dir = "DESC";
- }
- else {
- $gtlt = ">";
- $dir = "ASC";
- }
-
- if(count($tags) === 0) {
- $row = $database->get_row('
- SELECT images.*
- FROM images
- WHERE images.id '.$gtlt.' '.$this->id.'
- ORDER BY images.id '.$dir.'
- LIMIT 1
- ');
- }
- else {
- $tags[] = 'id'. $gtlt . $this->id;
- $querylet = Image::build_search_querylet($tags);
- $querylet->append_sql(' ORDER BY images.id '.$dir.' LIMIT 1');
- $row = $database->get_row($querylet->sql, $querylet->variables);
- }
-
- return ($row ? new Image($row) : null);
- }
-
- /**
- * The reverse of get_next
- *
- * @param string[] $tags
- * @return Image
- */
- public function get_prev($tags=array()) {
- return $this->get_next($tags, false);
- }
-
- /**
- * Find the User who owns this Image
- *
- * @return User
- */
- public function get_owner() {
- return User::by_id($this->owner_id);
- }
-
- /**
- * Set the image's owner.
- *
- * @param User $owner
- */
- public function set_owner(User $owner) {
- global $database;
- if($owner->id != $this->owner_id) {
- $database->execute("
- UPDATE images
- SET owner_id=:owner_id
- WHERE id=:id
- ", array("owner_id"=>$owner->id, "id"=>$this->id));
- log_info("core_image", "Owner for Image #{$this->id} set to {$owner->name}", false, array("image_id" => $this->id));
- }
- }
-
- /**
- * Get this image's tags as an array.
- *
- * @return string[]
- */
- public function get_tag_array() {
- global $database;
- if(!isset($this->tag_array)) {
- $this->tag_array = $database->get_col("
- SELECT tag
- FROM image_tags
- JOIN tags ON image_tags.tag_id = tags.id
- WHERE image_id=:id
- ORDER BY tag
- ", array("id"=>$this->id));
- }
- return $this->tag_array;
- }
-
- /**
- * Get this image's tags as a string.
- *
- * @return string
- */
- public function get_tag_list() {
- return Tag::implode($this->get_tag_array());
- }
-
- /**
- * Get the URL for the full size image
- *
- * @return string
- */
- public function get_image_link() {
- return $this->get_link('image_ilink', '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext');
- }
-
- /**
- * Get the URL for the thumbnail
- *
- * @return string
- */
- public function get_thumb_link() {
- return $this->get_link('image_tlink', '_thumbs/$hash/thumb.jpg', 'thumb/$id.jpg');
- }
-
- /**
- * Check configured template for a link, then try nice URL, then plain URL
- *
- * @param string $template
- * @param string $nice
- * @param string $plain
- * @return string
- */
- private function get_link($template, $nice, $plain) {
- global $config;
-
- $image_link = $config->get_string($template);
-
- if(!empty($image_link)) {
- if(!(strpos($image_link, "://") > 0) && !startsWith($image_link, "/")) {
- $image_link = make_link($image_link);
- }
- return $this->parse_link_template($image_link);
- }
- else if($config->get_bool('nice_urls', false)) {
- return $this->parse_link_template(make_link($nice));
- }
- else {
- return $this->parse_link_template(make_link($plain));
- }
- }
-
- /**
- * Get the tooltip for this image, formatted according to the
- * configured template.
- *
- * @return string
- */
- public function get_tooltip() {
- global $config;
- $tt = $this->parse_link_template($config->get_string('image_tip'), "no_escape");
-
- // Removes the size tag if the file is an mp3
- if($this->ext === 'mp3'){
- $iitip = $tt;
- $mp3tip = array("0x0");
- $h_tip = str_replace($mp3tip, " ", $iitip);
-
- // Makes it work with a variation of the default tooltips (I.E $tags // $filesize // $size)
- $justincase = array(" //", "// ", " //", "// ", " ");
- if(strstr($h_tip, " ")) {
- $h_tip = html_escape(str_replace($justincase, "", $h_tip));
- }else{
- $h_tip = html_escape($h_tip);
- }
- return $h_tip;
- }
- else {
- return $tt;
- }
- }
-
- /**
- * Figure out where the full size image is on disk.
- *
- * @return string
- */
- public function get_image_filename() {
- return warehouse_path("images", $this->hash);
- }
-
- /**
- * Figure out where the thumbnail is on disk.
- *
- * @return string
- */
- public function get_thumb_filename() {
- return warehouse_path("thumbs", $this->hash);
- }
-
- /**
- * Get the original filename.
- *
- * @return string
- */
- public function get_filename() {
- return $this->filename;
- }
-
- /**
- * Get the image's mime type.
- *
- * @return string
- */
- public function get_mime_type() {
- return getMimeType($this->get_image_filename(), $this->get_ext());
- }
-
- /**
- * Get the image's filename extension
- *
- * @return string
- */
- public function get_ext() {
- return $this->ext;
- }
-
- /**
- * Get the image's source URL
- *
- * @return string
- */
- public function get_source() {
- return $this->source;
- }
-
- /**
- * Set the image's source URL
- *
- * @param string $new_source
- */
- public function set_source(/*string*/ $new_source) {
- global $database;
- $old_source = $this->source;
- if(empty($new_source)) $new_source = null;
- if($new_source != $old_source) {
- $database->execute("UPDATE images SET source=:source WHERE id=:id", array("source"=>$new_source, "id"=>$this->id));
- log_info("core_image", "Source for Image #{$this->id} set to: $new_source (was $old_source)", false, array("image_id" => $this->id));
- }
- }
-
- /**
- * Check if the image is locked.
- * @return bool
- */
- public function is_locked() {
- return $this->locked;
- }
-
- /**
- * @param bool $tf
- * @throws SCoreException
- */
- public function set_locked($tf) {
- global $database;
- $ln = $tf ? "Y" : "N";
- $sln = $database->scoreql_to_sql('SCORE_BOOL_'.$ln);
- $sln = str_replace("'", "", $sln);
- $sln = str_replace('"', "", $sln);
- if(bool_escape($sln) !== $this->locked) {
- $database->execute("UPDATE images SET locked=:yn WHERE id=:id", array("yn"=>$sln, "id"=>$this->id));
- log_info("core_image", "Setting Image #{$this->id} lock to: $ln", false, array("image_id" => $this->id));
- }
- }
-
- /**
- * Delete all tags from this image.
- *
- * Normally in preparation to set them to a new set.
- */
- public function delete_tags_from_image() {
- global $database;
- if($database->get_driver_name() == "mysql") {
- //mysql < 5.6 has terrible subquery optimization, using EXISTS / JOIN fixes this
- $database->execute("
- UPDATE tags t
- INNER JOIN image_tags it ON t.id = it.tag_id
- SET count = count - 1
- WHERE it.image_id = :id",
- array("id"=>$this->id)
- );
- } else {
- $database->execute("
- UPDATE tags
- SET count = count - 1
- WHERE id IN (
- SELECT tag_id
- FROM image_tags
- WHERE image_id = :id
- )
- ", array("id"=>$this->id));
- }
- $database->execute("
- DELETE
- FROM image_tags
- WHERE image_id=:id
- ", array("id"=>$this->id));
- }
-
- /**
- * Set the tags for this image.
- *
- * @param string[] $tags
- * @throws Exception
- */
- public function set_tags($tags) {
- assert('is_array($tags) && count($tags) > 0', var_export($tags, true));
- global $database;
-
- if(count($tags) <= 0) {
- throw new SCoreException('Tried to set zero tags');
- }
-
- if(implode(" ", $tags) != $this->get_tag_list()) {
- // delete old
- $this->delete_tags_from_image();
- // insert each new tags
- foreach($tags as $tag) {
- if(mb_strlen($tag, 'UTF-8') > 255){
- flash_message("The tag below is longer than 255 characters, please use a shorter tag.\n$tag\n");
- continue;
- }
-
- $id = $database->get_one(
- $database->scoreql_to_sql("
- SELECT id
- FROM tags
- WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag)
- "),
- array("tag"=>$tag)
- );
- if(empty($id)) {
- // a new tag
- $database->execute(
- "INSERT INTO tags(tag) VALUES (:tag)",
- array("tag"=>$tag));
- $database->execute(
- "INSERT INTO image_tags(image_id, tag_id)
- VALUES(:id, (SELECT id FROM tags WHERE tag = :tag))",
- array("id"=>$this->id, "tag"=>$tag));
- }
- else {
- // user of an existing tag
- $database->execute("
- INSERT INTO image_tags(image_id, tag_id)
- VALUES(:iid, :tid)
- ", array("iid"=>$this->id, "tid"=>$id));
- }
- $database->execute(
- $database->scoreql_to_sql("
- UPDATE tags
- SET count = count + 1
- WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag)
- "),
- array("tag"=>$tag)
- );
- }
-
- log_info("core_image", "Tags for Image #{$this->id} set to: ".implode(" ", $tags), null, array("image_id" => $this->id));
- $database->cache->delete("image-{$this->id}-tags");
- }
- }
-
- /**
- * Send list of metatags to be parsed.
- *
- * @param string[] $metatags
- * @param int $image_id
- */
- public function parse_metatags($metatags, $image_id) {
- foreach($metatags as $tag) {
- $ttpe = new TagTermParseEvent($tag, $image_id, TRUE);
- send_event($ttpe);
- }
- }
-
- /**
- * Delete this image from the database and disk
- */
- public function delete() {
- global $database;
- $this->delete_tags_from_image();
- $database->execute("DELETE FROM images WHERE id=:id", array("id"=>$this->id));
- log_info("core_image", 'Deleted Image #'.$this->id.' ('.$this->hash.')', false, array("image_id" => $this->id));
-
- unlink($this->get_image_filename());
- unlink($this->get_thumb_filename());
- }
-
- /**
- * This function removes an image (and thumbnail) from the DISK ONLY.
- * It DOES NOT remove anything from the database.
- */
- public function remove_image_only() {
- log_info("core_image", 'Removed Image File ('.$this->hash.')', false, array("image_id" => $this->id));
- @unlink($this->get_image_filename());
- @unlink($this->get_thumb_filename());
- }
-
- /**
- * Someone please explain this
- *
- * @param string $tmpl
- * @param string $_escape
- * @return string
- */
- public function parse_link_template($tmpl, $_escape="url_escape") {
- global $config;
-
- // don't bother hitting the database if it won't be used...
- $tags = "";
- if(strpos($tmpl, '$tags') !== false) { // * stabs dynamically typed languages with a rusty spoon *
- $tags = $this->get_tag_list();
- $tags = str_replace("/", "", $tags);
- $tags = preg_replace("/^\.+/", "", $tags);
- }
-
- $base_href = $config->get_string('base_href');
- $fname = $this->get_filename();
- $base_fname = strpos($fname, '.') ? substr($fname, 0, strrpos($fname, '.')) : $fname;
-
- $tmpl = str_replace('$id', $this->id, $tmpl);
- $tmpl = str_replace('$hash_ab', substr($this->hash, 0, 2), $tmpl);
- $tmpl = str_replace('$hash_cd', substr($this->hash, 2, 2), $tmpl);
- $tmpl = str_replace('$hash', $this->hash, $tmpl);
- $tmpl = str_replace('$tags', $_escape($tags), $tmpl);
- $tmpl = str_replace('$base', $base_href, $tmpl);
- $tmpl = str_replace('$ext', $this->ext, $tmpl);
- $tmpl = str_replace('$size', "{$this->width}x{$this->height}", $tmpl);
- $tmpl = str_replace('$filesize', to_shorthand_int($this->filesize), $tmpl);
- $tmpl = str_replace('$filename', $_escape($base_fname), $tmpl);
- $tmpl = str_replace('$title', $_escape($config->get_string("title")), $tmpl);
- $tmpl = str_replace('$date', $_escape(autodate($this->posted, false)), $tmpl);
-
- // nothing seems to use this, sending the event out to 50 exts is a lot of overhead
- if(!SPEED_HAX) {
- $plte = new ParseLinkTemplateEvent($tmpl, $this);
- send_event($plte);
- $tmpl = $plte->link;
- }
-
- static $flexihash = null;
- static $fh_last_opts = null;
- $matches = array();
- if(preg_match("/(.*){(.*)}(.*)/", $tmpl, $matches)) {
- $pre = $matches[1];
- $opts = $matches[2];
- $post = $matches[3];
-
- if($opts != $fh_last_opts) {
- $fh_last_opts = $opts;
- $flexihash = new Flexihash\Flexihash();
- foreach(explode(",", $opts) as $opt) {
- $parts = explode("=", $opt);
- $parts_count = count($parts);
- $opt_val = "";
- $opt_weight = 0;
- if($parts_count === 2) {
- $opt_val = $parts[0];
- $opt_weight = $parts[1];
- }
- elseif($parts_count === 1) {
- $opt_val = $parts[0];
- $opt_weight = 1;
- }
- $flexihash->addTarget($opt_val, $opt_weight);
- }
- }
-
- $choice = $flexihash->lookup($pre.$post);
- $tmpl = $pre.$choice.$post;
- }
-
- return $tmpl;
- }
-
- /**
- * @param string[] $terms
- * @return \Querylet
- */
- private static function build_search_querylet($terms) {
- assert('is_array($terms)');
- global $database;
-
- $tag_querylets = array();
- $img_querylets = array();
- $positive_tag_count = 0;
- $negative_tag_count = 0;
-
- /*
- * Turn a bunch of strings into a bunch of TagQuerylet
- * and ImgQuerylet objects
- */
- $stpe = new SearchTermParseEvent(null, $terms);
- send_event($stpe);
- if ($stpe->is_querylet_set()) {
- foreach ($stpe->get_querylets() as $querylet) {
- $img_querylets[] = new ImgQuerylet($querylet, true);
- }
- }
-
- foreach ($terms as $term) {
- $positive = true;
- if (is_string($term) && !empty($term) && ($term[0] == '-')) {
- $positive = false;
- $term = substr($term, 1);
- }
- if (strlen($term) === 0) {
- continue;
- }
-
- $stpe = new SearchTermParseEvent($term, $terms);
- send_event($stpe);
- if ($stpe->is_querylet_set()) {
- foreach ($stpe->get_querylets() as $querylet) {
- $img_querylets[] = new ImgQuerylet($querylet, $positive);
- }
- }
- else {
- // if the whole match is wild, skip this;
- // if not, translate into SQL
- if(str_replace("*", "", $term) != "") {
- $term = str_replace('_', '\_', $term);
- $term = str_replace('%', '\%', $term);
- $term = str_replace('*', '%', $term);
- $tag_querylets[] = new TagQuerylet($term, $positive);
- if ($positive) $positive_tag_count++;
- else $negative_tag_count++;
- }
- }
- }
-
- /*
- * Turn a bunch of Querylet objects into a base query
- *
- * Must follow the format
- *
- * SELECT images.*
- * FROM (...) AS images
- * WHERE (...)
- *
- * ie, return a set of images.* columns, and end with a WHERE
- */
-
- // no tags, do a simple search
- if($positive_tag_count === 0 && $negative_tag_count === 0) {
- $query = new Querylet("
- SELECT images.*
- FROM images
- WHERE 1=1
- ");
- }
-
- // one positive tag (a common case), do an optimised search
- else if($positive_tag_count === 1 && $negative_tag_count === 0) {
- # "LIKE" to account for wildcards
- $query = new Querylet($database->scoreql_to_sql("
- SELECT *
- FROM (
- SELECT images.*
- FROM images
- JOIN image_tags ON images.id=image_tags.image_id
- JOIN tags ON image_tags.tag_id=tags.id
- WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag)
- GROUP BY images.id
- ) AS images
- WHERE 1=1
- "), array("tag"=>$tag_querylets[0]->tag));
- }
-
- // more than one positive tag, or more than zero negative tags
- else {
- if($database->get_driver_name() === "mysql")
- $query = Image::build_ugly_search_querylet($tag_querylets);
- else
- $query = Image::build_accurate_search_querylet($tag_querylets);
- }
-
- /*
- * Merge all the image metadata searches into one generic querylet
- * and append to the base querylet with "AND blah"
- */
- if(!empty($img_querylets)) {
- $n = 0;
- $img_sql = "";
- $img_vars = array();
- foreach ($img_querylets as $iq) {
- if ($n++ > 0) $img_sql .= " AND";
- if (!$iq->positive) $img_sql .= " NOT";
- $img_sql .= " (" . $iq->qlet->sql . ")";
- $img_vars = array_merge($img_vars, $iq->qlet->variables);
- }
- $query->append_sql(" AND ");
- $query->append(new Querylet($img_sql, $img_vars));
- }
-
- return $query;
- }
-
- /**
- * WARNING: this description is no longer accurate, though it does get across
- * the general idea - the actual method has a few extra optimisations
- *
- * "foo bar -baz user=foo" becomes
- *
- * SELECT * FROM images WHERE
- * images.id IN (SELECT image_id FROM image_tags WHERE tag='foo')
- * AND images.id IN (SELECT image_id FROM image_tags WHERE tag='bar')
- * AND NOT images.id IN (SELECT image_id FROM image_tags WHERE tag='baz')
- * AND images.id IN (SELECT id FROM images WHERE owner_name='foo')
- *
- * This is:
- * A) Incredibly simple:
- * Each search term maps to a list of image IDs
- * B) Runs really fast on a good database:
- * These lists are calculated once, and the set intersection taken
- * C) Runs really slow on bad databases:
- * All the subqueries are executed every time for every row in the
- * images table. Yes, MySQL does suck this much.
- *
- * @param TagQuerylet[] $tag_querylets
- * @return Querylet
- */
- private static function build_accurate_search_querylet($tag_querylets) {
- global $database;
-
- $positive_tag_id_array = array();
- $negative_tag_id_array = array();
-
- foreach ($tag_querylets as $tq) {
- $tag_ids = $database->get_col(
- $database->scoreql_to_sql("
- SELECT id
- FROM tags
- WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag)
- "),
- array("tag" => $tq->tag)
- );
- if ($tq->positive) {
- $positive_tag_id_array = array_merge($positive_tag_id_array, $tag_ids);
- if (count($tag_ids) == 0) {
- # one of the positive tags had zero results, therefor there
- # can be no results; "where 1=0" should shortcut things
- return new Querylet("
- SELECT images.*
- FROM images
- WHERE 1=0
- ");
- }
- } else {
- $negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids);
- }
- }
-
- assert('$positive_tag_id_array || $negative_tag_id_array', @$_GET['q']);
- $wheres = array();
- if (!empty($positive_tag_id_array)) {
- $positive_tag_id_list = join(', ', $positive_tag_id_array);
- $wheres[] = "tag_id IN ($positive_tag_id_list)";
- }
- if (!empty($negative_tag_id_array)) {
- $negative_tag_id_list = join(', ', $negative_tag_id_array);
- $wheres[] = "tag_id NOT IN ($negative_tag_id_list)";
- }
- $wheres_str = join(" AND ", $wheres);
- return new Querylet("
- SELECT images.*
- FROM images
- WHERE images.id IN (
- SELECT image_id
- FROM image_tags
- WHERE $wheres_str
- GROUP BY image_id
- HAVING COUNT(image_id) >= :search_score
- )
- ", array("search_score"=>count($positive_tag_id_array)));
- }
-
- /**
- * this function exists because mysql is a turd, see the docs for
- * build_accurate_search_querylet() for a full explanation
- *
- * @param TagQuerylet[] $tag_querylets
- * @return Querylet
- */
- private static function build_ugly_search_querylet($tag_querylets) {
- global $database;
-
- $positive_tag_count = 0;
- foreach($tag_querylets as $tq) {
- if($tq->positive) $positive_tag_count++;
- }
-
- // only negative tags - shortcut to fail
- if($positive_tag_count == 0) {
- // TODO: This isn't currently implemented.
- // SEE: https://github.com/shish/shimmie2/issues/66
- return new Querylet("
- SELECT images.*
- FROM images
- WHERE 1=0
- ");
- }
-
- // merge all the tag querylets into one generic one
- $sql = "0";
- $terms = array();
- foreach($tag_querylets as $tq) {
- $sign = $tq->positive ? "+" : "-";
- $sql .= ' '.$sign.' IF(SUM(tag LIKE :tag'.Image::$tag_n.'), 1, 0)';
- $terms['tag'.Image::$tag_n] = $tq->tag;
- Image::$tag_n++;
- }
- $tag_search = new Querylet($sql, $terms);
-
- $tag_id_array = array();
-
- foreach($tag_querylets as $tq) {
- $tag_ids = $database->get_col(
- $database->scoreql_to_sql("
- SELECT id
- FROM tags
- WHERE SCORE_STRNORM(tag) LIKE SCORE_STRNORM(:tag)
- "),
- array("tag" => $tq->tag)
- );
- $tag_id_array = array_merge($tag_id_array, $tag_ids);
-
- if($tq->positive && count($tag_ids) == 0) {
- # one of the positive tags had zero results, therefor there
- # can be no results; "where 1=0" should shortcut things
- return new Querylet("
- SELECT images.*
- FROM images
- WHERE 1=0
- ");
- }
- }
-
- Image::$tag_n = 0;
- return new Querylet('
- SELECT *
- FROM (
- SELECT images.*, ('.$tag_search->sql.') AS score
- FROM images
- LEFT JOIN image_tags ON image_tags.image_id = images.id
- JOIN tags ON image_tags.tag_id = tags.id
- WHERE tags.id IN (' . join(', ', $tag_id_array) . ')
- GROUP BY images.id
- HAVING score = :score
- ) AS images
- WHERE 1=1
- ', array_merge(
- $tag_search->variables,
- array("score"=>$positive_tag_count)
- ));
- }
-}
-
-/**
- * Class Tag
- *
- * A class for organising the tag related functions.
- *
- * All the methods are static, one should never actually use a tag object.
- *
- */
-class Tag {
- /**
- * @param string[] $tags
- * @return string
- */
- public static function implode($tags) {
- assert('is_array($tags)');
-
- sort($tags);
- $tags = implode(' ', $tags);
-
- return $tags;
- }
-
- /**
- * Turn a human-supplied string into a valid tag array.
- *
- * @param string $tags
- * @param bool $tagme add "tagme" if the string is empty
- * @return string[]
- */
- public static function explode($tags, $tagme=true) {
- global $database;
- assert('is_string($tags)');
-
- $tags = explode(' ', trim($tags));
-
- /* sanitise by removing invisible / dodgy characters */
- $tag_array = array();
- foreach($tags as $tag) {
- $tag = preg_replace("/\s/", "", $tag); # whitespace
- $tag = preg_replace('/\x20(\x0e|\x0f)/', '', $tag); # unicode RTL
- $tag = preg_replace("/\.+/", ".", $tag); # strings of dots?
- $tag = preg_replace("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes?
- $tag = trim($tag, ", \t\n\r\0\x0B");
-
- if(mb_strlen($tag, 'UTF-8') > 255){
- flash_message("The tag below is longer than 255 characters, please use a shorter tag.\n$tag\n");
- continue;
- }
-
- if(!empty($tag)) {
- $tag_array[] = $tag;
- }
- }
-
- /* if user supplied a blank string, add "tagme" */
- if(count($tag_array) === 0 && $tagme) {
- $tag_array = array("tagme");
- }
-
- /* resolve aliases */
- $new = array();
- $i = 0;
- $tag_count = count($tag_array);
- while($i<$tag_count) {
- $tag = $tag_array[$i];
- $negative = '';
- if(!empty($tag) && ($tag[0] == '-')) {
- $negative = '-';
- $tag = substr($tag, 1);
- }
-
- $newtags = $database->get_one(
- $database->scoreql_to_sql("
- SELECT newtag
- FROM aliases
- WHERE SCORE_STRNORM(oldtag)=SCORE_STRNORM(:tag)
- "),
- array("tag"=>$tag)
- );
- if(empty($newtags)) {
- //tag has no alias, use old tag
- $aliases = array($tag);
- }
- else {
- $aliases = Tag::explode($newtags);
- }
-
- foreach($aliases as $alias) {
- if(!in_array($alias, $new)) {
- if($tag == $alias) {
- $new[] = $negative.$alias;
- }
- elseif(!in_array($alias, $tag_array)) {
- $tag_array[] = $negative.$alias;
- $tag_count++;
- }
- }
- }
- $i++;
- }
-
- /* remove any duplicate tags */
- $tag_array = array_iunique($new);
-
- /* tidy up */
- sort($tag_array);
-
- return $tag_array;
- }
-}
-
-
-/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
-* Misc functions *
-\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
-/**
- * Move a file from PHP's temporary area into shimmie's image storage
- * hierarchy, or throw an exception trying.
- *
- * @param DataUploadEvent $event
- * @throws UploadException
- */
-function move_upload_to_archive(DataUploadEvent $event) {
- $target = warehouse_path("images", $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']}"
- );
- }
-}
-
-/**
- * Add a directory full of images
- *
- * @param $base string
- * @return array|string[]
- */
-function add_dir($base) {
- $results = array();
-
- foreach(list_files($base) as $full_path) {
- $short_path = str_replace($base, "", $full_path);
- $filename = basename($full_path);
-
- $tags = path_to_tags($short_path);
- $result = "$short_path (".str_replace(" ", ", ", $tags).")... ";
- try {
- add_image($full_path, $filename, $tags);
- $result .= "ok";
- }
- catch(UploadException $ex) {
- $result .= "failed: ".$ex->getMessage();
- }
- $results[] = $result;
- }
-
- return $results;
-}
-
-/**
- * @param string $tmpname
- * @param string $filename
- * @param string $tags
- * @throws UploadException
- */
-function add_image($tmpname, $filename, $tags) {
- assert(file_exists($tmpname));
-
- $pathinfo = pathinfo($filename);
- if(!array_key_exists('extension', $pathinfo)) {
- throw new UploadException("File has no extension");
- }
- $metadata = array();
- $metadata['filename'] = $pathinfo['basename'];
- $metadata['extension'] = $pathinfo['extension'];
- $metadata['tags'] = Tag::explode($tags);
- $metadata['source'] = null;
- $event = new DataUploadEvent($tmpname, $metadata);
- send_event($event);
- if($event->image_id == -1) {
- throw new UploadException("File type not recognised");
- }
-}
-
-/**
- * Given a full size pair of dimensions, return a pair scaled down to fit
- * into the configured thumbnail square, with ratio intact
- *
- * @param int $orig_width
- * @param int $orig_height
- * @return integer[]
- */
-function get_thumbnail_size(/*int*/ $orig_width, /*int*/ $orig_height) {
- global $config;
-
- if($orig_width === 0) $orig_width = 192;
- if($orig_height === 0) $orig_height = 192;
-
- if($orig_width > $orig_height * 5) $orig_width = $orig_height * 5;
- if($orig_height > $orig_width * 5) $orig_height = $orig_width * 5;
-
- $max_width = $config->get_int('thumb_width');
- $max_height = $config->get_int('thumb_height');
-
- $xscale = ($max_height / $orig_height);
- $yscale = ($max_width / $orig_width);
- $scale = ($xscale < $yscale) ? $xscale : $yscale;
-
- if($scale > 1 && $config->get_bool('thumb_upscale')) {
- return array((int)$orig_width, (int)$orig_height);
- }
- else {
- return array((int)($orig_width*$scale), (int)($orig_height*$scale));
- }
-}
-
diff --git a/core/imageboard/event.php b/core/imageboard/event.php
new file mode 100644
index 00000000..27a00f96
--- /dev/null
+++ b/core/imageboard/event.php
@@ -0,0 +1,149 @@
+image = $image;
+ }
+}
+
+class ImageAdditionException extends SCoreException
+{
+}
+
+/**
+ * An image is being deleted.
+ */
+class ImageDeletionEvent extends Event
+{
+ /** @var Image */
+ public $image;
+
+ /** @var bool */
+ public $force = false;
+
+ /**
+ * Deletes an image.
+ *
+ * Used by things like tags and comments handlers to
+ * clean out related rows in their tables.
+ */
+ public function __construct(Image $image, bool $force = false)
+ {
+ parent::__construct();
+ $this->image = $image;
+ $this->force = $force;
+ }
+}
+
+/**
+ * An image is being replaced.
+ */
+class ImageReplaceEvent extends Event
+{
+ /** @var int */
+ public $id;
+ /** @var Image */
+ public $image;
+
+ /**
+ * Replaces an image.
+ *
+ * Updates an existing ID in the database to use a new image
+ * file, leaving the tags and such unchanged. Also removes
+ * the old image file and thumbnail from the disk.
+ */
+ public function __construct(int $id, Image $image)
+ {
+ parent::__construct();
+ $this->id = $id;
+ $this->image = $image;
+ }
+}
+
+class ImageReplaceException extends SCoreException
+{
+}
+
+/**
+ * Request a thumbnail be made for an image object.
+ */
+class ThumbnailGenerationEvent extends Event
+{
+ /** @var string */
+ public $hash;
+ /** @var string */
+ public $type;
+ /** @var bool */
+ public $force;
+
+ /** @var bool */
+ public $generated;
+
+ /**
+ * Request a thumbnail be made for an image object
+ */
+ public function __construct(string $hash, string $type, bool $force=false)
+ {
+ parent::__construct();
+ $this->hash = $hash;
+ $this->type = $type;
+ $this->force = $force;
+ $this->generated = false;
+ }
+}
+
+
+/*
+ * ParseLinkTemplateEvent:
+ * $link -- the formatted text (with each element URL Escape'd)
+ * $text -- the formatted text (not escaped)
+ * $original -- the formatting string, for reference
+ * $image -- the image who's link is being parsed
+ */
+class ParseLinkTemplateEvent extends Event
+{
+ /** @var string */
+ public $link;
+ /** @var string */
+ public $text;
+ /** @var string */
+ public $original;
+ /** @var Image */
+ public $image;
+
+ public function __construct(string $link, Image $image)
+ {
+ parent::__construct();
+ $this->link = $link;
+ $this->text = $link;
+ $this->original = $link;
+ $this->image = $image;
+ }
+
+ public function replace(string $needle, ?string $replace): void
+ {
+ if (!is_null($replace)) {
+ $this->link = str_replace($needle, url_escape($replace), $this->link);
+ $this->text = str_replace($needle, $replace, $this->text);
+ }
+ }
+}
diff --git a/core/imageboard/image.php b/core/imageboard/image.php
new file mode 100644
index 00000000..513ddbc1
--- /dev/null
+++ b/core/imageboard/image.php
@@ -0,0 +1,983 @@
+ $value) {
+ // some databases use table.name rather than name
+ $name = str_replace("images.", "", $name);
+
+ // hax, this is likely the cause of much scrutinizer-ci complaints.
+ if (is_null($value)) {
+ $this->$name = null;
+ } elseif (in_array($name, self::$bool_props)) {
+ $this->$name = bool_escape((string)$value);
+ } elseif (in_array($name, self::$int_props)) {
+ $this->$name = int_escape((string)$value);
+ } else {
+ $this->$name = $value;
+ }
+ }
+ }
+ }
+
+ public static function by_id(int $id): ?Image
+ {
+ global $database;
+ $row = $database->get_row("SELECT * FROM images WHERE images.id=:id", ["id"=>$id]);
+ return ($row ? new Image($row) : null);
+ }
+
+ public static function by_hash(string $hash): ?Image
+ {
+ global $database;
+ $hash = strtolower($hash);
+ $row = $database->get_row("SELECT images.* FROM images WHERE hash=:hash", ["hash"=>$hash]);
+ return ($row ? new Image($row) : null);
+ }
+
+ public static function by_id_or_hash(string $id): ?Image
+ {
+ return (is_numeric($id) && strlen($id) != 32) ? Image::by_id((int)$id) : Image::by_hash($id);
+ }
+
+ public static function by_random(array $tags=[], int $limit_range=0): ?Image
+ {
+ $max = Image::count_images($tags);
+ if ($max < 1) {
+ return null;
+ } // From Issue #22 - opened by HungryFeline on May 30, 2011.
+ if ($limit_range > 0 && $max > $limit_range) {
+ $max = $limit_range;
+ }
+ $rand = mt_rand(0, $max-1);
+ $set = Image::find_images($rand, 1, $tags);
+ if (count($set) > 0) {
+ return $set[0];
+ } else {
+ return null;
+ }
+ }
+
+ private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags=[]): iterable
+ {
+ global $database, $user, $config;
+
+ if ($start < 0) {
+ $start = 0;
+ }
+ if ($limit!=null && $limit < 1) {
+ $limit = 1;
+ }
+
+ if (SPEED_HAX) {
+ if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) {
+ throw new SCoreException("Anonymous users may only search for up to 3 tags at a time");
+ }
+ }
+
+ $order = (Image::$order_sql ?: "images.".$config->get_string(IndexConfig::ORDER));
+ $querylet = Image::build_search_querylet($tags, $order, $limit, $start);
+ $result = $database->get_all_iterable($querylet->sql, $querylet->variables);
+
+ Image::$order_sql = null;
+
+ return $result;
+ }
+
+ /**
+ * Search for an array of images
+ *
+ * #param string[] $tags
+ * #return Image[]
+ */
+ public static function find_images(int $start, ?int $limit = null, array $tags=[]): array
+ {
+ $result = self::find_images_internal($start, $limit, $tags);
+
+ $images = [];
+ foreach ($result as $row) {
+ $images[] = new Image($row);
+ }
+ return $images;
+ }
+
+ /**
+ * Search for an array of images, returning a iterable object of Image
+ */
+ public static function find_images_iterable(int $start = 0, ?int $limit = null, array $tags=[]): Generator
+ {
+ $result = self::find_images_internal($start, $limit, $tags);
+ foreach ($result as $row) {
+ yield new Image($row);
+ }
+ }
+
+ /*
+ * Image-related utility functions
+ */
+
+ public static function count_total_images(): int
+ {
+ global $cache, $database;
+ $total = $cache->get("image-count");
+ if (!$total) {
+ $total = (int)$database->get_one("SELECT COUNT(*) FROM images");
+ $cache->set("image-count", $total, 600);
+ }
+ return $total;
+ }
+
+ public static function count_tag(string $tag): int
+ {
+ global $database;
+ return (int)$database->get_one(
+ "SELECT count FROM tags WHERE LOWER(tag) = LOWER(:tag)",
+ ["tag"=>$tag]
+ );
+ }
+
+ /**
+ * Count the number of image results for a given search
+ *
+ * #param string[] $tags
+ */
+ public static function count_images(array $tags=[]): int
+ {
+ global $cache, $database;
+ $tag_count = count($tags);
+
+ if ($tag_count === 0) {
+ // total number of images in the DB
+ $total = self::count_total_images();
+ } elseif ($tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
+ if (!startsWith($tags[0], "-")) {
+ // one tag - we can look that up directly
+ $total = self::count_tag($tags[0]);
+ } else {
+ // one negative tag - subtract from the total
+ $total = self::count_total_images() - self::count_tag(substr($tags[0], 1));
+ }
+ } else {
+ // complex query
+ // implode(tags) can be too long for memcache...
+ $cache_key = "image-count:" . md5(Tag::implode($tags));
+ $total = $cache->get($cache_key);
+ if (!$total) {
+ if (Extension::is_enabled(RatingsInfo::KEY)) {
+ $tags[] = "rating:*";
+ }
+ $querylet = Image::build_search_querylet($tags);
+ $total = (int)$database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables);
+ if (SPEED_HAX && $total > 5000) {
+ // when we have a ton of images, the count
+ // won't change dramatically very often
+ $cache->set($cache_key, $total, 3600);
+ }
+ }
+ }
+ if (is_null($total)) {
+ return 0;
+ }
+ return $total;
+ }
+
+ /**
+ * Count the number of pages for a given search
+ *
+ * #param string[] $tags
+ */
+ public static function count_pages(array $tags=[]): int
+ {
+ global $config;
+ return (int)ceil(Image::count_images($tags) / $config->get_int(IndexConfig::IMAGES));
+ }
+
+ private static function terms_to_conditions(array $terms): array
+ {
+ $tag_conditions = [];
+ $img_conditions = [];
+
+ /*
+ * Turn a bunch of strings into a bunch of TagCondition
+ * and ImgCondition objects
+ */
+ $stpe = send_event(new SearchTermParseEvent(null, $terms));
+ if ($stpe->is_querylet_set()) {
+ foreach ($stpe->get_querylets() as $querylet) {
+ $img_conditions[] = new ImgCondition($querylet, true);
+ }
+ }
+
+ foreach ($terms as $term) {
+ $positive = true;
+ if (is_string($term) && !empty($term) && ($term[0] == '-')) {
+ $positive = false;
+ $term = substr($term, 1);
+ }
+ if (strlen($term) === 0) {
+ continue;
+ }
+
+ $stpe = send_event(new SearchTermParseEvent($term, $terms));
+ if ($stpe->is_querylet_set()) {
+ foreach ($stpe->get_querylets() as $querylet) {
+ $img_conditions[] = new ImgCondition($querylet, $positive);
+ }
+ } else {
+ // if the whole match is wild, skip this
+ if (str_replace("*", "", $term) != "") {
+ $tag_conditions[] = new TagCondition($term, $positive);
+ }
+ }
+ }
+ return [$tag_conditions, $img_conditions];
+ }
+
+ /*
+ * Accessors & mutators
+ */
+
+ /**
+ * Find the next image in the sequence.
+ *
+ * Rather than simply $this_id + 1, one must take into account
+ * deleted images and search queries
+ *
+ * #param string[] $tags
+ */
+ public function get_next(array $tags=[], bool $next=true): ?Image
+ {
+ global $database;
+
+ if ($next) {
+ $gtlt = "<";
+ $dir = "DESC";
+ } else {
+ $gtlt = ">";
+ $dir = "ASC";
+ }
+
+ if (count($tags) === 0) {
+ $row = $database->get_row('
+ SELECT images.*
+ FROM images
+ WHERE images.id '.$gtlt.' '.$this->id.'
+ ORDER BY images.id '.$dir.'
+ LIMIT 1
+ ');
+ } else {
+ $tags[] = 'id'. $gtlt . $this->id;
+ $querylet = Image::build_search_querylet($tags);
+ $querylet->append_sql(' ORDER BY images.id '.$dir.' LIMIT 1');
+ $row = $database->get_row($querylet->sql, $querylet->variables);
+ }
+
+ return ($row ? new Image($row) : null);
+ }
+
+ /**
+ * The reverse of get_next
+ *
+ * #param string[] $tags
+ */
+ public function get_prev(array $tags=[]): ?Image
+ {
+ return $this->get_next($tags, false);
+ }
+
+ /**
+ * Find the User who owns this Image
+ */
+ public function get_owner(): User
+ {
+ return User::by_id($this->owner_id);
+ }
+
+ /**
+ * Set the image's owner.
+ */
+ public function set_owner(User $owner): void
+ {
+ global $database;
+ if ($owner->id != $this->owner_id) {
+ $database->execute("
+ UPDATE images
+ SET owner_id=:owner_id
+ WHERE id=:id
+ ", ["owner_id"=>$owner->id, "id"=>$this->id]);
+ log_info("core_image", "Owner for Image #{$this->id} set to {$owner->name}");
+ }
+ }
+
+ public function save_to_db()
+ {
+ global $database, $user;
+ $cut_name = substr($this->filename, 0, 255);
+
+ if (is_null($this->id)) {
+ $database->execute(
+ "INSERT INTO images(
+ owner_id, owner_ip,
+ filename, filesize,
+ hash, ext,
+ width, height,
+ posted, source
+ )
+ VALUES (
+ :owner_id, :owner_ip,
+ :filename, :filesize,
+ :hash, :ext,
+ 0, 0,
+ now(), :source
+ )",
+ [
+ "owner_id" => $user->id, "owner_ip" => $_SERVER['REMOTE_ADDR'],
+ "filename" => $cut_name, "filesize" => $this->filesize,
+ "hash" => $this->hash, "ext" => strtolower($this->ext),
+ "source" => $this->source
+ ]
+ );
+ $this->id = $database->get_last_insert_id('images_id_seq');
+ } else {
+ $database->execute(
+ "UPDATE images SET ".
+ "filename = :filename, filesize = :filesize, hash = :hash, ".
+ "ext = :ext, width = 0, height = 0, source = :source ".
+ "WHERE id = :id",
+ [
+ "filename" => $cut_name,
+ "filesize" => $this->filesize,
+ "hash" => $this->hash,
+ "ext" => strtolower($this->ext),
+ "source" => $this->source,
+ "id" => $this->id,
+ ]
+ );
+ }
+
+ $database->execute(
+ "UPDATE images SET ".
+ "lossless = :lossless, ".
+ "video = :video, audio = :audio,image = :image, ".
+ "height = :height, width = :width, ".
+ "length = :length WHERE id = :id",
+ [
+ "id" => $this->id,
+ "width" => $this->width ?? 0,
+ "height" => $this->height ?? 0,
+ "lossless" => $database->scoresql_value_prepare($this->lossless),
+ "video" => $database->scoresql_value_prepare($this->video),
+ "image" => $database->scoresql_value_prepare($this->image),
+ "audio" => $database->scoresql_value_prepare($this->audio),
+ "length" => $this->length
+ ]
+ );
+ }
+
+ /**
+ * Get this image's tags as an array.
+ *
+ * #return string[]
+ */
+ public function get_tag_array(): array
+ {
+ global $database;
+ if (!isset($this->tag_array)) {
+ $this->tag_array = $database->get_col("
+ SELECT tag
+ FROM image_tags
+ JOIN tags ON image_tags.tag_id = tags.id
+ WHERE image_id=:id
+ ORDER BY tag
+ ", ["id"=>$this->id]);
+ }
+ return $this->tag_array;
+ }
+
+ /**
+ * Get this image's tags as a string.
+ */
+ public function get_tag_list(): string
+ {
+ return Tag::implode($this->get_tag_array());
+ }
+
+ /**
+ * Get the URL for the full size image
+ */
+ public function get_image_link(): string
+ {
+ return $this->get_link(ImageConfig::ILINK, '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext');
+ }
+
+ /**
+ * Get the nicely formatted version of the file name
+ */
+ public function get_nice_image_name(): string
+ {
+ $plte = new ParseLinkTemplateEvent('$id - $tags.$ext', $this);
+ send_event($plte);
+ return $plte->text;
+ }
+
+ /**
+ * Get the URL for the thumbnail
+ */
+ public function get_thumb_link(): string
+ {
+ global $config;
+ $ext = $config->get_string(ImageConfig::THUMB_TYPE);
+ return $this->get_link(ImageConfig::TLINK, '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext);
+ }
+
+ /**
+ * Check configured template for a link, then try nice URL, then plain URL
+ */
+ private function get_link(string $template, string $nice, string $plain): string
+ {
+ global $config;
+
+ $image_link = $config->get_string($template);
+
+ if (!empty($image_link)) {
+ if (!(strpos($image_link, "://") > 0) && !startsWith($image_link, "/")) {
+ $image_link = make_link($image_link);
+ }
+ $chosen = $image_link;
+ } elseif ($config->get_bool('nice_urls', false)) {
+ $chosen = make_link($nice);
+ } else {
+ $chosen = make_link($plain);
+ }
+ return $this->parse_link_template($chosen);
+ }
+
+ /**
+ * Get the tooltip for this image, formatted according to the
+ * configured template.
+ */
+ public function get_tooltip(): string
+ {
+ global $config;
+ $plte = new ParseLinkTemplateEvent($config->get_string(ImageConfig::TIP), $this);
+ send_event($plte);
+ return $plte->text;
+ }
+
+ /**
+ * Figure out where the full size image is on disk.
+ */
+ public function get_image_filename(): string
+ {
+ return warehouse_path(self::IMAGE_DIR, $this->hash);
+ }
+
+ /**
+ * Figure out where the thumbnail is on disk.
+ */
+ public function get_thumb_filename(): string
+ {
+ return warehouse_path(self::THUMBNAIL_DIR, $this->hash);
+ }
+
+ /**
+ * Get the original filename.
+ */
+ public function get_filename(): string
+ {
+ return $this->filename;
+ }
+
+ /**
+ * Get the image's mime type.
+ */
+ public function get_mime_type(): string
+ {
+ return getMimeType($this->get_image_filename(), $this->get_ext());
+ }
+
+ /**
+ * Get the image's filename extension
+ */
+ public function get_ext(): string
+ {
+ return $this->ext;
+ }
+
+ /**
+ * Get the image's source URL
+ */
+ public function get_source(): ?string
+ {
+ return $this->source;
+ }
+
+ /**
+ * Set the image's source URL
+ */
+ public function set_source(string $new_source): void
+ {
+ global $database;
+ $old_source = $this->source;
+ if (empty($new_source)) {
+ $new_source = null;
+ }
+ if ($new_source != $old_source) {
+ $database->execute("UPDATE images SET source=:source WHERE id=:id", ["source"=>$new_source, "id"=>$this->id]);
+ log_info("core_image", "Source for Image #{$this->id} set to: $new_source (was $old_source)");
+ }
+ }
+
+ /**
+ * Check if the image is locked.
+ */
+ public function is_locked(): bool
+ {
+ return $this->locked;
+ }
+
+ public function set_locked(bool $tf): void
+ {
+ global $database;
+ $ln = $tf ? "Y" : "N";
+ $sln = $database->scoreql_to_sql('SCORE_BOOL_'.$ln);
+ $sln = str_replace("'", "", $sln);
+ $sln = str_replace('"', "", $sln);
+ if (bool_escape($sln) !== $this->locked) {
+ $database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$sln, "id"=>$this->id]);
+ log_info("core_image", "Setting Image #{$this->id} lock to: $ln");
+ }
+ }
+
+ /**
+ * Delete all tags from this image.
+ *
+ * Normally in preparation to set them to a new set.
+ */
+ public function delete_tags_from_image(): void
+ {
+ global $database;
+ if ($database->get_driver_name() == DatabaseDriver::MYSQL) {
+ //mysql < 5.6 has terrible subquery optimization, using EXISTS / JOIN fixes this
+ $database->execute(
+ "
+ UPDATE tags t
+ INNER JOIN image_tags it ON t.id = it.tag_id
+ SET count = count - 1
+ WHERE it.image_id = :id",
+ ["id"=>$this->id]
+ );
+ } else {
+ $database->execute("
+ UPDATE tags
+ SET count = count - 1
+ WHERE id IN (
+ SELECT tag_id
+ FROM image_tags
+ WHERE image_id = :id
+ )
+ ", ["id"=>$this->id]);
+ }
+ $database->execute("
+ DELETE
+ FROM image_tags
+ WHERE image_id=:id
+ ", ["id"=>$this->id]);
+ }
+
+ /**
+ * Set the tags for this image.
+ */
+ public function set_tags(array $unfiltered_tags): void
+ {
+ global $cache, $database, $page;
+
+ $unfiltered_tags = array_unique($unfiltered_tags);
+
+ $tags = [];
+ foreach ($unfiltered_tags as $tag) {
+ if (mb_strlen($tag, 'UTF-8') > 255) {
+ $page->flash("Can't set a tag longer than 255 characters");
+ continue;
+ }
+ if (startsWith($tag, "-")) {
+ $page->flash("Can't set a tag which starts with a minus");
+ continue;
+ }
+
+ $tags[] = $tag;
+ }
+
+ if (count($tags) <= 0) {
+ throw new SCoreException('Tried to set zero tags');
+ }
+
+ if (Tag::implode($tags) != $this->get_tag_list()) {
+ // delete old
+ $this->delete_tags_from_image();
+
+ $written_tags = [];
+
+ // insert each new tags
+ foreach ($tags as $tag) {
+ $id = $database->get_one(
+ "
+ SELECT id
+ FROM tags
+ WHERE LOWER(tag) = LOWER(:tag)
+ ",
+ ["tag"=>$tag]
+ );
+ if (empty($id)) {
+ // a new tag
+ $database->execute(
+ "INSERT INTO tags(tag) VALUES (:tag)",
+ ["tag"=>$tag]
+ );
+ $database->execute(
+ "INSERT INTO image_tags(image_id, tag_id)
+ VALUES(:id, (SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)))",
+ ["id"=>$this->id, "tag"=>$tag]
+ );
+ } else {
+ // check if tag has already been written
+ if (in_array($id, $written_tags)) {
+ continue;
+ }
+
+ $database->execute("
+ INSERT INTO image_tags(image_id, tag_id)
+ VALUES(:iid, :tid)
+ ", ["iid"=>$this->id, "tid"=>$id]);
+
+ array_push($written_tags, $id);
+ }
+ $database->execute(
+ "
+ UPDATE tags
+ SET count = count + 1
+ WHERE LOWER(tag) = LOWER(:tag)
+ ",
+ ["tag"=>$tag]
+ );
+ }
+
+ log_info("core_image", "Tags for Image #{$this->id} set to: ".Tag::implode($tags));
+ $cache->delete("image-{$this->id}-tags");
+ }
+ }
+
+ /**
+ * Delete this image from the database and disk
+ */
+ public function delete(): void
+ {
+ global $database;
+ $this->delete_tags_from_image();
+ $database->execute("DELETE FROM images WHERE id=:id", ["id"=>$this->id]);
+ log_info("core_image", 'Deleted Image #'.$this->id.' ('.$this->hash.')');
+
+ unlink($this->get_image_filename());
+ unlink($this->get_thumb_filename());
+ }
+
+ /**
+ * This function removes an image (and thumbnail) from the DISK ONLY.
+ * It DOES NOT remove anything from the database.
+ */
+ public function remove_image_only(): void
+ {
+ log_info("core_image", 'Removed Image File ('.$this->hash.')');
+ @unlink($this->get_image_filename());
+ @unlink($this->get_thumb_filename());
+ }
+
+ public function parse_link_template(string $tmpl, int $n=0): string
+ {
+ $plte = send_event(new ParseLinkTemplateEvent($tmpl, $this));
+ $tmpl = $plte->link;
+ return load_balance_url($tmpl, $this->hash, $n);
+ }
+
+ private static function tag_or_wildcard_to_ids(string $tag): array
+ {
+ global $database;
+ $sq = "SELECT id FROM tags WHERE LOWER(tag) LIKE LOWER(:tag)";
+ if ($database->get_driver_name() === DatabaseDriver::SQLITE) {
+ $sq .= "ESCAPE '\\'";
+ }
+ return $database->get_col($sq, ["tag" => Tag::sqlify($tag)]);
+ }
+
+ /**
+ * #param string[] $terms
+ */
+ private static function build_search_querylet(
+ array $tags,
+ ?string $order=null,
+ ?int $limit=null,
+ ?int $offset=null
+ ): Querylet {
+ list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags);
+
+ $positive_tag_count = 0;
+ $negative_tag_count = 0;
+ foreach ($tag_conditions as $tq) {
+ if ($tq->positive) {
+ $positive_tag_count++;
+ } else {
+ $negative_tag_count++;
+ }
+ }
+
+ /*
+ * Turn a bunch of Querylet objects into a base query
+ *
+ * Must follow the format
+ *
+ * SELECT images.*
+ * FROM (...) AS images
+ * WHERE (...)
+ *
+ * ie, return a set of images.* columns, and end with a WHERE
+ */
+
+ // no tags, do a simple search
+ if ($positive_tag_count === 0 && $negative_tag_count === 0) {
+ $query = new Querylet("SELECT images.* FROM images WHERE 1=1");
+ }
+
+ // one tag sorted by ID - we can fetch this from the image_tags table,
+ // and do the offset / limit there, which is 10x faster than fetching
+ // all the image_tags and doing the offset / limit on the result.
+ elseif (
+ (
+ ($positive_tag_count === 1 && $negative_tag_count === 0)
+ || ($positive_tag_count === 0 && $negative_tag_count === 1)
+ )
+ && empty($img_conditions)
+ && ($order == "id DESC" || $order == "images.id DESC")
+ && !is_null($offset)
+ && !is_null($limit)
+ ) {
+ $in = $positive_tag_count === 1 ? "IN" : "NOT IN";
+ // IN (SELECT id FROM tags) is 100x slower than doing a separate
+ // query and then a second query for IN(first_query_results)??
+ $tag_array = self::tag_or_wildcard_to_ids($tag_conditions[0]->tag);
+ if (count($tag_array) == 0) {
+ if ($positive_tag_count == 1) {
+ $query = new Querylet("SELECT images.* FROM images WHERE 1=0");
+ } else {
+ $query = new Querylet("SELECT images.* FROM images WHERE 1=1");
+ }
+ } else {
+ $set = implode(', ', $tag_array);
+ $query = new Querylet("
+ SELECT images.*
+ FROM images INNER JOIN (
+ SELECT it.image_id
+ FROM image_tags it
+ WHERE it.tag_id $in ($set)
+ ORDER BY it.image_id DESC
+ LIMIT :limit OFFSET :offset
+ ) a on a.image_id = images.id
+ ORDER BY images.id DESC
+ ", ["limit"=>$limit, "offset"=>$offset]);
+ // don't offset at the image level because
+ // we already offset at the image_tags level
+ $order = null;
+ $limit = null;
+ $offset = null;
+ }
+ }
+
+ // more than one positive tag, or more than zero negative tags
+ else {
+ $positive_tag_id_array = [];
+ $positive_wildcard_id_array = [];
+ $negative_tag_id_array = [];
+
+ foreach ($tag_conditions as $tq) {
+ $tag_ids = self::tag_or_wildcard_to_ids($tq->tag);
+ $tag_count = count($tag_ids);
+
+ if ($tq->positive) {
+ if ($tag_count== 0) {
+ # one of the positive tags had zero results, therefor there
+ # can be no results; "where 1=0" should shortcut things
+ return new Querylet("SELECT images.* FROM images WHERE 1=0");
+ } elseif ($tag_count==1) {
+ // All wildcard terms that qualify for a single tag can be treated the same as non-wildcards
+ $positive_tag_id_array[] = $tag_ids[0];
+ } else {
+ // Terms that resolve to multiple tags act as an OR within themselves
+ // and as an AND in relation to all other terms,
+ $positive_wildcard_id_array[] = $tag_ids;
+ }
+ } else {
+ // Unlike positive criteria, negative criteria are all handled in an OR fashion,
+ // so we can just compile them all into a single sub-query.
+ $negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids);
+ }
+ }
+
+ assert($positive_tag_id_array || $positive_wildcard_id_array || $negative_tag_id_array, @$_GET['q']);
+ if (!empty($positive_tag_id_array) || !empty($positive_wildcard_id_array)) {
+ $inner_joins = [];
+ if (!empty($positive_tag_id_array)) {
+ foreach ($positive_tag_id_array as $tag) {
+ $inner_joins[] = "= $tag";
+ }
+ }
+ if (!empty($positive_wildcard_id_array)) {
+ foreach ($positive_wildcard_id_array as $tags) {
+ $positive_tag_id_list = join(', ', $tags);
+ $inner_joins[] = "IN ($positive_tag_id_list)";
+ }
+ }
+
+ $first = array_shift($inner_joins);
+ $sub_query = "SELECT it.image_id FROM image_tags it ";
+ $i = 0;
+ foreach ($inner_joins as $inner_join) {
+ $i++;
+ $sub_query .= " INNER JOIN image_tags it$i ON it$i.image_id = it.image_id AND it$i.tag_id $inner_join ";
+ }
+ if (!empty($negative_tag_id_array)) {
+ $negative_tag_id_list = join(', ', $negative_tag_id_array);
+ $sub_query .= " LEFT JOIN image_tags negative ON negative.image_id = it.image_id AND negative.tag_id IN ($negative_tag_id_list) ";
+ }
+ $sub_query .= "WHERE it.tag_id $first ";
+ if (!empty($negative_tag_id_array)) {
+ $sub_query .= " AND negative.image_id IS NULL";
+ }
+ $sub_query .= " GROUP BY it.image_id ";
+
+ $query = new Querylet("
+ SELECT images.*
+ FROM images
+ INNER JOIN ($sub_query) a on a.image_id = images.id
+ ");
+ } elseif (!empty($negative_tag_id_array)) {
+ $negative_tag_id_list = join(', ', $negative_tag_id_array);
+ $query = new Querylet("
+ SELECT images.*
+ FROM images
+ LEFT JOIN image_tags negative ON negative.image_id = images.id AND negative.tag_id in ($negative_tag_id_list)
+ WHERE negative.image_id IS NULL
+ ");
+ } else {
+ throw new SCoreException("No criteria specified");
+ }
+ }
+
+ /*
+ * Merge all the image metadata searches into one generic querylet
+ * and append to the base querylet with "AND blah"
+ */
+ if (!empty($img_conditions)) {
+ $n = 0;
+ $img_sql = "";
+ $img_vars = [];
+ foreach ($img_conditions as $iq) {
+ if ($n++ > 0) {
+ $img_sql .= " AND";
+ }
+ if (!$iq->positive) {
+ $img_sql .= " NOT";
+ }
+ $img_sql .= " (" . $iq->qlet->sql . ")";
+ $img_vars = array_merge($img_vars, $iq->qlet->variables);
+ }
+ $query->append_sql(" AND ");
+ $query->append(new Querylet($img_sql, $img_vars));
+ }
+
+ if (!is_null($order)) {
+ $query->append(new Querylet(" ORDER BY ".$order));
+ }
+ if (!is_null($limit)) {
+ $query->append(new Querylet(" LIMIT :limit ", ["limit" => $limit]));
+ $query->append(new Querylet(" OFFSET :offset ", ["offset" => $offset]));
+ }
+
+ return $query;
+ }
+}
diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php
new file mode 100644
index 00000000..393e0f94
--- /dev/null
+++ b/core/imageboard/misc.php
@@ -0,0 +1,162 @@
+getMessage();
+ }
+ $results[] = $result;
+ }
+
+ return $results;
+}
+
+/**
+ * Sends a DataUploadEvent for a file.
+ *
+ * @param string $tmpname
+ * @param string $filename
+ * @param string $tags
+ * @throws UploadException
+ */
+function add_image(string $tmpname, string $filename, string $tags): void
+{
+ assert(file_exists($tmpname));
+
+ $pathinfo = pathinfo($filename);
+ $metadata = [];
+ $metadata['filename'] = $pathinfo['basename'];
+ if (array_key_exists('extension', $pathinfo)) {
+ $metadata['extension'] = $pathinfo['extension'];
+ }
+
+ $metadata['tags'] = Tag::explode($tags);
+ $metadata['source'] = null;
+ send_event(new DataUploadEvent($tmpname, $metadata));
+}
+
+/**
+ * Given a full size pair of dimensions, return a pair scaled down to fit
+ * into the configured thumbnail square, with ratio intact.
+ * Optionally uses the High-DPI scaling setting to adjust the final resolution.
+ *
+ * @param int $orig_width
+ * @param int $orig_height
+ * @param bool $use_dpi_scaling Enables the High-DPI scaling.
+ * @return array
+ */
+function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_scaling = false): array
+{
+ global $config;
+
+ if ($orig_width === 0) {
+ $orig_width = 192;
+ }
+ if ($orig_height === 0) {
+ $orig_height = 192;
+ }
+
+ if ($orig_width > $orig_height * 5) {
+ $orig_width = $orig_height * 5;
+ }
+ if ($orig_height > $orig_width * 5) {
+ $orig_height = $orig_width * 5;
+ }
+
+
+ if ($use_dpi_scaling) {
+ list($max_width, $max_height) = get_thumbnail_max_size_scaled();
+ } else {
+ $max_width = $config->get_int(ImageConfig::THUMB_WIDTH);
+ $max_height = $config->get_int(ImageConfig::THUMB_HEIGHT);
+ }
+
+ $output = get_scaled_by_aspect_ratio($orig_width, $orig_height, $max_width, $max_height);
+
+ if ($output[2] > 1 && $config->get_bool('thumb_upscale')) {
+ return [(int)$orig_width, (int)$orig_height];
+ } else {
+ return $output;
+ }
+}
+
+function get_scaled_by_aspect_ratio(int $original_width, int $original_height, int $max_width, int $max_height) : array
+{
+ $xscale = ($max_width/ $original_width);
+ $yscale = ($max_height/ $original_height);
+
+ $scale = ($yscale < $xscale) ? $yscale : $xscale ;
+
+ return [(int)($original_width*$scale), (int)($original_height*$scale), $scale];
+}
+
+/**
+ * Fetches the thumbnails height and width settings and applies the High-DPI scaling setting before returning the dimensions.
+ *
+ * @return array [width, height]
+ */
+function get_thumbnail_max_size_scaled(): array
+{
+ global $config;
+
+ $scaling = $config->get_int(ImageConfig::THUMB_SCALING);
+ $max_width = $config->get_int(ImageConfig::THUMB_WIDTH) * ($scaling/100);
+ $max_height = $config->get_int(ImageConfig::THUMB_HEIGHT) * ($scaling/100);
+ return [$max_width, $max_height];
+}
+
+
+function create_image_thumb(string $hash, string $type, string $engine = null)
+{
+ $inname = warehouse_path(Image::IMAGE_DIR, $hash);
+ $outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
+ $tsize = get_thumbnail_max_size_scaled();
+ create_scaled_image($inname, $outname, $tsize, $type, $engine);
+}
+
+function create_scaled_image(string $inname, string $outname, array $tsize, string $type, ?string $engine)
+{
+ global $config;
+ if (empty($engine)) {
+ $engine = $config->get_string(ImageConfig::THUMB_ENGINE);
+ }
+
+ $output_format = $config->get_string(ImageConfig::THUMB_TYPE);
+ if ($output_format=="webp") {
+ $output_format = Media::WEBP_LOSSY;
+ }
+
+ send_event(new MediaResizeEvent(
+ $engine,
+ $inname,
+ $type,
+ $outname,
+ $tsize[0],
+ $tsize[1],
+ false,
+ $output_format,
+ $config->get_int(ImageConfig::THUMB_QUALITY),
+ true,
+ $config->get_bool('thumb_upscale', false)
+ ));
+}
diff --git a/core/imageboard/search.php b/core/imageboard/search.php
new file mode 100644
index 00000000..30456459
--- /dev/null
+++ b/core/imageboard/search.php
@@ -0,0 +1,58 @@
+sql = $sql;
+ $this->variables = $variables;
+ }
+
+ public function append(Querylet $querylet): void
+ {
+ $this->sql .= $querylet->sql;
+ $this->variables = array_merge($this->variables, $querylet->variables);
+ }
+
+ public function append_sql(string $sql): void
+ {
+ $this->sql .= $sql;
+ }
+
+ public function add_variable($var): void
+ {
+ $this->variables[] = $var;
+ }
+}
+
+class TagCondition
+{
+ /** @var string */
+ public $tag;
+ /** @var bool */
+ public $positive;
+
+ public function __construct(string $tag, bool $positive)
+ {
+ $this->tag = $tag;
+ $this->positive = $positive;
+ }
+}
+
+class ImgCondition
+{
+ /** @var Querylet */
+ public $qlet;
+ /** @var bool */
+ public $positive;
+
+ public function __construct(Querylet $qlet, bool $positive)
+ {
+ $this->qlet = $qlet;
+ $this->positive = $positive;
+ }
+}
diff --git a/core/imageboard/tag.php b/core/imageboard/tag.php
new file mode 100644
index 00000000..5db83ffd
--- /dev/null
+++ b/core/imageboard/tag.php
@@ -0,0 +1,218 @@
+get_one(
+ "
+ SELECT newtag
+ FROM aliases
+ WHERE LOWER(oldtag)=LOWER(:tag)
+ ",
+ ["tag"=>$tag]
+ );
+ if (empty($newtags)) {
+ //tag has no alias, use old tag
+ $aliases = [$tag];
+ } else {
+ $aliases = explode(" ", $newtags); // Tag::explode($newtags); - recursion can be infinite
+ }
+
+ foreach ($aliases as $alias) {
+ if (!in_array($alias, $new)) {
+ if ($tag == $alias) {
+ $new[] = $negative.$alias;
+ } elseif (!in_array($alias, $tag_array)) {
+ $tag_array[] = $negative.$alias;
+ $tag_count++;
+ }
+ }
+ }
+ $i++;
+ }
+
+ /* remove any duplicate tags */
+ $tag_array = array_iunique($new);
+
+ /* tidy up */
+ sort($tag_array);
+
+ return $tag_array;
+ }
+
+ public static function sanitize(string $tag): string
+ {
+ $tag = preg_replace("/\s/", "", $tag); # whitespace
+ $tag = preg_replace('/\x20[\x0e\x0f]/', '', $tag); # unicode RTL
+ $tag = preg_replace("/\.+/", ".", $tag); # strings of dots?
+ $tag = preg_replace("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes?
+ $tag = trim($tag, ", \t\n\r\0\x0B");
+
+ if($tag == ".") $tag = ""; // hard-code one bad case...
+
+ if (mb_strlen($tag, 'UTF-8') > 255) {
+ throw new ScoreException("The tag below is longer than 255 characters, please use a shorter tag.\n$tag\n");
+ }
+ return $tag;
+ }
+
+ public static function compare(array $tags1, array $tags2): bool
+ {
+ if (count($tags1)!==count($tags2)) {
+ return false;
+ }
+
+ $tags1 = array_map("strtolower", $tags1);
+ $tags2 = array_map("strtolower", $tags2);
+ natcasesort($tags1);
+ natcasesort($tags2);
+
+ for ($i = 0; $i < count($tags1); $i++) {
+ if ($tags1[$i]!==$tags2[$i]) {
+ var_dump($tags1);
+ var_dump($tags2);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static function get_diff_tags(array $source, array $remove): array
+ {
+ $before = array_map('strtolower', $source);
+ $remove = array_map('strtolower', $remove);
+ $after = [];
+ foreach ($before as $tag) {
+ if (!in_array($tag, $remove)) {
+ $after[] = $tag;
+ }
+ }
+ return $after;
+ }
+
+ public static function sanitize_array(array $tags): array
+ {
+ global $page;
+ $tag_array = [];
+ foreach ($tags as $tag) {
+ try {
+ $tag = Tag::sanitize($tag);
+ } catch (Exception $e) {
+ $page->flash($e->getMessage());
+ continue;
+ }
+
+ if (!empty($tag)) {
+ $tag_array[] = $tag;
+ }
+ }
+ return $tag_array;
+ }
+
+ public static function sqlify(string $term): string
+ {
+ global $database;
+ if ($database->get_driver_name() === DatabaseDriver::SQLITE) {
+ $term = str_replace('\\', '\\\\', $term);
+ }
+ $term = str_replace('_', '\_', $term);
+ $term = str_replace('%', '\%', $term);
+ $term = str_replace('*', '%', $term);
+ // $term = str_replace("?", "_", $term);
+ return $term;
+ }
+
+ /**
+ * Kind of like urlencode, but using a custom scheme so that
+ * tags always fit neatly between slashes in a URL. Use this
+ * when you want to put an arbitrary tag into a URL.
+ */
+ public static function caret(string $input): string
+ {
+ $to_caret = [
+ "^" => "^",
+ "/" => "s",
+ "\\" => "b",
+ "?" => "q",
+ "&" => "a",
+ "." => "d",
+ ];
+
+ foreach ($to_caret as $from => $to) {
+ $input = str_replace($from, '^' . $to, $input);
+ }
+ return $input;
+ }
+
+ /**
+ * Use this when you want to get a tag out of a URL
+ */
+ public static function decaret(string $str): string
+ {
+ $from_caret = [
+ "^" => "^",
+ "s" => "/",
+ "b" => "\\",
+ "q" => "?",
+ "a" => "&",
+ "d" => ".",
+ ];
+
+ $out = "";
+ $length = strlen($str);
+ for ($i=0; $i<$length; $i++) {
+ if ($str[$i] == "^") {
+ $i++;
+ $out .= $from_caret[$str[$i]] ?? '';
+ } else {
+ $out .= $str[$i];
+ }
+ }
+ return $out;
+ }
+}
diff --git a/core/install.php b/core/install.php
new file mode 100644
index 00000000..e85299e4
--- /dev/null
+++ b/core/install.php
@@ -0,0 +1,364 @@
+Shimmie is unable to find the composer vendor directory.
+
Have you followed the composer setup instructions found in the
+ README?
+
If you are not intending to do any development with Shimmie,
+ it is highly recommend you use one of the pre-packaged releases
+ found on Github instead.
+ ");
+ }
+
+ // Pull in necessary files
+ require_once "vendor/autoload.php";
+ global $_tracer;
+ $_tracer = new EventTracer();
+
+ require_once "core/exceptions.php";
+ require_once "core/cacheengine.php";
+ require_once "core/dbengine.php";
+ require_once "core/database.php";
+ require_once "core/util.php";
+
+ $dsn = get_dsn();
+ if ($dsn) {
+ do_install($dsn);
+ } else {
+ ask_questions();
+ }
+}
+
+function get_dsn()
+{
+ if (file_exists("data/config/auto_install.conf.php")) {
+ $dsn = null;
+ /** @noinspection PhpIncludeInspection */
+ require_once "data/config/auto_install.conf.php";
+ } elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) {
+ /** @noinspection PhpUnhandledExceptionInspection */
+ $id = bin2hex(random_bytes(5));
+ $dsn = "sqlite:data/shimmie.{$id}.sqlite";
+ } elseif (isset($_POST['database_type']) && isset($_POST['database_host']) && isset($_POST['database_user']) && isset($_POST['database_name'])) {
+ $dsn = "{$_POST['database_type']}:user={$_POST['database_user']};password={$_POST['database_password']};host={$_POST['database_host']};dbname={$_POST['database_name']}";
+ } else {
+ $dsn = null;
+ }
+ return $dsn;
+}
+
+function do_install($dsn)
+{
+ try {
+ create_dirs();
+ create_tables(new Database($dsn));
+ write_config($dsn);
+ } catch (InstallerException $e) {
+ exit_with_page($e->title, $e->body, $e->code);
+ }
+}
+
+function ask_questions()
+{
+ $warnings = [];
+ $errors = [];
+
+ if (check_gd_version() == 0 && check_im_version() == 0) {
+ $errors[] = "
+ No thumbnailers could be found - install the imagemagick
+ tools (or the PHP-GD library, if imagemagick is unavailable).
+ ";
+ } elseif (check_im_version() == 0) {
+ $warnings[] = "
+ The 'convert' command (from the imagemagick package)
+ could not be found - PHP-GD can be used instead, but
+ the size of thumbnails will be limited.
+ ";
+ }
+
+ if (!function_exists('mb_strlen')) {
+ $errors[] = "
+ The mbstring PHP extension is missing - multibyte languages
+ (eg non-english languages) may not work right.
+ ";
+ }
+
+ $drivers = PDO::getAvailableDrivers();
+ if (
+ !in_array(DatabaseDriver::MYSQL, $drivers) &&
+ !in_array(DatabaseDriver::PGSQL, $drivers) &&
+ !in_array(DatabaseDriver::SQLITE, $drivers)
+ ) {
+ $errors[] = "
+ No database connection library could be found; shimmie needs
+ PDO with either Postgres, MySQL, or SQLite drivers
+ ";
+ }
+
+ $db_m = in_array(DatabaseDriver::MYSQL, $drivers) ? '' : "";
+ $db_p = in_array(DatabaseDriver::PGSQL, $drivers) ? '' : "";
+ $db_s = in_array(DatabaseDriver::SQLITE, $drivers) ? '' : "";
+
+ $warn_msg = $warnings ? "
If you aren't redirected, click here to Continue."
+ );
+ } else {
+ $h_file_content = htmlentities($file_content);
+ throw new InstallerException(
+ "File Permissions Error:",
+ "The web server isn't allowed to write to the config file; please copy
+ the text below, save it as 'data/config/shimmie.conf.php', and upload it into the shimmie
+ folder manually. Make sure that when you save it, there is no whitespace
+ before the \"<?php\".
+
+
+
+
Once done, click here to Continue.",
+ 0
+ );
+ }
+}
+
+function exit_with_page($title, $body, $code=0)
+{
+ print("
+
+
+ Shimmie Installer
+
+
+
+
+
+
Shimmie Installer
+
$title
+
+ $body
+
+
+
+");
+ exit($code);
+}
diff --git a/core/logging.php b/core/logging.php
new file mode 100644
index 00000000..ae76d01c
--- /dev/null
+++ b/core/logging.php
@@ -0,0 +1,73 @@
+= $threshold)) {
+ print date("c")." $section: $message\n";
+ ob_flush();
+ }
+ if (!is_null($flash)) {
+ $page->flash($flash);
+ }
+}
+
+// More shorthand ways of logging
+function log_debug(string $section, string $message, ?string $flash=null)
+{
+ log_msg($section, SCORE_LOG_DEBUG, $message, $flash);
+}
+function log_info(string $section, string $message, ?string $flash=null)
+{
+ log_msg($section, SCORE_LOG_INFO, $message, $flash);
+}
+function log_warning(string $section, string $message, ?string $flash=null)
+{
+ log_msg($section, SCORE_LOG_WARNING, $message, $flash);
+}
+function log_error(string $section, string $message, ?string $flash=null)
+{
+ log_msg($section, SCORE_LOG_ERROR, $message, $flash);
+}
+function log_critical(string $section, string $message, ?string $flash=null)
+{
+ log_msg($section, SCORE_LOG_CRITICAL, $message, $flash);
+}
+
+
+/**
+ * Get a unique ID for this request, useful for grouping log messages.
+ */
+function get_request_id(): string
+{
+ static $request_id = null;
+ if (!$request_id) {
+ // not completely trustworthy, as a user can spoof this
+ if (@$_SERVER['HTTP_X_VARNISH']) {
+ $request_id = $_SERVER['HTTP_X_VARNISH'];
+ } else {
+ $request_id = "P" . uniqid();
+ }
+ }
+ return $request_id;
+}
diff --git a/core/page.class.php b/core/page.class.php
deleted file mode 100644
index 3d02bbe6..00000000
--- a/core/page.class.php
+++ /dev/null
@@ -1,424 +0,0 @@
-mode = $mode;
- }
-
- /**
- * Set the page's MIME type.
- * @param string $type
- */
- public function set_type($type) {
- $this->type = $type;
- }
-
-
- //@}
- // ==============================================
- /** @name "data" mode */
- //@{
-
- /** @var string; public only for unit test */
- public $data = "";
-
- /** @var string; public only for unit test */
- public $filename = null;
-
- /**
- * Set the raw data to be sent.
- * @param string $data
- */
- public function set_data($data) {
- $this->data = $data;
- }
-
- /**
- * Set the recommended download filename.
- * @param string $filename
- */
- public function set_filename($filename) {
- $this->filename = $filename;
- }
-
-
- //@}
- // ==============================================
- /** @name "redirect" mode */
- //@{
-
- /** @var string */
- private $redirect = "";
-
- /**
- * Set the URL to redirect to (remember to use make_link() if linking
- * to a page in the same site).
- * @param string $redirect
- */
- public function set_redirect($redirect) {
- $this->redirect = $redirect;
- }
-
-
- //@}
- // ==============================================
- /** @name "page" mode */
- //@{
-
- /** @var int */
- public $code = 200;
-
- /** @var string */
- public $title = "";
-
- /** @var string */
- public $heading = "";
-
- /** @var string */
- public $subheading = "";
-
- /** @var string */
- public $quicknav = "";
-
- /** @var string[] */
- public $html_headers = array();
-
- /** @var string[] */
- public $http_headers = array();
-
- /** @var string[][] */
- public $cookies = array();
-
- /** @var Block[] */
- public $blocks = array();
-
- /**
- * Set the HTTP status code
- * @param int $code
- */
- public function set_code($code) {
- $this->code = $code;
- }
-
- /**
- * Set the window title.
- * @param string $title
- */
- public function set_title($title) {
- $this->title = $title;
- }
-
- /**
- * Set the main heading.
- * @param string $heading
- */
- public function set_heading($heading) {
- $this->heading = $heading;
- }
-
- /**
- * Set the sub heading.
- * @param string $subheading
- */
- public function set_subheading($subheading) {
- $this->subheading = $subheading;
- }
-
- /**
- * Add a line to the HTML head section.
- * @param string $line
- * @param int $position
- */
- public function add_html_header($line, $position=50) {
- while(isset($this->html_headers[$position])) $position++;
- $this->html_headers[$position] = $line;
- }
-
- /**
- * Add a http header to be sent to the client.
- * @param string $line
- * @param int $position
- */
- public function add_http_header($line, $position=50) {
- while(isset($this->http_headers[$position])) $position++;
- $this->http_headers[$position] = $line;
- }
-
- /**
- * The counterpart for get_cookie, this works like php's
- * setcookie method, but prepends the site-wide cookie prefix to
- * the $name argument before doing anything.
- *
- * @param string $name
- * @param string $value
- * @param int $time
- * @param string $path
- */
- public function add_cookie($name, $value, $time, $path) {
- $full_name = COOKIE_PREFIX."_".$name;
- $this->cookies[] = array($full_name, $value, $time, $path);
- }
-
- /**
- * @param string $name
- * @return string|null
- */
- public function get_cookie(/*string*/ $name) {
- $full_name = COOKIE_PREFIX."_".$name;
- if(isset($_COOKIE[$full_name])) {
- return $_COOKIE[$full_name];
- }
- else {
- return null;
- }
- }
-
- /**
- * Get all the HTML headers that are currently set and return as a string.
- * @return string
- */
- public function get_all_html_headers() {
- $data = '';
- ksort($this->html_headers);
- foreach ($this->html_headers as $line) {
- $data .= "\t\t" . $line . "\n";
- }
- return $data;
- }
-
- /**
- * Removes all currently set HTML headers (Be careful..).
- */
- public function delete_all_html_headers() {
- $this->html_headers = array();
- }
-
- /**
- * Add a Block of data to the page.
- * @param Block $block
- */
- public function add_block(Block $block) {
- $this->blocks[] = $block;
- }
-
-
- //@}
- // ==============================================
-
- /**
- * Display the page according to the mode and data given.
- */
- public function display() {
- global $page, $user;
-
- header("HTTP/1.0 {$this->code} Shimmie");
- header("Content-type: ".$this->type);
- header("X-Powered-By: SCore-".SCORE_VERSION);
-
- if (!headers_sent()) {
- foreach($this->http_headers as $head) {
- header($head);
- }
- foreach($this->cookies as $c) {
- setcookie($c[0], $c[1], $c[2], $c[3]);
- }
- } else {
- print "Error: Headers have already been sent to the client.";
- }
-
- switch($this->mode) {
- case "page":
- if(CACHE_HTTP) {
- header("Vary: Cookie, Accept-Encoding");
- if($user->is_anonymous() && $_SERVER["REQUEST_METHOD"] == "GET") {
- header("Cache-control: public, max-age=600");
- header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 600) . ' GMT');
- }
- else {
- #header("Cache-control: private, max-age=0");
- header("Cache-control: no-cache");
- header('Expires: ' . gmdate('D, d M Y H:i:s', time() - 600) . ' GMT');
- }
- }
- #else {
- # header("Cache-control: no-cache");
- # header('Expires: ' . gmdate('D, d M Y H:i:s', time() - 600) . ' GMT');
- #}
- if($this->get_cookie("flash_message") !== null) {
- $this->add_cookie("flash_message", "", -1, "/");
- }
- usort($this->blocks, "blockcmp");
- $this->add_auto_html_headers();
- $layout = new Layout();
- $layout->display_page($page);
- break;
- case "data":
- header("Content-Length: ".strlen($this->data));
- if(!is_null($this->filename)) {
- header('Content-Disposition: attachment; filename='.$this->filename);
- }
- print $this->data;
- break;
- case "redirect":
- header('Location: '.$this->redirect);
- print 'You should be redirected to '.$this->redirect.'';
- break;
- default:
- print "Invalid page mode";
- break;
- }
- }
-
- /**
- * This function grabs all the CSS and JavaScript files sprinkled throughout Shimmie's folders,
- * concatenates them together into two large files (one for CSS and one for JS) and then stores
- * them in the /cache/ directory for serving to the user.
- *
- * Why do this? Two reasons:
- * 1. Reduces the number of files the user's browser needs to download.
- * 2. Allows these cached files to be compressed/minified by the admin.
- *
- * TODO: This should really be configurable somehow...
- */
- public function add_auto_html_headers() {
- global $config;
-
- $data_href = get_base_href();
- $theme_name = $config->get_string('theme', 'default');
-
- $this->add_html_header("", 40);
-
- # 404/static handler will map these to themes/foo/bar.ico or lib/static/bar.ico
- $this->add_html_header("", 41);
- $this->add_html_header("", 42);
-
- //We use $config_latest to make sure cache is reset if config is ever updated.
- $config_latest = 0;
- foreach(zglob("data/config/*") as $conf) {
- $config_latest = max($config_latest, filemtime($conf));
- }
-
- /*** Generate CSS cache files ***/
- $css_lib_latest = $config_latest;
- $css_lib_files = zglob("lib/vendor/css/*.css");
- foreach($css_lib_files as $css) {
- $css_lib_latest = max($css_lib_latest, filemtime($css));
- }
- $css_lib_md5 = md5(serialize($css_lib_files));
- $css_lib_cache_file = data_path("cache/style.lib.{$theme_name}.{$css_lib_latest}.{$css_lib_md5}.css");
- if(!file_exists($css_lib_cache_file)) {
- $css_lib_data = "";
- foreach($css_lib_files as $file) {
- $file_data = file_get_contents($file);
- $pattern = '/url[\s]*\([\s]*["\']?([^"\'\)]+)["\']?[\s]*\)/';
- $replace = 'url("../../'.dirname($file).'/$1")';
- $file_data = preg_replace($pattern, $replace, $file_data);
- $css_lib_data .= $file_data . "\n";
- }
- file_put_contents($css_lib_cache_file, $css_lib_data);
- }
- $this->add_html_header("", 43);
-
- $css_latest = $config_latest;
- $css_files = array_merge(zglob("lib/shimmie.css"), zglob("ext/{".ENABLED_EXTS."}/style.css"), zglob("themes/$theme_name/style.css"));
- foreach($css_files as $css) {
- $css_latest = max($css_latest, filemtime($css));
- }
- $css_md5 = md5(serialize($css_files));
- $css_cache_file = data_path("cache/style.main.{$theme_name}.{$css_latest}.{$css_md5}.css");
- if(!file_exists($css_cache_file)) {
- $css_data = "";
- foreach($css_files as $file) {
- $file_data = file_get_contents($file);
- $pattern = '/url[\s]*\([\s]*["\']?([^"\'\)]+)["\']?[\s]*\)/';
- $replace = 'url("../../'.dirname($file).'/$1")';
- $file_data = preg_replace($pattern, $replace, $file_data);
- $css_data .= $file_data . "\n";
- }
- file_put_contents($css_cache_file, $css_data);
- }
- $this->add_html_header("", 100);
-
- /*** Generate JS cache files ***/
- $js_lib_latest = $config_latest;
- $js_lib_files = zglob("lib/vendor/js/*.js");
- foreach($js_lib_files as $js) {
- $js_lib_latest = max($js_lib_latest, filemtime($js));
- }
- $js_lib_md5 = md5(serialize($js_lib_files));
- $js_lib_cache_file = data_path("cache/script.lib.{$theme_name}.{$js_lib_latest}.{$js_lib_md5}.js");
- if(!file_exists($js_lib_cache_file)) {
- $js_data = "";
- foreach($js_lib_files as $file) {
- $js_data .= file_get_contents($file) . "\n";
- }
- file_put_contents($js_lib_cache_file, $js_data);
- }
- $this->add_html_header("", 45);
-
- $js_latest = $config_latest;
- $js_files = array_merge(zglob("lib/shimmie.js"), zglob("ext/{".ENABLED_EXTS."}/script.js"), zglob("themes/$theme_name/script.js"));
- foreach($js_files as $js) {
- $js_latest = max($js_latest, filemtime($js));
- }
- $js_md5 = md5(serialize($js_files));
- $js_cache_file = data_path("cache/script.main.{$theme_name}.{$js_latest}.{$js_md5}.js");
- if(!file_exists($js_cache_file)) {
- $js_data = "";
- foreach($js_files as $file) {
- $js_data .= file_get_contents($file) . "\n";
- }
- file_put_contents($js_cache_file, $js_data);
- }
- $this->add_html_header("", 100);
- }
-}
-
-class MockPage extends Page {
-}
diff --git a/core/permissions.php b/core/permissions.php
new file mode 100644
index 00000000..1b514a46
--- /dev/null
+++ b/core/permissions.php
@@ -0,0 +1,94 @@
+read())) {
+ if ($entry == '.' || $entry == '..') {
+ continue;
+ }
+
+ $Entry = $source . '/' . $entry;
+ if (is_dir($Entry)) {
+ full_copy($Entry, $target . '/' . $entry);
+ continue;
+ }
+ copy($Entry, $target . '/' . $entry);
+ }
+ $d->close();
+ } else {
+ copy($source, $target);
+ }
+}
+
+/**
+ * Return a list of all the regular files in a directory and subdirectories
+ */
+function list_files(string $base, string $_sub_dir=""): array
+{
+ assert(is_dir($base));
+
+ $file_list = [];
+
+ $files = [];
+ $dir = opendir("$base/$_sub_dir");
+ while ($f = readdir($dir)) {
+ $files[] = $f;
+ }
+ closedir($dir);
+ sort($files);
+
+ foreach ($files as $filename) {
+ $full_path = "$base/$_sub_dir/$filename";
+
+ if (!is_link($full_path) && is_dir($full_path)) {
+ if (!($filename == "." || $filename == "..")) {
+ //subdirectory found
+ $file_list = array_merge(
+ $file_list,
+ list_files($base, "$_sub_dir/$filename")
+ );
+ }
+ } else {
+ $full_path = str_replace("//", "/", $full_path);
+ $file_list[] = $full_path;
+ }
+ }
+
+ return $file_list;
+}
+
+function stream_file(string $file, int $start, int $end): void
+{
+ $fp = fopen($file, 'r');
+ try {
+ set_time_limit(0);
+ fseek($fp, $start);
+ $buffer = 1024 * 1024;
+ while (!feof($fp) && ($p = ftell($fp)) <= $end) {
+ if ($p + $buffer > $end) {
+ $buffer = $end - $p + 1;
+ }
+ echo fread($fp, $buffer);
+ if (!defined("UNITTEST")) {
+ @ob_flush();
+ }
+ flush();
+
+ // After flush, we can tell if the client browser has disconnected.
+ // This means we can start sending a large file, and if we detect they disappeared
+ // then we can just stop and not waste any more resources or bandwidth.
+ if (connection_status() != 0) {
+ break;
+ }
+ }
+ } finally {
+ fclose($fp);
+ }
+}
+
+if (!function_exists('http_parse_headers')) { #http://www.php.net/manual/en/function.http-parse-headers.php#112917
+
+ /**
+ * #return string[]
+ */
+ function http_parse_headers(string $raw_headers): array
+ {
+ $headers = []; // $headers = [];
+
+ foreach (explode("\n", $raw_headers) as $i => $h) {
+ $h = explode(':', $h, 2);
+
+ if (isset($h[1])) {
+ if (!isset($headers[$h[0]])) {
+ $headers[$h[0]] = trim($h[1]);
+ } elseif (is_array($headers[$h[0]])) {
+ $tmp = array_merge($headers[$h[0]], [trim($h[1])]);
+ $headers[$h[0]] = $tmp;
+ } else {
+ $tmp = array_merge([$headers[$h[0]]], [trim($h[1])]);
+ $headers[$h[0]] = $tmp;
+ }
+ }
+ }
+ return $headers;
+ }
+}
+
+/**
+ * HTTP Headers can sometimes be lowercase which will cause issues.
+ * In cases like these, we need to make sure to check for them if the camelcase version does not exist.
+ */
+function findHeader(array $headers, string $name): ?string
+{
+ if (!is_array($headers)) {
+ return null;
+ }
+
+ $header = null;
+
+ if (array_key_exists($name, $headers)) {
+ $header = $headers[$name];
+ } else {
+ $headers = array_change_key_case($headers); // convert all to lower case.
+ $lc_name = strtolower($name);
+
+ if (array_key_exists($lc_name, $headers)) {
+ $header = $headers[$lc_name];
+ }
+ }
+
+ return $header;
+}
+
+if (!function_exists('mb_strlen')) {
+ // TODO: we should warn the admin that they are missing multibyte support
+ function mb_strlen($str, $encoding)
+ {
+ return strlen($str);
+ }
+ function mb_internal_encoding($encoding)
+ {
+ }
+ function mb_strtolower($str)
+ {
+ return strtolower($str);
+ }
+}
+
+const MIME_TYPE_MAP = [
+ 'jpg' => 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'png' => 'image/png',
+ 'tif' => 'image/tiff',
+ 'tiff' => 'image/tiff',
+ 'ico' => 'image/x-icon',
+ 'swf' => 'application/x-shockwave-flash',
+ 'flv' => 'video/x-flv',
+ 'svg' => 'image/svg+xml',
+ 'pdf' => 'application/pdf',
+ 'zip' => 'application/zip',
+ 'gz' => 'application/x-gzip',
+ 'tar' => 'application/x-tar',
+ 'bz' => 'application/x-bzip',
+ 'bz2' => 'application/x-bzip2',
+ 'txt' => 'text/plain',
+ 'asc' => 'text/plain',
+ 'htm' => 'text/html',
+ 'html' => 'text/html',
+ 'css' => 'text/css',
+ 'js' => 'text/javascript',
+ 'xml' => 'text/xml',
+ 'xsl' => 'application/xsl+xml',
+ 'ogg' => 'application/ogg',
+ 'mp3' => 'audio/mpeg',
+ 'wav' => 'audio/x-wav',
+ 'avi' => 'video/x-msvideo',
+ 'mpg' => 'video/mpeg',
+ 'mpeg' => 'video/mpeg',
+ 'mov' => 'video/quicktime',
+ 'php' => 'text/x-php',
+ 'mp4' => 'video/mp4',
+ 'ogv' => 'video/ogg',
+ 'webm' => 'video/webm',
+ 'webp' => 'image/webp',
+ 'bmp' =>'image/x-ms-bmp',
+ 'psd' => 'image/vnd.adobe.photoshop',
+ 'mkv' => 'video/x-matroska'
+];
+
+/**
+ * Get MIME type for file
+ *
+ * The contents of this function are taken from the __getMimeType() function
+ * from the "Amazon S3 PHP class" which is Copyright (c) 2008, Donovan Schönknecht
+ * and released under the 'Simplified BSD License'.
+ */
+function getMimeType(string $file, string $ext=""): string
+{
+ // Static extension lookup
+ $ext = strtolower($ext);
+
+ if (array_key_exists($ext, MIME_TYPE_MAP)) {
+ return MIME_TYPE_MAP[$ext];
+ }
+
+ $type = false;
+ // Fileinfo documentation says fileinfo_open() will use the
+ // MAGIC env var for the magic file
+ if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
+ ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) {
+ if (($type = finfo_file($finfo, $file)) !== false) {
+ // Remove the charset and grab the last content-type
+ $type = explode(' ', str_replace('; charset=', ';charset=', $type));
+ $type = array_pop($type);
+ $type = explode(';', $type);
+ $type = trim(array_shift($type));
+ }
+ finfo_close($finfo);
+
+ // If anyone is still using mime_content_type()
+ } elseif (function_exists('mime_content_type')) {
+ $type = trim(mime_content_type($file));
+ }
+
+ if ($type !== false && strlen($type) > 0) {
+ return $type;
+ }
+
+ return 'application/octet-stream';
+}
+
+function get_extension(?string $mime_type): ?string
+{
+ if (empty($mime_type)) {
+ return null;
+ }
+
+ $ext = array_search($mime_type, MIME_TYPE_MAP);
+ return ($ext ? $ext : null);
+}
+
+/** @noinspection PhpUnhandledExceptionInspection */
+function getSubclassesOf(string $parent)
+{
+ $result = [];
+ foreach (get_declared_classes() as $class) {
+ $rclass = new ReflectionClass($class);
+ if (!$rclass->isAbstract() && is_subclass_of($class, $parent)) {
+ $result[] = $class;
+ }
+ }
+ return $result;
+}
+
+/**
+ * Like glob, with support for matching very long patterns with braces.
+ */
+function zglob(string $pattern): array
+{
+ $results = [];
+ if (preg_match('/(.*)\{(.*)\}(.*)/', $pattern, $matches)) {
+ $braced = explode(",", $matches[2]);
+ foreach ($braced as $b) {
+ $sub_pattern = $matches[1].$b.$matches[3];
+ $results = array_merge($results, zglob($sub_pattern));
+ }
+ return $results;
+ } else {
+ $r = glob($pattern);
+ if ($r) {
+ return $r;
+ } else {
+ return [];
+ }
+ }
+}
+
+/**
+ * Figure out the path to the shimmie install directory.
+ *
+ * eg if shimmie is visible at http://foo.com/gallery, this
+ * function should return /gallery
+ *
+ * PHP really, really sucks.
+ */
+function get_base_href(): string
+{
+ if (defined("BASE_HREF") && !empty(BASE_HREF)) {
+ return BASE_HREF;
+ }
+ $possible_vars = ['SCRIPT_NAME', 'PHP_SELF', 'PATH_INFO', 'ORIG_PATH_INFO'];
+ $ok_var = null;
+ foreach ($possible_vars as $var) {
+ if (isset($_SERVER[$var]) && substr($_SERVER[$var], -4) === '.php') {
+ $ok_var = $_SERVER[$var];
+ break;
+ }
+ }
+ assert(!empty($ok_var));
+ $dir = dirname($ok_var);
+ $dir = str_replace("\\", "/", $dir);
+ $dir = str_replace("//", "/", $dir);
+ $dir = rtrim($dir, "/");
+ return $dir;
+}
+
+function startsWith(string $haystack, string $needle): bool
+{
+ $length = strlen($needle);
+ return (substr($haystack, 0, $length) === $needle);
+}
+
+function endsWith(string $haystack, string $needle): bool
+{
+ $length = strlen($needle);
+ $start = $length * -1; //negative
+ return (substr($haystack, $start) === $needle);
+}
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
+* Input / Output Sanitising *
+\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/**
+ * Make some data safe for printing into HTML
+ */
+function html_escape(?string $input): string
+{
+ if (is_null($input)) {
+ return "";
+ }
+ return htmlentities($input, ENT_QUOTES, "UTF-8");
+}
+
+/**
+ * Unescape data that was made safe for printing into HTML
+ */
+function html_unescape(string $input): string
+{
+ return html_entity_decode($input, ENT_QUOTES, "UTF-8");
+}
+
+/**
+ * Make sure some data is safe to be used in integer context
+ */
+function int_escape(?string $input): int
+{
+ /*
+ Side note, Casting to an integer is FASTER than using intval.
+ http://hakre.wordpress.com/2010/05/13/php-casting-vs-intval/
+ */
+ if (is_null($input)) {
+ return 0;
+ }
+ return (int)$input;
+}
+
+/**
+ * Make sure some data is safe to be used in URL context
+ */
+function url_escape(?string $input): string
+{
+ if (is_null($input)) {
+ return "";
+ }
+ $input = rawurlencode($input);
+ return $input;
+}
+
+/**
+ * Turn all manner of HTML / INI / JS / DB booleans into a PHP one
+ */
+function bool_escape($input): bool
+{
+ /*
+ Sometimes, I don't like PHP -- this, is one of those times...
+ "a boolean FALSE is not considered a valid boolean value by this function."
+ Yay for Got'chas!
+ http://php.net/manual/en/filter.filters.validate.php
+ */
+ if (is_bool($input)) {
+ return $input;
+ } elseif (is_int($input)) {
+ return ($input === 1);
+ } else {
+ $value = filter_var($input, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
+ if (!is_null($value)) {
+ return $value;
+ } else {
+ $input = strtolower(trim($input));
+ return (
+ $input === "y" ||
+ $input === "yes" ||
+ $input === "t" ||
+ $input === "true" ||
+ $input === "on" ||
+ $input === "1"
+ );
+ }
+ }
+}
+
+/**
+ * Some functions require a callback function for escaping,
+ * but we might not want to alter the data
+ */
+function no_escape(string $input): string
+{
+ return $input;
+}
+
+function clamp(?int $val, ?int $min=null, ?int $max=null): int
+{
+ if (!is_numeric($val) || (!is_null($min) && $val < $min)) {
+ $val = $min;
+ }
+ if (!is_null($max) && $val > $max) {
+ $val = $max;
+ }
+ if (!is_null($min) && !is_null($max)) {
+ assert($val >= $min && $val <= $max, "$min <= $val <= $max");
+ }
+ return $val;
+}
+
+function xml_tag(string $name, array $attrs=[], array $children=[]): string
+{
+ $xml = "<$name ";
+ foreach ($attrs as $k => $v) {
+ $xv = str_replace(''', ''', htmlspecialchars((string)$v, ENT_QUOTES));
+ $xml .= "$k=\"$xv\" ";
+ }
+ if (count($children) > 0) {
+ $xml .= ">\n";
+ foreach ($children as $child) {
+ $xml .= xml_tag($child);
+ }
+ $xml .= "$name>\n";
+ } else {
+ $xml .= "/>\n";
+ }
+ return $xml;
+}
+
+/**
+ * Original PHP code by Chirp Internet: www.chirp.com.au
+ * Please acknowledge use of this code by including this header.
+ */
+function truncate(string $string, int $limit, string $break=" ", string $pad="..."): string
+{
+ // return with no change if string is shorter than $limit
+ if (strlen($string) <= $limit) {
+ return $string;
+ }
+
+ // is $break present between $limit and the end of the string?
+ if (false !== ($breakpoint = strpos($string, $break, $limit))) {
+ if ($breakpoint < strlen($string) - 1) {
+ $string = substr($string, 0, $breakpoint) . $pad;
+ }
+ }
+
+ return $string;
+}
+
+/**
+ * Turn a human readable filesize into an integer, eg 1KB -> 1024
+ */
+function parse_shorthand_int(string $limit): int
+{
+ if (preg_match('/^([\d\.]+)([gmk])?b?$/i', (string)$limit, $m)) {
+ $value = $m[1];
+ if (isset($m[2])) {
+ switch (strtolower($m[2])) {
+ /** @noinspection PhpMissingBreakStatementInspection */
+ case 'g': $value *= 1024; // fall through
+ /** @noinspection PhpMissingBreakStatementInspection */
+ // no break
+ case 'm': $value *= 1024; // fall through
+ /** @noinspection PhpMissingBreakStatementInspection */
+ // no break
+ case 'k': $value *= 1024; break;
+ default: $value = -1;
+ }
+ }
+ return (int)$value;
+ } else {
+ return -1;
+ }
+}
+
+/**
+ * Turn an integer into a human readable filesize, eg 1024 -> 1KB
+ */
+function to_shorthand_int(int $int): string
+{
+ assert($int >= 0);
+
+ if ($int >= pow(1024, 3)) {
+ return sprintf("%.1fGB", $int / pow(1024, 3));
+ } elseif ($int >= pow(1024, 2)) {
+ return sprintf("%.1fMB", $int / pow(1024, 2));
+ } elseif ($int >= 1024) {
+ return sprintf("%.1fKB", $int / 1024);
+ } else {
+ return (string)$int;
+ }
+}
+
+const TIME_UNITS = ["s"=>60,"m"=>60,"h"=>24,"d"=>365,"y"=>PHP_INT_MAX];
+function format_milliseconds(int $input): string
+{
+ $output = "";
+
+ $remainder = floor($input / 1000);
+
+ foreach (TIME_UNITS as $unit=>$conversion) {
+ $count = $remainder % $conversion;
+ $remainder = floor($remainder / $conversion);
+ if ($count==0&&$remainder<1) {
+ break;
+ }
+ $output = "$count".$unit." ".$output;
+ }
+
+ return trim($output);
+}
+
+/**
+ * Turn a date into a time, a date, an "X minutes ago...", etc
+ */
+function autodate(string $date, bool $html=true): string
+{
+ $cpu = date('c', strtotime($date));
+ $hum = date('F j, Y; H:i', strtotime($date));
+ return ($html ? "" : $hum);
+}
+
+/**
+ * Check if a given string is a valid date-time. ( Format: yyyy-mm-dd hh:mm:ss )
+ */
+function isValidDateTime(string $dateTime): bool
+{
+ if (preg_match("/^(\d{4})-(\d{2})-(\d{2}) ([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/", $dateTime, $matches)) {
+ if (checkdate((int)$matches[2], (int)$matches[3], (int)$matches[1])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Check if a given string is a valid date. ( Format: yyyy-mm-dd )
+ */
+function isValidDate(string $date): bool
+{
+ if (preg_match("/^(\d{4})-(\d{2})-(\d{2})$/", $date, $matches)) {
+ // checkdate wants (month, day, year)
+ if (checkdate((int)$matches[2], (int)$matches[3], (int)$matches[1])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function validate_input(array $inputs): array
+{
+ $outputs = [];
+
+ foreach ($inputs as $key => $validations) {
+ $flags = explode(',', $validations);
+
+ if (in_array('bool', $flags) && !isset($_POST[$key])) {
+ $_POST[$key] = 'off';
+ }
+
+ if (in_array('optional', $flags)) {
+ if (!isset($_POST[$key]) || trim($_POST[$key]) == "") {
+ $outputs[$key] = null;
+ continue;
+ }
+ }
+ if (!isset($_POST[$key]) || trim($_POST[$key]) == "") {
+ throw new InvalidInput("Input '$key' not set");
+ }
+
+ $value = trim($_POST[$key]);
+
+ if (in_array('user_id', $flags)) {
+ $id = int_escape($value);
+ if (in_array('exists', $flags)) {
+ if (is_null(User::by_id($id))) {
+ throw new InvalidInput("User #$id does not exist");
+ }
+ }
+ $outputs[$key] = $id;
+ } elseif (in_array('user_name', $flags)) {
+ if (strlen($value) < 1) {
+ throw new InvalidInput("Username must be at least 1 character");
+ } elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $value)) {
+ throw new InvalidInput(
+ "Username contains invalid characters. Allowed characters are ".
+ "letters, numbers, dash, and underscore"
+ );
+ }
+ $outputs[$key] = $value;
+ } elseif (in_array('user_class', $flags)) {
+ global $_shm_user_classes;
+ if (!array_key_exists($value, $_shm_user_classes)) {
+ throw new InvalidInput("Invalid user class: ".html_escape($value));
+ }
+ $outputs[$key] = $value;
+ } elseif (in_array('email', $flags)) {
+ $outputs[$key] = trim($value);
+ } elseif (in_array('password', $flags)) {
+ $outputs[$key] = $value;
+ } elseif (in_array('int', $flags)) {
+ $value = trim($value);
+ if (empty($value) || !is_numeric($value)) {
+ throw new InvalidInput("Invalid int: ".html_escape($value));
+ }
+ $outputs[$key] = (int)$value;
+ } elseif (in_array('bool', $flags)) {
+ $outputs[$key] = bool_escape($value);
+ } elseif (in_array('date', $flags)) {
+ $outputs[$key] = date("Y-m-d H:i:s", strtotime(trim($value)));
+ } elseif (in_array('string', $flags)) {
+ if (in_array('trim', $flags)) {
+ $value = trim($value);
+ }
+ if (in_array('lower', $flags)) {
+ $value = strtolower($value);
+ }
+ if (in_array('not-empty', $flags)) {
+ throw new InvalidInput("$key must not be blank");
+ }
+ if (in_array('nullify', $flags)) {
+ if (empty($value)) {
+ $value = null;
+ }
+ }
+ $outputs[$key] = $value;
+ } else {
+ throw new InvalidInput("Unknown validation '$validations'");
+ }
+ }
+
+ return $outputs;
+}
+
+/**
+ * Translates all possible directory separators to the appropriate one for the current system,
+ * and removes any duplicate separators.
+ */
+function sanitize_path(string $path): string
+{
+ return preg_replace('|[\\\\/]+|S', DIRECTORY_SEPARATOR, $path);
+}
+
+/**
+ * Combines all path segments specified, ensuring no duplicate separators occur,
+ * as well as converting all possible separators to the one appropriate for the current system.
+ */
+function join_path(string ...$paths): string
+{
+ $output = "";
+ foreach ($paths as $path) {
+ if (empty($path)) {
+ continue;
+ }
+ $path = sanitize_path($path);
+ if (empty($output)) {
+ $output = $path;
+ } else {
+ $output = rtrim($output, DIRECTORY_SEPARATOR);
+ $path = ltrim($path, DIRECTORY_SEPARATOR);
+ $output .= DIRECTORY_SEPARATOR . $path;
+ }
+ }
+ return $output;
+}
+
+/**
+ * Perform callback on each item returned by an iterator.
+ */
+function iterator_map(callable $callback, iterator $iter): Generator
+{
+ foreach ($iter as $i) {
+ yield call_user_func($callback, $i);
+ }
+}
+
+/**
+ * Perform callback on each item returned by an iterator and combine the result into an array.
+ */
+function iterator_map_to_array(callable $callback, iterator $iter): array
+{
+ return iterator_to_array(iterator_map($callback, $iter));
+}
+
+function stringer($s)
+{
+ if (is_array($s)) {
+ if (isset($s[0])) {
+ return "[" . implode(", ", array_map("stringer", $s)) . "]";
+ } else {
+ $pairs = [];
+ foreach ($s as $k=>$v) {
+ $pairs[] = "\"$k\"=>" . stringer($v);
+ }
+ return "[" . implode(", ", $pairs) . "]";
+ }
+ }
+ if (is_string($s)) {
+ return "\"$s\""; // FIXME: handle escaping quotes
+ }
+ return (string)$s;
+}
diff --git a/core/send_event.php b/core/send_event.php
new file mode 100644
index 00000000..98a48462
--- /dev/null
+++ b/core/send_event.php
@@ -0,0 +1,128 @@
+info->is_supported()) {
+ continue;
+ }
+
+ foreach (get_class_methods($extension) as $method) {
+ if (substr($method, 0, 2) == "on") {
+ $event = substr($method, 2) . "Event";
+ $pos = $extension->get_priority() * 100;
+ while (isset($_shm_event_listeners[$event][$pos])) {
+ $pos += 1;
+ }
+ $_shm_event_listeners[$event][$pos] = $extension;
+ }
+ }
+ }
+}
+
+function _dump_event_listeners(array $event_listeners, string $path): void
+{
+ $p = "<"."?php\n";
+
+ foreach (getSubclassesOf("Extension") as $class) {
+ $p .= "\$$class = new $class(); ";
+ }
+
+ $p .= "\$_shm_event_listeners = array(\n";
+ foreach ($event_listeners as $event => $listeners) {
+ $p .= "\t'$event' => array(\n";
+ foreach ($listeners as $id => $listener) {
+ $p .= "\t\t$id => \$".get_class($listener).",\n";
+ }
+ $p .= "\t),\n";
+ }
+ $p .= ");\n";
+
+ file_put_contents($path, $p);
+}
+
+
+/** @private */
+global $_shm_event_count;
+$_shm_event_count = 0;
+
+/**
+ * Send an event to all registered Extensions.
+ */
+function send_event(Event $event): Event
+{
+ global $tracer_enabled;
+
+ global $_shm_event_listeners, $_shm_event_count, $_tracer;
+ if (!isset($_shm_event_listeners[get_class($event)])) {
+ return $event;
+ }
+ $method_name = "on".str_replace("Event", "", get_class($event));
+
+ // send_event() is performance sensitive, and with the number
+ // of times tracer gets called the time starts to add up
+ if ($tracer_enabled) {
+ $_tracer->begin(get_class($event));
+ }
+ // SHIT: http://bugs.php.net/bug.php?id=35106
+ $my_event_listeners = $_shm_event_listeners[get_class($event)];
+ ksort($my_event_listeners);
+
+ foreach ($my_event_listeners as $listener) {
+ if ($tracer_enabled) {
+ $_tracer->begin(get_class($listener));
+ }
+ if (method_exists($listener, $method_name)) {
+ $listener->$method_name($event);
+ }
+ if ($tracer_enabled) {
+ $_tracer->end();
+ }
+ if ($event->stop_processing===true) {
+ break;
+ }
+ }
+ $_shm_event_count++;
+ if ($tracer_enabled) {
+ $_tracer->end();
+ }
+
+ return $event;
+}
diff --git a/core/sys_config.inc.php b/core/sys_config.inc.php
deleted file mode 100644
index cb8d2b4a..00000000
--- a/core/sys_config.inc.php
+++ /dev/null
@@ -1,53 +0,0 @@
-set_mode(PageMode::PAGE);
+ ob_start();
+ $page->display();
+ ob_end_clean();
+ $this->assertTrue(true); // doesn't crash
+ }
+
+ public function test_file()
+ {
+ $page = new BasePage();
+ $page->set_mode(PageMode::FILE);
+ $page->set_file("tests/pbx_screenshot.jpg");
+ ob_start();
+ $page->display();
+ ob_end_clean();
+ $this->assertTrue(true); // doesn't crash
+ }
+
+ public function test_data()
+ {
+ $page = new BasePage();
+ $page->set_mode(PageMode::DATA);
+ $page->set_data("hello world");
+ ob_start();
+ $page->display();
+ ob_end_clean();
+ $this->assertTrue(true); // doesn't crash
+ }
+
+ public function test_redirect()
+ {
+ $page = new BasePage();
+ $page->set_mode(PageMode::REDIRECT);
+ $page->set_redirect("/new/page");
+ ob_start();
+ $page->display();
+ ob_end_clean();
+ $this->assertTrue(true); // doesn't crash
+ }
+}
diff --git a/core/tests/block.test.php b/core/tests/block.test.php
new file mode 100644
index 00000000..bdaa1e97
--- /dev/null
+++ b/core/tests/block.test.php
@@ -0,0 +1,17 @@
+assertEquals(
+ "
head
body
\n",
+ $b->get_html()
+ );
+ }
+}
diff --git a/core/tests/polyfills.test.php b/core/tests/polyfills.test.php
new file mode 100644
index 00000000..84c3eea0
--- /dev/null
+++ b/core/tests/polyfills.test.php
@@ -0,0 +1,223 @@
+assertEquals(
+ html_escape("Foo & "),
+ "Foo & <waffles>"
+ );
+
+ $this->assertEquals(
+ html_unescape("Foo & <waffles>"),
+ "Foo & "
+ );
+
+ $x = "Foo & <waffles>";
+ $this->assertEquals(html_escape(html_unescape($x)), $x);
+ }
+
+ public function test_int_escape()
+ {
+ $this->assertEquals(int_escape(""), 0);
+ $this->assertEquals(int_escape("1"), 1);
+ $this->assertEquals(int_escape("-1"), -1);
+ $this->assertEquals(int_escape("-1.5"), -1);
+ $this->assertEquals(int_escape(null), 0);
+ }
+
+ public function test_url_escape()
+ {
+ $this->assertEquals(url_escape("^\o/^"), "%5E%5Co%2F%5E");
+ $this->assertEquals(url_escape(null), "");
+ }
+
+ public function test_bool_escape()
+ {
+ $this->assertTrue(bool_escape(true));
+ $this->assertFalse(bool_escape(false));
+
+ $this->assertTrue(bool_escape("true"));
+ $this->assertFalse(bool_escape("false"));
+
+ $this->assertTrue(bool_escape("t"));
+ $this->assertFalse(bool_escape("f"));
+
+ $this->assertTrue(bool_escape("T"));
+ $this->assertFalse(bool_escape("F"));
+
+ $this->assertTrue(bool_escape("yes"));
+ $this->assertFalse(bool_escape("no"));
+
+ $this->assertTrue(bool_escape("Yes"));
+ $this->assertFalse(bool_escape("No"));
+
+ $this->assertTrue(bool_escape("on"));
+ $this->assertFalse(bool_escape("off"));
+
+ $this->assertTrue(bool_escape(1));
+ $this->assertFalse(bool_escape(0));
+
+ $this->assertTrue(bool_escape("1"));
+ $this->assertFalse(bool_escape("0"));
+ }
+
+ public function test_clamp()
+ {
+ $this->assertEquals(clamp(0, 5, 10), 5);
+ $this->assertEquals(clamp(5, 5, 10), 5);
+ $this->assertEquals(clamp(7, 5, 10), 7);
+ $this->assertEquals(clamp(10, 5, 10), 10);
+ $this->assertEquals(clamp(15, 5, 10), 10);
+ }
+
+ public function test_xml_tag()
+ {
+ $this->assertEquals(
+ "\n\n\n",
+ xml_tag("test", ["foo"=>"bar"], ["cake"])
+ );
+ }
+
+ public function test_truncate()
+ {
+ $this->assertEquals(truncate("test words", 10), "test words");
+ $this->assertEquals(truncate("test...", 9), "test...");
+ $this->assertEquals(truncate("test...", 6), "test...");
+ $this->assertEquals(truncate("te...", 2), "te...");
+ }
+
+ public function test_to_shorthand_int()
+ {
+ $this->assertEquals(to_shorthand_int(1231231231), "1.1GB");
+ $this->assertEquals(to_shorthand_int(2), "2");
+ }
+
+ public function test_parse_shorthand_int()
+ {
+ $this->assertEquals(parse_shorthand_int("foo"), -1);
+ $this->assertEquals(parse_shorthand_int("32M"), 33554432);
+ $this->assertEquals(parse_shorthand_int("43.4KB"), 44441);
+ $this->assertEquals(parse_shorthand_int("1231231231"), 1231231231);
+ }
+
+ public function test_format_milliseconds()
+ {
+ $this->assertEquals("", format_milliseconds(5));
+ $this->assertEquals("5s", format_milliseconds(5000));
+ $this->assertEquals("1y 213d 16h 53m 20s", format_milliseconds(50000000000));
+ }
+
+ public function test_autodate()
+ {
+ $this->assertEquals(
+ "",
+ autodate("2012-06-23 16:14:22")
+ );
+ }
+
+ public function test_validate_input()
+ {
+ $_POST = [
+ "foo" => " bar ",
+ "to_null" => " ",
+ "num" => "42",
+ ];
+ $this->assertEquals(
+ ["foo"=>"bar"],
+ validate_input(["foo"=>"string,trim,lower"])
+ );
+ //$this->assertEquals(
+ // ["to_null"=>null],
+ // validate_input(["to_null"=>"string,trim,nullify"])
+ //);
+ $this->assertEquals(
+ ["num"=>42],
+ validate_input(["num"=>"int"])
+ );
+ }
+
+ public function test_sanitize_path()
+ {
+ $this->assertEquals(
+ "one",
+ sanitize_path("one")
+ );
+
+ $this->assertEquals(
+ "one".DIRECTORY_SEPARATOR."two",
+ sanitize_path("one\\two")
+ );
+
+ $this->assertEquals(
+ "one".DIRECTORY_SEPARATOR."two",
+ sanitize_path("one/two")
+ );
+
+ $this->assertEquals(
+ "one".DIRECTORY_SEPARATOR."two",
+ sanitize_path("one\\\\two")
+ );
+
+ $this->assertEquals(
+ "one".DIRECTORY_SEPARATOR."two",
+ sanitize_path("one//two")
+ );
+
+ $this->assertEquals(
+ "one".DIRECTORY_SEPARATOR."two",
+ sanitize_path("one\\\\\\two")
+ );
+
+ $this->assertEquals(
+ "one".DIRECTORY_SEPARATOR."two",
+ sanitize_path("one///two")
+ );
+
+ $this->assertEquals(
+ DIRECTORY_SEPARATOR."one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR,
+ sanitize_path("\\/one/\\/\\/two\\/")
+ );
+ }
+
+ public function test_join_path()
+ {
+ $this->assertEquals(
+ "one",
+ join_path("one")
+ );
+
+ $this->assertEquals(
+ "one".DIRECTORY_SEPARATOR."two",
+ join_path("one", "two")
+ );
+
+ $this->assertEquals(
+ "one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR."three",
+ join_path("one", "two", "three")
+ );
+
+ $this->assertEquals(
+ "one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR."three",
+ join_path("one/two", "three")
+ );
+
+ $this->assertEquals(
+ DIRECTORY_SEPARATOR."one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR."three".DIRECTORY_SEPARATOR,
+ join_path("\\/////\\\\one/\///"."\\//two\/\\//\\//", "//\/\\\/three/\\/\/")
+ );
+ }
+
+ public function test_stringer()
+ {
+ $this->assertEquals(
+ '["foo"=>"bar", "baz"=>[1, 2, 3], "qux"=>["a"=>"b"]]',
+ stringer(["foo"=>"bar", "baz"=>[1,2,3], "qux"=>["a"=>"b"]])
+ );
+ }
+}
diff --git a/core/tests/tag.test.php b/core/tests/tag.test.php
new file mode 100644
index 00000000..ef8e947f
--- /dev/null
+++ b/core/tests/tag.test.php
@@ -0,0 +1,22 @@
+assertEquals("foo", Tag::decaret("foo"));
+ $this->assertEquals("foo?", Tag::decaret("foo^q"));
+ $this->assertEquals("a^b/c\\d?e&f", Tag::decaret("a^^b^sc^bd^qe^af"));
+ }
+
+ public function test_decaret()
+ {
+ $this->assertEquals("foo", Tag::caret("foo"));
+ $this->assertEquals("foo^q", Tag::caret("foo?"));
+ $this->assertEquals("a^^b^sc^bd^qe^af", Tag::caret("a^b/c\\d?e&f"));
+ }
+}
diff --git a/core/tests/urls.test.php b/core/tests/urls.test.php
new file mode 100644
index 00000000..b6ff90f0
--- /dev/null
+++ b/core/tests/urls.test.php
@@ -0,0 +1,42 @@
+assertEquals(
+ "/test/foo",
+ make_link("foo")
+ );
+
+ $this->assertEquals(
+ "/test/foo",
+ make_link("/foo")
+ );
+ }
+
+ public function test_make_http()
+ {
+ // relative to shimmie install
+ $this->assertEquals(
+ "http:///test/foo",
+ make_http("foo")
+ );
+
+ // relative to web server
+ $this->assertEquals(
+ "http:///foo",
+ make_http("/foo")
+ );
+
+ // absolute
+ $this->assertEquals(
+ "http://foo.com",
+ make_http("http://foo.com")
+ );
+ }
+}
diff --git a/core/tests/util.test.php b/core/tests/util.test.php
new file mode 100644
index 00000000..94f2273b
--- /dev/null
+++ b/core/tests/util.test.php
@@ -0,0 +1,86 @@
+assertEquals(
+ join_path(DATA_DIR, "base", $hash),
+ warehouse_path("base", $hash, false, 0)
+ );
+
+ $this->assertEquals(
+ join_path(DATA_DIR, "base", "7a", $hash),
+ warehouse_path("base", $hash, false, 1)
+ );
+
+ $this->assertEquals(
+ join_path(DATA_DIR, "base", "7a", "c1", $hash),
+ warehouse_path("base", $hash, false, 2)
+ );
+
+ $this->assertEquals(
+ join_path(DATA_DIR, "base", "7a", "c1", "9c", $hash),
+ warehouse_path("base", $hash, false, 3)
+ );
+
+ $this->assertEquals(
+ join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", $hash),
+ warehouse_path("base", $hash, false, 4)
+ );
+
+ $this->assertEquals(
+ join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", $hash),
+ warehouse_path("base", $hash, false, 5)
+ );
+
+ $this->assertEquals(
+ join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", $hash),
+ warehouse_path("base", $hash, false, 6)
+ );
+
+ $this->assertEquals(
+ join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", $hash),
+ warehouse_path("base", $hash, false, 7)
+ );
+
+ $this->assertEquals(
+ join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
+ warehouse_path("base", $hash, false, 8)
+ );
+
+ $this->assertEquals(
+ join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
+ warehouse_path("base", $hash, false, 9)
+ );
+
+ $this->assertEquals(
+ join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
+ warehouse_path("base", $hash, false, 10)
+ );
+ }
+
+ public function test_load_balance_url()
+ {
+ $hash = "7ac19c10d6859415";
+ $ext = "jpg";
+
+ // pseudo-randomly select one of the image servers, balanced in given ratio
+ $this->assertEquals(
+ "https://baz.mycdn.com/7ac19c10d6859415.jpg",
+ load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash)
+ );
+
+ // N'th and N+1'th results should be different
+ $this->assertNotEquals(
+ load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash, 0),
+ load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash, 1)
+ );
+ }
+}
diff --git a/core/urls.php b/core/urls.php
new file mode 100644
index 00000000..72dc1b79
--- /dev/null
+++ b/core/urls.php
@@ -0,0 +1,118 @@
+page = $page;
+ $this->query = $query;
+ }
+
+ public function make_link(): string
+ {
+ return make_link($this->page, $this->query);
+ }
+}
+
+/**
+ * Figure out the correct way to link to a page, taking into account
+ * things like the nice URLs setting.
+ *
+ * eg make_link("post/list") becomes "/v2/index.php?q=post/list"
+ */
+function make_link(?string $page=null, ?string $query=null): string
+{
+ global $config;
+
+ if (is_null($page)) {
+ $page = $config->get_string(SetupConfig::MAIN_PAGE);
+ }
+
+ $install_dir = get_base_href();
+ if (SPEED_HAX || $config->get_bool('nice_urls', false)) {
+ $base = $install_dir;
+ } else {
+ $base = "$install_dir/index.php?q=";
+ }
+
+ if (is_null($query)) {
+ return str_replace("//", "/", $base.'/'.$page);
+ } else {
+ if (strpos($base, "?")) {
+ return $base .'/'. $page .'&'. $query;
+ } elseif (strpos($query, "#") === 0) {
+ return $base .'/'. $page . $query;
+ } else {
+ return $base .'/'. $page .'?'. $query;
+ }
+ }
+}
+
+
+/**
+ * Take the current URL and modify some parameters
+ */
+function modify_current_url(array $changes): string
+{
+ return modify_url($_SERVER['QUERY_STRING'], $changes);
+}
+
+function modify_url(string $url, array $changes): string
+{
+ // SHIT: PHP is officially the worst web API ever because it does not
+ // have a built-in function to do this.
+
+ // SHIT: parse_str is magically retarded; not only is it a useless name, it also
+ // didn't return the parsed array, preferring to overwrite global variables with
+ // whatever data the user supplied. Thankfully, 4.0.3 added an extra option to
+ // give it an array to use...
+ $params = [];
+ parse_str($url, $params);
+
+ if (isset($changes['q'])) {
+ $base = $changes['q'];
+ unset($changes['q']);
+ } else {
+ $base = _get_query();
+ }
+
+ if (isset($params['q'])) {
+ unset($params['q']);
+ }
+
+ foreach ($changes as $k => $v) {
+ if (is_null($v) and isset($params[$k])) {
+ unset($params[$k]);
+ }
+ $params[$k] = $v;
+ }
+
+ return make_link($base, http_build_query($params));
+}
+
+
+/**
+ * Turn a relative link into an absolute one, including hostname
+ */
+function make_http(string $link): string
+{
+ if (strpos($link, "://") > 0) {
+ return $link;
+ }
+
+ if (strlen($link) > 0 && $link[0] != '/') {
+ $link = get_base_href() . '/' . $link;
+ }
+
+ $protocol = is_https_enabled() ? "https://" : "http://";
+ $link = $protocol . $_SERVER["HTTP_HOST"] . $link;
+ $link = str_replace("/./", "/", $link);
+
+ return $link;
+}
diff --git a/core/user.class.php b/core/user.class.php
deleted file mode 100644
index 662300cd..00000000
--- a/core/user.class.php
+++ /dev/null
@@ -1,300 +0,0 @@
-id = int_escape($row['id']);
- $this->name = $row['name'];
- $this->email = $row['email'];
- $this->join_date = $row['joindate'];
- $this->passhash = $row['pass'];
-
- if(array_key_exists($row["class"], $_shm_user_classes)) {
- $this->class = $_shm_user_classes[$row["class"]];
- }
- else {
- throw new SCoreException("User '{$this->name}' has invalid class '{$row["class"]}'");
- }
- }
-
- /**
- * Construct a User by session.
- *
- * @param string $name
- * @param string $session
- * @return null|User
- */
- public static function by_session(/*string*/ $name, /*string*/ $session) {
- global $config, $database;
- $row = $database->cache->get("user-session:$name-$session");
- if(!$row) {
- if($database->get_driver_name() === "mysql") {
- $query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess";
- }
- else {
- $query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess";
- }
- $row = $database->get_row($query, array("name"=>$name, "ip"=>get_session_ip($config), "sess"=>$session));
- $database->cache->set("user-session:$name-$session", $row, 600);
- }
- return is_null($row) ? null : new User($row);
- }
-
- /**
- * Construct a User by session.
- * @param int $id
- * @return null|User
- */
- public static function by_id(/*int*/ $id) {
- assert('is_numeric($id)', var_export($id, true));
- global $database;
- if($id === 1) {
- $cached = $database->cache->get('user-id:'.$id);
- if($cached) return new User($cached);
- }
- $row = $database->get_row("SELECT * FROM users WHERE id = :id", array("id"=>$id));
- if($id === 1) $database->cache->set('user-id:'.$id, $row, 600);
- return is_null($row) ? null : new User($row);
- }
-
- /**
- * Construct a User by name.
- * @param string $name
- * @return null|User
- */
- public static function by_name(/*string*/ $name) {
- assert('is_string($name)', var_export($name, true));
- global $database;
- $row = $database->get_row($database->scoreql_to_sql("SELECT * FROM users WHERE SCORE_STRNORM(name) = SCORE_STRNORM(:name)"), array("name"=>$name));
- return is_null($row) ? null : new User($row);
- }
-
- /**
- * Construct a User by name and password.
- * @param string $name
- * @param string $pass
- * @return null|User
- */
- public static function by_name_and_pass(/*string*/ $name, /*string*/ $pass) {
- assert('is_string($name)', var_export($name, true));
- assert('is_string($pass)', var_export($pass, true));
- $user = User::by_name($name);
- if($user) {
- if($user->passhash == md5(strtolower($name) . $pass)) {
- $user->set_password($pass);
- }
- if(password_verify($pass, $user->passhash)) {
- return $user;
- }
- }
- }
-
-
- /* useful user object functions start here */
-
-
- /**
- * @param string $ability
- * @return bool
- */
- public function can($ability) {
- return $this->class->can($ability);
- }
-
-
- /**
- * Test if this user is anonymous (not logged in).
- *
- * @return bool
- */
- public function is_anonymous() {
- global $config;
- return ($this->id === $config->get_int('anon_id'));
- }
-
- /**
- * Test if this user is logged in.
- *
- * @return bool
- */
- public function is_logged_in() {
- global $config;
- return ($this->id !== $config->get_int('anon_id'));
- }
-
- /**
- * Test if this user is an administrator.
- *
- * @return bool
- */
- public function is_admin() {
- return ($this->class->name === "admin");
- }
-
- /**
- * @param string $class
- */
- public function set_class(/*string*/ $class) {
- assert('is_string($class)', var_export($class, true));
- global $database;
- $database->Execute("UPDATE users SET class=:class WHERE id=:id", array("class"=>$class, "id"=>$this->id));
- log_info("core-user", 'Set class for '.$this->name.' to '.$class);
- }
-
- /**
- * @param string $name
- * @throws Exception
- */
- public function set_name(/*string*/ $name) {
- global $database;
- if(User::by_name($name)) {
- throw new Exception("Desired username is already in use");
- }
- $old_name = $this->name;
- $this->name = $name;
- $database->Execute("UPDATE users SET name=:name WHERE id=:id", array("name"=>$this->name, "id"=>$this->id));
- log_info("core-user", "Changed username for {$old_name} to {$this->name}");
- }
-
- /**
- * @param string $password
- */
- public function set_password(/*string*/ $password) {
- global $database;
- $hash = password_hash($password, PASSWORD_BCRYPT);
- if(is_string($hash)) {
- $this->passhash = $hash;
- $database->Execute("UPDATE users SET pass=:hash WHERE id=:id", array("hash"=>$this->passhash, "id"=>$this->id));
- log_info("core-user", 'Set password for '.$this->name);
- }
- else {
- throw new SCoreException("Failed to hash password");
- }
- }
-
- /**
- * @param string $address
- */
- public function set_email(/*string*/ $address) {
- global $database;
- $database->Execute("UPDATE users SET email=:email WHERE id=:id", array("email"=>$address, "id"=>$this->id));
- log_info("core-user", 'Set email for '.$this->name);
- }
-
- /**
- * Get a snippet of HTML which will render the user's avatar, be that
- * a local file, a remote file, a gravatar, a something else, etc.
- *
- * @return String of HTML
- */
- public function get_avatar_html() {
- // FIXME: configurable
- global $config;
- if($config->get_string("avatar_host") === "gravatar") {
- if(!empty($this->email)) {
- $hash = md5(strtolower($this->email));
- $s = $config->get_string("avatar_gravatar_size");
- $d = urlencode($config->get_string("avatar_gravatar_default"));
- $r = $config->get_string("avatar_gravatar_rating");
- $cb = date("Y-m-d");
- return "";
- }
- }
- return "";
- }
-
- /**
- * Get an auth token to be used in POST forms
- *
- * password = secret, avoid storing directly
- * passhash = bcrypt(password), so someone who gets to the database can't get passwords
- * sesskey = md5(passhash . IP), so if it gets sniffed it can't be used from another IP,
- * and it can't be used to get the passhash to generate new sesskeys
- * authtok = md5(sesskey, salt), presented to the user in web forms, to make sure that
- * the form was generated within the session. Salted and re-hashed so that
- * reading a web page from the user's cache doesn't give access to the session key
- *
- * @return string A string containing auth token (MD5sum)
- */
- public function get_auth_token() {
- global $config;
- $salt = DATABASE_DSN;
- $addr = get_session_ip($config);
- return md5(md5($this->passhash . $addr) . "salty-csrf-" . $salt);
- }
-
- public function get_auth_html() {
- $at = $this->get_auth_token();
- return '';
- }
-
- public function check_auth_token() {
- return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token());
- }
-}
-
-class MockUser extends User {
- public function __construct($name) {
- $row = array(
- "name" => $name,
- "id" => 1,
- "email" => "",
- "joindate" => "",
- "pass" => "",
- "class" => "admin",
- );
- parent::__construct($row);
- }
-}
-
diff --git a/core/user.php b/core/user.php
new file mode 100644
index 00000000..75406597
--- /dev/null
+++ b/core/user.php
@@ -0,0 +1,259 @@
+id = int_escape((string)$row['id']);
+ $this->name = $row['name'];
+ $this->email = $row['email'];
+ $this->join_date = $row['joindate'];
+ $this->passhash = $row['pass'];
+
+ if (array_key_exists($row["class"], $_shm_user_classes)) {
+ $this->class = $_shm_user_classes[$row["class"]];
+ } else {
+ throw new SCoreException("User '{$this->name}' has invalid class '{$row["class"]}'");
+ }
+ }
+
+ public static function by_session(string $name, string $session): ?User
+ {
+ global $cache, $config, $database;
+ $row = $cache->get("user-session:$name-$session");
+ if (!$row) {
+ if ($database->get_driver_name() === DatabaseDriver::MYSQL) {
+ $query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess";
+ } else {
+ $query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess";
+ }
+ $row = $database->get_row($query, ["name"=>$name, "ip"=>get_session_ip($config), "sess"=>$session]);
+ $cache->set("user-session:$name-$session", $row, 600);
+ }
+ return is_null($row) ? null : new User($row);
+ }
+
+ public static function by_id(int $id): ?User
+ {
+ global $cache, $database;
+ if ($id === 1) {
+ $cached = $cache->get('user-id:'.$id);
+ if ($cached) {
+ return new User($cached);
+ }
+ }
+ $row = $database->get_row("SELECT * FROM users WHERE id = :id", ["id"=>$id]);
+ if ($id === 1) {
+ $cache->set('user-id:'.$id, $row, 600);
+ }
+ return is_null($row) ? null : new User($row);
+ }
+
+ public static function by_name(string $name): ?User
+ {
+ global $database;
+ $row = $database->get_row("SELECT * FROM users WHERE LOWER(name) = LOWER(:name)", ["name"=>$name]);
+ return is_null($row) ? null : new User($row);
+ }
+
+ public static function name_to_id(string $name): int
+ {
+ $u = User::by_name($name);
+ if (is_null($u)) {
+ throw new ScoreException("Can't find any user named $name");
+ } else {
+ return $u->id;
+ }
+ }
+
+ public static function by_name_and_pass(string $name, string $pass): ?User
+ {
+ $my_user = User::by_name($name);
+
+ // If user tried to log in as "foo bar" and failed, try "foo_bar"
+ if (!$my_user && strpos($name, " ") !== false) {
+ $my_user = User::by_name(str_replace(" ", "_", $name));
+ }
+
+ if ($my_user) {
+ if ($my_user->passhash == md5(strtolower($name) . $pass)) {
+ log_info("core-user", "Migrating from md5 to bcrypt for $name");
+ $my_user->set_password($pass);
+ }
+ if (password_verify($pass, $my_user->passhash)) {
+ log_info("core-user", "Logged in as $name ({$my_user->class->name})");
+ return $my_user;
+ } else {
+ log_warning("core-user", "Failed to log in as $name (Invalid password)");
+ }
+ } else {
+ log_warning("core-user", "Failed to log in as $name (Invalid username)");
+ }
+ return null;
+ }
+
+
+ /* useful user object functions start here */
+
+ public function can(string $ability): bool
+ {
+ return $this->class->can($ability);
+ }
+
+
+ public function is_anonymous(): bool
+ {
+ global $config;
+ return ($this->id === $config->get_int('anon_id'));
+ }
+
+ public function is_logged_in(): bool
+ {
+ global $config;
+ return ($this->id !== $config->get_int('anon_id'));
+ }
+
+ public function set_class(string $class): void
+ {
+ global $database;
+ $database->Execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]);
+ log_info("core-user", 'Set class for '.$this->name.' to '.$class);
+ }
+
+ public function set_name(string $name): void
+ {
+ global $database;
+ if (User::by_name($name)) {
+ throw new ScoreException("Desired username is already in use");
+ }
+ $old_name = $this->name;
+ $this->name = $name;
+ $database->Execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]);
+ log_info("core-user", "Changed username for {$old_name} to {$this->name}");
+ }
+
+ public function set_password(string $password): void
+ {
+ global $database;
+ $hash = password_hash($password, PASSWORD_BCRYPT);
+ if (is_string($hash)) {
+ $this->passhash = $hash;
+ $database->Execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]);
+ log_info("core-user", 'Set password for '.$this->name);
+ } else {
+ throw new SCoreException("Failed to hash password");
+ }
+ }
+
+ public function set_email(string $address): void
+ {
+ global $database;
+ $database->Execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]);
+ log_info("core-user", 'Set email for '.$this->name);
+ }
+
+ /**
+ * Get a snippet of HTML which will render the user's avatar, be that
+ * a local file, a remote file, a gravatar, a something else, etc.
+ */
+ public function get_avatar_html(): string
+ {
+ // FIXME: configurable
+ global $config;
+ if ($config->get_string("avatar_host") === "gravatar") {
+ if (!empty($this->email)) {
+ $hash = md5(strtolower($this->email));
+ $s = $config->get_string("avatar_gravatar_size");
+ $d = urlencode($config->get_string("avatar_gravatar_default"));
+ $r = $config->get_string("avatar_gravatar_rating");
+ $cb = date("Y-m-d");
+ return "";
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Get an auth token to be used in POST forms
+ *
+ * password = secret, avoid storing directly
+ * passhash = bcrypt(password), so someone who gets to the database can't get passwords
+ * sesskey = md5(passhash . IP), so if it gets sniffed it can't be used from another IP,
+ * and it can't be used to get the passhash to generate new sesskeys
+ * authtok = md5(sesskey, salt), presented to the user in web forms, to make sure that
+ * the form was generated within the session. Salted and re-hashed so that
+ * reading a web page from the user's cache doesn't give access to the session key
+ */
+ public function get_auth_token(): string
+ {
+ global $config;
+ $salt = DATABASE_DSN;
+ $addr = get_session_ip($config);
+ return md5(md5($this->passhash . $addr) . "salty-csrf-" . $salt);
+ }
+
+ public function get_auth_html(): string
+ {
+ $at = $this->get_auth_token();
+ return '';
+ }
+
+ public function check_auth_token(): bool
+ {
+ return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token());
+ }
+
+ public function ensure_authed(): void
+ {
+ if (!$this->check_auth_token()) {
+ die("Invalid auth token");
+ }
+ }
+}
diff --git a/core/userclass.class.php b/core/userclass.class.php
deleted file mode 100644
index 5780b3fe..00000000
--- a/core/userclass.class.php
+++ /dev/null
@@ -1,200 +0,0 @@
-name = $name;
- $this->abilities = $abilities;
-
- if(!is_null($parent)) {
- $this->parent = $_shm_user_classes[$parent];
- }
-
- $_shm_user_classes[$name] = $this;
- }
-
- /**
- * Determine if this class of user can perform an action or has ability.
- *
- * @param string $ability
- * @return bool
- * @throws SCoreException
- */
- public function can(/*string*/ $ability) {
- if(array_key_exists($ability, $this->abilities)) {
- $val = $this->abilities[$ability];
- return $val;
- }
- else if(!is_null($this->parent)) {
- return $this->parent->can($ability);
- }
- else {
- global $_shm_user_classes;
- $min_dist = 9999;
- $min_ability = null;
- foreach($_shm_user_classes['base']->abilities as $a => $cando) {
- $v = levenshtein($ability, $a);
- if($v < $min_dist) {
- $min_dist = $v;
- $min_ability = $a;
- }
- }
- throw new SCoreException("Unknown ability '".html_escape($ability)."'. Did the developer mean '".html_escape($min_ability)."'?");
- }
- }
-}
-
-// action_object_attribute
-// action = create / view / edit / delete
-// object = image / user / tag / setting
-new UserClass("base", null, array(
- "change_setting" => False, # modify web-level settings, eg the config table
- "override_config" => False, # modify sys-level settings, eg shimmie.conf.php
- "big_search" => False, # search for more than 3 tags at once (speed mode only)
-
- "manage_extension_list" => False,
- "manage_alias_list" => False,
- "mass_tag_edit" => False,
-
- "view_ip" => False, # view IP addresses associated with things
- "ban_ip" => False,
-
- "edit_user_name" => False,
- "edit_user_password" => False,
- "edit_user_info" => False, # email address, etc
- "edit_user_class" => False,
- "delete_user" => False,
-
- "create_comment" => False,
- "delete_comment" => False,
- "bypass_comment_checks" => False, # spam etc
-
- "replace_image" => False,
- "create_image" => False,
- "edit_image_tag" => False,
- "edit_image_source" => False,
- "edit_image_owner" => False,
- "edit_image_lock" => False,
- "bulk_edit_image_tag" => False,
- "bulk_edit_image_source" => False,
- "delete_image" => False,
-
- "ban_image" => False,
-
- "view_eventlog" => False,
- "ignore_downtime" => False,
-
- "create_image_report" => False,
- "view_image_report" => False, # deal with reported images
-
- "edit_wiki_page" => False,
- "delete_wiki_page" => False,
-
- "manage_blocks" => False,
-
- "manage_admintools" => False,
-
- "view_other_pms" => False,
- "edit_feature" => False,
- "bulk_edit_vote" => False,
- "edit_other_vote" => False,
- "view_sysinfo" => False,
-
- "hellbanned" => False,
- "view_hellbanned" => False,
-
- "protected" => False, # only admins can modify protected users (stops a moderator changing an admin's password)
-));
-
-new UserClass("anonymous", "base", array(
-));
-
-new UserClass("user", "base", array(
- "big_search" => True,
- "create_image" => True,
- "create_comment" => True,
- "edit_image_tag" => True,
- "edit_image_source" => True,
- "create_image_report" => True,
-));
-
-new UserClass("admin", "base", array(
- "change_setting" => True,
- "override_config" => True,
- "big_search" => True,
- "edit_image_lock" => True,
- "view_ip" => True,
- "ban_ip" => True,
- "edit_user_name" => True,
- "edit_user_password" => True,
- "edit_user_info" => True,
- "edit_user_class" => True,
- "delete_user" => True,
- "create_image" => True,
- "delete_image" => True,
- "ban_image" => True,
- "create_comment" => True,
- "delete_comment" => True,
- "bypass_comment_checks" => True,
- "replace_image" => True,
- "manage_extension_list" => True,
- "manage_alias_list" => True,
- "edit_image_tag" => True,
- "edit_image_source" => True,
- "edit_image_owner" => True,
- "bulk_edit_image_tag" => True,
- "bulk_edit_image_source" => True,
- "mass_tag_edit" => True,
- "create_image_report" => True,
- "view_image_report" => True,
- "edit_wiki_page" => True,
- "delete_wiki_page" => True,
- "view_eventlog" => True,
- "manage_blocks" => True,
- "manage_admintools" => True,
- "ignore_downtime" => True,
- "view_other_pms" => True,
- "edit_feature" => True,
- "bulk_edit_vote" => True,
- "edit_other_vote" => True,
- "view_sysinfo" => True,
- "view_hellbanned" => True,
- "protected" => True,
-));
-
-new UserClass("hellbanned", "user", array(
- "hellbanned" => True,
-));
-
-@include_once "data/config/user-classes.conf.php";
-
diff --git a/core/userclass.php b/core/userclass.php
new file mode 100644
index 00000000..b5409b25
--- /dev/null
+++ b/core/userclass.php
@@ -0,0 +1,263 @@
+name = $name;
+ $this->abilities = $abilities;
+
+ if (!is_null($parent)) {
+ $this->parent = $_shm_user_classes[$parent];
+ }
+
+ $_shm_user_classes[$name] = $this;
+ }
+
+ /**
+ * Determine if this class of user can perform an action or has ability.
+ *
+ * @throws SCoreException
+ */
+ public function can(string $ability): bool
+ {
+ if (array_key_exists($ability, $this->abilities)) {
+ return $this->abilities[$ability];
+ } elseif (!is_null($this->parent)) {
+ return $this->parent->can($ability);
+ } else {
+ global $_shm_user_classes;
+ $min_dist = 9999;
+ $min_ability = null;
+ foreach ($_shm_user_classes['base']->abilities as $a => $cando) {
+ $v = levenshtein($ability, $a);
+ if ($v < $min_dist) {
+ $min_dist = $v;
+ $min_ability = $a;
+ }
+ }
+ throw new SCoreException("Unknown ability '$ability'. Did the developer mean '$min_ability'?");
+ }
+ }
+}
+
+// action_object_attribute
+// action = create / view / edit / delete
+// object = image / user / tag / setting
+new UserClass("base", null, [
+ Permissions::CHANGE_SETTING => false, # modify web-level settings, eg the config table
+ Permissions::OVERRIDE_CONFIG => false, # modify sys-level settings, eg shimmie.conf.php
+ Permissions::BIG_SEARCH => false, # search for more than 3 tags at once (speed mode only)
+
+ Permissions::MANAGE_EXTENSION_LIST => false,
+ Permissions::MANAGE_ALIAS_LIST => false,
+ Permissions::MANAGE_AUTO_TAG => false,
+ Permissions::MASS_TAG_EDIT => false,
+
+ Permissions::VIEW_IP => false, # view IP addresses associated with things
+ Permissions::BAN_IP => false,
+
+ Permissions::CREATE_USER => false,
+ Permissions::EDIT_USER_NAME => false,
+ Permissions::EDIT_USER_PASSWORD => false,
+ Permissions::EDIT_USER_INFO => false, # email address, etc
+ Permissions::EDIT_USER_CLASS => false,
+ Permissions::DELETE_USER => false,
+
+ Permissions::CREATE_COMMENT => false,
+ Permissions::DELETE_COMMENT => false,
+ Permissions::BYPASS_COMMENT_CHECKS => false, # spam etc
+
+ Permissions::REPLACE_IMAGE => false,
+ Permissions::CREATE_IMAGE => false,
+ Permissions::EDIT_IMAGE_TAG => false,
+ Permissions::EDIT_IMAGE_SOURCE => false,
+ Permissions::EDIT_IMAGE_OWNER => false,
+ Permissions::EDIT_IMAGE_LOCK => false,
+ Permissions::EDIT_IMAGE_TITLE => false,
+ Permissions::EDIT_IMAGE_RELATIONSHIPS => false,
+ Permissions::EDIT_IMAGE_ARTIST => false,
+ Permissions::BULK_EDIT_IMAGE_TAG => false,
+ Permissions::BULK_EDIT_IMAGE_SOURCE => false,
+ Permissions::DELETE_IMAGE => false,
+
+ Permissions::BAN_IMAGE => false,
+
+ Permissions::VIEW_EVENTLOG => false,
+ Permissions::IGNORE_DOWNTIME => false,
+
+ Permissions::CREATE_IMAGE_REPORT => false,
+ Permissions::VIEW_IMAGE_REPORT => false, # deal with reported images
+
+ Permissions::WIKI_ADMIN => false,
+ Permissions::EDIT_WIKI_PAGE => false,
+ Permissions::DELETE_WIKI_PAGE => false,
+
+ Permissions::MANAGE_BLOCKS => false,
+
+ Permissions::MANAGE_ADMINTOOLS => false,
+
+ Permissions::SEND_PM => false,
+ Permissions::READ_PM => false,
+ Permissions::VIEW_OTHER_PMS => false,
+ Permissions::EDIT_FEATURE => false,
+ Permissions::BULK_EDIT_VOTE => false,
+ Permissions::EDIT_OTHER_VOTE => false,
+ Permissions::VIEW_SYSINTO => false,
+
+ Permissions::HELLBANNED => false,
+ Permissions::VIEW_HELLBANNED => false,
+
+ Permissions::PROTECTED => false, # only admins can modify protected users (stops a moderator changing an admin's password)
+
+ Permissions::EDIT_IMAGE_RATING => false,
+ Permissions::BULK_EDIT_IMAGE_RATING => false,
+
+ Permissions::VIEW_TRASH => false,
+
+ Permissions::PERFORM_BULK_ACTIONS => false,
+
+ Permissions::BULK_ADD => false,
+ Permissions::EDIT_FILES => false,
+ Permissions::EDIT_TAG_CATEGORIES => false,
+ Permissions::RESCAN_MEDIA => false,
+ Permissions::SEE_IMAGE_VIEW_COUNTS => false,
+
+ Permissions::EDIT_FAVOURITES => false,
+
+ Permissions::ARTISTS_ADMIN => false,
+ Permissions::BLOTTER_ADMIN => false,
+ Permissions::FORUM_ADMIN => false,
+ Permissions::NOTES_ADMIN => false,
+ Permissions::POOLS_ADMIN => false,
+ Permissions::TIPS_ADMIN => false,
+ Permissions::CRON_ADMIN => false,
+
+ Permissions::APPROVE_IMAGE => false,
+ Permissions::APPROVE_COMMENT => false,
+]);
+
+// Ghost users can't do anything
+new UserClass("ghost", "base", [
+]);
+
+// Anonymous users can't do anything by default, but
+// the admin might grant them some permissions
+new UserClass("anonymous", "base", [
+ Permissions::CREATE_USER => true,
+]);
+
+new UserClass("user", "base", [
+ Permissions::BIG_SEARCH => true,
+ Permissions::CREATE_IMAGE => true,
+ Permissions::CREATE_COMMENT => true,
+ Permissions::EDIT_IMAGE_TAG => true,
+ Permissions::EDIT_IMAGE_SOURCE => true,
+ Permissions::EDIT_IMAGE_TITLE => true,
+ Permissions::EDIT_IMAGE_RELATIONSHIPS => true,
+ Permissions::EDIT_IMAGE_ARTIST => true,
+ Permissions::CREATE_IMAGE_REPORT => true,
+ Permissions::EDIT_IMAGE_RATING => true,
+ Permissions::EDIT_FAVOURITES => true,
+ Permissions::SEND_PM => true,
+ Permissions::READ_PM => true,
+]);
+
+new UserClass("admin", "base", [
+ Permissions::CHANGE_SETTING => true,
+ Permissions::OVERRIDE_CONFIG => true,
+ Permissions::BIG_SEARCH => true,
+ Permissions::EDIT_IMAGE_LOCK => true,
+ Permissions::VIEW_IP => true,
+ Permissions::BAN_IP => true,
+ Permissions::EDIT_USER_NAME => true,
+ Permissions::EDIT_USER_PASSWORD => true,
+ Permissions::EDIT_USER_INFO => true,
+ Permissions::EDIT_USER_CLASS => true,
+ Permissions::DELETE_USER => true,
+ Permissions::CREATE_IMAGE => true,
+ Permissions::DELETE_IMAGE => true,
+ Permissions::BAN_IMAGE => true,
+ Permissions::CREATE_COMMENT => true,
+ Permissions::DELETE_COMMENT => true,
+ Permissions::BYPASS_COMMENT_CHECKS => true,
+ Permissions::REPLACE_IMAGE => true,
+ Permissions::MANAGE_EXTENSION_LIST => true,
+ Permissions::MANAGE_ALIAS_LIST => true,
+ Permissions::MANAGE_AUTO_TAG => true,
+ Permissions::EDIT_IMAGE_TAG => true,
+ Permissions::EDIT_IMAGE_SOURCE => true,
+ Permissions::EDIT_IMAGE_OWNER => true,
+ Permissions::EDIT_IMAGE_TITLE => true,
+ Permissions::BULK_EDIT_IMAGE_TAG => true,
+ Permissions::BULK_EDIT_IMAGE_SOURCE => true,
+ Permissions::MASS_TAG_EDIT => true,
+ Permissions::CREATE_IMAGE_REPORT => true,
+ Permissions::VIEW_IMAGE_REPORT => true,
+ Permissions::WIKI_ADMIN => true,
+ Permissions::EDIT_WIKI_PAGE => true,
+ Permissions::DELETE_WIKI_PAGE => true,
+ Permissions::VIEW_EVENTLOG => true,
+ Permissions::MANAGE_BLOCKS => true,
+ Permissions::MANAGE_ADMINTOOLS => true,
+ Permissions::IGNORE_DOWNTIME => true,
+ Permissions::SEND_PM => true,
+ Permissions::READ_PM => true,
+ Permissions::VIEW_OTHER_PMS => true,
+ Permissions::EDIT_FEATURE => true,
+ Permissions::BULK_EDIT_VOTE => true,
+ Permissions::EDIT_OTHER_VOTE => true,
+ Permissions::VIEW_SYSINTO => true,
+ Permissions::VIEW_HELLBANNED => true,
+ Permissions::PROTECTED => true,
+ Permissions::EDIT_IMAGE_RATING => true,
+ Permissions::BULK_EDIT_IMAGE_RATING => true,
+ Permissions::VIEW_TRASH => true,
+ Permissions::PERFORM_BULK_ACTIONS => true,
+ Permissions::BULK_ADD => true,
+ Permissions::EDIT_FILES => true,
+ Permissions::EDIT_TAG_CATEGORIES => true,
+ Permissions::RESCAN_MEDIA => true,
+ Permissions::SEE_IMAGE_VIEW_COUNTS => true,
+ Permissions::ARTISTS_ADMIN => true,
+ Permissions::BLOTTER_ADMIN => true,
+ Permissions::FORUM_ADMIN => true,
+ Permissions::NOTES_ADMIN => true,
+ Permissions::POOLS_ADMIN => true,
+ Permissions::TIPS_ADMIN => true,
+ Permissions::CRON_ADMIN => true,
+ Permissions::APPROVE_IMAGE => true,
+ Permissions::APPROVE_COMMENT => true,
+]);
+
+new UserClass("hellbanned", "user", [
+ Permissions::HELLBANNED => true,
+]);
+
+@include_once "data/config/user-classes.conf.php";
diff --git a/core/util.inc.php b/core/util.inc.php
deleted file mode 100644
index f6a357ec..00000000
--- a/core/util.inc.php
+++ /dev/null
@@ -1,1826 +0,0 @@
-escape($input);
-}
-
-
-/**
- * Turn all manner of HTML / INI / JS / DB booleans into a PHP one
- *
- * @param mixed $input
- * @return boolean
- */
-function bool_escape($input) {
- /*
- Sometimes, I don't like PHP -- this, is one of those times...
- "a boolean FALSE is not considered a valid boolean value by this function."
- Yay for Got'chas!
- http://php.net/manual/en/filter.filters.validate.php
- */
- if (is_bool($input)) {
- return $input;
- } else if (is_numeric($input)) {
- return ($input === 1);
- } else {
- $value = filter_var($input, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
- if (!is_null($value)) {
- return $value;
- } else {
- $input = strtolower( trim($input) );
- return (
- $input === "y" ||
- $input === "yes" ||
- $input === "t" ||
- $input === "true" ||
- $input === "on" ||
- $input === "1"
- );
- }
- }
-}
-
-/**
- * Some functions require a callback function for escaping,
- * but we might not want to alter the data
- *
- * @param string $input
- * @return string
- */
-function no_escape($input) {
- return $input;
-}
-
-/**
- * @param int $val
- * @param int|null $min
- * @param int|null $max
- * @return int
- */
-function clamp($val, $min, $max) {
- if(!is_numeric($val) || (!is_null($min) && $val < $min)) {
- $val = $min;
- }
- if(!is_null($max) && $val > $max) {
- $val = $max;
- }
- if(!is_null($min) && !is_null($max)) {
- assert('$val >= $min && $val <= $max', "$min <= $val <= $max");
- }
- return $val;
-}
-
-/**
- * @param string $name
- * @param array $attrs
- * @param array $children
- * @return string
- */
-function xml_tag($name, $attrs=array(), $children=array()) {
- $xml = "<$name ";
- foreach($attrs as $k => $v) {
- $xv = str_replace(''', ''', htmlspecialchars($v, ENT_QUOTES));
- $xml .= "$k=\"$xv\" ";
- }
- if(count($children) > 0) {
- $xml .= ">\n";
- foreach($children as $child) {
- $xml .= xml_tag($child);
- }
- $xml .= "$name>\n";
- }
- else {
- $xml .= "/>\n";
- }
- return $xml;
-}
-
-/**
- * Original PHP code by Chirp Internet: www.chirp.com.au
- * Please acknowledge use of this code by including this header.
- *
- * @param string $string input data
- * @param int $limit how long the string should be
- * @param string $break where to break the string
- * @param string $pad what to add to the end of the string after truncating
- */
-function truncate($string, $limit, $break=" ", $pad="...") {
- // return with no change if string is shorter than $limit
- if(strlen($string) <= $limit) return $string;
-
- // is $break present between $limit and the end of the string?
- if(false !== ($breakpoint = strpos($string, $break, $limit))) {
- if($breakpoint < strlen($string) - 1) {
- $string = substr($string, 0, $breakpoint) . $pad;
- }
- }
-
- return $string;
-}
-
-/**
- * Turn a human readable filesize into an integer, eg 1KB -> 1024
- *
- * @param string|integer $limit
- * @return int
- */
-function parse_shorthand_int($limit) {
- if(is_numeric($limit)) {
- return (int)$limit;
- }
-
- if(preg_match('/^([\d\.]+)([gmk])?b?$/i', (string)$limit, $m)) {
- $value = $m[1];
- if (isset($m[2])) {
- switch(strtolower($m[2])) {
- /** @noinspection PhpMissingBreakStatementInspection */
- case 'g': $value *= 1024; // fall through
- /** @noinspection PhpMissingBreakStatementInspection */
- case 'm': $value *= 1024; // fall through
- /** @noinspection PhpMissingBreakStatementInspection */
- case 'k': $value *= 1024; break;
- default: $value = -1;
- }
- }
- return (int)$value;
- } else {
- return -1;
- }
-}
-
-/**
- * Turn an integer into a human readable filesize, eg 1024 -> 1KB
- *
- * @param integer $int
- * @return string
- */
-function to_shorthand_int($int) {
- if($int >= pow(1024, 3)) {
- return sprintf("%.1fGB", $int / pow(1024, 3));
- }
- else if($int >= pow(1024, 2)) {
- return sprintf("%.1fMB", $int / pow(1024, 2));
- }
- else if($int >= 1024) {
- return sprintf("%.1fKB", $int / 1024);
- }
- else {
- return (string)$int;
- }
-}
-
-
-/**
- * Turn a date into a time, a date, an "X minutes ago...", etc
- *
- * @param string $date
- * @param bool $html
- * @return string
- */
-function autodate($date, $html=true) {
- $cpu = date('c', strtotime($date));
- $hum = date('F j, Y; H:i', strtotime($date));
- return ($html ? "" : $hum);
-}
-
-/**
- * Check if a given string is a valid date-time. ( Format: yyyy-mm-dd hh:mm:ss )
- *
- * @param string $dateTime
- * @return bool
- */
-function isValidDateTime($dateTime) {
- if (preg_match("/^(\d{4})-(\d{2})-(\d{2}) ([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/", $dateTime, $matches)) {
- if (checkdate($matches[2], $matches[3], $matches[1])) {
- return true;
- }
- }
-
- return false;
-}
-
-/**
- * Check if a given string is a valid date. ( Format: yyyy-mm-dd )
- *
- * @param string $date
- * @return bool
- */
-function isValidDate($date) {
- if (preg_match("/^(\d{4})-(\d{2})-(\d{2})$/", $date, $matches)) {
- // checkdate wants (month, day, year)
- if (checkdate($matches[2], $matches[3], $matches[1])) {
- return true;
- }
- }
-
- return false;
-}
-
-/**
- * @param string[] $inputs
- */
-function validate_input($inputs) {
- $outputs = array();
-
- foreach($inputs as $key => $validations) {
- $flags = explode(',', $validations);
-
- if(in_array('bool', $flags) && !isset($_POST[$key])) {
- $_POST[$key] = 'off';
- }
-
- if(in_array('optional', $flags)) {
- if(!isset($_POST[$key]) || trim($_POST[$key]) == "") {
- $outputs[$key] = null;
- continue;
- }
- }
- if(!isset($_POST[$key]) || trim($_POST[$key]) == "") {
- throw new InvalidInput("Input '$key' not set");
- }
-
- $value = trim($_POST[$key]);
-
- if(in_array('user_id', $flags)) {
- $id = int_escape($value);
- if(in_array('exists', $flags)) {
- if(is_null(User::by_id($id))) {
- throw new InvalidInput("User #$id does not exist");
- }
- }
- $outputs[$key] = $id;
- }
- else if(in_array('user_name', $flags)) {
- if(strlen($value) < 1) {
- throw new InvalidInput("Username must be at least 1 character");
- }
- else if(!preg_match('/^[a-zA-Z0-9-_]+$/', $value)) {
- throw new InvalidInput(
- "Username contains invalid characters. Allowed characters are ".
- "letters, numbers, dash, and underscore");
- }
- $outputs[$key] = $value;
- }
- else if(in_array('user_class', $flags)) {
- global $_shm_user_classes;
- if(!array_key_exists($value, $_shm_user_classes)) {
- throw new InvalidInput("Invalid user class: ".html_escape($value));
- }
- $outputs[$key] = $value;
- }
- else if(in_array('email', $flags)) {
- $outputs[$key] = trim($value);
- }
- else if(in_array('password', $flags)) {
- $outputs[$key] = $value;
- }
- else if(in_array('int', $flags)) {
- $value = trim($value);
- if(empty($value) || !is_numeric($value)) {
- throw new InvalidInput("Invalid int: ".html_escape($value));
- }
- $outputs[$key] = (int)$value;
- }
- else if(in_array('bool', $flags)) {
- $outputs[$key] = bool_escape($value);
- }
- else if(in_array('string', $flags)) {
- if(in_array('trim', $flags)) {
- $value = trim($value);
- }
- if(in_array('lower', $flags)) {
- $value = strtolower($value);
- }
- if(in_array('not-empty', $flags)) {
- throw new InvalidInput("$key must not be blank");
- }
- if(in_array('nullify', $flags)) {
- if(empty($value)) $value = null;
- }
- $outputs[$key] = $value;
- }
- else {
- throw new InvalidInput("Unknown validation '$validations'");
- }
- }
-
- return $outputs;
-}
-
-/**
- * Give a HTML string which shows an IP (if the user is allowed to see IPs),
- * and a link to ban that IP (if the user is allowed to ban IPs)
- *
- * FIXME: also check that IP ban ext is installed
- *
- * @param string $ip
- * @param string $ban_reason
- * @return string
- */
-function show_ip($ip, $ban_reason) {
- global $user;
- $u_reason = url_escape($ban_reason);
- $u_end = url_escape("+1 week");
- $ban = $user->can("ban_ip") ? ", Ban" : "";
- $ip = $user->can("view_ip") ? $ip.$ban : "";
- return $ip;
-}
-
-/**
- * Checks if a given string contains another at the beginning.
- *
- * @param string $haystack String to examine.
- * @param string $needle String to look for.
- * @return bool
- */
-function startsWith(/*string*/ $haystack, /*string*/ $needle) {
- $length = strlen($needle);
- return (substr($haystack, 0, $length) === $needle);
-}
-
-/**
- * Checks if a given string contains another at the end.
- *
- * @param string $haystack String to examine.
- * @param string $needle String to look for.
- * @return bool
- */
-function endsWith(/*string*/ $haystack, /*string*/ $needle) {
- $length = strlen($needle);
- $start = $length * -1; //negative
- return (substr($haystack, $start) === $needle);
-}
-
-/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
-* HTML Generation *
-\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
-/**
- * Figure out the correct way to link to a page, taking into account
- * things like the nice URLs setting.
- *
- * eg make_link("post/list") becomes "/v2/index.php?q=post/list"
- *
- * @param null|string $page
- * @param null|string $query
- * @return string
- */
-function make_link($page=null, $query=null) {
- global $config;
-
- if(is_null($page)) $page = $config->get_string('main_page');
-
- if(!is_null(BASE_URL)) {
- $base = BASE_URL;
- }
- elseif(NICE_URLS || $config->get_bool('nice_urls', false)) {
- $base = str_replace('/'.basename($_SERVER["SCRIPT_FILENAME"]), "", $_SERVER["PHP_SELF"]);
- }
- else {
- $base = "./".basename($_SERVER["SCRIPT_FILENAME"])."?q=";
- }
-
- if(is_null($query)) {
- return str_replace("//", "/", $base.'/'.$page );
- }
- else {
- if(strpos($base, "?")) {
- return $base .'/'. $page .'&'. $query;
- }
- else if(strpos($query, "#") === 0) {
- return $base .'/'. $page . $query;
- }
- else {
- return $base .'/'. $page .'?'. $query;
- }
- }
-}
-
-
-/**
- * Take the current URL and modify some parameters
- *
- * @param $changes
- * @return string
- */
-function modify_current_url($changes) {
- return modify_url($_SERVER['QUERY_STRING'], $changes);
-}
-
-function modify_url($url, $changes) {
- // SHIT: PHP is officially the worst web API ever because it does not
- // have a built-in function to do this.
-
- // SHIT: parse_str is magically retarded; not only is it a useless name, it also
- // didn't return the parsed array, preferring to overwrite global variables with
- // whatever data the user supplied. Thankfully, 4.0.3 added an extra option to
- // give it an array to use...
- $params = array();
- parse_str($url, $params);
-
- if(isset($changes['q'])) {
- $base = $changes['q'];
- unset($changes['q']);
- }
- else {
- $base = _get_query();
- }
-
- if(isset($params['q'])) {
- unset($params['q']);
- }
-
- foreach($changes as $k => $v) {
- if(is_null($v) and isset($params[$k])) unset($params[$k]);
- $params[$k] = $v;
- }
-
- return make_link($base, http_build_query($params));
-}
-
-
-/**
- * Turn a relative link into an absolute one, including hostname
- *
- * @param string $link
- * @return string
- */
-function make_http(/*string*/ $link) {
- if(strpos($link, "://") > 0) {
- return $link;
- }
-
- if(strlen($link) > 0 && $link[0] != '/') {
- $link = get_base_href() . '/' . $link;
- }
-
- $protocol = is_https_enabled() ? "https://" : "http://";
- $link = $protocol . $_SERVER["HTTP_HOST"] . $link;
- $link = str_replace("/./", "/", $link);
-
- return $link;
-}
-
-/**
- * Make a form tag with relevant auth token and stuff
- *
- * @param string $target
- * @param string $method
- * @param bool $multipart
- * @param string $form_id
- * @param string $onsubmit
- *
- * @return string
- */
-function make_form($target, $method="POST", $multipart=False, $form_id="", $onsubmit="") {
- global $user;
- if($method == "GET") {
- $link = html_escape($target);
- $target = make_link($target);
- $extra_inputs = "";
- }
- else {
- $extra_inputs = $user->get_auth_html();
- }
-
- $extra = empty($form_id) ? '' : 'id="'. $form_id .'"';
- if($multipart) {
- $extra .= " enctype='multipart/form-data'";
- }
- if($onsubmit) {
- $extra .= ' onsubmit="'.$onsubmit.'"';
- }
- return '\n";
+ return $html;
+ }
- /*
- * Show a form which links to admin_utils with POST[action] set to one of:
- * 'lowercase all tags'
- * 'recount tag use'
- * etc
- */
- public function display_form() {
- global $page, $database;
+ /*
+ * Show a form which links to admin_utils with POST[action] set to one of:
+ * 'lowercase all tags'
+ * 'recount tag use'
+ * etc
+ */
+ public function display_form()
+ {
+ global $page;
- $html = "";
- $html .= $this->button("All tags to lowercase", "lowercase_all_tags", true);
- $html .= $this->button("Recount tag use", "recount_tag_use", false);
- if(class_exists('ZipArchive'))
- $html .= $this->button("Download all images", "download_all_images", false);
- $html .= $this->button("Download database contents", "database_dump", false);
- if($database->get_driver_name() == "mysql")
- $html .= $this->button("Reset image IDs", "reset_image_ids", true);
- $page->add_block(new Block("Misc Admin Tools", $html));
+ $html = "";
+ $html .= $this->button("All tags to lowercase", "lowercase_all_tags", true);
+ $html .= $this->button("Recount tag use", "recount_tag_use", false);
+ $page->add_block(new Block("Misc Admin Tools", $html));
- $html = make_form(make_link("admin/set_tag_case"), "POST");
- $html .= "";
- $html .= "";
- $html .= "\n";
- $page->add_block(new Block("Set Tag Case", $html));
- }
-
- public function dbq_html($terms) {
- $h_terms = html_escape($terms);
- $h_reason = "";
- if(class_exists("ImageBan")) {
- $h_reason = "";
- }
- $html = make_form(make_link("admin/delete_by_query"), "POST") . "
-
-
- $h_reason
-
-
- ";
- return $html;
- }
+ $html = (string)SHM_SIMPLE_FORM(
+ "admin/set_tag_case",
+ INPUT(["type"=>'text', "name"=>'tag', "placeholder"=>'Enter tag with correct case', "class"=>'autocomplete_tags', "autocomplete"=>'off']),
+ SHM_SUBMIT('Set Tag Case'),
+ );
+ $page->add_block(new Block("Set Tag Case", $html));
+ }
}
-
diff --git a/ext/alias_editor/info.php b/ext/alias_editor/info.php
new file mode 100644
index 00000000..1df8f477
--- /dev/null
+++ b/ext/alias_editor/info.php
@@ -0,0 +1,15 @@
+/alias/list; only site admins can edit it, other people can view and download it';
+ public $core = true;
+}
diff --git a/ext/alias_editor/main.php b/ext/alias_editor/main.php
index d6923693..e8a3a678 100644
--- a/ext/alias_editor/main.php
+++ b/ext/alias_editor/main.php
@@ -1,178 +1,209 @@
-
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: Edit the alias list
- * Documentation:
- * The list is visible at /alias/list; only
- * site admins can edit it, other people can view and download it
- */
+oldtag = trim($oldtag);
- $this->newtag = trim($newtag);
- }
+class AliasTable extends Table
+{
+ public function __construct(\FFSPHP\PDO $db)
+ {
+ parent::__construct($db);
+ $this->table = "aliases";
+ $this->base_query = "SELECT * FROM aliases";
+ $this->primary_key = "oldtag";
+ $this->size = 100;
+ $this->limit = 1000000;
+ $this->set_columns([
+ new TextColumn("oldtag", "Old Tag"),
+ new TextColumn("newtag", "New Tag"),
+ new ActionColumn("oldtag"),
+ ]);
+ $this->order_by = ["oldtag"];
+ $this->table_attrs = ["class" => "zebra"];
+ }
}
-class AddAliasException extends SCoreException {}
+class AddAliasEvent extends Event
+{
+ /** @var string */
+ public $oldtag;
+ /** @var string */
+ public $newtag;
-class AliasEditor extends Extension {
- public function onPageRequest(PageRequestEvent $event) {
- global $config, $database, $page, $user;
-
- if($event->page_matches("alias")) {
- if($event->get_arg(0) == "add") {
- if($user->can("manage_alias_list")) {
- if(isset($_POST['oldtag']) && isset($_POST['newtag'])) {
- try {
- $aae = new AddAliasEvent($_POST['oldtag'], $_POST['newtag']);
- send_event($aae);
- $page->set_mode("redirect");
- $page->set_redirect(make_link("alias/list"));
- }
- catch(AddAliasException $ex) {
- $this->theme->display_error(500, "Error adding alias", $ex->getMessage());
- }
- }
- }
- }
- else if($event->get_arg(0) == "remove") {
- if($user->can("manage_alias_list")) {
- if(isset($_POST['oldtag'])) {
- $database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", array("oldtag" => $_POST['oldtag']));
- log_info("alias_editor", "Deleted alias for ".$_POST['oldtag'], true);
-
- $page->set_mode("redirect");
- $page->set_redirect(make_link("alias/list"));
- }
- }
- }
- else if($event->get_arg(0) == "list") {
- $page_number = $event->get_arg(1);
- if(is_null($page_number) || !is_numeric($page_number)) {
- $page_number = 0;
- }
- else if ($page_number <= 0) {
- $page_number = 0;
- }
- else {
- $page_number--;
- }
-
- $alias_per_page = $config->get_int('alias_items_per_page', 30);
-
- $query = "SELECT oldtag, newtag FROM aliases ORDER BY newtag ASC LIMIT :limit OFFSET :offset";
- $alias = $database->get_pairs($query,
- array("limit"=>$alias_per_page, "offset"=>$page_number * $alias_per_page)
- );
-
- $total_pages = ceil($database->get_one("SELECT COUNT(*) FROM aliases") / $alias_per_page);
-
- $this->theme->display_aliases($alias, $page_number + 1, $total_pages);
- }
- else if($event->get_arg(0) == "export") {
- $page->set_mode("data");
- $page->set_type("text/csv");
- $page->set_filename("aliases.csv");
- $page->set_data($this->get_alias_csv($database));
- }
- else if($event->get_arg(0) == "import") {
- if($user->can("manage_alias_list")) {
- if(count($_FILES) > 0) {
- $tmp = $_FILES['alias_file']['tmp_name'];
- $contents = file_get_contents($tmp);
- $this->add_alias_csv($database, $contents);
- log_info("alias_editor", "Imported aliases from file", true); # FIXME: how many?
- $page->set_mode("redirect");
- $page->set_redirect(make_link("alias/list"));
- }
- else {
- $this->theme->display_error(400, "No File Specified", "You have to upload a file");
- }
- }
- else {
- $this->theme->display_error(401, "Admins Only", "Only admins can edit the alias list");
- }
- }
- }
- }
-
- public function onAddAlias(AddAliasEvent $event) {
- global $database;
- $pair = array("oldtag" => $event->oldtag, "newtag" => $event->newtag);
- if($database->get_row("SELECT * FROM aliases WHERE oldtag=:oldtag AND lower(newtag)=lower(:newtag)", $pair)) {
- throw new AddAliasException("That alias already exists");
- }
- else if($database->get_row("SELECT * FROM aliases WHERE oldtag=:newtag", array("newtag" => $event->newtag))) {
- throw new AddAliasException("{$event->newtag} is itself an alias");
- }
- else {
- $database->execute("INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)", $pair);
- log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", true);
- }
- }
-
- public function onUserBlockBuilding(UserBlockBuildingEvent $event) {
- global $user;
- if($user->can("manage_alias_list")) {
- $event->add_link("Alias Editor", make_link("alias/list"));
- }
- }
-
- /**
- * @param Database $database
- * @return string
- */
- private function get_alias_csv(Database $database) {
- $csv = "";
- $aliases = $database->get_pairs("SELECT oldtag, newtag FROM aliases ORDER BY newtag");
- foreach($aliases as $old => $new) {
- $csv .= "\"$old\",\"$new\"\n";
- }
- return $csv;
- }
-
- /**
- * @param Database $database
- * @param string $csv
- */
- private function add_alias_csv(Database $database, /*string*/ $csv) {
- $csv = str_replace("\r", "\n", $csv);
- foreach(explode("\n", $csv) as $line) {
- $parts = str_getcsv($line);
- if(count($parts) == 2) {
- try {
- $aae = new AddAliasEvent($parts[0], $parts[1]);
- send_event($aae);
- }
- catch(AddAliasException $ex) {
- $this->theme->display_error(500, "Error adding alias", $ex->getMessage());
- }
- }
- }
- }
-
- /**
- * Get the priority for this extension.
- *
- * Add alias *after* mass tag editing, else the MTE will
- * search for the images and be redirected to the alias,
- * missing out the images tagged with the old tag.
- *
- * @return int
- */
- public function get_priority() {return 60;}
+ public function __construct(string $oldtag, string $newtag)
+ {
+ parent::__construct();
+ $this->oldtag = trim($oldtag);
+ $this->newtag = trim($newtag);
+ }
}
+class DeleteAliasEvent extends Event
+{
+ public $oldtag;
+
+ public function __construct(string $oldtag)
+ {
+ parent::__construct();
+ $this->oldtag = $oldtag;
+ }
+}
+
+class AddAliasException extends SCoreException
+{
+}
+
+class AliasEditor extends Extension
+{
+ /** @var AliasEditorTheme */
+ protected $theme;
+
+ public function onPageRequest(PageRequestEvent $event)
+ {
+ global $config, $database, $page, $user;
+
+ if ($event->page_matches("alias")) {
+ if ($event->get_arg(0) == "add") {
+ if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
+ $user->ensure_authed();
+ $input = validate_input(["c_oldtag"=>"string", "c_newtag"=>"string"]);
+ try {
+ send_event(new AddAliasEvent($input['c_oldtag'], $input['c_newtag']));
+ $page->set_mode(PageMode::REDIRECT);
+ $page->set_redirect(make_link("alias/list"));
+ } catch (AddAliasException $ex) {
+ $this->theme->display_error(500, "Error adding alias", $ex->getMessage());
+ }
+ }
+ } elseif ($event->get_arg(0) == "remove") {
+ if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
+ $user->ensure_authed();
+ $input = validate_input(["d_oldtag"=>"string"]);
+ send_event(new DeleteAliasEvent($input['d_oldtag']));
+ $page->set_mode(PageMode::REDIRECT);
+ $page->set_redirect(make_link("alias/list"));
+ }
+ } elseif ($event->get_arg(0) == "list") {
+ $t = new AliasTable($database->raw_db());
+ $t->token = $user->get_auth_token();
+ $t->inputs = $_GET;
+ $t->size = $config->get_int('alias_items_per_page', 30);
+ if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
+ $t->create_url = make_link("alias/add");
+ $t->delete_url = make_link("alias/remove");
+ }
+ $this->theme->display_aliases($t->table($t->query()), $t->paginator());
+ } elseif ($event->get_arg(0) == "export") {
+ $page->set_mode(PageMode::DATA);
+ $page->set_type("text/csv");
+ $page->set_filename("aliases.csv");
+ $page->set_data($this->get_alias_csv($database));
+ } elseif ($event->get_arg(0) == "import") {
+ if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
+ if (count($_FILES) > 0) {
+ $tmp = $_FILES['alias_file']['tmp_name'];
+ $contents = file_get_contents($tmp);
+ $this->add_alias_csv($database, $contents);
+ log_info("alias_editor", "Imported aliases from file", "Imported aliases"); # FIXME: how many?
+ $page->set_mode(PageMode::REDIRECT);
+ $page->set_redirect(make_link("alias/list"));
+ } else {
+ $this->theme->display_error(400, "No File Specified", "You have to upload a file");
+ }
+ } else {
+ $this->theme->display_error(401, "Admins Only", "Only admins can edit the alias list");
+ }
+ }
+ }
+ }
+
+ public function onAddAlias(AddAliasEvent $event)
+ {
+ global $database;
+
+ $row = $database->get_row(
+ "SELECT * FROM aliases WHERE lower(oldtag)=lower(:oldtag)",
+ ["oldtag"=>$event->oldtag]
+ );
+ if ($row) {
+ throw new AddAliasException("{$row['oldtag']} is already an alias for {$row['newtag']}");
+ }
+
+ $row = $database->get_row(
+ "SELECT * FROM aliases WHERE lower(oldtag)=lower(:newtag)",
+ ["newtag" => $event->newtag]
+ );
+ if ($row) {
+ throw new AddAliasException("{$row['oldtag']} is itself an alias for {$row['newtag']}");
+ }
+
+ $database->execute(
+ "INSERT INTO aliases(oldtag, newtag) VALUES(:oldtag, :newtag)",
+ ["oldtag" => $event->oldtag, "newtag" => $event->newtag]
+ );
+ log_info("alias_editor", "Added alias for {$event->oldtag} -> {$event->newtag}", "Added alias");
+ }
+
+ public function onDeleteAlias(DeleteAliasEvent $event)
+ {
+ global $database;
+ $database->execute("DELETE FROM aliases WHERE oldtag=:oldtag", ["oldtag" => $event->oldtag]);
+ log_info("alias_editor", "Deleted alias for {$event->oldtag}", "Deleted alias");
+ }
+
+ public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+ {
+ if ($event->parent=="tags") {
+ $event->add_nav_link("aliases", new Link('alias/list'), "Aliases", NavLink::is_active(["alias"]));
+ }
+ }
+
+ public function onUserBlockBuilding(UserBlockBuildingEvent $event)
+ {
+ global $user;
+ if ($user->can(Permissions::MANAGE_ALIAS_LIST)) {
+ $event->add_link("Alias Editor", make_link("alias/list"));
+ }
+ }
+
+ private function get_alias_csv(Database $database): string
+ {
+ $csv = "";
+ $aliases = $database->get_pairs("SELECT oldtag, newtag FROM aliases ORDER BY newtag");
+ foreach ($aliases as $old => $new) {
+ $csv .= "\"$old\",\"$new\"\n";
+ }
+ return $csv;
+ }
+
+ private function add_alias_csv(Database $database, string $csv): int
+ {
+ $csv = str_replace("\r", "\n", $csv);
+ $i = 0;
+ foreach (explode("\n", $csv) as $line) {
+ $parts = str_getcsv($line);
+ if (count($parts) == 2) {
+ try {
+ send_event(new AddAliasEvent($parts[0], $parts[1]));
+ $i++;
+ } catch (AddAliasException $ex) {
+ $this->theme->display_error(500, "Error adding alias", $ex->getMessage());
+ }
+ }
+ }
+ return $i;
+ }
+
+ /**
+ * Get the priority for this extension.
+ *
+ * Add alias *after* mass tag editing, else the MTE will
+ * search for the images and be redirected to the alias,
+ * missing out the images tagged with the old tag.
+ */
+ public function get_priority(): int
+ {
+ return 60;
+ }
+}
diff --git a/ext/alias_editor/test.php b/ext/alias_editor/test.php
index 0b8e4512..9829cec3 100644
--- a/ext/alias_editor/test.php
+++ b/ext/alias_editor/test.php
@@ -1,104 +1,85 @@
-get_page('alias/list');
- $this->assert_response(200);
- $this->assert_title("Alias List");
- }
+get_page('alias/list');
+ $this->assert_response(200);
+ $this->assert_title("Alias List");
+ }
- public function testAliasListReadOnly() {
- // Check that normal users can't add aliases.
- $this->log_in_as_user();
- $this->get_page('alias/list');
- $this->assert_title("Alias List");
- $this->assert_no_text("Add");
- }
+ public function testAliasListReadOnly()
+ {
+ $this->log_in_as_user();
+ $this->get_page('alias/list');
+ $this->assert_title("Alias List");
+ $this->assert_no_text("Add");
- public function testAliasEditor() {
- /*
- **********************************************************************
- * FIXME: TODO:
- * For some reason the alias tests always fail when they are running
- * inside the TravisCI VM environment. I have tried to determine
- * the exact cause of this, but have been unable to pin it down.
- *
- * For now, I am commenting them out until I have more time to
- * dig into this and determine exactly what is happening.
- *
- *********************************************************************
- */
- $this->markTestIncomplete();
+ $this->log_out();
+ $this->get_page('alias/list');
+ $this->assert_title("Alias List");
+ $this->assert_no_text("Add");
+ }
- $this->log_in_as_admin();
+ public function testAliasOneToOne()
+ {
+ $this->log_in_as_admin();
- # test one to one
- $this->get_page('alias/list');
- $this->assert_title("Alias List");
- $this->set_field('oldtag', "test1");
- $this->set_field('newtag', "test2");
- $this->clickSubmit('Add');
- $this->assert_no_text("Error adding alias");
+ $this->get_page("alias/export/aliases.csv");
+ $this->assert_no_text("test1");
- $this->get_page('alias/list');
- $this->assert_text("test1");
+ send_event(new AddAliasEvent("test1", "test2"));
+ $this->get_page('alias/list');
+ $this->assert_text("test1");
+ $this->get_page("alias/export/aliases.csv");
+ $this->assert_text('"test1","test2"');
- $this->get_page("alias/export/aliases.csv");
- $this->assert_text("test1,test2");
+ $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
+ $this->get_page("post/view/$image_id"); # check that the tag has been replaced
+ $this->assert_title("Image $image_id: test2");
+ $this->get_page("post/list/test1/1"); # searching for an alias should find the master tag
+ $this->assert_response(302);
+ $this->get_page("post/list/test2/1"); # check that searching for the main tag still works
+ $this->assert_response(302);
+ $this->delete_image($image_id);
- $image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
- $this->get_page("post/view/$image_id"); # check that the tag has been replaced
- $this->assert_title("Image $image_id: test2");
- $this->get_page("post/list/test1/1"); # searching for an alias should find the master tag
- $this->assert_title("Image $image_id: test2");
- $this->get_page("post/list/test2/1"); # check that searching for the main tag still works
- $this->assert_title("Image $image_id: test2");
- $this->delete_image($image_id);
+ send_event(new DeleteAliasEvent("test1"));
+ $this->get_page('alias/list');
+ $this->assert_title("Alias List");
+ $this->assert_no_text("test1");
+ }
- $this->get_page('alias/list');
- $this->click("Remove");
- $this->get_page('alias/list');
- $this->assert_title("Alias List");
- $this->assert_no_text("test1");
+ public function testAliasOneToMany()
+ {
+ $this->log_in_as_admin();
- # test one to many
- $this->get_page('alias/list');
- $this->assert_title("Alias List");
- $this->set_field('oldtag', "onetag");
- $this->set_field('newtag', "multi tag");
- $this->click("Add");
- $this->get_page('alias/list');
- $this->assert_text("multi");
- $this->assert_text("tag");
+ $this->get_page("alias/export/aliases.csv");
+ $this->assert_no_text("multi");
- $this->get_page("alias/export/aliases.csv");
- $this->assert_text("onetag,multi tag");
+ send_event(new AddAliasEvent("onetag", "multi tag"));
+ $this->get_page('alias/list');
+ $this->assert_text("multi");
+ $this->assert_text("tag");
+ $this->get_page("alias/export/aliases.csv");
+ $this->assert_text('"onetag","multi tag"');
- $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "onetag");
- $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "onetag");
- // FIXME: known broken
- //$this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases
- //$this->assert_title("onetag");
- //$this->assert_no_text("No Images Found");
- $this->get_page("post/list/multi/1");
- $this->assert_title("multi");
- $this->assert_no_text("No Images Found");
- $this->get_page("post/list/multi%20tag/1");
- $this->assert_title("multi tag");
- $this->assert_no_text("No Images Found");
- $this->delete_image($image_id_1);
- $this->delete_image($image_id_2);
+ $image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "onetag");
+ $image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "onetag");
+ $this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases
+ $this->assert_title("multi tag");
+ $this->assert_no_text("No Images Found");
+ $this->get_page("post/list/multi/1");
+ $this->assert_title("multi");
+ $this->assert_no_text("No Images Found");
+ $this->get_page("post/list/multi tag/1");
+ $this->assert_title("multi tag");
+ $this->assert_no_text("No Images Found");
+ $this->delete_image($image_id_1);
+ $this->delete_image($image_id_2);
- $this->get_page('alias/list');
- $this->click("Remove");
- $this->get_page('alias/list');
- $this->assert_title("Alias List");
- $this->assert_no_text("test1");
-
- $this->log_out();
-
- $this->get_page('alias/list');
- $this->assert_title("Alias List");
- $this->assert_no_text("Add");
- }
+ send_event(new DeleteAliasEvent("onetag"));
+ $this->get_page('alias/list');
+ $this->assert_title("Alias List");
+ $this->assert_no_text("test1");
+ }
}
-
diff --git a/ext/alias_editor/theme.php b/ext/alias_editor/theme.php
index 02b7a3a1..61568d29 100644
--- a/ext/alias_editor/theme.php
+++ b/ext/alias_editor/theme.php
@@ -1,79 +1,36 @@
- $new_tag)
- * @param int $pageNumber
- * @param int $totalPages
- */
- public function display_aliases($aliases, $pageNumber, $totalPages) {
- global $page, $user;
+class AliasEditorTheme extends Themelet
+{
+ /**
+ * Show a page of aliases.
+ *
+ * Note: $can_manage = whether things like "add new alias" should be shown
+ */
+ public function display_aliases($table, $paginator): void
+ {
+ global $page, $user;
- $can_manage = $user->can("manage_alias_list");
- if($can_manage) {
- $h_action = "
";
- //TODO how will notes be edited? On edit artist? (should there be an editartist?) or on a editnotes?
- //same question for deletion
- $html .= "
+ if ($userIsLogged) {
+ $html .= "
";
+ }
+ if ($userIsAdmin) {
+ $html .= "
";
+ }
+ //TODO how will notes be edited? On edit artist? (should there be an editartist?) or on a editnotes?
+ //same question for deletion
+ $html .= "
Note that for non-regex matches, only whole words are
+matched, eg banning \"sex\" would block the comment \"get free
+sex call this number\", but allow \"This is a photo of Bob
+from Essex\"";
+}
diff --git a/ext/ban_words/main.php b/ext/ban_words/main.php
index 9d9493d4..44a9f48e 100644
--- a/ext/ban_words/main.php
+++ b/ext/ban_words/main.php
@@ -1,29 +1,11 @@
-
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: For stopping spam and other comment abuse
- * Documentation:
- * Allows an administrator to ban certain words
- * from comments. This can be a very simple but effective way
- * of stopping spam; just add "viagra", "porn", etc to the
- * banned words list.
- *
Regex bans are also supported, allowing more complicated
- * bans like /http:.*\.cn\// to block links to
- * chinese websites, or /.*?http.*?http.*?http.*?http.*?/
- * to block comments with four (or more) links in.
- *
Note that for non-regex matches, only whole words are
- * matched, eg banning "sex" would block the comment "get free
- * sex call this number", but allow "This is a photo of Bob
- * from Essex"
- */
+set_default_string('banned_words', "
+class BanWords extends Extension
+{
+ public function onInitExt(InitExtEvent $event)
+ {
+ global $config;
+ $config->set_default_string('banned_words', "
a href=
anal
blowjob
@@ -51,86 +33,87 @@ very nice site
viagra
xanax
");
- }
+ }
- public function onCommentPosting(CommentPostingEvent $event) {
- global $user;
- if(!$user->can("bypass_comment_checks")) {
- $this->test_text($event->comment, new CommentPostingException("Comment contains banned terms"));
- }
- }
+ public function onCommentPosting(CommentPostingEvent $event)
+ {
+ global $user;
+ if (!$user->can(Permissions::BYPASS_COMMENT_CHECKS)) {
+ $this->test_text($event->comment, new CommentPostingException("Comment contains banned terms"));
+ }
+ }
- public function onSourceSet(SourceSetEvent $event) {
- $this->test_text($event->source, new SCoreException("Source contains banned terms"));
- }
+ public function onSourceSet(SourceSetEvent $event)
+ {
+ $this->test_text($event->source, new SCoreException("Source contains banned terms"));
+ }
- public function onTagSet(TagSetEvent $event) {
- $this->test_text(Tag::implode($event->tags), new SCoreException("Tags contain banned terms"));
- }
+ public function onTagSet(TagSetEvent $event)
+ {
+ $this->test_text(Tag::implode($event->tags), new SCoreException("Tags contain banned terms"));
+ }
- public function onSetupBuilding(SetupBuildingEvent $event) {
- $sb = new SetupBlock("Banned Phrases");
- $sb->add_label("One per line, lines that start with slashes are treated as regex ");
- $sb->add_longtext_option("banned_words");
- $failed = array();
- foreach($this->get_words() as $word) {
- if($word[0] == '/') {
- if(preg_match($word, "") === false) {
- $failed[] = $word;
- }
- }
- }
- if($failed) {
- $sb->add_label("Failed regexes: ".join(", ", $failed));
- }
- $event->panel->add_block($sb);
- }
+ public function onSetupBuilding(SetupBuildingEvent $event)
+ {
+ $sb = new SetupBlock("Banned Phrases");
+ $sb->add_label("One per line, lines that start with slashes are treated as regex ");
+ $sb->add_longtext_option("banned_words");
+ $failed = [];
+ foreach ($this->get_words() as $word) {
+ if ($word[0] == '/') {
+ if (preg_match($word, "") === false) {
+ $failed[] = $word;
+ }
+ }
+ }
+ if ($failed) {
+ $sb->add_label("Failed regexes: ".join(", ", $failed));
+ }
+ $event->panel->add_block($sb);
+ }
- /**
- * Throws if the comment contains banned words.
- * @param string $comment
- * @param CommentPostingException|SCoreException $ex
- * @throws CommentPostingException|SCoreException if the comment contains banned words.
- */
- private function test_text($comment, $ex) {
- $comment = strtolower($comment);
+ /**
+ * Throws if the comment contains banned words.
+ */
+ private function test_text(string $comment, SCoreException $ex): void
+ {
+ $comment = strtolower($comment);
- foreach($this->get_words() as $word) {
- if($word[0] == '/') {
- // lines that start with slash are regex
- if(preg_match($word, $comment) === 1) {
- throw $ex;
- }
- }
- else {
- // other words are literal
- if(strpos($comment, $word) !== false) {
- throw $ex;
- }
- }
- }
- }
+ foreach ($this->get_words() as $word) {
+ if ($word[0] == '/') {
+ // lines that start with slash are regex
+ if (preg_match($word, $comment) === 1) {
+ throw $ex;
+ }
+ } else {
+ // other words are literal
+ if (strpos($comment, $word) !== false) {
+ throw $ex;
+ }
+ }
+ }
+ }
- /**
- * @return string[]
- */
- private function get_words() {
- global $config;
- $words = array();
+ private function get_words(): array
+ {
+ global $config;
+ $words = [];
- $banned = $config->get_string("banned_words");
- foreach(explode("\n", $banned) as $word) {
- $word = trim(strtolower($word));
- if(strlen($word) == 0) {
- // line is blank
- continue;
- }
- $words[] = $word;
- }
+ $banned = $config->get_string("banned_words");
+ foreach (explode("\n", $banned) as $word) {
+ $word = trim(strtolower($word));
+ if (strlen($word) == 0) {
+ // line is blank
+ continue;
+ }
+ $words[] = $word;
+ }
- return $words;
- }
+ return $words;
+ }
- public function get_priority() {return 30;}
+ public function get_priority(): int
+ {
+ return 30;
+ }
}
-
diff --git a/ext/ban_words/test.php b/ext/ban_words/test.php
index 886aee18..448694dc 100644
--- a/ext/ban_words/test.php
+++ b/ext/ban_words/test.php
@@ -1,33 +1,34 @@
-fail("Exception not thrown");
- }
- catch(CommentPostingException $e) {
- $this->assertEquals($e->getMessage(), "Comment contains banned terms");
- }
- }
+fail("Exception not thrown");
+ } catch (CommentPostingException $e) {
+ $this->assertEquals($e->getMessage(), "Comment contains banned terms");
+ }
+ }
- public function testWordBan() {
- global $config;
- $config->set_string("banned_words", "viagra\nporn\n\n/http:.*\.cn\//");
+ public function testWordBan()
+ {
+ global $config;
+ $config->set_string("banned_words", "viagra\nporn\n\n/http:.*\.cn\//");
- $this->log_in_as_user();
- $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot");
+ $this->log_in_as_user();
+ $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot");
- $this->check_blocked($image_id, "kittens and viagra");
- $this->check_blocked($image_id, "kittens and ViagrA");
- $this->check_blocked($image_id, "kittens and viagra!");
- $this->check_blocked($image_id, "some link to http://something.cn/");
+ $this->check_blocked($image_id, "kittens and viagra");
+ $this->check_blocked($image_id, "kittens and ViagrA");
+ $this->check_blocked($image_id, "kittens and viagra!");
+ $this->check_blocked($image_id, "some link to http://something.cn/");
- $this->get_page('comment/list');
- $this->assert_title('Comments');
- $this->assert_no_text('viagra');
- $this->assert_no_text('ViagrA');
- $this->assert_no_text('http://something.cn/');
- }
+ $this->get_page('comment/list');
+ $this->assert_title('Comments');
+ $this->assert_no_text('viagra');
+ $this->assert_no_text('ViagrA');
+ $this->assert_no_text('http://something.cn/');
+ }
}
-
diff --git a/ext/bbcode/info.php b/ext/bbcode/info.php
new file mode 100644
index 00000000..c0291a84
--- /dev/null
+++ b/ext/bbcode/info.php
@@ -0,0 +1,32 @@
+
+
Add entries to the blotter, and they will be displayed.
";
- return $html;
- }
+ return $html;
+ }
- private function get_html_for_blotter_page($entries) {
- /**
- * This one displays a list of all blotter entries.
- */
- global $config;
- $i_color = $config->get_string("blotter_color", "#FF0000");
- $html = "
";
+ private function get_html_for_blotter_page($entries)
+ {
+ /**
+ * This one displays a list of all blotter entries.
+ */
+ global $config;
+ $i_color = $config->get_string("blotter_color", "#FF0000");
+ $html = "
";
+ }
+
+ if (!$hasQuery) {
+ $body .= "";
+ }
+ $block = new Block("Bulk Actions", $body, "left", 30);
+ $page->add_block($block);
+ }
+
+ public function render_ban_reason_input()
+ {
+ if (class_exists("ImageBan")) {
+ return "";
+ } else {
+ return "";
+ }
+ }
+
+ public function render_tag_input()
+ {
+ return "" .
+ "";
+ }
+
+ public function render_source_input()
+ {
+ return "";
+ }
+}
diff --git a/ext/bulk_add/info.php b/ext/bulk_add/info.php
new file mode 100644
index 00000000..568b333b
--- /dev/null
+++ b/ext/bulk_add/info.php
@@ -0,0 +1,22 @@
+/home/bob/uploads/holiday/2008/ and point
+ shimmie at /home/bob/uploads, then images will be
+ tagged \"holiday 2008\")
+
Note: requires the \"admin\" extension to be enabled
+";
+}
diff --git a/ext/bulk_add/main.php b/ext/bulk_add/main.php
index fa532526..022589b6 100644
--- a/ext/bulk_add/main.php
+++ b/ext/bulk_add/main.php
@@ -1,74 +1,64 @@
-
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: Bulk add server-side images
- * Documentation:
- * Upload the images into a new directory via ftp or similar, go to
- * shimmie's admin page and put that directory in the bulk add box.
- * If there are subdirectories, they get used as tags (eg if you
- * upload into /home/bob/uploads/holiday/2008/ and point
- * shimmie at /home/bob/uploads, then images will be
- * tagged "holiday 2008")
- *
Note: requires the "admin" extension to be enabled
- */
+dir = $dir;
- $this->results = array();
- }
+ public function __construct(string $dir)
+ {
+ parent::__construct();
+ $this->dir = $dir;
+ $this->results = [];
+ }
}
-class BulkAdd extends Extension {
- public function onPageRequest(PageRequestEvent $event) {
- global $page, $user;
- if($event->page_matches("bulk_add")) {
- if($user->is_admin() && $user->check_auth_token() && isset($_POST['dir'])) {
- set_time_limit(0);
- $bae = new BulkAddEvent($_POST['dir']);
- send_event($bae);
- if(is_array($bae->results)) {
- foreach($bae->results as $result) {
- $this->theme->add_status("Adding files", $result);
- }
- } else if(strlen($bae->results) > 0) {
- $this->theme->add_status("Adding files", $bae->results);
- }
- $this->theme->display_upload_results($page);
- }
- }
- }
+class BulkAdd extends Extension
+{
+ /** @var BulkAddTheme */
+ protected $theme;
- public function onCommand(CommandEvent $event) {
- if($event->cmd == "help") {
- print "\tbulk-add [directory]\n";
- print "\t\tImport this directory\n\n";
- }
- if($event->cmd == "bulk-add") {
- if(count($event->args) == 1) {
- $bae = new BulkAddEvent($event->args[0]);
- send_event($bae);
- print(implode("\n", $bae->results));
- }
- }
- }
+ public function onPageRequest(PageRequestEvent $event)
+ {
+ global $page, $user;
+ if ($event->page_matches("bulk_add")) {
+ if ($user->can(Permissions::BULK_ADD) && $user->check_auth_token() && isset($_POST['dir'])) {
+ set_time_limit(0);
+ $bae = send_event(new BulkAddEvent($_POST['dir']));
+ foreach ($bae->results as $result) {
+ $this->theme->add_status("Adding files", $result);
+ }
+ $this->theme->display_upload_results($page);
+ }
+ }
+ }
- public function onAdminBuilding(AdminBuildingEvent $event) {
- $this->theme->display_admin_block();
- }
+ public function onCommand(CommandEvent $event)
+ {
+ if ($event->cmd == "help") {
+ print "\tbulk-add [directory]\n";
+ print "\t\tImport this directory\n\n";
+ }
+ if ($event->cmd == "bulk-add") {
+ if (count($event->args) == 1) {
+ $bae = send_event(new BulkAddEvent($event->args[0]));
+ print(implode("\n", $bae->results));
+ }
+ }
+ }
- public function onBulkAdd(BulkAddEvent $event) {
- if(is_dir($event->dir) && is_readable($event->dir)) {
- $event->results = add_dir($event->dir);
- }
- else {
- $h_dir = html_escape($event->dir);
- $event->results[] = "Error, $h_dir is not a readable directory";
- }
- }
+ public function onAdminBuilding(AdminBuildingEvent $event)
+ {
+ $this->theme->display_admin_block();
+ }
+
+ public function onBulkAdd(BulkAddEvent $event)
+ {
+ if (is_dir($event->dir) && is_readable($event->dir)) {
+ $event->results = add_dir($event->dir);
+ } else {
+ $h_dir = html_escape($event->dir);
+ $event->results[] = "Error, $h_dir is not a readable directory";
+ }
+ }
}
diff --git a/ext/bulk_add/test.php b/ext/bulk_add/test.php
index 5ecb7de1..48dad449 100644
--- a/ext/bulk_add/test.php
+++ b/ext/bulk_add/test.php
@@ -1,37 +1,23 @@
-log_in_as_admin();
+get_page('admin');
- $this->assert_title("Admin Tools");
+class BulkAddTest extends ShimmiePHPUnitTestCase
+{
+ public function testInvalidDir()
+ {
+ send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
+ $bae = send_event(new BulkAddEvent('asdf'));
+ $this->assertContains(
+ "Error, asdf is not a readable directory",
+ $bae->results,
+ implode("\n", $bae->results)
+ );
+ }
- $bae = new BulkAddEvent('asdf');
- send_event($bae);
- $this->assertContains("Error, asdf is not a readable directory",
- $bae->results, implode("\n", $bae->results));
-
- // FIXME: have BAE return a list of successes as well as errors?
- $this->markTestIncomplete();
-
- $this->get_page('admin');
- $this->assert_title("Admin Tools");
- send_event(new BulkAddEvent('tests'));
-
- # FIXME: test that the output here makes sense, no "adding foo.php ... ok"
-
- $this->get_page("post/list/hash=17fc89f372ed3636e28bd25cc7f3bac1/1");
- $this->assert_title(new PatternExpectation("/^Image \d+: data/"));
- $this->click("Delete");
-
- $this->get_page("post/list/hash=feb01bab5698a11dd87416724c7a89e3/1");
- $this->assert_title(new PatternExpectation("/^Image \d+: data/"));
- $this->click("Delete");
-
- $this->get_page("post/list/hash=e106ea2983e1b77f11e00c0c54e53805/1");
- $this->assert_title(new PatternExpectation("/^Image \d+: data/"));
- $this->click("Delete");
-
- $this->log_out();
- }
+ public function testValidDir()
+ {
+ send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
+ send_event(new BulkAddEvent('tests'));
+ $page = $this->get_page("post/list/hash=17fc89f372ed3636e28bd25cc7f3bac1/1");
+ $this->assertEquals(302, $page->code);
+ }
}
diff --git a/ext/bulk_add/theme.php b/ext/bulk_add/theme.php
index 98cd9b7f..bfdcf5c4 100644
--- a/ext/bulk_add/theme.php
+++ b/ext/bulk_add/theme.php
@@ -1,30 +1,33 @@
-set_title("Adding folder");
- $page->set_heading("Adding folder");
- $page->add_block(new NavBlock());
- $html = "";
- foreach($this->messages as $block) {
- $html .= " " . $block->body;
- }
- $page->add_block(new Block("Results", $html));
- }
+ /*
+ * Show a standard page for results to be put into
+ */
+ public function display_upload_results(Page $page)
+ {
+ $page->set_title("Adding folder");
+ $page->set_heading("Adding folder");
+ $page->add_block(new NavBlock());
+ $html = "";
+ foreach ($this->messages as $block) {
+ $html .= " " . $block->body;
+ }
+ $page->add_block(new Block("Results", $html));
+ }
- /*
- * Add a section to the admin page. This should contain a form which
- * links to bulk_add with POST[dir] set to the name of a server-side
- * directory full of images
- */
- public function display_admin_block() {
- global $page;
- $html = "
+ /*
+ * Add a section to the admin page. This should contain a form which
+ * links to bulk_add with POST[dir] set to the name of a server-side
+ * directory full of images
+ */
+ public function display_admin_block()
+ {
+ global $page;
+ $html = "
Add a folder full of images; any subfolders will have their names
used as tags for the images within.
Note: this is the folder as seen by the server -- you need to
@@ -37,10 +40,11 @@ class BulkAddTheme extends Themelet {
";
- $page->add_block(new Block("Bulk Add", $html));
- }
+ $page->add_block(new Block("Bulk Add", $html));
+ }
- public function add_status($title, $body) {
- $this->messages[] = new Block($title, $body);
- }
+ public function add_status($title, $body)
+ {
+ $this->messages[] = new Block($title, $body);
+ }
}
diff --git a/ext/bulk_add_csv/info.php b/ext/bulk_add_csv/info.php
new file mode 100644
index 00000000..35f72e1f
--- /dev/null
+++ b/ext/bulk_add_csv/info.php
@@ -0,0 +1,24 @@
+"velocity37@gmail.com"];
+ public $license = self::LICENSE_GPLV2;
+ public $description = "Bulk add server-side images with metadata from CSV file";
+ public $documentation =
+"Modification of \"Bulk Add\" by Shish.
+Adds images from a CSV with the five following values:
+\"/path/to/image.jpg\",\"spaced tags\",\"source\",\"rating s/q/e\",\"/path/thumbnail.jpg\"
+e.g. \"/tmp/cat.png\",\"shish oekaki\",\"shimmie.shishnet.org\",\"s\",\"tmp/custom.jpg\"
+Any value but the first may be omitted, but there must be five values per line.
+e.g. \"/why/not/try/bulk_add.jpg\",\"\",\"\",\"\",\"\"
+Image thumbnails will be displayed at the AR of the full image. Thumbnails that are
+normally static (e.g. SWF) will be displayed at the board's max thumbnail size
+Useful for importing tagged images without having to do database manipulation.
+
Note: requires \"Admin Controls\" and optionally \"Image Ratings\" to be enabled
";
+}
diff --git a/ext/bulk_add_csv/main.php b/ext/bulk_add_csv/main.php
index a5b999e7..5816dded 100644
--- a/ext/bulk_add_csv/main.php
+++ b/ext/bulk_add_csv/main.php
@@ -1,147 +1,125 @@
-
- * License: GPLv2
- * Description: Bulk add server-side images with metadata from CSV file
- * Documentation:
- * Modification of "Bulk Add" by Shish.
- * Adds images from a CSV with the five following values:
- * "/path/to/image.jpg","spaced tags","source","rating s/q/e","/path/thumbnail.jpg"
- * e.g. "/tmp/cat.png","shish oekaki","shimmie.shishnet.org","s","tmp/custom.jpg"
- * Any value but the first may be omitted, but there must be five values per line.
- * e.g. "/why/not/try/bulk_add.jpg","","","",""
- * Image thumbnails will be displayed at the AR of the full image. Thumbnails that are
- * normally static (e.g. SWF) will be displayed at the board's max thumbnail size
- * Useful for importing tagged images without having to do database manipulation.
- *
Note: requires "Admin Controls" and optionally "Image Ratings" to be enabled
- *
- */
+page_matches("bulk_add_csv")) {
- if($user->is_admin() && $user->check_auth_token() && isset($_POST['csv'])) {
- set_time_limit(0);
- $this->add_csv($_POST['csv']);
- $this->theme->display_upload_results($page);
- }
- }
- }
+class BulkAddCSV extends Extension
+{
+ /** @var BulkAddCSVTheme */
+ protected $theme;
- public function onCommand(CommandEvent $event) {
- if($event->cmd == "help") {
- print " bulk-add-csv [/path/to.csv]\n";
- print " Import this .csv file (refer to documentation)\n\n";
- }
- if($event->cmd == "bulk-add-csv") {
- global $user;
-
- //Nag until CLI is admin by default
- if (!$user->is_admin()) {
- print "Not running as an admin, which can cause problems.\n";
- print "Please add the parameter: -u admin_username";
- } elseif(count($event->args) == 1) {
- $this->add_csv($event->args[0]);
- }
- }
- }
+ public function onPageRequest(PageRequestEvent $event)
+ {
+ global $page, $user;
+ if ($event->page_matches("bulk_add_csv")) {
+ if ($user->can(Permissions::BULK_ADD) && $user->check_auth_token() && isset($_POST['csv'])) {
+ set_time_limit(0);
+ $this->add_csv($_POST['csv']);
+ $this->theme->display_upload_results($page);
+ }
+ }
+ }
- public function onAdminBuilding(AdminBuildingEvent $event) {
- $this->theme->display_admin_block();
- }
+ public function onCommand(CommandEvent $event)
+ {
+ if ($event->cmd == "help") {
+ print " bulk-add-csv [/path/to.csv]\n";
+ print " Import this .csv file (refer to documentation)\n\n";
+ }
+ if ($event->cmd == "bulk-add-csv") {
+ global $user;
- /**
- * Generate the necessary DataUploadEvent for a given image and tags.
- *
- * @param string $tmpname
- * @param string $filename
- * @param string $tags
- * @param string $source
- * @param string $rating
- * @param string $thumbfile
- * @throws UploadException
- */
- private function add_image($tmpname, $filename, $tags, $source, $rating, $thumbfile) {
- assert(file_exists($tmpname));
+ //Nag until CLI is admin by default
+ if (!$user->can(Permissions::BULK_ADD)) {
+ print "Not running as an admin, which can cause problems.\n";
+ print "Please add the parameter: -u admin_username";
+ } elseif (count($event->args) == 1) {
+ $this->add_csv($event->args[0]);
+ }
+ }
+ }
- $pathinfo = pathinfo($filename);
- if(!array_key_exists('extension', $pathinfo)) {
- throw new UploadException("File has no extension");
- }
- $metadata = array();
- $metadata['filename'] = $pathinfo['basename'];
- $metadata['extension'] = $pathinfo['extension'];
- $metadata['tags'] = Tag::explode($tags);
- $metadata['source'] = $source;
- $event = new DataUploadEvent($tmpname, $metadata);
- send_event($event);
- if($event->image_id == -1) {
- throw new UploadException("File type not recognised");
- } else {
- if(class_exists("RatingSetEvent") && in_array($rating, array("s", "q", "e"))) {
- $ratingevent = new RatingSetEvent(Image::by_id($event->image_id), $rating);
- send_event($ratingevent);
- }
- if (file_exists($thumbfile)) {
- copy($thumbfile, warehouse_path("thumbs", $event->hash));
- }
- }
- }
+ public function onAdminBuilding(AdminBuildingEvent $event)
+ {
+ $this->theme->display_admin_block();
+ }
- private function add_csv(/*string*/ $csvfile) {
- if(!file_exists($csvfile)) {
- $this->theme->add_status("Error", "$csvfile not found");
- return;
- }
- if (!is_file($csvfile) || strtolower(substr($csvfile, -4)) != ".csv") {
- $this->theme->add_status("Error", "$csvfile doesn't appear to be a csv file");
- return;
- }
-
- $linenum = 1;
- $list = "";
- $csvhandle = fopen($csvfile, "r");
-
- while (($csvdata = fgetcsv($csvhandle, 0, ",")) !== FALSE) {
- if(count($csvdata) != 5) {
- if(strlen($list) > 0) {
- $this->theme->add_status("Error", "Encountered malformed data. Line $linenum $csvfile ".$list);
- fclose($csvhandle);
- return;
- } else {
- $this->theme->add_status("Error", "Encountered malformed data. Line $linenum $csvfile Check here for the expected format");
- fclose($csvhandle);
- return;
- }
- }
- $fullpath = $csvdata[0];
- $tags = trim($csvdata[1]);
- $source = $csvdata[2];
- $rating = $csvdata[3];
- $thumbfile = $csvdata[4];
- $pathinfo = pathinfo($fullpath);
- $shortpath = $pathinfo["basename"];
- $list .= " ".html_escape("$shortpath (".str_replace(" ", ", ", $tags).")... ");
- if (file_exists($csvdata[0]) && is_file($csvdata[0])) {
- try{
- $this->add_image($fullpath, $pathinfo["basename"], $tags, $source, $rating, $thumbfile);
- $list .= "ok\n";
- }
- catch(Exception $ex) {
- $list .= "failed: ". $ex->getMessage();
- }
- } else {
- $list .= "failed: File doesn't exist ".html_escape($csvdata[0]);
- }
- $linenum += 1;
- }
-
- if(strlen($list) > 0) {
- $this->theme->add_status("Adding $csvfile", $list);
- }
- fclose($csvhandle);
- }
+ /**
+ * Generate the necessary DataUploadEvent for a given image and tags.
+ */
+ private function add_image(string $tmpname, string $filename, string $tags, string $source, string $rating, string $thumbfile)
+ {
+ assert(file_exists($tmpname));
+
+ $pathinfo = pathinfo($filename);
+ $metadata = [];
+ $metadata['filename'] = $pathinfo['basename'];
+ if (array_key_exists('extension', $pathinfo)) {
+ $metadata['extension'] = $pathinfo['extension'];
+ }
+ $metadata['tags'] = Tag::explode($tags);
+ $metadata['source'] = $source;
+ $event = send_event(new DataUploadEvent($tmpname, $metadata));
+ if ($event->image_id == -1) {
+ throw new UploadException("File type not recognised");
+ } else {
+ if (class_exists("RatingSetEvent") && in_array($rating, ["s", "q", "e"])) {
+ send_event(new RatingSetEvent(Image::by_id($event->image_id), $rating));
+ }
+ if (file_exists($thumbfile)) {
+ copy($thumbfile, warehouse_path(Image::THUMBNAIL_DIR, $event->hash));
+ }
+ }
+ }
+
+ private function add_csv(string $csvfile)
+ {
+ if (!file_exists($csvfile)) {
+ $this->theme->add_status("Error", "$csvfile not found");
+ return;
+ }
+ if (!is_file($csvfile) || strtolower(substr($csvfile, -4)) != ".csv") {
+ $this->theme->add_status("Error", "$csvfile doesn't appear to be a csv file");
+ return;
+ }
+
+ $linenum = 1;
+ $list = "";
+ $csvhandle = fopen($csvfile, "r");
+
+ while (($csvdata = fgetcsv($csvhandle, 0, ",")) !== false) {
+ if (count($csvdata) != 5) {
+ if (strlen($list) > 0) {
+ $this->theme->add_status("Error", "Encountered malformed data. Line $linenum $csvfile ".$list);
+ fclose($csvhandle);
+ return;
+ } else {
+ $this->theme->add_status("Error", "Encountered malformed data. Line $linenum $csvfile Check here for the expected format");
+ fclose($csvhandle);
+ return;
+ }
+ }
+ $fullpath = $csvdata[0];
+ $tags = trim($csvdata[1]);
+ $source = $csvdata[2];
+ $rating = $csvdata[3];
+ $thumbfile = $csvdata[4];
+ $pathinfo = pathinfo($fullpath);
+ $shortpath = $pathinfo["basename"];
+ $list .= " ".html_escape("$shortpath (".str_replace(" ", ", ", $tags).")... ");
+ if (file_exists($csvdata[0]) && is_file($csvdata[0])) {
+ try {
+ $this->add_image($fullpath, $pathinfo["basename"], $tags, $source, $rating, $thumbfile);
+ $list .= "ok\n";
+ } catch (Exception $ex) {
+ $list .= "failed: ". $ex->getMessage();
+ }
+ } else {
+ $list .= "failed: File doesn't exist ".html_escape($csvdata[0]);
+ }
+ $linenum += 1;
+ }
+
+ if (strlen($list) > 0) {
+ $this->theme->add_status("Adding $csvfile", $list);
+ }
+ fclose($csvhandle);
+ }
}
-
diff --git a/ext/bulk_add_csv/theme.php b/ext/bulk_add_csv/theme.php
index 88fcc41d..b552b07d 100644
--- a/ext/bulk_add_csv/theme.php
+++ b/ext/bulk_add_csv/theme.php
@@ -1,28 +1,31 @@
-set_title("Adding images from csv");
- $page->set_heading("Adding images from csv");
- $page->add_block(new NavBlock());
- foreach($this->messages as $block) {
- $page->add_block($block);
- }
- }
+ /*
+ * Show a standard page for results to be put into
+ */
+ public function display_upload_results(Page $page)
+ {
+ $page->set_title("Adding images from csv");
+ $page->set_heading("Adding images from csv");
+ $page->add_block(new NavBlock());
+ foreach ($this->messages as $block) {
+ $page->add_block($block);
+ }
+ }
- /*
- * Add a section to the admin page. This should contain a form which
- * links to bulk_add_csv with POST[csv] set to the name of a server-side
- * csv file
- */
- public function display_admin_block() {
- global $page;
- $html = "
+ /*
+ * Add a section to the admin page. This should contain a form which
+ * links to bulk_add_csv with POST[csv] set to the name of a server-side
+ * csv file
+ */
+ public function display_admin_block()
+ {
+ global $page;
+ $html = "
Add images from a csv. Images will be tagged and have their
source and rating set (if \"Image Ratings\" is enabled)
Specify the absolute or relative path to a local .csv file. Check here for the expected format.
@@ -34,11 +37,11 @@ class BulkAddCSVTheme extends Themelet {
";
- $page->add_block(new Block("Bulk Add CSV", $html));
- }
+ $page->add_block(new Block("Bulk Add CSV", $html));
+ }
- public function add_status($title, $body) {
- $this->messages[] = new Block($title, $body);
- }
+ public function add_status($title, $body)
+ {
+ $this->messages[] = new Block($title, $body);
+ }
}
-
diff --git a/ext/bulk_remove/main.php b/ext/bulk_remove/main.php
deleted file mode 100644
index 592d85f6..00000000
--- a/ext/bulk_remove/main.php
+++ /dev/null
@@ -1,133 +0,0 @@
-
- * Link: http://www.drudexsoftware.com/
- * License: GPLv2
- * Description: Allows admin to delete many images at once through Board Admin.
- * Documentation:
- *
- */
-//todo: removal by tag returns 1 less image in test for some reason, actually a combined search doesn't seem to work for shit either
-
-class BulkRemove extends Extension {
- public function onPageRequest(PageRequestEvent $event) {
- global $user;
- if($event->page_matches("bulk_remove") && $user->is_admin() && $user->check_auth_token()) {
- if ($event->get_arg(0) == "confirm") $this->do_bulk_remove();
- else $this->show_confirm();
- }
- }
-
- public function onAdminBuilding(AdminBuildingEvent $event) {
- global $page;
- $html = "Be extremely careful when using this!
- Once an image is removed there is no way to recover it so it is recommended that
- you first take when removing a large amount of images.
- Note: Entering both an ID range and tags will only remove images between the given ID's that have the given tags.
-
-
".make_form(make_link("bulk_remove"))."
-
-
Remove images by ID
-
From
-
Until
-
-
Where tags are
-
-
-
-
-
-
- ";
- $page->add_block(new Block("Bulk Remove", $html));
- }
-
- // returns a list of images to be removed
- private function determine_images()
- {
- // set vars
- $images_for_removal = array();
- $error = "";
-
- $min_id = $_POST['remove_id_min'];
- $max_id = $_POST['remove_id_max'];
- $tags = $_POST['remove_tags'];
-
-
- // if using id range to remove (comined removal with tags)
- if ($min_id != "" && $max_id != "")
- {
- // error if values are not correctly entered
- if (!is_numeric($min_id) || !is_numeric($max_id) ||
- intval($max_id) < intval($min_id))
- $error = "Values not correctly entered for removal between id.";
-
- else { // if min & max id are valid
-
- // Grab the list of images & place it in the removing array
- foreach (Image::find_images(intval($min_id), intval($max_id)) as $image)
- array_push($images_for_removal, $image);
- }
- }
-
- // refine previous results or create results from tags
- if ($tags != "")
- {
- $tags_arr = explode(" ", $_POST['remove_tags']);
-
- // Search all images with the specified tags & add to list
- foreach (Image::find_images(1, 2147483647, $tags_arr) as $image)
- array_push($images_for_removal, $image);
- }
-
-
- // if no images were found with the given info
- if (count($images_for_removal) == 0)
- $error = "No images selected for removal";
-
- //var_dump($tags_arr);
- return array(
- "error" => $error,
- "images_for_removal" => $images_for_removal);
- }
-
- // displays confirmation to admin before removal
- private function show_confirm()
- {
- global $page;
-
- // set vars
- $determined_imgs = $this->determine_images();
- $error = $determined_imgs["error"];
- $images_for_removal = $determined_imgs["images_for_removal"];
-
- // if there was an error in determine_images()
- if ($error != "") {
- $page->add_block(new Block("Cannot remove images", $error));
- return;
- }
- // generates the image array & places it in $_POST["bulk_remove_images"]
- $_POST["bulk_remove_images"] = $images_for_removal;
-
- // Display confirmation message
- $html = make_form(make_link("bulk_remove")).
- "Are you sure you want to PERMANENTLY remove ".
- count($images_for_removal) ." images? ";
- $page->add_block(new Block("Confirm Removal", $html));
- }
-
- private function do_bulk_remove()
- {
- global $page;
- // display error if user didn't go through admin board
- if (!isset($_POST["bulk_remove_images"])) {
- $page->add_block(new Block("Bulk Remove Error",
- "Please use Board Admin to use bulk remove."));
- }
-
- //
- $image_arr = $_POST["bulk_remove_images"];
- }
-}
-
diff --git a/ext/chatbox/cp/ajax.php b/ext/chatbox/cp/ajax.php
deleted file mode 100644
index f682649f..00000000
--- a/ext/chatbox/cp/ajax.php
+++ /dev/null
@@ -1,457 +0,0 @@
- false,
- 'html' => cp()
- );
-
- echo json_encode($result);
- return;
- }
-
- login(md5($_POST['password']));
- $result = array();
- if (loggedIn()) {
- $result['error'] = false;
- $result['html'] = cp();
- } else
- $result['error'] = 'invalid';
-
- echo json_encode($result);
-}
-
-function doLogout() {
- logout();
-
- $result = array(
- 'error' => false
- );
-
- echo json_encode($result);
-}
-
-function doUnban() {
- global $kioskMode;
-
- if ($kioskMode) {
- $result = array(
- 'error' => false
- );
-
- echo json_encode($result);
- return;
- }
-
- if (!loggedIn()) return;
-
- $ys = ys();
- $result = array();
-
- $ip = $_POST['ip'];
-
- if ($ys->banned($ip)) {
- $ys->unban($ip);
- $result['error'] = false;
- } else
- $result['error'] = 'notbanned';
-
-
- echo json_encode($result);
-}
-
-function doUnbanAll() {
- global $kioskMode;
-
- if ($kioskMode) {
- $result = array(
- 'error' => false
- );
-
- echo json_encode($result);
- return;
- }
-
- if (!loggedIn()) return;
-
- $ys = ys();
- $ys->unbanAll();
-
- $result = array(
- 'error' => false
- );
-
- echo json_encode($result);
-}
-
-
-function doSetPreference() {
- global $prefs, $kioskMode;
-
- if ($kioskMode) {
- $result = array(
- 'error' => false
- );
-
- echo json_encode($result);
- return;
- }
-
- if (!loggedIn()) return;
-
- $pref = $_POST['preference'];
- $value = magic($_POST['value']);
-
- if ($value === 'true') $value = true;
- if ($value === 'false') $value = false;
-
- $prefs[$pref] = $value;
-
- savePrefs($prefs);
-
- if ($pref == 'password') login(md5($value));
-
- $result = array(
- 'error' => false
- );
-
- echo json_encode($result);
-}
-
-
-function doResetPreferences() {
- global $prefs, $kioskMode;
-
- if ($kioskMode) {
- $result = array(
- 'error' => false
- );
-
- echo json_encode($result);
- return;
- }
-
- if (!loggedIn()) return;
-
- resetPrefs();
- login(md5($prefs['password']));
-
- // $prefs['password'] = 'lol no';
- $result = array(
- 'error' => false,
- 'prefs' => $prefs
- );
-
- echo json_encode($result);
-}
-
-/* CP Display */
-
-function cp() {
- global $kioskMode;
-
- if (!loggedIn() && !$kioskMode) return 'You\'re not logged in!';
-
- return '
-
-
YShout was created and developed by Yuri Vishnevsky. Version 5 is the first one with an about page, so you\'ll have to excuse the lack of appropriate information — I\'m not quite sure what it is that goes on "About" pages anyway.
-
Other than that obviously important tidbit of information, there\'s really nothing else that I can think of putting here... If anyone knows what a good and proper about page should contain, please contact me!
-
-
-
-
Contact Yuri
-
If you have any questions or comments, you can contact me by email at yurivish@gmail.com, or on AIM at yurivish42.
';
- }
-}
+ }
+ public function get_help_html()
+ {
+ return '
Search for images containing a certain number of comments, or comments by a particular individual.
+
+
comments=1
+
Returns images with exactly 1 comment.
+
+
+
comments>0
+
Returns images with 1 or more comments.
+
+
Can use <, <=, >, >=, or =.
+
+
commented_by:username
+
Returns images that have been commented on by "username".
+
+
+
commented_by_userno:123
+
Returns images that have been commented on by user 123.
+
+ ';
+ }
+}
diff --git a/ext/cron_uploader/config.php b/ext/cron_uploader/config.php
new file mode 100644
index 00000000..5c1d962c
--- /dev/null
+++ b/ext/cron_uploader/config.php
@@ -0,0 +1,96 @@
+set_default_int(self::COUNT, 1);
+ $config->set_default_string(self::DIR, data_path(self::DEFAULT_PATH));
+
+ $upload_key = $config->get_string(self::KEY, "");
+ if (empty($upload_key)) {
+ $upload_key = self::generate_key();
+
+ $config->set_string(self::KEY, $upload_key);
+ }
+ }
+
+ public static function get_user(): int
+ {
+ global $config;
+ return $config->get_int(self::USER);
+ }
+
+ public static function set_user(int $value): void
+ {
+ global $config;
+ $config->set_int(self::USER, $value);
+ }
+
+ public static function get_key(): string
+ {
+ global $config;
+ return $config->get_string(self::KEY);
+ }
+
+ public static function set_key(string $value): void
+ {
+ global $config;
+ $config->set_string(self::KEY, $value);
+ }
+
+ public static function get_count(): int
+ {
+ global $config;
+ return $config->get_int(self::COUNT);
+ }
+
+ public static function set_count(int $value): void
+ {
+ global $config;
+ $config->set_int(self::COUNT, $value);
+ }
+
+ public static function get_dir(): string
+ {
+ global $config;
+ $value = $config->get_string(self::DIR);
+ if (empty($value)) {
+ $value = data_path("cron_uploader");
+ self::set_dir($value);
+ }
+ return $value;
+ }
+
+ public static function set_dir(string $value): void
+ {
+ global $config;
+ $config->set_string(self::DIR, $value);
+ }
+
+
+ /*
+ * Generates a unique key for the website to prevent unauthorized access.
+ */
+ private static function generate_key()
+ {
+ $length = 20;
+ $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ $randomString = '';
+
+ for ($i = 0; $i < $length; $i++) {
+ $randomString .= $characters [rand(0, strlen($characters) - 1)];
+ }
+
+ return $randomString;
+ }
+}
diff --git a/ext/cron_uploader/info.php b/ext/cron_uploader/info.php
new file mode 100644
index 00000000..7b920aa3
--- /dev/null
+++ b/ext/cron_uploader/info.php
@@ -0,0 +1,28 @@
+, Matthew Barbour
+ * Link: http://www.yaoifox.com/
+ * License: GPLv2
+ * Description: Uploads images automatically using Cron Jobs
+ * Documentation: Installation guide: activate this extension and navigate to www.yoursite.com/cron_upload
+ */
+
+class CronUploaderInfo extends ExtensionInfo
+{
+ public const KEY = "cron_uploader";
+
+ public $key = self::KEY;
+ public $name = "Cron Uploader";
+ public $url = self::SHIMMIE_URL;
+ public $authors = ["YaoiFox"=>"admin@yaoifox.com", "Matthew Barbour"=>"matthew@darkholme.net"];
+ public $license = self::LICENSE_GPLV2;
+ public $description = "Uploads images automatically using Cron Jobs";
+
+ public function __construct()
+ {
+ $this->documentation = "Installation guide: activate this extension and navigate to System Config screen.";
+ parent::__construct();
+ }
+}
diff --git a/ext/cron_uploader/main.php b/ext/cron_uploader/main.php
index a9b05650..a8d2c585 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -1,430 +1,505 @@
-
- * Link: http://www.yaoifox.com/
- * License: GPLv2
- * Description: Uploads images automatically using Cron Jobs
- * Documentation: Installation guide: activate this extension and navigate to www.yoursite.com/cron_upload
- */
-class CronUploader extends Extension {
- // TODO: Checkbox option to only allow localhost + a list of additional IP adresses that can be set in /cron_upload
- // TODO: Change logging to MySQL + display log at /cron_upload
- // TODO: Move stuff to theme.php
-
- /**
- * Lists all log events this session
- * @var string
- */
- private $upload_info = "";
-
- /**
- * Lists all files & info required to upload.
- * @var array
- */
- private $image_queue = array();
-
- /**
- * Cron Uploader root directory
- * @var string
- */
- private $root_dir = "";
-
- /**
- * Key used to identify uploader
- * @var string
- */
- private $upload_key = "";
-
- /**
- * Checks if the cron upload page has been accessed
- * and initializes the upload.
- * @param PageRequestEvent $event
- */
- public function onPageRequest(PageRequestEvent $event) {
- global $config, $user;
-
- if ($event->page_matches ( "cron_upload" )) {
- $this->upload_key = $config->get_string ( "cron_uploader_key", "" );
-
- // If the key is in the url, upload
- if ($this->upload_key != "" && $event->get_arg ( 0 ) == $this->upload_key) {
- // log in as admin
- $this->process_upload(); // Start upload
- }
- else if ($user->is_admin()) {
- $this->set_dir();
- $this->display_documentation();
- }
-
- }
- }
-
- private function display_documentation() {
- global $page;
- $this->set_dir(); // Determines path to cron_uploader_dir
-
-
- $queue_dir = $this->root_dir . "/queue";
- $uploaded_dir = $this->root_dir . "/uploaded";
- $failed_dir = $this->root_dir . "/failed_to_upload";
-
- $queue_dirinfo = $this->scan_dir($queue_dir);
- $uploaded_dirinfo = $this->scan_dir($uploaded_dir);
- $failed_dirinfo = $this->scan_dir($failed_dir);
-
- $cron_url = make_http(make_link("/cron_upload/" . $this->upload_key));
- $cron_cmd = "curl --silent $cron_url";
- $log_path = $this->root_dir . "/uploads.log";
-
- $info_html = "Information
-
-
-
-
Directory
-
Files
-
Size (MB)
-
Directory Path
-
-
Queue
-
{$queue_dirinfo['total_files']}
-
{$queue_dirinfo['total_mb']}
-
-
-
Uploaded
-
{$uploaded_dirinfo['total_files']}
-
{$uploaded_dirinfo['total_mb']}
-
-
-
Failed
-
{$failed_dirinfo['total_files']}
-
{$failed_dirinfo['total_mb']}
-
-
-
- Cron Command:
- Create a cron job with the command above.
- Read the documentation if you're not sure what to do. ";
-
- $install_html = "
- This cron uploader is fairly easy to use but has to be configured first.
- 1. Install & activate this plugin.
-
- 2. Upload your images you want to be uploaded to the queue directory using your FTP client.
- ($queue_dir)
- This also supports directory names to be used as tags.
-
- 3. Go to the Board Config to the Cron Uploader menu and copy the Cron Command.
- ($cron_cmd)
-
- 4. Create a cron job or something else that can open a url on specified times.
- If you're not sure how to do this, you can give the command to your web host and you can ask them to create the cron job for you.
- When you create the cron job, you choose when to upload new images.
-
- 5. When the cron command is set up, your image queue will upload x file(s) at the specified times.
- You can see any uploads or failed uploads in the log file. ($log_path)
- Your uploaded images will be moved to the 'uploaded' directory, it's recommended that you remove everything out of this directory from time to time.
- ($uploaded_dir)
-
- Whenever the url in that cron job command is opened, a new file will upload from the queue.
- So when you want to manually upload an image, all you have to do is open the link once.
- This link can be found under 'Cron Command' in the board config, just remove the 'wget ' part and only the url remains.
- ($cron_url)";
-
-
- $block = new Block("Cron Uploader", $info_html, "main", 10);
- $block_install = new Block("Installation Guide", $install_html, "main", 20);
- $page->add_block($block);
- $page->add_block($block_install);
- }
+get_string("cron_uploader_key", "")) {
- $this->upload_key = $this->generate_key ();
-
- $config->set_default_int ( 'cron_uploader_count', 1 );
- $config->set_default_string ( 'cron_uploader_key', $this->upload_key );
- $this->set_dir();
- }
- }
-
- public function onSetupBuilding(SetupBuildingEvent $event) {
- $this->set_dir();
-
- $cron_url = make_http(make_link("/cron_upload/" . $this->upload_key));
- $cron_cmd = "curl --silent $cron_url";
- $documentation_link = make_http(make_link("cron_upload"));
-
- $sb = new SetupBlock ( "Cron Uploader" );
- $sb->add_label ( "Settings " );
- $sb->add_int_option ( "cron_uploader_count", "How many to upload each time" );
- $sb->add_text_option ( "cron_uploader_dir", " Set Cron Uploader root directory ");
-
- $sb->add_label (" Cron Command:
- Create a cron job with the command above.
- Read the documentation if you're not sure what to do.");
+require_once "config.php";
- $event->panel->add_block ( $sb );
- }
-
- /*
- * Generates a unique key for the website to prevent unauthorized access.
- */
- private function generate_key() {
- $length = 20;
- $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
- $randomString = '';
-
- for($i = 0; $i < $length; $i ++) {
- $randomString .= $characters [rand ( 0, strlen ( $characters ) - 1 )];
- }
-
- return $randomString;
- }
-
- /*
- * Set the directory for the image queue. If no directory was given, set it to the default directory.
- */
- private function set_dir() {
- global $config;
- // Determine directory (none = default)
-
- $dir = $config->get_string("cron_uploader_dir", "");
-
- // Sets new default dir if not in config yet/anymore
- if ($dir == "") {
- $dir = data_path("cron_uploader");
- $config->set_string ('cron_uploader_dir', $dir);
- }
-
- // Make the directory if it doesn't exist yet
- if (!is_dir($dir . "/queue/"))
- mkdir ( $dir . "/queue/", 0775, true );
- if (!is_dir($dir . "/uploaded/"))
- mkdir ( $dir . "/uploaded/", 0775, true );
- if (!is_dir($dir . "/failed_to_upload/"))
- mkdir ( $dir . "/failed_to_upload/", 0775, true );
-
- $this->root_dir = $dir;
- return $dir;
- }
-
- /**
- * Returns amount of files & total size of dir.
- * @param string $path directory name to scan
- * @return multitype:number
- */
- function scan_dir($path){
- $ite=new RecursiveDirectoryIterator($path);
-
- $bytestotal=0;
- $nbfiles=0;
- foreach (new RecursiveIteratorIterator($ite) as $filename=>$cur) {
- $filesize = $cur->getSize();
- $bytestotal += $filesize;
- $nbfiles++;
- }
-
- $size_mb = $bytestotal / 1048576; // to mb
- $size_mb = number_format($size_mb, 2, '.', '');
- return array('total_files'=>$nbfiles,'total_mb'=>$size_mb);
- }
-
- /**
- * Uploads the image & handles everything
- * @param int $upload_count to upload a non-config amount of imgs
- * @return boolean returns true if the upload was successful
- */
- public function process_upload($upload_count = 0) {
- global $config;
- set_time_limit(0);
- $this->set_dir();
- $this->generate_image_queue();
-
- // Gets amount of imgs to upload
- if ($upload_count == 0) $upload_count = $config->get_int ("cron_uploader_count", 1);
-
- // Throw exception if there's nothing in the queue
- if (count($this->image_queue) == 0) {
- $this->add_upload_info("Your queue is empty so nothing could be uploaded.");
- $this->handle_log();
- return false;
- }
-
- // Randomize Images
- shuffle($this->image_queue);
+class CronUploader extends Extension
+{
+ /** @var CronUploaderTheme */
+ protected $theme;
- // Upload the file(s)
- for ($i = 0; $i < $upload_count; $i++) {
- $img = $this->image_queue[$i];
-
- try {
- $this->add_image($img[0], $img[1], $img[2]);
- $this->move_uploaded($img[0], $img[1], false);
-
- }
- catch (Exception $e) {
- $this->move_uploaded($img[0], $img[1], true);
- }
-
- // Remove img from queue array
- unset($this->image_queue[$i]);
- }
-
- // Display & save upload log
- $this->handle_log();
-
- return true;
- }
-
- private function move_uploaded($path, $filename, $corrupt = false) {
- global $config;
-
- // Create
- $newDir = $this->root_dir;
-
- // Determine which dir to move to
- if ($corrupt) {
- // Move to corrupt dir
- $newDir .= "/failed_to_upload/";
- $info = "ERROR: Image was not uploaded.";
- }
- else {
- $newDir .= "/uploaded/";
- $info = "Image successfully uploaded. ";
- }
-
- // move file to correct dir
- rename($path, $newDir.$filename);
-
- $this->add_upload_info($info . "Image \"$filename\" moved from queue to \"$newDir\".");
- }
+ public const NAME = "cron_uploader";
- /**
- * Generate the necessary DataUploadEvent for a given image and tags.
- *
- * @param string $tmpname
- * @param string $filename
- * @param string $tags
- */
- private function add_image($tmpname, $filename, $tags) {
- assert ( file_exists ( $tmpname ) );
- assert('is_string($tags)');
-
- $pathinfo = pathinfo ( $filename );
- if (! array_key_exists ( 'extension', $pathinfo )) {
- throw new UploadException ( "File has no extension" );
- }
- $metadata = array();
- $metadata ['filename'] = $pathinfo ['basename'];
- $metadata ['extension'] = $pathinfo ['extension'];
- $metadata ['tags'] = array(); // = $tags; doesn't work when not logged in here
- $metadata ['source'] = null;
- $event = new DataUploadEvent ( $tmpname, $metadata );
- send_event ( $event );
-
- // Generate info message
- $infomsg = ""; // Will contain info message
- if ($event->image_id == -1)
- $infomsg = "File type not recognised. Filename: {$filename}";
- else $infomsg = "Image uploaded. ID: {$event->image_id} - Filename: {$filename} - Tags: {$tags}";
- $msgNumber = $this->add_upload_info($infomsg);
-
- // Set tags
- $img = Image::by_id($event->image_id);
- $img->set_tags(Tag::explode($tags));
- }
-
- private function generate_image_queue($base = "", $subdir = "") {
- if ($base == "")
- $base = $this->root_dir . "/queue";
-
- if (! is_dir ( $base )) {
- $this->add_upload_info("Image Queue Directory could not be found at \"$base\".");
- return array();
- }
-
- foreach ( glob ( "$base/$subdir/*" ) as $fullpath ) {
- $fullpath = str_replace ( "//", "/", $fullpath );
- //$shortpath = str_replace ( $base, "", $fullpath );
-
- if (is_link ( $fullpath )) {
- // ignore
- } else if (is_dir ( $fullpath )) {
- $this->generate_image_queue ( $base, str_replace ( $base, "", $fullpath ) );
- } else {
- $pathinfo = pathinfo ( $fullpath );
- $matches = array ();
-
- if (preg_match ( "/\d+ - (.*)\.([a-zA-Z]+)/", $pathinfo ["basename"], $matches )) {
- $tags = $matches [1];
- } else {
- $tags = $subdir;
- $tags = str_replace ( "/", " ", $tags );
- $tags = str_replace ( "__", " ", $tags );
- if ($tags == "") $tags = " ";
- $tags = trim ( $tags );
- }
-
- $img = array (
- 0 => $fullpath,
- 1 => $pathinfo ["basename"],
- 2 => $tags
- );
- array_push ($this->image_queue, $img );
- }
- }
- }
-
- /**
- * Adds a message to the info being published at the end
- * @param $text string
- * @param $addon int Enter a value to modify an existing value (enter value number)
- * @return int
- */
- private function add_upload_info($text, $addon = 0) {
- $info = $this->upload_info;
- $time = "[" .date('Y-m-d H:i:s'). "]";
-
- // If addon function is not used
- if ($addon == 0) {
- $this->upload_info .= "$time $text\r\n";
-
- // Returns the number of the current line
- $currentLine = substr_count($this->upload_info, "\n") -1;
- return $currentLine;
- }
-
- // else if addon function is used, select the line & modify it
- $lines = substr($info, "\n"); // Seperate the string to array in lines
- $lines[$addon] = "$lines[$addon] $text"; // Add the content to the line
- $this->upload_info = implode("\n", $lines); // Put string back together & update
-
- return $addon; // Return line number
- }
-
- /**
- * This is run at the end to display & save the log.
- */
- private function handle_log() {
- global $page;
-
- // Display message
- $page->set_mode("data");
- $page->set_type("text/plain");
- $page->set_data($this->upload_info);
-
- // Save log
- $log_path = $this->root_dir . "/uploads.log";
-
- if (file_exists($log_path))
- $prev_content = file_get_contents($log_path);
- else $prev_content = "";
-
- $content = $prev_content ."\r\n".$this->upload_info;
- file_put_contents ($log_path, $content);
- }
+ // TODO: Checkbox option to only allow localhost + a list of additional IP addresses that can be set in /cron_upload
+
+ const QUEUE_DIR = "queue";
+ const UPLOADED_DIR = "uploaded";
+ const FAILED_DIR = "failed_to_upload";
+
+ public $output_buffer = [];
+
+ public function onInitExt(InitExtEvent $event)
+ {
+ // Set default values
+ CronUploaderConfig::set_defaults();
+ }
+
+ public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+ {
+ if ($event->parent=="system") {
+ $event->add_nav_link("cron_docs", new Link('cron_upload'), "Cron Upload");
+ }
+ }
+
+ /**
+ * Checks if the cron upload page has been accessed
+ * and initializes the upload.
+ */
+ public function onPageRequest(PageRequestEvent $event)
+ {
+ global $user;
+
+ if ($event->page_matches("cron_upload")) {
+ if ($event->count_args() == 1) {
+ $this->process_upload($event->get_arg(0)); // Start upload
+ } elseif ($user->can(Permissions::CRON_ADMIN)) {
+ $this->display_documentation();
+ }
+ }
+ }
+
+ public function onSetupBuilding(SetupBuildingEvent $event)
+ {
+ global $database;
+
+ $documentation_link = make_http(make_link("cron_upload"));
+
+ $users = $database->get_pairs("SELECT name, id FROM users UNION ALL SELECT '', null order by name");
+
+ $sb = new SetupBlock("Cron Uploader");
+ $sb->start_table();
+ $sb->add_int_option(CronUploaderConfig::COUNT, "Upload per run", true);
+ $sb->add_text_option(CronUploaderConfig::DIR, "Root dir", true);
+ $sb->add_text_option(CronUploaderConfig::KEY, "Key", true);
+ $sb->add_choice_option(CronUploaderConfig::USER, $users, "User", true);
+ $sb->end_table();
+ $sb->add_label("Read the documentation for cron setup instructions.");
+
+ $event->panel->add_block($sb);
+ }
+
+ public function onAdminBuilding(AdminBuildingEvent $event)
+ {
+ $failed_dir = $this->get_failed_dir();
+ $results = get_dir_contents($failed_dir);
+
+ $failed_dirs = [];
+ foreach ($results as $result) {
+ $path = join_path($failed_dir, $result);
+ if (is_dir($path)) {
+ $failed_dirs[] = $result;
+ }
+ }
+
+ $this->theme->display_form($failed_dirs);
+ }
+
+ public function onAdminAction(AdminActionEvent $event)
+ {
+ $action = $event->action;
+ switch ($action) {
+ case "cron_uploader_clear_queue":
+ $event->redirect = true;
+ $this->clear_folder(self::QUEUE_DIR);
+ break;
+ case "cron_uploader_clear_uploaded":
+ $event->redirect = true;
+ $this->clear_folder(self::UPLOADED_DIR);
+ break;
+ case "cron_uploader_clear_failed":
+ $event->redirect = true;
+ $this->clear_folder(self::FAILED_DIR);
+ break;
+ case "cron_uploader_restage":
+ $event->redirect = true;
+ if (array_key_exists("failed_dir", $_POST) && !empty($_POST["failed_dir"])) {
+ $this->restage_folder($_POST["failed_dir"]);
+ }
+ break;
+ }
+ }
+
+ private function restage_folder(string $folder)
+ {
+ global $page;
+ if (empty($folder)) {
+ throw new SCoreException("folder empty");
+ }
+ $queue_dir = $this->get_queue_dir();
+ $stage_dir = join_path($this->get_failed_dir(), $folder);
+
+ if (!is_dir($stage_dir)) {
+ throw new SCoreException("Could not find $stage_dir");
+ }
+
+ $this->prep_root_dir();
+
+ $results = get_dir_contents($queue_dir);
+
+ if (count($results) > 0) {
+ $page->flash("Queue folder must be empty to re-stage");
+ return;
+ }
+
+ $results = get_dir_contents($stage_dir);
+
+ if (count($results) == 0) {
+ if (rmdir($stage_dir)===false) {
+ $page->flash("Nothing to stage from $folder, cannot remove folder");
+ } else {
+ $page->flash("Nothing to stage from $folder, removing folder");
+ }
+ return;
+ }
+
+ foreach ($results as $result) {
+ $original_path = join_path($stage_dir, $result);
+ $new_path = join_path($queue_dir, $result);
+
+ rename($original_path, $new_path);
+ }
+
+ $page->flash("Re-staged $folder to queue");
+ rmdir($stage_dir);
+ }
+
+ private function clear_folder($folder)
+ {
+ global $page;
+ $path = join_path(CronUploaderConfig::get_dir(), $folder);
+ deltree($path);
+ $page->flash("Cleared $path");
+ }
+
+
+ private function get_cron_url()
+ {
+ return make_http(make_link("/cron_upload/" . CronUploaderConfig::get_key()));
+ }
+
+ private function get_cron_cmd()
+ {
+ return "curl --silent " . $this->get_cron_url();
+ }
+
+ private function display_documentation()
+ {
+ global $database;
+
+ $this->prep_root_dir();
+
+ $queue_dir = $this->get_queue_dir();
+ $uploaded_dir = $this->get_uploaded_dir();
+ $failed_dir = $this->get_failed_dir();
+
+ $queue_dirinfo = scan_dir($queue_dir);
+ $uploaded_dirinfo = scan_dir($uploaded_dir);
+ $failed_dirinfo = scan_dir($failed_dir);
+
+
+ $running = false;
+ $lockfile = fopen($this->get_lock_file(), "w");
+ try {
+ if (!flock($lockfile, LOCK_EX | LOCK_NB)) {
+ $running = true;
+ } else {
+ flock($lockfile, LOCK_UN);
+ }
+ } finally {
+ fclose($lockfile);
+ }
+
+ $logs = [];
+ if (Extension::is_enabled(LogDatabaseInfo::KEY)) {
+ $logs = $database->get_all(
+ "SELECT * FROM score_log WHERE section = :section ORDER BY date_sent DESC LIMIT 100",
+ ["section" => self::NAME]
+ );
+ }
+
+ $this->theme->display_documentation(
+ $running,
+ $queue_dirinfo,
+ $uploaded_dirinfo,
+ $failed_dirinfo,
+ $this->get_cron_cmd(),
+ $this->get_cron_url(),
+ $logs
+ );
+ }
+
+ public function get_queue_dir()
+ {
+ $dir = CronUploaderConfig::get_dir();
+ return join_path($dir, self::QUEUE_DIR);
+ }
+
+ public function get_uploaded_dir()
+ {
+ $dir = CronUploaderConfig::get_dir();
+ return join_path($dir, self::UPLOADED_DIR);
+ }
+
+ public function get_failed_dir()
+ {
+ $dir = CronUploaderConfig::get_dir();
+ return join_path($dir, self::FAILED_DIR);
+ }
+
+ private function prep_root_dir(): string
+ {
+ // Determine directory (none = default)
+ $dir = CronUploaderConfig::get_dir();
+
+ // Make the directory if it doesn't exist yet
+ if (!is_dir($this->get_queue_dir())) {
+ mkdir($this->get_queue_dir(), 0775, true);
+ }
+ if (!is_dir($this->get_uploaded_dir())) {
+ mkdir($this->get_uploaded_dir(), 0775, true);
+ }
+ if (!is_dir($this->get_failed_dir())) {
+ mkdir($this->get_failed_dir(), 0775, true);
+ }
+
+ return $dir;
+ }
+
+ private function get_lock_file(): string
+ {
+ $root_dir = CronUploaderConfig::get_dir();
+ return join_path($root_dir, ".lock");
+ }
+
+ /**
+ * Uploads the image & handles everything
+ */
+ public function process_upload(string $key, ?int $upload_count = null): bool
+ {
+ global $database;
+
+ if ($key!=CronUploaderConfig::get_key()) {
+ throw new SCoreException("Cron upload key incorrect");
+ }
+ $user_id = CronUploaderConfig::get_user();
+ if (empty($user_id)) {
+ throw new SCoreException("Cron upload user not set");
+ }
+ $my_user = User::by_id($user_id);
+ if ($my_user == null) {
+ throw new SCoreException("No user found for cron upload user $user_id");
+ }
+
+ send_event(new UserLoginEvent($my_user));
+ $this->log_message(SCORE_LOG_INFO, "Logged in as user {$my_user->name}");
+
+ $lockfile = fopen($this->get_lock_file(), "w");
+ if (!flock($lockfile, LOCK_EX | LOCK_NB)) {
+ throw new SCoreException("Cron upload process is already running");
+ }
+
+ try {
+ //set_time_limit(0);
+
+ // Gets amount of imgs to upload
+ if ($upload_count == null) {
+ $upload_count = CronUploaderConfig::get_count();
+ }
+
+ $output_subdir = date('Ymd-His', time());
+ $image_queue = $this->generate_image_queue(CronUploaderConfig::get_dir(), $upload_count);
+
+
+ // Throw exception if there's nothing in the queue
+ if (count($image_queue) == 0) {
+ $this->log_message(SCORE_LOG_WARNING, "Your queue is empty so nothing could be uploaded.");
+ $this->handle_log();
+ return false;
+ }
+
+ // Randomize Images
+ //shuffle($this->image_queue);
+
+ $merged = 0;
+ $added = 0;
+ $failed = 0;
+
+ // Upload the file(s)
+ for ($i = 0; $i < $upload_count && sizeof($image_queue) > 0; $i++) {
+ $img = array_pop($image_queue);
+
+ try {
+ $database->beginTransaction();
+ $this->log_message(SCORE_LOG_INFO, "Adding file: {$img[0]} - tags: {$img[2]}");
+ $result = $this->add_image($img[0], $img[1], $img[2]);
+ $database->commit();
+ $this->move_uploaded($img[0], $img[1], $output_subdir, false);
+ if ($result->merged) {
+ $merged++;
+ } else {
+ $added++;
+ }
+ } catch (Exception $e) {
+ try {
+ $database->rollback();
+ } catch (Exception $e) {
+ }
+
+ $failed++;
+ $this->move_uploaded($img[0], $img[1], $output_subdir, true);
+ $this->log_message(SCORE_LOG_ERROR, "(" . gettype($e) . ") " . $e->getMessage());
+ $this->log_message(SCORE_LOG_ERROR, $e->getTraceAsString());
+ }
+ }
+
+
+ $this->log_message(SCORE_LOG_INFO, "Items added: $added");
+ $this->log_message(SCORE_LOG_INFO, "Items merged: $merged");
+ $this->log_message(SCORE_LOG_INFO, "Items failed: $failed");
+
+
+ // Display upload log
+ $this->handle_log();
+
+ return true;
+ } finally {
+ flock($lockfile, LOCK_UN);
+ fclose($lockfile);
+ }
+ }
+
+ private function move_uploaded(string $path, string $filename, string $output_subdir, bool $corrupt = false)
+ {
+ $relativeDir = dirname(substr($path, strlen(CronUploaderConfig::get_dir()) + 7));
+
+ if ($relativeDir==".") {
+ $relativeDir = "";
+ }
+
+ // Determine which dir to move to
+ if ($corrupt) {
+ // Move to corrupt dir
+ $newDir = join_path($this->get_failed_dir(), $output_subdir, $relativeDir);
+ $info = "ERROR: Image was not uploaded. ";
+ } else {
+ $newDir = join_path($this->get_uploaded_dir(), $output_subdir, $relativeDir);
+ $info = "Image successfully uploaded. ";
+ }
+ $newDir = str_replace(DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, $newDir);
+
+ if (!is_dir($newDir)) {
+ mkdir($newDir, 0775, true);
+ }
+
+ $newFile = join_path($newDir, $filename);
+ // move file to correct dir
+ rename($path, $newFile);
+
+ $this->log_message(SCORE_LOG_INFO, $info . "Image \"$filename\" moved from queue to \"$newDir\".");
+ }
+
+ /**
+ * Generate the necessary DataUploadEvent for a given image and tags.
+ */
+ private function add_image(string $tmpname, string $filename, string $tags): DataUploadEvent
+ {
+ assert(file_exists($tmpname));
+
+ $tagArray = Tag::explode($tags);
+ if (count($tagArray) == 0) {
+ $tagArray[] = "tagme";
+ }
+
+ $pathinfo = pathinfo($filename);
+ $metadata = [];
+ $metadata ['filename'] = $pathinfo ['basename'];
+ if (array_key_exists('extension', $pathinfo)) {
+ $metadata ['extension'] = $pathinfo ['extension'];
+ }
+ $metadata ['tags'] = $tagArray; // doesn't work when not logged in here, handled below
+ $metadata ['source'] = null;
+ $event = new DataUploadEvent($tmpname, $metadata);
+ send_event($event);
+
+ // Generate info message
+ if ($event->image_id == -1) {
+ throw new UploadException("File type not recognised. Filename: {$filename}");
+ } elseif ($event->merged === true) {
+ $infomsg = "Image merged. ID: {$event->image_id} - Filename: {$filename}";
+ } else {
+ $infomsg = "Image uploaded. ID: {$event->image_id} - Filename: {$filename}";
+ }
+ $this->log_message(SCORE_LOG_INFO, $infomsg);
+
+ // Set tags
+ $img = Image::by_id($event->image_id);
+ $img->set_tags(array_merge($tagArray, $img->get_tag_array()));
+
+ return $event;
+ }
+
+ private const PARTIAL_DOWNLOAD_EXTENSIONS = ['crdownload','part'];
+
+ private function is_skippable_file(string $path)
+ {
+ $info = pathinfo($path);
+
+ if (in_array(strtolower($info['extension']), self::PARTIAL_DOWNLOAD_EXTENSIONS)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private function generate_image_queue(string $root_dir, ?int $limit = null): array
+ {
+ $base = $this->get_queue_dir();
+ $output = [];
+
+ if (!is_dir($base)) {
+ $this->log_message(SCORE_LOG_WARNING, "Image Queue Directory could not be found at \"$base\".");
+ return [];
+ }
+
+ $ite = new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS);
+ foreach (new RecursiveIteratorIterator($ite) as $fullpath => $cur) {
+ if (!is_link($fullpath) && !is_dir($fullpath) && !$this->is_skippable_file($fullpath)) {
+ $pathinfo = pathinfo($fullpath);
+
+ $relativePath = substr($fullpath, strlen($base));
+ $tags = path_to_tags($relativePath);
+
+ $img = [
+ 0 => $fullpath,
+ 1 => $pathinfo ["basename"],
+ 2 => $tags
+ ];
+ $output[] = $img;
+ if (!empty($limit) && count($output) >= $limit) {
+ break;
+ }
+ }
+ }
+ return $output;
+ }
+
+
+ private function log_message(int $severity, string $message): void
+ {
+ log_msg(self::NAME, $severity, $message);
+
+ $time = "[" . date('Y-m-d H:i:s') . "]";
+ $this->output_buffer[] = $time . " " . $message;
+
+ $log_path = $this->get_log_file();
+
+ file_put_contents($log_path, $time . " " . $message);
+ }
+
+ private function get_log_file(): string
+ {
+ return join_path(CronUploaderConfig::get_dir(), "uploads.log");
+ }
+
+ /**
+ * This is run at the end to display & save the log.
+ */
+ private function handle_log()
+ {
+ global $page;
+
+ // Display message
+ $page->set_mode(PageMode::DATA);
+ $page->set_type("text/plain");
+ $page->set_data(implode("\r\n", $this->output_buffer));
+ }
}
-
diff --git a/ext/cron_uploader/style.css b/ext/cron_uploader/style.css
new file mode 100644
index 00000000..2643a6a1
--- /dev/null
+++ b/ext/cron_uploader/style.css
@@ -0,0 +1,3 @@
+table.log th {
+ width: 200px;
+}
\ No newline at end of file
diff --git a/ext/cron_uploader/theme.php b/ext/cron_uploader/theme.php
new file mode 100644
index 00000000..ea09f6fb
--- /dev/null
+++ b/ext/cron_uploader/theme.php
@@ -0,0 +1,132 @@
+Information
+
+
+ " . ($running ? "
Cron upload is currently running
" : "") . "
+
+
Directory
+
Files
+
Size (MB)
+
Directory Path
+
+
Queue
+
{$queue_dirinfo['total_files']}
+
{$queue_dirinfo['total_mb']}
+
{$queue_dirinfo['path']}
+
+
Uploaded
+
{$uploaded_dirinfo['total_files']}
+
{$uploaded_dirinfo['total_mb']}
+
{$uploaded_dirinfo['path']}
+
+
Failed
+
{$failed_dirinfo['total_files']}
+
{$failed_dirinfo['total_mb']}
+
{$failed_dirinfo['path']}
+
+
+ Cron Command:
+ Create a cron job with the command above.
+ Read the documentation if you're not sure what to do. ";
+
+ $install_html = "
+ This cron uploader is fairly easy to use but has to be configured first.
+
+
Install & activate this plugin.
+
Go to the Board Config and change any settings to match your preference.
+
Copy the cron command above.
+
Create a cron job or something else that can open a url on specified times.
+ cron is a service that runs commands over and over again on a a schedule. You can set up cron (or any similar tool) to run the command above to trigger the import on whatever schedule you desire.
+ If you're not sure how to do this, you can give the command to your web host and you can ask them to create the cron job for you.
+ When you create the cron job, you choose when to upload new images.
+ ";
+
+ $usage_html = "Upload your images you want to be uploaded to the queue directory using your FTP client or other means.
+ ({$queue_dirinfo['path']})
+
+
Any sub-folders will be turned into tags.
+
If the file name matches \"## - tag1 tag2.png\" the tags will be used.
+
If both are found, they will all be used.
+
The character \";\" will be changed into \":\" in any tags.
+
You can inherit categories by creating a folder that ends with \";\". For instance category;\\tag1 would result in the tag category:tag1. This allows creating a category folder, then creating many subfolders that will use that category.
+
+ The cron uploader works by importing files from the queue folder whenever this url is visited:
+
If an import is already running, another cannot start until it is done.
+
Each time it runs it will import up to ".CronUploaderConfig::get_count()." file(s). This is controlled from Board Config.
+
Uploaded images will be moved to the 'uploaded' directory into a subfolder named after the time the import started. It's recommended that you remove everything out of this directory from time to time. If you have admin controls enabled, this can be done from Board Admin.
+
If you enable the db logging extension, you can view the log output on this screen. Otherwise the log will be written to a file at ".CronUploaderConfig::get_dir().DIRECTORY_SEPARATOR."uploads.log
";
+
+ $html .= make_form(make_link("admin/cron_uploader_clear_queue"), "POST", false, "", "return confirm('Are you sure you want to delete everything in the queue folder?');")
+ ."
"
+ ."
";
+ $html .= make_form(make_link("admin/cron_uploader_clear_uploaded"), "POST", false, "", "return confirm('Are you sure you want to delete everything in the uploaded folder?');")
+ ."
"
+ ."
";
+ $html .= make_form(make_link("admin/cron_uploader_clear_failed"), "POST", false, "", "return confirm('Are you sure you want to delete everything in the failed folder?');")
+ ."
"
+ ."
";
+ $html .= "
\n";
+ $page->add_block(new Block("Cron Upload", $html));
+ }
+}
diff --git a/ext/custom_html_headers/info.php b/ext/custom_html_headers/info.php
new file mode 100644
index 00000000..08f14a36
--- /dev/null
+++ b/ext/custom_html_headers/info.php
@@ -0,0 +1,21 @@
+"support@drudexsoftware.com"];
+ public $license = self::LICENSE_GPLV2;
+ public $description = "Allows admins to modify & set custom <head> content";
+ public $documentation =
+"When you go to board config you can find a block named Custom HTML Headers.
+In that block you can simply place any thing you can place within <head></head>
+
+This can be useful if you want to add website tracking code or other javascript.
+NOTE: Only use if you know what you're doing.
+
+You can also add your website name as prefix or suffix to the title of each page on your website.";
+}
diff --git a/ext/custom_html_headers/main.php b/ext/custom_html_headers/main.php
index df04b436..ffeb938e 100644
--- a/ext/custom_html_headers/main.php
+++ b/ext/custom_html_headers/main.php
@@ -1,72 +1,68 @@
-
- * Link: http://www.drudexsoftware.com
- * License: GPLv2
- * Description: Allows admins to modify & set custom <head> content
- * Documentation:
- * When you go to board config you can find a block named Custom HTML Headers.
- * In that block you can simply place any thing you can place within <head></head>
- *
- * This can be useful if you want to add website tracking code or other javascript.
- * NOTE: Only use if you know what you're doing.
- *
- * You can also add your website name as prefix or suffix to the title of each page on your website.
- */
-class custom_html_headers extends Extension {
+ content
- public function onSetupBuilding(SetupBuildingEvent $event) {
- $sb = new SetupBlock("Custom HTML Headers");
+ public function onSetupBuilding(SetupBuildingEvent $event)
+ {
+ $sb = new SetupBlock("Custom HTML Headers");
- // custom headers
- $sb->add_longtext_option("custom_html_headers",
- "HTML Code to place within <head></head> on all pages ");
+ // custom headers
+ $sb->add_longtext_option(
+ "custom_html_headers",
+ "HTML Code to place within <head></head> on all pages "
+ );
- // modified title
- $sb->add_choice_option("sitename_in_title", array(
- "none" => 0,
- "as prefix" => 1,
- "as suffix" => 2
- ), " Add website name in title");
+ // modified title
+ $sb->add_choice_option("sitename_in_title", [
+ "none" => "none",
+ "as prefix" => "prefix",
+ "as suffix" => "suffix"
+ ], " Add website name in title");
- $event->panel->add_block($sb);
- }
-
- public function onInitExt(InitExtEvent $event) {
- global $config;
- $config->set_default_int("sitename_in_title", 0);
+ $event->panel->add_block($sb);
+ }
+
+ public function onInitExt(InitExtEvent $event)
+ {
+ global $config;
+ $config->set_default_string("sitename_in_title", "none");
+ }
+
+ # Load Analytics tracking code on page request
+ public function onPageRequest(PageRequestEvent $event)
+ {
+ $this->handle_custom_html_headers();
+ $this->handle_modified_page_title();
+ }
+
+ private function handle_custom_html_headers()
+ {
+ global $config, $page;
+
+ $header = $config->get_string('custom_html_headers', '');
+ if ($header!='') {
+ $page->add_html_header($header);
}
-
- # Load Analytics tracking code on page request
- public function onPageRequest(PageRequestEvent $event) {
- $this->handle_custom_html_headers();
- $this->handle_modified_page_title();
+ }
+
+ private function handle_modified_page_title()
+ {
+ global $config, $page;
+
+ // get config values
+ $site_title = $config->get_string(SetupConfig::TITLE);
+ $sitename_in_title = $config->get_string("sitename_in_title");
+
+ // sitename is already in title (can occur on index & other pages)
+ if (strstr($page->title, $site_title)) {
+ return;
}
-
- private function handle_custom_html_headers() {
- global $config, $page;
-
- $header = $config->get_string('custom_html_headers','');
- if ($header!='') $page->add_html_header($header);
- }
-
- private function handle_modified_page_title() {
- global $config, $page;
-
- // get config values
- $site_title = $config->get_string("title");
- $sitename_in_title = $config->get_int("sitename_in_title");
-
- // if feature is enabled & sitename isn't already in title
- // (can occur on index & other pages)
- if ($sitename_in_title != 0 && !strstr($page->title, $site_title))
- {
- if ($sitename_in_title == 1)
- $page->title = "$site_title - $page->title"; // as prefix
- else if ($sitename_in_title == 2)
- $page->title = "$page->title - $site_title"; // as suffix
- }
+
+ if ($sitename_in_title == "prefix") {
+ $page->title = "$site_title - $page->title";
+ } elseif ($sitename_in_title == "suffix") {
+ $page->title = "$page->title - $site_title";
}
+ }
}
-
diff --git a/ext/danbooru_api/info.php b/ext/danbooru_api/info.php
new file mode 100644
index 00000000..0e958d72
--- /dev/null
+++ b/ext/danbooru_api/info.php
@@ -0,0 +1,53 @@
+"jsutinen@gmail.com"];
+ public $description = "Allow Danbooru apps like Danbooru Uploader for Firefox to communicate with Shimmie";
+ public $documentation =
+"
Notes:
+ danbooru API based on documentation from danbooru 1.0 -
+ http://attachr.com/7569
+ I've only been able to test add_post and find_tags because I use the
+ old danbooru firefox extension for firefox 1.5
+
Functions currently implemented:
+
+
add_post - title and rating are currently ignored because shimmie does not support them
+
find_posts - sort of works, filename is returned as the original filename and probably won't help when it comes to actually downloading it
+
find_tags - id, name, and after_id all work but the tags parameter is ignored just like danbooru 1.0 ignores it
+
+
+CHANGELOG
+13-OCT-08 8:00PM CST - JJS
+Bugfix - Properly escape source attribute
+
+17-SEP-08 10:00PM CST - JJS
+Bugfix for changed page name checker in PageRequestEvent
+
+13-APR-08 10:00PM CST - JJS
+Properly escape the tags returned in find_tags and find_posts - Caught by ATravelingGeek
+Updated extension info to be a bit more clear about its purpose
+Deleted add_comment code as it didn't do anything anyway
+
+01-MAR-08 7:00PM CST - JJS
+Rewrote to make it compatible with Shimmie trunk again (r723 at least)
+It may or may not support the new file handling stuff correctly, I'm only testing with images and the danbooru uploader for firefox
+
+21-OCT-07 9:07PM CST - JJS
+Turns out I actually did need to implement the new parameter names
+for danbooru api v1.8.1. Now danbooruup should work when used with /api/danbooru/post/create.xml
+Also correctly redirects the url provided by danbooruup in the event
+of a duplicate image.
+
+19-OCT-07 4:46PM CST - JJS
+Add compatibility with danbooru api v1.8.1 style urls
+for find_posts and add_post. NOTE: This does not implement
+the changes to the parameter names, it is simply a
+workaround for the latest danbooruup firefox extension.
+Completely compatibility will probably involve a rewrite with a different URL
+";
+}
diff --git a/ext/danbooru_api/main.php b/ext/danbooru_api/main.php
index 3bbad680..d85319da 100644
--- a/ext/danbooru_api/main.php
+++ b/ext/danbooru_api/main.php
@@ -1,269 +1,217 @@
-
-Description: Allow Danbooru apps like Danbooru Uploader for Firefox to communicate with Shimmie
-Documentation:
-
Notes:
- danbooru API based on documentation from danbooru 1.0 -
- http://attachr.com/7569
- I've only been able to test add_post and find_tags because I use the
- old danbooru firefox extension for firefox 1.5
-
Functions currently implemented:
-
-
add_post - title and rating are currently ignored because shimmie does not support them
-
find_posts - sort of works, filename is returned as the original filename and probably won't help when it comes to actually downloading it
-
find_tags - id, name, and after_id all work but the tags parameter is ignored just like danbooru 1.0 ignores it
-
+page_matches("api/danbooru")) {
+ global $page;
+ $page->set_mode(PageMode::DATA);
-17-SEP-08 10:00PM CST - JJS
-Bugfix for changed page name checker in PageRequestEvent
+ if ($event->page_matches("api/danbooru/add_post") || $event->page_matches("api/danbooru/post/create.xml")) {
+ // No XML data is returned from this function
+ $page->set_type("text/plain");
+ $this->api_add_post();
+ } elseif ($event->page_matches("api/danbooru/find_posts") || $event->page_matches("api/danbooru/post/index.xml")) {
+ $page->set_type("application/xml");
+ $page->set_data($this->api_find_posts());
+ } elseif ($event->page_matches("api/danbooru/find_tags")) {
+ $page->set_type("application/xml");
+ $page->set_data($this->api_find_tags());
+ }
-13-APR-08 10:00PM CST - JJS
-Properly escape the tags returned in find_tags and find_posts - Caught by ATravelingGeek
-Updated extension info to be a bit more clear about its purpose
-Deleted add_comment code as it didn't do anything anyway
+ // Hackery for danbooruup 0.3.2 providing the wrong view url. This simply redirects to the proper
+ // Shimmie view page
+ // Example: danbooruup says the url is http://shimmie/api/danbooru/post/show/123
+ // This redirects that to http://shimmie/post/view/123
+ elseif ($event->page_matches("api/danbooru/post/show")) {
+ $fixedlocation = make_link("post/view/" . $event->get_arg(0));
+ $page->set_mode(PageMode::REDIRECT);
+ $page->set_redirect($fixedlocation);
+ }
+ }
+ }
-01-MAR-08 7:00PM CST - JJS
-Rewrote to make it compatible with Shimmie trunk again (r723 at least)
-It may or may not support the new file handling stuff correctly, I'm only testing with images and the danbooru uploader for firefox
+ /**
+ * Turns out I use this a couple times so let's make it a utility function
+ * Authenticates a user based on the contents of the login and password parameters
+ * or makes them anonymous. Does not set any cookies or anything permanent.
+ */
+ private function authenticate_user()
+ {
+ global $config, $user;
-21-OCT-07 9:07PM CST - JJS
-Turns out I actually did need to implement the new parameter names
-for danbooru api v1.8.1. Now danbooruup should work when used with /api/danbooru/post/create.xml
-Also correctly redirects the url provided by danbooruup in the event
-of a duplicate image.
+ if (isset($_REQUEST['login']) && isset($_REQUEST['password'])) {
+ // Get this user from the db, if it fails the user becomes anonymous
+ // Code borrowed from /ext/user
+ $name = $_REQUEST['login'];
+ $pass = $_REQUEST['password'];
+ $duser = User::by_name_and_pass($name, $pass);
+ if (!is_null($duser)) {
+ $user = $duser;
+ } else {
+ $user = User::by_id($config->get_int("anon_id", 0));
+ }
+ send_event(new UserLoginEvent($user));
+ }
+ }
-19-OCT-07 4:46PM CST - JJS
-Add compatibility with danbooru api v1.8.1 style urls
-for find_posts and add_post. NOTE: This does not implement
-the changes to the parameter names, it is simply a
-workaround for the latest danbooruup firefox extension.
-Completely compatibility will probably involve a rewrite with a different URL
-
-*/
-
-class DanbooruApi extends Extension {
- public function onPageRequest(PageRequestEvent $event) {
- if($event->page_matches("api") && ($event->get_arg(0) == 'danbooru')) {
- $this->api_danbooru($event);
- }
- }
-
- // Danbooru API
- private function api_danbooru(PageRequestEvent $event) {
- global $page;
- $page->set_mode("data");
-
- if(($event->get_arg(1) == 'add_post') || (($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'create.xml'))) {
- // No XML data is returned from this function
- $page->set_type("text/plain");
- $this->api_add_post();
- }
-
- elseif(($event->get_arg(1) == 'find_posts') || (($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'index.xml'))) {
- $page->set_type("application/xml");
- $page->set_data($this->api_find_posts());
- }
-
- elseif($event->get_arg(1) == 'find_tags') {
- $page->set_type("application/xml");
- $page->set_data($this->api_find_tags());
- }
-
- // Hackery for danbooruup 0.3.2 providing the wrong view url. This simply redirects to the proper
- // Shimmie view page
- // Example: danbooruup says the url is http://shimmie/api/danbooru/post/show/123
- // This redirects that to http://shimmie/post/view/123
- elseif(($event->get_arg(1) == 'post') && ($event->get_arg(2) == 'show')) {
- $fixedlocation = make_link("post/view/" . $event->get_arg(3));
- $page->set_mode("redirect");
- $page->set_redirect($fixedlocation);
- }
- }
-
- /**
- * Turns out I use this a couple times so let's make it a utility function
- * Authenticates a user based on the contents of the login and password parameters
- * or makes them anonymous. Does not set any cookies or anything permanent.
- */
- private function authenticate_user() {
- global $config, $user;
-
- if(isset($_REQUEST['login']) && isset($_REQUEST['password'])) {
- // Get this user from the db, if it fails the user becomes anonymous
- // Code borrowed from /ext/user
- $name = $_REQUEST['login'];
- $pass = $_REQUEST['password'];
- $duser = User::by_name_and_pass($name, $pass);
- if(!is_null($duser)) {
- $user = $duser;
- }
- else {
- $user = User::by_id($config->get_int("anon_id", 0));
- }
- }
- }
-
- /**
+ /**
* find_tags()
- * Find all tags that match the search criteria.
- *
+ * Find all tags that match the search criteria.
+ *
* Parameters
* - id: A comma delimited list of tag id numbers.
* - name: A comma delimited list of tag names.
* - tags: any typical tag query. See Tag#parse_query for details.
* - after_id: limit results to tags with an id number after after_id. Useful if you only want to refresh
- *
- * @return string
- */
- private function api_find_tags() {
- global $database;
- $results = array();
- if(isset($_GET['id'])) {
- $idlist = explode(",", $_GET['id']);
- foreach ($idlist as $id) {
- $sqlresult = $database->get_all(
- "SELECT id,tag,count FROM tags WHERE id = ?",
- array($id));
- foreach ($sqlresult as $row) {
- $results[] = array($row['count'], $row['tag'], $row['id']);
- }
- }
- }
- elseif(isset($_GET['name'])) {
- $namelist = explode(",", $_GET['name']);
- foreach ($namelist as $name) {
- $sqlresult = $database->get_all(
- "SELECT id,tag,count FROM tags WHERE tag = ?",
- array($name));
- foreach ($sqlresult as $row) {
- $results[] = array($row['count'], $row['tag'], $row['id']);
- }
- }
- }
- // Currently disabled to maintain identical functionality to danbooru 1.0's own "broken" find_tags
- elseif(false && isset($_GET['tags'])) {
- $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0;
- $tags = Tag::explode($_GET['tags']);
- }
- else {
- $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0;
- $sqlresult = $database->get_all(
- "SELECT id,tag,count FROM tags WHERE count > 0 AND id >= ? ORDER BY id DESC",
- array($start));
- foreach ($sqlresult as $row) {
- $results[] = array($row['count'], $row['tag'], $row['id']);
- }
- }
+ */
+ private function api_find_tags(): string
+ {
+ global $database;
+ $results = [];
+ if (isset($_GET['id'])) {
+ $idlist = explode(",", $_GET['id']);
+ foreach ($idlist as $id) {
+ $sqlresult = $database->get_all(
+ "SELECT id,tag,count FROM tags WHERE id = :id",
+ ['id'=>$id]
+ );
+ foreach ($sqlresult as $row) {
+ $results[] = [$row['count'], $row['tag'], $row['id']];
+ }
+ }
+ } elseif (isset($_GET['name'])) {
+ $namelist = explode(",", $_GET['name']);
+ foreach ($namelist as $name) {
+ $sqlresult = $database->get_all(
+ "SELECT id,tag,count FROM tags WHERE LOWER(tag) = LOWER(:tag)",
+ ['tag'=>$name]
+ );
+ foreach ($sqlresult as $row) {
+ $results[] = [$row['count'], $row['tag'], $row['id']];
+ }
+ }
+ }
+ // Currently disabled to maintain identical functionality to danbooru 1.0's own "broken" find_tags
+ elseif (false && isset($_GET['tags'])) {
+ $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0;
+ $tags = Tag::explode($_GET['tags']);
+ assert(!is_null($start) && !is_null($tags));
+ } else {
+ $start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0;
+ $sqlresult = $database->get_all(
+ "SELECT id,tag,count FROM tags WHERE count > 0 AND id >= :id ORDER BY id DESC",
+ ['id'=>$start]
+ );
+ foreach ($sqlresult as $row) {
+ $results[] = [$row['count'], $row['tag'], $row['id']];
+ }
+ }
- // Tag results collected, build XML output
- $xml = "\n";
- foreach ($results as $tag) {
- $xml .= xml_tag("tag", array(
- "type" => "0",
- "counts" => $tag[0],
- "name" => $tag[1],
- "id" => $tag[2],
- ));
- }
- $xml .= "";
- return $xml;
- }
+ // Tag results collected, build XML output
+ $xml = "\n";
+ foreach ($results as $tag) {
+ $xml .= xml_tag("tag", [
+ "type" => "0",
+ "counts" => $tag[0],
+ "name" => $tag[1],
+ "id" => $tag[2],
+ ]);
+ }
+ $xml .= "";
+ return $xml;
+ }
- /**
- * find_posts()
- * Find all posts that match the search criteria. Posts will be ordered by id descending.
- *
- * Parameters:
- * - md5: md5 hash to search for (comma delimited)
- * - id: id to search for (comma delimited)
- * - tags: what tags to search for
- * - limit: limit
- * - page: page number
- * - after_id: limit results to posts added after this id
- *
- * @return string
- * @throws SCoreException
- */
- private function api_find_posts() {
- $results = array();
+ /**
+ * find_posts()
+ * Find all posts that match the search criteria. Posts will be ordered by id descending.
+ *
+ * Parameters:
+ * - md5: md5 hash to search for (comma delimited)
+ * - id: id to search for (comma delimited)
+ * - tags: what tags to search for
+ * - limit: limit
+ * - page: page number
+ * - after_id: limit results to posts added after this id
+ *
+ * #return string
+ */
+ private function api_find_posts()
+ {
+ $results = [];
- $this->authenticate_user();
- $start = 0;
+ $this->authenticate_user();
+ $start = 0;
- if(isset($_GET['md5'])) {
- $md5list = explode(",", $_GET['md5']);
- foreach ($md5list as $md5) {
- $results[] = Image::by_hash($md5);
- }
- $count = count($results);
- }
- elseif(isset($_GET['id'])) {
- $idlist = explode(",", $_GET['id']);
- foreach ($idlist as $id) {
- $results[] = Image::by_id($id);
- }
- $count = count($results);
- }
- else {
- $limit = isset($_GET['limit']) ? int_escape($_GET['limit']) : 100;
+ if (isset($_GET['md5'])) {
+ $md5list = explode(",", $_GET['md5']);
+ foreach ($md5list as $md5) {
+ $results[] = Image::by_hash($md5);
+ }
+ $count = count($results);
+ } elseif (isset($_GET['id'])) {
+ $idlist = explode(",", $_GET['id']);
+ foreach ($idlist as $id) {
+ $results[] = Image::by_id(int_escape($id));
+ }
+ $count = count($results);
+ } else {
+ $limit = isset($_GET['limit']) ? int_escape($_GET['limit']) : 100;
- // Calculate start offset.
- if (isset($_GET['page'])) // Danbooru API uses 'page' >= 1
- $start = (int_escape($_GET['page']) - 1) * $limit;
- else if (isset($_GET['pid'])) // Gelbooru API uses 'pid' >= 0
- $start = int_escape($_GET['pid']) * $limit;
- else
- $start = 0;
+ // Calculate start offset.
+ if (isset($_GET['page'])) { // Danbooru API uses 'page' >= 1
+ $start = (int_escape($_GET['page']) - 1) * $limit;
+ } elseif (isset($_GET['pid'])) { // Gelbooru API uses 'pid' >= 0
+ $start = int_escape($_GET['pid']) * $limit;
+ } else {
+ $start = 0;
+ }
- $tags = isset($_GET['tags']) ? Tag::explode($_GET['tags']) : array();
- $count = Image::count_images($tags);
- $results = Image::find_images(max($start, 0), min($limit, 100), $tags);
- }
+ $tags = isset($_GET['tags']) ? Tag::explode($_GET['tags']) : [];
+ $count = Image::count_images($tags);
+ $results = Image::find_images(max($start, 0), min($limit, 100), $tags);
+ }
- // Now we have the array $results filled with Image objects
- // Let's display them
- $xml = "\n";
- foreach ($results as $img) {
- // Sanity check to see if $img is really an image object
- // If it isn't (e.g. someone requested an invalid md5 or id), break out of the this
- if (!is_object($img))
- continue;
- $taglist = $img->get_tag_list();
- $owner = $img->get_owner();
- $previewsize = get_thumbnail_size($img->width, $img->height);
- $xml .= xml_tag("post", array(
- "id" => $img->id,
- "md5" => $img->hash,
- "file_name" => $img->filename,
- "file_url" => $img->get_image_link(),
- "height" => $img->height,
- "width" => $img->width,
- "preview_url" => $img->get_thumb_link(),
- "preview_height" => $previewsize[1],
- "preview_width" => $previewsize[0],
- "rating" => "u",
- "date" => $img->posted,
- "is_warehoused" => false,
- "tags" => $taglist,
- "source" => $img->source,
- "score" => 0,
- "author" => $owner->name
- ));
- }
- $xml .= "";
- return $xml;
- }
+ // Now we have the array $results filled with Image objects
+ // Let's display them
+ $xml = "\n";
+ foreach ($results as $img) {
+ // Sanity check to see if $img is really an image object
+ // If it isn't (e.g. someone requested an invalid md5 or id), break out of the this
+ if (!is_object($img)) {
+ continue;
+ }
+ $taglist = $img->get_tag_list();
+ $owner = $img->get_owner();
+ $previewsize = get_thumbnail_size($img->width, $img->height);
+ $xml .= xml_tag("post", [
+ "id" => $img->id,
+ "md5" => $img->hash,
+ "file_name" => $img->filename,
+ "file_url" => $img->get_image_link(),
+ "height" => $img->height,
+ "width" => $img->width,
+ "preview_url" => $img->get_thumb_link(),
+ "preview_height" => $previewsize[1],
+ "preview_width" => $previewsize[0],
+ "rating" => "?",
+ "date" => $img->posted,
+ "is_warehoused" => false,
+ "tags" => $taglist,
+ "source" => $img->source,
+ "score" => 0,
+ "author" => $owner->name
+ ]);
+ }
+ $xml .= "";
+ return $xml;
+ }
- /**
+ /**
* add_post()
* Adds a post to the database.
- *
+ *
* Parameters:
* - login: login
* - password: password
@@ -273,124 +221,129 @@ class DanbooruApi extends Extension {
* - tags: list of tags as a string, delimited by whitespace
* - md5: MD5 hash of upload in hexadecimal format
* - rating: rating of the post. can be explicit, questionable, or safe. **IGNORED**
- *
+ *
* Notes:
* - The only necessary parameter is tags and either file or source.
* - If you want to sign your post, you need a way to authenticate your account, either by supplying login and password, or by supplying a cookie.
* - If an account is not supplied or if it doesnt authenticate, he post will be added anonymously.
* - If the md5 parameter is supplied and does not match the hash of whats on the server, the post is rejected.
- *
+ *
* Response
* The response depends on the method used:
* Post:
* - X-Danbooru-Location set to the URL for newly uploaded post.
* Get:
* - Redirected to the newly uploaded post.
- */
- private function api_add_post() {
- global $user, $config, $page;
- $danboorup_kludge = 1; // danboorup for firefox makes broken links out of location: /path
+ */
+ private function api_add_post()
+ {
+ global $user, $page;
+ $danboorup_kludge = 1; // danboorup for firefox makes broken links out of location: /path
- // Check first if a login was supplied, if it wasn't check if the user is logged in via cookie
- // If all that fails, it's an anonymous upload
- $this->authenticate_user();
- // Now we check if a file was uploaded or a url was provided to transload
- // Much of this code is borrowed from /ext/upload
+ // Check first if a login was supplied, if it wasn't check if the user is logged in via cookie
+ // If all that fails, it's an anonymous upload
+ $this->authenticate_user();
+ // Now we check if a file was uploaded or a url was provided to transload
+ // Much of this code is borrowed from /ext/upload
- if (!$user->can("create_image")) {
- $page->set_code(409);
- $page->add_http_header("X-Danbooru-Errors: authentication error");
- return;
- }
+ if (!$user->can(Permissions::CREATE_IMAGE)) {
+ $page->set_code(409);
+ $page->add_http_header("X-Danbooru-Errors: authentication error");
+ return;
+ }
- if (isset($_FILES['file'])) { // A file was POST'd in
- $file = $_FILES['file']['tmp_name'];
- $filename = $_FILES['file']['name'];
- // If both a file is posted and a source provided, I'm assuming source is the source of the file
- if (isset($_REQUEST['source']) && !empty($_REQUEST['source'])) {
- $source = $_REQUEST['source'];
- } else {
- $source = null;
- }
- } elseif (isset($_FILES['post'])) {
- $file = $_FILES['post']['tmp_name']['file'];
- $filename = $_FILES['post']['name']['file'];
- if (isset($_REQUEST['post']['source']) && !empty($_REQUEST['post']['source'])) {
- $source = $_REQUEST['post']['source'];
- } else {
- $source = null;
- }
- } elseif (isset($_REQUEST['source']) || isset($_REQUEST['post']['source'])) { // A url was provided
- $source = isset($_REQUEST['source']) ? $_REQUEST['source'] : $_REQUEST['post']['source'];
- $file = tempnam("/tmp", "shimmie_transload");
- $ok = transload($source, $file);
- if (!$ok) {
- $page->set_code(409);
- $page->add_http_header("X-Danbooru-Errors: fopen read error");
- return;
- }
- $filename = basename($source);
- } else { // Nothing was specified at all
- $page->set_code(409);
- $page->add_http_header("X-Danbooru-Errors: no input files");
- return;
- }
+ if (isset($_FILES['file'])) { // A file was POST'd in
+ $file = $_FILES['file']['tmp_name'];
+ $filename = $_FILES['file']['name'];
+ // If both a file is posted and a source provided, I'm assuming source is the source of the file
+ if (isset($_REQUEST['source']) && !empty($_REQUEST['source'])) {
+ $source = $_REQUEST['source'];
+ } else {
+ $source = null;
+ }
+ } elseif (isset($_FILES['post'])) {
+ $file = $_FILES['post']['tmp_name']['file'];
+ $filename = $_FILES['post']['name']['file'];
+ if (isset($_REQUEST['post']['source']) && !empty($_REQUEST['post']['source'])) {
+ $source = $_REQUEST['post']['source'];
+ } else {
+ $source = null;
+ }
+ } elseif (isset($_REQUEST['source']) || isset($_REQUEST['post']['source'])) { // A url was provided
+ $source = isset($_REQUEST['source']) ? $_REQUEST['source'] : $_REQUEST['post']['source'];
+ $file = tempnam("/tmp", "shimmie_transload");
+ $ok = transload($source, $file);
+ if (!$ok) {
+ $page->set_code(409);
+ $page->add_http_header("X-Danbooru-Errors: fopen read error");
+ return;
+ }
+ $filename = basename($source);
+ } else { // Nothing was specified at all
+ $page->set_code(409);
+ $page->add_http_header("X-Danbooru-Errors: no input files");
+ return;
+ }
- // Get tags out of url
- $posttags = Tag::explode(isset($_REQUEST['tags']) ? $_REQUEST['tags'] : $_REQUEST['post']['tags']);
+ // Get tags out of url
+ $posttags = Tag::explode(isset($_REQUEST['tags']) ? $_REQUEST['tags'] : $_REQUEST['post']['tags']);
- // Was an md5 supplied? Does it match the file hash?
- $hash = md5_file($file);
- if (isset($_REQUEST['md5']) && strtolower($_REQUEST['md5']) != $hash) {
- $page->set_code(409);
- $page->add_http_header("X-Danbooru-Errors: md5 mismatch");
- return;
- }
- // Upload size checking is now performed in the upload extension
- // It is also currently broken due to some confusion over file variable ($tmp_filename?)
+ // Was an md5 supplied? Does it match the file hash?
+ $hash = md5_file($file);
+ if (isset($_REQUEST['md5']) && strtolower($_REQUEST['md5']) != $hash) {
+ $page->set_code(409);
+ $page->add_http_header("X-Danbooru-Errors: md5 mismatch");
+ return;
+ }
+ // Upload size checking is now performed in the upload extension
+ // It is also currently broken due to some confusion over file variable ($tmp_filename?)
- // Does it exist already?
- $existing = Image::by_hash($hash);
- if (!is_null($existing)) {
- $page->set_code(409);
- $page->add_http_header("X-Danbooru-Errors: duplicate");
- $existinglink = make_link("post/view/" . $existing->id);
- if ($danboorup_kludge) $existinglink = make_http($existinglink);
- $page->add_http_header("X-Danbooru-Location: $existinglink");
- return;
- }
+ // Does it exist already?
+ $existing = Image::by_hash($hash);
+ if (!is_null($existing)) {
+ $page->set_code(409);
+ $page->add_http_header("X-Danbooru-Errors: duplicate");
+ $existinglink = make_link("post/view/" . $existing->id);
+ if ($danboorup_kludge) {
+ $existinglink = make_http($existinglink);
+ }
+ $page->add_http_header("X-Danbooru-Location: $existinglink");
+ return;
+ }
- // Fire off an event which should process the new file and add it to the db
- $fileinfo = pathinfo($filename);
- $metadata = array();
- $metadata['filename'] = $fileinfo['basename'];
- $metadata['extension'] = $fileinfo['extension'];
- $metadata['tags'] = $posttags;
- $metadata['source'] = $source;
- //log_debug("danbooru_api","========== NEW($filename) =========");
- //log_debug("danbooru_api", "upload($filename): fileinfo(".var_export($fileinfo,TRUE)."), metadata(".var_export($metadata,TRUE).")...");
+ // Fire off an event which should process the new file and add it to the db
+ $fileinfo = pathinfo($filename);
+ $metadata = [];
+ $metadata['filename'] = $fileinfo['basename'];
+ if (array_key_exists('extension', $fileinfo)) {
+ $metadata['extension'] = $fileinfo['extension'];
+ }
+ $metadata['tags'] = $posttags;
+ $metadata['source'] = $source;
+ //log_debug("danbooru_api","========== NEW($filename) =========");
+ //log_debug("danbooru_api", "upload($filename): fileinfo(".var_export($fileinfo,TRUE)."), metadata(".var_export($metadata,TRUE).")...");
- try {
- $nevent = new DataUploadEvent($file, $metadata);
- //log_debug("danbooru_api", "send_event(".var_export($nevent,TRUE).")");
- send_event($nevent);
- // If it went ok, grab the id for the newly uploaded image and pass it in the header
- $newimg = Image::by_hash($hash); // FIXME: Unsupported file doesn't throw an error?
- $newid = make_link("post/view/" . $newimg->id);
- if ($danboorup_kludge) $newid = make_http($newid);
+ try {
+ $nevent = new DataUploadEvent($file, $metadata);
+ //log_debug("danbooru_api", "send_event(".var_export($nevent,TRUE).")");
+ send_event($nevent);
+ // If it went ok, grab the id for the newly uploaded image and pass it in the header
+ $newimg = Image::by_hash($hash); // FIXME: Unsupported file doesn't throw an error?
+ $newid = make_link("post/view/" . $newimg->id);
+ if ($danboorup_kludge) {
+ $newid = make_http($newid);
+ }
- // Did we POST or GET this call?
- if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- $page->add_http_header("X-Danbooru-Location: $newid");
- } else {
- $page->add_http_header("Location: $newid");
- }
- } catch (UploadException $ex) {
- // Did something screw up?
- $page->set_code(409);
- $page->add_http_header("X-Danbooru-Errors: exception - " . $ex->getMessage());
- }
- }
+ // Did we POST or GET this call?
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $page->add_http_header("X-Danbooru-Location: $newid");
+ } else {
+ $page->add_http_header("Location: $newid");
+ }
+ } catch (UploadException $ex) {
+ // Did something screw up?
+ $page->set_code(409);
+ $page->add_http_header("X-Danbooru-Errors: exception - " . $ex->getMessage());
+ }
+ }
}
-
-
diff --git a/ext/danbooru_api/test.php b/ext/danbooru_api/test.php
index 6ea0fef7..f2a82e0a 100644
--- a/ext/danbooru_api/test.php
+++ b/ext/danbooru_api/test.php
@@ -1,23 +1,25 @@
-log_in_as_admin();
+log_in_as_admin();
- $image_id = $this->post_image("tests/bedroom_workshop.jpg", "data");
+ $image_id = $this->post_image("tests/bedroom_workshop.jpg", "data");
- $this->get_page("api/danbooru/find_posts");
- $this->get_page("api/danbooru/find_posts?id=$image_id");
- $this->get_page("api/danbooru/find_posts?md5=17fc89f372ed3636e28bd25cc7f3bac1");
+ $this->get_page("api/danbooru/find_posts");
+ $this->get_page("api/danbooru/find_posts?id=$image_id");
+ $this->get_page("api/danbooru/find_posts?md5=17fc89f372ed3636e28bd25cc7f3bac1");
- $this->get_page("api/danbooru/find_tags");
- $this->get_page("api/danbooru/find_tags?id=1");
- $this->get_page("api/danbooru/find_tags?name=data");
+ $this->get_page("api/danbooru/find_tags");
+ $this->get_page("api/danbooru/find_tags?id=1");
+ $this->get_page("api/danbooru/find_tags?name=data");
- $this->get_page("api/danbooru/post/show/$image_id");
- //$this->assert_response(302); // FIXME
+ $page = $this->get_page("api/danbooru/post/show/$image_id");
+ $this->assertEquals(302, $page->code);
- $this->get_page("post/list/md5:17fc89f372ed3636e28bd25cc7f3bac1/1");
- //$this->assert_title(new PatternExpectation("/^Image \d+: data/"));
- //$this->click("Delete");
- }
+ $this->get_page("post/list/md5:17fc89f372ed3636e28bd25cc7f3bac1/1");
+ //$this->assert_title(new PatternExpectation("/^Image \d+: data/"));
+ //$this->click("Delete");
+ }
}
diff --git a/ext/downtime/info.php b/ext/downtime/info.php
new file mode 100644
index 00000000..a47dc7f0
--- /dev/null
+++ b/ext/downtime/info.php
@@ -0,0 +1,18 @@
+
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: Show a "down for maintenance" page
- * Documentation:
- * Once installed there will be some more options on the config page --
- * Ticking "disable non-admin access" will mean that regular and anonymous
- * users will be blocked from accessing the site, only able to view the
- * message specified in the box.
- */
+add_bool_option("downtime", "Disable non-admin access: ");
- $sb->add_longtext_option("downtime_message", " ");
- $event->panel->add_block($sb);
- }
+ public function get_priority(): int
+ {
+ return 10;
+ }
- public function onPageRequest(PageRequestEvent $event) {
- global $config, $page, $user;
+ public function onSetupBuilding(SetupBuildingEvent $event)
+ {
+ $sb = new SetupBlock("Downtime");
+ $sb->add_bool_option("downtime", "Disable non-admin access: ");
+ $sb->add_longtext_option("downtime_message", " ");
+ $event->panel->add_block($sb);
+ }
- if($config->get_bool("downtime")) {
- if(!$user->can("ignore_downtime") && !$this->is_safe_page($event)) {
- $msg = $config->get_string("downtime_message");
- $this->theme->display_message($msg);
- if(!defined("UNITTEST")) { // hax D:
- header("HTTP/1.0 {$page->code} Downtime");
- print($page->data);
- exit;
- }
- }
- $this->theme->display_notification($page);
- }
- }
+ public function onPageRequest(PageRequestEvent $event)
+ {
+ global $config, $page, $user;
- private function is_safe_page(PageRequestEvent $event) {
- if($event->page_matches("user_admin/login")) return true;
- else return false;
- }
+ if ($config->get_bool("downtime")) {
+ if (!$user->can(Permissions::IGNORE_DOWNTIME) && !$this->is_safe_page($event)) {
+ $msg = $config->get_string("downtime_message");
+ $this->theme->display_message($msg);
+ if (!defined("UNITTEST")) { // hax D:
+ header("HTTP/1.0 {$page->code} Downtime");
+ print($page->data);
+ exit;
+ }
+ }
+ $this->theme->display_notification($page);
+ }
+ }
+
+ private function is_safe_page(PageRequestEvent $event)
+ {
+ if ($event->page_matches("user_admin/login")) {
+ return true;
+ } else {
+ return false;
+ }
+ }
}
diff --git a/ext/downtime/test.php b/ext/downtime/test.php
index 4331e27f..f2184e34 100644
--- a/ext/downtime/test.php
+++ b/ext/downtime/test.php
@@ -1,39 +1,42 @@
-set_bool("downtime", false);
- }
+set_bool("downtime", false);
+ }
- public function testDowntime() {
- global $config;
+ public function testDowntime()
+ {
+ global $config;
- $config->set_string("downtime_message", "brb, unit testing");
+ $config->set_string("downtime_message", "brb, unit testing");
- // downtime on
- $config->set_bool("downtime", true);
+ // downtime on
+ $config->set_bool("downtime", true);
- $this->log_in_as_admin();
- $this->get_page("post/list");
- $this->assert_text("DOWNTIME MODE IS ON!");
- $this->assert_response(200);
+ $this->log_in_as_admin();
+ $this->get_page("post/list");
+ $this->assert_text("DOWNTIME MODE IS ON!");
+ $this->assert_response(200);
- $this->log_in_as_user();
- $this->get_page("post/list");
- $this->assert_content("brb, unit testing");
- $this->assert_response(503);
+ $this->log_in_as_user();
+ $this->get_page("post/list");
+ $this->assert_content("brb, unit testing");
+ $this->assert_response(503);
- // downtime off
- $config->set_bool("downtime", false);
+ // downtime off
+ $config->set_bool("downtime", false);
- $this->log_in_as_admin();
- $this->get_page("post/list");
- $this->assert_no_text("DOWNTIME MODE IS ON!");
- $this->assert_response(200);
+ $this->log_in_as_admin();
+ $this->get_page("post/list");
+ $this->assert_no_text("DOWNTIME MODE IS ON!");
+ $this->assert_response(200);
- $this->log_in_as_user();
- $this->get_page("post/list");
- $this->assert_no_content("brb, unit testing");
- $this->assert_response(200);
- }
+ $this->log_in_as_user();
+ $this->get_page("post/list");
+ $this->assert_no_content("brb, unit testing");
+ $this->assert_response(200);
+ }
}
diff --git a/ext/downtime/theme.php b/ext/downtime/theme.php
index 965b10b2..c1a752bc 100644
--- a/ext/downtime/theme.php
+++ b/ext/downtime/theme.php
@@ -1,32 +1,36 @@
-add_block(new Block("Downtime",
- "
DOWNTIME MODE IS ON!
", "left", 0));
- }
+class DowntimeTheme extends Themelet
+{
+ /**
+ * Show the admin that downtime mode is enabled
+ */
+ public function display_notification(Page $page)
+ {
+ $page->add_block(new Block(
+ "Downtime",
+ "DOWNTIME MODE IS ON!",
+ "left",
+ 0
+ ));
+ }
- /**
- * Display $message and exit
- *
- * @param string $message
- */
- public function display_message(/*string*/ $message) {
- global $config, $user, $page;
- $theme_name = $config->get_string('theme');
- $data_href = get_base_href();
- $login_link = make_link("user_admin/login");
- $auth = $user->get_auth_html();
+ /**
+ * Display $message and exit
+ */
+ public function display_message(string $message)
+ {
+ global $config, $user, $page;
+ $theme_name = $config->get_string(SetupConfig::THEME);
+ $data_href = get_base_href();
+ $login_link = make_link("user_admin/login");
+ $auth = $user->get_auth_html();
- $page->set_mode('data');
- $page->set_code(503);
- $page->set_data(<<
+ $page->set_mode(PageMode::DATA);
+ $page->set_code(503);
+ $page->set_data(
+ <<Downtime
@@ -34,7 +38,7 @@ class DowntimeTheme extends Themelet {
-
Down for Maintenance
+
Down for Maintenance
$message
@@ -62,6 +66,6 @@ class DowntimeTheme extends Themelet {
EOD
-);
- }
+ );
+ }
}
diff --git a/ext/emoticons/info.php b/ext/emoticons/info.php
new file mode 100644
index 00000000..eadcbf67
--- /dev/null
+++ b/ext/emoticons/info.php
@@ -0,0 +1,20 @@
+Images are stored in /ext/emoticons/default/, and you can
+add more emoticons by uploading images into that folder.";
+}
diff --git a/ext/emoticons/main.php b/ext/emoticons/main.php
index e6245daf..38563341 100644
--- a/ext/emoticons/main.php
+++ b/ext/emoticons/main.php
@@ -1,49 +1,20 @@
-
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: Lets users use graphical smilies
- * Documentation:
- * This extension will turn colon-something-colon into a link
- * to an image with that something as the name, eg :smile:
- * becomes a link to smile.gif
- *
Images are stored in /ext/emoticons/default/, and you can
- * add more emoticons by uploading images into that folder.
- */
+", $text);
- return $text;
- }
+class Emoticons extends FormatterExtension
+{
+ public function format(string $text): string
+ {
+ $data_href = get_base_href();
+ $text = preg_replace("/:([a-z]*?):/s", "", $text);
+ return $text;
+ }
- /**
- * @param string $text
- * @return string
- */
- public function strip(/*string*/ $text) {
- return $text;
- }
+ public function strip(string $text): string
+ {
+ return $text;
+ }
}
-
-/**
- * Class EmoticonList
- */
-class EmoticonList extends Extension {
- public function onPageRequest(PageRequestEvent $event) {
- if($event->page_matches("emote/list")) {
- $this->theme->display_emotes(glob("ext/emoticons/default/*"));
- }
- }
-}
-
diff --git a/ext/emoticons/test.php b/ext/emoticons/test.php
index bc4a8af9..47df8ac4 100644
--- a/ext/emoticons/test.php
+++ b/ext/emoticons/test.php
@@ -1,19 +1,20 @@
-log_in_as_user();
- $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot");
+ $this->log_in_as_user();
+ $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot");
- send_event(new CommentPostingEvent($image_id, $user, ":cool: :beans:"));
+ send_event(new CommentPostingEvent($image_id, $user, ":cool: :beans:"));
- $this->get_page("post/view/$image_id");
- $this->assert_no_text(":cool:"); # FIXME: test for working image link
- //$this->assert_text(":beans:"); # FIXME: this should be left as-is
+ $this->get_page("post/view/$image_id");
+ $this->assert_no_text(":cool:"); # FIXME: test for working image link
+ //$this->assert_text(":beans:"); # FIXME: this should be left as-is
- $this->get_page("emote/list");
- //$this->assert_text(":arrow:");
- }
+ $this->get_page("emote/list");
+ //$this->assert_text(":arrow:");
+ }
}
-
diff --git a/ext/emoticons/theme.php b/ext/emoticons/theme.php
deleted file mode 100644
index 07f033dd..00000000
--- a/ext/emoticons/theme.php
+++ /dev/null
@@ -1,24 +0,0 @@
-