public function get_file_metadata_with_valid_extension_but_illegal_file_contents_test() { copy(MODPATH . "gallery/tests/Photo_Helper_Test.php", TMPPATH . "test_php_with_jpg_extension.jpg"); try { $metadata = photo::get_file_metadata(TMPPATH . "test_php_with_jpg_extension.jpg"); $this->assert_true(false, "Shouldn't get here"); } catch (Exception $e) { // pass } unlink(TMPPATH . "test_php_with_jpg_extension.jpg"); }
static function item_before_create($item) { $max_size = module::get_var("max_size", "max_size", 600); if ($item->is_photo()) { list($width, $height, $mime_type) = photo::get_file_metadata($item->data_file); if ($width > $max_size || $height > $max_size) { $tempnam = tempnam(TMPPATH, "size"); $tmpfile = $tempnam . "." . pathinfo($item->data_file, PATHINFO_EXTENSION); gallery_graphics::resize($item->data_file, $tmpfile, array("width" => $max_size, "height" => $max_size, "master" => Image::AUTO)); rename($tmpfile, $item->data_file); unlink($tempnam); } } }
public function generate_album_cover_from_png_test() { $input_file = MODPATH . "gallery/tests/test.jpg"; $output_file = TMPPATH . test::random_name() . ".png"; gallery_graphics::resize($input_file, $output_file, null, null); $album = test::random_album(); $photo = test::random_photo_unsaved($album); $photo->set_data_file($output_file); $photo->name = "album_cover_from_png.png"; $photo->save(); $album->reload(); // Check that the image was correctly resized and converted to jpg $this->assert_equal(array(200, 150, "image/jpeg", "jpg"), photo::get_file_metadata($album->thumb_path())); // Check that the items table got updated $this->assert_equal(array(200, 150), array($album->thumb_width, $album->thumb_height)); // Check that the image is not marked dirty $this->assert_equal(0, $album->thumb_dirty); }
public function resize_jpg_to_png_without_options_test() { // Input is a 1024x768 jpg, output is png without options - should not attempt resize $input_file = MODPATH . "gallery/tests/test.jpg"; $output_file = TMPPATH . test::random_name() . ".png"; gallery_graphics::resize($input_file, $output_file, null, null); // Output is converted from input without resize $this->assert_equal(array(1024, 768, "image/png", "png"), photo::get_file_metadata($output_file)); }
/** * Handle any business logic necessary to create or modify an item. * @see ORM::save() * * @return ORM Item_Model */ public function save() { $significant_changes = $this->changed; unset($significant_changes["view_count"]); unset($significant_changes["relative_url_cache"]); unset($significant_changes["relative_path_cache"]); if (!empty($this->changed) && $significant_changes || isset($this->data_file)) { $this->updated = time(); if (!$this->loaded()) { // Create a new item. module::event("item_before_create", $this); // Set a weight if it's missing. We don't do this in the constructor because it's not a // simple assignment. if (empty($this->weight)) { $this->weight = item::get_max_weight(); } // Make an url friendly slug from the name, if necessary if (empty($this->slug)) { $this->slug = item::convert_filename_to_slug(pathinfo($this->name, PATHINFO_FILENAME)); // If the filename is all invalid characters, then the slug may be empty here. Pick a // random value. if (empty($this->slug)) { $this->slug = (string) rand(1000, 9999); } } // Get the width, height and mime type from our data file for photos and movies. if ($this->is_photo() || $this->is_movie()) { if ($this->is_photo()) { list($this->width, $this->height, $this->mime_type, $extension) = photo::get_file_metadata($this->data_file); } else { if ($this->is_movie()) { list($this->width, $this->height, $this->mime_type, $extension) = movie::get_file_metadata($this->data_file); } } // Force an extension onto the name if necessary $pi = pathinfo($this->data_file); if (empty($pi["extension"])) { $this->name = "{$this->name}.{$extension}"; } } $this->_randomize_name_or_slug_on_conflict(); parent::save(); // Build our url caches, then save again. We have to do this after it's already been // saved once because we use only information from the database to build the paths. If we // could depend on a save happening later we could defer this 2nd save. $this->_build_relative_caches(); parent::save(); // Take any actions that we can only do once all our paths are set correctly after saving. switch ($this->type) { case "album": mkdir($this->file_path()); mkdir(dirname($this->thumb_path())); mkdir(dirname($this->resize_path())); break; case "photo": case "movie": // The thumb or resize may already exist in the case where a movie and a photo generate // a thumbnail of the same name (eg, foo.flv movie and foo.jpg photo will generate // foo.jpg thumbnail). If that happens, randomize and save again. if (file_exists($this->resize_path()) || file_exists($this->thumb_path())) { $pi = pathinfo($this->name); $this->name = $pi["filename"] . "-" . random::int() . "." . $pi["extension"]; parent::save(); } copy($this->data_file, $this->file_path()); break; } // This will almost definitely trigger another save, so put it at the end so that we're // tail recursive. Null out the data file variable first, otherwise the next save will // trigger an item_updated_data_file event. $this->data_file = null; module::event("item_created", $this); } else { // Update an existing item module::event("item_before_update", $item); // If any significant fields have changed, load up a copy of the original item and // keep it around. $original = ORM::factory("item", $this->id); // Preserve the extension of the data file. Many helpers, (e.g. ImageMagick), assume // the MIME type from the extension. So when we adopt the new data file, it's important // to adopt the new extension. That ensures that the item's extension is always // appropriate for its data. We don't try to preserve the name of the data file, though, // because the name is typically a temporary randomly-generated name. if (isset($this->data_file)) { $extension = pathinfo($this->data_file, PATHINFO_EXTENSION); $new_name = pathinfo($this->name, PATHINFO_FILENAME) . ".{$extension}"; if (!empty($extension) && strcmp($this->name, $new_name)) { $this->name = $new_name; } if ($this->is_photo()) { list($this->width, $this->height, $this->mime_type, $extension) = photo::get_file_metadata($this->data_file); } else { if ($this->is_movie()) { list($this->width, $this->height, $this->mime_type, $extension) = movie::get_file_metadata($this->data_file); } } } if (array_intersect($this->changed, array("parent_id", "name", "slug"))) { $original->_build_relative_caches(); $this->relative_path_cache = null; $this->relative_url_cache = null; } $this->_randomize_name_or_slug_on_conflict(); parent::save(); // Now update the filesystem and any database caches if there were significant value // changes. If anything past this point fails, then we'll have an inconsistent database // so this code should be as robust as we can make it. // Update the MPTT pointers, if necessary. We have to do this before we generate any // cached paths! if ($original->parent_id != $this->parent_id) { parent::move_to($this->parent()); } if ($original->parent_id != $this->parent_id || $original->name != $this->name) { $this->_build_relative_caches(); // If there is a data file, then we want to preserve both the old data and the new data. // (Third-party event handlers would like access to both). The old data file will be // accessible via the $original item, and the new one via $this item. But in that case, // we don't want to rename the original as below, because the old data would end up being // clobbered by the new data file. Also, the rename isn't necessary, because the new item // data is coming from the data file anyway. So we only perform the rename if there isn't // a data file. Another way to solve this would be to copy the original file rather than // conditionally rename it, but a copy would cost far more than the rename. if (!isset($this->data_file)) { @rename($original->file_path(), $this->file_path()); } // Move all of the items associated data files if ($this->is_album()) { @rename(dirname($original->resize_path()), dirname($this->resize_path())); @rename(dirname($original->thumb_path()), dirname($this->thumb_path())); } else { @rename($original->resize_path(), $this->resize_path()); @rename($original->thumb_path(), $this->thumb_path()); } if ($original->parent_id != $this->parent_id) { // This will result in 2 events since we'll still fire the item_updated event below module::event("item_moved", $this, $original->parent()); } } // Changing the name, slug or parent ripples downwards if ($this->is_album() && ($original->name != $this->name || $original->slug != $this->slug || $original->parent_id != $this->parent_id)) { db::build()->update("items")->set("relative_url_cache", null)->set("relative_path_cache", null)->where("left_ptr", ">", $this->left_ptr)->where("right_ptr", "<", $this->right_ptr)->execute(); } // Replace the data file, if requested. if ($this->data_file && ($this->is_photo() || $this->is_movie())) { copy($this->data_file, $this->file_path()); // Get the width, height and mime type from our data file for photos and movies. if ($this->is_photo()) { list($this->width, $this->height) = photo::get_file_metadata($this->file_path()); } else { if ($this->is_movie()) { list($this->width, $this->height) = movie::get_file_metadata($this->file_path()); } } $this->thumb_dirty = 1; $this->resize_dirty = 1; } module::event("item_updated", $original, $this); if ($this->data_file) { // Null out the data file variable here, otherwise this event will trigger another // save() which will think that we're doing another file move. $this->data_file = null; if ($original->file_path() != $this->file_path()) { @unlink($original->file_path()); } module::event("item_updated_data_file", $this); } } } else { if (!empty($this->changed)) { // Insignificant changes only. Don't fire events or do any special checking to try to keep // this lightweight. parent::save(); } } return $this; }
/** * Process the data file info. Get its metadata and extension. * If valid, use it to sanitize the item name and update the * width, height, and mime type. */ private function _process_data_file_info() { try { if ($this->is_photo()) { list($this->width, $this->height, $this->mime_type, $extension) = photo::get_file_metadata($this->data_file); } else { if ($this->is_movie()) { list($this->width, $this->height, $this->mime_type, $extension) = movie::get_file_metadata($this->data_file); } else { // Albums don't have data files. $this->data_file = null; return; } } // Sanitize the name based on the idenified extension, but only set $this->name if different // to ensure it isn't unnecessarily marked as "changed" $name = legal_file::sanitize_filename($this->name, $extension, $this->type); if ($this->name != $name) { $this->name = $name; } // Data file valid - make sure the flag is reset to false. $this->data_file_error = false; } catch (Exception $e) { // Data file invalid - set the flag so it's reported during item validation. $this->data_file_error = true; } }
public function add() { access::verify_csrf(); $form = watermark::get_add_form(); // For TEST_MODE, we want to simulate a file upload. Because this is not a true upload, Forge's // validation logic will correctly reject it. So, we skip validation when we're running tests. if (TEST_MODE || $form->validate()) { $file = $_POST["file"]; // Forge prefixes files with "uploadfile-xxxxxxx" for uniqueness $name = preg_replace("/uploadfile-[^-]+-(.*)/", '$1', basename($file)); try { list($width, $height, $mime_type, $extension) = photo::get_file_metadata($file); // Sanitize filename, which ensures a valid extension. This renaming prevents the issues // addressed in ticket #1855, where an image that looked valid (header said jpg) with a // php extension was previously accepted without changing its extension. $name = legal_file::sanitize_filename($name, $extension, "photo"); } catch (Exception $e) { message::error(t("Invalid or unidentifiable image file")); system::delete_later($file); return; } rename($file, VARPATH . "modules/watermark/{$name}"); module::set_var("watermark", "name", $name); module::set_var("watermark", "width", $width); module::set_var("watermark", "height", $height); module::set_var("watermark", "mime_type", $mime_type); module::set_var("watermark", "position", $form->add_watermark->position->value); module::set_var("watermark", "transparency", $form->add_watermark->transparency->value); $this->_update_graphics_rules(); system::delete_later($file); message::success(t("Watermark saved")); log::success("watermark", t("Watermark saved")); json::reply(array("result" => "success", "location" => url::site("admin/watermarks"))); } else { // rawurlencode the results because the JS code that uploads the file buffers it in an // iframe which entitizes the HTML and makes it difficult for the JS to process. If we url // encode it now, it passes through cleanly. See ticket #797. json::reply(array("result" => "error", "html" => rawurlencode((string) $form))); } // Override the application/json mime type. The dialog based HTML uploader uses an iframe to // buffer the reply, and on some browsers (Firefox 3.6) it does not know what to do with the // JSON that it gets back so it puts up a dialog asking the user what to do with it. So force // the encoding type back to HTML for the iframe. // See: http://jquery.malsup.com/form/#file-upload header("Content-Type: text/html; charset=" . Kohana::CHARSET); }
private static function _update_item_dimensions($item) { if ($item->is_photo()) { list($item->resize_width, $item->resize_height) = photo::get_file_metadata($item->resize_path()); } list($item->thumb_width, $item->thumb_height) = photo::get_file_metadata($item->thumb_path()); }
/** * Make sure that the data file is well formed (it exists and isn't empty). */ public function valid_data_file(Validation $v, $field) { if (!is_file($this->data_file)) { $v->add_error("name", "bad_data_file_path"); } else { if (filesize($this->data_file) == 0) { $v->add_error("name", "empty_data_file"); } } if ($this->loaded()) { if ($this->is_photo()) { list($a, $b, $mime_type) = photo::get_file_metadata($this->data_file); } else { if ($this->is_movie()) { list($a, $b, $mime_type) = movie::get_file_metadata($this->data_file); } } if ($mime_type != $this->mime_type) { $v->add_error("name", "cant_change_mime_type"); } } }
/** * Get PDF file metadata (height and width) * (ref: movie::get_file_metadata()) */ static function movie_get_file_metadata($file_path, $metadata) { if (strtolower(pathinfo($file_path, PATHINFO_EXTENSION)) == "pdf" && ($path = pdf::find_gs())) { // Parsing gs output properly can be a pain. So, let's go for a reliable albeit inefficient // approach: re-extract the frame (into tmp) and get its image size. $temp_file = system::temp_filename("pdf_", "jpg"); pdf_event::movie_extract_frame($file_path, $temp_file, null, null); list($metadata->height, $metadata->width) = photo::get_file_metadata($temp_file); } }
/** * Overlay an image on top of the input file. * * Valid options are: file, position, transparency, padding * * Valid positions: northwest, north, northeast, * west, center, east, * southwest, south, southeast * * padding is in pixels * * @param string $input_file * @param string $output_file * @param array $options * @param Item_Model $item (optional) */ static function composite($input_file, $output_file, $options, $item = null) { try { graphics::init_toolkit(); $temp_file = system::temp_filename("composite_", pathinfo($output_file, PATHINFO_EXTENSION)); module::event("graphics_composite", $input_file, $temp_file, $options, $item); if (@filesize($temp_file) > 0) { // A graphics_composite event made an image - move it to output_file and use it. @rename($temp_file, $output_file); } else { // No events made an image - proceed with standard process. list($width, $height) = photo::get_file_metadata($input_file); list($w_width, $w_height) = photo::get_file_metadata($options["file"]); $pad = isset($options["padding"]) ? $options["padding"] : 10; $top = $pad; $left = $pad; $y_center = max($height / 2 - $w_height / 2, $pad); $x_center = max($width / 2 - $w_width / 2, $pad); $bottom = max($height - $w_height - $pad, $pad); $right = max($width - $w_width - $pad, $pad); switch ($options["position"]) { case "northwest": $x = $left; $y = $top; break; case "north": $x = $x_center; $y = $top; break; case "northeast": $x = $right; $y = $top; break; case "west": $x = $left; $y = $y_center; break; case "center": $x = $x_center; $y = $y_center; break; case "east": $x = $right; $y = $y_center; break; case "southwest": $x = $left; $y = $bottom; break; case "south": $x = $x_center; $y = $bottom; break; case "southeast": $x = $right; $y = $bottom; break; } Image::factory($input_file)->composite($options["file"], $x, $y, $options["transparency"])->quality(module::get_var("gallery", "image_quality"))->save($output_file); } module::event("graphics_composite_completed", $input_file, $output_file, $options, $item); } catch (ErrorException $e) { // Unlike rotate and resize, composite catches its exceptions here. This is because // composite is typically called for watermarks. If during thumb/resize generation // the watermark fails, we'd still like the image resized, just without its watermark. // If the exception isn't caught here, graphics::generate will replace it with a // placeholder. Kohana_Log::add("error", $e->getMessage()); } }
public function add() { access::verify_csrf(); $form = watermark::get_add_form(); // For TEST_MODE, we want to simulate a file upload. Because this is not a true upload, Forge's // validation logic will correctly reject it. So, we skip validation when we're running tests. if (TEST_MODE || $form->validate()) { $file = $_POST["file"]; // Forge prefixes files with "uploadfile-xxxxxxx" for uniqueness $name = preg_replace("/uploadfile-[^-]+-(.*)/", '$1', basename($file)); try { list($width, $height, $mime_type, $extension) = photo::get_file_metadata($file); // Sanitize filename, which ensures a valid extension. This renaming prevents the issues // addressed in ticket #1855, where an image that looked valid (header said jpg) with a // php extension was previously accepted without changing its extension. $name = legal_file::sanitize_filename($name, $extension, "photo"); } catch (Exception $e) { message::error(t("Invalid or unidentifiable image file")); system::delete_later($file); return; } rename($file, VARPATH . "modules/watermark/{$name}"); module::set_var("watermark", "name", $name); module::set_var("watermark", "width", $width); module::set_var("watermark", "height", $height); module::set_var("watermark", "mime_type", $mime_type); module::set_var("watermark", "position", $form->add_watermark->position->value); module::set_var("watermark", "transparency", $form->add_watermark->transparency->value); $this->_update_graphics_rules(); system::delete_later($file); message::success(t("Watermark saved")); log::success("watermark", t("Watermark saved")); json::reply(array("result" => "success", "location" => url::site("admin/watermarks"))); } else { json::reply(array("result" => "error", "html" => (string) $form)); } // Override the application/json mime type for iframe compatibility. See ticket #2022. header("Content-Type: text/plain; charset=" . Kohana::CHARSET); }