Compare commits

...

86 Commits

Author SHA1 Message Date
6631e2cb6f Add Rin to homepage counter 2023-10-23 23:10:45 -07:00
James Shiffer
7040b1b8e5 Fembooru changes 2022-05-14 16:44:50 -07:00
Shish
c1068f1b2b bump 2020-06-24 16:14:49 +01:00
Shish
bb5614c5ef strip auth info from debug info dump 2020-06-24 15:09:53 +01:00
Shish
81417a5031 make info command match help text 2020-06-24 15:00:44 +01:00
Shish
2197b15012 Add core/sanitize_php.php
A small number of PHP-sanity things (eg don't silently ignore errors) to
be included right at the very start of index.php and tests/bootstrap.php
2020-06-24 14:54:46 +01:00
Shish
eecd35d175 turn 'Use of undefined constant' into an error 2020-06-24 13:00:46 +01:00
Shish
1c216e8d51 formatting 2020-06-24 12:44:35 +01:00
Shish
04987ea70e 'resize' support for static thumbnailer 2020-06-23 15:17:54 +01:00
Matthew Barbour
844ec8b53e Removed stump user config section 2020-06-23 15:15:55 +01:00
Matthew Barbour
72de50aa71 Added bulk download extension 2020-06-23 15:15:43 +01:00
Matthew Barbour
ac63992efa Added BulkActionException to allow clean error feedback from bulk actions 2020-06-23 15:15:10 +01:00
Matthew Barbour
7c32b1f7a8 Fixed issue where enabling bulk selection, then disabling it, resulted in subsequent bulk actions not applying to any items 2020-06-23 15:14:58 +01:00
Matthew Barbour
919a3039c4 Added user API key system 2020-06-23 15:14:04 +01:00
Matthew Barbour
ea34d9b756 Changes and bugfixes for bulk import extension 2020-06-23 15:12:26 +01:00
Matthew Barbour
30f62c2ff8 Fixed transaction issue between cron uploader and bulk import 2020-06-23 15:11:35 +01:00
Matthew Barbour
9b9f1d0341 Cleaned up some warnings in bulk import/export extension
Added transactions to bulk import
Renamed beginTransaction to begin_transaction for naming consistency
Updated cron uploader to handle bulk import transactions
2020-06-23 15:07:00 +01:00
Matthew Barbour
835c3b68a1 Added null return option 2020-06-23 15:06:26 +01:00
Matthew Barbour
b937ad6255 Added thumbnail scaling options
Changed ffmpeg thumbnailer to instead output a full-size png which is forwarded to the image thumbnailer, to allow it to take advantage of all available scaling options
2020-06-23 15:05:55 +01:00
Matthew Barbour
8e976fb812 Added "any" search option for private images 2020-06-23 15:03:44 +01:00
Matthew Barbour
dd08b936e3 Added skipped count to bulk import 2020-06-23 15:03:32 +01:00
Matthew Barbour
1fdd5bf575 New private image extension 2020-06-23 15:03:17 +01:00
Matthew Barbour
6d16c52367 New bulk import/export extension 2020-06-23 15:01:22 +01:00
Matthew Barbour
587735a866 Added terabyte support to the shorthand functions 2020-06-23 14:58:41 +01:00
Shish
a2fe0725f5 extra credit for Tag EditCloud 2020-06-22 18:14:07 +01:00
Shish
73660b376e
Merge pull request #732 from LaureeGrd/master
Tag EditCloud: Added category sorting and grouped tags.
2020-06-22 18:13:32 +01:00
Matthew Barbour
d243867b18 Removed errant colon 2020-06-22 18:07:45 +01:00
Matthew Barbour
fac2067069 Corrected issue with post title edit field width 2020-06-22 18:07:02 +01:00
Matthew Barbour
2f313b704a Making document ready events consistent 2020-06-22 18:06:35 +01:00
Matthew Barbour
8fe7038e73 Added option to BulkActionEvent to prevent redirect 2020-06-22 18:06:05 +01:00
Matthew Barbour
c171e98591 Changed int input field to type number 2020-06-22 18:03:40 +01:00
Matthew Barbour
edc8e5aa43 Added bulk import/export to gd suggestion 2020-06-22 18:03:13 +01:00
Matthew Barbour
f6923af8ab Added source logging to cron upload output when all sources are enabled 2020-06-22 18:03:03 +01:00
Matthew Barbour
18cd74f57d Changed a bunch of core extensions to be hidden since there's nothing to interact with 2020-06-22 18:02:47 +01:00
Matthew Barbour
4d69e7ce34 Added early 404 kill if requested image is not found 2020-06-22 18:02:21 +01:00
Matthew Barbour
04cde74226 Performance improvement for counting image category tags 2020-06-22 18:02:01 +01:00
Matthew Barbour
b2405166b3 Added git information to system info for telling what git commit a submission is for 2020-06-22 18:01:02 +01:00
Matthew Barbour
4d0b90921d Added missing AutoTaggerException 2020-06-22 18:00:23 +01:00
Shish
05d4a3a592
Merge pull request #734 from shish/pg-action
Fix postgres testing
2020-06-22 17:27:18 +01:00
Shish
b3fb923cd1 Fix postgres testing
Looks like github updated their default container, so now it contains
postgres and postgres-client by default - we just need to manually
launch the installed-but-disabled daemon
2020-06-22 16:19:11 +01:00
Shish
ad1e52bf05 DATABASE_TIMEOUT isn't set in the installer 2020-06-22 16:08:04 +01:00
LaureeGrd
549ec593bb Tag EditCloud: Added category sorting and grouped tags.
This change implements a simple category-based alphabetical sorting system that puts all tags containing ':' in front of general tabs. It also groups them together for easier styling into columns, grids, or even opening the door for drop-down categories in the edit menu. A much needed feature for me since I have hundreds of tags and I manage them all by my own.
2020-06-14 05:32:53 -03:00
Matthew Barbour
ed8a9fca52 Removed function stub 2020-06-03 20:02:55 +01:00
Matthew Barbour
ec290d8676 Added additional optional video formats to the video handler 2020-06-03 20:01:52 +01:00
Matthew Barbour
5446f29141 improved filetype error handling 2020-06-03 20:01:08 +01:00
Matthew Barbour
86f7a06ed0 New options for cron uploader:
Logging level
Including all logs in output
Stop on error instead of continuing
2020-06-03 20:00:54 +01:00
Matthew Barbour
e1aefb78ab Fix for cron uploader issue with root paths ending in a slash 2020-06-03 19:59:29 +01:00
Matthew Barbour
12c331cbd2 Removed extra tag set from cron uploader, no longer needed now that it runs as a user 2020-06-03 19:58:57 +01:00
Matthew Barbour
8b407e3df3 Changed video html element to use the image objects height and width, if available 2020-06-03 19:58:23 +01:00
Matthew Barbour
bccb206369 formatting pass 2020-06-03 19:58:13 +01:00
Matthew Barbour
2bb5f349f9 Added file type names
Formatted file
2020-06-03 19:57:47 +01:00
Matthew Barbour
007e07e507 Various changes to cron uploader:
Removed count limit, the cron job now checks the max PH execution time and auto-stops itself at 80% of that value.

Now skips os-specific image cache files like thumbs.db and the __macosx folder.

Changed failed folder re-deployment to allow re-deploying to populated queue, making it easier to re-process lots of failed batches all at once.

Changed page to output as a stream, allowing a long-running process to provide output as it runs rather than just at the very end.

Changed import loop to use the yield convention, allowing faster consumption of found files and lower memory use overall.
2020-06-03 19:57:27 +01:00
Matthew Barbour
63b2601e67 Mime type handling overhaul
Changed mime type map to deal with the reality that certain file types have multiple extensions and/or multiple mime types, as well as constants supporting all of the data. Created new functions using the updated mime type map to resolve mime types and extensions. Updated various items around the project that determine mime/extension to take advantage of the new functions.
2020-06-03 19:47:40 +01:00
Matthew Barbour
16c58e266b Added manual page mode to allow extensions to have direct control of the output 2020-06-03 19:40:43 +01:00
Matthew Barbour
6145ecc6f8 Updated post title extension to resolve set_title being removed from display image event
Resolves #724
2020-05-29 22:59:37 +01:00
Matthew Barbour
830915adf2 Fixed lite theme not showing image titles 2020-05-29 22:59:30 +01:00
Shish
06bd4589da option for admins to create new users 2020-05-19 19:33:51 +01:00
Shish
1e76fb239e s/fullrandom/dailyshuffle/, and run formatter 2020-05-13 13:03:49 +01:00
Shish
f7c6b662cd
Merge pull request #720 from MetallicAchu/master
Added a new search criteria "fullrandom"
2020-05-13 13:02:11 +01:00
MetallicAchu
00060c34c2
Merge pull request #1 from MetallicAchu/MetallicAchu-patch-1
Update main.php
2020-05-07 08:12:32 +03:00
MetallicAchu
10d46395d7
Update main.php
Added order[=|:]fullrandom so user doesn't need to choose a new seed every time.
Thus the list will change on a daily basis without user interaction, giving a more dynamic feel to the website
2020-05-07 08:10:15 +03:00
Shish
72645af9a4 refactor a bunch of weirdness in image replacement 2020-04-25 21:38:11 +01:00
Shish
2cae6cd273 format 2020-04-25 21:36:28 +01:00
Shish
0b2e36303d allow bypassing auth tokens in unit tests 2020-04-25 21:35:14 +01:00
Shish
b0cb46abca test a couple extra branches 2020-04-24 14:10:45 +01:00
Shish
78710166a1 add a extra escape, fixes #718 2020-04-20 09:53:44 +01:00
Shish
c146a9f53d don't double-escape 2020-04-12 12:45:19 +01:00
Shish
f6112d26a2 unify single and global history pages 2020-04-12 12:43:12 +01:00
Shish
b04b5af190
Merge pull request #717 from DanielOaks/add-logout-button
Add logout button to themes that use subnav bar
2020-04-06 16:54:48 +01:00
Daniel Oaks
02d42a01b4 Add logout button to themes that use subnav bar 2020-04-07 01:36:10 +10:00
Shish
0039aafe94 avoid excess ampersands 2020-04-02 22:38:50 +01:00
Shish
1d389f0156 make tests/router.php more like .htaccess 2020-04-02 22:31:57 +01:00
Shish
69cb67fe24 stagger thumbnail cache 2020-03-28 16:11:05 +00:00
Shish
5ea26a80cc nicetest without http vs https pain 2020-03-28 15:48:27 +00:00
Shish
126c629a1a test 2020-03-28 14:39:03 +00:00
Shish
ab4b745310 test 2020-03-28 14:33:48 +00:00
Shish
f47e35e4e5 make make_link more sane 2020-03-28 14:11:14 +00:00
Shish
fd359fb08c remove broken tagger extension 2020-03-28 00:56:54 +00:00
Shish
866b77ab19 set max-width / max-height for random and featured image blocks 2020-03-28 00:23:29 +00:00
Shish
b60e8ac5b4 make modify_url work better 2020-03-27 23:35:07 +00:00
Shish
70acc6015b drop support for ie6 2020-03-27 20:57:15 +00:00
Shish
a3a129df5f more niceurlness 2020-03-27 20:53:21 +00:00
Shish
02675609b4 more referer dedupe 2020-03-27 20:24:26 +00:00
Shish
85662575c5 stop warning for lack of referer / user-agent - not having those is now pretty normal 2020-03-27 19:42:46 +00:00
Shish
c16e3fd939 dedupe some referer handling 2020-03-27 19:41:34 +00:00
Shish
5ea7cc5b36 SCRIPT_NAME instead of PHP_SELF to find self for niceurl test 2020-03-27 19:03:46 +00:00
151 changed files with 2843 additions and 3181 deletions

View File

@ -32,7 +32,7 @@ jobs:
run: |
mkdir -p data/config
if [[ "${{ matrix.database }}" == "pgsql" ]]; then
sudo apt update && sudo apt-get install -y postgresql postgresql-client ;
sudo systemctl start postgresql ;
psql --version ;
sudo -u postgres psql -c "SELECT set_config('log_statement', 'all', false);" -U postgres ;
sudo -u postgres psql -c "CREATE USER shimmie WITH PASSWORD 'shimmie';" -U postgres ;

View File

@ -17,8 +17,7 @@
# rather than link to images/ha/hash and have an ugly filename,
# we link to images/hash/tags.ext; mod_rewrite splits things so
# that shimmie sees hash and the user sees tags.ext
RewriteRule ^_images/([0-9a-f]{2})([0-9a-f]{30}).*$ data/images/$1/$1$2 [L]
RewriteRule ^_thumbs/([0-9a-f]{2})([0-9a-f]{30}).*$ data/thumbs/$1/$1$2 [L]
RewriteRule ^_(images|thumbs)/([0-9a-f]{2})([0-9a-f]{30}).*$ data/$1/$2/$2$3 [L]
# any requests for files which don't physically exist should be handled by index.php
RewriteCond %{REQUEST_FILENAME} !-f

View File

@ -6,10 +6,6 @@
"minimum-stability" : "dev",
"repositories" : [
{
"type": "composer",
"url": "https://asset-packagist.org"
},
{
"type" : "package",
"package" : {
@ -25,18 +21,19 @@
],
"require" : {
"php" : ">=7.3",
"php" : "^7.3",
"ext-pdo": "*",
"ext-json": "*",
"ext-fileinfo": "*",
"flexihash/flexihash" : "^2.0.0",
"ifixit/php-akismet" : "1.*",
"google/recaptcha" : "~1.1",
"dapphp/securimage" : "3.6.*",
"shish/eventtracer-php" : "^1.0.0",
"shish/ffsphp" : "0.0.*",
"shish/microcrud" : "^1.0.0",
"shish/microhtml" : "^1.0.0",
"shish/eventtracer-php" : "^2.0.0",
"shish/ffsphp" : "^1.0.0",
"shish/microcrud" : "^2.0.0",
"shish/microhtml" : "^2.0.0",
"enshrined/svg-sanitize" : "0.13.*",
"bower-asset/jquery" : "1.12.*",
@ -46,8 +43,7 @@
},
"require-dev" : {
"phpunit/phpunit" : "8.*"
},
},
"suggest": {
"ext-memcache": "memcache caching",
@ -58,9 +54,14 @@
"ext-curl": "some extensions",
"ext-ctype": "some extensions",
"ext-json": "some extensions",
"ext-zip": "self-updater extension",
"ext-zip": "self-updater extension, bulk import/export",
"ext-zlib": "anti-spam",
"ext-xml": "some extensions",
"ext-gd": "GD-based thumbnailing"
}
},"replace": {
"bower-asset/jquery": ">=1.11.0",
"bower-asset/inputmask": ">=3.2.0",
"bower-asset/punycode": ">=1.3.0",
"bower-asset/yii2-pjax": ">=2.0.0"
}
}

