/** * 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; foreach (array("view_count", "relative_url_cache", "relative_path_cache", "resize_width", "resize_height", "resize_dirty", "thumb_width", "thumb_height", "thumb_dirty") as $key) { unset($significant_changes[$key]); } 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(); } if ($this->is_album()) { // Sanitize the album name. $this->name = legal_file::sanitize_dirname($this->name); } else { // Process the data file info. This also sanitizes the item name. if (isset($this->data_file)) { $this->_process_data_file_info(); } else { // New photos and movies must have a data file. $this->data_file_error = true; } } // 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. We set a // generic name ("photo", "movie", or "album") based on its type, then rely on // check_and_fix_conflicts to ensure it doesn't conflict with another name. if (empty($this->slug)) { $this->slug = $this->type; } } $this->_check_and_fix_conflicts(); 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": 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", $this); // If any significant fields have changed, load up a copy of the original item and // keep it around. $original = ORM::factory("item", $this->id); // If we have a new data file, process its info. This will get its metadata and // 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)) { $this->_process_data_file_info(); } else { if (!$this->is_album() && array_key_exists("name", $this->changed)) { // There's no new data file, but the name changed. If it's a photo or movie, // make sure the new name still agrees with the file type. $this->name = legal_file::sanitize_filename($this->name, pathinfo($original->name, PATHINFO_EXTENSION), $this->type); } } // If an album's name changed, sanitize it. if ($this->is_album() && array_key_exists("name", $this->changed)) { $this->name = legal_file::sanitize_dirname($this->name); } // If an album's cover has changed (or been removed), delete any existing album cover, // reset the thumb metadata, and mark the thumb as dirty. if (array_key_exists("album_cover_item_id", $this->changed) && $this->is_album()) { @unlink($original->thumb_path()); $this->thumb_dirty = 1; $this->thumb_height = 0; $this->thumb_width = 0; } 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->_check_and_fix_conflicts(); 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()); } } // 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()); $this->thumb_dirty = 1; $this->resize_dirty = 1; } 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()); } 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; }
public function sanitize_filename_with_corrections_test() { $this->assert_equal("foo_bar", legal_file::sanitize_dirname("/foo/bar/")); $this->assert_equal("foo_bar", legal_file::sanitize_dirname("\\foo\\bar\\")); $this->assert_equal(".foo..bar", legal_file::sanitize_dirname(".foo..bar.")); $this->assert_equal("foo_bar", legal_file::sanitize_dirname("_foo__bar_")); $this->assert_equal("album", legal_file::sanitize_dirname("_")); $this->assert_equal("album", legal_file::sanitize_dirname(null)); }