Copyright (C) Rodolfo Berrios A. All rights reserved. BY USING THIS SOFTWARE YOU DECLARE TO ACCEPT THE CHEVERETO EULA http://chevereto.com/license --------------------------------------------------------------------- */ namespace CHV; use G, Exception; class Image { static $table_chv_image = [ 'name', 'extension', 'album_id', 'size', 'width', 'height', 'date', 'date_gmt', 'nsfw', 'user_id', 'uploader_ip', 'storage_mode', 'storage_id', 'md5', 'original_filename', 'original_exifdata', 'category_id', 'description', 'chain', 'thumb_size', 'medium_size', 'title', 'expiration_date_gmt', 'likes', 'is_animated' ]; static $chain_sizes = ['original', 'image', 'medium', 'thumb']; public static function getSingle($id, $sumview=FALSE, $pretty=FALSE, $requester=NULL) { $tables = DB::getTables(); $query = 'SELECT * FROM '.$tables['images']."\n"; $joins = [ 'LEFT JOIN '.$tables['storages'].' ON '.$tables['images'].'.image_storage_id = '.$tables['storages'].'.storage_id', 'LEFT JOIN '.$tables['storage_apis'].' ON '.$tables['storages'].'.storage_api_id = '.$tables['storage_apis'].'.storage_api_id', 'LEFT JOIN '.$tables['users'].' ON '.$tables['images'].'.image_user_id = '.$tables['users'].'.user_id', 'LEFT JOIN '.$tables['albums'].' ON '.$tables['images'].'.image_album_id = '.$tables['albums'].'.album_id' ]; $query .= implode("\n", $joins) . "\n"; $query .= 'WHERE image_id=:image_id;'."\n"; if($sumview) { $query .= 'UPDATE '.$tables['images'].' SET image_views = image_views + 1 WHERE image_id=:image_id'; } try { $db = DB::getInstance(); $db->query($query); $db->bind(':image_id', $id); $image_db = $db->fetchSingle(); if($image_db) { if($sumview) { $image_db['image_views'] += 1; // Track stats Stat::track([ 'action' => 'update', 'table' => 'images', 'value' => '+1', 'date_gmt' => $image_db['image_date_gmt'], 'user_id' => $image_db['image_user_id'], ]); } if($requester) { $image_db['image_liked'] = $image_db['like_user_id'] ? TRUE : FALSE; } $return = $image_db; $return = $pretty ? self::formatArray($return) : $return; if(!$return['file_resource']) { $return['file_resource'] = self::getSrcTargetSingle($image_db, true); } return $return; } else { return $image_db; } } catch(Exception $e) { throw new ImageException($e->getMessage(), 400); } } public static function getMultiple($ids, $pretty=FALSE) { if(!is_array($ids)) { $ids = func_get_args(); $aux = array(); foreach($ids as $k => $v) { $aux[] = $v; } $ids = $aux; } if(count($ids) == 0) { throw new ImageException('Null ids provided in Image::get_multiple', 100); } $tables = DB::getTables(); $query = 'SELECT * FROM '.$tables['images']."\n"; $joins = array( 'LEFT JOIN '.$tables['users'].' ON '.$tables['images'].'.image_user_id = '.$tables['users'].'.user_id', 'LEFT JOIN '.$tables['albums'].' ON '.$tables['images'].'.image_album_id = '.$tables['albums'].'.album_id' ); $query .= implode("\n", $joins) . "\n"; $query .= 'WHERE image_id IN ('. join(',', $ids). ')' . "\n"; try { $db = DB::getInstance(); $db->query($query); $images_db = $db->fetchAll(); if($images_db) { foreach($images_db as $k => $v) { $images_db[$k] = array_merge($v, self::getSrcTargetSingle($v, true)); // todo } } if($pretty) { $return = []; foreach($images_db as $k => $v) { $return[] = self::formatArray($v); } return $return; } return $images_db; } catch(Exception $e) { throw new ImageException($e->getMessage(), 400); } } public static function getAlbumSlice($image_id, $album_id=NULL, $padding=2) { $tables = DB::getTables(); if($image_id == NULL) { throw new ImageException("Image id can't be NULL", 100); } if($album_id == NULL) { try { $db = DB::getInstance(); $db->query('SELECT image_album_id FROM '.$tables['images'].' WHERE image_id=:image_id'); $db->bind(':image_id', $image_id); $image_album_db = $db->fetchSingle(); $album_id = $image_album_db['image_album_id']; } catch(Excepton $e) { throw new ImageException($e->getMessage(), 400); } if($album_id == NULL) { return; } } if(!is_numeric($padding)) { $padding = 2; } //$where_album = $album_id !== NULL ? "image_album_id=:image_album_id" : "image_album_id IS NULL"; try { $db = DB::getInstance(); $db->query('SELECT * FROM ( (SELECT * FROM '.$tables['images'].' LEFT JOIN '.$tables['storages'].' ON '.$tables['images'].'.image_storage_id = '.$tables['storages'].'.storage_id WHERE image_album_id=:image_album_id AND image_id <= :image_id ORDER BY image_id DESC LIMIT 0,'.($padding*2 + 1).') UNION (SELECT * FROM '.$tables['images'].' LEFT JOIN '.$tables['storages'].' ON '.$tables['images'].'.image_storage_id = '.$tables['storages'].'.storage_id WHERE image_album_id=:image_album_id AND image_id > :image_id ORDER BY image_id ASC LIMIT 0,'.($padding*2).') ) images ORDER BY images.image_id ASC'); $db->bind(':image_album_id', $album_id); $db->bind(':image_id', $image_id); $image_album_slice_db = $db->fetchAll(); $album_offset = array('top' => 0, 'bottom' => 0); foreach($image_album_slice_db as $v) { if($image_id > $v['image_id']) { $album_offset['top']++; } if($image_id < $v['image_id']) { $album_offset['bottom']++; } } $album_chop_count = count($image_album_slice_db); $album_iteration_times = $album_chop_count - ($padding*2 + 1); if($album_chop_count > ($padding*2 + 1)) { if($album_offset['top'] > $padding && $album_offset['bottom'] > $padding) { // Cut on top for($i=0; $i<$album_offset['top']-$padding; $i++) { unset($image_album_slice_db[$i]); } // Cut on bottom for($i=1; $i<=$album_offset['bottom']-$padding; $i++) { unset($image_album_slice_db[$album_chop_count - $i]); } } else if($album_offset['top'] <= $padding) { // Cut bottom for($i=0; $i<$album_iteration_times; $i++) { unset($image_album_slice_db[$album_chop_count - 1 - $i]); } } else if($album_offset['bottom'] <= $padding) { // Cut top for($i=0; $i<$album_iteration_times; $i++) { unset($image_album_slice_db[$i]); } } // Some cleaning after the unsets $image_album_slice_db = array_values($image_album_slice_db); } $album_cursor = ''; foreach($image_album_slice_db as $k => $v) { if($v['image_id'] == $image_id) { $album_cursor = $k; break; } } $image_album_slice['images'] = array(); foreach($image_album_slice_db as $k => $v) { $image_album_slice['images'][$k] = self::formatArray($v); } if($image_album_slice['images'][$album_cursor-1]) { $image_album_slice['prev'] = $image_album_slice['images'][$album_cursor-1]; } if($image_album_slice['images'][$album_cursor+1]) { $image_album_slice['next'] = $image_album_slice['images'][$album_cursor+1]; } return array( 'db' => $image_album_slice_db, 'formatted' => $image_album_slice ); } catch(Exception $e) { throw new ImageException($e->getMessage(), 400); } } public static function getSrcTargetSingle($filearray, $prefix=true) { $prefix = $prefix ? 'image_' : NULL; $folder = CHV_PATH_IMAGES; $pretty = !isset($filearray['image_id']); $chain_mask = str_split((string) str_pad(decbin($filearray[$pretty ? 'chain' : 'image_chain']), 4, '0', STR_PAD_LEFT)); $chain_to_sufix = [ //'original' => '.original.', 'image' => '.', 'thumb' => '.th.', 'medium' => '.md.' ]; if($pretty) { $type = $filearray['storage']['id'] ? 'url' : 'path'; } else { $type = $filearray['storage_id'] ? 'url' : 'path'; } if($type == 'url') { // URL resource folder $folder = G\add_ending_slash($pretty ? $filearray['storage']['url'] : $filearray['storage_url']); } switch($filearray[$prefix.'storage_mode']) { case 'datefolder': $datetime = $filearray[$prefix.'date']; $datefolder = preg_replace('/(.*)(\s.*)/', '$1', str_replace('-', '/', $datetime)); $folder .= G\add_ending_slash($datefolder); // Y/m/d/ break; case 'old': $folder .= 'old/'; break; case 'direct': // use direct $folder break; } $targets = [ 'type' => $type, 'chain' => [ //'original' => NULL, 'image' => NULL, 'thumb' => NULL, 'medium' => NULL ] ]; foreach($targets['chain'] as $k => $v) { $targets['chain'][$k] = $folder.$filearray[$prefix.'name'] . $chain_to_sufix[$k] . $filearray[$prefix.'extension']; } if($type == 'path') { foreach($targets['chain'] as $k => $v) { if(!file_exists($v)) { unset($targets['chain'][$k]); }; } } else { foreach($chain_mask as $k => $v) { if(!(bool) $v) { unset($targets['chain'][self::$chain_sizes[$k]]); } } } return $targets; } public static function getUrlViewer($id_encoded) { return G\get_base_url(getSetting('route_image') . '/' . $id_encoded); } public static function getAvailableExpirations() { $string = _s('After %n %t'); return [ NULL => _s("Don't autodelete"), 'PT5M' => strtr($string, ['%n' => 5, '%t' => _n('minute', 'minutes', 5)]), 'PT30M' => strtr($string, ['%n' => 30, '%t' => _n('minute', 'minutes', 30)]), 'PT1H' => strtr($string, ['%n' => 1, '%t' => _n('hour', 'hours', 1)]), 'PT2H' => strtr($string, ['%n' => 2, '%t' => _n('hour', 'hours', 2)]), 'PT6H' => strtr($string, ['%n' => 6, '%t' => _n('hour', 'hours', 6)]), 'PT12H' => strtr($string, ['%n' => 12, '%t' => _n('hour', 'hours', 12)]), 'P1D' => strtr($string, ['%n' => 1, '%t' => _n('day','days', 1)]), 'P2D' => strtr($string, ['%n' => 2, '%t' => _n('day','days', 2)]) ]; } public static function watermark($image_path, $options=[]) { // Watermark options $options = array_merge([ 'ratio' => getSetting('watermark_percentage') / 100, 'position' => explode(' ', getSetting('watermark_position')), 'file' => CHV_PATH_CONTENT_IMAGES_SYSTEM . getSetting('watermark_image') ], $options); if(!is_readable($options['file'])) { throw new Exception("Can't read watermark file", 100); } // Fail-safe ratio $options['ratio'] = min(1, (!is_numeric($options['ratio']) ? 0.01 : max(0.01, $options['ratio']))); // Fail-safe positioning if(!in_array($options['position'][0], ['left', 'center', 'right'])) { $options['position'][0] = 'right'; } if(!in_array($options['position'][1], ['top', 'center', 'bottom'])) { $options['position'][0] = 'bottom'; } // Get source fileinfo $image_fileinfo = G\get_image_fileinfo($image_path); // Create working source image switch($image_fileinfo['extension']) { case 'gif': $src = imagecreatefromgif($image_path); break; case 'png': $src = imagecreatefrompng($image_path); break; case 'jpg': $src = imagecreatefromjpeg($image_path); break; } $src_width = imagesx($src); $src_height = imagesy($src); // Create working watermark image $watermark_fileinfo = G\get_image_fileinfo($options['file']); $watermark_width = $watermark_fileinfo['width']; $watermark_height = $watermark_fileinfo['height']; $watermark_max_upscale = 1.2; $watermark_max_width = $watermark_fileinfo['width'] * $watermark_max_upscale; $watermark_max_height = $watermark_fileinfo['height'] * $watermark_max_upscale; $watermark_area = $image_fileinfo['width'] * $image_fileinfo['height'] * $options['ratio']; $watermark_image_ratio = $watermark_fileinfo['ratio']; $watermark_new_height = round(sqrt($watermark_area/$watermark_image_ratio), 0); if($watermark_new_height > $watermark_max_height) { // Set a max cap $watermark_new_height = $watermark_max_height; } // To legit to quit if($watermark_new_height > $src_height) { $watermark_new_height = $src_height; } // Fix watermark margin issues on height if(getSetting('watermark_margin') and $options['position'][1] !== 'center' and $watermark_new_height + getSetting('watermark_margin') > $src_height) { $watermark_new_height -= $watermark_new_height + 2*getSetting('watermark_margin') - $src_height; } $watermark_new_width = round($watermark_image_ratio * $watermark_new_height, 0); // To legit to quit yo if($watermark_new_width > $src_width) { $watermark_new_width = $src_width; } // Fix watermark margin issues on width if(getSetting('watermark_margin') and $options['position'][0] !== 'center' and $watermark_new_width + getSetting('watermark_margin') > $src_width) { $watermark_new_width -= $watermark_new_width + 2*getSetting('watermark_margin') - $src_width; $watermark_new_height = $watermark_new_width / $watermark_image_ratio; } if($watermark_new_height <= $watermark_max_height or $watermark_new_width <= $watermark_fileinfo['width']*2) { // Resizable watermark image try { $watermark_tempnam = @tempnam(sys_get_temp_dir(), 'chvtemp'); if(!$watermark_tempnam) { $watermark_tempnam = @tempnam(dirname($image_path), 'chvtemp'); } if(!$watermark_tempnam) { throw new Exception("Can't tempnam a watermak file", 400); } self::resize($options['file'], dirname($watermark_tempnam), basename($watermark_tempnam), ['width' => $watermark_new_width]); $watermark_temp = $watermark_tempnam . '.png'; $watermark_src = imagecreatefrompng($watermark_temp); $watermark_width = imagesx($watermark_src); $watermark_height = imagesy($watermark_src); @unlink($watermark_tempnam); } catch(Exception $e) { error_log($e); } // Silence } else { // Watermark "as is" $watermark_src = imagecreatefrompng($options['file']); } // Calculate the watermark position switch($options['position'][0]) { case 'left': $watermark_x = getSetting('watermark_margin'); break; case 'center': $watermark_x = $src_width/2 - $watermark_width/2; break; case 'right': $watermark_x = $src_width - $watermark_width - getSetting('watermark_margin'); break; } switch($options['position'][1]) { case 'top': $watermark_y = getSetting('watermark_margin'); break; case 'center': $watermark_y = $src_height/2 - $watermark_height/2; break; case 'bottom': $watermark_y = $src_height - $watermark_height - getSetting('watermark_margin'); break; } /* // Watermark has the same or greater size of the image ? // --> Center the watermark if($watermark_width == $src_width && $watermark_height == $src_height) { $watermark_x = $src_width/2 - $watermark_width/2; $watermark_y = $src_height/2 - $watermark_height/2; } // Watermark is too big ? // --> Fit the watermark on the image if($watermark_width > $src_width || $watermark_height > $src_height) { // Watermark is wider than the image if($watermark_width > $src_width) { $watermark_new_width = $src_width; $watermark_new_height = $src_width * $watermark_height / $watermark_width; if($watermark_new_height > $src_height) { $watermark_new_width = $src_height * $watermark_width / $watermark_height; $watermark_new_height = $src_height; } } else { $watermark_new_width = $src_height * $watermark_width / $watermark_height; $watermark_new_height = $src_height; } try { $watermark_temp = @tempnam(sys_get_temp_dir(), 'chvtemp'); self::resize($options['file'], $watermark_temp, ['width' => $watermark_new_width]); $watermark_width = $watermark_new_width; $watermark_height = $watermark_new_height; $watermark_src = imagecreatefrompng($watermark_temp); $watermark_x = $src_width/2 - $watermark_width/2; $watermark_y = $src_height/2 - $watermark_height/2; } catch(Exception $e) {} // Silence } */ // Apply and save the watermark G\imagecopymerge_alpha($src, $watermark_src, $watermark_x, $watermark_y, 0, 0, $watermark_width, $watermark_height, getSetting('watermark_opacity'), $image_fileinfo['extension']); switch($image_fileinfo['extension']) { case 'gif': // Cast gif as png (gif has very poor quality) imagegif($src, $image_path); case 'png': imagepng($src, $image_path); break; case 'jpg': imagejpeg($src, $image_path, 90); break; } imagedestroy($src); @unlink($watermark_temp); return true; } public static function upload($source, $destination, $filename=NULL, $options=[], $storage_id=NULL) { $default_options = array( 'max_size' => G\get_bytes('2 MB'), 'filenaming' => 'original', 'exif' => TRUE, ); $options = array_merge($default_options, $options); if(!is_null($filename) and !$options['filenaming']) { $options['filenaming'] = 'original'; } try { $upload = new Upload; $upload->setSource($source); $upload->setDestination($destination); $upload->setOptions($options); if(!is_null($storage_id)) { $upload->setStorageId($storage_id); } if(!is_null($filename)) { $upload->setFilename($filename); } $upload->exec(); $original_md5 = $upload->uploaded['fileinfo']['md5']; $is_animated_image = ($upload->uploaded['fileinfo']['extension'] == 'gif' and G\is_animated_image($upload->uploaded['file'])); $apply_watermark = ($options['watermark'] and !$is_animated_image); // Disable animated image watermark if($is_animated_image) { $apply_watermark = FALSE; } if($apply_watermark) { // Detect watermark min image requirement foreach(['width', 'height'] as $k) { $min_value = getSetting('watermark_target_min_' . $k); if($min_value == 0) { // Skip on zero continue; } $apply_watermark = $upload->uploaded['fileinfo'][$k] >= $min_value; } // Disable on GIF image? if($apply_watermark and $upload->uploaded['fileinfo']['extension'] == 'gif' and !$options['watermark_gif']) { $apply_watermark = FALSE; } } if($apply_watermark && self::watermark($upload->uploaded['file'])) { $upload->uploaded['fileinfo'] = G\get_image_fileinfo($upload->uploaded['file']); // Remake the fileinfo array, new full array file info (todo: faster!) $upload->uploaded['fileinfo']['md5'] = $original_md5; // Preserve original MD5 for watermarked images } return [ 'uploaded' => $upload->uploaded, 'source' => $upload->source ]; } catch(Exception $e) { throw new UploadException($e->getMessage(), $e->getCode()); } } // Mostly for people uploading two times the same image to test or just bug you public static function isDuplicatedUpload($md5_file, $time_frame='P1D') { $db = DB::getInstance(); $db->query('SELECT * FROM ' . DB::getTable('images') . ' WHERE image_md5=:md5 AND image_uploader_ip=:ip AND image_date_gmt > :date_gmt'); $db->bind(':md5', $md5_file); $db->bind(':ip', G\get_client_ip()); $db->bind(':date_gmt', G\datetime_sub(G\datetimegmt(), $time_frame)); $db->exec(); return $db->fetchColumn(); } public static function uploadToWebsite($source, $user=NULL, $params=[]) { try { // Get user if($user) { switch(gettype($user)) { case 'string': $user = User::getSingle($user, 'username'); break; case 'integer': $user = User::getSingle($user); break; } } // Detect duplicated uploads (local) by IP + MD5 (first layer) if(!getSetting('enable_duplicate_uploads') && is_array($source) && array_key_exists('tmp_name', $source) && !$user['is_admin']) { $md5_file = md5_file($source['tmp_name']); if($md5_file && self::isDuplicatedUpload($md5_file)) { throw new Exception(_s('Duplicated upload'), 100); } } $storage_mode = getSetting('upload_storage_mode'); $datefolder = date('Y/m/d/'); switch($storage_mode) { case 'direct': $upload_path = CHV_PATH_IMAGES; break; case 'datefolder': $upload_path = CHV_PATH_IMAGES . $datefolder; break; } $filenaming = getSetting('upload_filenaming'); if($filenaming !== 'id' and in_array($params['privacy'], ['password', 'private', 'private_but_link']) ) { $filenaming = 'random'; } $upload_options = [ 'max_size' => G\get_bytes(getSetting('upload_max_filesize_mb') . ' MB'), 'watermark' => getSetting('watermark_enable'), 'exif' => (getSetting('upload_image_exif_user_setting') && $user) ? $user['image_keep_exif'] : getSetting('upload_image_exif'), ]; // Reserve this ID if($filenaming == 'id') { $AUTO_INCREMENT = DB::queryFetchSingle("SELECT AUTO_INCREMENT FROM information_schema.tables WHERE table_name = '" . DB::getTable('images') . "' AND table_schema = DATABASE();")['AUTO_INCREMENT']; $target_id = $AUTO_INCREMENT; // Wipe any garbage /*$db = DB::getInstance(); $db->query("DELETE FROM `" . DB::getTable('id_reservations') . "` WHERE"); $db->exec();*/ $last_reservation = DB::queryFetchSingle("SELECT * FROM `" . DB::getTable('id_reservations') . "` ORDER BY `id_reservation_id` DESC LIMIT 0,1"); if($last_reservation && $last_reservation['id_reservation_next_id'] > $target_id) { $target_id = $last_reservation['id_reservation_next_id']; } $reserve = [ 'reserved_id' => $target_id, 'date_gmt' => G\datetimegmt(), 'next_id' => $target_id + 1 ]; try { $reserved_id = DB::insert('id_reservations', $reserve); } catch(Exception $e) { $filenaming = 'original'; // fallback } } // Workaround watermark by user group if($upload_options['watermark']) { $watermark_enable = []; $watermark_user = $user ? ($user['is_admin'] ? 'admin' : 'user') : 'guest'; $upload_options['watermark'] = getSetting('watermark_enable_' . $watermark_user); } // Watermark by filetype $upload_options['watermark_gif'] = (bool) getSetting('watermark_enable_file_gif'); // Filenaming $upload_options['filenaming'] = $filenaming; $image_upload = self::upload($source, $upload_path, $filenaming == 'id' ? encodeID($target_id) : NULL, $upload_options, $storage_id); $chain_mask = [0, 1, 0, 1]; // original image medium thumb $chain_array = []; // Detect duplicated uploads (all) by IP + MD5 (second layer) if(!getSetting('enable_duplicate_uploads') && $image_upload['uploaded']['fileinfo']['md5'] && !$user['is_admin']) { if(self::isDuplicatedUpload($image_upload['uploaded']['fileinfo']['md5'])) { throw new Exception(_s('Duplicated upload'), 100); } } // Handle resizing KEEP 'source', change 'uploaded' $must_resize = FALSE; foreach(['width', 'height'] as $k) { if(!isset($params[$k]) or !is_numeric($params[$k])) continue; if($params[$k] != $image_upload['uploaded']['fileinfo'][$k]) { $must_resize = TRUE; } } // Disable resize for animated images (for now) if(G\is_animated_image($image_upload['uploaded']['file'])) { $must_resize = FALSE; } if($must_resize) { $image_ratio = $image_upload['uploaded']['fileinfo']['width'] / $image_upload['uploaded']['fileinfo']['height']; if($image_ratio == $params['width']/$params['height']) { $image_resize_options = [ 'width' => $params['width'], 'height' => $params['height'] ]; } else { $image_resize_options = ['width' => $params['width']]; } $image_upload['uploaded'] = self::resize($image_upload['uploaded']['file'], dirname($image_upload['uploaded']['file']), NULL, $image_resize_options); } // Try to generate the thumb $image_thumb_options = [ 'width' => getSetting('upload_thumb_width'), 'height' => getSetting('upload_thumb_height') ]; // Try to generate the medium $medium_size = getSetting('upload_medium_size'); $medium_fixed_dimension = getSetting('upload_medium_fixed_dimension'); $is_animated_image = $image_upload['uploaded']['fileinfo']['extension'] == 'gif' && G\is_animated_image($image_upload['uploaded']['file']); // Medium sized image if($image_upload['uploaded']['fileinfo'][$medium_fixed_dimension] > $medium_size or $is_animated_image) { $image_medium_options = []; $image_medium_options[$medium_fixed_dimension] = $medium_size; if($is_animated_image) { $image_medium_options['forced'] = true; $image_medium_options[$medium_fixed_dimension] = min($image_medium_options[$medium_fixed_dimension], $image_upload['uploaded']['fileinfo'][$medium_fixed_dimension]); } $image_medium = self::resize($image_upload['uploaded']['file'], dirname($image_upload['uploaded']['file']), $image_upload['uploaded']['name'] . '.md', $image_medium_options); $chain_mask[2] = 1; } // Thumb sized image $image_thumb = self::resize($image_upload['uploaded']['file'], dirname($image_upload['uploaded']['file']), $image_upload['uploaded']['name'] . '.th', $image_thumb_options); // Image chain (binary) $chain_value = bindec((int)implode('', $chain_mask)); $disk_space_needed = $image_upload['uploaded']['fileinfo']['size'] + $image_thumb['fileinfo']['size'] + ($image_medium['fileinfo']['size'] ?: 0); // Can the storage allocate all the files? if($storage_id and !empty($storage['capacity']) and $disk_space_needed > ($storage['capacity'] - $storage['space_used'])) { if(count($active_storages) > 0) { // Moar $capable_storages = []; foreach($active_storages as $k => $v) { if($v['id'] == $storage_id or $disk_space_needed > ($v['capacity'] - $v['space_used'])) continue; $capable_storages[] = $v['id']; } if(count($capable_storages) == 0) { $switch_to_local = true; } else { $storage_id = $capable_storages[0]; $storage = $active_storages[$storage_id]; } } else { $switch_to_local = true; } // Switch to local storage if($switch_to_local) { $storage_id = NULL; $downstream = $image_upload['uploaded']['file']; $fixed_filename = $image_upload['uploaded']['filename']; $uploaded_file = G\name_unique_file($upload_path, $upload_options['filenaming'], $fixed_filename); if(!@rename($downstream, $uploaded_file)) { throw new Exception("Can't re-allocate image to local storage", 500); } // Re-build the uploaded array $image_upload['uploaded'] = [ 'file' => $uploaded_file, 'filename' => G\get_filename($uploaded_file), 'name' => G\get_filename_without_extension($uploaded_file), 'fileinfo' => G\get_image_fileinfo($uploaded_file) ]; // ...And re-build all the chain $chain_props = [ 'thumb' => ['suffix' => 'th'], 'medium' => ['suffix' => 'md'] ]; if(!$image_medium) unset($chain_props['medium']); foreach($chain_props as $k => $v) { $chain_file = G\add_ending_slash(dirname($image_upload['uploaded']['file'])) . $image_upload['uploaded']['name'] . '.'.$v['suffix'].'.' . ${"image_$k"}['fileinfo']['extension']; if(!@rename(${"image_$k"}['file'], $chain_file)) { throw new Exception("Can't re-allocate image ".$k." to local storage", 500); } ${"image_$k"} = [ 'file' => $chain_file, 'filename' => G\get_filename($chain_file), 'name' => G\get_filename_without_extension($chain_file), 'fileinfo' => G\get_image_fileinfo($chain_file) ]; } } } $image_insert_values = [ 'storage_mode' => $storage_mode, 'storage_id' => $storage_id, 'user_id' => $user['id'], 'album_id' => $params['album_id'], 'nsfw' => $params['nsfw'], 'category_id' => $params['category_id'], 'title' => $params['title'], 'description' => $params['description'], 'chain' => $chain_value, 'thumb_size' => $image_thumb['fileinfo']['size'], 'medium_size' => $image_medium['fileinfo']['size'] ?: 0, 'is_animated' => $is_animated_image ]; // Expirable upload if(getSetting('enable_expirable_uploads')) { // Inject user's default expiration date if(!isset($params['expiration']) and !is_null($user['image_expiration'])) { $params['expiration'] = $user['image_expiration']; } try { // Handle image expire time (source comes as DateInterval string) if(!empty($params['expiration'])) { $params['expiration_date_gmt'] = G\datetime_add(G\datetimegmt(), strtoupper($params['expiration'])); } // Image expirable handling if(!empty($params['expiration_date_gmt'])) { $expirable_diff = G\datetime_diff(G\datetimegmt(), $params['expiration_date_gmt'], 'm'); // 5 minutes minimum $image_insert_values['expiration_date_gmt'] = $expirable_diff < 5 ? G\datetime_modify(G\datetimegmt(), '+5 minutes') : $params['expiration_date_gmt']; } } catch(Exception $e) {} // Silence } // Inject image title if(!array_key_exists('title', $params)) { // From Exif $title_from_exif = $image_upload['source']['image_exif']['ImageDescription'] ? trim($image_upload['source']['image_exif']['ImageDescription']) : NULL; if($title_from_exif) { // Get rid of any unicode stuff $title_from_exif = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $title_from_exif); $image_title = $title_from_exif; } else { // From filename $title_from_filename = preg_replace('/[-_\s]+/', ' ', trim($image_upload['source']['name'])); $image_title = $title_from_filename; } $image_insert_values['title'] = $image_title; } if($filenaming == 'id' and $target_id) { // Insert as a reserved ID $image_insert_values['id'] = $target_id; } // Trim image_title to the actual DB limit $image_insert_values['title'] = mb_substr($image_insert_values['title'], 0, 100, 'UTF-8'); $uploaded_id = self::insert($image_upload, $image_insert_values); if($filenaming == 'id') { DB::delete('id_reservations', ['id' => $reserved_id]); unset($reserved_id); } if($toStorage) { foreach($toStorage as $k => $v) { @unlink($v['file']); // Remove the source image } } if($image_insert_values['album_id']) { $album = Album::getSingle($image_insert_values['album_id']); } else { $album = NULL; } // Private upload? Create a private album then (if needed) if(in_array($params['privacy'], ['private', 'private_but_link'])) { if(is_null($album) or !in_array($album['privacy'], ['private', 'private_but_link'])) { $upload_timestamp = $params['timestamp'] ?: time(); $session_handle = 'upload_'.$upload_timestamp; // Get timestamp based album if(isset($_SESSION[$session_handle])) { $album = Album::getSingle(decodeID($_SESSION[$session_handle])); } else { $album = NULL; } // Test that... if(!$album or !in_array($album['privacy'], ['private', 'private_but_link'])) { $inserted_album = Album::insert(_s('Private upload').' '.G\datetime('Y-m-d'), $user['id'], $params['privacy']); $_SESSION[$session_handle] = encodeID($inserted_album); $image_insert_values['album_id'] = $inserted_album; } else { $image_insert_values['album_id'] = $album['id']; } } } // Update album (if any) if(isset($image_insert_values['album_id'])) { Album::addImage($image_insert_values['album_id'], $uploaded_id); } // Update user (if any) if($user) { DB::increment('users', ['image_count' => '+1'], ['id' => $user['id']]); } else { // Save this upload into "session" record if(!isset($_SESSION['guest_uploads'])) { $_SESSION['guest_uploads'] = []; } $_SESSION['guest_uploads'][] = $uploaded_id; } if($switch_to_local) { $image_viewer = self::getUrlViewer(encodeID($uploaded_id)); // NOTIFY > External storage switched to local storage system_notification_email(['subject' => _s('Upload switched to local storage'), 'message' => _s('System has switched to local storage due to not enough disk capacity (%c) in the external storage server(s). The image %s has been allocated to local storage.', ['%c' => $disk_space_needed . ' B', '%s' => ''.$image_viewer.''])]); } return $uploaded_id; } catch(Exception $e) { @unlink($image_upload['uploaded']['file']); @unlink($image_medium['file']); @unlink($image_thumb['file']); if($filenaming == 'id' and $reserved_id) { // Remove any garbage try { DB::delete('id_reservations', ['id' => $reserved_id]); } catch(Exception $e) {} // Silence } throw $e; } } public static function resize($source, $destination, $filename=NULL, $options=[]) { try { $resize = new Imageresize; $resize->setSource($source); $resize->setDestination($destination); if($filename) { $resize->setFilename($filename); } $resize->setOptions($options); if($options['width']) { $resize->set_width($options['width']); } if($options['height']) { $resize->set_height($options['height']); } if($options['width'] == $options['height']) { $resize->set_fixed(); } if($options['forced']) { $resize->setOption('forced', true); } $resize->exec(); return $resize->resized; } catch(Exception $e) { throw new ImageException($e->getMessage(), $e->getCode()); } } public static function insert($image_upload, $values=[]) { try { $table_chv_image = self::$table_chv_image; foreach($table_chv_image as $k => $v) { $table_chv_image[$k] = 'image_' . $v; } // Remove eternal/useless Exif MakerNote if($image_upload['source']['image_exif']['MakerNote']) { unset($image_upload['source']['image_exif']['MakerNote']); } $original_exifdata = $image_upload['source']['image_exif'] ? json_encode(G\array_utf8encode($image_upload['source']['image_exif'])) : NULL; // Fix some values $values['nsfw'] = in_array(strval($values['nsfw']), ['0','1']) ? $values['nsfw'] : 0; $populate_values = [ 'date' => G\datetime(), 'date_gmt' => G\datetimegmt(), 'uploader_ip' => G\get_client_ip(), 'md5' => $image_upload['uploaded']['fileinfo']['md5'], 'original_filename' => $image_upload['source']['filename'], 'original_exifdata' => $original_exifdata ]; // Populate values with fileinfo + populate_values $values = array_merge($image_upload['uploaded']['fileinfo'], $populate_values, $values); foreach(['title', 'description', 'category_id'] as $v) { G\nullify_string($values[$v]); } // Now use only the values accepted by the table foreach($values as $k => $v) { if(!in_array('image_' . $k, $table_chv_image)) { unset($values[$k]); } } // Insert image $insert = DB::insert('images', $values); $disk_space_used = $values['size'] + $values['thumb_size'] + $values['medium_size']; // Track stats Stat::track([ 'action' => 'insert', 'table' => 'images', 'value' => '+1', 'date_gmt' => $values['date_gmt'], 'disk_sum' => $disk_space_used, ]); // Update album count if(!is_null($values['album_id']) and $insert) { Album::updateImageCount($values['album_id'], 1); } return $insert; } catch(Exception $e) { throw new ImageException($e->getMessage(), $e->getCode()); } } public static function update($id, $values) { try { $values = G\array_filter_array($values, self::$table_chv_image, 'exclusion'); foreach(['title', 'description', 'category_id'] as $v) { if(!array_key_exists($v, $values)) continue; G\nullify_string($values[$v]); } if(isset($values['album_id'])) { $image_db = self::getSingle($id, FALSE, FALSE); $old_album = $image_db['image_album_id']; $new_album = $values['album_id']; $update = DB::update('images', $values, ['id' => $id]); if($update and $old_album !== $new_album) { if(!is_null($old_album)) { // Update the old album Album::updateImageCount($old_album, 1, '-'); } if(!is_null($new_album)) { // Update the new album Album::updateImageCount($new_album, 1); } } return $update; } else { return DB::update('images', $values, ['id' => $id]); } } catch(Excepton $e) { throw new ImageException($e->getMessage(), 400); } } public static function delete($id, $update_user=TRUE) { try { $image = self::getSingle($id, FALSE, TRUE); $disk_space_used = $image['size'] + $image['thumb']['size'] + $image['medium']['size']; foreach($image['file_resource']['chain'] as $file_delete) { if(file_exists($file_delete) and !@unlink($file_delete)) { throw new ImageException("Can't delete file", 200); } } if($update_user and isset($image['user']['id'])) { DB::increment('users', ['image_count' => '-1'], ['id' => $image['user']['id']]); } // Update album count if($image['album']['id'] > 0) { Album::updateImageCount($image['album']['id'], 1, '-'); } // Track stats Stat::track([ 'action' => 'delete', 'table' => 'images', 'value' => '-1', 'date_gmt' => $image['date_gmt'], 'disk_sum' => $disk_space_used, 'likes' => $image['likes'], ]); // Remove "liked" counter for each user who liked this image DB::queryExec('UPDATE '.DB::getTable('users').' INNER JOIN '.DB::getTable('likes').' ON user_id = like_user_id AND like_content_type = "image" AND like_content_id = '.$image['id'].' SET user_liked = GREATEST(cast(user_liked AS SIGNED) - 1, 0);'); if(isset($image['user']['id'])) { // Detect autolike $autoliked = DB::get('likes', ['user_id' => $image['user']['id'], 'content_type' => 'image', 'content_id' => $image['id']])[0]; $likes_counter = $image['likes']; if($autoliked) { $likes_counter -= 1; } // Update user "likes" counter DB::increment('users', ['likes' => '-' . $likes_counter], ['id' => $image['user']['id']]); // Remove notifications related to this image (owner notifications) Notification::delete([ 'table' => 'images', 'image_id' => $image['id'], 'user_id' => $image['user']['id'], ]); } // Remove image likes DB::delete('likes', ['content_type' => 'image', 'content_id' => $image['id']]); // Log image deletion DB::insert('deletions', [ 'date_gmt' => G\datetimegmt(), 'content_id' => $image['id'], 'content_date_gmt' => $image['date_gmt'], 'content_user_id' => $image['user']['id'], 'content_ip' => $image['uploader_ip'], 'content_views' => $image['views'], 'content_md5' => $image['md5'], 'content_likes' => $image['likes'], 'content_original_filename' => $image['original_filename'], ]); return DB::delete('images', ['id' => $id]); } catch(Exception $e) { throw new ImageException($e->getMessage(), 400); } } public static function deleteMultiple($ids) { if(!is_array($ids)) { throw new ImageException('Expecting array argument, '.gettype($ids).' given in '. __METHOD__, 100); } try { $affected = 0; foreach($ids as $id) { if(self::delete($id)) { $affected += 1; } } return $affected; } catch(Excepton $e) { throw new ImageException($e->getMessage(), 400); } } public static function deleteExpired() { try { $db = DB::getInstance(); $db->query('SELECT image_id FROM ' . DB::getTable('images') . ' WHERE image_expiration_date_gmt IS NOT NULL AND image_expiration_date_gmt < :datetimegmt ORDER BY image_expiration_date_gmt DESC LIMIT 50;'); // Just 50 files per request to prevent CPU meltdown or something like that $db->bind(':datetimegmt', G\datetimegmt()); $expired_db = $db->fetchAll(); if($expired_db) { $expired = []; foreach($expired_db as $k => $v) { $expired[] = $v['image_id']; } self::deleteMultiple($expired); } else { return NULL; } return $return ? $return['image_id'] : FALSE; } catch(Exception $e) { throw new ImageException($e->getMessage(), 400); } } public static function fill(&$image) { $image['id_encoded'] = encodeID($image['id']); $targets = self::getSrcTargetSingle($image, false); if($targets['type'] == 'path') { // Re-create missing stuff if($image['size'] == 0) { $get_image_fileinfo = G\get_image_fileinfo($targets['chain']['image']); $update_missing_values = [ 'width' => $get_image_fileinfo['width'], 'height' => $get_image_fileinfo['height'], 'size' => $get_image_fileinfo['size'], ]; foreach(['thumb', 'medium'] as $k) { if(!array_key_exists($k, $targets['chain'])) { continue; } if($image[$k . '_size'] == 0) { $update_missing_values[$k . '_size'] = intval(filesize(G\get_image_fileinfo($targets['chain'][$k]))); } } self::update($image['id'], $update_missing_values); $image = array_merge($image, $update_missing_values); } $is_animated = $image['extension'] == 'gif' && G\is_animated_image($targets['chain']['image']); // Recreate thumb if(count($targets['chain']) > 0 && !$targets['chain']['thumb']) { try { $thumb_options = [ 'width' => getSetting('upload_thumb_width'), 'height' => getSetting('upload_thumb_height'), 'forced' => $image['extension'] == 'gif' && $is_animated ]; $targets['chain']['thumb'] = self::resize($targets['chain']['image'], pathinfo($targets['chain']['image'], PATHINFO_DIRNAME), $image['name'] . '.th', $thumb_options)['file']; } catch(Exception $e) {} } // Recreate medium $medium_size = getSetting('upload_medium_size'); $medium_fixed_dimension = getSetting('upload_medium_fixed_dimension'); if($image[$medium_fixed_dimension] > $medium_size && count($targets['chain']) > 0 && !$targets['chain']['medium']) { try { $medium_options = [ $medium_fixed_dimension => $medium_size, 'forced' => $image['extension'] == 'gif' && $is_animated ]; $targets['chain']['medium'] = self::resize($targets['chain']['image'], pathinfo($targets['chain']['image'], PATHINFO_DIRNAME), $image['name'] . '.md', $medium_options)['file']; } catch(Exception $e) {} } if(count($targets['chain']) > 0) { $original_md5 = $image['md5']; $image = array_merge($image, (array) @get_image_fileinfo($targets['chain']['image'])); // Never do an array merge over an empty thing! $image['md5'] = $original_md5; } // Update is_animated flag if($is_animated && !$image['is_animated']) { self::update($image['id'], ['is_animated' => 1]); $image['is_animated'] = 1; } } else { $image_fileinfo = [ 'ratio' => $image['width'] / $image['height'], 'size' => intval($image['size']), 'size_formatted' => G\format_bytes($image['size']) ]; $image = array_merge($image, get_image_fileinfo($targets['chain']['image']), $image_fileinfo); } $image['file_resource'] = $targets; $image['url_viewer'] = self::getUrlViewer($image['id_encoded']); foreach($targets['chain'] as $k => $v) { if($targets['type'] == 'path') { $image[$k] = file_exists($v) ? get_image_fileinfo($v) : NULL; } else { $image[$k] = get_image_fileinfo($v); } $image[$k]['size'] = $image[($k == 'image' ? '' : $k . '_') . 'size']; } $display = $image['medium'] !== NULL ? $image['medium'] : ($image['size'] < G\get_bytes('500 KB') ? $image : $image['thumb']); $display_thumb = $display == $image['thumb']; $image['size_formatted'] = G\format_bytes($image['size']); $image['display_url'] = $display['url']; $image['display_width'] = $display_thumb ? getSetting('upload_thumb_width') : $image['width']; $image['display_height'] = $display_thumb ? getSetting('upload_thumb_height') : $image['height']; $image['views_label'] = _n('view', 'views', $image['views']); $image['likes_label'] = _n('like', 'likes', $image['likes']); $image['how_long_ago'] = time_elapsed_string($image['date_gmt']); $image['date_fixed_peer'] = Login::getUser() ? G\datetimegmt_convert_tz($image['date_gmt'], Login::getUser()['timezone']) : $image['date_gmt']; $image['title_truncated'] = G\truncate($image['title'], 28); $image['title_truncated_html'] = G\safe_html($image['title_truncated']); } public static function formatArray($dbrow, $safe=false) { try { $output = DB::formatRow($dbrow); if(!is_null($output['user']['id'])) { User::fill($output['user']); } else { unset($output['user']); } if(!is_null($output['album']['id']) or !is_null($output['user']['id'])) { Album::fill($output['album'], $output['user']); } else { unset($output['album']); } self::fill($output); if($safe) { unset($output['storage']); unset($output['id'], $output['path'], $output['uploader_ip']); unset($output['album']['id'], $output['album']['privacy_extra']); unset($output['user']['id']); unset($output['file_resource']); } return $output; } catch(Excepton $e) { throw new ImageException($e->getMessage(), 400); } } } class ImageException extends Exception {}