1636
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ abstract class PageMode
const DATA = 'data';
const PAGE = 'page';
const FILE = 'file';
const MANUAL = 'manual';
}
/**
@ -238,16 +239,13 @@ class BasePage
// ==============================================
/**
* Display the page according to the mode and data given.
*/
public function display(): void
public function send_headers(): void
{
header("HTTP/1.0 {$this->code} Shimmie");
header("Content-type: " . $this->type);
header("X-Powered-By: Shimmie-" . VERSION);
if (!headers_sent()) {
header("HTTP/1.0 {$this->code} Shimmie");
header("Content-type: " . $this->type);
header("X-Powered-By: Shimmie-" . VERSION);
foreach ($this->http_headers as $head) {
header($head);
}
@ -257,8 +255,20 @@ class BasePage
} else {
print "Error: Headers have already been sent to the client.";
}
}
/**
* Display the page according to the mode and data given.
*/
public function display(): void
{
if ($this->mode!=PageMode::MANUAL) {
$this->send_headers();
}
switch ($this->mode) {
case PageMode::MANUAL:
break;
case PageMode::PAGE:
usort($this->blocks, "blockcmp");
$this->add_auto_html_headers();
@ -394,7 +404,6 @@ class BasePage
$js_latest = $config_latest;
$js_files = array_merge(
[
"vendor/bower-asset/jquery/dist/jquery.min.js",
"vendor/bower-asset/jquery-timeago/jquery.timeago.js",
"vendor/bower-asset/js-cookie/src/js.cookie.js",
"ext/static_files/modernizr-3.3.1.custom.js",

View File

@ -53,7 +53,7 @@ class BaseThemelet
$h_tip = html_escape($image->get_tooltip());
$h_tags = html_escape(strtolower($image->get_tag_list()));
$extArr = array_flip(['swf', 'svg', 'mp3']); //List of thumbless filetypes
$extArr = array_flip([EXTENSION_FLASH, EXTENSION_SVG, EXTENSION_MP3]); //List of thumbless filetypes
if (!isset($extArr[$image->ext])) {
$tsize = get_thumbnail_size($image->width, $image->height);
} else {

View File

@ -60,7 +60,7 @@ class Database
$this->connect_engine();
$this->engine->init($this->db);
$this->beginTransaction();
$this->begin_transaction();
}
private function connect_engine(): void
@ -78,11 +78,14 @@ class Database
} elseif ($db_proto === DatabaseDriver::SQLITE) {
$this->engine = new SQLite();
} else {
die('Unknown PDO driver: '.$db_proto);
die_nicely(
'Unknown PDO driver: '.$db_proto,
"Please check that this is a valid driver, installing the PHP modules if needed"
);
}
}
public function beginTransaction(): void
public function begin_transaction(): void
{
if ($this->transaction === false) {
$this->db->beginTransaction();
@ -90,9 +93,14 @@ class Database
}
}
public function is_transaction_open(): bool
{
return !is_null($this->db) && $this->transaction === true;
}
public function commit(): bool
{
if (!is_null($this->db) && $this->transaction === true) {
if ($this->is_transaction_open()) {
$this->transaction = false;
return $this->db->commit();
} else {
@ -102,7 +110,7 @@ class Database
public function rollback(): bool
{
if (!is_null($this->db) && $this->transaction === true) {
if ($this->is_transaction_open()) {
$this->transaction = false;
return $this->db->rollback();
} else {

View File

@ -92,7 +92,9 @@ class PostgreSQL extends DBEngine
} else {
$db->exec("SET application_name TO 'shimmie [local]';");
}
$this->set_timeout($db, DATABASE_TIMEOUT);
if (defined("DATABASE_TIMEOUT")) {
$this->set_timeout($db, DATABASE_TIMEOUT);
}
}
public function scoreql_to_sql(string $data): string

View File

@ -275,7 +275,7 @@ abstract class FormatterExtension extends Extension
*/
abstract class DataHandlerExtension extends Extension
{
protected $SUPPORTED_EXT = [];
protected $SUPPORTED_MIME = [];
protected function move_upload_to_archive(DataUploadEvent $event)
{
@ -298,14 +298,11 @@ abstract class DataHandlerExtension extends Extension
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'])) {
if (!is_null($event->replace_id)) {
/* 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);
$existing = Image::by_id($event->replace_id);
if (is_null($existing)) {
throw new UploadException("Image to replace does not exist!");
@ -320,19 +317,25 @@ abstract class DataHandlerExtension extends Extension
if (is_null($image)) {
throw new UploadException("Data handler failed to create image object from data");
}
if (empty($image->ext)) {
throw new UploadException("Unable to determine extension for ". $event->tmpname);
}
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;
send_event(new ImageReplaceEvent($event->replace_id, $image));
$event->image_id = $event->replace_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");
}
if (empty($image->ext)) {
throw new UploadException("Unable to determine extension for ". $event->tmpname);
}
try {
send_event(new MediaCheckPropertiesEvent($image));
} catch (MediaException $e) {
@ -406,10 +409,12 @@ abstract class DataHandlerExtension extends Extension
$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 = get_extension_for_file($filename);
}
if (empty($image->ext)) {
$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'];
@ -422,15 +427,20 @@ abstract class DataHandlerExtension extends Extension
protected function supported_ext(string $ext): bool
{
return in_array(strtolower($ext), $this->SUPPORTED_EXT);
return in_array(get_mime_for_extension($ext), $this->SUPPORTED_MIME);
}
public static function get_all_supported_exts(): array
{
$arr = [];
foreach (getSubclassesOf("DataHandlerExtension") as $handler) {
$arr = array_merge($arr, (new $handler())->SUPPORTED_EXT);
$handler = (new $handler());
foreach ($handler->SUPPORTED_MIME as $mime) {
$arr = array_merge($arr, get_all_extension_for_mime($mime));
}
}
$arr = array_unique($arr);
return $arr;
}
}

448
core/filetypes.php Normal file
View File

@ -0,0 +1,448 @@
<?php
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* MIME types and extension information and resolvers *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
const EXTENSION_ANI = 'ani';
const EXTENSION_ASC = 'asc';
const EXTENSION_ASF = 'asf';
const EXTENSION_AVI = 'avi';
const EXTENSION_BMP = 'bmp';
const EXTENSION_BZIP = 'bz';
const EXTENSION_BZIP2 = 'bz2';
const EXTENSION_CBR = 'cbr';
const EXTENSION_CBZ = 'cbz';
const EXTENSION_CBT = 'cbt';
const EXTENSION_CBA = 'cbA';
const EXTENSION_CB7 = 'cb7';
const EXTENSION_CSS = 'css';
const EXTENSION_CSV = 'csv';
const EXTENSION_CUR = 'cur';
const EXTENSION_FLASH = 'swf';
const EXTENSION_FLASH_VIDEO = 'flv';
const EXTENSION_GIF = 'gif';
const EXTENSION_GZIP = 'gz';
const EXTENSION_HTML = 'html';
const EXTENSION_HTM = 'htm';
const EXTENSION_ICO = 'ico';
const EXTENSION_JFIF = 'jfif';
const EXTENSION_JFI = 'jfi';
const EXTENSION_JPEG = 'jpeg';
const EXTENSION_JPG = 'jpg';
const EXTENSION_JS = 'js';
const EXTENSION_JSON = 'json';
const EXTENSION_MKV = 'mkv';
const EXTENSION_MP3 = 'mp3';
const EXTENSION_MP4 = 'mp4';
const EXTENSION_M4V = 'm4v';
const EXTENSION_M4A = 'm4a';
const EXTENSION_MPEG = 'mpeg';
const EXTENSION_MPG = 'mpg';
const EXTENSION_OGG = 'ogg';
const EXTENSION_OGG_VIDEO = 'ogv';
const EXTENSION_OGG_AUDIO = 'oga';
const EXTENSION_PDF = 'pdf';
const EXTENSION_PHP = 'php';
const EXTENSION_PHP5 = 'php5';
const EXTENSION_PNG = 'png';
const EXTENSION_PSD = 'psd';
const EXTENSION_MOV = 'mov';
const EXTENSION_RSS = 'rss';
const EXTENSION_SVG = 'svg';
const EXTENSION_TAR = 'tar';
const EXTENSION_TEXT = 'txt';
const EXTENSION_TIFF = 'tiff';
const EXTENSION_TIF = 'tif';
const EXTENSION_WAV = 'wav';
const EXTENSION_WEBM = 'webm';
const EXTENSION_WEBP = 'webp';
const EXTENSION_WMA = 'wma';
const EXTENSION_WMV = 'wmv';
const EXTENSION_XML = 'xml';
const EXTENSION_XSL = 'xsl';
const EXTENSION_ZIP = 'zip';
// Couldn't find a mimetype for ani, so made one up based on it being a riff container
const MIME_TYPE_ANI = 'application/riff+ani';
const MIME_TYPE_ASF = 'video/x-ms-asf';
const MIME_TYPE_AVI = 'video/x-msvideo';
// Went with mime types from http://fileformats.archiveteam.org/wiki/Comic_Book_Archive
const MIME_TYPE_COMIC_ZIP = 'application/vnd.comicbook+zip';
const MIME_TYPE_COMIC_RAR = 'application/vnd.comicbook-rar';
const MIME_TYPE_BMP = 'image/x-ms-bmp';
const MIME_TYPE_BZIP = 'application/x-bzip';
const MIME_TYPE_BZIP2 = 'application/x-bzip2';
const MIME_TYPE_CSS = 'text/css';
const MIME_TYPE_CSV = 'text/csv';
const MIME_TYPE_FLASH = 'application/x-shockwave-flash';
const MIME_TYPE_FLASH_VIDEO = 'video/x-flv';
const MIME_TYPE_GIF = 'image/gif';
const MIME_TYPE_GZIP = 'application/x-gzip';
const MIME_TYPE_HTML = 'text/html';
const MIME_TYPE_ICO = 'image/x-icon';
const MIME_TYPE_JPEG = 'image/jpeg';
const MIME_TYPE_JS = 'text/javascript';
const MIME_TYPE_JSON = 'application/json';
const MIME_TYPE_MKV = 'video/x-matroska';
const MIME_TYPE_MP3 = 'audio/mpeg';
const MIME_TYPE_MP4_AUDIO = 'audio/mp4';
const MIME_TYPE_MP4_VIDEO = 'video/mp4';
const MIME_TYPE_MPEG = 'video/mpeg';
const MIME_TYPE_OCTET_STREAM = 'application/octet-stream';
const MIME_TYPE_OGG = 'application/ogg';
const MIME_TYPE_OGG_VIDEO = 'video/ogg';
const MIME_TYPE_OGG_AUDIO = 'audio/ogg';
const MIME_TYPE_PDF = 'application/pdf';
const MIME_TYPE_PHP = 'text/x-php';
const MIME_TYPE_PNG = 'image/png';
const MIME_TYPE_PSD = 'image/vnd.adobe.photoshop';
const MIME_TYPE_QUICKTIME = 'video/quicktime';
const MIME_TYPE_RSS = 'application/rss+xml';
const MIME_TYPE_SVG = 'image/svg+xml';
const MIME_TYPE_TAR = 'application/x-tar';
const MIME_TYPE_TEXT = 'text/plain';
const MIME_TYPE_TIFF = 'image/tiff';
const MIME_TYPE_WAV = 'audio/x-wav';
const MIME_TYPE_WEBM = 'video/webm';
const MIME_TYPE_WEBP = 'image/webp';
const MIME_TYPE_WIN_BITMAP = 'image/x-win-bitmap';
const MIME_TYPE_XML = 'text/xml';
const MIME_TYPE_XML_APPLICATION = 'application/xml';
const MIME_TYPE_XSL = 'application/xsl+xml';
const MIME_TYPE_ZIP = 'application/zip';
const MIME_TYPE_MAP_NAME = 'name';
const MIME_TYPE_MAP_EXT = 'ext';
const MIME_TYPE_MAP_MIME = 'mime';
// Mime type map. Each entry in the MIME_TYPE_ARRAY represents a kind of file, identified by the "correct" mimetype as the key.
// The value for each entry is a map of twokeys, ext and mime.
// ext's value is an array of all of the extensions that the file type can use, with the "correct" one being first.
// mime's value is an array of all mime types that the file type is known to use, with the current "correct" one being first.
const MIME_TYPE_MAP = [
MIME_TYPE_ANI => [
MIME_TYPE_MAP_NAME => "ANI Cursor",
MIME_TYPE_MAP_EXT => [EXTENSION_ANI],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ANI],
],
MIME_TYPE_AVI => [
MIME_TYPE_MAP_NAME => "AVI",
MIME_TYPE_MAP_EXT => [EXTENSION_AVI],
MIME_TYPE_MAP_MIME => [MIME_TYPE_AVI,'video/avi','video/msvideo'],
],
MIME_TYPE_ASF => [
MIME_TYPE_MAP_NAME => "ASF/WMV",
MIME_TYPE_MAP_EXT => [EXTENSION_ASF,EXTENSION_WMA,EXTENSION_WMV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ASF,'audio/x-ms-wma','video/x-ms-wmv'],
],
MIME_TYPE_BMP => [
MIME_TYPE_MAP_NAME => "BMP",
MIME_TYPE_MAP_EXT => [EXTENSION_BMP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_BMP],
],
MIME_TYPE_BZIP => [
MIME_TYPE_MAP_NAME => "BZIP",
MIME_TYPE_MAP_EXT => [EXTENSION_BZIP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_BZIP],
],
MIME_TYPE_BZIP2 => [
MIME_TYPE_MAP_NAME => "BZIP2",
MIME_TYPE_MAP_EXT => [EXTENSION_BZIP2],
MIME_TYPE_MAP_MIME => [MIME_TYPE_BZIP2],
],
MIME_TYPE_COMIC_ZIP => [
MIME_TYPE_MAP_NAME => "CBZ",
MIME_TYPE_MAP_EXT => [EXTENSION_CBZ],
MIME_TYPE_MAP_MIME => [MIME_TYPE_COMIC_ZIP],
],
MIME_TYPE_CSS => [
MIME_TYPE_MAP_NAME => "Cascading Style Sheet",
MIME_TYPE_MAP_EXT => [EXTENSION_CSS],
MIME_TYPE_MAP_MIME => [MIME_TYPE_CSS],
],
MIME_TYPE_CSV => [
MIME_TYPE_MAP_NAME => "CSV",
MIME_TYPE_MAP_EXT => [EXTENSION_CSV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_CSV],
],
MIME_TYPE_FLASH => [
MIME_TYPE_MAP_NAME => "Flash",
MIME_TYPE_MAP_EXT => [EXTENSION_FLASH],
MIME_TYPE_MAP_MIME => [MIME_TYPE_FLASH],
],
MIME_TYPE_FLASH_VIDEO => [
MIME_TYPE_MAP_NAME => "Flash Video",
MIME_TYPE_MAP_EXT => [EXTENSION_FLASH_VIDEO],
MIME_TYPE_MAP_MIME => [MIME_TYPE_FLASH_VIDEO,'video/flv'],
],
MIME_TYPE_GIF => [
MIME_TYPE_MAP_NAME => "GIF",
MIME_TYPE_MAP_EXT => [EXTENSION_GIF],
MIME_TYPE_MAP_MIME => [MIME_TYPE_GIF],
],
MIME_TYPE_GZIP => [
MIME_TYPE_MAP_NAME => "GZIP",
MIME_TYPE_MAP_EXT => [EXTENSION_GZIP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TAR],
],
MIME_TYPE_HTML => [
MIME_TYPE_MAP_NAME => "HTML",
MIME_TYPE_MAP_EXT => [EXTENSION_HTM, EXTENSION_HTML],
MIME_TYPE_MAP_MIME => [MIME_TYPE_HTML],
],
MIME_TYPE_ICO => [
MIME_TYPE_MAP_NAME => "Icon",
MIME_TYPE_MAP_EXT => [EXTENSION_ICO, EXTENSION_CUR],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ICO, MIME_TYPE_WIN_BITMAP],
],
MIME_TYPE_JPEG => [
MIME_TYPE_MAP_NAME => "JPEG",
MIME_TYPE_MAP_EXT => [EXTENSION_JPG, EXTENSION_JPEG, EXTENSION_JFIF, EXTENSION_JFI],
MIME_TYPE_MAP_MIME => [MIME_TYPE_JPEG],
],
MIME_TYPE_JS => [
MIME_TYPE_MAP_NAME => "JavaScript",
MIME_TYPE_MAP_EXT => [EXTENSION_JS],
MIME_TYPE_MAP_MIME => [MIME_TYPE_JS],
],
MIME_TYPE_JSON => [
MIME_TYPE_MAP_NAME => "JSON",
MIME_TYPE_MAP_EXT => [EXTENSION_JSON],
MIME_TYPE_MAP_MIME => [MIME_TYPE_JSON],
],
MIME_TYPE_MKV => [
MIME_TYPE_MAP_NAME => "Matroska",
MIME_TYPE_MAP_EXT => [EXTENSION_MKV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MKV],
],
MIME_TYPE_MP3 => [
MIME_TYPE_MAP_NAME => "MP3",
MIME_TYPE_MAP_EXT => [EXTENSION_MP3],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP3],
],
MIME_TYPE_MP4_AUDIO => [
MIME_TYPE_MAP_NAME => "MP4 Audio",
MIME_TYPE_MAP_EXT => [EXTENSION_M4A],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP4_AUDIO,"audio/m4a"],
],
MIME_TYPE_MP4_VIDEO => [
MIME_TYPE_MAP_NAME => "MP4 Video",
MIME_TYPE_MAP_EXT => [EXTENSION_MP4,EXTENSION_M4V],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP4_VIDEO,'video/x-m4v'],
],
MIME_TYPE_MPEG => [
MIME_TYPE_MAP_NAME => "MPEG",
MIME_TYPE_MAP_EXT => [EXTENSION_MPG,EXTENSION_MPEG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_MPEG],
],
MIME_TYPE_PDF => [
MIME_TYPE_MAP_NAME => "PDF",
MIME_TYPE_MAP_EXT => [EXTENSION_PDF],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PDF],
],
MIME_TYPE_PHP => [
MIME_TYPE_MAP_NAME => "PHP",
MIME_TYPE_MAP_EXT => [EXTENSION_PHP,EXTENSION_PHP5],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PHP],
],
MIME_TYPE_PNG => [
MIME_TYPE_MAP_NAME => "PNG",
MIME_TYPE_MAP_EXT => [EXTENSION_PNG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PNG],
],
MIME_TYPE_PSD => [
MIME_TYPE_MAP_NAME => "PSD",
MIME_TYPE_MAP_EXT => [EXTENSION_PSD],
MIME_TYPE_MAP_MIME => [MIME_TYPE_PSD],
],
MIME_TYPE_OGG_AUDIO => [
MIME_TYPE_MAP_NAME => "Ogg Vorbis",
MIME_TYPE_MAP_EXT => [EXTENSION_OGG_AUDIO,EXTENSION_OGG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_OGG_AUDIO,MIME_TYPE_OGG],
],
MIME_TYPE_OGG_VIDEO => [
MIME_TYPE_MAP_NAME => "Ogg Theora",
MIME_TYPE_MAP_EXT => [EXTENSION_OGG_VIDEO],
MIME_TYPE_MAP_MIME => [MIME_TYPE_OGG_VIDEO],
],
MIME_TYPE_QUICKTIME => [
MIME_TYPE_MAP_NAME => "Quicktime",
MIME_TYPE_MAP_EXT => [EXTENSION_MOV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_QUICKTIME],
],
MIME_TYPE_RSS => [
MIME_TYPE_MAP_NAME => "RSS",
MIME_TYPE_MAP_EXT => [EXTENSION_RSS],
MIME_TYPE_MAP_MIME => [MIME_TYPE_RSS],
],
MIME_TYPE_SVG => [
MIME_TYPE_MAP_NAME => "SVG",
MIME_TYPE_MAP_EXT => [EXTENSION_SVG],
MIME_TYPE_MAP_MIME => [MIME_TYPE_SVG],
],
MIME_TYPE_TAR => [
MIME_TYPE_MAP_NAME => "TAR",
MIME_TYPE_MAP_EXT => [EXTENSION_TAR],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TAR],
],
MIME_TYPE_TEXT => [
MIME_TYPE_MAP_NAME => "Text",
MIME_TYPE_MAP_EXT => [EXTENSION_TEXT, EXTENSION_ASC],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TEXT],
],
MIME_TYPE_TIFF => [
MIME_TYPE_MAP_NAME => "TIFF",
MIME_TYPE_MAP_EXT => [EXTENSION_TIF,EXTENSION_TIFF],
MIME_TYPE_MAP_MIME => [MIME_TYPE_TIFF],
],
MIME_TYPE_WAV => [
MIME_TYPE_MAP_NAME => "Wave",
MIME_TYPE_MAP_EXT => [EXTENSION_WAV],
MIME_TYPE_MAP_MIME => [MIME_TYPE_WAV],
],
MIME_TYPE_WEBM => [
MIME_TYPE_MAP_NAME => "WebM",
MIME_TYPE_MAP_EXT => [EXTENSION_WEBM],
MIME_TYPE_MAP_MIME => [MIME_TYPE_WEBM],
],
MIME_TYPE_WEBP => [
MIME_TYPE_MAP_NAME => "WebP",
MIME_TYPE_MAP_EXT => [EXTENSION_WEBP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_WEBP],
],
MIME_TYPE_XML => [
MIME_TYPE_MAP_NAME => "XML",
MIME_TYPE_MAP_EXT => [EXTENSION_XML],
MIME_TYPE_MAP_MIME => [MIME_TYPE_XML,MIME_TYPE_XML_APPLICATION],
],
MIME_TYPE_XSL => [
MIME_TYPE_MAP_NAME => "XSL",
MIME_TYPE_MAP_EXT => [EXTENSION_XSL],
MIME_TYPE_MAP_MIME => [MIME_TYPE_XSL],
],
MIME_TYPE_ZIP => [
MIME_TYPE_MAP_NAME => "ZIP",
MIME_TYPE_MAP_EXT => [EXTENSION_ZIP],
MIME_TYPE_MAP_MIME => [MIME_TYPE_ZIP],
],
];
/**
* Returns the mimetype that matches the provided extension.
*/
function get_mime_for_extension(string $ext): ?string
{
$ext = strtolower($ext);
foreach (MIME_TYPE_MAP as $key=>$value) {
if (in_array($ext, $value[MIME_TYPE_MAP_EXT])) {
return $key;
}
}
return null;
}
/**
* Returns the mimetype for the specified file, trying file inspection methods before falling back on extension-based detection.
* @param String $file
* @param String $ext The files extension, for if the current filename somehow lacks the extension
* @return String The extension that was found.
*/
function get_mime(string $file, string $ext=""): string
{
if (!file_exists($file)) {
throw new SCoreException("File not found: ".$file);
}
$type = false;
if (extension_loaded('fileinfo')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
try {
$type = finfo_file($finfo, $file);
} finally {
finfo_close($finfo);
}
} elseif (function_exists('mime_content_type')) {
// If anyone is still using mime_content_type()
$type = trim(mime_content_type($file));
}
if ($type===false || empty($type)) {
// Checking by extension is our last resort
if ($ext==null||strlen($ext) == 0) {
$ext = pathinfo($file, PATHINFO_EXTENSION);
}
$type = get_mime_for_extension($ext);
}
if ($type !== false && strlen($type) > 0) {
return $type;
}
return MIME_TYPE_OCTET_STREAM;
}
/**
* Returns the file extension associated with the specified mimetype.
*/
function get_extension(?string $mime_type): ?string
{
if (empty($mime_type)) {
return null;
}
if ($mime_type==MIME_TYPE_OCTET_STREAM) {
return null;
}
foreach (MIME_TYPE_MAP as $key=>$value) {
if (in_array($mime_type, $value[MIME_TYPE_MAP_MIME])) {
return $value[MIME_TYPE_MAP_EXT][0];
}
}
return null;
}
/**
* Returns all of the file extensions associated with the specified mimetype.
*/
function get_all_extension_for_mime(?string $mime_type): array
{
$output = [];
if (empty($mime_type)) {
return $output;
}
foreach (MIME_TYPE_MAP as $key=>$value) {
if (in_array($mime_type, $value[MIME_TYPE_MAP_MIME])) {
$output = array_merge($output, $value[MIME_TYPE_MAP_EXT]);
}
}
return $output;
}
/**
* Gets an the extension defined in MIME_TYPE_MAP for a file.
*
* @param String $file_path
* @return String The extension that was found, or null if one can not be found.
*/
function get_extension_for_file(String $file_path): ?String
{
$mime = get_mime($file_path);
if (!empty($mime)) {
if ($mime==MIME_TYPE_OCTET_STREAM) {
return null;
} else {
$ext = get_extension($mime);
}
if (!empty($ext)) {
return $ext;
}
}
return null;
}

View File

@ -213,6 +213,27 @@ class Image
);
}
/**
* Counts consecutive days of image uploads
*/
public static function count_upload_streak(): int
{
$now = date_create();
$last_date = $now;
foreach (self::find_images_iterable() as $img) {
$next_date = date_create($img->posted);
if (date_diff($next_date, $last_date)->days > 0) {
break;
}
$last_date = $next_date;
}
if ($last_date === $now) {
return 0;
}
$diff_d = ($now->getTimestamp() - $last_date->getTimestamp()) / 86400;
return (int)ceil($diff_d);
}
/**
* Count the number of image results for a given search
*
@ -570,7 +591,7 @@ class Image
*/
public function get_mime_type(): string
{
return getMimeType($this->get_image_filename(), $this->get_ext());
return get_mime($this->get_image_filename(), $this->get_ext());
}
/**

View File

@ -39,7 +39,7 @@ function add_dir(string $base): array
* @param string $tags
* @throws UploadException
*/
function add_image(string $tmpname, string $filename, string $tags): void
function add_image(string $tmpname, string $filename, string $tags): int
{
assert(file_exists($tmpname));
@ -52,7 +52,11 @@ function add_image(string $tmpname, string $filename, string $tags): void
$metadata['tags'] = Tag::explode($tags);
$metadata['source'] = null;
send_event(new DataUploadEvent($tmpname, $metadata));
$due = new DataUploadEvent($tmpname, $metadata);
send_event($due);
return $due->image_id;
}
/**
@ -69,6 +73,12 @@ function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_sca
{
global $config;
$fit = $config->get_string(ImageConfig::THUMB_FIT);
if (in_array($fit, [Media::RESIZE_TYPE_FILL, Media::RESIZE_TYPE_STRETCH, Media::RESIZE_TYPE_FIT_BLUR])) {
return [$config->get_int(ImageConfig::THUMB_WIDTH), $config->get_int(ImageConfig::THUMB_HEIGHT)];
}
if ($orig_width === 0) {
$orig_width = 192;
}
@ -128,21 +138,35 @@ function get_thumbnail_max_size_scaled(): array
function create_image_thumb(string $hash, string $type, string $engine = null)
{
global $config;
$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);
create_scaled_image(
$inname,
$outname,
$tsize,
$type,
$engine,
$config->get_string(ImageConfig::THUMB_FIT)
);
}
function create_scaled_image(string $inname, string $outname, array $tsize, string $type, ?string $engine)
function create_scaled_image(string $inname, string $outname, array $tsize, string $type, ?string $engine = null, ?string $resize_type = null)
{
global $config;
if (empty($engine)) {
$engine = $config->get_string(ImageConfig::THUMB_ENGINE);
}
if (empty($resize_type)) {
$resize_type = $config->get_string(ImageConfig::THUMB_FIT);
}
$output_format = $config->get_string(ImageConfig::THUMB_TYPE);
if ($output_format=="webp") {
if ($output_format==EXTENSION_WEBP) {
$output_format = Media::WEBP_LOSSY;
}
@ -153,10 +177,10 @@ function create_scaled_image(string $inname, string $outname, array $tsize, stri
$outname,
$tsize[0],
$tsize[1],
false,
$resize_type,
$output_format,
$config->get_int(ImageConfig::THUMB_QUALITY),
true,
$config->get_bool('thumb_upscale', false)
true
));
}

View File

@ -116,8 +116,6 @@ class Tag
for ($i = 0; $i < count($tags1); $i++) {
if ($tags1[$i]!==$tags2[$i]) {
var_dump($tags1);
var_dump($tags2);
return false;
}
}

View File

@ -20,7 +20,7 @@ function install()
date_default_timezone_set('UTC');
if (is_readable("data/config/shimmie.conf.php")) {
exit_with_page(
die_nicely(
"Shimmie is already installed.",
"data/config/shimmie.conf.php exists, how did you get here?"
);
@ -69,7 +69,7 @@ function do_install($dsn)
create_tables(new Database($dsn));
write_config($dsn);
} catch (InstallerException $e) {
exit_with_page($e->title, $e->body, $e->code);
die_nicely($e->title, $e->body, $e->code);
}
}
@ -117,7 +117,7 @@ function ask_questions()
$warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : "";
$err_msg = $errors ? "<h3>Errors</h3>".implode("\n<p>", $errors) : "";
exit_with_page(
die_nicely(
"Install Options",
<<<EOD
$warn_msg
@ -304,7 +304,7 @@ function write_config($dsn)
if (file_put_contents("data/config/shimmie.conf.php", $file_content, LOCK_EX)) {
header("Location: index.php?flash=Installation%20complete");
exit_with_page(
die_nicely(
"Installation Successful",
"<p>If you aren't redirected, <a href=\"index.php\">click here to Continue</a>."
);
@ -324,25 +324,3 @@ function write_config($dsn)
);
}
}
function exit_with_page($title, $body, $code=0)
{
print("<!DOCTYPE html>
<html lang='en'>
<head>
<title>Shimmie Installer</title>
<link rel=\"shortcut icon\" href=\"ext/static_files/static/favicon.ico\">
<link rel=\"stylesheet\" href=\"ext/static_files/style.css\" type=\"text/css\">
</head>
<body>
<div id=\"installer\">
<h1>Shimmie Installer</h1>
<h3>$title</h3>
<div class=\"container\">
$body
</div>
</div>
</body>
</html>");
exit($code);
}

View File

@ -10,6 +10,15 @@ define("SCORE_LOG_INFO", 20);
define("SCORE_LOG_DEBUG", 10);
define("SCORE_LOG_NOTSET", 0);
const LOGGING_LEVEL_NAMES = [
SCORE_LOG_NOTSET=>"Not Set",
SCORE_LOG_DEBUG=>"Debug",
SCORE_LOG_INFO=>"Info",
SCORE_LOG_WARNING=>"Warning",
SCORE_LOG_ERROR=>"Error",
SCORE_LOG_CRITICAL=>"Critical",
];
/**
* A shorthand way to send a LogEvent
*

View File

@ -18,6 +18,7 @@ abstract class Permissions
public const BAN_IP = "ban_ip";
public const CREATE_USER = "create_user";
public const CREATE_OTHER_USER = "create_other_user";
public const EDIT_USER_NAME = "edit_user_name";
public const EDIT_USER_PASSWORD = "edit_user_password";
public const EDIT_USER_INFO = "edit_user_info"; # email address, etc
@ -95,4 +96,11 @@ abstract class Permissions
public const CRON_ADMIN = "cron_admin";
public const APPROVE_IMAGE = "approve_image";
public const APPROVE_COMMENT = "approve_comment";
public const SET_PRIVATE_IMAGE = "set_private_image";
public const SET_OTHERS_PRIVATE_IMAGES = "set_others_private_images";
public const BULK_IMPORT = "bulk_import";
public const BULK_EXPORT = "bulk_export";
public const BULK_DOWNLOAD = "bulk_download";
}

View File

@ -3,6 +3,9 @@
* Things which should be in the core API *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
require_once "filetypes.php";
/**
* Return the unique elements of an array, case insensitively
*/
@ -150,6 +153,14 @@ function list_files(string $base, string $_sub_dir=""): array
return $file_list;
}
function flush_output(): void
{
if (!defined("UNITTEST")) {
@ob_flush();
}
flush();
}
function stream_file(string $file, int $start, int $end): void
{
$fp = fopen($file, 'r');
@ -162,10 +173,7 @@ function stream_file(string $file, int $start, int $end): void
$buffer = $end - $p + 1;
}
echo fread($fp, $buffer);
if (!defined("UNITTEST")) {
@ob_flush();
}
flush();
flush_output();
// 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
@ -248,99 +256,6 @@ if (!function_exists('mb_strlen')) {
}
}
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)
{
@ -406,6 +321,23 @@ function get_base_href(): string
return $dir;
}
/**
* The opposite of the standard library's parse_url
*/
function unparse_url($parsed_url)
{
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? "$pass@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = !empty($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
return "$scheme$user$pass$host$port$path$query$fragment";
}
function startsWith(string $haystack, string $needle): bool
{
$length = strlen($needle);
@ -588,11 +520,14 @@ function truncate(string $string, int $limit, string $break=" ", string $pad="..
*/
function parse_shorthand_int(string $limit): int
{
if (preg_match('/^([\d\.]+)([gmk])?b?$/i', (string)$limit, $m)) {
if (preg_match('/^([\d\.]+)([tgmk])?b?$/i', (string)$limit, $m)) {
$value = $m[1];
if (isset($m[2])) {
switch (strtolower($m[2])) {
/** @noinspection PhpMissingBreakStatementInspection */
case 't': $value *= 1024; // fall through
/** @noinspection PhpMissingBreakStatementInspection */
// no break
case 'g': $value *= 1024; // fall through
/** @noinspection PhpMissingBreakStatementInspection */
// no break
@ -616,7 +551,9 @@ function to_shorthand_int(int $int): string
{
assert($int >= 0);
if ($int >= pow(1024, 3)) {
if ($int >= pow(1024, 4)) {
return sprintf("%.1fTB", $int / pow(1024, 4));
} elseif ($int >= pow(1024, 3)) {
return sprintf("%.1fGB", $int / pow(1024, 3));
} elseif ($int >= pow(1024, 2)) {
return sprintf("%.1fMB", $int / pow(1024, 2));

63
core/sanitize_php.php Normal file
View File

@ -0,0 +1,63 @@
<?php declare(strict_types=1);
/*
* A small number of PHP-sanity things (eg don't silently ignore errors) to
* be included right at the very start of index.php and tests/bootstrap.php
*/
$min_php = "7.3";
if (version_compare(phpversion(), $min_php, ">=") === false) {
print "
Shimmie does not support versions of PHP lower than $min_php
(PHP reports that it is version ".phpversion().").
If your web host is running an older version, they are dangerously out of
date and you should plan on moving elsewhere.
";
exit;
}
# ini_set('zend.assertions', '1'); // generate assertions
ini_set('assert.exception', '1'); // throw exceptions when failed
set_error_handler(function ($errNo, $errStr) {
// Should we turn ALL notices into errors? PHP allows a lot of
// terrible things to happen by default...
if (strpos($errStr, 'Use of undefined constant ') === 0) {
throw new Exception("PHP Error#$errNo: $errStr");
} else {
return false;
}
});
ob_start();
if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') {
if (isset($_SERVER['REMOTE_ADDR'])) {
die("CLI with remote addr? Confused, not taking the risk.");
}
$_SERVER['REMOTE_ADDR'] = "0.0.0.0";
$_SERVER['HTTP_HOST'] = "<cli command>";
}
function die_nicely($title, $body, $code=0)
{
print("<!DOCTYPE html>
<html lang='en'>
<head>
<title>Shimmie</title>
<link rel=\"shortcut icon\" href=\"ext/static_files/static/favicon.ico\">
<link rel=\"stylesheet\" href=\"ext/static_files/style.css\" type=\"text/css\">
</head>
<body>
<div id=\"installer\">
<h1>Shimmie</h1>
<h3>$title</h3>
<div class=\"container\">
$body
</div>
</div>
</body>
</html>");
if ($code != 0) {
http_response_code(500);
}
exit($code);
}

View File

@ -26,7 +26,7 @@ _d("DEBUG", false); // boolean print various debugging details
_d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
_d("VERSION", "2.8.3$_g"); // string shimmie version
_d("VERSION", "2.8.4$_g"); // string shimmie version
_d("TIMEZONE", null); // string timezone
_d("EXTRA_EXTS", ""); // string optional extra extensions
_d("BASE_HREF", null); // string force a specific base URL (default is auto-detect)

View File

@ -2,7 +2,8 @@
use PHPUnit\Framework\TestCase;
class TestInit extends TestCase {
class TestInit extends TestCase
{
public function testInitExt()
{
send_event(new InitExtEvent());

View File

@ -8,15 +8,35 @@ class UrlsTest extends TestCase
{
public function test_make_link()
{
// basic
$this->assertEquals(
"/test/foo",
make_link("foo")
);
// remove leading slash from path
$this->assertEquals(
"/test/foo",
make_link("/foo")
);
// query
$this->assertEquals(
"/test/foo?a=1&b=2",
make_link("foo", "a=1&b=2")
);
// hash
$this->assertEquals(
"/test/foo#cake",
make_link("foo", null, "cake")
);
// query + hash
$this->assertEquals(
"/test/foo?a=1&b=2#cake",
make_link("foo", "a=1&b=2", "cake")
);
}
public function test_make_http()
@ -39,4 +59,43 @@ class UrlsTest extends TestCase
make_http("https://foo.com")
);
}
public function test_modify_url()
{
$this->assertEquals(
"/foo/bar?a=3&b=2",
modify_url("/foo/bar?a=1&b=2", ["a"=>"3"])
);
$this->assertEquals(
"https://blah.com/foo/bar?b=2",
modify_url("https://blah.com/foo/bar?a=1&b=2", ["a"=>null])
);
$this->assertEquals(
"/foo/bar",
modify_url("/foo/bar?a=1&b=2", ["a"=>null, "b"=>null])
);
}
public function test_referer_or()
{
unset($_SERVER['HTTP_REFERER']);
$this->assertEquals(
"foo",
referer_or("foo")
);
$_SERVER['HTTP_REFERER'] = "cake";
$this->assertEquals(
"cake",
referer_or("foo")
);
$_SERVER['HTTP_REFERER'] = "cake";
$this->assertEquals(
"foo",
referer_or("foo", ["cake"])
);
}
}

View File

@ -1,7 +1,4 @@
<?php declare(strict_types=1);
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
* HTML Generation *
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
class Link
{
@ -26,32 +23,27 @@ class Link
*
* eg make_link("post/list") becomes "/v2/index.php?q=post/list"
*/
function make_link(?string $page=null, ?string $query=null): string
function make_link(?string $page=null, ?string $query=null, ?string $fragment=null): string
{
global $config;
if (is_null($page)) {
$page = $config->get_string(SetupConfig::MAIN_PAGE);
}
$page = trim($page, "/");
$parts = [];
$install_dir = get_base_href();
if (SPEED_HAX || $config->get_bool('nice_urls', false)) {
$base = $install_dir;
$parts['path'] = "$install_dir/$page";
} else {
$base = "$install_dir/index.php?q=";
$parts['path'] = "$install_dir/index.php";
$query = empty($query) ? "q=$page" : "q=$page&$query";
}
$parts['query'] = $query; // http_build_query($query);
$parts['fragment'] = $fragment; // http_build_query($hash);
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;
}
}
return unparse_url($parts);
}
@ -60,43 +52,28 @@ function make_link(?string $page=null, ?string $query=null): string
*/
function modify_current_url(array $changes): string
{
return modify_url($_SERVER['QUERY_STRING'], $changes);
return modify_url($_SERVER['REQUEST_URI'], $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.
$parts = parse_url($url);
// 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($parts['query'])) {
parse_str($parts['query'], $params);
}
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;
}
$parts['query'] = http_build_query($params);
return make_link($base, http_build_query($params));
return unparse_url($parts);
}
/**
* Turn a relative link into an absolute one, including hostname
*/
@ -116,3 +93,22 @@ function make_http(string $link): string
return $link;
}
/**
* If HTTP_REFERER is set, and not blacklisted, then return it
* Else return a default $dest
*/
function referer_or(string $dest, ?array $blacklist=null): string
{
if (empty($_SERVER['HTTP_REFERER'])) {
return $dest;
}
if ($blacklist) {
foreach ($blacklist as $b) {
if (strstr($_SERVER['HTTP_REFERER'], $b)) {
return $dest;
}
}
}
return $_SERVER['HTTP_REFERER'];
}

View File

@ -247,6 +247,9 @@ class User
public function check_auth_token(): bool
{
if (defined("UNITTEST")) {
return true;
}
return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token());
}

View File

@ -98,6 +98,8 @@ new UserClass("user", "base", [
Permissions::EDIT_FAVOURITES => true,
Permissions::SEND_PM => true,
Permissions::READ_PM => true,
Permissions::SET_PRIVATE_IMAGE => true,
Permissions::BULK_DOWNLOAD => true,
]);
new UserClass("hellbanned", "user", [
@ -118,6 +120,7 @@ new UserClass("admin", "base", [
Permissions::BAN_IP => true,
Permissions::CREATE_USER => true,
Permissions::CREATE_OTHER_USER => true,
Permissions::EDIT_USER_NAME => true,
Permissions::EDIT_USER_PASSWORD => true,
Permissions::EDIT_USER_INFO => true,
@ -196,6 +199,13 @@ new UserClass("admin", "base", [
Permissions::APPROVE_IMAGE => true,
Permissions::APPROVE_COMMENT => true,
Permissions::BULK_IMPORT =>true,
Permissions::BULK_EXPORT =>true,
Permissions::BULK_DOWNLOAD => true,
Permissions::SET_PRIVATE_IMAGE => true,
Permissions::SET_OTHERS_PRIVATE_IMAGES => true,
]);
@include_once "data/config/user-classes.conf.php";

View File

@ -393,6 +393,67 @@ function get_dir_contents(string $dir): array
);
}
function remove_empty_dirs(string $dir): bool
{
assert(!empty($dir));
$result = true;
if (!is_dir($dir)) {
return false;
}
$items = array_diff(
scandir(
$dir
),
['..', '.']
);
foreach ($items as $item) {
$path = join_path($dir, $item);
if (is_dir($path)) {
$result = $result && remove_empty_dirs($path);
} else {
$result = false;
}
}
if ($result===true) {
$result = $result && rmdir($dir);
}
return $result;
}
function get_files_recursively(string $dir): array
{
assert(!empty($dir));
if (!is_dir($dir)) {
return [];
}
$things = array_diff(
scandir(
$dir
),
['..', '.']
);
$output = [];
foreach ($things as $thing) {
$path = join_path($dir, $thing);
if (is_file($path)) {
$output[] = $path;
} else {
$output = array_merge($output, get_files_recursively($path));
}
}
return $output;
}
/**
* Returns amount of files & total size of dir.
*/
@ -493,31 +554,18 @@ function _load_theme_files()
require_all(_get_themelet_files(get_theme()));
}
function _sanitise_environment(): void
function _set_up_shimmie_environment(): void
{
global $tracer_enabled;
$min_php = "7.3";
if (version_compare(phpversion(), $min_php, ">=") === false) {
print "
Shimmie does not support versions of PHP lower than $min_php
(PHP reports that it is version ".phpversion().").
If your web host is running an older version, they are dangerously out of
date and you should plan on moving elsewhere.
";
exit;
}
if (file_exists("images") && !file_exists("data/images")) {
die("As of Shimmie 2.7 images and thumbs should be moved to data/images and data/thumbs");
die_nicely("Upgrade error", "As of Shimmie 2.7 images and thumbs should be moved to data/images and data/thumbs");
}
if (TIMEZONE) {
date_default_timezone_set(TIMEZONE);
}
# ini_set('zend.assertions', '1'); // generate assertions
ini_set('assert.exception', '1'); // throw exceptions when failed
if (DEBUG) {
error_reporting(E_ALL);
}
@ -526,16 +574,6 @@ date and you should plan on moving elsewhere.
// so to prevent running out of memory during complex operations code that uses it should
// check if tracer output is enabled before making use of it.
$tracer_enabled = constant('TRACE_FILE')!==null;
ob_start();
if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') {
if (isset($_SERVER['REMOTE_ADDR'])) {
die("CLI with remote addr? Confused, not taking the risk.");
}
$_SERVER['REMOTE_ADDR'] = "0.0.0.0";
$_SERVER['HTTP_HOST'] = "<cli command>";
}
}
@ -598,6 +636,7 @@ function _fatal_error(Exception $e): void
<p><b>Message:</b> '.html_escape($message).'
'.$q.'
<p><b>Version:</b> '.$version.' (on '.$phpver.')
<p><b>Stack Trace:</b></p><pre>'.$e->getTraceAsString().'</pre>
</body>
</html>
';
@ -643,7 +682,7 @@ function show_ip(string $ip, string $ban_reason): string
global $user;
$u_reason = url_escape($ban_reason);
$u_end = url_escape("+1 week");
$ban = $user->can(Permissions::BAN_IP) ? ", <a href='".make_link("ip_ban/list", "c_ip=$ip&c_reason=$u_reason&c_expires=$u_end#create")."'>Ban</a>" : "";
$ban = $user->can(Permissions::BAN_IP) ? ", <a href='".make_link("ip_ban/list", "c_ip=$ip&c_reason=$u_reason&c_expires=$u_end", "create")."'>Ban</a>" : "";
$ip = $user->can(Permissions::VIEW_IP) ? $ip.$ban : "";
return $ip;
}
@ -743,3 +782,18 @@ function human_filesize(int $bytes, $decimals = 2)
$factor = floor((strlen(strval($bytes)) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @BYTE_DENOMINATIONS[$factor];
}
/*
* Generates a unique key for the website to prevent unauthorized access.
*/
function generate_key(int $length = 20)
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters [rand(0, strlen($characters) - 1)];
}
return $randomString;
}

View File

@ -96,7 +96,7 @@ class AliasEditor extends Extension
$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_type(MIME_TYPE_CSV);
$page->set_filename("aliases.csv");
$page->set_data($this->get_alias_csv($database));
} elseif ($event->get_arg(0) == "import") {

View File

@ -52,6 +52,10 @@ class DeleteAutoTagEvent extends Event
}
}
class AutoTaggerException extends SCoreException
{
}
class AddAutoTagException extends SCoreException
{
}
@ -98,7 +102,7 @@ class AutoTagger extends Extension
$this->theme->display_auto_tagtable($t->table($t->query()), $t->paginator());
} elseif ($event->get_arg(0) == "export") {
$page->set_mode(PageMode::DATA);
$page->set_type("text/csv");
$page->set_type(MIME_TYPE_CSV);
$page->set_filename("auto_tag.csv");
$page->set_data($this->get_auto_tag_csv($database));
} elseif ($event->get_arg(0) == "import") {

View File

@ -20,7 +20,7 @@ class AutoComplete extends Extension
}
$page->set_mode(PageMode::DATA);
$page->set_type("application/json");
$page->set_type(MIME_TYPE_JSON);
$s = strtolower($_GET["s"]);
if (

View File

@ -107,4 +107,32 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
$bb = new BBCode();
return $bb->strip($in);
}
public function testSiteLinks()
{
$this->assertEquals(
'<a class="shm-clink" data-clink-sel="" href="/test/post/view/123">&gt;&gt;123</a>',
$this->filter("&gt;&gt;123")
);
$this->assertEquals(
'<a class="shm-clink" data-clink-sel="#c456" href="/test/post/view/123#c456">&gt;&gt;123#c456</a>',
$this->filter("&gt;&gt;123#c456")
);
$this->assertEquals(
'<a class="shm-clink" data-clink-sel="" href="/test/foo/bar">foo/bar</a>',
$this->filter("[url]site://foo/bar[/url]")
);
$this->assertEquals(
'<a class="shm-clink" data-clink-sel="#c123" href="/test/foo/bar#c123">foo/bar#c123</a>',
$this->filter("[url]site://foo/bar#c123[/url]")
);
$this->assertEquals(
'<a class="shm-clink" data-clink-sel="" href="/test/foo/bar">look at my post</a>',
$this->filter("[url=site://foo/bar]look at my post[/url]")
);
$this->assertEquals(
'<a class="shm-clink" data-clink-sel="#c123" href="/test/foo/bar#c123">look at my comment</a>',
$this->filter("[url=site://foo/bar#c123]look at my comment[/url]")
);
}
}

View File

@ -42,7 +42,7 @@ class BrowserSearch extends Extension
// And now to send it to the browser
$page->set_mode(PageMode::DATA);
$page->set_type("text/xml");
$page->set_type(MIME_TYPE_XML);
$page->set_data($xml);
} elseif ($event->page_matches("browser_search")) {
$suggestions = $config->get_string("search_suggestions_results_order");

View File

@ -1,5 +1,8 @@
<?php declare(strict_types=1);
class BulkActionException extends SCoreException
{
}
class BulkActionBlockBuildingEvent extends Event
{
/** @var array */
@ -39,6 +42,8 @@ class BulkActionEvent extends Event
public $action;
/** @var array */
public $items;
/** @var bool */
public $redirect = true;
public function __construct(String $action, Generator $items)
{
@ -164,28 +169,38 @@ class BulkActions extends Extension
$action = $_POST['bulk_action'];
$items = null;
if (isset($_POST['bulk_selected_ids']) && $_POST['bulk_selected_ids'] != "") {
$data = json_decode($_POST['bulk_selected_ids']);
if (is_array($data)&&!empty($data)) {
$items = $this->yield_items($data);
try {
$items = null;
if (isset($_POST['bulk_selected_ids']) && !empty($_POST['bulk_selected_ids'])) {
$data = json_decode($_POST['bulk_selected_ids']);
if (empty($data)) {
throw new BulkActionException("No ids specified in bulk_selected_ids");
}
if (is_array($data) && !empty($data)) {
$items = $this->yield_items($data);
}
} elseif (isset($_POST['bulk_query']) && $_POST['bulk_query'] != "") {
$query = $_POST['bulk_query'];
if ($query != null && $query != "") {
$items = $this->yield_search_results($query);
}
} else {
throw new BulkActionException("No ids selected and no query present, cannot perform bulk operation on entire collection");
}
} elseif (isset($_POST['bulk_query']) && $_POST['bulk_query'] != "") {
$query = $_POST['bulk_query'];
if ($query != null && $query != "") {
$items = $this->yield_search_results($query);
$bae = new BulkActionEvent($action, $items);
if (is_iterable($items)) {
send_event($bae);
}
} catch (BulkActionException $e) {
log_error(BulkActionsInfo::KEY, $e->getMessage(), $e->getMessage());
}
if (is_iterable($items)) {
send_event(new BulkActionEvent($action, $items));
if ($bae->redirect) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(referer_or(make_link()));
}
$page->set_mode(PageMode::REDIRECT);
if (!isset($_SERVER['HTTP_REFERER'])) {
$_SERVER['HTTP_REFERER'] = make_link();
}
$page->set_redirect($_SERVER['HTTP_REFERER']);
}
}

View File

@ -53,6 +53,7 @@ function deactivate_bulk_selector() {
set_selected_items([]);
$('#bulk_selector_controls').hide();
$('#bulk_selector_activate').show();
$('input[name="bulk_selected_ids"]').val("");
bulk_selector_active = false;
}
@ -94,7 +95,6 @@ function deselect_item(id) {
function toggle_selection( id ) {
var data = get_selected_items();
console.log(id);
if(data.includes(id)) {
data.splice(data.indexOf(id),1);
set_selected_items(data);

View File

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
class BulkDownloadInfo extends ExtensionInfo
{
public const KEY = "bulk_download";
public $key = self::KEY;
public $name = "Bulk Download";
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL;
public $description = "Allows bulk downloading images.";
public $dependencies = [BulkActionsInfo::KEY];
}

View File

@ -0,0 +1,78 @@
<?php declare(strict_types=1);
class BulkDownloadConfig
{
public const SIZE_LIMIT = "bulk_download_size_limit";
}
class BulkDownloadException extends BulkActionException
{
}
class BulkDownload extends Extension
{
private const DOWNLOAD_ACTION_NAME = "bulk_download";
public function onInitExt(InitExtEvent $event)
{
global $config;
$config->set_default_int(BulkDownloadConfig::SIZE_LIMIT, parse_shorthand_int('100MB'));
}
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
{
global $user, $config;
if ($user->can(Permissions::BULK_DOWNLOAD)) {
$event->add_action(BulkDownload::DOWNLOAD_ACTION_NAME, "Download ZIP");
}
}
public function onSetupBuilding(SetupBuildingEvent $event)
{
$sb = new SetupBlock("Bulk Download");
$sb->start_table();
$sb->add_shorthand_int_option(BulkDownloadConfig::SIZE_LIMIT, "Size Limit", true);
$sb->end_table();
$event->panel->add_block($sb);
}
public function onBulkAction(BulkActionEvent $event)
{
global $user, $page, $config;
if ($user->can(Permissions::BULK_DOWNLOAD)&&
($event->action == BulkDownload::DOWNLOAD_ACTION_NAME)) {
$download_filename = $user->name . '-' . date('YmdHis') . '.zip';
$zip_filename = tempnam(sys_get_temp_dir(), "shimmie_bulk_download");
$zip = new ZipArchive;
$size_total = 0;
$max_size = $config->get_int(BulkDownloadConfig::SIZE_LIMIT);
if ($zip->open($zip_filename, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE) === true) {
foreach ($event->items as $image) {
$img_loc = warehouse_path(Image::IMAGE_DIR, $image->hash, false);
$size_total += filesize($img_loc);
if ($size_total>$max_size) {
throw new BulkDownloadException("Bulk download limited to ".human_filesize($max_size));
}
$filename = urldecode($image->get_nice_image_name());
$filename = str_replace(":", ";", $filename);
$zip->addFile($img_loc, $filename);
}
$zip->close();
$page->set_mode(PageMode::FILE);
$page->set_file($zip_filename, true);
$page->set_filename($download_filename);
$event->redirect = false;
}
}
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
class BulkExportEvent extends Event
{
public $image;
public $fields = [];
public function __construct(Image $image)
{
$this->image = $image;
}
}
class BulkImportEvent extends Event
{
public $image;
public $fields = [];
public function __construct(Image $image, $fields)
{
$this->image = $image;
$this->fields = $fields;
}
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
include_once "events.php";
class BulkImportExportInfo extends ExtensionInfo
{
public const KEY = "bulk_import_export";
public $key = self::KEY;
public $name = "Bulk Import/Export";
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL;
public $description = "Allows bulk exporting/importing of images and associated data.";
public $dependencies = [BulkActionsInfo::KEY];
}

View File

@ -0,0 +1,172 @@
<?php declare(strict_types=1);
class BulkImportExport extends DataHandlerExtension
{
const EXPORT_ACTION_NAME = "bulk_export";
const EXPORT_INFO_FILE_NAME = "export.json";
protected $SUPPORTED_MIME = [MIME_TYPE_ZIP];
public function onDataUpload(DataUploadEvent $event)
{
global $user, $database;
if ($this->supported_ext($event->type) &&
$user->can(Permissions::BULK_IMPORT)) {
$zip = new ZipArchive;
if ($zip->open($event->tmpname) === true) {
$info = $zip->getStream(self::EXPORT_INFO_FILE_NAME);
$json_data = [];
if ($info !== false) {
try {
$json_string = stream_get_contents($info);
$json_data = json_decode($json_string);
} finally {
fclose($info);
}
} else {
throw new SCoreException("Could not get " . self::EXPORT_INFO_FILE_NAME . " from archive");
}
$total = 0;
$skipped = 0;
$failed = 0;
$database->commit();
while (!empty($json_data)) {
$item = array_pop($json_data);
$database->begin_transaction();
try {
$image = Image::by_hash($item->hash);
if ($image!=null) {
$skipped++;
log_info(BulkImportExportInfo::KEY, "Image $item->hash already present, skipping");
$database->commit();
continue;
}
$tmpfile = tempnam(sys_get_temp_dir(), "shimmie_bulk_import");
$stream = $zip->getStream($item->hash);
if ($zip === false) {
throw new SCoreException("Could not import " . $item->hash . ": File not in zip");
}
file_put_contents($tmpfile, $stream);
$id = add_image($tmpfile, $item->filename, Tag::implode($item->tags));
if ($id==-1) {
throw new SCoreException("Unable to import file $item->hash");
}
$image = Image::by_id($id);
if ($image==null) {
throw new SCoreException("Unable to import file $item->hash");
}
if ($item->source!=null) {
$image->set_source($item->source);
}
send_event(new BulkImportEvent($image, $item));
$database->commit();
$total++;
} catch (Exception $ex) {
$failed++;
try {
$database->rollBack();
} catch (Exception $ex2) {
log_error(BulkImportExportInfo::KEY, "Could not roll back transaction: " . $ex2->getMessage(), "Could not import " . $item->hash . ": " . $ex->getMessage());
}
log_error(BulkImportExportInfo::KEY, "Could not import " . $item->hash . ": " . $ex->getMessage(), "Could not import " . $item->hash . ": " . $ex->getMessage());
continue;
} finally {
if (!empty($tmpfile) && is_file($tmpfile)) {
unlink($tmpfile);
}
}
}
$event->image_id = -2; // default -1 = upload wasn't handled
log_info(
BulkImportExportInfo::KEY,
"Imported $total items, skipped $skipped, $failed failed",
"Imported $total items, skipped $skipped, $failed failed"
);
} else {
throw new SCoreException("Could not open zip archive");
}
}
}
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
{
global $user, $config;
if ($user->can(Permissions::BULK_EXPORT)) {
$event->add_action(self::EXPORT_ACTION_NAME, "Export");
}
}
public function onBulkAction(BulkActionEvent $event)
{
global $user, $page;
if ($user->can(Permissions::BULK_EXPORT) &&
($event->action == self::EXPORT_ACTION_NAME)) {
$download_filename = $user->name . '-' . date('YmdHis') . '.zip';
$zip_filename = tempnam(sys_get_temp_dir(), "shimmie_bulk_export");
$zip = new ZipArchive;
$json_data = [];
if ($zip->open($zip_filename, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE) === true) {
foreach ($event->items as $image) {
$img_loc = warehouse_path(Image::IMAGE_DIR, $image->hash, false);
$export_event = new BulkExportEvent($image);
send_event($export_event);
$data = $export_event->fields;
$data["hash"] = $image->hash;
$data["tags"] = $image->get_tag_array();
$data["filename"] = $image->filename;
$data["source"] = $image->source;
array_push($json_data, $data);
$zip->addFile($img_loc, $image->hash);
}
$json_data = json_encode($json_data, JSON_PRETTY_PRINT);
$zip->addFromString(self::EXPORT_INFO_FILE_NAME, $json_data);
$zip->close();
$page->set_mode(PageMode::FILE);
$page->set_file($zip_filename, true);
$page->set_filename($download_filename);
$event->redirect = false;
}
}
}
// we don't actually do anything, just accept one upload and spawn several
protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{
}
protected function check_contents(string $tmpname): bool
{
return false;
}
protected function create_thumb(string $hash, string $type): bool
{
return false;
}
}

View File

@ -210,7 +210,7 @@ class CommentList extends Extension
$cpe = new CommentPostingEvent(int_escape($_POST['image_id']), $user, $_POST['comment']);
send_event($cpe);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/$i_iid#comment_on_$i_iid"));
$page->set_redirect(make_link("post/view/$i_iid", null, "comment_on_$i_iid"));
} catch (CommentPostingException $ex) {
$this->theme->display_error(403, "Comment Blocked", $ex->getMessage());
}
@ -226,11 +226,7 @@ class CommentList extends Extension
send_event(new CommentDeletionEvent(int_escape($event->get_arg(1))));
$page->flash("Deleted comment");
$page->set_mode(PageMode::REDIRECT);
if (!empty($_SERVER['HTTP_REFERER'])) {
$page->set_redirect($_SERVER['HTTP_REFERER']);
} else {
$page->set_redirect(make_link("post/view/" . $event->get_arg(2)));
}
$page->set_redirect(referer_or(make_link("post/view/" . $event->get_arg(2))));
}
} else {
$this->theme->display_permission_denied();
@ -555,18 +551,9 @@ class CommentList extends Extension
'website' => '',
'body' => $text,
'permalink' => '',
];
# akismet breaks if there's no referrer in the environment; so if there
# isn't, supply one manually
if (!isset($_SERVER['HTTP_REFERER'])) {
$comment['referrer'] = 'none';
log_warning("comment", "User '{$user->name}' commented with no referrer: $text");
}
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
$comment['user_agent'] = 'none';
log_warning("comment", "User '{$user->name}' commented with no user-agent: $text");
}
'referrer' => $_SERVER['HTTP_REFERER'] ?? 'none',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'none',
];
$akismet = new Akismet(
$_SERVER['SERVER_NAME'],

View File

@ -234,7 +234,7 @@ class CommentListTheme extends Themelet
$html = "
<div class=\"comment $hb\">
$h_userlink: $h_comment
<a href=\"".make_link("post/view/$i_image_id#c$i_comment_id")."\">&gt;&gt;&gt;</a>
<a href=\"".make_link("post/view/$i_image_id", null, "c$i_comment_id")."\">&gt;&gt;&gt;</a>
</div>
";
} else {

View File

@ -3,22 +3,25 @@
abstract class CronUploaderConfig
{
const DEFAULT_PATH = "cron_uploader";
public const DEFAULT_PATH = "cron_uploader";
const KEY = "cron_uploader_key";
const COUNT = "cron_uploader_count";
const DIR = "cron_uploader_dir";
const USER = "cron_uploader_user";
public const KEY = "cron_uploader_key";
public const DIR = "cron_uploader_dir";
public const USER = "cron_uploader_user";
public const STOP_ON_ERROR = "cron_uploader_stop_on_error";
public const INCLUDE_ALL_LOGS = "cron_uploader_include_all_logs";
public const LOG_LEVEL = "cron_uploader_log_level";
public static function set_defaults(): void
{
global $config;
$config->set_default_int(self::COUNT, 1);
$config->set_default_string(self::DIR, data_path(self::DEFAULT_PATH));
$config->set_default_bool(self::INCLUDE_ALL_LOGS, false);
$config->set_default_bool(self::STOP_ON_ERROR, false);
$config->set_default_int(self::LOG_LEVEL, SCORE_LOG_INFO);
$upload_key = $config->get_string(self::KEY, "");
if (empty($upload_key)) {
$upload_key = self::generate_key();
$upload_key = generate_key();
$config->set_string(self::KEY, $upload_key);
}
@ -48,18 +51,6 @@ abstract class CronUploaderConfig
$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;
@ -76,21 +67,4 @@ abstract class CronUploaderConfig
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;
}
}

View File

@ -15,7 +15,7 @@ class CronUploader extends Extension
const UPLOADED_DIR = "uploaded";
const FAILED_DIR = "failed_to_upload";
public $output_buffer = [];
private static $IMPORT_RUNNING = false;
public function onInitExt(InitExtEvent $event)
{
@ -57,10 +57,18 @@ class CronUploader extends Extension
$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->add_bool_option(CronUploaderConfig::STOP_ON_ERROR, "Stop On Error", true);
$sb->add_choice_option(CronUploaderConfig::LOG_LEVEL, [
LOGGING_LEVEL_NAMES[SCORE_LOG_DEBUG] => SCORE_LOG_DEBUG,
LOGGING_LEVEL_NAMES[SCORE_LOG_INFO] => SCORE_LOG_INFO,
LOGGING_LEVEL_NAMES[SCORE_LOG_WARNING] => SCORE_LOG_WARNING,
LOGGING_LEVEL_NAMES[SCORE_LOG_ERROR] => SCORE_LOG_ERROR,
LOGGING_LEVEL_NAMES[SCORE_LOG_CRITICAL] => SCORE_LOG_CRITICAL,
], "Output Log Level: ", true);
$sb->add_bool_option(CronUploaderConfig::INCLUDE_ALL_LOGS, "Include All Logs", true);
$sb->end_table();
$sb->add_label("<a href='$documentation_link'>Read the documentation</a> for cron setup instructions.");
@ -108,6 +116,24 @@ class CronUploader extends Extension
}
}
public function onLog(LogEvent $event)
{
global $config;
$all = $config->get_bool(CronUploaderConfig::INCLUDE_ALL_LOGS);
if (self::$IMPORT_RUNNING &&
$event->priority >= $config->get_int(CronUploaderConfig::LOG_LEVEL) &&
($event->section==self::NAME || $all)
) {
$output = "[" . date('Y-m-d H:i:s') . "] " . ($all ? '['. $event->section .'] ' :'') . "[" . LOGGING_LEVEL_NAMES[$event->priority] . "] " . $event->message ;
echo $output . "\r\n";
flush_output();
$log_path = $this->get_log_file();
file_put_contents($log_path, $output);
}
}
private function restage_folder(string $folder)
{
global $page;
@ -123,33 +149,46 @@ class CronUploader extends Extension
$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);
$results = get_files_recursively($stage_dir);
if (count($results) == 0) {
if (rmdir($stage_dir)===false) {
if (remove_empty_dirs($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);
$new_path = join_path($queue_dir, substr($result, strlen($stage_dir)));
rename($original_path, $new_path);
if (file_exists($new_path)) {
$page->flash("File already exists in queue folder: " .$result);
return;
}
}
$page->flash("Re-staged $folder to queue");
rmdir($stage_dir);
$success = true;
foreach ($results as $result) {
$new_path = join_path($queue_dir, substr($result, strlen($stage_dir)));
$dir = dirname($new_path);
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
if (rename($result, $new_path)===false) {
$page->flash("Could not move file: " .$result);
$success = false;
}
}
if ($success===true) {
$page->flash("Re-staged $folder to queue");
if (remove_empty_dirs($stage_dir)===false) {
$page->flash("Could not remove $folder");
}
}
}
private function clear_folder($folder)
@ -265,7 +304,11 @@ class CronUploader extends Extension
*/
public function process_upload(string $key, ?int $upload_count = null): bool
{
global $database;
global $database, $config, $_shm_load_start;
$max_time = intval(ini_get('max_execution_time'))*.8;
$this->set_headers();
if ($key!=CronUploaderConfig::get_key()) {
throw new SCoreException("Cron upload key incorrect");
@ -287,24 +330,12 @@ class CronUploader extends Extension
throw new SCoreException("Cron upload process is already running");
}
self::$IMPORT_RUNNING = true;
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;
}
$image_queue = $this->generate_image_queue(CronUploaderConfig::get_dir());
// Randomize Images
//shuffle($this->image_queue);
@ -314,14 +345,18 @@ class CronUploader extends Extension
$failed = 0;
// Upload the file(s)
for ($i = 0; $i < $upload_count && sizeof($image_queue) > 0; $i++) {
$img = array_pop($image_queue);
foreach ($image_queue as $img) {
$execution_time = microtime(true) - $_shm_load_start;
if ($execution_time>$max_time) {
break;
}
try {
$database->beginTransaction();
$database->begin_transaction();
$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();
if ($database->is_transaction_open()) {
$database->commit();
}
$this->move_uploaded($img[0], $img[1], $output_subdir, false);
if ($result->merged) {
$merged++;
@ -330,28 +365,37 @@ class CronUploader extends Extension
}
} catch (Exception $e) {
try {
$database->rollback();
if ($database->is_transaction_open()) {
$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());
if ($config->get_bool(CronUploaderConfig::STOP_ON_ERROR)) {
break;
} else {
$this->move_uploaded($img[0], $img[1], $output_subdir, true);
}
}
}
// Throw exception if there's nothing in the queue
if ($merged+$failed+$added === 0) {
$this->log_message(SCORE_LOG_WARNING, "Your queue is empty so nothing could be uploaded.");
return false;
}
$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 {
self::$IMPORT_RUNNING = false;
flock($lockfile, LOCK_UN);
fclose($lockfile);
}
@ -359,7 +403,13 @@ class CronUploader extends Extension
private function move_uploaded(string $path, string $filename, string $output_subdir, bool $corrupt = false)
{
$relativeDir = dirname(substr($path, strlen(CronUploaderConfig::get_dir()) + 7));
$rootDir = CronUploaderConfig::get_dir();
$rootLength = strlen($rootDir);
if ($rootDir[$rootLength-1]=="/"||$rootDir[$rootLength-1]=="\\") {
$rootLength--;
}
$relativeDir = dirname(substr($path, $rootLength + 7));
if ($relativeDir==".") {
$relativeDir = "";
@ -405,7 +455,7 @@ class CronUploader extends Extension
if (array_key_exists('extension', $pathinfo)) {
$metadata ['extension'] = $pathinfo ['extension'];
}
$metadata ['tags'] = $tagArray; // doesn't work when not logged in here, handled below
$metadata ['tags'] = $tagArray;
$metadata ['source'] = null;
$event = new DataUploadEvent($tmpname, $metadata);
send_event($event);
@ -420,34 +470,46 @@ class CronUploader extends Extension
}
$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 const SKIPPABLE_FILES = ['.ds_store','thumbs.db'];
private const SKIPPABLE_DIRECTORIES = ['__macosx'];
private function is_skippable_file(string $path)
private function is_skippable_dir(string $path)
{
$info = pathinfo($path);
if (in_array(strtolower($info['extension']), self::PARTIAL_DOWNLOAD_EXTENSIONS)) {
if (array_key_exists("basename", $info) && in_array(strtolower($info['basename']), self::SKIPPABLE_DIRECTORIES)) {
return true;
}
return false;
}
private function generate_image_queue(string $root_dir, ?int $limit = null): array
private function is_skippable_file(string $path)
{
$info = pathinfo($path);
if (array_key_exists("basename", $info) && in_array(strtolower($info['basename']), self::SKIPPABLE_FILES)) {
return true;
}
if (array_key_exists("extension", $info) && in_array(strtolower($info['extension']), self::PARTIAL_DOWNLOAD_EXTENSIONS)) {
return true;
}
return false;
}
private function generate_image_queue(string $root_dir, ?int $limit = null): Generator
{
$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 [];
return;
}
$ite = new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS);
@ -458,31 +520,19 @@ class CronUploader extends Extension
$relativePath = substr($fullpath, strlen($base));
$tags = path_to_tags($relativePath);
$img = [
yield [
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
@ -490,16 +540,12 @@ class CronUploader extends Extension
return join_path(CronUploaderConfig::get_dir(), "uploads.log");
}
/**
* This is run at the end to display & save the log.
*/
private function handle_log()
private function set_headers(): void
{
global $page;
// Display message
$page->set_mode(PageMode::DATA);
$page->set_type("text/plain");
$page->set_data(implode("\r\n", $this->output_buffer));
$page->set_mode(PageMode::MANUAL);
$page->set_type(MIME_TYPE_TEXT);
$page->send_headers();
}
}

View File

@ -57,6 +57,9 @@ class CronUploaderTheme extends Themelet
<br />When you create the cron job, you choose when to upload new images.</li>
</ol>";
$max_time = intval(ini_get('max_execution_time'))*.8;
$usage_html = "Upload your images you want to be uploaded to the queue directory using your FTP client or other means.
<br />(<b>{$queue_dirinfo['path']}</b>)
<ol>
@ -71,7 +74,7 @@ class CronUploaderTheme extends Themelet
<ul>
<li>If an import is already running, another cannot start until it is done.</li>
<li>Each time it runs it will import up to ".CronUploaderConfig::get_count()." file(s). This is controlled from <a href='".make_link("setup")."'>Board Config</a>.</li>
<li>Each time it runs it will import for up to ".number_format($max_time)." seconds. This is controlled by the PHP max execution time.</li>
<li>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 <a href='".make_link("admin")."'>Board Admin</a>.</li>
<li>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</li>
</ul>
@ -107,7 +110,7 @@ class CronUploaderTheme extends Themelet
$html .= make_form(make_link("admin/cron_uploader_restage"));
$html .= "<table class='form'>";
$html .= "<tr><th>Failed dir</th><td><select name='failed_dir' required='required'><option></option>";
$html .= "<tr><th>Failed dir</th><td><select name='failed_dir' required='required'>";
foreach ($failed_dirs as $dir) {
$html .= "<option value='$dir'>$dir</option>";

View File

@ -10,13 +10,13 @@ class DanbooruApi extends Extension
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");
$page->set_type(MIME_TYPE_TEXT);
$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_type(MIME_TYPE_XML_APPLICATION);
$page->set_data($this->api_find_posts());
} elseif ($event->page_matches("api/danbooru/find_tags")) {
$page->set_type("application/xml");
$page->set_type(MIME_TYPE_XML_APPLICATION);
$page->set_data($this->api_find_tags());
}

View File

@ -36,10 +36,10 @@ class ET extends Extension
public function onCommand(CommandEvent $event)
{
if ($event->cmd == "help") {
print "\tget-info\n";
print "\tshimmie-info\n";
print "\t\tList a bunch of info\n\n";
}
if ($event->cmd == "info") {
if ($event->cmd == "shimmie-info") {
print($this->to_yaml($this->get_info()));
}
}
@ -63,7 +63,7 @@ class ET extends Extension
"about" => [
'title' => $config->get_string(SetupConfig::TITLE),
'theme' => $config->get_string(SetupConfig::THEME),
'url' => "http://" . $_SERVER["HTTP_HOST"] . get_base_href(),
'url' => make_http(make_link("/")),
],
"versions" => [
'shimmie' => VERSION,
@ -71,7 +71,7 @@ class ET extends Extension
'php' => phpversion(),
'db' => $database->get_driver_name() . " " . $database->get_version(),
'os' => php_uname(),
'server' => isset($_SERVER["SERVER_SOFTWARE"]) ? $_SERVER["SERVER_SOFTWARE"] : 'unknown',
'server' => $_SERVER["SERVER_SOFTWARE"] ?? 'unknown',
],
"extensions" => [
"core" => $core_exts,
@ -98,6 +98,21 @@ class ET extends Extension
],
];
if (file_exists(".git")) {
try {
$commitHash = trim(exec('git log --pretty="%h" -n1 HEAD'));
$commitBranch= trim(exec('git rev-parse --abbrev-ref HEAD'));
$commitOrigin= trim(exec('git config --get remote.origin.url'));
$commitOrigin= preg_replace("#//.*@#", "//xxx@", $commitOrigin);
$info['git'] = [
'commit' => $commitHash,
'branch' => $commitBranch,
'origin' => $commitOrigin,
];
} catch (Exception $e) {
}
}
return $info;
}

View File

@ -1,5 +1,8 @@
<?php declare(strict_types=1);
use function MicroHTML\INPUT;
use function MicroHTML\DIV;
use function MicroHTML\A;
use function MicroHTML\IMG;
class FeaturedTheme extends Themelet
{
@ -19,16 +22,21 @@ class FeaturedTheme extends Themelet
public function build_featured_html(Image $image, ?string $query=null): string
{
$i_id = $image->id;
$h_view_link = make_link("post/view/$i_id", $query);
$h_thumb_link = $image->get_thumb_link();
$h_tip = html_escape($image->get_tooltip());
$tsize = get_thumbnail_size($image->width, $image->height);
return "
<a href='$h_view_link'>
<img id='thumb_{$i_id}' title='{$h_tip}' alt='{$h_tip}' class='highlighted' style='height: {$tsize[1]}px; width: {$tsize[0]}px;' src='{$h_thumb_link}'>
</a>
";
return (string)DIV(
["style"=>"text-align: center;"],
A(
["href"=>make_link("post/view/{$image->id}", $query)],
IMG([
"id"=>"thumb_rand_{$image->id}",
"title"=>$image->get_tooltip(),
"alt"=>$image->get_tooltip(),
"class"=>'highlighted',
"style"=>"max-height: {$tsize[1]}px; max-width: 100%;",
"src"=>$image->get_thumb_link()
])
)
);
}
}

View File

@ -9,7 +9,7 @@ class FourOhFourInfo extends ExtensionInfo
public $url = self::SHIMMIE_URL;
public $authors = self::SHISH_AUTHOR;
public $license = self::LICENSE_GPLV2;
public $visibility = self::VISIBLE_ADMIN;
public $visibility = self::VISIBLE_HIDDEN;
public $description = "If no other extension puts anything onto the page, show 404";
public $core = true;
}

View File

@ -2,7 +2,7 @@
class ArchiveFileHandler extends DataHandlerExtension
{
protected $SUPPORTED_EXT = ["zip"];
protected $SUPPORTED_MIME = [MIME_TYPE_ZIP];
public function onInitExt(InitExtEvent $event)
{

View File

@ -2,7 +2,7 @@
class CBZFileHandler extends DataHandlerExtension
{
public $SUPPORTED_EXT = ["cbz"];
public $SUPPORTED_MIME = [MIME_TYPE_COMIC_ZIP];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{
@ -27,7 +27,7 @@ class CBZFileHandler extends DataHandlerExtension
$cover,
warehouse_path(Image::THUMBNAIL_DIR, $hash),
get_thumbnail_max_size_scaled(),
get_extension(getMimeType($cover)),
get_extension(get_mime($cover)),
null
);
return true;

View File

@ -2,7 +2,7 @@
class FlashFileHandler extends DataHandlerExtension
{
protected $SUPPORTED_EXT = ["swf"];
protected $SUPPORTED_MIME = [MIME_TYPE_FLASH];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{

View File

@ -2,7 +2,7 @@
class IcoFileHandler extends DataHandlerExtension
{
protected $SUPPORTED_EXT = ["ico", "ani", "cur"];
protected $SUPPORTED_MIME = [MIME_TYPE_ICO, MIME_TYPE_ANI];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{

View File

@ -2,7 +2,7 @@
class MP3FileHandler extends DataHandlerExtension
{
protected $SUPPORTED_EXT = ["mp3"];
protected $SUPPORTED_MIME = [MIME_TYPE_MP3];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{
@ -23,6 +23,6 @@ class MP3FileHandler extends DataHandlerExtension
protected function check_contents(string $tmpname): bool
{
return getMimeType($tmpname) == 'audio/mpeg';
return get_mime($tmpname) === MIME_TYPE_MP3;
}
}

View File

@ -2,13 +2,13 @@
class PixelFileHandler extends DataHandlerExtension
{
protected $SUPPORTED_EXT = ["jpg", "jpeg", "gif", "png", "webp"];
protected $SUPPORTED_MIME = [MIME_TYPE_JPEG, MIME_TYPE_GIF, MIME_TYPE_PNG, MIME_TYPE_WEBP];
protected function media_check_properties(MediaCheckPropertiesEvent $event): void
{
if (in_array($event->ext, Media::LOSSLESS_FORMATS)) {
$event->image->lossless = true;
} elseif ($event->ext=="webp") {
} elseif ($event->ext==EXTENSION_WEBP) {
$event->image->lossless = Media::is_lossless_webp($event->file_name);
}
@ -17,10 +17,10 @@ class PixelFileHandler extends DataHandlerExtension
}
$event->image->audio = false;
switch ($event->ext) {
case "gif":
case EXTENSION_GIF:
$event->image->video = Media::is_animated_gif($event->file_name);
break;
case "webp":
case EXTENSION_WEBP:
$event->image->video = Media::is_animated_webp($event->file_name);
break;
default:

View File

@ -3,7 +3,7 @@ use enshrined\svgSanitize\Sanitizer;
class SVGFileHandler extends DataHandlerExtension
{
protected $SUPPORTED_EXT = ["svg"];
protected $SUPPORTED_MIME = [MIME_TYPE_SVG];
/** @var SVGFileHandlerTheme */
protected $theme;
@ -16,7 +16,7 @@ class SVGFileHandler extends DataHandlerExtension
$image = Image::by_id($id);
$hash = $image->hash;
$page->set_type("image/svg+xml");
$page->set_type(MIME_TYPE_SVG);
$page->set_mode(PageMode::DATA);
$sanitizer = new Sanitizer();
@ -67,7 +67,7 @@ class SVGFileHandler extends DataHandlerExtension
protected function check_contents(string $file): bool
{
if (getMimeType($file)!="image/svg+xml") {
if (get_mime($file)!==MIME_TYPE_SVG) {
return false;
}

View File

@ -1,30 +1,55 @@
<?php declare(strict_types=1);
abstract class VideoFileHandlerConfig
{
public const PLAYBACK_AUTOPLAY = "video_playback_autoplay";
public const PLAYBACK_LOOP = "video_playback_loop";
public const ENABLED_FORMATS = "video_enabled_formats";
}
class VideoFileHandler extends DataHandlerExtension
{
protected $SUPPORTED_MIME = [
'video/webm',
'video/mp4',
'video/ogg',
'video/flv',
'video/x-flv'
public const SUPPORTED_MIME = [
MIME_TYPE_ASF,
MIME_TYPE_AVI,
MIME_TYPE_FLASH_VIDEO,
MIME_TYPE_MKV,
MIME_TYPE_MP4_VIDEO,
MIME_TYPE_OGG_VIDEO,
MIME_TYPE_QUICKTIME,
MIME_TYPE_WEBM,
];
protected $SUPPORTED_EXT = ["flv", "mp4", "m4v", "ogv", "webm"];
protected $SUPPORTED_MIME = self::SUPPORTED_MIME;
public function onInitExt(InitExtEvent $event)
{
global $config;
$config->set_default_bool('video_playback_autoplay', true);
$config->set_default_bool('video_playback_loop', true);
$config->set_default_bool(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY, true);
$config->set_default_bool(VideoFileHandlerConfig::PLAYBACK_LOOP, true);
$config->set_default_array(
VideoFileHandlerConfig::ENABLED_FORMATS,
[MIME_TYPE_FLASH_VIDEO, MIME_TYPE_MP4_VIDEO, MIME_TYPE_OGG_VIDEO, MIME_TYPE_WEBM]
);
}
private function get_options(): array
{
$output = [];
foreach ($this->SUPPORTED_MIME as $format) {
$output[MIME_TYPE_MAP[$format][MIME_TYPE_MAP_NAME]] = $format;
}
return $output;
}
public function onSetupBuilding(SetupBuildingEvent $event)
{
$sb = new SetupBlock("Video Options");
$sb->add_bool_option("video_playback_autoplay", "Autoplay: ");
$sb->add_bool_option(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY, "Autoplay: ");
$sb->add_label("<br>");
$sb->add_bool_option("video_playback_loop", "Loop: ");
$sb->add_bool_option(VideoFileHandlerConfig::PLAYBACK_LOOP, "Loop: ");
$sb->add_label("<br>Enabled Formats:");
$sb->add_multichoice_option(VideoFileHandlerConfig::ENABLED_FORMATS, $this->get_options());
$event->panel->add_block($sb);
}
@ -80,6 +105,19 @@ class VideoFileHandler extends DataHandlerExtension
}
}
protected function supported_ext(string $ext): bool
{
global $config;
$enabled_formats = $config->get_array(VideoFileHandlerConfig::ENABLED_FORMATS);
foreach ($enabled_formats as $format) {
if (in_array($ext, MIME_TYPE_MAP[$format][MIME_TYPE_MAP_EXT])) {
return true;
}
}
return false;
}
protected function create_thumb(string $hash, string $type): bool
{
return Media::create_thumbnail_ffmpeg($hash);
@ -87,6 +125,18 @@ class VideoFileHandler extends DataHandlerExtension
protected function check_contents(string $tmpname): bool
{
return in_array(getMimeType($tmpname), $this->SUPPORTED_MIME);
global $config;
if (file_exists($tmpname)) {
$mime = get_mime($tmpname);
$enabled_formats = $config->get_array(VideoFileHandlerConfig::ENABLED_FORMATS);
foreach ($enabled_formats as $format) {
if (in_array($mime, MIME_TYPE_MAP[$format][MIME_TYPE_MAP_MIME])) {
return true;
}
}
}
return false;
}
}

View File

@ -9,17 +9,27 @@ class VideoFileHandlerTheme extends Themelet
$thumb_url = make_http($image->get_thumb_link()); //used as fallback image
$ext = strtolower($image->get_ext());
$full_url = make_http($ilink);
$autoplay = $config->get_bool("video_playback_autoplay");
$loop = $config->get_bool("video_playback_loop");
$autoplay = $config->get_bool(VideoFileHandlerConfig::PLAYBACK_AUTOPLAY);
$loop = $config->get_bool(VideoFileHandlerConfig::PLAYBACK_LOOP);
$player = make_link('vendor/bower-asset/mediaelement/build/flashmediaelement.swf');
$width="auto";
if ($image->width>1) {
$width = $image->width."px";
}
$height="auto";
if ($image->height>1) {
$height = $image->height."px";
}
$html = "Video not playing? <a href='$ilink'>Click here</a> to download the file.<br/>";
//Browser media format support: https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
$supportedExts = ['mp4' => 'video/mp4', 'm4v' => 'video/mp4', 'ogv' => 'video/ogg', 'webm' => 'video/webm', 'flv' => 'video/flv'];
if (array_key_exists($ext, $supportedExts)) {
$mime = get_mime_for_extension($ext);
if (in_array($mime, VideoFileHandler::SUPPORTED_MIME)) {
//FLV isn't supported by <video>, but it should always fallback to the flash-based method.
if ($ext == "webm") {
if ($mime == MIME_TYPE_WEBM) {
//Several browsers still lack WebM support sadly: https://caniuse.com/#feat=webm
$html .= "<!--[if IE]><p>To view webm files with IE, please <a href='https://tools.google.com/dlpage/webmmf/' target='_blank'>download this plugin</a>.</p><![endif]-->";
}
@ -40,7 +50,7 @@ class VideoFileHandlerTheme extends Themelet
<img alt='thumb' src=\"{$thumb_url}\" />
</object>";
if ($ext == "flv") {
if ($mime == MIME_TYPE_FLASH_VIDEO) {
//FLV doesn't support <video>.
$html .= $html_fallback;
} else {
@ -49,8 +59,8 @@ class VideoFileHandlerTheme extends Themelet
$html .= "
<video controls class='shm-main-image' id='main_image' alt='main image' poster='$thumb_url' {$autoplay} {$loop}
style='max-width: 100%'>
<source src='{$ilink}' type='{$supportedExts[$ext]}'>
style='height: $height; width: $width; max-width: 100%'>
<source src='{$ilink}' type='{$mime}'>
<!-- If browser doesn't support filetype, fallback to flash -->
{$html_fallback}
@ -60,7 +70,7 @@ class VideoFileHandlerTheme extends Themelet
}
} else {
//This should never happen, but just in case let's have a fallback..
$html = "Video type '$ext' not recognised";
$html = "Video type '$mime' not recognised";
}
$page->add_block(new Block("Video", $html, "main", 10));
}

View File

@ -9,5 +9,6 @@ class HelpPagesInfo extends ExtensionInfo
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL;
public $description = "Provides documentation screens";
public $visibility = self::VISIBLE_HIDDEN;
public $core = true;
}

View File

@ -2,7 +2,6 @@
class HelpPagesTest extends ShimmiePHPUnitTestCase
{
public function test_list()
{
send_event(new HelpPageListBuildingEvent());

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -5,6 +5,22 @@ class Home extends Extension
/** @var HomeTheme */
protected $theme;
private $femDimensions = array(
array(23, 64),
array(14, 86),
array(31, 63),
array(37, 100),
array(24, 90)
);
private $femTags = array(
"Hatsune_Miku",
"Monika",
"Violet_Parr",
"Keith_Kogane",
"Rin_Kagamine",
);
public function onPageRequest(PageRequestEvent $event)
{
global $config, $page;
@ -34,6 +50,27 @@ class Home extends Extension
$event->panel->add_block($sb);
}
private function addCountToBlankImage($charId, $digit)
{
$font = realpath('ext/home/vga.ttf');
$file = "ext/home/counters/femcounter/$charId.png";
$img = imagecreatefrompng($file);
$black = imagecolorallocate($img, 0, 0, 0);
$x = $this->femDimensions[$charId][0];
$y = $this->femDimensions[$charId][1];
imagettftext($img, 20, 0, $x, $y + 20, $black, $font, $digit);
imagetruecolortopalette($img, true, 16);
imagesavealpha($img, true);
imagecolortransparent($img, imagecolorat($img, 0, 0));
ob_start();
imagegif($img);
$image_data = ob_get_contents();
ob_end_clean();
$data = base64_encode($image_data);
imagedestroy($img);
return $data;
}
private function get_body()
{
@ -48,14 +85,23 @@ class Home extends Extension
$counter_dir = $config->get_string('home_counter', 'default');
$total = Image::count_images();
$streak = Image::count_upload_streak();
$strtotal = "$total";
$num_comma = number_format($total);
$streak_comma = number_format($streak);
$counter_text = "";
$length = strlen($strtotal);
for ($n=0; $n<$length; $n++) {
$cur = $strtotal[$n];
$counter_text .= " <img alt='$cur' src='$base_href/ext/home/counters/$counter_dir/$cur.gif' /> ";
if ($counter_dir === 'femcounter') {
$charId = $n % count($this->femTags);
$base64url = $this->addCountToBlankImage($charId, $cur);
$tag = $this->femTags[$charId];
$counter_text .= " <a href='$base_href/post/list/$tag/1'><img alt='$cur' title='$tag' src='data:image/gif;base64,$base64url' /></a> ";
} else {
$counter_text .= " <img alt='$cur' src='$base_href/ext/home/counters/$counter_dir/$cur.gif' /> ";
}
}
// get the homelinks and process them
@ -74,6 +120,6 @@ class Home extends Extension
$main_links = format_text($main_links);
$main_text = $config->get_string('home_text', '');
return $this->theme->build_body($sitename, $main_links, $main_text, $contact_link, $num_comma, $counter_text);
return $this->theme->build_body($sitename, $main_links, $main_text, $contact_link, $num_comma, $counter_text, $streak_comma);
}
}

View File

@ -5,7 +5,7 @@ div#front-page div#links a {margin: 0 0.5em;}
div#front-page li {list-style-type: none; margin: 0;}
@media (max-width: 800px) {
div#front-page h1 {font-size: 3em; margin-top: 0.5em; margin-bottom: 0.5em;}
#counter {display: none;}
/*#counter {display: none;}*/
}
div#front-page > #search > form { margin: 0 auto; }

BIN
ext/home/vga.ttf Normal file

Binary file not shown.

View File

@ -8,6 +8,7 @@ abstract class ImageConfig
const THUMB_SCALING = 'thumb_scaling';
const THUMB_QUALITY = 'thumb_quality';
const THUMB_TYPE = 'thumb_type';
const THUMB_FIT = 'thumb_fit';
const SHOW_META = 'image_show_meta';
const ILINK = 'image_ilink';

View File

@ -10,6 +10,6 @@ class ImageIOInfo extends ExtensionInfo
public $authors = [self::SHISH_NAME=> self::SHISH_EMAIL, "jgen"=>"jgen.tech@gmail.com"];
public $license = self::LICENSE_GPLV2;
public $description = "Handle the image database";
public $visibility = self::VISIBLE_ADMIN;
public $visibility = self::VISIBLE_HIDDEN;
public $core = true;
}

View File

@ -21,8 +21,8 @@ class ImageIO extends Extension
];
const THUMBNAIL_TYPES = [
'JPEG' => "jpg",
'WEBP (Not IE/Safari compatible)' => "webp"
'JPEG' => EXTENSION_JPG,
'WEBP (Not IE/Safari compatible)' => EXTENSION_WEBP
];
public function onInitExt(InitExtEvent $event)
@ -32,7 +32,8 @@ class ImageIO extends Extension
$config->set_default_int(ImageConfig::THUMB_HEIGHT, 192);
$config->set_default_int(ImageConfig::THUMB_SCALING, 100);
$config->set_default_int(ImageConfig::THUMB_QUALITY, 75);
$config->set_default_string(ImageConfig::THUMB_TYPE, 'jpg');
$config->set_default_string(ImageConfig::THUMB_TYPE, EXTENSION_JPG);
$config->set_default_string(ImageConfig::THUMB_FIT, Media::RESIZE_TYPE_FIT);
if (function_exists(self::EXIF_READ_FUNCTION)) {
$config->set_default_bool(ImageConfig::SHOW_META, false);
@ -53,17 +54,13 @@ class ImageIO extends Extension
if ($image) {
send_event(new ImageDeletionEvent($image));
$page->set_mode(PageMode::REDIRECT);
if (isset($_SERVER['HTTP_REFERER']) && !strstr($_SERVER['HTTP_REFERER'], 'post/view')) {
$page->set_redirect($_SERVER['HTTP_REFERER']);
} else {
$page->set_redirect(make_link("post/list"));
}
$page->set_redirect(referer_or(make_link("post/list"), ['post/view']));
}
}
} elseif ($event->page_matches("image/replace")) {
global $page, $user;
if ($user->can(Permissions::REPLACE_IMAGE) && isset($_POST['image_id']) && $user->check_auth_token()) {
$image = Image::by_id($_POST['image_id']);
$image = Image::by_id(int_escape($_POST['image_id']));
if ($image) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link('upload/replace/'.$image->id));
@ -219,35 +216,41 @@ class ImageIO extends Extension
public function onSetupBuilding(SetupBuildingEvent $event)
{
global $config;
$sb = new SetupBlock("Image Options");
$sb->start_table();
$sb->position = 30;
// advanced only
//$sb->add_text_option(ImageConfig::ILINK, "Image link: ");
//$sb->add_text_option(ImageConfig::TLINK, "<br>Thumbnail link: ");
$sb->add_text_option(ImageConfig::TIP, "Image tooltip: ");
$sb->add_choice_option(ImageConfig::UPLOAD_COLLISION_HANDLER, self::COLLISION_OPTIONS, "<br>Upload collision handler: ");
$sb->add_text_option(ImageConfig::TIP, "Image tooltip", true);
$sb->add_choice_option(ImageConfig::UPLOAD_COLLISION_HANDLER, self::COLLISION_OPTIONS, "Upload collision handler", true);
if (function_exists(self::EXIF_READ_FUNCTION)) {
$sb->add_bool_option(ImageConfig::SHOW_META, "<br>Show metadata: ");
$sb->add_bool_option(ImageConfig::SHOW_META, "Show metadata", true);
}
$sb->end_table();
$event->panel->add_block($sb);
$sb = new SetupBlock("Thumbnailing");
$sb->add_choice_option(ImageConfig::THUMB_ENGINE, self::THUMBNAIL_ENGINES, "Engine: ");
$sb->add_label("<br>");
$sb->add_choice_option(ImageConfig::THUMB_TYPE, self::THUMBNAIL_TYPES, "Filetype: ");
$sb->start_table();
$sb->add_choice_option(ImageConfig::THUMB_ENGINE, self::THUMBNAIL_ENGINES, "Engine", true);
$sb->add_choice_option(ImageConfig::THUMB_TYPE, self::THUMBNAIL_TYPES, "Filetype", true);
$sb->add_label("<br>Size ");
$sb->add_int_option(ImageConfig::THUMB_WIDTH);
$sb->add_label(" x ");
$sb->add_int_option(ImageConfig::THUMB_HEIGHT);
$sb->add_label(" px at ");
$sb->add_int_option(ImageConfig::THUMB_QUALITY);
$sb->add_label(" % quality ");
$sb->add_int_option(ImageConfig::THUMB_WIDTH, "Max Width", true);
$sb->add_int_option(ImageConfig::THUMB_HEIGHT, "Max Height", true);
$sb->add_label("<br>High-DPI scaling ");
$sb->add_int_option(ImageConfig::THUMB_SCALING);
$sb->add_label("%");
$options = [];
foreach (MediaEngine::RESIZE_TYPE_SUPPORT[$config->get_string(ImageConfig::THUMB_ENGINE)] as $type) {
$options[$type] = $type;
}
$sb->add_choice_option(ImageConfig::THUMB_FIT, $options, "Fit", true);
$sb->add_int_option(ImageConfig::THUMB_QUALITY, "Quality", true);
$sb->add_int_option(ImageConfig::THUMB_SCALING, "High-DPI Scale %", true);
$sb->end_table();
$event->panel->add_block($sb);
}
@ -275,11 +278,7 @@ class ImageIO extends Extension
if (!is_null($image)) {
if ($type == "thumb") {
$ext = $config->get_string(ImageConfig::THUMB_TYPE);
if (array_key_exists($ext, MIME_TYPE_MAP)) {
$page->set_type(MIME_TYPE_MAP[$ext]);
} else {
$page->set_type("image/jpeg");
}
$page->set_type(get_mime_for_extension($ext));
$file = $image->get_thumb_filename();
} else {
@ -287,6 +286,12 @@ class ImageIO extends Extension
$file = $image->get_image_filename();
}
if (!file_exists($file)) {
http_response_code(404);
die();
}
if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])) {
$if_modified_since = preg_replace('/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"]);
} else {

View File

@ -20,4 +20,23 @@ class ImageIOTest extends ShimmiePHPUnitTestCase
$page = $this->get_page("thumb/$image_id/moo.jpg");
$this->assertEquals(200, $page->code);
}
public function testDeleteRequest()
{
$this->log_in_as_admin();
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test");
$_POST['image_id'] = "$image_id";
send_event(new PageRequestEvent("image/delete"));
$this->assertTrue(true); // FIXME: assert image was deleted?
}
public function testReplaceRequest()
{
global $page;
$this->log_in_as_admin();
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test");
$_POST['image_id'] = "$image_id";
send_event(new PageRequestEvent("image/replace"));
$this->assertEquals("redirect", $page->mode);
}
}

View File

@ -105,7 +105,7 @@ class ImageBan extends Extension
}
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER['HTTP_REFERER']);
$page->set_redirect(referer_or(make_link()));
}
} elseif ($event->get_arg(0) == "remove") {
$user->ensure_authed();
@ -113,7 +113,7 @@ class ImageBan extends Extension
send_event(new RemoveImageHashBanEvent($input['d_hash']));
$page->flash("Image ban removed");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER['HTTP_REFERER']);
$page->set_redirect(referer_or(make_link()));
} elseif ($event->get_arg(0) == "list") {
$t = new HashBanTable($database->raw_db());
$t->token = $user->get_auth_token();

View File

@ -3,6 +3,13 @@ class ImageBanTest extends ShimmiePHPUnitTestCase
{
private $hash = "feb01bab5698a11dd87416724c7a89e3";
public function testPages()
{
$this->log_in_as_admin();
$page = $this->get_page("image_hash_ban/list");
$this->assertEquals(200, $page->code);
}
public function testBan()
{
$this->log_in_as_admin();

View File

@ -245,11 +245,18 @@ class Index extends Extension
Image::$order_sql = "images.$ord $sort";
$event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag
} elseif (preg_match("/^order[=|:]random[_]([0-9]{1,4})$/i", $event->term, $matches)) {
//order[=|:]random requires a seed to avoid duplicates
//since the tag can't be changed during the parseevent, we instead generate the seed during submit using js
// requires a seed to avoid duplicates
// since the tag can't be changed during the parseevent, we instead generate the seed during submit using js
$seed = $matches[1];
Image::$order_sql = "RAND($seed)";
$event->add_querylet(new Querylet("1=1")); //small hack to avoid metatag being treated as normal tag
} elseif (preg_match("/^order[=|:]dailyshuffle$/i", $event->term, $matches)) {
// will use today's date as seed, thus allowing for a dynamic randomized list without outside intervention.
// This way the list will change every day, giving a more dynamic feel to the imageboard.
// recommended to change homepage to "post/list/order:dailyshuffle/1"
$seed = date("Ymd");
Image::$order_sql = "RAND($seed)";
$event->add_querylet(new Querylet("1=1"));
}
$this->stpen++;

View File

@ -197,7 +197,10 @@ class IndexTest extends ShimmiePHPUnitTestCase
{
send_event(new UserLoginEvent(User::by_name(self::$user_name)));
send_event(new PageNavBuildingEvent());
send_event(new PageSubNavBuildingEvent("parent"));
// just a few common parents
foreach (["help", "posts", "system", "user"] as $parent) {
send_event(new PageSubNavBuildingEvent($parent));
}
$this->assertTrue(true);
}
}

View File

@ -234,11 +234,11 @@ class LogDatabase extends Extension
{
$sb = new SetupBlock("Logging (Database)");
$sb->add_choice_option("log_db_priority", [
"Debug" => SCORE_LOG_DEBUG,
"Info" => SCORE_LOG_INFO,
"Warning" => SCORE_LOG_WARNING,
"Error" => SCORE_LOG_ERROR,
"Critical" => SCORE_LOG_CRITICAL,
LOGGING_LEVEL_NAMES[SCORE_LOG_DEBUG] => SCORE_LOG_DEBUG,
LOGGING_LEVEL_NAMES[SCORE_LOG_INFO] => SCORE_LOG_INFO,
LOGGING_LEVEL_NAMES[SCORE_LOG_WARNING] => SCORE_LOG_WARNING,
LOGGING_LEVEL_NAMES[SCORE_LOG_ERROR] => SCORE_LOG_ERROR,
LOGGING_LEVEL_NAMES[SCORE_LOG_CRITICAL] => SCORE_LOG_CRITICAL,
], "Debug Level: ");
$event->panel->add_block($sb);
}

View File

@ -11,8 +11,8 @@ class MediaResizeEvent extends Event
public $target_height;
public $target_quality;
public $minimize;
public $ignore_aspect_ratio;
public $allow_upscale;
public $resize_type;
public function __construct(
String $engine,
@ -21,7 +21,7 @@ class MediaResizeEvent extends Event
string $output_path,
int $target_width,
int $target_height,
bool $ignore_aspect_ratio = false,
string $resize_type = Media::RESIZE_TYPE_FIT,
string $target_format = null,
int $target_quality = 80,
bool $minimize = false,
@ -38,8 +38,8 @@ class MediaResizeEvent extends Event
$this->target_format = $target_format;
$this->target_quality = $target_quality;
$this->minimize = $minimize;
$this->ignore_aspect_ratio = $ignore_aspect_ratio;
$this->allow_upscale = $allow_upscale;
$this->resize_type = $resize_type;
}
}

View File

@ -26,28 +26,32 @@ class Media extends Extension
const LOSSLESS_FORMATS = [
self::WEBP_LOSSLESS,
"png",
"psd",
"bmp",
"ico",
"cur",
"ani",
"gif"
EXTENSION_PNG,
EXTENSION_PSD,
EXTENSION_BMP,
EXTENSION_ICO,
EXTENSION_CUR,
EXTENSION_ANI,
EXTENSION_GIF
];
const ALPHA_FORMATS = [
self::WEBP_LOSSLESS,
self::WEBP_LOSSY,
"webp",
"png",
EXTENSION_WEBP,
EXTENSION_PNG,
];
const FORMAT_ALIASES = [
"tif" => "tiff",
"jpeg" => "jpg",
EXTENSION_TIF => EXTENSION_TIFF,
EXTENSION_JPEG => EXTENSION_JPG,
];
const RESIZE_TYPE_FIT = "Fit";
const RESIZE_TYPE_FIT_BLUR = "Fit Blur";
const RESIZE_TYPE_FILL = "Fill";
const RESIZE_TYPE_STRETCH = "Stretch";
//RIFF####WEBPVP8?..............ANIM
private const WEBP_ANIMATION_HEADER =
@ -118,7 +122,7 @@ class Media extends Extension
$sb->add_text_option(MediaConfig::FFMPEG_PATH, "ffmpeg", true);
$sb->add_text_option(MediaConfig::FFPROBE_PATH, "ffprobe", true);
$sb->add_shorthand_int_option(MediaConfig::MEM_LIMIT, "Mem limit: ", true);
$sb->add_shorthand_int_option(MediaConfig::MEM_LIMIT, "Mem limit", true);
$sb->end_table();
$event->panel->add_block($sb);
@ -206,6 +210,10 @@ class Media extends Extension
*/
public function onMediaResize(MediaResizeEvent $event)
{
if (!in_array($event->resize_type, MediaEngine::RESIZE_TYPE_SUPPORT[MediaEngine::IMAGICK])) {
throw new MediaException("Resize type $event->resize_type not supported by selected media engine $event->engine");
}
switch ($event->engine) {
case MediaEngine::GD:
$info = getimagesize($event->input_path);
@ -220,7 +228,7 @@ class Media extends Extension
$event->target_height,
$event->output_path,
$event->target_format,
$event->ignore_aspect_ratio,
$event->resize_type,
$event->target_quality,
$event->allow_upscale
);
@ -236,7 +244,7 @@ class Media extends Extension
$event->target_height,
$event->output_path,
$event->target_format,
$event->ignore_aspect_ratio,
$event->resize_type,
$event->target_quality,
$event->minimize,
$event->allow_upscale
@ -356,6 +364,7 @@ class Media extends Extension
}
$inname = warehouse_path(Image::IMAGE_DIR, $hash);
$tmpname = tempnam("/tmp", "shimmie_ffmpeg_thumb");
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
$orig_size = self::video_size($inname);
@ -363,7 +372,7 @@ class Media extends Extension
$codec = "mjpeg";
$quality = $config->get_int(ImageConfig::THUMB_QUALITY);
if ($config->get_string(ImageConfig::THUMB_TYPE) == "webp") {
if ($config->get_string(ImageConfig::THUMB_TYPE) == EXTENSION_WEBP) {
$codec = "libwebp";
} else {
// mjpeg quality ranges from 2-31, with 2 being the best quality.
@ -376,12 +385,11 @@ class Media extends Extension
$args = [
escapeshellarg($ffmpeg),
"-y", "-i", escapeshellarg($inname),
"-vf", "thumbnail,scale={$scaled_size[0]}:{$scaled_size[1]}",
"-vf", "thumbnail",
"-f", "image2",
"-vframes", "1",
"-c:v", $codec,
"-q:v", $quality,
escapeshellarg($outname),
"-c:v", "png",
escapeshellarg($tmpname),
];
$cmd = escapeshellcmd(implode(" ", $args));
@ -390,6 +398,10 @@ class Media extends Extension
if ((int)$ret == (int)0) {
log_debug('media', "Generating thumbnail with command `$cmd`, returns $ret");
create_scaled_image($tmpname, $outname, $scaled_size, "png");
return true;
} else {
log_error('media', "Generating thumbnail with command `$cmd`, returns $ret");
@ -436,7 +448,7 @@ class Media extends Extension
switch ($format) {
case self::WEBP_LOSSLESS:
case self::WEBP_LOSSY:
return "webp";
return EXTENSION_WEBP;
default:
return $format;
}
@ -445,7 +457,7 @@ class Media extends Extension
// private static function image_save_imagick(Imagick $image, string $path, string $format, int $output_quality = 80, bool $minimize)
// {
// switch ($format) {
// case "png":
// case EXTENSION_PNG:
// $result = $image->setOption('png:compression-level', 9);
// if ($result !== true) {
// throw new GraphicsException("Could not set png compression option");
@ -555,7 +567,7 @@ class Media extends Extension
return true;
}
switch ($format) {
case "webp":
case EXTENSION_WEBP:
return self::is_lossless_webp($filename);
break;
}
@ -569,7 +581,7 @@ class Media extends Extension
int $new_height,
string $output_filename,
string $output_type = null,
bool $ignore_aspect_ratio = false,
string $resize_type = self::RESIZE_TYPE_FIT,
int $output_quality = 80,
bool $minimize = false,
bool $allow_upscale = true
@ -586,7 +598,7 @@ class Media extends Extension
$output_type = $input_type;
}
if ($output_type=="webp" && self::is_lossless($input_path, $input_type)) {
if ($output_type==EXTENSION_WEBP && self::is_lossless($input_path, $input_type)) {
$output_type = self::WEBP_LOSSLESS;
}
@ -598,36 +610,58 @@ class Media extends Extension
$input_type = $input_type . ":";
}
$resize_args = "";
$resize_suffix = "";
if (!$allow_upscale) {
$resize_args .= "\>";
$resize_suffix .= "\>";
}
if ($ignore_aspect_ratio) {
$resize_args .= "\!";
if ($resize_type==Media::RESIZE_TYPE_STRETCH) {
$resize_suffix .= "\!";
}
$args = "";
$resize_arg = "-resize";
if ($minimize) {
$args .= "-strip ";
$resize_arg = "-thumbnail";
}
$file_arg = "${input_type}\"${input_path}[0]\"";
switch ($resize_type) {
case Media::RESIZE_TYPE_FIT:
case Media::RESIZE_TYPE_STRETCH:
$args .= "${resize_arg} ${new_width}x${new_height}${resize_suffix} ${file_arg}";
break;
case Media::RESIZE_TYPE_FILL:
$args .= "${resize_arg} ${new_width}x${new_height}\^ -gravity center -extent ${new_width}x${new_height} ${file_arg}";
break;
case Media::RESIZE_TYPE_FIT_BLUR:
$blur_size = max(ceil(max($new_width, $new_height) / 25), 5);
$args .= "${file_arg} ".
"\( -clone 0 -resize ${new_width}x${new_height}\^ -gravity center -fill black -colorize 50% -extent ${new_width}x${new_height} -blur 0x${blur_size} \) ".
"\( -clone 0 -resize ${new_width}x${new_height} \) ".
"-delete 0 -gravity center -compose over -composite";
break;
}
switch ($output_type) {
case Media::WEBP_LOSSLESS:
$args .= '-define webp:lossless=true';
break;
case "png":
case EXTENSION_PNG:
$args .= '-define png:compression-level=9';
break;
}
if ($minimize) {
$args .= " -strip -thumbnail";
} else {
$args .= " -resize";
}
$args .= " -quality ${output_quality} -background ${bg}";
$output_ext = self::determine_ext($output_type);
$format = '"%s" %s %ux%u%s -quality %u -background %s %s"%s[0]" %s:"%s" 2>&1';
$cmd = sprintf($format, $convert, $args, $new_width, $new_height, $resize_args, $output_quality, $bg, $input_type, $input_path, $output_ext, $output_filename);
$format = '"%s" %s %s:"%s" 2>&1';
$cmd = sprintf($format, $convert, $args, $output_ext, $output_filename);
$cmd = str_replace("\"convert\"", "convert", $cmd); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
exec($cmd, $output, $ret);
if ($ret != 0) {
@ -657,7 +691,7 @@ class Media extends Extension
int $new_height,
string $output_filename,
string $output_type = null,
bool $ignore_aspect_ratio = false,
string $resize_type = self::RESIZE_TYPE_FIT,
int $output_quality = 80,
bool $allow_upscale = true
) {
@ -668,19 +702,19 @@ class Media extends Extension
/* If not specified, output to the same format as the original image */
switch ($info[2]) {
case IMAGETYPE_GIF:
$output_type = "gif";
$output_type = EXTENSION_GIF;
break;
case IMAGETYPE_JPEG:
$output_type = "jpeg";
$output_type = EXTENSION_JPEG;
break;
case IMAGETYPE_PNG:
$output_type = "png";
$output_type = EXTENSION_PNG;
break;
case IMAGETYPE_WEBP:
$output_type = "webp";
$output_type = EXTENSION_WEBP;
break;
case IMAGETYPE_BMP:
$output_type = "bmp";
$output_type = EXTENSION_BMP;
break;
default:
throw new MediaException("Failed to save the new image - Unsupported image type.");
@ -693,7 +727,7 @@ class Media extends Extension
throw new InsufficientMemoryException("The image is too large to resize given the memory limits. ($memory_use > $memory_limit)");
}
if (!$ignore_aspect_ratio) {
if ($resize_type==Media::RESIZE_TYPE_FIT) {
list($new_width, $new_height) = get_scaled_by_aspect_ratio($width, $height, $new_width, $new_height);
}
if (!$allow_upscale &&
@ -776,21 +810,21 @@ class Media extends Extension
}
switch ($output_type) {
case "bmp":
case EXTENSION_BMP:
$result = imagebmp($image_resized, $output_filename, true);
break;
case "webp":
case EXTENSION_WEBP:
case Media::WEBP_LOSSY:
$result = imagewebp($image_resized, $output_filename, $output_quality);
break;
case "jpg":
case "jpeg":
case EXTENSION_JPG:
case EXTENSION_JPEG:
$result = imagejpeg($image_resized, $output_filename, $output_quality);
break;
case "png":
case EXTENSION_PNG:
$result = imagepng($image_resized, $output_filename, 9);
break;
case "gif":
case EXTENSION_GIF:
$result = imagegif($image_resized, $output_filename);
break;
default:
@ -904,7 +938,7 @@ class Media extends Extension
*/
public static function normalize_format(string $format, ?bool $lossless = null): ?string
{
if ($format == "webp") {
if ($format == EXTENSION_WEBP) {
if ($lossless === true) {
$format = Media::WEBP_LOSSLESS;
} else {
@ -1016,7 +1050,7 @@ class Media extends Extension
$this->set_version(MediaConfig::VERSION, 2);
$database->beginTransaction();
$database->begin_transaction();
}
}
}

View File

@ -15,63 +15,81 @@ abstract class MediaEngine
];
public const OUTPUT_SUPPORT = [
MediaEngine::GD => [
"gif",
"jpg",
"png",
"webp",
EXTENSION_GIF,
EXTENSION_JPG,
EXTENSION_PNG,
EXTENSION_WEBP,
Media::WEBP_LOSSY,
],
MediaEngine::IMAGICK => [
"gif",
"jpg",
"png",
"webp",
EXTENSION_GIF,
EXTENSION_JPG,
EXTENSION_PNG,
EXTENSION_WEBP,
Media::WEBP_LOSSY,
Media::WEBP_LOSSLESS,
],
MediaEngine::FFMPEG => [
"jpg",
"webp",
"png",
EXTENSION_JPG,
EXTENSION_WEBP,
EXTENSION_PNG,
],
MediaEngine::STATIC => [
"jpg",
EXTENSION_JPG,
],
];
public const INPUT_SUPPORT = [
MediaEngine::GD => [
"bmp",
"gif",
"jpg",
"png",
"webp",
EXTENSION_BMP,
EXTENSION_GIF,
EXTENSION_JPG,
EXTENSION_PNG,
EXTENSION_WEBP,
Media::WEBP_LOSSY,
Media::WEBP_LOSSLESS,
],
MediaEngine::IMAGICK => [
"bmp",
"gif",
"jpg",
"png",
"psd",
"tiff",
"webp",
EXTENSION_BMP,
EXTENSION_GIF,
EXTENSION_JPG,
EXTENSION_PNG,
EXTENSION_PSD,
EXTENSION_TIFF,
EXTENSION_WEBP,
Media::WEBP_LOSSY,
Media::WEBP_LOSSLESS,
"ico",
EXTENSION_ICO,
],
MediaEngine::FFMPEG => [
"avi",
"mkv",
"webm",
"mp4",
"mov",
"flv",
EXTENSION_AVI,
EXTENSION_MKV,
EXTENSION_WEBM,
EXTENSION_MP4,
EXTENSION_MOV,
EXTENSION_FLASH_VIDEO,
],
MediaEngine::STATIC => [
"jpg",
"gif",
"png",
EXTENSION_JPG,
EXTENSION_GIF,
EXTENSION_PNG,
],
];
public const RESIZE_TYPE_SUPPORT = [
MediaEngine::GD => [
Media::RESIZE_TYPE_FIT,
Media::RESIZE_TYPE_STRETCH
],
MediaEngine::IMAGICK => [
Media::RESIZE_TYPE_FIT,
Media::RESIZE_TYPE_FIT_BLUR,
Media::RESIZE_TYPE_FILL,
Media::RESIZE_TYPE_STRETCH,
],
MediaEngine::FFMPEG => [
Media::RESIZE_TYPE_FIT
],
MediaEngine::STATIC => [
Media::RESIZE_TYPE_FIT
]
];
}

View File

@ -113,7 +113,7 @@ class NotATag extends Extension
["tag"=>$input['c_tag'], "redirect"=>$input['c_redirect']]
);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER['HTTP_REFERER']);
$page->set_redirect(referer_or(make_link()));
} elseif ($event->get_arg(0) == "remove") {
$user->ensure_authed();
$input = validate_input(["d_tag"=>"string"]);
@ -123,7 +123,7 @@ class NotATag extends Extension
);
$page->flash("Image ban removed");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER['HTTP_REFERER']);
$page->set_redirect(referer_or(make_link()));
} elseif ($event->get_arg(0) == "list") {
$t = new NotATagTable($database->raw_db());
$t->token = $user->get_auth_token();

View File

@ -76,8 +76,8 @@ class NumericScoreTheme extends Themelet
$pop_images .= $this->build_thumb_html($image)."\n";
}
$b_dte = make_link("popular_by_".$dte[3]."?".date($dte[2], (strtotime('-1 '.$dte[3], strtotime($dte[0])))));
$f_dte = make_link("popular_by_".$dte[3]."?".date($dte[2], (strtotime('+1 '.$dte[3], strtotime($dte[0])))));
$b_dte = make_link("popular_by_".$dte[3], date($dte[2], (strtotime('-1 '.$dte[3], strtotime($dte[0])))));
$f_dte = make_link("popular_by_".$dte[3], date($dte[2], (strtotime('+1 '.$dte[3], strtotime($dte[0])))));
$html = "\n".
"<h3 style='text-align: center;'>\n".

View File

@ -114,7 +114,7 @@ class PrivMsg extends Extension
if ($user->can(Permissions::READ_PM)) {
$count = $this->count_pms($user);
$h_count = $count > 0 ? " <span class='unread'>($count)</span>" : "";
$event->add_link("Private Messages$h_count", make_link("user#private-messages"));
$event->add_link("Private Messages$h_count", make_link("user", null, "private-messages"));
}
}
@ -167,7 +167,7 @@ class PrivMsg extends Extension
$cache->delete("pm-count-{$user->id}");
log_info("pm", "Deleted PM #$pm_id", "PM deleted");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER["HTTP_REFERER"]);
$page->set_redirect(referer_or(make_link()));
}
}
}
@ -182,7 +182,7 @@ class PrivMsg extends Extension
send_event(new SendPMEvent(new PM($from_id, $_SERVER["REMOTE_ADDR"], $to_id, $subject, $message)));
$page->flash("PM sent");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect($_SERVER["HTTP_REFERER"]);
$page->set_redirect(referer_or(make_link()));
}
}
break;

View File

@ -47,8 +47,7 @@ class PoolCreationEvent extends Event
User $pool_user = null,
bool $public = false,
string $description = ""
)
{
) {
parent::__construct();
global $user;
@ -59,7 +58,8 @@ class PoolCreationEvent extends Event
}
}
class Pool {
class Pool
{
public $id;
public $user_id;
public $user_name;
@ -81,7 +81,8 @@ class Pool {
$this->posts = (int)$row['posts'];
}
public static function makePool(array $row): Pool {
public static function makePool(array $row): Pool
{
return new Pool($row);
}
}
@ -316,7 +317,8 @@ class Pools extends Extension
case "import":
if ($this->have_permission($user, $pool)) {
$images = Image::find_images(
0, $config->get_int(PoolsConfig::MAX_IMPORT_RESULTS, 1000),
0,
$config->get_int(PoolsConfig::MAX_IMPORT_RESULTS, 1000),
Tag::explode($_POST["pool_tag"])
);
$this->theme->pool_result($page, $images, $pool);
@ -703,7 +705,7 @@ class Pools extends Extension
*
* #return int[] Array returning two elements (prev, next) in 1 dimension. Each returns ImageID or NULL if none.
*/
private function get_nav_posts(Pool $pool, int $imageID): array
private function get_nav_posts(Pool $pool, int $imageID): ?array
{
global $database;

View File

@ -33,10 +33,10 @@ class PostTitles extends Extension
public function onDisplayingImage(DisplayingImageEvent $event)
{
global $config;
global $config, $page;
if ($config->get_bool(PostTitlesConfig::SHOW_IN_WINDOW_TITLE)) {
$event->set_title(self::get_title($event->get_image()));
$page->set_title(self::get_title($event->get_image()));
}
}
@ -73,7 +73,16 @@ class PostTitles extends Extension
$event->panel->add_block($sb);
}
public function onBulkExport(BulkExportEvent $event)
{
$event->fields["title"] = $event->image->title;
}
public function onBulkImport(BulkImportEvent $event)
{
if (property_exists($event->fields, "title") && $event->fields->title!=null) {
$this->set_title($event->image->id, $event->fields->title);
}
}
private function set_title(int $image_id, string $title)
{

View File

@ -9,9 +9,7 @@ class PostTitlesTheme extends Themelet
<td>
".($can_set ? "
<span class='view'>".html_escape($title)."</span>
<span class='edit'>
<input class='edit' type='text' name='post_title' value='".html_escape($title)."' />
</span>
" : html_escape("
$title
"))."

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
class PrivateImageInfo extends ExtensionInfo
{
public const KEY = "private_image";
public $key = self::KEY;
public $name = "Private Image";
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
public $license = self::LICENSE_WTFPL;
public $description = "Allows users to mark images as private, which prevents other users from seeing them.";
}

292
ext/private_image/main.php Normal file
View File

@ -0,0 +1,292 @@
<?php declare(strict_types=1);
abstract class PrivateImageConfig
{
const VERSION = "ext_private_image_version";
const USER_SET_DEFAULT = "user_private_image_set_default";
const USER_VIEW_DEFAULT = "user_private_image_view_default";
}
class PrivateImage extends Extension
{
/** @var PrivateImageTheme */
protected $theme;
public function onInitExt(InitExtEvent $event)
{
global $config;
Image::$bool_props[] = "private ";
}
public function onInitUserConfig(InitUserConfigEvent $event)
{
$event->user_config->set_default_bool(PrivateImageConfig::USER_SET_DEFAULT, false);
$event->user_config->set_default_bool(PrivateImageConfig::USER_VIEW_DEFAULT, true);
}
public function onUserOptionsBuilding(UserOptionsBuildingEvent $event)
{
global $user, $user_config;
$event->add_html(
$this->theme->get_user_options(
$user,
$user_config->get_bool(PrivateImageConfig::USER_SET_DEFAULT),
$user_config->get_bool(PrivateImageConfig::USER_VIEW_DEFAULT),
)
);
}
public function onPageRequest(PageRequestEvent $event)
{
global $page, $user, $user_config;
if ($event->page_matches("privatize_image") && $user->can(Permissions::SET_PRIVATE_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 make image private: No valid Image ID given.");
}
$image = Image::by_id($image_id);
if ($image==null) {
throw new SCoreException("Image not found.");
}
if ($image->owner_id!=$user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) {
throw new SCoreException("Cannot set another user's image to private.");
}
self::privatize_image($image_id);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/" . $image_id));
}
if ($event->page_matches("publicize_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 make image public: No valid Image ID given.");
}
$image = Image::by_id($image_id);
if ($image==null) {
throw new SCoreException("Image not found.");
}
if ($image->owner_id!=$user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) {
throw new SCoreException("Cannot set another user's image to private.");
}
self::publicize_image($image_id);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/".$image_id));
}
if ($event->page_matches("user_admin")) {
if (!$user->check_auth_token()) {
return;
}
switch ($event->get_arg(0)) {
case "private_image":
if (!array_key_exists("id", $_POST) || empty($_POST["id"])) {
return;
}
$id = intval($_POST["id"]);
if ($id != $user->id) {
throw new SCoreException("Cannot change another user's settings");
}
$set_default = array_key_exists("set_default", $_POST);
$view_default = array_key_exists("view_default", $_POST);
$user_config->set_bool(PrivateImageConfig::USER_SET_DEFAULT, $set_default);
$user_config->set_bool(PrivateImageConfig::USER_VIEW_DEFAULT, $view_default);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("user"));
break;
}
}
}
public function onDisplayingImage(DisplayingImageEvent $event)
{
global $user, $page, $config;
if ($event->image->private===true && $event->image->owner_id!=$user->id && !$user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) {
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/list"));
}
}
const SEARCH_REGEXP = "/^private:(yes|no|any)/";
public function onSearchTermParse(SearchTermParseEvent $event)
{
global $user, $database, $user_config;
$show_private = $user_config->get_bool(PrivateImageConfig::USER_VIEW_DEFAULT);
$matches = [];
if (is_null($event->term) && $this->no_private_query($event->context)) {
if ($show_private) {
$event->add_querylet(
new Querylet(
$database->scoreql_to_sql("private = SCORE_BOOL_N OR owner_id = :private_owner_id"),
["private_owner_id"=>$user->id]
)
);
} else {
$event->add_querylet(
new Querylet(
$database->scoreql_to_sql("private = SCORE_BOOL_N")
)
);
}
}
if (is_null($event->term)) {
return;
}
if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) {
$params = [];
$query = "";
switch ($matches[1]) {
case "no":
$query .= "private = SCORE_BOOL_N";
break;
case "yes":
$query .= "private = SCORE_BOOL_Y";
// Admins can view others private images, but they have to specify the user
if (!$user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES) ||
!UserPage::has_user_query($event->context)) {
$query .= " AND owner_id = :private_owner_id";
$params["private_owner_id"] = $user->id;
}
break;
case "any":
$query .= "private = SCORE_BOOL_N OR owner_id = :private_owner_id";
$params["private_owner_id"] = $user->id;
break;
}
$event->add_querylet(new Querylet($database->scoreql_to_sql($query), $params));
}
}
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
{
if ($event->key===HelpPages::SEARCH) {
$block = new Block();
$block->header = "Private Images";
$block->body = $this->theme->get_help_html();
$event->add_block($block);
}
}
private function no_private_query(array $context): bool
{
foreach ($context as $term) {
if (preg_match(self::SEARCH_REGEXP, $term)) {
return false;
}
}
return true;
}
public static function privatize_image($image_id)
{
global $database, $user;
$database->execute(
"UPDATE images SET private = :true WHERE id = :id AND private = :false",
["id"=>$image_id, "true"=>true, "false"=>$database->scoresql_value_prepare(false)]
);
}
public static function publicize_image($image_id)
{
global $database;
$database->execute(
"UPDATE images SET private = :false WHERE id = :id AND private = :true",
["id"=>$image_id, "true"=>true, "false"=>$database->scoresql_value_prepare(false)]
);
}
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
{
global $user, $config;
if ($user->can(Permissions::SET_PRIVATE_IMAGE) && $user->id==$event->image->owner_id) {
$event->add_part($this->theme->get_image_admin_html($event->image));
}
}
public function onImageAddition(ImageAdditionEvent $event)
{
global $user_config;
if ($user_config->get_bool(PrivateImageConfig::USER_SET_DEFAULT)) {
self::privatize_image($event->image->id);
}
}
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
{
global $user, $config;
if ($user->can(Permissions::SET_PRIVATE_IMAGE)) {
$event->add_action("bulk_privatize_image", "Make Private");
$event->add_action("bulk_publicize_image", "Make Public");
}
}
public function onBulkAction(BulkActionEvent $event)
{
global $page, $user;
switch ($event->action) {
case "bulk_privatize_image":
if ($user->can(Permissions::SET_PRIVATE_IMAGE)) {
$total = 0;
foreach ($event->items as $image) {
if ($image->owner_id==$user->id ||
$user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) {
self::privatize_image($image->id);
$total++;
}
}
$page->flash("Made $total items private");
}
break;
case "bulk_publicize_image":
$total = 0;
foreach ($event->items as $image) {
if ($image->owner_id==$user->id ||
$user->can(Permissions::SET_OTHERS_PRIVATE_IMAGES)) {
self::publicize_image($image->id);
$total++;
}
}
$page->flash("Made $total items public");
break;
}
}
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
{
global $database;
if ($this->get_version(PrivateImageConfig::VERSION) < 1) {
$database->execute($database->scoreql_to_sql(
"ALTER TABLE images ADD COLUMN private SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N"
));
$database->execute("CREATE INDEX images_private_idx ON images(private)");
$this->set_version(PrivateImageConfig::VERSION, 1);
}
}
}

View File

@ -0,0 +1,68 @@
<?php declare(strict_types=1);
use function MicroHTML\BR;
use function MicroHTML\BUTTON;
use function MicroHTML\INPUT;
class PrivateImageTheme extends Themelet
{
public function get_image_admin_html(Image $image)
{
if ($image->private===false) {
$html = SHM_SIMPLE_FORM(
'privatize_image/'.$image->id,
INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image->id]),
SHM_SUBMIT("Make Private")
);
} else {
$html = SHM_SIMPLE_FORM(
'publicize_image/'.$image->id,
INPUT(["type"=>'hidden', "name"=>'image_id', "value"=>$image->id]),
SHM_SUBMIT("Make Public")
);
}
return (string)$html;
}
public function get_help_html()
{
return '<p>Search for images that are private/public.</p>
<div class="command_example">
<pre>private:yes</pre>
<p>Returns images that are private, restricted to yourself if you are not an admin.</p>
</div>
<div class="command_example">
<pre>private:no</pre>
<p>Returns images that are public.</p>
</div>
';
}
public function get_user_options(User $user, bool $set_by_default, bool $view_by_default): string
{
$html = "
<p>".make_form(make_link("user_admin/private_image"))."
<input type='hidden' name='id' value='$user->id'>
<table style='width: 300px;'>
<tbody>
<tr><th colspan='2'>Private Images</th></tr>
<tr>
<td>
<label><input type='checkbox' name='set_default' value='true' " .($set_by_default ? 'checked=checked': ''). " />Mark images private by default</label>
</td>
</tr><tr>
<td>
<label><input type='checkbox' name='view_default' value='true' " .($view_by_default ? 'checked=checked': ''). " />View private images by default</label>
</td>
</tr>
</tbody>
<tfoot>
<tr><td><input type='submit' value='Save'></td></tr>
</tfoot>
</table>
</form>
";
return $html;
}
}

View File

@ -35,7 +35,7 @@ class RandomImage extends Extension
send_event(new DisplayingImageEvent($image));
} elseif ($action === "widget") {
$page->set_mode(PageMode::DATA);
$page->set_type("text/html");
$page->set_type(MIME_TYPE_HTML);
$page->set_data($this->theme->build_thumb_html($image));
}
}

View File

@ -17,16 +17,13 @@ class RandomImageTheme extends Themelet
return (string)DIV(
["style"=>"text-align: center;"],
A(
[
"href"=>make_link("post/view/{$image->id}", $query),
"style"=>"position: relative; height: {$tsize[1]}px; width: {$tsize[0]}px;"
],
["href"=>make_link("post/view/{$image->id}", $query)],
IMG([
"id"=>"thumb_rand_{$image->id}",
"title"=>$image->get_tooltip(),
"alt"=>$image->get_tooltip(),
"class"=>'highlighted',
"style"=>"height: {$tsize[1]}px; width: {$tsize[0]}px;",
"style"=>"max-height: {$tsize[1]}px; max-width: 100%;",
"src"=>$image->get_thumb_link()
])
)

View File

@ -170,6 +170,19 @@ class Ratings extends Extension
}
}
public function onBulkExport(BulkExportEvent $event)
{
$event->fields["rating"] = $event->image->rating;
}
public function onBulkImport(BulkImportEvent $event)
{
if (property_exists($event->fields, "rating")
&& $event->fields->rating != null
&& Ratings::rating_is_valid($event->fields->rating)) {
$this->set_rating($event->image->id, $event->fields->rating, "");
}
}
public function onRatingSet(RatingSetEvent $event)
{
if (empty($event->image->rating)) {

View File

@ -84,7 +84,7 @@ class ResizeImage extends Extension
$height = $config->get_int(ResizeConfig::DEFAULT_HEIGHT);
}
$isanigif = 0;
if ($image_obj->ext == "gif") {
if ($image_obj->ext == EXTENSION_GIF) {
$image_filename = warehouse_path(Image::IMAGE_DIR, $image_obj->hash);
if (($fh = @fopen($image_filename, 'rb'))) {
//check if gif is animated (via https://www.php.net/manual/en/function.imagecreatefromgif.php#104473)

View File

@ -15,7 +15,7 @@ class RotateImage extends Extension
/** @var RotateImageTheme */
protected $theme;
const SUPPORTED_EXT = ["jpg","jpeg","png","gif","webp"];
const SUPPORTED_MIME = [MIME_TYPE_JPEG, MIME_TYPE_PNG, MIME_TYPE_GIF, MIME_TYPE_WEBP];
public function onInitExt(InitExtEvent $event)
{
@ -28,7 +28,7 @@ class RotateImage extends Extension
{
global $user, $config;
if ($user->can(Permissions::EDIT_FILES) && $config->get_bool("rotate_enabled")
&& in_array($event->image->ext, self::SUPPORTED_EXT)) {
&& in_array(get_mime_for_extension($event->image->ext), self::SUPPORTED_MIME)) {
/* Add a link to rotate the image */
$event->add_part($this->theme->get_rotate_html($event->image->id));
}

View File

@ -16,7 +16,7 @@ class RSSComments extends Extension
global $config, $database, $page;
if ($event->page_matches("rss/comments")) {
$page->set_mode(PageMode::DATA);
$page->set_type("application/rss+xml");
$page->set_type(MIME_TYPE_RSS);
$comments = $database->get_all("
SELECT

View File

@ -9,7 +9,7 @@ class RSSCommentsTest extends ShimmiePHPUnitTestCase
send_event(new CommentPostingEvent($image_id, $user, "ASDFASDF"));
$this->get_page('rss/comments');
//$this->assert_mime("application/rss+xml");
//$this->assert_mime(MIME_TYPE_RSS);
$this->assert_no_content("Exception");
$this->assert_content("ASDFASDF");
}

View File

@ -43,7 +43,7 @@ class RSSImages extends Extension
global $page;
global $config;
$page->set_mode(PageMode::DATA);
$page->set_type("application/rss+xml");
$page->set_type(MIME_TYPE_RSS);
$data = "";
foreach ($images as $image) {
@ -115,7 +115,7 @@ class RSSImages extends Extension
</item>
";
$cache->set("rss-item-image:{$image->id}", $data, 86400);
$cache->set("rss-item-image:{$image->id}", $data, rand(43200, 86400));
return $data;
}

View File

@ -8,26 +8,26 @@ class RSSImagesTest extends ShimmiePHPUnitTestCase
$this->log_out();
$this->get_page('rss/images');
//$this->assert_mime("application/rss+xml");
//$this->assert_mime(MIME_TYPE_RSS);
$this->assert_no_content("Exception");
$this->get_page('rss/images/1');
//$this->assert_mime("application/rss+xml");
//$this->assert_mime(MIME_TYPE_RSS);
$this->assert_no_content("Exception");
# FIXME: test that the image is actually found
$this->get_page('rss/images/computer/1');
//$this->assert_mime("application/rss+xml");
//$this->assert_mime(MIME_TYPE_RSS);
$this->assert_no_content("Exception");
# valid tag, invalid page
$this->get_page('rss/images/computer/2');
//$this->assert_mime("application/rss+xml");
//$this->assert_mime(MIME_TYPE_RSS);
$this->assert_no_content("Exception");
# not found
$this->get_page('rss/images/waffle/2');
//$this->assert_mime("application/rss+xml");
//$this->assert_mime(MIME_TYPE_RSS);
$this->assert_no_content("Exception");
}
}

View File

@ -118,14 +118,14 @@ class Rule34 extends Extension
['is_admin'=>$input['is_admin'] ? 't' : 'f', 'id'=>$input['user_id']]
);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(@$_SERVER['HTTP_REFERER']);
$page->set_redirect(referer_or(make_link()));
}
}
if ($event->page_matches("tnc_agreed")) {
setcookie("ui-tnc-agreed", "true", 0, "/");
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(@$_SERVER['HTTP_REFERER'] ?? "/");
$page->set_redirect(referer_or("/"));
}
if ($event->page_matches("admin/cache_purge")) {

View File

@ -10,4 +10,5 @@ class SetupInfo extends ExtensionInfo
public $authors = self::SHISH_AUTHOR;
public $description = "Allows the site admin to configure the board to his or her taste";
public $core = true;
public $visibility = self::VISIBLE_HIDDEN;
}

View File

@ -111,9 +111,9 @@ class SetupBlock extends Block
$this->body .= $content;
$this->end_table_cell();
}
public function start_table_header_cell(int $colspan = 1)
public function start_table_header_cell(int $colspan = 1, string $align = 'right')
{
$this->body .= "<th colspan='$colspan'>";
$this->body .= "<th colspan='$colspan' style='text-align: $align'>";
}
public function end_table_header_cell()
{
@ -126,23 +126,34 @@ class SetupBlock extends Block
$this->end_table_header_cell();
}
private function format_option(string $name, $html, ?string $label, bool $table_row)
{
private function format_option(
string $name,
$html,
?string $label,
bool $table_row,
bool $label_row = false
) {
if ($table_row) {
$this->start_table_row();
}
if ($table_row) {
$this->start_table_header_cell();
$this->start_table_header_cell($label_row ? 2 : 1, $label_row ? 'center' : 'right');
}
if (!is_null($label)) {
$this->body .= "<label for='{$name}'>{$label}</label>";
}
if ($table_row) {
$this->end_table_header_cell();
}
if ($table_row && $label_row) {
$this->end_table_row();
$this->start_table_row();
}
if ($table_row) {
$this->start_table_cell();
$this->start_table_cell($label_row ? 2 : 1);
}
$this->body .= $html;
if ($table_row) {
@ -173,7 +184,7 @@ class SetupBlock extends Block
$html = "<textarea rows='$rows' id='$name' name='_config_$name'>$val</textarea>\n";
$html .= "<input type='hidden' name='_type_$name' value='string'>\n";
$this->format_option($name, $html, $label, $table_row);
$this->format_option($name, $html, $label, $table_row, true);
}
public function add_bool_option(string $name, string $label=null, bool $table_row = false)
@ -207,8 +218,8 @@ class SetupBlock extends Block
global $config;
$val = $config->get_int($name);
$html = "<input type='text' id='$name' name='_config_$name' value='$val' size='4' style='text-align: center;'>\n";
$html .= "<input type='hidden' name='_type_$name' value='int'>\n";
$html = "<input type='number' id='$name' name='_config_$name' value='$val' size='4' style='text-align: center;' step='1' />\n";
$html .= "<input type='hidden' name='_type_$name' value='int' />\n";
$this->format_option($name, $html, $label, $table_row);
}
@ -324,27 +335,9 @@ class Setup extends Extension
$themes[$human] = $name;
}
if (isset($_SERVER["HTTP_HOST"])) {
$host = $_SERVER["HTTP_HOST"];
} else {
$host = $_SERVER["SERVER_NAME"];
if ($_SERVER["SERVER_PORT"] != "80") {
$host .= ":" . $_SERVER["SERVER_PORT"];
}
}
$full = "//" . $host . $_SERVER["PHP_SELF"];
$test_url = str_replace("/index.php", "/nicetest", $full);
$test_url = str_replace("/index.php", "/nicetest", $_SERVER["SCRIPT_NAME"]);
$nicescript = "<script type='text/javascript'>
function getHTTPObject() {
if (window.XMLHttpRequest){
return new XMLHttpRequest();
}
else if(window.ActiveXObject){
return new ActiveXObject('Microsoft.XMLHTTP');
}
}
checkbox = document.getElementById('nice_urls');
out_span = document.getElementById('nicetest');
@ -352,11 +345,11 @@ class Setup extends Extension
out_span.innerHTML = '(testing...)';
document.addEventListener('DOMContentLoaded', () => {
var http_request = getHTTPObject();
var http_request = new XMLHttpRequest();
http_request.open('GET', '$test_url', false);
http_request.send(null);
if(http_request.status == 200 && http_request.responseText == 'ok') {
if(http_request.status === 200 && http_request.responseText === 'ok') {
checkbox.disabled = false;
out_span.innerHTML = '(tested ok)';
}
@ -375,7 +368,7 @@ class Setup extends Extension
$sb->add_choice_option(SetupConfig::THEME, $themes, "<br>Theme: ");
//$sb->add_multichoice_option("testarray", array("a" => "b", "c" => "d"), "<br>Test Array: ");
$sb->add_bool_option("nice_urls", "<br>Nice URLs: ");
$sb->add_label("<span id='nicetest'>(Javascript inactive, can't test!)</span>$nicescript");
$sb->add_label("<span title='$test_url' id='nicetest'>(Javascript inactive, can't test!)</span>$nicescript");
$event->panel->add_block($sb);
$sb = new SetupBlock("Remote API Integration");

View File

@ -36,7 +36,7 @@ class ShimmieApi extends Extension
if ($event->page_matches("api/shimmie")) {
$page->set_mode(PageMode::DATA);
$page->set_type("text/plain");
$page->set_type(MIME_TYPE_TEXT);
if ($event->page_matches("api/shimmie/get_tags")) {
if ($event->count_args() > 0) {

View File

@ -155,7 +155,7 @@ class XMLSitemap extends Extension
// Generate new sitemap
file_put_contents($this->sitemap_filepath, $xml);
$page->set_mode(PageMode::DATA);
$page->set_type("application/xml");
$page->set_type(MIME_TYPE_XML_APPLICATION);
$page->set_data($xml);
}
@ -191,7 +191,7 @@ class XMLSitemap extends Extension
$xml = file_get_contents($this->sitemap_filepath);
$page->set_mode(PageMode::DATA);
$page->set_type("application/xml");
$page->set_type(MIME_TYPE_XML_APPLICATION);
$page->set_data($xml);
}
}

Some files were not shown because too many files have changed in this diff Show More