diff options
Diffstat (limited to 'inc')
26 files changed, 756 insertions, 592 deletions
diff --git a/inc/api.php b/inc/api.php index 3337c613..a8ae9eed 100644 --- a/inc/api.php +++ b/inc/api.php @@ -26,12 +26,6 @@ class Api { 'trip' => 'trip', 'capcode' => 'capcode', 'time' => 'time', - 'thumbheight' => 'tn_w', - 'thumbwidth' => 'tn_h', - 'fileheight' => 'w', - 'filewidth' => 'h', - 'filesize' => 'fsize', - 'filename' => 'filename', 'omitted' => 'omitted_posts', 'omitted_images' => 'omitted_images', 'replies' => 'replies', @@ -46,6 +40,15 @@ class Api { 'bump' => 'last_modified' ); + $this->fileFields = array( + 'thumbheight' => 'tn_w', + 'thumbwidth' => 'tn_h', + 'height' => 'w', + 'width' => 'h', + 'size' => 'fsize', + 'file' => 'filename', + ); + if (isset($config['api']['extra_fields']) && gettype($config['api']['extra_fields']) == 'array'){ $this->postFields = array_merge($this->postFields, $config['api']['extra_fields']); } @@ -67,30 +70,26 @@ class Api { 'last_modified' => 1 ); - private function translatePost($post, $threadsPage = false) { - $apiPost = array(); - $fields = $threadsPage ? $this->threadsPageFields : $this->postFields; + private function translateFields($fields, $object, &$apiPost) { foreach ($fields as $local => $translated) { - if (!isset($post->$local)) + if (!isset($object->$local)) continue; $toInt = isset(self::$ints[$translated]); - $val = $post->$local; + $val = $object->$local; if ($val !== null && $val !== '') { $apiPost[$translated] = $toInt ? (int) $val : $val; } } + } - if ($threadsPage) return $apiPost; + private function translatePost($post, $threadsPage = false) { + $apiPost = array(); + $fields = $threadsPage ? $this->threadsPageFields : $this->postFields; + $this->translateFields($fields, $post, $apiPost); - if (isset($post->filename)) { - $dotPos = strrpos($post->filename, '.'); - $apiPost['filename'] = substr($post->filename, 0, $dotPos); - $apiPost['ext'] = substr($post->filename, $dotPos); - $dotPos = strrpos($post->file, '.'); - $apiPost['tim'] = substr($post->file, 0, $dotPos); - } + if ($threadsPage) return $apiPost; // Handle country field if (isset($post->body_nomarkup) && $this->config['country_flags']) { @@ -104,6 +103,18 @@ class Api { } } + // Handle files + // Note: 4chan only supports one file, so only the first file is taken into account for 4chan-compatible API. + if (isset($post->files) && $post->files && !$threadsPage) { + $file = $post->files[0]; + $this->translateFields($this->fileFields, $file, $apiPost); + $dotPos = strrpos($file->file, '.'); + $apiPost['filename'] = substr($file->file, 0, $dotPos); + $apiPost['ext'] = substr($file->file, $dotPos); + $dotPos = strrpos($file->file, '.'); + $apiPost['tim'] = substr($file->file, 0, $dotPos); + } + return $apiPost; } diff --git a/inc/config.php b/inc/config.php index 416209b7..494ccda1 100644 --- a/inc/config.php +++ b/inc/config.php @@ -270,7 +270,6 @@ 'recaptcha_challenge_field', 'recaptcha_response_field', 'spoiler', - 'quick-reply', 'page', 'file_url', 'json_response', @@ -451,6 +450,8 @@ // Maximum filename length to display (the rest can be viewed upon mouseover). $config['max_filename_display'] = 30; + // Allow users to delete their own posts? + $config['allow_delete'] = true; // How long after posting should you have to wait before being able to delete that post? (In seconds.) $config['delete_time'] = 10; // Reply limit (stops bumping thread when this is reached). @@ -607,6 +608,17 @@ * Image settings * ==================== */ + // Maximum number of images allowed. Increasing this number enabled multi image. + // If you make it more than 1, make sure to enable the below script for the post form to change. + // $config['additional_javascript'][] = 'js/multi_image.js'; + $config['max_images'] = 1; + + // Method to use for determing the max filesize. + // "split" means that your max filesize is split between the images. For example, if your max filesize + // is 2MB, the filesizes of all files must add up to 2MB for it to work. + // "each" means that each file can be 2MB, so if your max_images is 3, each post could contain 6MB of + // images. "split" is recommended. + $config['multiimage_method'] = 'split'; // For resizing, maximum thumbnail dimensions. $config['thumb_width'] = 255; @@ -628,22 +640,22 @@ /* * Thumbnailing method: * - * 'gd' PHP GD (default). Only handles the most basic image formats (GIF, JPEG, PNG). - * GD is a prerequisite for Tinyboard no matter what method you choose. + * 'gd' PHP GD (default). Only handles the most basic image formats (GIF, JPEG, PNG). + * GD is a prerequisite for Tinyboard no matter what method you choose. * - * 'imagick' PHP's ImageMagick bindings. Fast and efficient, supporting many image formats. - * A few minor bugs. http://pecl.php.net/package/imagick + * 'imagick' PHP's ImageMagick bindings. Fast and efficient, supporting many image formats. + * A few minor bugs. http://pecl.php.net/package/imagick * - * 'convert' The command line version of ImageMagick (`convert`). Fixes most of the bugs in - * PHP Imagick. `convert` produces the best still thumbnails and is highly recommended. + * 'convert' The command line version of ImageMagick (`convert`). Fixes most of the bugs in + * PHP Imagick. `convert` produces the best still thumbnails and is highly recommended. * - * 'gm' GraphicsMagick (`gm`) is a fork of ImageMagick with many improvements. It is more - * efficient and gets thumbnailing done using fewer resources. + * 'gm' GraphicsMagick (`gm`) is a fork of ImageMagick with many improvements. It is more + * efficient and gets thumbnailing done using fewer resources. * * 'convert+gifscale' - * OR 'gm+gifsicle' Same as above, with the exception of using `gifsicle` (command line application) - * instead of `convert` for resizing GIFs. It's faster and resulting animated - * thumbnails have less artifacts than if resized with ImageMagick. + * OR 'gm+gifsicle' Same as above, with the exception of using `gifsicle` (command line application) + * instead of `convert` for resizing GIFs. It's faster and resulting animated + * thumbnails have less artifacts than if resized with ImageMagick. */ $config['thumb_method'] = 'gd'; // $config['thumb_method'] = 'convert'; @@ -693,7 +705,7 @@ // An alternative function for generating image filenames, instead of the default UNIX timestamp. // $config['filename_func'] = function($post) { - // return sprintf("%s", time() . substr(microtime(), 2, 3)); + // return sprintf("%s", time() . substr(microtime(), 2, 3)); // }; // Thumbnail to use for the non-image file uploads. @@ -730,8 +742,12 @@ // Display the file's original filename. $config['show_filename'] = true; - // Display image identification links using regex.info/exif, TinEye and Google Images. + // Display image identification links using ImgOps, regex.info/exif and Google Images. $config['image_identification'] = false; + // Which of the identification links to display. Only works if $config['image_identification'] is true. + $config['image_identification_imgops'] = true; + $config['image_identification_exif'] = true; + $config['image_identification_google'] = true; // Number of posts in a "View Last X Posts" page $config['noko50_count'] = 50; @@ -763,11 +779,6 @@ // Number of reports you can create at once. $config['report_limit'] = 3; - // Attention Whoring Bar - // REMEMBER TO CHMOD attentionbar.txt PROPERLY - // Oh, and add jQuery in additional_javascript. - $config['attention_bar'] = false; - // Allow unfiltered HTML in board subtitle. This is useful for placing icons and links. $config['allow_subtitle_html'] = false; @@ -928,9 +939,6 @@ // Minify Javascript using http://code.google.com/p/minify/. $config['minify_js'] = false; - // Allows js/quick-reply-old.js to work. This could make your imageboard more vulnerable to flood attacks. - $config['quick_reply'] = false; - /* * ==================== * Video embedding @@ -986,6 +994,7 @@ $config['error']['toolong_body'] = _('The body was too long.'); $config['error']['tooshort_body'] = _('The body was too short or empty.'); $config['error']['noimage'] = _('You must upload an image.'); + $config['error']['toomanyimages'] = _('You have attempted to upload too many images!'); $config['error']['nomove'] = _('The server failed to handle your upload.'); $config['error']['fileext'] = _('Unsupported image format.'); $config['error']['noboard'] = _('Invalid board!'); @@ -1399,6 +1408,8 @@ $config['mod']['view_ban_appeals'] = MOD; // Accept and deny ban appeals $config['mod']['ban_appeals'] = MOD; + // View the recent posts page + $config['mod']['recent'] = MOD; // Config editor permissions $config['mod']['config'] = array(); @@ -1447,16 +1458,16 @@ $config['search']['enable'] = false; // Maximal number of queries per IP address per minutes - $config['search']['queries_per_minutes'] = Array(15, 2); + $config['search']['queries_per_minutes'] = Array(15, 2); // Global maximal number of queries per minutes - $config['search']['queries_per_minutes_all'] = Array(50, 2); + $config['search']['queries_per_minutes_all'] = Array(50, 2); // Limit of search results - $config['search']['search_limit'] = 100; - + $config['search']['search_limit'] = 100; + // Boards for searching - //$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e'); + //$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e'); /* * ==================== diff --git a/inc/display.php b/inc/display.php index 4e8ab8fb..e5ce1da1 100644 --- a/inc/display.php +++ b/inc/display.php @@ -96,7 +96,7 @@ function error($message, $priority = true, $debug_stuff = false) { // Return the bad request header, necessary for AJAX posts // czaks: is it really so? the ajax errors only work when this is commented out - // better yet use it when ajax is disabled + // better yet use it when ajax is disabled if (!isset ($_POST['json_response'])) { header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); } @@ -352,6 +352,9 @@ class Post { foreach ($post as $key => $value) { $this->{$key} = $value; } + + if (isset($this->files)) + $this->files = json_decode($this->files); $this->subject = utf8tohtml($this->subject); $this->name = utf8tohtml($this->name); @@ -382,64 +385,11 @@ class Post { return $this->root . $board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $this->thread) . '#' . $pre . $this->id; } - public function postControls() { - global $board, $config; - - $built = ''; - if ($this->mod) { - // Mod controls (on posts) - - // Delete - if (hasPermission($config['mod']['delete'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_delete'], 'Delete', 'Are you sure you want to delete this?', $board['dir'] . 'delete/' . $this->id); - - // Delete all posts by IP - if (hasPermission($config['mod']['deletebyip'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_deletebyip'], 'Delete all posts by IP', 'Are you sure you want to delete all posts by this IP address?', $board['dir'] . 'deletebyip/' . $this->id); - - // Delete all posts by IP (global) - if (hasPermission($config['mod']['deletebyip_global'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_deletebyip_global'], 'Delete all posts by IP across all boards', 'Are you sure you want to delete all posts by this IP address, across all boards?', $board['dir'] . 'deletebyip/' . $this->id . '/global'); - - // Ban - if (hasPermission($config['mod']['ban'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Ban').'" href="?/' . $board['dir'] . 'ban/' . $this->id . '">' . $config['mod']['link_ban'] . '</a>'; - - // Ban & Delete - if (hasPermission($config['mod']['bandelete'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Ban & Delete').'" href="?/' . $board['dir'] . 'ban&delete/' . $this->id . '">' . $config['mod']['link_bandelete'] . '</a>'; - - // Delete file (keep post) - if (!empty($this->file) && hasPermission($config['mod']['deletefile'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_deletefile'], _('Delete file'), _('Are you sure you want to delete this file?'), $board['dir'] . 'deletefile/' . $this->id); - - // Spoiler file (keep post) - if (!empty($this->file) && $this->file != "deleted" && $this->file != null && $this->thumb != 'spoiler' && hasPermission($config['mod']['spoilerimage'], $board['uri'], $this->mod) && $config['spoiler_images']) - $built .= ' ' . secure_link_confirm($config['mod']['link_spoilerimage'], _('Spoiler File'), _('Are you sure you want to spoiler this file?'), $board['uri'] . '/spoiler/' . $this->id); - - // Move post - if (hasPermission($config['mod']['move'], $board['uri'], $this->mod) && $config['move_replies']) - $built .= ' <a title="'._('Move reply to another board').'" href="?/' . $board['uri'] . '/move_reply/' . $this->id . '">' . $config['mod']['link_move'] . '</a>'; - - // Edit post - if (hasPermission($config['mod']['editpost'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Edit post').'" href="?/' . $board['dir'] . 'edit' . ($config['mod']['raw_html_default'] ? '_raw' : '') . '/' . $this->id . '">' . $config['mod']['link_editpost'] . '</a>'; - - - if (!empty($built)) - $built = '<span class="controls">' . $built . '</span>'; - } - return $built; - } - - public function ratio() { - return fraction($this->filewidth, $this->fileheight, ':'); - } public function build($index=false) { global $board, $config; - return Element('post_reply.html', array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index)); + return Element('post_reply.html', array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'mod' => $this->mod)); } }; @@ -453,6 +403,9 @@ class Thread { $this->{$key} = $value; } + if (isset($this->files)) + $this->files = json_decode($this->files); + $this->subject = utf8tohtml($this->subject); $this->name = utf8tohtml($this->name); $this->mod = $mod; @@ -491,79 +444,8 @@ class Thread { $this->posts[] = $post; } public function postCount() { - return count($this->posts) + $this->omitted; + return count($this->posts) + $this->omitted; } - public function postControls() { - global $board, $config; - - $built = ''; - if ($this->mod) { - // Mod controls (on posts) - // Delete - if (hasPermission($config['mod']['delete'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_delete'], _('Delete'), _('Are you sure you want to delete this?'), $board['dir'] . 'delete/' . $this->id); - - // Delete all posts by IP - if (hasPermission($config['mod']['deletebyip'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_deletebyip'], _('Delete all posts by IP'), _('Are you sure you want to delete all posts by this IP address?'), $board['dir'] . 'deletebyip/' . $this->id); - - // Delete all posts by IP (global) - if (hasPermission($config['mod']['deletebyip_global'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_deletebyip_global'], _('Delete all posts by IP across all boards'), _('Are you sure you want to delete all posts by this IP address, across all boards?'), $board['dir'] . 'deletebyip/' . $this->id . '/global'); - - // Ban - if (hasPermission($config['mod']['ban'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Ban').'" href="?/' . $board['dir'] . 'ban/' . $this->id . '">' . $config['mod']['link_ban'] . '</a>'; - - // Ban & Delete - if (hasPermission($config['mod']['bandelete'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Ban & Delete').'" href="?/' . $board['dir'] . 'ban&delete/' . $this->id . '">' . $config['mod']['link_bandelete'] . '</a>'; - - // Delete file (keep post) - if (!empty($this->file) && $this->file != 'deleted' && hasPermission($config['mod']['deletefile'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_deletefile'], _('Delete file'), _('Are you sure you want to delete this file?'), $board['dir'] . 'deletefile/' . $this->id); - - // Spoiler file (keep post) - if (!empty($this->file) && $this->file != "deleted" && $this->file != null && $this->thumb != 'spoiler' && hasPermission($config['mod']['spoilerimage'], $board['uri'], $this->mod) && $config['spoiler_images']) - $built .= ' ' . secure_link_confirm($config['mod']['link_spoilerimage'], _('Spoiler File'), _('Are you sure you want to spoiler this file?'), $board['uri'] . '/spoiler/' . $this->id); - - // Sticky - if (hasPermission($config['mod']['sticky'], $board['uri'], $this->mod)) - if ($this->sticky) - $built .= ' <a title="'._('Make thread not sticky').'" href="?/' . secure_link($board['dir'] . 'unsticky/' . $this->id) . '">' . $config['mod']['link_desticky'] . '</a>'; - else - $built .= ' <a title="'._('Make thread sticky').'" href="?/' . secure_link($board['dir'] . 'sticky/' . $this->id) . '">' . $config['mod']['link_sticky'] . '</a>'; - - if (hasPermission($config['mod']['bumplock'], $board['uri'], $this->mod)) - if ($this->sage) - $built .= ' <a title="'._('Allow thread to be bumped').'" href="?/' . secure_link($board['dir'] . 'bumpunlock/' . $this->id) . '">' . $config['mod']['link_bumpunlock'] . '</a>'; - else - $built .= ' <a title="'._('Prevent thread from being bumped').'" href="?/' . secure_link($board['dir'] . 'bumplock/' . $this->id) . '">' . $config['mod']['link_bumplock'] . '</a>'; - - // Lock - if (hasPermission($config['mod']['lock'], $board['uri'], $this->mod)) - if ($this->locked) - $built .= ' <a title="'._('Unlock thread').'" href="?/' . secure_link($board['dir'] . 'unlock/' . $this->id) . '">' . $config['mod']['link_unlock'] . '</a>'; - else - $built .= ' <a title="'._('Lock thread').'" href="?/' . secure_link($board['dir'] . 'lock/' . $this->id) . '">' . $config['mod']['link_lock'] . '</a>'; - - if (hasPermission($config['mod']['move'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Move thread to another board').'" href="?/' . $board['dir'] . 'move/' . $this->id . '">' . $config['mod']['link_move'] . '</a>'; - - // Edit post - if (hasPermission($config['mod']['editpost'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Edit post').'" href="?/' . $board['dir'] . 'edit' . ($config['mod']['raw_html_default'] ? '_raw' : '') . '/' . $this->id . '">' . $config['mod']['link_editpost'] . '</a>'; - - if (!empty($built)) - $built = '<span class="controls op">' . $built . '</span>'; - } - return $built; - } - - public function ratio() { - return fraction($this->filewidth, $this->fileheight, ':'); - } - public function build($index=false, $isnoko50=false) { global $board, $config, $debug; @@ -571,7 +453,7 @@ class Thread { event('show-thread', $this); - $built = Element('post_thread.html', array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'hasnoko50' => $hasnoko50, 'isnoko50' => $isnoko50)); + $built = Element('post_thread.html', array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'hasnoko50' => $hasnoko50, 'isnoko50' => $isnoko50, 'mod' => $this->mod)); return $built; } diff --git a/inc/functions.php b/inc/functions.php index efc0594b..db9c91b0 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -96,9 +96,9 @@ function loadConfig() { $configstr = file_get_contents('inc/instance-config.php'); - if (isset($board['dir']) && file_exists($board['dir'] . '/config.php')) { - $configstr .= file_get_contents($board['dir'] . '/config.php'); - } + if (isset($board['dir']) && file_exists($board['dir'] . '/config.php')) { + $configstr .= file_get_contents($board['dir'] . '/config.php'); + } $matches = array(); preg_match_all('/[^\/*#]\$config\s*\[\s*[\'"]locale[\'"]\s*\]\s*=\s*([\'"])(.*?)\1/', $configstr, $matches); if ($matches && isset ($matches[2]) && $matches[2]) { @@ -691,22 +691,15 @@ function displayBan($ban) { } $ban['ip'] = $_SERVER['REMOTE_ADDR']; + if ($ban['post'] && isset($ban['post']['board'], $ban['post']['id'])) { if (openBoard($ban['post']['board'])) { - - $query = query(sprintf("SELECT `thumb`, `file` FROM ``posts_%s`` WHERE `id` = " . + $query = query(sprintf("SELECT `files` FROM ``posts_%s`` WHERE `id` = " . (int)$ban['post']['id'], $board['uri'])); if ($_post = $query->fetch(PDO::FETCH_ASSOC)) { $ban['post'] = array_merge($ban['post'], $_post); - } else { - $ban['post']['file'] = 'deleted'; - $ban['post']['thumb'] = false; } - } else { - $ban['post']['file'] = 'deleted'; - $ban['post']['thumb'] = false; } - if ($ban['post']['thread']) { $post = new Post($ban['post']); } else { @@ -861,7 +854,7 @@ function insertFloodPost(array $post) { function post(array $post) { global $pdo, $board; - $query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :thumb, :thumbwidth, :thumbheight, :file, :width, :height, :filesize, :filename, :filehash, :password, :ip, :sticky, :locked, 0, :embed)", $board['uri'])); + $query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :files, :num_files, :filehash, :password, :ip, :sticky, :locked, 0, :embed)", $board['uri'])); // Basic stuff if (!empty($post['subject'])) { @@ -921,31 +914,12 @@ function post(array $post) { } if ($post['has_file']) { - $query->bindValue(':thumb', $post['thumb']); - $query->bindValue(':thumbwidth', $post['thumbwidth'], PDO::PARAM_INT); - $query->bindValue(':thumbheight', $post['thumbheight'], PDO::PARAM_INT); - $query->bindValue(':file', $post['file']); - - if (isset($post['width'], $post['height'])) { - $query->bindValue(':width', $post['width'], PDO::PARAM_INT); - $query->bindValue(':height', $post['height'], PDO::PARAM_INT); - } else { - $query->bindValue(':width', null, PDO::PARAM_NULL); - $query->bindValue(':height', null, PDO::PARAM_NULL); - } - - $query->bindValue(':filesize', $post['filesize'], PDO::PARAM_INT); - $query->bindValue(':filename', $post['filename']); + $query->bindValue(':files', json_encode($post['files'])); + $query->bindValue(':num_files', $post['num_files']); $query->bindValue(':filehash', $post['filehash']); } else { - $query->bindValue(':thumb', null, PDO::PARAM_NULL); - $query->bindValue(':thumbwidth', null, PDO::PARAM_NULL); - $query->bindValue(':thumbheight', null, PDO::PARAM_NULL); - $query->bindValue(':file', null, PDO::PARAM_NULL); - $query->bindValue(':width', null, PDO::PARAM_NULL); - $query->bindValue(':height', null, PDO::PARAM_NULL); - $query->bindValue(':filesize', null, PDO::PARAM_NULL); - $query->bindValue(':filename', null, PDO::PARAM_NULL); + $query->bindValue(':files', null, PDO::PARAM_NULL); + $query->bindValue(':num_files', 0); $query->bindValue(':filehash', null, PDO::PARAM_NULL); } @@ -973,33 +947,40 @@ function bumpThread($id) { } // Remove file from post -function deleteFile($id, $remove_entirely_if_already=true) { +function deleteFile($id, $remove_entirely_if_already=true, $file=null) { global $board, $config; - $query = prepare(sprintf("SELECT `thread`,`thumb`,`file` FROM ``posts_%s`` WHERE `id` = :id LIMIT 1", $board['uri'])); + $query = prepare(sprintf("SELECT `thread`, `files`, `num_files` FROM ``posts_%s`` WHERE `id` = :id LIMIT 1", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); if (!$post = $query->fetch(PDO::FETCH_ASSOC)) error($config['error']['invalidpost']); + $files = json_decode($post['files']); + $file_to_delete = $file !== false ? $files[(int)$file] : (object)array('file' => false); - if ($post['file'] == 'deleted' && !$post['thread']) + if ($files[0]->file == 'deleted' && $post['num_files'] == 1 && !$post['thread']) return; // Can't delete OP's image completely. - $query = prepare(sprintf("UPDATE ``posts_%s`` SET `thumb` = NULL, `thumbwidth` = NULL, `thumbheight` = NULL, `filewidth` = NULL, `fileheight` = NULL, `filesize` = NULL, `filename` = NULL, `filehash` = NULL, `file` = :file WHERE `id` = :id", $board['uri'])); - if ($post['file'] == 'deleted' && $remove_entirely_if_already) { + $query = prepare(sprintf("UPDATE ``posts_%s`` SET `files` = :file WHERE `id` = :id", $board['uri'])); + if (($file && $file_to_delete->file == 'deleted') && $remove_entirely_if_already) { // Already deleted; remove file fully - $query->bindValue(':file', null, PDO::PARAM_NULL); + $files[$file] = null; } else { - // Delete thumbnail - file_unlink($board['dir'] . $config['dir']['thumb'] . $post['thumb']); - - // Delete file - file_unlink($board['dir'] . $config['dir']['img'] . $post['file']); + foreach ($files as $i => $f) { + if (($file !== false && $i == $file) || $file === null) { + // Delete thumbnail + file_unlink($board['dir'] . $config['dir']['thumb'] . $f->thumb); + unset($files[$i]->thumb); - // Set file to 'deleted' - $query->bindValue(':file', 'deleted', PDO::PARAM_INT); + // Delete file + file_unlink($board['dir'] . $config['dir']['img'] . $f->file); + $files[$i]->file = 'deleted'; + } + } } + $query->bindValue(':file', json_encode($files), PDO::PARAM_STR); + $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); @@ -1037,7 +1018,7 @@ function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) { global $board, $config; // Select post and replies (if thread) in one query - $query = prepare(sprintf("SELECT `id`,`thread`,`thumb`,`file` FROM ``posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri'])); + $query = prepare(sprintf("SELECT `id`,`thread`,`files` FROM ``posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); @@ -1067,13 +1048,14 @@ function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) { // Rebuild thread $rebuild = &$post['thread']; } - if ($post['thumb']) { - // Delete thumbnail - file_unlink($board['dir'] . $config['dir']['thumb'] . $post['thumb']); - } - if ($post['file']) { + if ($post['files']) { // Delete file - file_unlink($board['dir'] . $config['dir']['img'] . $post['file']); + foreach (json_decode($post['files']) as $i => $f) { + if ($f->file !== 'deleted') { + file_unlink($board['dir'] . $config['dir']['img'] . $f->file); + file_unlink($board['dir'] . $config['dir']['thumb'] . $f->thumb); + } + } } $ids[] = (int)$post['id']; @@ -1190,8 +1172,8 @@ function index($page, $mod=false) { $num_images = 0; foreach ($replies as $po) { - if ($po['file']) - $num_images++; + if ($po['num_files']) + $num_images+=$po['num_files']; $thread->add(new Post($po, $mod ? '?/' : $config['root'], $mod)); } @@ -1214,7 +1196,7 @@ function index($page, $mod=false) { 'post_url' => $config['post_url'], 'config' => $config, 'boardlist' => createBoardlist($mod), - 'threads' => $threads + 'threads' => $threads, ); } @@ -1349,7 +1331,7 @@ function checkRobot($body) { // Returns an associative array with 'replies' and 'images' keys function numPosts($id) { global $board; - $query = prepare(sprintf("SELECT COUNT(*) AS `replies`, COUNT(NULLIF(`file`, 0)) AS `images` FROM ``posts_%s`` WHERE `thread` = :thread", $board['uri'], $board['uri'])); + $query = prepare(sprintf("SELECT COUNT(*) AS `replies`, SUM(`num_files`) AS `images` FROM ``posts_%s`` WHERE `thread` = :thread", $board['uri'], $board['uri'])); $query->bindValue(':thread', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); @@ -1907,7 +1889,7 @@ function markup(&$body, $track_cites = false) { } // replace tabs with 8 spaces - $body = str_replace("\t", ' ', $body); + $body = str_replace("\t", ' ', $body); return $tracked_cites; } @@ -2054,8 +2036,8 @@ function buildThread50($id, $return = false, $mod = false, $thread = null, $anti if (!isset($thread)) { $thread = new Thread($post, $mod ? '?/' : $config['root'], $mod); } else { - if ($post['file']) - $num_images++; + if ($post['files']) + $num_images += $post['num_files']; $thread->add(new Post($post, $mod ? '?/' : $config['root'], $mod)); } @@ -2067,7 +2049,8 @@ function buildThread50($id, $return = false, $mod = false, $thread = null, $anti if ($query->rowCount() == $config['noko50_count']+1) { - $count = prepare(sprintf("SELECT COUNT(`id`) as `num` FROM ``posts_%s`` WHERE `thread` = :thread UNION ALL SELECT COUNT(`id`) FROM ``posts_%s`` WHERE `file` IS NOT NULL AND `thread` = :thread", $board['uri'], $board['uri'])); + $count = prepare(sprintf("SELECT COUNT(`id`) as `num` FROM ``posts_%s`` WHERE `thread` = :thread UNION ALL + SELECT SUM(`num_files`) FROM ``posts_%s`` WHERE `files` IS NOT NULL AND `thread` = :thread", $board['uri'], $board['uri'])); $count->bindValue(':thread', $id, PDO::PARAM_INT); $count->execute() or error(db_error($count)); @@ -2087,8 +2070,8 @@ function buildThread50($id, $return = false, $mod = false, $thread = null, $anti foreach ($allPosts as $index => $post) { if ($index == count($allPosts)-count($thread->posts)) break; - if ($post->file) - $thread->omitted_images++; + if ($post->files) + $thread->omitted_images += $post->num_files; } } @@ -2234,13 +2217,15 @@ function getPostByHashInThread($hash, $thread) { } function undoImage(array $post) { - if (!$post['has_file']) + if (!$post['has_file'] || !isset($post['files'])) return; - if (isset($post['file_path'])) - file_unlink($post['file_path']); - if (isset($post['thumb_path'])) - file_unlink($post['thumb_path']); + foreach ($post['files'] as $key => $file) { + if (isset($file['file_path'])) + file_unlink($file['file_path']); + if (isset($file['thumb_path'])) + file_unlink($file['thumb_path']); + } } function rDNS($ip_addr) { diff --git a/inc/lib/Twig/Extensions/Extension/Tinyboard.php b/inc/lib/Twig/Extensions/Extension/Tinyboard.php index 727d5d2c..986ecf45 100644 --- a/inc/lib/Twig/Extensions/Extension/Tinyboard.php +++ b/inc/lib/Twig/Extensions/Extension/Tinyboard.php @@ -13,6 +13,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension new Twig_SimpleFilter('filesize', 'format_bytes'), new Twig_SimpleFilter('truncate', 'twig_truncate_filter'), new Twig_SimpleFilter('truncate_body', 'truncate'), + new Twig_SimpleFilter('truncate_filename', 'twig_filename_truncate_filter'), new Twig_SimpleFilter('extension', 'twig_extension_filter'), new Twig_SimpleFilter('sprintf', 'sprintf'), new Twig_SimpleFilter('capcode', 'capcode'), @@ -25,7 +26,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension new Twig_SimpleFilter('until', 'until'), new Twig_SimpleFilter('push', 'twig_push_filter'), new Twig_SimpleFilter('bidi_cleanup', 'bidi_cleanup'), - new Twig_SimpleFilter('addslashes', 'addslashes') + new Twig_SimpleFilter('addslashes', 'addslashes'), ); } @@ -42,6 +43,9 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension new Twig_SimpleFunction('timezone', 'twig_timezone_function'), new Twig_SimpleFunction('hiddenInputs', 'hiddenInputs'), new Twig_SimpleFunction('hiddenInputsHash', 'hiddenInputsHash'), + new Twig_SimpleFunction('ratio', 'twig_ratio_function'), + new Twig_SimpleFunction('secure_link_confirm', 'twig_secure_link_confirm'), + new Twig_SimpleFunction('secure_link', 'twig_secure_link') ); } @@ -100,3 +104,30 @@ function twig_truncate_filter($value, $length = 30, $preserve = false, $separato return $value; } +function twig_filename_truncate_filter($value, $length = 30, $separator = '…') { + if (mb_strlen($value) > $length) { + $value = strrev($value); + $array = array_reverse(explode(".", $value, 2)); + $array = array_map("strrev", $array); + + $filename = &$array[0]; + $extension = isset($array[1]) ? $array[1] : false; + + $filename = mb_substr($filename, 0, $length - ($extension ? mb_strlen($extension) + 1 : 0)) . $separator; + + return implode(".", $array); + } + return $value; +} + +function twig_ratio_function($w, $h) { + return fraction($w, $h, ':'); +} +function twig_secure_link_confirm($text, $title, $confirm_message, $href) { + global $config; + + return '<a onclick="if (event.which==2) return true;if (confirm(\'' . htmlentities(addslashes($confirm_message)) . '\')) document.location=\'?/' . htmlspecialchars(addslashes($href . '/' . make_secure_link_token($href))) . '\';return false;" title="' . htmlentities($title) . '" href="?/' . $href . '">' . $text . '</a>'; +} +function twig_secure_link($href) { + return $href . '/' . make_secure_link_token($href); +} diff --git a/inc/lib/minify/JSMin.php b/inc/lib/minify/JSMin.php index b6879f37..9840d8b3 100755 --- a/inc/lib/minify/JSMin.php +++ b/inc/lib/minify/JSMin.php @@ -7,14 +7,14 @@ * </code> * * This is a modified port of jsmin.c. Improvements: - * + * * Does not choke on some regexp literals containing quote characters. E.g. /'/ - * - * Spaces are preserved after some add/sub operators, so they are not mistakenly + * + * Spaces are preserved after some add/sub operators, so they are not mistakenly * converted to post-inc/dec. E.g. a + ++b -> a+ ++b * * Preserves multi-line comments that begin with /*! - * + * * PHP 5 or higher is required. * * Permission is hereby granted to use this version of the library under the @@ -69,6 +69,7 @@ class JSMin { protected $lookAhead = null; protected $output = ''; protected $lastByteOut = ''; + protected $keptComment = ''; /** * Minify Javascript. @@ -116,8 +117,8 @@ class JSMin { // determine next command $command = self::ACTION_KEEP_A; // default if ($this->a === ' ') { - if (($this->lastByteOut === '+' || $this->lastByteOut === '-') - && ($this->b === $this->lastByteOut)) { + if (($this->lastByteOut === '+' || $this->lastByteOut === '-') + && ($this->b === $this->lastByteOut)) { // Don't delete this space. If we do, the addition/subtraction // could be parsed as a post-increment } elseif (! $this->isAlphaNum($this->b)) { @@ -126,16 +127,17 @@ class JSMin { } elseif ($this->a === "\n") { if ($this->b === ' ') { $command = self::ACTION_DELETE_A_B; - // in case of mbstring.func_overload & 2, must check for null b, - // otherwise mb_strpos will give WARNING + + // in case of mbstring.func_overload & 2, must check for null b, + // otherwise mb_strpos will give WARNING } elseif ($this->b === null - || (false === strpos('{[(+-', $this->b) + || (false === strpos('{[(+-!~', $this->b) && ! $this->isAlphaNum($this->b))) { $command = self::ACTION_DELETE_A; } } elseif (! $this->isAlphaNum($this->a)) { if ($this->b === ' ' - || ($this->b === "\n" + || ($this->b === "\n" && (false === strpos('}])+-"\'', $this->a)))) { $command = self::ACTION_DELETE_A_B; } @@ -160,7 +162,8 @@ class JSMin { */ protected function action($command) { - if ($command === self::ACTION_DELETE_A_B + // make sure we don't compress "a + ++b" to "a+++b", etc. + if ($command === self::ACTION_DELETE_A_B && $this->b === ' ' && ($this->a === '+' || $this->a === '-')) { // Note: we're at an addition/substraction operator; the inputIndex @@ -170,58 +173,88 @@ class JSMin { $command = self::ACTION_KEEP_A; } } + switch ($command) { - case self::ACTION_KEEP_A: + case self::ACTION_KEEP_A: // 1 $this->output .= $this->a; + + if ($this->keptComment) { + $this->output = rtrim($this->output, "\n"); + $this->output .= $this->keptComment; + $this->keptComment = ''; + } + $this->lastByteOut = $this->a; - - // fallthrough - case self::ACTION_DELETE_A: + + // fallthrough intentional + case self::ACTION_DELETE_A: // 2 $this->a = $this->b; if ($this->a === "'" || $this->a === '"') { // string literal $str = $this->a; // in case needed for exception - while (true) { + for(;;) { $this->output .= $this->a; $this->lastByteOut = $this->a; - - $this->a = $this->get(); + + $this->a = $this->get(); if ($this->a === $this->b) { // end quote break; } - if (ord($this->a) <= self::ORD_LF) { + if ($this->isEOF($this->a)) { + $byte = $this->inputIndex - 1; throw new JSMin_UnterminatedStringException( - "JSMin: Unterminated String at byte " - . $this->inputIndex . ": {$str}"); + "JSMin: Unterminated String at byte {$byte}: {$str}"); } $str .= $this->a; if ($this->a === '\\') { $this->output .= $this->a; $this->lastByteOut = $this->a; - + $this->a = $this->get(); $str .= $this->a; } } } - // fallthrough - case self::ACTION_DELETE_A_B: + + // fallthrough intentional + case self::ACTION_DELETE_A_B: // 3 $this->b = $this->next(); - if ($this->b === '/' && $this->isRegexpLiteral()) { // RegExp literal + if ($this->b === '/' && $this->isRegexpLiteral()) { $this->output .= $this->a . $this->b; - $pattern = '/'; // in case needed for exception - while (true) { + $pattern = '/'; // keep entire pattern in case we need to report it in the exception + for(;;) { $this->a = $this->get(); $pattern .= $this->a; + if ($this->a === '[') { + for(;;) { + $this->output .= $this->a; + $this->a = $this->get(); + $pattern .= $this->a; + if ($this->a === ']') { + break; + } + if ($this->a === '\\') { + $this->output .= $this->a; + $this->a = $this->get(); + $pattern .= $this->a; + } + if ($this->isEOF($this->a)) { + throw new JSMin_UnterminatedRegExpException( + "JSMin: Unterminated set in RegExp at byte " + . $this->inputIndex .": {$pattern}"); + } + } + } + if ($this->a === '/') { // end pattern break; // while (true) } elseif ($this->a === '\\') { $this->output .= $this->a; - $this->a = $this->get(); - $pattern .= $this->a; - } elseif (ord($this->a) <= self::ORD_LF) { + $this->a = $this->get(); + $pattern .= $this->a; + } elseif ($this->isEOF($this->a)) { + $byte = $this->inputIndex - 1; throw new JSMin_UnterminatedRegExpException( - "JSMin: Unterminated RegExp at byte " - . $this->inputIndex .": {$pattern}"); + "JSMin: Unterminated RegExp at byte {$byte}: {$pattern}"); } $this->output .= $this->a; $this->lastByteOut = $this->a; @@ -237,31 +270,43 @@ class JSMin { */ protected function isRegexpLiteral() { - if (false !== strpos("\n{;(,=:[!&|?", $this->a)) { // we aren't dividing + if (false !== strpos("(,=:[!&|?+-~*{;", $this->a)) { + // we obviously aren't dividing return true; } - if (' ' === $this->a) { - $length = strlen($this->output); - if ($length < 2) { // weird edge case - return true; + + // we have to check for a preceding keyword, and we don't need to pattern + // match over the whole output. + $recentOutput = substr($this->output, -10); + + // check if return/typeof directly precede a pattern without a space + foreach (array('return', 'typeof') as $keyword) { + if ($this->a !== substr($keyword, -1)) { + // certainly wasn't keyword + continue; } - // you can't divide a keyword - if (preg_match('/(?:case|else|in|return|typeof)$/', $this->output, $m)) { - if ($this->output === $m[0]) { // odd but could happen - return true; - } - // make sure it's a keyword, not end of an identifier - $charBeforeKeyword = substr($this->output, $length - strlen($m[0]) - 1, 1); - if (! $this->isAlphaNum($charBeforeKeyword)) { + if (preg_match("~(^|[\\s\\S])" . substr($keyword, 0, -1) . "$~", $recentOutput, $m)) { + if ($m[1] === '' || !$this->isAlphaNum($m[1])) { return true; } } } + + // check all keywords + if ($this->a === ' ' || $this->a === "\n") { + if (preg_match('~(^|[\\s\\S])(?:case|else|in|return|typeof)$~', $recentOutput, $m)) { + if ($m[1] === '' || !$this->isAlphaNum($m[1])) { + return true; + } + } + } + return false; } /** - * Get next char. Convert ctrl char to space. + * Return the next character from stdin. Watch out for lookahead. If the character is a control character, + * translate it to a space or linefeed. * * @return string */ @@ -270,24 +315,36 @@ class JSMin { $c = $this->lookAhead; $this->lookAhead = null; if ($c === null) { + // getc(stdin) if ($this->inputIndex < $this->inputLength) { $c = $this->input[$this->inputIndex]; $this->inputIndex += 1; } else { - return null; + $c = null; } } - if ($c === "\r" || $c === "\n") { - return "\n"; + if (ord($c) >= self::ORD_SPACE || $c === "\n" || $c === null) { + return $c; } - if (ord($c) < self::ORD_SPACE) { // control char - return ' '; + if ($c === "\r") { + return "\n"; } - return $c; + return ' '; + } + + /** + * Does $a indicate end of input? + * + * @param string $a + * @return bool + */ + protected function isEOF($a) + { + return ord($a) <= self::ORD_LF; } /** - * Get next char. If is ctrl character, translate to a space or newline. + * Get next char (without getting it). If is ctrl character, translate to a space or newline. * * @return string */ @@ -298,7 +355,7 @@ class JSMin { } /** - * Is $c a letter, digit, underscore, dollar sign, escape, or non-ASCII? + * Return true if the character is a letter, digit, underscore, dollar sign, or non-ASCII character. * * @param string $c * @@ -306,77 +363,84 @@ class JSMin { */ protected function isAlphaNum($c) { - return (preg_match('/^[0-9a-zA-Z_\\$\\\\]$/', $c) || ord($c) > 126); + return (preg_match('/^[a-z0-9A-Z_\\$\\\\]$/', $c) || ord($c) > 126); } /** - * @return string + * Consume a single line comment from input (possibly retaining it) */ - protected function singleLineComment() + protected function consumeSingleLineComment() { $comment = ''; while (true) { $get = $this->get(); $comment .= $get; - if (ord($get) <= self::ORD_LF) { // EOL reached + if (ord($get) <= self::ORD_LF) { // end of line reached // if IE conditional comment if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) { - return "/{$comment}"; + $this->keptComment .= "/{$comment}"; } - return $get; + return; } } } /** - * @return string + * Consume a multiple line comment from input (possibly retaining it) + * * @throws JSMin_UnterminatedCommentException */ - protected function multipleLineComment() + protected function consumeMultipleLineComment() { $this->get(); $comment = ''; - while (true) { + for(;;) { $get = $this->get(); if ($get === '*') { if ($this->peek() === '/') { // end of comment reached $this->get(); - // if comment preserved by YUI Compressor if (0 === strpos($comment, '!')) { - return "\n/*!" . substr($comment, 1) . "*/\n"; - } - // if IE conditional comment - if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) { - return "/*{$comment}*/"; + // preserved by YUI Compressor + if (!$this->keptComment) { + // don't prepend a newline if two comments right after one another + $this->keptComment = "\n"; + } + $this->keptComment .= "/*!" . substr($comment, 1) . "*/\n"; + } else if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) { + // IE conditional + $this->keptComment .= "/*{$comment}*/"; } - return ' '; + return; } } elseif ($get === null) { throw new JSMin_UnterminatedCommentException( - "JSMin: Unterminated comment at byte " - . $this->inputIndex . ": /*{$comment}"); + "JSMin: Unterminated comment at byte {$this->inputIndex}: /*{$comment}"); } $comment .= $get; } } /** - * Get the next character, skipping over comments. - * Some comments may be preserved. + * Get the next character, skipping over comments. Some comments may be preserved. * * @return string */ protected function next() { $get = $this->get(); - if ($get !== '/') { - return $get; - } - switch ($this->peek()) { - case '/': return $this->singleLineComment(); - case '*': return $this->multipleLineComment(); - default: return $get; + if ($get === '/') { + switch ($this->peek()) { + case '/': + $this->consumeSingleLineComment(); + $get = "\n"; + break; + case '*': + $this->consumeMultipleLineComment(); + $get = ' '; + break; + } } + return $get; } } diff --git a/inc/lib/minify/Minify.php b/inc/lib/minify/Minify.php index 9634f221..2b38f89b 100755 --- a/inc/lib/minify/Minify.php +++ b/inc/lib/minify/Minify.php @@ -3,11 +3,6 @@ * Class Minify * @package Minify */ - -/** - * Minify_Source - */ -require_once 'Minify/Source.php'; /** * Minify - Combines, minifies, and caches JavaScript and CSS files on demand. @@ -29,7 +24,7 @@ require_once 'Minify/Source.php'; */ class Minify { - const VERSION = '2.1.5'; + const VERSION = '2.2.0'; const TYPE_CSS = 'text/css'; const TYPE_HTML = 'text/html'; // there is some debate over the ideal JS Content-Type, but this is the @@ -85,7 +80,6 @@ class Minify { public static function setCache($cache = '', $fileLocking = true) { if (is_string($cache)) { - require_once 'Minify/Cache/File.php'; self::$_cache = new Minify_Cache_File($cache, $fileLocking); } else { self::$_cache = $cache; @@ -161,9 +155,11 @@ class Minify { * * @param array $options controller/serve options * - * @return mixed null, or, if the 'quiet' option is set to true, an array + * @return null|array if the 'quiet' option is set to true, an array * with keys "success" (bool), "statusCode" (int), "content" (string), and * "headers" (array). + * + * @throws Exception */ public static function serve($controller, $options = array()) { @@ -174,10 +170,6 @@ class Minify { if (is_string($controller)) { // make $controller into object $class = 'Minify_Controller_' . $controller; - if (! class_exists($class, false)) { - require_once "Minify/Controller/" - . str_replace('_', '/', $controller) . ".php"; - } $controller = new $class(); /* @var Minify_Controller_Base $controller */ } @@ -219,8 +211,7 @@ class Minify { $contentEncoding = self::$_options['encodeMethod']; } else { // sniff request header - require_once 'HTTP/Encoder.php'; - // depending on what the client accepts, $contentEncoding may be + // depending on what the client accepts, $contentEncoding may be // 'x-gzip' while our internal encodeMethod is 'gzip'. Calling // getAcceptedEncoding(false, false) leaves out compress and deflate as options. list(self::$_options['encodeMethod'], $contentEncoding) = HTTP_Encoder::getAcceptedEncoding(false, false); @@ -231,7 +222,6 @@ class Minify { } // check client cache - require_once 'HTTP/ConditionalGet.php'; $cgOptions = array( 'lastModifiedTime' => self::$_options['lastModifiedTime'] ,'isPublic' => self::$_options['isPublic'] @@ -300,7 +290,7 @@ class Minify { throw $e; } self::$_cache->store($cacheId, $content); - if (function_exists('gzencode')) { + if (function_exists('gzencode') && self::$_options['encodeMethod']) { self::$_cache->store($cacheId . '.gz', gzencode($content, self::$_options['encodeLevel'])); } } @@ -451,7 +441,7 @@ class Minify { /** * Set up sources to use Minify_Lines * - * @param array $sources Minify_Source instances + * @param Minify_Source[] $sources Minify_Source instances */ protected static function _setupDebug($sources) { @@ -468,6 +458,8 @@ class Minify { * Combines sources and minifies the result. * * @return string + * + * @throws Exception */ protected static function _combineMinify() { @@ -526,7 +518,6 @@ class Minify { $imploded = implode($implodeSeparator, $groupToProcessTogether); $groupToProcessTogether = array(); if ($lastMinifier) { - self::$_controller->loadMinifier($lastMinifier); try { $content[] = call_user_func($lastMinifier, $imploded, $lastOptions); } catch (Exception $e) { @@ -574,7 +565,7 @@ class Minify { { $name = preg_replace('/[^a-zA-Z0-9\\.=_,]/', '', self::$_controller->selectionId); $name = preg_replace('/\\.+/', '.', $name); - $name = substr($name, 0, 200 - 34 - strlen($prefix)); + $name = substr($name, 0, 100 - 34 - strlen($prefix)); $md5 = md5(serialize(array( Minify_Source::getDigest(self::$_controller->sources) ,self::$_options['minifiers'] diff --git a/inc/lib/minify/Minify/Build.php b/inc/lib/minify/Minify/Build.php index e625165e..1185fbe7 100755 --- a/inc/lib/minify/Minify/Build.php +++ b/inc/lib/minify/Minify/Build.php @@ -4,8 +4,6 @@ * @package Minify */ -require_once 'Minify/Source.php'; - /** * Maintain a single last modification time for a group of Minify sources to * allow use of far off Expires headers in Minify. diff --git a/inc/lib/minify/Minify/CSS.php b/inc/lib/minify/Minify/CSS.php index 49882e9d..32414551 100755 --- a/inc/lib/minify/Minify/CSS.php +++ b/inc/lib/minify/Minify/CSS.php @@ -56,6 +56,7 @@ class Minify_CSS { public static function minify($css, $options = array())
{
$options = array_merge(array(
+ 'compress' => true,
'removeCharsets' => true,
'preserveComments' => true,
'currentDir' => null,
@@ -67,21 +68,20 @@ class Minify_CSS { if ($options['removeCharsets']) {
$css = preg_replace('/@charset[^;]+;\\s*/', '', $css);
}
- require_once 'Minify/CSS/Compressor.php';
- if (! $options['preserveComments']) {
- $css = Minify_CSS_Compressor::process($css, $options);
- } else {
- require_once 'Minify/CommentPreserver.php';
- $css = Minify_CommentPreserver::process(
- $css
- ,array('Minify_CSS_Compressor', 'process')
- ,array($options)
- );
+ if ($options['compress']) {
+ if (! $options['preserveComments']) {
+ $css = Minify_CSS_Compressor::process($css, $options);
+ } else {
+ $css = Minify_CommentPreserver::process(
+ $css
+ ,array('Minify_CSS_Compressor', 'process')
+ ,array($options)
+ );
+ }
}
if (! $options['currentDir'] && ! $options['prependRelativePath']) {
return $css;
}
- require_once 'Minify/CSS/UriRewriter.php';
if ($options['currentDir']) {
return Minify_CSS_UriRewriter::rewrite(
$css
diff --git a/inc/lib/minify/Minify/CSS/UriRewriter.php b/inc/lib/minify/Minify/CSS/UriRewriter.php index 8845f156..43cc2548 100755 --- a/inc/lib/minify/Minify/CSS/UriRewriter.php +++ b/inc/lib/minify/Minify/CSS/UriRewriter.php @@ -70,7 +70,7 @@ class Minify_CSS_UriRewriter { // rewrite $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/' ,array(self::$className, '_processUriCB'), $css); - $css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/' + $css = preg_replace_callback('/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/' ,array(self::$className, '_processUriCB'), $css); return $css; @@ -94,7 +94,7 @@ class Minify_CSS_UriRewriter { // append $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/' ,array(self::$className, '_processUriCB'), $css); - $css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/' + $css = preg_replace_callback('/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/' ,array(self::$className, '_processUriCB'), $css); self::$_prependPath = null; @@ -282,11 +282,8 @@ class Minify_CSS_UriRewriter { ? $m[1] : substr($m[1], 1, strlen($m[1]) - 2); } - // analyze URI - if ('/' !== $uri[0] // root-relative - && false === strpos($uri, '//') // protocol (non-data) - && 0 !== strpos($uri, 'data:') // data protocol - ) { + // if not root/scheme relative and not starts with scheme + if (!preg_match('~^(/|[a-z]+\:)~', $uri)) { // URI is file-relative: rewrite depending on options if (self::$_prependPath === null) { $uri = self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks); diff --git a/inc/lib/minify/Minify/Cache/File.php b/inc/lib/minify/Minify/Cache/File.php index 3fa6b18f..c228eb2b 100755 --- a/inc/lib/minify/Minify/Cache/File.php +++ b/inc/lib/minify/Minify/Cache/File.php @@ -98,6 +98,9 @@ class Minify_Cache_File { { if ($this->_locking) { $fp = fopen($this->_path . '/' . $id, 'rb'); + if (!$fp) { + return false; + } flock($fp, LOCK_SH); $ret = stream_get_contents($fp); flock($fp, LOCK_UN); @@ -186,7 +189,6 @@ class Minify_Cache_File { */ protected function _log($msg) { - require_once 'Minify/Logger.php'; Minify_Logger::log($msg); } diff --git a/inc/lib/minify/Minify/Controller/Base.php b/inc/lib/minify/Minify/Controller/Base.php index 240b5449..5a863290 100755 --- a/inc/lib/minify/Minify/Controller/Base.php +++ b/inc/lib/minify/Minify/Controller/Base.php @@ -79,33 +79,6 @@ abstract class Minify_Controller_Base { } /** - * Load any code necessary to execute the given minifier callback. - * - * The controller is responsible for loading minification code on demand - * via this method. This built-in function will only load classes for - * static method callbacks where the class isn't already defined. It uses - * the PEAR convention, so, given array('Jimmy_Minifier', 'minCss'), this - * function will include 'Jimmy/Minifier.php'. - * - * If you need code loaded on demand and this doesn't suit you, you'll need - * to override this function in your subclass. - * @see Minify_Controller_Page::loadMinifier() - * - * @param callback $minifierCallback callback of minifier function - * - * @return null - */ - public function loadMinifier($minifierCallback) - { - if (is_array($minifierCallback) - && is_string($minifierCallback[0]) - && !class_exists($minifierCallback[0], false)) { - - require str_replace('_', '/', $minifierCallback[0]) . '.php'; - } - } - - /** * Is a user-given file within an allowable directory, existing, * and having an extension js/css/html/txt ? * @@ -244,7 +217,6 @@ abstract class Minify_Controller_Base { * @return null */ public function log($msg) { - require_once 'Minify/Logger.php'; Minify_Logger::log($msg); } } diff --git a/inc/lib/minify/Minify/Controller/Files.php b/inc/lib/minify/Minify/Controller/Files.php index 83f028ad..f084cd07 100755 --- a/inc/lib/minify/Minify/Controller/Files.php +++ b/inc/lib/minify/Minify/Controller/Files.php @@ -4,8 +4,6 @@ * @package Minify */ -require_once 'Minify/Controller/Base.php'; - /** * Controller class for minifying a set of files * diff --git a/inc/lib/minify/Minify/Controller/Groups.php b/inc/lib/minify/Minify/Controller/Groups.php index 2d4e43b2..c4c25db1 100755 --- a/inc/lib/minify/Minify/Controller/Groups.php +++ b/inc/lib/minify/Minify/Controller/Groups.php @@ -4,8 +4,6 @@ * @package Minify */ -require_once 'Minify/Controller/Base.php'; - /** * Controller class for serving predetermined groups of minimized sets, selected * by PATH_INFO diff --git a/inc/lib/minify/Minify/Controller/MinApp.php b/inc/lib/minify/Minify/Controller/MinApp.php index d47c60d9..6943ee6b 100755 --- a/inc/lib/minify/Minify/Controller/MinApp.php +++ b/inc/lib/minify/Minify/Controller/MinApp.php @@ -4,8 +4,6 @@ * @package Minify */ -require_once 'Minify/Controller/Base.php'; - /** * Controller class for requests to /min/index.php * @@ -22,6 +20,13 @@ class Minify_Controller_MinApp extends Minify_Controller_Base { * @return array Minify options */ public function setupSources($options) { + // PHP insecure by default: realpath() and other FS functions can't handle null bytes. + foreach (array('g', 'b', 'f') as $key) { + if (isset($_GET[$key])) { + $_GET[$key] = str_replace("\x00", '', (string)$_GET[$key]); + } + } + // filter controller options $cOptions = array_merge( array( @@ -36,7 +41,6 @@ class Minify_Controller_MinApp extends Minify_Controller_Base { $sources = array(); $this->selectionId = ''; $firstMissingResource = null; - if (isset($_GET['g'])) { // add group(s) $this->selectionId .= 'g=' . $_GET['g']; @@ -195,9 +199,12 @@ class Minify_Controller_MinApp extends Minify_Controller_Base { protected function _getFileSource($file, $cOptions) { $spec['filepath'] = $file; - if ($cOptions['noMinPattern'] - && preg_match($cOptions['noMinPattern'], basename($file))) { - $spec['minifier'] = ''; + if ($cOptions['noMinPattern'] && preg_match($cOptions['noMinPattern'], basename($file))) { + if (preg_match('~\.css$~i', $file)) { + $spec['minifyOptions']['compress'] = false; + } else { + $spec['minifier'] = ''; + } } return new Minify_Source($spec); } diff --git a/inc/lib/minify/Minify/Controller/Page.php b/inc/lib/minify/Minify/Controller/Page.php index de471e12..1095fb46 100755 --- a/inc/lib/minify/Minify/Controller/Page.php +++ b/inc/lib/minify/Minify/Controller/Page.php @@ -4,8 +4,6 @@ * @package Minify */ -require_once 'Minify/Controller/Base.php'; - /** * Controller class for serving a single HTML page * @@ -59,7 +57,6 @@ class Minify_Controller_Page extends Minify_Controller_Base { 'cssMinifier' => array('Minify_CSS', 'minify') ,'jsMinifier' => array('JSMin', 'minify') ); - $this->_loadCssJsMinifiers = true; unset($options['minifyAll']); } $this->sources[] = new Minify_Source($sourceSpec); @@ -67,21 +64,5 @@ class Minify_Controller_Page extends Minify_Controller_Base { $options['contentType'] = Minify::TYPE_HTML; return $options; } - - protected $_loadCssJsMinifiers = false; - - /** - * @see Minify_Controller_Base::loadMinifier() - */ - public function loadMinifier($minifierCallback) - { - if ($this->_loadCssJsMinifiers) { - // Minify will not call for these so we must manually load - // them when Minify/HTML.php is called for. - require_once 'Minify/CSS.php'; - require_once 'JSMin.php'; - } - parent::loadMinifier($minifierCallback); // load Minify/HTML.php - } } diff --git a/inc/lib/minify/Minify/Controller/Version1.php b/inc/lib/minify/Minify/Controller/Version1.php index 5279d36d..91fcf614 100755 --- a/inc/lib/minify/Minify/Controller/Version1.php +++ b/inc/lib/minify/Minify/Controller/Version1.php @@ -4,8 +4,6 @@ * @package Minify */ -require_once 'Minify/Controller/Base.php'; - /** * Controller class for emulating version 1 of minify.php (mostly a proof-of-concept) * @@ -26,6 +24,11 @@ class Minify_Controller_Version1 extends Minify_Controller_Base { * */ public function setupSources($options) { + // PHP insecure by default: realpath() and other FS functions can't handle null bytes. + if (isset($_GET['files'])) { + $_GET['files'] = str_replace("\x00", '', (string)$_GET['files']); + } + self::_setupDefines(); if (MINIFY_USE_CACHE) { $cacheDir = defined('MINIFY_CACHE_DIR') @@ -51,8 +54,7 @@ class Minify_Controller_Version1 extends Minify_Controller_Base { ) { return $options; } - $extension = $m[1]; - + $files = explode(',', $_GET['files']); if (count($files) > MINIFY_MAX_FILES) { return $options; @@ -63,7 +65,6 @@ class Minify_Controller_Version1 extends Minify_Controller_Base { . DIRECTORY_SEPARATOR; $prependAbsPaths = $_SERVER['DOCUMENT_ROOT']; - $sources = array(); $goodFiles = array(); $hasBadSource = false; diff --git a/inc/lib/minify/Minify/HTML.php b/inc/lib/minify/Minify/HTML.php index e9453ffb..40f73076 100755 --- a/inc/lib/minify/Minify/HTML.php +++ b/inc/lib/minify/Minify/HTML.php @@ -1,22 +1,26 @@ <?php
/**
- * Class Minify_HTML
+ * Class Minify_HTML
* @package Minify
*/
/**
* Compress HTML
*
- * This is a heavy regex-based removal of whitespace, unnecessary comments and
+ * This is a heavy regex-based removal of whitespace, unnecessary comments and
* tokens. IE conditional comments are preserved. There are also options to have
- * STYLE and SCRIPT blocks compressed by callback functions.
- *
+ * STYLE and SCRIPT blocks compressed by callback functions.
+ *
* A test suite is available.
- *
+ *
* @package Minify
* @author Stephen Clay <steve@mrclay.org>
*/
class Minify_HTML {
+ /**
+ * @var boolean
+ */
+ protected $_jsCleanComments = true;
/**
* "Minify" an HTML page
@@ -27,21 +31,21 @@ class Minify_HTML { *
* 'cssMinifier' : (optional) callback function to process content of STYLE
* elements.
- *
+ *
* 'jsMinifier' : (optional) callback function to process content of SCRIPT
* elements. Note: the type attribute is ignored.
- *
+ *
* 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If
* unset, minify will sniff for an XHTML doctype.
- *
+ *
* @return string
*/
public static function minify($html, $options = array()) {
- $min = new Minify_HTML($html, $options);
+ $min = new self($html, $options);
return $min->process();
}
-
-
+
+
/**
* Create a minifier object
*
@@ -51,14 +55,14 @@ class Minify_HTML { *
* 'cssMinifier' : (optional) callback function to process content of STYLE
* elements.
- *
+ *
* 'jsMinifier' : (optional) callback function to process content of SCRIPT
* elements. Note: the type attribute is ignored.
- *
+ *
+ * 'jsCleanComments' : (optional) whether to remove HTML comments beginning and end of script block
+ *
* 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If
* unset, minify will sniff for an XHTML doctype.
- *
- * @return null
*/
public function __construct($html, $options = array())
{
@@ -72,9 +76,12 @@ class Minify_HTML { if (isset($options['jsMinifier'])) {
$this->_jsMinifier = $options['jsMinifier'];
}
+ if (isset($options['jsCleanComments'])) {
+ $this->_jsCleanComments = (bool)$options['jsCleanComments'];
+ }
}
-
-
+
+
/**
* Minify the markeup given in the constructor
*
@@ -124,7 +131,7 @@ class Minify_HTML { // remove ws around block/undisplayed elements
$this->_html = preg_replace('/\\s+(<\\/?(?:area|base(?:font)?|blockquote|body'
- .'|caption|center|cite|col(?:group)?|dd|dir|div|dl|dt|fieldset|form'
+ .'|caption|center|col(?:group)?|dd|dir|div|dl|dt|fieldset|form'
.'|frame(?:set)?|h[1-6]|head|hr|html|legend|li|link|map|menu|meta'
.'|ol|opt(?:group|ion)|p|param|t(?:able|body|head|d|h||r|foot|itle)'
.'|ul)\\b[^>]*>)/i', '$1', $this->_html);
@@ -213,17 +220,19 @@ class Minify_HTML { // whitespace surrounding? preserve at least one space
$ws1 = ($m[1] === '') ? '' : ' ';
$ws2 = ($m[4] === '') ? '' : ' ';
-
+
// remove HTML comments (and ending "//" if present)
- $js = preg_replace('/(?:^\\s*<!--\\s*|\\s*(?:\\/\\/)?\\s*-->\\s*$)/', '', $js);
-
+ if ($this->_jsCleanComments) {
+ $js = preg_replace('/(?:^\\s*<!--\\s*|\\s*(?:\\/\\/)?\\s*-->\\s*$)/', '', $js);
+ }
+
// remove CDATA section markers
$js = $this->_removeCdata($js);
// minify
$minifier = $this->_jsMinifier
? $this->_jsMinifier
- : 'trim';
+ : 'trim';
$js = call_user_func($minifier, $js);
return $this->_reservePlace($this->_needsCdata($js)
diff --git a/inc/lib/minify/Minify/HTML/Helper.php b/inc/lib/minify/Minify/HTML/Helper.php index 807fdc9c..f92ab854 100755 --- a/inc/lib/minify/Minify/HTML/Helper.php +++ b/inc/lib/minify/Minify/HTML/Helper.php @@ -15,10 +15,10 @@ class Minify_HTML_Helper { public $minAppUri = '/min'; public $groupsConfigFile = ''; - /* + /** * Get an HTML-escaped Minify URI for a group or set of files * - * @param mixed $keyOrFiles a group key or array of filepaths/URIs + * @param string|array $keyOrFiles a group key or array of filepaths/URIs * @param array $opts options: * 'farExpires' : (default true) append a modified timestamp for cache revving * 'debug' : (default false) append debug flag @@ -51,8 +51,12 @@ class Minify_HTML_Helper { return htmlspecialchars($uri, ENT_QUOTES, $opts['charset']); } - /* + /** * Get non-HTML-escaped URI to minify the specified files + * + * @param bool $farExpires + * @param bool $debug + * @return string */ public function getRawUri($farExpires = true, $debug = false) { @@ -74,6 +78,12 @@ class Minify_HTML_Helper { return $path; } + /** + * Set the files that will comprise the URI we're building + * + * @param array $files + * @param bool $checkLastModified + */ public function setFiles($files, $checkLastModified = true) { $this->_groupKey = null; @@ -94,6 +104,12 @@ class Minify_HTML_Helper { $this->_filePaths = $files; } + /** + * Set the group of files that will comprise the URI we're building + * + * @param string $key + * @param bool $checkLastModified + */ public function setGroup($key, $checkLastModified = true) { $this->_groupKey = $key; @@ -103,13 +119,23 @@ class Minify_HTML_Helper { } if (is_file($this->groupsConfigFile)) { $gc = (require $this->groupsConfigFile); - if (isset($gc[$key])) { - $this->_lastModified = self::getLastModified($gc[$key]); + $keys = explode(',', $key); + foreach ($keys as $key) { + if (isset($gc[$key])) { + $this->_lastModified = self::getLastModified($gc[$key], $this->_lastModified); + } } } } } - + + /** + * Get the max(lastModified) of all files + * + * @param array|string $sources + * @param int $lastModified + * @return int + */ public static function getLastModified($sources, $lastModified = 0) { $max = $lastModified; @@ -142,13 +168,19 @@ class Minify_HTML_Helper { * @return mixed a common char or '' if any do not match */ protected static function _getCommonCharAtPos($arr, $pos) { - $l = count($arr); + if (!isset($arr[0][$pos])) { + return ''; + } $c = $arr[0][$pos]; - if ($c === '' || $l === 1) + $l = count($arr); + if ($l === 1) { return $c; - for ($i = 1; $i < $l; ++$i) - if ($arr[$i][$pos] !== $c) + } + for ($i = 1; $i < $l; ++$i) { + if ($arr[$i][$pos] !== $c) { return ''; + } + } return $c; } @@ -157,11 +189,11 @@ class Minify_HTML_Helper { * * @param array $paths root-relative URIs of files * @param string $minRoot root-relative URI of the "min" application + * @return string */ protected static function _getShortestUri($paths, $minRoot = '/min/') { $pos = 0; $base = ''; - $c; while (true) { $c = self::_getCommonCharAtPos($paths, $pos); if ($c === '') { diff --git a/inc/lib/minify/Minify/JS/ClosureCompiler.php b/inc/lib/minify/Minify/JS/ClosureCompiler.php index f4ed5bdb..e067d7c8 100755 --- a/inc/lib/minify/Minify/JS/ClosureCompiler.php +++ b/inc/lib/minify/Minify/JS/ClosureCompiler.php @@ -14,13 +14,68 @@ * @todo can use a stream wrapper to unit test this? */ class Minify_JS_ClosureCompiler { - const URL = 'http://closure-compiler.appspot.com/compile'; /** - * Minify Javascript code via HTTP request to the Closure Compiler API + * @var string The option key for the maximum POST byte size + */ + const OPTION_MAX_BYTES = 'maxBytes'; + + /** + * @var string The option key for additional params. @see __construct + */ + const OPTION_ADDITIONAL_OPTIONS = 'additionalParams'; + + /** + * @var string The option key for the fallback Minifier + */ + const OPTION_FALLBACK_FUNCTION = 'fallbackFunc'; + + /** + * @var string The option key for the service URL + */ + const OPTION_COMPILER_URL = 'compilerUrl'; + + /** + * @var int The default maximum POST byte size according to https://developers.google.com/closure/compiler/docs/api-ref + */ + const DEFAULT_MAX_BYTES = 200000; + + /** + * @var string[] $DEFAULT_OPTIONS The default options to pass to the compiler service + * + * @note This would be a constant if PHP allowed it + */ + private static $DEFAULT_OPTIONS = array( + 'output_format' => 'text', + 'compilation_level' => 'SIMPLE_OPTIMIZATIONS' + ); + + /** + * @var string $url URL of compiler server. defaults to Google's + */ + protected $serviceUrl = 'http://closure-compiler.appspot.com/compile'; + + /** + * @var int $maxBytes The maximum JS size that can be sent to the compiler server in bytes + */ + protected $maxBytes = self::DEFAULT_MAX_BYTES; + + /** + * @var string[] $additionalOptions Additional options to pass to the compiler service + */ + protected $additionalOptions = array(); + + /** + * @var callable Function to minify JS if service fails. Default is JSMin + */ + protected $fallbackMinifier = array('JSMin', 'minify'); + + /** + * Minify JavaScript code via HTTP request to a Closure Compiler API * * @param string $js input code - * @param array $options unused at this point + * @param array $options Options passed to __construct(). @see __construct + * * @return string */ public static function minify($js, array $options = array()) @@ -30,63 +85,101 @@ class Minify_JS_ClosureCompiler { } /** + * @param array $options Options with keys available below: + * + * fallbackFunc : (callable) function to minify if service unavailable. Default is JSMin. * - * @param array $options + * compilerUrl : (string) URL to closure compiler server * - * fallbackFunc : default array($this, 'fallback'); + * maxBytes : (int) The maximum amount of bytes to be sent as js_code in the POST request. + * Defaults to 200000. + * + * additionalParams : (string[]) Additional parameters to pass to the compiler server. Can be anything named + * in https://developers.google.com/closure/compiler/docs/api-ref except for js_code and + * output_info */ public function __construct(array $options = array()) { - $this->_fallbackFunc = isset($options['fallbackMinifier']) - ? $options['fallbackMinifier'] - : array($this, '_fallback'); + if (isset($options[self::OPTION_FALLBACK_FUNCTION])) { + $this->fallbackMinifier = $options[self::OPTION_FALLBACK_FUNCTION]; + } + if (isset($options[self::OPTION_COMPILER_URL])) { + $this->serviceUrl = $options[self::OPTION_COMPILER_URL]; + } + if (isset($options[self::OPTION_ADDITIONAL_OPTIONS]) && is_array($options[self::OPTION_ADDITIONAL_OPTIONS])) { + $this->additionalOptions = $options[self::OPTION_ADDITIONAL_OPTIONS]; + } + if (isset($options[self::OPTION_MAX_BYTES])) { + $this->maxBytes = (int) $options[self::OPTION_MAX_BYTES]; + } } + /** + * Call the service to perform the minification + * + * @param string $js JavaScript code + * @return string + * @throws Minify_JS_ClosureCompiler_Exception + */ public function min($js) { - $postBody = $this->_buildPostBody($js); - $bytes = (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) - ? mb_strlen($postBody, '8bit') - : strlen($postBody); - if ($bytes > 200000) { - throw new Minify_JS_ClosureCompiler_Exception( - 'POST content larger than 200000 bytes' - ); + $postBody = $this->buildPostBody($js); + + if ($this->maxBytes > 0) { + $bytes = (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($postBody, '8bit') + : strlen($postBody); + if ($bytes > $this->maxBytes) { + throw new Minify_JS_ClosureCompiler_Exception( + 'POST content larger than ' . $this->maxBytes . ' bytes' + ); + } } - $response = $this->_getResponse($postBody); + + $response = $this->getResponse($postBody); + if (preg_match('/^Error\(\d\d?\):/', $response)) { - if (is_callable($this->_fallbackFunc)) { + if (is_callable($this->fallbackMinifier)) { + // use fallback $response = "/* Received errors from Closure Compiler API:\n$response" . "\n(Using fallback minifier)\n*/\n"; - $response .= call_user_func($this->_fallbackFunc, $js); + $response .= call_user_func($this->fallbackMinifier, $js); } else { throw new Minify_JS_ClosureCompiler_Exception($response); } } + if ($response === '') { - $errors = $this->_getResponse($this->_buildPostBody($js, true)); + $errors = $this->getResponse($this->buildPostBody($js, true)); throw new Minify_JS_ClosureCompiler_Exception($errors); } + return $response; } - - protected $_fallbackFunc = null; - protected function _getResponse($postBody) + /** + * Get the response for a given POST body + * + * @param string $postBody + * @return string + * @throws Minify_JS_ClosureCompiler_Exception + */ + protected function getResponse($postBody) { $allowUrlFopen = preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen')); + if ($allowUrlFopen) { - $contents = file_get_contents(self::URL, false, stream_context_create(array( + $contents = file_get_contents($this->serviceUrl, false, stream_context_create(array( 'http' => array( 'method' => 'POST', - 'header' => 'Content-type: application/x-www-form-urlencoded', + 'header' => "Content-type: application/x-www-form-urlencoded\r\nConnection: close\r\n", 'content' => $postBody, 'max_redirects' => 0, 'timeout' => 15, ) ))); } elseif (defined('CURLOPT_POST')) { - $ch = curl_init(self::URL); + $ch = curl_init($this->serviceUrl); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/x-www-form-urlencoded')); @@ -100,33 +193,37 @@ class Minify_JS_ClosureCompiler { "Could not make HTTP request: allow_url_open is false and cURL not available" ); } + if (false === $contents) { throw new Minify_JS_ClosureCompiler_Exception( "No HTTP response from server" ); } - return trim($contents); - } - protected function _buildPostBody($js, $returnErrors = false) - { - return http_build_query(array( - 'js_code' => $js, - 'output_info' => ($returnErrors ? 'errors' : 'compiled_code'), - 'output_format' => 'text', - 'compilation_level' => 'SIMPLE_OPTIMIZATIONS' - ), null, '&'); + return trim($contents); } /** - * Default fallback function if CC API fails - * @param string $js + * Build a POST request body + * + * @param string $js JavaScript code + * @param bool $returnErrors * @return string */ - protected function _fallback($js) + protected function buildPostBody($js, $returnErrors = false) { - require_once 'JSMin.php'; - return JSMin::minify($js); + return http_build_query( + array_merge( + self::$DEFAULT_OPTIONS, + $this->additionalOptions, + array( + 'js_code' => $js, + 'output_info' => ($returnErrors ? 'errors' : 'compiled_code') + ) + ), + null, + '&' + ); } } diff --git a/inc/lib/minify/Minify/Lines.php b/inc/lib/minify/Minify/Lines.php index ca8afa1b..e999a6c2 100755 --- a/inc/lib/minify/Minify/Lines.php +++ b/inc/lib/minify/Minify/Lines.php @@ -55,7 +55,11 @@ class Minify_Lines { $newLines = array(); while (null !== ($line = array_shift($lines))) { if (('' !== $id) && (0 == $i % 50)) { - array_push($newLines, '', "/* {$id} */", ''); + if ($inComment) { + array_push($newLines, '', "/* {$id} *|", ''); + } else { + array_push($newLines, '', "/* {$id} */", ''); + } } ++$i; $newLines[] = self::_addNote($line, $i, $inComment, $padTo); @@ -65,7 +69,6 @@ class Minify_Lines { // check for desired URI rewriting if (isset($options['currentDir'])) { - require_once 'Minify/CSS/UriRewriter.php'; Minify_CSS_UriRewriter::$debugText = ''; $content = Minify_CSS_UriRewriter::rewrite( $content @@ -93,6 +96,9 @@ class Minify_Lines { */ private static function _eolInComment($line, $inComment) { + // crude way to avoid things like // */ + $line = preg_replace('~//.*?(\\*/|/\\*).*~', '', $line); + while (strlen($line)) { $search = $inComment ? '*/' diff --git a/inc/lib/minify/Minify/YUICompressor.php b/inc/lib/minify/Minify/YUICompressor.php index c5bd8a1e..5762e890 100755 --- a/inc/lib/minify/Minify/YUICompressor.php +++ b/inc/lib/minify/Minify/YUICompressor.php @@ -13,14 +13,17 @@ * Java environment. * * <code> - * Minify_YUICompressor::$jarFile = '/path/to/yuicompressor-2.3.5.jar'; + * Minify_YUICompressor::$jarFile = '/path/to/yuicompressor-2.4.6.jar'; * Minify_YUICompressor::$tempDir = '/tmp'; * $code = Minify_YUICompressor::minifyJs( * $code * ,array('nomunge' => true, 'line-break' => 1000) * ); * </code> - * + * + * Note: In case you run out stack (default is 512k), you may increase stack size in $options: + * array('stack-size' => '2048k') + * * @todo unit tests, $options docs * * @package Minify @@ -87,7 +90,7 @@ class Minify_YUICompressor { { self::_prepare(); if (! ($tmpFile = tempnam(self::$tempDir, 'yuic_'))) { - throw new Exception('Minify_YUICompressor : could not create temp file.'); + throw new Exception('Minify_YUICompressor : could not create temp file in "'.self::$tempDir.'".'); } file_put_contents($tmpFile, $content); exec(self::_getCmd($options, $type, $tmpFile), $output, $result_code); @@ -108,10 +111,15 @@ class Minify_YUICompressor { ,'nomunge' => false ,'preserve-semi' => false ,'disable-optimizations' => false + ,'stack-size' => '' ) ,$userOptions ); - $cmd = self::$javaExecutable . ' -jar ' . escapeshellarg(self::$jarFile) + $cmd = self::$javaExecutable + . (!empty($o['stack-size']) + ? ' -Xss' . $o['stack-size'] + : '') + . ' -jar ' . escapeshellarg(self::$jarFile) . " --type {$type}" . (preg_match('/^[\\da-zA-Z0-9\\-]+$/', $o['charset']) ? " --charset {$o['charset']}" @@ -134,8 +142,8 @@ class Minify_YUICompressor { if (! is_file(self::$jarFile)) { throw new Exception('Minify_YUICompressor : $jarFile('.self::$jarFile.') is not a valid file.'); } - if (! is_executable(self::$jarFile)) { - throw new Exception('Minify_YUICompressor : $jarFile('.self::$jarFile.') is not executable.'); + if (! is_readable(self::$jarFile)) { + throw new Exception('Minify_YUICompressor : $jarFile('.self::$jarFile.') is not readable.'); } if (! is_dir(self::$tempDir)) { throw new Exception('Minify_YUICompressor : $tempDir('.self::$tempDir.') is not a valid direcotry.'); diff --git a/inc/lib/minify/MrClay/Cli.php b/inc/lib/minify/MrClay/Cli.php index ac201582..9aa8e06f 100755 --- a/inc/lib/minify/MrClay/Cli.php +++ b/inc/lib/minify/MrClay/Cli.php @@ -2,6 +2,9 @@ namespace MrClay; +use MrClay\Cli\Arg; +use InvalidArgumentException; + /** * Forms a front controller for a console app, handling and validating arguments (options) * @@ -51,7 +54,7 @@ class Cli { public $isHelpRequest = false; /** - * @var array of Cli\Arg + * @var Arg[] */ protected $_args = array(); @@ -80,8 +83,8 @@ class Cli { } /** - * @param Cli\Arg|string $letter - * @return Cli\Arg + * @param Arg|string $letter + * @return Arg */ public function addOptionalArg($letter) { @@ -89,8 +92,8 @@ class Cli { } /** - * @param Cli\Arg|string $letter - * @return Cli\Arg + * @param Arg|string $letter + * @return Arg */ public function addRequiredArg($letter) { @@ -100,17 +103,17 @@ class Cli { /** * @param string $letter * @param bool $required - * @param Cli\Arg|null $arg - * @return Cli\Arg - * @throws \InvalidArgumentException + * @param Arg|null $arg + * @return Arg + * @throws InvalidArgumentException */ - public function addArgument($letter, $required, Cli\Arg $arg = null) + public function addArgument($letter, $required, Arg $arg = null) { if (! preg_match('/^[a-zA-Z]$/', $letter)) { - throw new \InvalidArgumentException('$letter must be in [a-zA-z]'); + throw new InvalidArgumentException('$letter must be in [a-zA-Z]'); } if (! $arg) { - $arg = new Cli\Arg($required); + $arg = new Arg($required); } $this->_args[$letter] = $arg; return $arg; @@ -118,7 +121,7 @@ class Cli { /** * @param string $letter - * @return Cli\Arg|null + * @return Arg|null */ public function getArgument($letter) { @@ -143,7 +146,7 @@ class Cli { $lettersUsed = ''; foreach ($this->_args as $letter => $arg) { - /* @var Cli\Arg $arg */ + /* @var Arg $arg */ $options .= $letter; $lettersUsed .= $letter; @@ -159,7 +162,7 @@ class Cli { $this->debug['getopt_return'] = $o; foreach ($this->_args as $letter => $arg) { - /* @var Cli\Arg $arg */ + /* @var Arg $arg */ $this->values[$letter] = false; if (isset($o[$letter])) { if (is_bool($o[$letter])) { @@ -295,7 +298,7 @@ class Cli { { $r = "\n"; foreach ($this->_args as $letter => $arg) { - /* @var Cli\Arg $arg */ + /* @var Arg $arg */ $desc = $arg->getDescription(); $flag = " -$letter "; if ($arg->mayHaveValue) { diff --git a/inc/lib/minify/MrClay/Cli/Arg.php b/inc/lib/minify/MrClay/Cli/Arg.php index 81146a7f..5fa59327 100755 --- a/inc/lib/minify/MrClay/Cli/Arg.php +++ b/inc/lib/minify/MrClay/Cli/Arg.php @@ -2,6 +2,8 @@ namespace MrClay\Cli; +use BadMethodCallException; + /** * An argument for a CLI app. This specifies the argument, what values it expects and * how it's treated during validation. @@ -150,7 +152,7 @@ class Arg { * @param string $name * @param array $args * @return Arg - * @throws \BadMethodCallException + * @throws BadMethodCallException */ public function __call($name, array $args = array()) { @@ -160,7 +162,7 @@ class Arg { $this->spec['mustHaveValue'] = true; } } else { - throw new \BadMethodCallException('Method does not exist'); + throw new BadMethodCallException('Method does not exist'); } return $this; } diff --git a/inc/lib/webm/posthandler.php b/inc/lib/webm/posthandler.php index 50adbe80..1f831f5a 100644 --- a/inc/lib/webm/posthandler.php +++ b/inc/lib/webm/posthandler.php @@ -5,43 +5,43 @@ function postHandler($post) { global $board, $config; - if ($post->has_file && $post->extension == 'webm') { + if ($post->has_file) foreach ($post->files as &$file) if ($file->extension == 'webm') { require_once dirname(__FILE__) . '/videodata.php'; - $videoDetails = videoData($post->file_path); + $videoDetails = videoData($file->file_path); if (!isset($videoDetails['container']) || $videoDetails['container'] != 'webm') return "not a WebM file"; // Set thumbnail - $thumbName = $board['dir'] . $config['dir']['thumb'] . $post->file_id . '.webm'; + $thumbName = $board['dir'] . $config['dir']['thumb'] . $file->file_id . '.webm'; if ($config['spoiler_images'] && isset($_POST['spoiler'])) { // Use spoiler thumbnail - $post->thumb = 'spoiler'; + $file->thumb = 'spoiler'; $size = @getimagesize($config['spoiler_image']); - $post->thumbwidth = $size[0]; - $post->thumbheight = $size[1]; + $file->thumbwidth = $size[0]; + $file->thumbheight = $size[1]; } elseif (isset($videoDetails['frame']) && $thumbFile = fopen($thumbName, 'wb')) { // Use single frame from video as pseudo-thumbnail fwrite($thumbFile, $videoDetails['frame']); fclose($thumbFile); - $post->thumb = $post->file_id . '.webm'; + $file->thumb = $file->file_id . '.webm'; } else { // Fall back to file thumbnail - $post->thumb = 'file'; + $file->thumb = 'file'; } unset($videoDetails['frame']); // Set width and height if (isset($videoDetails['width']) && isset($videoDetails['height'])) { - $post->width = $videoDetails['width']; - $post->height = $videoDetails['height']; - if ($post->thumb != 'file' && $post->thumb != 'spoiler') { + $file->width = $videoDetails['width']; + $file->height = $videoDetails['height']; + if ($file->thumb != 'file' && $file->thumb != 'spoiler') { $thumbMaxWidth = $post->op ? $config['thumb_op_width'] : $config['thumb_width']; $thumbMaxHeight = $post->op ? $config['thumb_op_height'] : $config['thumb_height']; if ($videoDetails['width'] > $thumbMaxWidth || $videoDetails['height'] > $thumbMaxHeight) { - $post->thumbwidth = min($thumbMaxWidth, intval(round($videoDetails['width'] * $thumbMaxHeight / $videoDetails['height']))); - $post->thumbheight = min($thumbMaxHeight, intval(round($videoDetails['height'] * $thumbMaxWidth / $videoDetails['width']))); + $file->thumbwidth = min($thumbMaxWidth, intval(round($videoDetails['width'] * $thumbMaxHeight / $videoDetails['height']))); + $file->thumbheight = min($thumbMaxHeight, intval(round($videoDetails['height'] * $thumbMaxWidth / $videoDetails['width']))); } else { - $post->thumbwidth = $videoDetails['width']; - $post->thumbheight = $videoDetails['height']; + $file->thumbwidth = $videoDetails['width']; + $file->thumbheight = $videoDetails['height']; } } } diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 05a24865..9b9d3127 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -962,17 +962,22 @@ function mod_ban_appeals() { if ($ban['post'] && isset($ban['post']['board'], $ban['post']['id'])) { if (openBoard($ban['post']['board'])) { - $query = query(sprintf("SELECT `thumb`, `file` FROM ``posts_%s`` WHERE `id` = " . + $query = query(sprintf("SELECT `num_files`, `files` FROM ``posts_%s`` WHERE `id` = " . (int)$ban['post']['id'], $board['uri'])); if ($_post = $query->fetch(PDO::FETCH_ASSOC)) { + $_post['files'] = $_post['files'] ? json_decode($_post['files']) : array(); $ban['post'] = array_merge($ban['post'], $_post); } else { - $ban['post']['file'] = 'deleted'; - $ban['post']['thumb'] = false; + $ban['post']['files'] = array(array()); + $ban['post']['files'][0]['file'] = 'deleted'; + $ban['post']['files'][0]['thumb'] = false; + $ban['post']['num_files'] = 1; } } else { - $ban['post']['file'] = 'deleted'; - $ban['post']['thumb'] = false; + $ban['post']['files'] = array(array()); + $ban['post']['files'][0]['file'] = 'deleted'; + $ban['post']['files'][0]['thumb'] = false; + $ban['post']['num_files'] = 1; } if ($ban['post']['thread']) { @@ -1096,13 +1101,13 @@ function mod_move_reply($originBoard, $postID) { $post['op'] = true; } - if ($post['file']) { + if ($post['files']) { + $post['files'] = json_decode($post['files'], TRUE); $post['has_file'] = true; - $post['width'] = &$post['filewidth']; - $post['height'] = &$post['fileheight']; - - $file_src = sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $post['file']; - $file_thumb = sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $post['thumb']; + foreach ($post['files'] as $i => &$file) { + $file['file_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file']; + $file['thumb_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb']; + } } else { $post['has_file'] = false; } @@ -1117,10 +1122,12 @@ function mod_move_reply($originBoard, $postID) { $newID = post($post); if ($post['has_file']) { - // move the image - rename($file_src, sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $post['file']); - if ($post['thumb'] != 'spoiler') { //trying to move/copy the spoiler thumb raises an error - rename($file_thumb, sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $post['thumb']); + foreach ($post['files'] as $i => &$file) { + // move the image + rename($file['file_path'], sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file']); + if ($file['thumb'] != 'spoiler') { //trying to move/copy the spoiler thumb raises an error + rename($file['thumb_path'], sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb']); + } } } @@ -1193,13 +1200,15 @@ function mod_move($originBoard, $postID) { // indicate that the post is a thread $post['op'] = true; - if ($post['file']) { + if ($post['files']) { + $post['files'] = json_decode($post['files'], TRUE); $post['has_file'] = true; - $post['width'] = &$post['filewidth']; - $post['height'] = &$post['fileheight']; - - $file_src = sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $post['file']; - $file_thumb = sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $post['thumb']; + foreach ($post['files'] as $i => &$file) { + if ($file['file'] === 'deleted') + continue; + $file['file_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file']; + $file['thumb_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb']; + } } else { $post['has_file'] = false; } @@ -1215,9 +1224,12 @@ function mod_move($originBoard, $postID) { if ($post['has_file']) { // copy image - $clone($file_src, sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $post['file']); - if (!in_array($post['thumb'], array('spoiler', 'deleted', 'file'))) - $clone($file_thumb, sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $post['thumb']); + foreach ($post['files'] as $i => &$file) { + if ($file['file'] !== 'deleted') + $clone($file['file_path'], sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file']); + if (isset($file['thumb']) && !in_array($file['thumb'], array('spoiler', 'deleted', 'file'))) + $clone($file['thumb_path'], sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb']); + } } // go back to the original board to fetch replies @@ -1233,13 +1245,13 @@ function mod_move($originBoard, $postID) { $post['mod'] = true; $post['thread'] = $newID; - if ($post['file']) { + if ($post['files']) { + $post['files'] = json_decode($post['files'], TRUE); $post['has_file'] = true; - $post['width'] = &$post['filewidth']; - $post['height'] = &$post['fileheight']; - - $post['file_src'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $post['file']; - $post['file_thumb'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $post['thumb']; + foreach ($post['files'] as $i => &$file) { + $file['file_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file']; + $file['thumb_path'] = sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb']; + } } else { $post['has_file'] = false; } @@ -1274,14 +1286,16 @@ function mod_move($originBoard, $postID) { $post['op'] = false; $post['tracked_cites'] = markup($post['body'], true); - // insert reply - $newIDs[$post['id']] = $newPostID = post($post); - if ($post['has_file']) { // copy image - $clone($post['file_src'], sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $post['file']); - $clone($post['file_thumb'], sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $post['thumb']); + foreach ($post['files'] as $i => &$file) { + $clone($file['file_path'], sprintf($config['board_path'], $board['uri']) . $config['dir']['img'] . $file['file']); + $clone($file['thumb_path'], sprintf($config['board_path'], $board['uri']) . $config['dir']['thumb'] . $file['thumb']); + } } + // insert reply + $newIDs[$post['id']] = $newPostID = post($post); + if (!empty($post['tracked_cites'])) { $insert_rows = array(); @@ -1513,7 +1527,7 @@ function mod_delete($board, $post) { header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']); } -function mod_deletefile($board, $post) { +function mod_deletefile($board, $post, $file) { global $config, $mod; if (!openBoard($board)) @@ -1523,7 +1537,7 @@ function mod_deletefile($board, $post) { error($config['error']['noaccess']); // Delete file - deleteFile($post); + deleteFile($post, TRUE, $file); // Record the action modLog("Deleted file from post #{$post}"); @@ -1536,28 +1550,30 @@ function mod_deletefile($board, $post) { header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']); } -function mod_spoiler_image($board, $post) { +function mod_spoiler_image($board, $post, $file) { global $config, $mod; - + if (!openBoard($board)) error($config['error']['noboard']); - + if (!hasPermission($config['mod']['spoilerimage'], $board)) error($config['error']['noaccess']); - // Delete file - $query = prepare(sprintf("SELECT `thumb`, `thread` FROM ``posts_%s`` WHERE id = :id", $board)); + // Delete file thumbnail + $query = prepare(sprintf("SELECT `files`, `thread` FROM ``posts_%s`` WHERE id = :id", $board)); $query->bindValue(':id', $post, PDO::PARAM_INT); $query->execute() or error(db_error($query)); $result = $query->fetch(PDO::FETCH_ASSOC); + $files = json_decode($result['files']); - file_unlink($board . '/' . $config['dir']['thumb'] . $result['thumb']); - + file_unlink($board . '/' . $config['dir']['thumb'] . $files[$file]->thumb); + $files[$file]->thumb = 'spoiler'; + $files[$file]->thumbheight = 128; + $files[$file]->thumbwidth = 128; + // Make thumbnail spoiler - $query = prepare(sprintf("UPDATE ``posts_%s`` SET `thumb` = :thumb, `thumbwidth` = :thumbwidth, `thumbheight` = :thumbheight WHERE `id` = :id", $board)); - $query->bindValue(':thumb', "spoiler"); - $query->bindValue(':thumbwidth', 128, PDO::PARAM_INT); - $query->bindValue(':thumbheight', 128, PDO::PARAM_INT); + $query = prepare(sprintf("UPDATE ``posts_%s`` SET `files` = :files WHERE `id` = :id", $board)); + $query->bindValue(':files', json_encode($files)); $query->bindValue(':id', $post, PDO::PARAM_INT); $query->execute() or error(db_error($query)); @@ -1572,7 +1588,7 @@ function mod_spoiler_image($board, $post) { // Rebuild themes rebuildThemes('post-delete', $board); - + // Redirect header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']); } @@ -2213,6 +2229,68 @@ function mod_report_dismiss($id, $all = false) { header('Location: ?/reports', true, $config['redirect_http']); } +function mod_recent_posts($lim) { + global $config, $mod, $pdo; + + if (!hasPermission($config['mod']['recent'])) + error($config['error']['noaccess']); + + $limit = (is_numeric($lim))? $lim : 25; + + $mod_boards = array(); + $boards = listBoards(); + + //if not all boards + if ($mod['boards'][0]!='*') { + foreach ($boards as $board) { + if (in_array($board['uri'], $mod['boards'])) + $mod_boards[] = $board; + } + } else { + $mod_boards = $boards; + } + + // Manually build an SQL query + $query = 'SELECT * FROM ('; + foreach ($mod_boards as $board) { + $query .= sprintf('SELECT *, %s AS `board` FROM ``posts_%s`` UNION ALL ', $pdo->quote($board['uri']), $board['uri']); + } + // Remove the last "UNION ALL" seperator and complete the query + $query = preg_replace('/UNION ALL $/', ') AS `all_posts` ORDER BY `time` DESC LIMIT ' . $limit, $query); + $query = query($query) or error(db_error()); + $posts = $query->fetchAll(PDO::FETCH_ASSOC); + + $body = '<h4>Viewing last '.$limit.' posts</h4> + <p>View <a href="?/recent/25"> 25 </a>|<a href="?/recent/50"> 50 </a>|<a href="?/recent/100"> 100 </a></p> + <a href="javascript:void(0)" id="erase-local-data" style="float:right; clear:both">Erase local data</a></div>'; + foreach ($posts as $post) { + openBoard($post['board']); + if (!$post['thread']) { + // Still need to fix this: + $po = new Thread($post, '?/', $mod, false); + $string = $po->build(true); + $string = '<div class="post-wrapper" data-board="'.$post['board'].'"><hr/><a class="eita-link" id="eita-'.$post['board'].'-'.$post['id'].'" href="?/'.$post['board'].'/res/'.$post['id'].'.html#'.$post['id'].'">/'.$post['board'].'/'.$post['id'].'</a><br>' . $string; + } else { + $po = new Post($post, '?/', $mod); + $string = $po->build(true); + $string = '<div class="post-wrapper" data-board="'.$post['board'].'"><hr/><a class="eita-link" id="eita-'.$post['board'].'-'.$post['id'].'" href="?/'.$post['board'].'/res/'.$post['thread'].'.html#'.$post['id'].'">/'.$post['board'].'/'.$post['id'].'</a><br>' . $string; + } + $body .= $string . '</div>'; + } + + echo Element('page.html', array( + 'config' => $config, + 'mod' => $mod, + 'hide_dashboard_link' => true, + 'title' => _('Recent posts'), + 'subtitle' => '', + 'nojavascript' => false, + 'is_recent_posts' => true, + 'body' => $body + ) + ); + +} function mod_config($board_config = false) { global $config, $mod, $board; |
