diff --git a/core/_install.php b/core/_install.php
index a37553da..3f799d46 100644
--- a/core/_install.php
+++ b/core/_install.php
@@ -74,8 +74,7 @@ if (is_readable("data/config/shimmie.conf.php")) {
do_install();
-// utilities {{{
- // TODO: Can some of these be pushed into "core/???.inc.php" ?
+// TODO: Can some of these be pushed into "core/???.inc.php" ?
function check_gd_version(): int
{
@@ -99,10 +98,9 @@ function check_im_version(): int
return (empty($convert_check) ? 0 : 1);
}
-// }}}
function do_install()
-{ // {{{
+{
if (file_exists("data/config/auto_install.conf.php")) {
require_once "data/config/auto_install.conf.php";
} elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) {
@@ -118,10 +116,10 @@ function do_install()
define("CACHE_DSN", null);
define("DATABASE_KA", true);
install_process();
-} // }}}
+}
function ask_questions()
-{ // {{{
+{
$warnings = [];
$errors = [];
@@ -232,21 +230,21 @@ function ask_questions()
EOD;
-} // }}}
+}
/**
* This is where the install really takes place.
*/
function install_process()
-{ // {{{
+{
build_dirs();
create_tables();
insert_defaults();
write_config();
-} // }}}
+}
function create_tables()
-{ // {{{
+{
try {
$db = new Database();
@@ -331,10 +329,10 @@ EOD;
} catch (Exception $e) {
handle_db_errors(false, "An unknown error occurred while trying to insert data into the database.", $e->getMessage(), 4);
}
-} // }}}
+}
function insert_defaults()
-{ // {{{
+{
try {
$db = new Database();
@@ -350,10 +348,10 @@ function insert_defaults()
} catch (Exception $e) {
handle_db_errors(false, "An unknown error occurred while trying to insert data into the database.", $e->getMessage(), 6);
}
-} // }}}
+}
function build_dirs()
-{ // {{{
+{
$data_exists = file_exists("data") || mkdir("data");
$data_writable = is_writable("data") || chmod("data", 0755);
@@ -373,10 +371,10 @@ function build_dirs()
";
exit(7);
}
-} // }}}
+}
function write_config()
-{ // {{{
+{
$file_content = '<' . '?php' . "\n" .
"define('DATABASE_DSN', '".DATABASE_DSN."');\n" .
'?' . '>';
@@ -417,7 +415,7 @@ EOD;
EOD;
}
echo "\n";
-} // }}}
+}
function handle_db_errors(bool $isPDO, string $errorMessage1, string $errorMessage2, int $exitCode)
{
diff --git a/core/database.php b/core/database.php
index 3c06c5a3..535548a7 100644
--- a/core/database.php
+++ b/core/database.php
@@ -178,9 +178,18 @@ class Database
$this->dbtime += $dur;
}
- public function execute(string $query, array $args=[]): PDOStatement
+ public function set_timeout(int $time): void
+ {
+ $this->engine->set_timeout($this->db, $time);
+ }
+
+ public function execute(string $query, array $args=[], bool $scoreql = false): PDOStatement
{
try {
+ if ($scoreql===true) {
+ $query = $this->scoreql_to_sql($query);
+ }
+
if (is_null($this->db)) {
$this->connect_db();
}
@@ -211,8 +220,12 @@ class Database
/**
* Execute an SQL query and return a 2D array.
*/
- public function get_all(string $query, array $args=[]): array
+ public function get_all(string $query, array $args=[], bool $scoreql = false): array
{
+ if ($scoreql===true) {
+ $query = $this->scoreql_to_sql($query);
+ }
+
$_start = microtime(true);
$data = $this->execute($query, $args)->fetchAll();
$this->count_time("get_all", $_start, $query, $args);
@@ -222,8 +235,11 @@ class Database
/**
* Execute an SQL query and return a iterable object for use with generators.
*/
- public function get_all_iterable(string $query, array $args=[]): PDOStatement
+ public function get_all_iterable(string $query, array $args=[], bool $scoreql = false): PDOStatement
{
+ if ($scoreql===true) {
+ $query = $this->scoreql_to_sql($query);
+ }
$_start = microtime(true);
$data = $this->execute($query, $args);
$this->count_time("get_all_iterable", $_start, $query, $args);
@@ -233,19 +249,40 @@ class Database
/**
* Execute an SQL query and return a single row.
*/
- public function get_row(string $query, array $args=[]): ?array
+ public function get_row(string $query, array $args=[], bool $scoreql = false): ?array
{
+ if ($scoreql===true) {
+ $query = $this->scoreql_to_sql($query);
+ }
$_start = microtime(true);
$row = $this->execute($query, $args)->fetch();
$this->count_time("get_row", $_start, $query, $args);
return $row ? $row : null;
}
+
+ /**
+ * Execute an SQL query and return a boolean based on whether it returns a result
+ */
+ public function exists(string $query, array $args=[], bool $scoreql = false): bool
+ {
+ if ($scoreql===true) {
+ $query = $this->scoreql_to_sql($query);
+ }
+ $_start = microtime(true);
+ $result = $this->execute($query, $args);
+ $this->count_time("exists", $_start, $query, $args);
+ return $result->rowCount()>0;
+ }
+
/**
* Execute an SQL query and return the first column of each row.
*/
- public function get_col(string $query, array $args=[]): array
+ public function get_col(string $query, array $args=[], bool $scoreql = false): array
{
+ if ($scoreql===true) {
+ $query = $this->scoreql_to_sql($query);
+ }
$_start = microtime(true);
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_COLUMN);
$this->count_time("get_col", $_start, $query, $args);
@@ -255,8 +292,11 @@ class Database
/**
* Execute an SQL query and return the first column of each row as a single iterable object.
*/
- public function get_col_iterable(string $query, array $args=[]): Generator
+ public function get_col_iterable(string $query, array $args=[], bool $scoreql = false): Generator
{
+ if ($scoreql===true) {
+ $query = $this->scoreql_to_sql($query);
+ }
$_start = microtime(true);
$stmt = $this->execute($query, $args);
$this->count_time("get_col_iterable", $_start, $query, $args);
@@ -268,8 +308,11 @@ class Database
/**
* Execute an SQL query and return the the first column => the second column.
*/
- public function get_pairs(string $query, array $args=[]): array
+ public function get_pairs(string $query, array $args=[], bool $scoreql = false): array
{
+ if ($scoreql===true) {
+ $query = $this->scoreql_to_sql($query);
+ }
$_start = microtime(true);
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_KEY_PAIR);
$this->count_time("get_pairs", $_start, $query, $args);
@@ -279,8 +322,11 @@ class Database
/**
* Execute an SQL query and return a single value.
*/
- public function get_one(string $query, array $args=[])
+ public function get_one(string $query, array $args=[], bool $scoreql = false)
{
+ if ($scoreql===true) {
+ $query = $this->scoreql_to_sql($query);
+ }
$_start = microtime(true);
$row = $this->execute($query, $args)->fetch();
$this->count_time("get_one", $_start, $query, $args);
@@ -354,7 +400,7 @@ class MockDatabase extends Database
$this->responses = $responses;
}
- public function execute(string $query, array $params=[]): PDOStatement
+ public function execute(string $query, array $params=[], bool $scoreql = false): PDOStatement
{
log_debug(
"mock-database",
@@ -376,23 +422,23 @@ class MockDatabase extends Database
return $this->responses[$this->query_id++];
}
- public function get_all(string $query, array $args=[]): array
+ public function get_all(string $query, array $args=[], bool $scoreql = false): array
{
return $this->_execute($query, $args);
}
- public function get_row(string $query, array $args=[]): ?array
+ public function get_row(string $query, array $args=[], bool $scoreql = false): ?array
{
return $this->_execute($query, $args);
}
- public function get_col(string $query, array $args=[]): array
+ public function get_col(string $query, array $args=[], bool $scoreql = false): array
{
return $this->_execute($query, $args);
}
- public function get_pairs(string $query, array $args=[]): array
+ public function get_pairs(string $query, array $args=[], bool $scoreql = false): array
{
return $this->_execute($query, $args);
}
- public function get_one(string $query, array $args=[])
+ public function get_one(string $query, array $args=[], bool $scoreql = false)
{
return $this->_execute($query, $args);
}
diff --git a/core/dbengine.php b/core/dbengine.php
index 03b4b851..86b1a9d6 100644
--- a/core/dbengine.php
+++ b/core/dbengine.php
@@ -12,7 +12,7 @@ abstract class SCORE
const ILIKE = "SCORE_ILIKE";
}
-class DBEngine
+abstract class DBEngine
{
/** @var null|string */
public $name = null;
@@ -33,6 +33,8 @@ class DBEngine
{
return 'CREATE TABLE '.$name.' ('.$data.')';
}
+
+ abstract public function set_timeout(PDO $db, int $time);
}
class MySQL extends DBEngine
@@ -68,6 +70,12 @@ class MySQL extends DBEngine
$ctes = "ENGINE=InnoDB DEFAULT CHARSET='utf8'";
return 'CREATE TABLE '.$name.' ('.$data.') '.$ctes;
}
+
+ public function set_timeout(PDO $db, int $time): void
+ {
+ // These only apply to read-only queries, which appears to be the best we can to mysql-wise
+ $db->exec("SET SESSION MAX_EXECUTION_TIME=".$time.";");
+ }
}
class PostgreSQL extends DBEngine
@@ -87,7 +95,7 @@ class PostgreSQL extends DBEngine
} else {
$db->exec("SET application_name TO 'shimmie [local]';");
}
- $db->exec("SET statement_timeout TO ".DATABASE_TIMEOUT.";");
+ $this->set_timeout($db, DATABASE_TIMEOUT);
}
public function scoreql_to_sql(string $data): string
@@ -109,6 +117,11 @@ class PostgreSQL extends DBEngine
$data = $this->scoreql_to_sql($data);
return "CREATE TABLE $name ($data)";
}
+
+ public function set_timeout(PDO $db, int $time): void
+ {
+ $db->exec("SET statement_timeout TO ".$time.";");
+ }
}
// shimmie functions for export to sqlite
@@ -213,4 +226,9 @@ class SQLite extends DBEngine
$cols_redone = implode(", ", $cols);
return "CREATE TABLE $name ($cols_redone); $extras";
}
+
+ public function set_timeout(PDO $db, int $time): void
+ {
+ // There doesn't seem to be such a thing for SQLite, so it does nothing
+ }
}
diff --git a/core/event.php b/core/event.php
index 5ae1ad55..248bf6ae 100644
--- a/core/event.php
+++ b/core/event.php
@@ -154,7 +154,7 @@ class PageRequestEvent extends Event
public function get_page_size(): int
{
global $config;
- return $config->get_int('index_images');
+ return $config->get_int(IndexConfig::IMAGES);
}
}
diff --git a/core/imageboard/image.php b/core/imageboard/image.php
index 4277443c..7a66ec04 100644
--- a/core/imageboard/image.php
+++ b/core/imageboard/image.php
@@ -115,7 +115,7 @@ class Image
if ($max < 1) {
return null;
} // From Issue #22 - opened by HungryFeline on May 30, 2011.
- if ($max > $limit_range) {
+ if ($limit_range > 0 && $max > $limit_range) {
$max = $limit_range;
}
$rand = mt_rand(0, $max-1);
@@ -150,7 +150,7 @@ class Image
$result = Image::get_accelerated_result($tag_conditions, $img_conditions, $start, $limit);
if (!$result) {
$querylet = Image::build_search_querylet($tag_conditions, $img_conditions);
- $querylet->append(new Querylet(" ORDER BY ".(Image::$order_sql ?: "images.".$config->get_string("index_order"))));
+ $querylet->append(new Querylet(" ORDER BY ".(Image::$order_sql ?: "images.".$config->get_string(IndexConfig::ORDER))));
if ($limit!=null) {
$querylet->append(new Querylet(" LIMIT :limit ", ["limit" => $limit]));
$querylet->append(new Querylet(" OFFSET :offset ", ["offset"=>$start]));
@@ -334,7 +334,7 @@ class Image
public static function count_pages(array $tags=[]): float
{
global $config;
- return ceil(Image::count_images($tags) / $config->get_int('index_images'));
+ return ceil(Image::count_images($tags) / $config->get_int(IndexConfig::IMAGES));
}
private static function terms_to_conditions(array $terms): array
@@ -731,8 +731,10 @@ class Image
["tag"=>$tag]
);
$database->execute(
+ $database->scoreql_to_sql(
"INSERT INTO image_tags(image_id, tag_id)
- VALUES(:id, (SELECT id FROM tags WHERE tag = :tag))",
+ VALUES(:id, (SELECT id FROM tags WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(:tag)))"
+ ),
["id"=>$this->id, "tag"=>$tag]
);
} else {
diff --git a/core/imageboard/tag.php b/core/imageboard/tag.php
index 7fa8b0b7..0deeeef7 100644
--- a/core/imageboard/tag.php
+++ b/core/imageboard/tag.php
@@ -29,23 +29,7 @@ class Tag
$tags = explode(' ', trim($tags));
/* sanitise by removing invisible / dodgy characters */
- $tag_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;
- }
- }
+ $tag_array = self::sanitize_array($tags);
/* if user supplied a blank string, add "tagme" */
if (count($tag_array) === 0 && $tagme) {
@@ -101,6 +85,74 @@ class Tag
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 (mb_strlen($tag, 'UTF-8') > 255) {
+ throw new Exception("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
+ {
+ $tag_array = [];
+ foreach ($tags as $tag) {
+ try {
+ $tag = Tag::sanitize($tag);
+ } catch (Exception $e) {
+ flash_message($e->getMessage());
+ continue;
+ }
+
+ if (!empty($tag)) {
+ $tag_array[] = $tag;
+ }
+ }
+ return $tag_array;
+ }
+
+
public static function sqlify(string $term): string
{
global $database;
diff --git a/core/permissions.php b/core/permissions.php
index bd60b0c0..84eb292e 100644
--- a/core/permissions.php
+++ b/core/permissions.php
@@ -80,4 +80,7 @@ abstract class Permissions
public const NOTES_ADMIN = "notes_admin";
public const POOLS_ADMIN = "pools_admin";
public const TIPS_ADMIN = "tips_admin";
+ public const CRON_ADMIN = "cron_admin";
+ public const APPROVE_IMAGE = "approve_image";
+ public const APPROVE_COMMENT = "approve_comment";
}
diff --git a/core/polyfills.php b/core/polyfills.php
index 87e739e3..b5ec84c8 100644
--- a/core/polyfills.php
+++ b/core/polyfills.php
@@ -502,7 +502,7 @@ function bool_escape($input): bool
*/
if (is_bool($input)) {
return $input;
- } elseif (is_int($input)) {
+ } elseif (is_numeric($input)) {
return ($input === 1);
} else {
$value = filter_var($input, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
diff --git a/core/userclass.php b/core/userclass.php
index 5c60c9a0..f474d154 100644
--- a/core/userclass.php
+++ b/core/userclass.php
@@ -150,6 +150,10 @@ new UserClass("base", null, [
Permissions::NOTES_ADMIN => false,
Permissions::POOLS_ADMIN => false,
Permissions::TIPS_ADMIN => false,
+ Permissions::CRON_ADMIN => false,
+
+ Permissions::APPROVE_IMAGE => false,
+ Permissions::APPROVE_COMMENT => false,
]);
new UserClass("anonymous", "base", [
@@ -226,6 +230,9 @@ new UserClass("admin", "base", [
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", [
diff --git a/core/util.php b/core/util.php
index 70b5f2ea..44f05c6c 100644
--- a/core/util.php
+++ b/core/util.php
@@ -350,6 +350,54 @@ function join_url(string $base, string ...$paths)
return $output;
}
+function get_dir_contents(string $dir): array
+{
+ if (empty($dir)) {
+ throw new Exception("dir required");
+ }
+ if (!is_dir($dir)) {
+ return [];
+ }
+ $results = array_diff(
+ scandir(
+ $dir
+ ),
+ ['..', '.']
+ );
+
+ return $results;
+}
+
+/**
+ * Returns amount of files & total size of dir.
+ */
+function scan_dir(string $path): array
+{
+ $bytestotal = 0;
+ $nbfiles = 0;
+
+ $ite = new RecursiveDirectoryIterator(
+ $path,
+ FilesystemIterator::KEY_AS_PATHNAME |
+ FilesystemIterator::CURRENT_AS_FILEINFO |
+ FilesystemIterator::SKIP_DOTS
+ );
+ foreach (new RecursiveIteratorIterator($ite) as $filename => $cur) {
+ try {
+ $filesize = $cur->getSize();
+ $bytestotal += $filesize;
+ $nbfiles++;
+ } catch (RuntimeException $e) {
+ // This usually just means that the file got eaten by the import
+ continue;
+ }
+ }
+
+ $size_mb = $bytestotal / 1048576; // to mb
+ $size_mb = number_format($size_mb, 2, '.', '');
+ return ['path' => $path, 'total_files' => $nbfiles, 'total_mb' => $size_mb];
+}
+
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* Debugging functions *
diff --git a/ext/admin/info.php b/ext/admin/info.php
index 84db9927..bdaafa4a 100644
--- a/ext/admin/info.php
+++ b/ext/admin/info.php
@@ -1,15 +1,5 @@
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: Various things to make admins' lives easier
- * Documentation:
-
- */
-
class AdminPageInfo extends ExtensionInfo
{
public const KEY = "admin";
diff --git a/ext/alias_editor/info.php b/ext/alias_editor/info.php
index 4990ac6d..b846b815 100644
--- a/ext/alias_editor/info.php
+++ b/ext/alias_editor/info.php
@@ -1,14 +1,5 @@
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: Edit the alias list
- * Documentation:
- */
-
class AliasEditorInfo extends ExtensionInfo
{
public const KEY = "alias_editor";
diff --git a/ext/approval/info.php b/ext/approval/info.php
new file mode 100644
index 00000000..c2af31cd
--- /dev/null
+++ b/ext/approval/info.php
@@ -0,0 +1,13 @@
+"matthew@darkholme.net"];
+ public $license = self::LICENSE_WTFPL;
+ public $description = "Adds an approval step to the upload/import process.";
+ public $db_support = [DatabaseDriver::MYSQL, DatabaseDriver::PGSQL];
+}
diff --git a/ext/approval/main.php b/ext/approval/main.php
new file mode 100644
index 00000000..f012b2e0
--- /dev/null
+++ b/ext/approval/main.php
@@ -0,0 +1,260 @@
+set_default_bool(ApprovalConfig::IMAGES, false);
+ $config->set_default_bool(ApprovalConfig::COMMENTS, false);
+
+ if ($config->get_int(ApprovalConfig::VERSION) < 1) {
+ $this->install();
+ }
+ }
+
+ public function onPageRequest(PageRequestEvent $event)
+ {
+ global $page, $user;
+
+ if ($event->page_matches("approve_image") && $user->can(Permissions::APPROVE_IMAGE)) {
+ // Try to get the image ID
+ $image_id = int_escape($event->get_arg(0));
+ if (empty($image_id)) {
+ $image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
+ }
+ if (empty($image_id)) {
+ throw new SCoreException("Can not approve image: No valid Image ID given.");
+ }
+
+ self::approve_image($image_id);
+ $page->set_mode(PageMode::REDIRECT);
+ $page->set_redirect(make_link("post/view/" . $image_id));
+ }
+
+ if ($event->page_matches("disapprove_image") && $user->can(Permissions::APPROVE_IMAGE)) {
+ // Try to get the image ID
+ $image_id = int_escape($event->get_arg(0));
+ if (empty($image_id)) {
+ $image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
+ }
+ if (empty($image_id)) {
+ throw new SCoreException("Can not disapprove image: No valid Image ID given.");
+ }
+
+ self::disapprove_image($image_id);
+ $page->set_mode(PageMode::REDIRECT);
+ $page->set_redirect(make_link("post/view/".$image_id));
+ }
+ }
+
+ public function onSetupBuilding(SetupBuildingEvent $event)
+ {
+ $this->theme->display_admin_block($event);
+ }
+
+ public function onAdminBuilding(AdminBuildingEvent $event)
+ {
+ global $config;
+
+ $this->theme->display_admin_form();
+ }
+
+ public function onAdminAction(AdminActionEvent $event)
+ {
+ global $database, $user;
+
+ $action = $event->action;
+ $event->redirect = true;
+ if ($action==="approval") {
+ $approval_action = $_POST["approval_action"];
+ switch ($approval_action) {
+ case "approve_all":
+ $database->set_timeout(300000); // These updates can take a little bit
+ $database->execute(
+ $database->scoreql_to_sql(
+ "UPDATE images SET approved = SCORE_BOOL_Y, approved_by_id = :approved_by_id WHERE approved = SCORE_BOOL_N"
+ ),
+ ["approved_by_id"=>$user->id]
+ );
+ break;
+ case "disapprove_all":
+ $database->set_timeout(300000); // These updates can take a little bit
+ $database->execute($database->scoreql_to_sql(
+ "UPDATE images SET approved = SCORE_BOOL_N, approved_by_id = NULL WHERE approved = SCORE_BOOL_Y"
+ ));
+ break;
+ default:
+
+ break;
+ }
+ }
+ }
+
+ public function onDisplayingImage(DisplayingImageEvent $event)
+ {
+ global $user, $page, $config;
+
+ if ($config->get_bool(ApprovalConfig::IMAGES) && $event->image->approved===false && !$user->can(Permissions::APPROVE_IMAGE)) {
+ $page->set_mode(PageMode::REDIRECT);
+ $page->set_redirect(make_link("post/list"));
+ }
+ }
+
+ public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
+ {
+ global $user;
+ if ($event->parent=="posts") {
+ if ($user->can(Permissions::APPROVE_IMAGE)) {
+ $event->add_nav_link("posts_unapproved", new Link('/post/list/approved%3Ano/1'), "Pending Approval", null, 60);
+ }
+ }
+ }
+
+
+ const SEARCH_REGEXP = "/^approved:(yes|no)/";
+ public function onSearchTermParse(SearchTermParseEvent $event)
+ {
+ global $user, $database, $config;
+
+ if ($config->get_bool(ApprovalConfig::IMAGES)) {
+ $matches = [];
+
+ if (is_null($event->term) && $this->no_approval_query($event->context)) {
+ $event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
+ }
+
+
+ if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) {
+ if ($user->can(Permissions::APPROVE_IMAGE) && $matches[1] == "no") {
+ $event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_N ")));
+ } else {
+ $event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
+ }
+ }
+ }
+ }
+
+ public function onHelpPageBuilding(HelpPageBuildingEvent $event)
+ {
+ global $user, $config;
+ if ($event->key===HelpPages::SEARCH) {
+ if ($user->can(Permissions::APPROVE_IMAGE) && $config->get_bool(ApprovalConfig::IMAGES)) {
+ $block = new Block();
+ $block->header = "Approval";
+ $block->body = $this->theme->get_help_html();
+ $event->add_block($block);
+ }
+ }
+ }
+
+
+ private function no_approval_query(array $context): bool
+ {
+ foreach ($context as $term) {
+ if (preg_match(self::SEARCH_REGEXP, $term)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static function approve_image($image_id)
+ {
+ global $database, $user;
+
+ $database->execute(
+ $database->scoreql_to_sql(
+ "UPDATE images SET approved = SCORE_BOOL_Y, approved_by_id = :approved_by_id WHERE id = :id AND approved = SCORE_BOOL_N"
+ ),
+ ["approved_by_id"=>$user->id, "id"=>$image_id]
+ );
+ }
+
+ public static function disapprove_image($image_id)
+ {
+ global $database, $user;
+
+ $database->execute(
+ $database->scoreql_to_sql(
+ "UPDATE images SET approved = SCORE_BOOL_N, approved_by_id = NULL WHERE id = :id AND approved = SCORE_BOOL_Y"
+ ),
+ ["id"=>$image_id]
+ );
+ }
+
+ public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
+ {
+ global $user, $config;
+ if ($user->can(Permissions::APPROVE_IMAGE) && $config->get_bool(ApprovalConfig::IMAGES)) {
+ $event->add_part($this->theme->get_image_admin_html($event->image));
+ }
+ }
+
+ public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
+ {
+ global $user, $config;
+
+ if ($user->can(Permissions::APPROVE_IMAGE)&& $config->get_bool(ApprovalConfig::IMAGES)) {
+ if (in_array("approved:no", $event->search_terms)) {
+ $event->add_action("bulk_approve_image", "Approve", "a");
+ } else {
+ $event->add_action("bulk_disapprove_image", "Disapprove");
+ }
+ }
+ }
+
+ public function onBulkAction(BulkActionEvent $event)
+ {
+ global $user;
+
+ switch ($event->action) {
+ case "bulk_approve_image":
+ if ($user->can(Permissions::APPROVE_IMAGE)) {
+ $total = 0;
+ foreach ($event->items as $image) {
+ self::approve_image($image->id);
+ $total++;
+ }
+ flash_message("Approved $total items");
+ }
+ break;
+ case "bulk_disapprove_image":
+ if ($user->can(Permissions::APPROVE_IMAGE)) {
+ $total = 0;
+ foreach ($event->items as $image) {
+ self::disapprove_image($image->id);
+ $total++;
+ }
+ flash_message("Disapproved $total items");
+ }
+ break;
+ }
+ }
+
+
+ private function install()
+ {
+ global $database, $config;
+
+ if ($config->get_int(ApprovalConfig::VERSION) < 1) {
+ $database->Execute($database->scoreql_to_sql(
+ "ALTER TABLE images ADD COLUMN approved SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N"
+ ));
+ $database->Execute($database->scoreql_to_sql(
+ "ALTER TABLE images ADD COLUMN approved_by_id INTEGER NULL"
+ ));
+
+ $database->Execute("CREATE INDEX images_approved_idx ON images(approved)");
+ $config->set_int(ApprovalConfig::VERSION, 1);
+ }
+ }
+}
diff --git a/ext/approval/theme.php b/ext/approval/theme.php
new file mode 100644
index 00000000..b7ca5b23
--- /dev/null
+++ b/ext/approval/theme.php
@@ -0,0 +1,58 @@
+approved===true) {
+ $html = "
+ ".make_form(make_link('disapprove_image/'.$image->id), 'POST')."
+
+
+
+ ";
+ } else {
+ $html = "
+ ".make_form(make_link('approve_image/'.$image->id), 'POST')."
+
+
+
+ ";
+ }
+
+ return $html;
+ }
+
+
+ public function get_help_html()
+ {
+ return '
Search for images that are approved/not approved.
+
+
approved:yes
+
Returns images that have been approved.
+
+
+
approved:no
+
Returns images that have not been approved.
+
+ ';
+ }
+
+ public function display_admin_block(SetupBuildingEvent $event)
+ {
+ $sb = new SetupBlock("Approval");
+ $sb->add_bool_option(ApprovalConfig::IMAGES, "Images: ");
+ $event->panel->add_block($sb);
+ }
+
+ public function display_admin_form()
+ {
+ global $page;
+
+ $html = make_form(make_link("admin/approval"), "POST");
+ $html .= " ";
+ $html .= "";
+ $html .= "\n";
+ $page->add_block(new Block("Approval", $html));
+ }
+}
diff --git a/ext/arrowkey_navigation/info.php b/ext/arrowkey_navigation/info.php
index ee6b88f2..4d0a2f38 100644
--- a/ext/arrowkey_navigation/info.php
+++ b/ext/arrowkey_navigation/info.php
@@ -1,13 +1,5 @@
- * Link: http://www.drudexsoftware.com/
- * License: GPLv2
- * Description: Allows viewers no navigate between images using the left & right arrow keys.
- * Documentation:
- * Simply enable this extention in the extention manager to enable arrow key navigation.
- */
+
class ArrowkeyNavigationInfo extends ExtensionInfo
{
public const KEY = "arrowkey_navigation";
diff --git a/ext/arrowkey_navigation/main.php b/ext/arrowkey_navigation/main.php
index 640d2b12..6464f592 100644
--- a/ext/arrowkey_navigation/main.php
+++ b/ext/arrowkey_navigation/main.php
@@ -52,7 +52,7 @@ class ArrowkeyNavigation extends Extension
global $config, $database;
// get the amount of images per page
- $images_per_page = $config->get_int('index_images');
+ $images_per_page = $config->get_int(IndexConfig::IMAGES);
// if there are no tags, use default
if (is_null($event->get_arg(1))) {
diff --git a/ext/artists/info.php b/ext/artists/info.php
index 5da88896..efdc77a9 100644
--- a/ext/artists/info.php
+++ b/ext/artists/info.php
@@ -1,14 +1,5 @@
- * Alpha
- * License: GPLv2
- * Description: Simple artists extension
- * Documentation:
- *
- */
class ArtistsInfo extends ExtensionInfo
{
public const KEY = "artists";
diff --git a/ext/artists/main.php b/ext/artists/main.php
index e0a4cf7e..2fbd3500 100644
--- a/ext/artists/main.php
+++ b/ext/artists/main.php
@@ -905,7 +905,7 @@ class Artists extends Extension
$pageNumber * $artistsPerPage
, $artistsPerPage
]
- );
+ );
$number_of_listings = count($listing);
diff --git a/ext/autocomplete/info.php b/ext/autocomplete/info.php
index 3d420496..259ab70b 100644
--- a/ext/autocomplete/info.php
+++ b/ext/autocomplete/info.php
@@ -1,11 +1,5 @@
- * Description: Adds autocomplete to search & tagging.
- */
-
class AutoCompleteInfo extends ExtensionInfo
{
public const KEY = "autocomplete";
diff --git a/ext/ban_words/info.php b/ext/ban_words/info.php
index 6b56c045..b816b89c 100644
--- a/ext/ban_words/info.php
+++ b/ext/ban_words/info.php
@@ -1,15 +1,5 @@
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: For stopping spam and other comment abuse
- * Documentation:
- *
- */
-
class BanWordsInfo extends ExtensionInfo
{
public const KEY = "ban_words";
diff --git a/ext/bbcode/info.php b/ext/bbcode/info.php
index 06798aab..d97ae411 100644
--- a/ext/bbcode/info.php
+++ b/ext/bbcode/info.php
@@ -1,13 +1,5 @@
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: Turns BBCode into HTML
- */
-
class BBCodeInfo extends ExtensionInfo
{
public const KEY = "bbcode";
diff --git a/ext/blocks/info.php b/ext/blocks/info.php
index 23d24604..2bf68f2b 100644
--- a/ext/blocks/info.php
+++ b/ext/blocks/info.php
@@ -1,13 +1,5 @@
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: Add HTML to some space (News, Ads, etc)
- */
-
class BlocksInfo extends ExtensionInfo
{
public const KEY = "blocks";
diff --git a/ext/blotter/info.php b/ext/blotter/info.php
index d03891ec..7fab798c 100644
--- a/ext/blotter/info.php
+++ b/ext/blotter/info.php
@@ -1,11 +1,5 @@
[http://seemslegit.com/]
- * License: GPLv2
- * Description:
- */
class BlotterInfo extends ExtensionInfo
{
public const KEY = "blotter";
diff --git a/ext/browser_search/info.php b/ext/browser_search/info.php
index ba353e4c..34360c5b 100644
--- a/ext/browser_search/info.php
+++ b/ext/browser_search/info.php
@@ -1,17 +1,5 @@
- * Some code (and lots of help) by Artanis (Erik Youngren ) from the 'tagger' extention - Used with permission
- * Link: http://atravelinggeek.com/
- * License: GPLv2
- * Description: Allows the user to add a browser 'plugin' to search the site with real-time suggestions
- * Version: 0.1c, October 26, 2007
- * Documentation:
- *
- */
-
class BrowserSearchInfo extends ExtensionInfo
{
public const KEY = "browser_search";
diff --git a/ext/bulk_actions/info.php b/ext/bulk_actions/info.php
index 00c66576..76158a76 100644
--- a/ext/bulk_actions/info.php
+++ b/ext/bulk_actions/info.php
@@ -1,15 +1,5 @@
, contributions by Shish and Agasa.
- */
-
-
class BulkActionsInfo extends ExtensionInfo
{
public const KEY = "bulk_actions";
diff --git a/ext/bulk_add/info.php b/ext/bulk_add/info.php
index 333bf0ba..1d25d373 100644
--- a/ext/bulk_add/info.php
+++ b/ext/bulk_add/info.php
@@ -1,17 +1,8 @@
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: Bulk add server-side images
- * Documentation:
- */
-
class BulkAddInfo extends ExtensionInfo
{
- public const KEY = "builk_add";
+ public const KEY = "bulk_add";
public $key = self::KEY;
public $name = "Bulk Add";
@@ -20,7 +11,7 @@ class BulkAddInfo extends ExtensionInfo
public $license = self::LICENSE_GPLV2;
public $description = "Bulk add server-side images";
public $documentation =
-" Upload the images into a new directory via ftp or similar, go to
+"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
diff --git a/ext/bulk_add_csv/info.php b/ext/bulk_add_csv/info.php
index a2e6af69..30a407eb 100644
--- a/ext/bulk_add_csv/info.php
+++ b/ext/bulk_add_csv/info.php
@@ -1,15 +1,5 @@
- * License: GPLv2
- * Description: Bulk add server-side images with metadata from CSV file
- * Documentation:
- *
- *
- */
-
class BulkAddCSVInfo extends ExtensionInfo
{
public const KEY = "bulk_add_csv";
diff --git a/ext/bulk_remove/info.php b/ext/bulk_remove/info.php
index 7f90b825..6a48ee2b 100644
--- a/ext/bulk_remove/info.php
+++ b/ext/bulk_remove/info.php
@@ -1,14 +1,5 @@
- * Link: http://www.drudexsoftware.com/
- * License: GPLv2
- * Description: Allows admin to delete many images at once through Board Admin.
- * Documentation:
- *
- */
class BulkRemoveInfo extends ExtensionInfo
{
public const KEY = "bulk_remove";
diff --git a/ext/bulk_remove/main.php b/ext/bulk_remove/main.php
index 1d2517b3..50ea8edb 100644
--- a/ext/bulk_remove/main.php
+++ b/ext/bulk_remove/main.php
@@ -123,7 +123,7 @@ class BulkRemove extends Extension
$page->add_block(new Block(
"Bulk Remove Error",
"Please use Board Admin to use bulk remove."
- ));
+ ));
}
//
diff --git a/ext/comment/info.php b/ext/comment/info.php
index 600d1eab..38191656 100644
--- a/ext/comment/info.php
+++ b/ext/comment/info.php
@@ -1,15 +1,5 @@
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: Allow users to make comments on images
- * Documentation:
- * Formatting is done with the standard formatting API (normally BBCode)
- */
-
class CommentListInfo extends ExtensionInfo
{
public const KEY = "comment";
diff --git a/ext/comment/main.php b/ext/comment/main.php
index 3791ea00..8dca6983 100644
--- a/ext/comment/main.php
+++ b/ext/comment/main.php
@@ -367,7 +367,6 @@ class CommentList extends Extension
}
}
- // page building {{{
private function build_page(int $current_page)
{
global $cache, $database, $user;
@@ -417,9 +416,7 @@ class CommentList extends Extension
$this->theme->display_comment_list($images, $current_page, $total_pages, $user->can(Permissions::CREATE_COMMENT));
}
- // }}}
- // get comments {{{
/**
* #return Comment[]
*/
@@ -488,9 +485,7 @@ class CommentList extends Extension
ORDER BY comments.id ASC
", ["image_id"=>$image_id]);
}
- // }}}
- // add / remove / edit comments {{{
private function is_comment_limit_hit(): bool
{
global $config, $database;
@@ -651,5 +646,4 @@ class CommentList extends Extension
throw new CommentPostingException("Akismet thinks that your comment is spam. Try rewriting the comment, or logging in.");
}
}
- // }}}
}
diff --git a/ext/cron_uploader/config.php b/ext/cron_uploader/config.php
new file mode 100644
index 00000000..aa6637dc
--- /dev/null
+++ b/ext/cron_uploader/config.php
@@ -0,0 +1,97 @@
+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): int
+ {
+ global $config;
+ $config->get_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/main.php b/ext/cron_uploader/main.php
index 68a85635..49f4f171 100644
--- a/ext/cron_uploader/main.php
+++ b/ext/cron_uploader/main.php
@@ -1,43 +1,31 @@
parent=="system") {
+ $event->add_nav_link("cron_docs", new Link('cron_upload'), "Cron Upload");
+ }
+ }
/**
* Checks if the cron upload page has been accessed
@@ -45,308 +33,354 @@ class CronUploader extends Extension
*/
public function onPageRequest(PageRequestEvent $event)
{
- global $config, $user;
+ global $user;
if ($event->page_matches("cron_upload")) {
- $this->upload_key = $config->get_string(self::CONFIG_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->set_dir();
-
- $lockfile = fopen($this->root_dir . "/.lock", "w");
- if (!flock($lockfile, LOCK_EX | LOCK_NB)) {
- throw new Exception("Cron upload process is already running");
- }
- try {
- $this->process_upload(); // Start upload
- } finally {
- flock($lockfile, LOCK_UN);
- fclose($lockfile);
- }
- } elseif ($user->can(Permissions::BULK_ADD)) {
- $this->set_dir();
+ $key = $event->get_arg(0);
+ if (!empty($key)) {
+ $this->process_upload($key); // Start upload
+ } elseif ($user->can(Permissions::CRON_ADMIN)) {
$this->display_documentation();
}
}
}
- private function display_documentation()
- {
- global $page;
- $this->set_dir(); // Determines path to cron_uploader_dir
-
-
- $queue_dir = $this->root_dir . "/" . self::QUEUE_DIR;
- $uploaded_dir = $this->root_dir . "/" . self::UPLOADED_DIR;
- $failed_dir = $this->root_dir . "/" . self::FAILED_DIR;
-
- $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)";
-
- $page->set_title("Cron Uploader");
- $page->set_heading("Cron Uploader");
-
- $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);
- }
-
- public function onInitExt(InitExtEvent $event)
- {
- global $config;
- // Set default values
- $config->set_default_int(self::CONFIG_COUNT, 1);
- $this->set_dir();
-
- $this->upload_key = $config->get_string(self::CONFIG_KEY, "");
- if (empty($this->upload_key)) {
- $this->upload_key = $this->generate_key();
-
- $config->set_string(self::CONFIG_KEY, $this->upload_key);
- }
- }
-
public function onSetupBuilding(SetupBuildingEvent $event)
{
- $this->set_dir();
+ global $database;
- $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(self::CONFIG_COUNT, "How many to upload each time");
- $sb->add_text_option(self::CONFIG_DIR, " Set Cron Uploader root directory ");
+ $users = $database->get_pairs("SELECT name, id FROM users UNION ALL SELECT '', null order by name");
- $sb->add_label(" Cron Command:
- Create a cron job with the command above.
- Read the documentation if you're not sure what to do.");
+ $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);
}
- /*
- * Generates a unique key for the website to prevent unauthorized access.
- */
- private function generate_key()
+ public function onAdminBuilding(AdminBuildingEvent $event)
{
- $length = 20;
- $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
- $randomString = '';
+ $failed_dir = $this->get_failed_dir();
+ $results = get_dir_contents($failed_dir);
- for ($i = 0; $i < $length; $i++) {
- $randomString .= $characters [rand(0, strlen($characters) - 1)];
+ $failed_dirs = [];
+ foreach ($results as $result) {
+ $path = join_path($failed_dir, $result);
+ if (is_dir($path)) {
+ $failed_dirs[] = $result;
+ }
}
- return $randomString;
+ $this->theme->display_form($failed_dirs);
}
- /*
- * Set the directory for the image queue. If no directory was given, set it to the default directory.
- */
- private function set_dir()
+ public function onAdminAction(AdminActionEvent $event)
{
- global $config;
- // Determine directory (none = default)
-
- $dir = $config->get_string(self::CONFIG_DIR, "");
-
- // Sets new default dir if not in config yet/anymore
- if ($dir == "") {
- $dir = data_path("cron_uploader");
- $config->set_string(self::CONFIG_DIR, $dir);
+ $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)
+ {
+ if (empty($folder)) {
+ throw new Exception("folder empty");
+ }
+ $queue_dir = $this->get_queue_dir();
+ $stage_dir = join_path($this->get_failed_dir(), $folder);
+
+ if (!is_dir($stage_dir)) {
+ throw new Exception("Could not find $stage_dir");
+ }
+
+ $this->prep_root_dir();
+
+ $results = get_dir_contents($queue_dir);
+
+ if (count($results) > 0) {
+ flash_message("Queue folder must be empty to re-stage", "error");
+ return;
+ }
+
+ $results = get_dir_contents($stage_dir);
+
+ if (count($results) == 0) {
+ if (rmdir($stage_dir)===false) {
+ flash_message("Nothing to stage from $folder, cannot remove folder");
+ } else {
+ flash_message("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);
+ }
+
+ flash_message("Re-staged $folder to queue");
+ rmdir($stage_dir);
+ }
+
+ private function clear_folder($folder)
+ {
+ $path = join_path(CronUploaderConfig::get_dir(), $folder);
+ deltree($path);
+ flash_message("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($dir . "/" . self::QUEUE_DIR . "/")) {
- mkdir($dir . "/" . self::QUEUE_DIR . "/", 0775, true);
+ if (!is_dir($this->get_queue_dir())) {
+ mkdir($this->get_queue_dir(), 0775, true);
}
- if (!is_dir($dir . "/" . self::UPLOADED_DIR . "/")) {
- mkdir($dir . "/" . self::UPLOADED_DIR . "/", 0775, true);
+ if (!is_dir($this->get_uploaded_dir())) {
+ mkdir($this->get_uploaded_dir(), 0775, true);
}
- if (!is_dir($dir . "/" . self::FAILED_DIR . "/")) {
- mkdir($dir . "/" . self::FAILED_DIR . "/", 0775, true);
+ if (!is_dir($this->get_failed_dir())) {
+ mkdir($this->get_failed_dir(), 0775, true);
}
- $this->root_dir = $dir;
return $dir;
}
- /**
- * Returns amount of files & total size of dir.
- */
- public function scan_dir(string $path): array
+ private function get_lock_file(): string
{
- $bytestotal = 0;
- $nbfiles = 0;
-
- $ite = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
- 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 ['total_files' => $nbfiles, 'total_mb' => $size_mb];
+ $root_dir = CronUploaderConfig::get_dir();
+ return join_path($root_dir, ".lock");
}
/**
* Uploads the image & handles everything
*/
- public function process_upload(int $upload_count = 0): bool
+ public function process_upload(string $key, ?int $upload_count = null): bool
{
- global $config, $database;
+ global $database;
- //set_time_limit(0);
-
-
- $output_subdir = date('Ymd-His', time()) . "/";
- $this->generate_image_queue();
-
- // Gets amount of imgs to upload
- if ($upload_count == 0) {
- $upload_count = $config->get_int(self::CONFIG_COUNT, 1);
+ 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");
+ }
+ $user = User::by_id($user_id);
+ if ($user == null) {
+ throw new SCoreException("No user found for cron upload user $user_id");
}
- // 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;
+ send_event(new UserLoginEvent($user));
+ $this->log_message(SCORE_LOG_INFO, "Logged in as user {$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");
}
- // Randomize Images
- //shuffle($this->image_queue);
+ try {
+ //set_time_limit(0);
- $merged = 0;
- $added = 0;
- $failed = 0;
-
- // Upload the file(s)
- for ($i = 0; $i < $upload_count && sizeof($this->image_queue) > 0; $i++) {
- $img = array_pop($this->image_queue);
-
- $database->beginTransaction();
- try {
- $this->add_upload_info("Adding file: {$img[1]} - 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) {
- $database->rollback();
- $failed++;
- $this->move_uploaded($img[0], $img[1], $output_subdir, true);
- $msgNumber = $this->add_upload_info("(" . gettype($e) . ") " . $e->getMessage());
- $msgNumber = $this->add_upload_info($e->getTraceAsString());
+ // 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($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);
}
-
- $msgNumber = $this->add_upload_info("Items added: $added");
- $msgNumber = $this->add_upload_info("Items merged: $merged");
- $msgNumber = $this->add_upload_info("Items failed: $failed");
-
- // Display & save upload log
- $this->handle_log();
-
- return true;
}
- private function move_uploaded($path, $filename, $output_subdir, $corrupt = false)
+ private function move_uploaded(string $path, string $filename, string $output_subdir, bool $corrupt = false)
{
- // Create
- $newDir = $this->root_dir;
+ $relativeDir = dirname(substr($path, strlen(CronUploaderConfig::get_dir()) + 7));
- $relativeDir = dirname(substr($path, strlen($this->root_dir) + 7));
+ if ($relativeDir==".") {
+ $relativeDir = "";
+ }
// Determine which dir to move to
if ($corrupt) {
// Move to corrupt dir
- $newDir .= "/" . self::FAILED_DIR . "/" . $output_subdir . $relativeDir;
- $info = "ERROR: Image was not uploaded.";
+ $newDir = join_path($this->get_failed_dir(), $output_subdir, $relativeDir);
+ $info = "ERROR: Image was not uploaded. ";
} else {
- $newDir .= "/" . self::UPLOADED_DIR . "/" . $output_subdir . $relativeDir;
+ $newDir = join_path($this->get_uploaded_dir(), $output_subdir, $relativeDir);
$info = "Image successfully uploaded. ";
}
- $newDir = str_replace("//", "/", $newDir . "/");
+ $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, $newDir . $filename);
+ rename($path, $newFile);
- $this->add_upload_info($info . "Image \"$filename\" moved from queue to \"$newDir\".");
+ $this->log_message(SCORE_LOG_INFO, $info . "Image \"$filename\" moved from queue to \"$newDir\".");
}
/**
@@ -357,7 +391,7 @@ class CronUploader extends Extension
assert(file_exists($tmpname));
$tagArray = Tag::explode($tags);
- if (count($tagArray)==0) {
+ if (count($tagArray) == 0) {
$tagArray[] = "tagme";
}
@@ -377,11 +411,11 @@ class CronUploader extends Extension
if ($event->image_id == -1) {
throw new Exception("File type not recognised. Filename: {$filename}");
} elseif ($event->merged === true) {
- $infomsg = "Image merged. ID: {$event->image_id} Filename: {$filename}";
+ $infomsg = "Image merged. ID: {$event->image_id} - Filename: {$filename}";
} else {
$infomsg = "Image uploaded. ID: {$event->image_id} - Filename: {$filename}";
}
- $msgNumber = $this->add_upload_info($infomsg);
+ $this->log_message(SCORE_LOG_INFO, $infomsg);
// Set tags
$img = Image::by_id($event->image_id);
@@ -390,18 +424,32 @@ class CronUploader extends Extension
return $event;
}
- private function generate_image_queue(): void
+ private const PARTIAL_DOWNLOAD_EXTENSIONS = ['crdownload','part'];
+
+ private function is_skippable_file(string $path)
{
- $base = $this->root_dir . "/" . self::QUEUE_DIR;
+ $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->add_upload_info("Image Queue Directory could not be found at \"$base\".");
- return;
+ $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)) {
+ if (!is_link($fullpath) && !is_dir($fullpath) && !$this->is_skippable_file($fullpath)) {
$pathinfo = pathinfo($fullpath);
$relativePath = substr($fullpath, strlen($base));
@@ -412,34 +460,33 @@ class CronUploader extends Extension
1 => $pathinfo ["basename"],
2 => $tags
];
- array_push($this->image_queue, $img);
+ $output[] = $img;
+ if (!empty($limit) && count($output) >= $limit) {
+ break;
+ }
}
}
+ return $output;
}
- /**
- * Adds a message to the info being published at the end
- */
- private function add_upload_info(string $text, int $addon = 0): int
+
+ private function log_message(int $severity, string $message): void
{
- $info = $this->upload_info;
+ global $database;
+
+ log_msg(self::NAME, $severity, $message);
+
$time = "[" . date('Y-m-d H:i:s') . "]";
+ $this->output_buffer[] = $time . " " . $message;
- // If addon function is not used
- if ($addon == 0) {
- $this->upload_info .= "$time $text\r\n";
+ $log_path = $this->get_log_file();
- // Returns the number of the current line
- $currentLine = substr_count($this->upload_info, "\n") - 1;
- return $currentLine;
- }
+ file_put_contents($log_path, $time . " " . $message);
+ }
- // 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
+ private function get_log_file(): string
+ {
+ return join_path(CronUploaderConfig::get_dir(), "uploads.log");
}
/**
@@ -452,18 +499,6 @@ class CronUploader extends Extension
// Display message
$page->set_mode(PageMode::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);
+ $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..0b53dbf8
--- /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
index 5ac483d3..617ce791 100644
--- a/ext/custom_html_headers/info.php
+++ b/ext/custom_html_headers/info.php
@@ -1,14 +1,5 @@
- * Link: http://www.drudexsoftware.com
- * License: GPLv2
- * Description: Allows admins to modify & set custom <head> content
- * Documentation:
- *
- */
class CustomHtmlHeadersInfo extends ExtensionInfo
{
public const KEY = "custom_html_headers";
diff --git a/ext/danbooru_api/info.php b/ext/danbooru_api/info.php
index 89047d3b..fd68c229 100644
--- a/ext/danbooru_api/info.php
+++ b/ext/danbooru_api/info.php
@@ -1,13 +1,5 @@
-Description: Allow Danbooru apps like Danbooru Uploader for Firefox to communicate with Shimmie
-Documentation:
-
-*/
-
class DanbooruApiInfo extends ExtensionInfo
{
public const KEY = "danbooru_api";
diff --git a/ext/danbooru_api/main.php b/ext/danbooru_api/main.php
index aa0ec9a7..dfbce69a 100644
--- a/ext/danbooru_api/main.php
+++ b/ext/danbooru_api/main.php
@@ -91,7 +91,9 @@ class DanbooruApi extends Extension
$namelist = explode(",", $_GET['name']);
foreach ($namelist as $name) {
$sqlresult = $database->get_all(
- "SELECT id,tag,count FROM tags WHERE tag = ?",
+ $database->scoreql_to_sql(
+ "SELECT id,tag,count FROM tags WHERE SCORE_STRNORM(tag) = SCORE_STRNORM(?)"
+ ),
[$name]
);
foreach ($sqlresult as $row) {
diff --git a/ext/downtime/info.php b/ext/downtime/info.php
index fd8df943..671af143 100644
--- a/ext/downtime/info.php
+++ b/ext/downtime/info.php
@@ -1,15 +1,5 @@
- * Link: http://code.shishnet.org/shimmie2/
- * License: GPLv2
- * Description: Show a "down for maintenance" page
- * Documentation:
- *
- */
-
class DowntimeInfo extends ExtensionInfo
{
public const KEY = "downtime";
diff --git a/ext/downtime/theme.php b/ext/downtime/theme.php
index 99c4cffc..1399dbbb 100644
--- a/ext/downtime/theme.php
+++ b/ext/downtime/theme.php
@@ -66,6 +66,6 @@ class DowntimeTheme extends Themelet