PageRenderTime 64ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/3.0/modules/videos/models/item.php

http://github.com/gallery/gallery3-contrib
PHP | 1030 lines | 662 code | 104 blank | 264 comment | 192 complexity | c4bfe6b1ab9863b6c7584ed3cb9f07b6 MD5 | raw file
Possible License(s): GPL-3.0, GPL-2.0, LGPL-2.1
  1. <?php defined("SYSPATH") or die("No direct script access.");
  2. /**
  3. * Gallery - a web based photo album viewer and editor
  4. * Copyright (C) 2000-2013 Bharat Mediratta
  5. *
  6. * This program is free software; you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation; either version 2 of the License, or (at
  9. * your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful, but
  12. * WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program; if not, write to the Free Software
  18. * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. class Item_Model_Core extends ORM_MPTT {
  21. protected $children = "items";
  22. protected $sorting = array();
  23. public $data_file = null;
  24. public function __construct($id=null) {
  25. parent::__construct($id);
  26. if (!$this->loaded()) {
  27. // Set reasonable defaults
  28. $this->created = time();
  29. $this->rand_key = random::percent();
  30. $this->thumb_dirty = 1;
  31. $this->resize_dirty = 1;
  32. $this->sort_column = "created";
  33. $this->sort_order = "ASC";
  34. $this->owner_id = identity::active_user()->id;
  35. }
  36. }
  37. /**
  38. * Add a set of restrictions to any following queries to restrict access only to items
  39. * viewable by the active user.
  40. * @chainable
  41. */
  42. public function viewable() {
  43. return item::viewable($this);
  44. }
  45. /**
  46. * Is this item an album?
  47. * @return true if it's an album
  48. */
  49. public function is_album() {
  50. return $this->type == 'album';
  51. }
  52. /**
  53. * Is this item a photo?
  54. * @return true if it's a photo
  55. */
  56. public function is_photo() {
  57. return $this->type == 'photo';
  58. }
  59. /**
  60. * Is this item a movie?
  61. * @return true if it's a movie
  62. */
  63. public function is_movie() {
  64. return $this->type == 'movie';
  65. }
  66. public function delete($ignored_id=null) {
  67. if (!$this->loaded()) {
  68. // Concurrent deletes may result in this item already being gone. Ignore it.
  69. return;
  70. }
  71. if ($this->id == 1) {
  72. $v = new Validation(array("id"));
  73. $v->add_error("id", "cant_delete_root_album");
  74. ORM_Validation_Exception::handle_validation($this->table_name, $v);
  75. }
  76. $old = clone $this;
  77. module::event("item_before_delete", $this);
  78. $parent = $this->parent();
  79. if ($parent->album_cover_item_id == $this->id) {
  80. item::remove_album_cover($parent);
  81. }
  82. $path = $this->file_path();
  83. $resize_path = $this->resize_path();
  84. $thumb_path = $this->thumb_path();
  85. parent::delete();
  86. if (is_dir($path)) {
  87. // Take some precautions against accidentally deleting way too much
  88. $delete_resize_path = dirname($resize_path);
  89. $delete_thumb_path = dirname($thumb_path);
  90. if ($delete_resize_path == VARPATH . "resizes" ||
  91. $delete_thumb_path == VARPATH . "thumbs" ||
  92. $path == VARPATH . "albums") {
  93. throw new Exception(
  94. "@todo DELETING_TOO_MUCH ($delete_resize_path, $delete_thumb_path, $path)");
  95. }
  96. @dir::unlink($path);
  97. @dir::unlink($delete_resize_path);
  98. @dir::unlink($delete_thumb_path);
  99. } else {
  100. @unlink($path);
  101. @unlink($resize_path);
  102. @unlink($thumb_path);
  103. }
  104. module::event("item_deleted", $old);
  105. }
  106. /**
  107. * Specify the path to the data file associated with this item. To actually associate it,
  108. * you still have to call save().
  109. * @chainable
  110. */
  111. public function set_data_file($data_file) {
  112. $this->data_file = $data_file;
  113. return $this;
  114. }
  115. /**
  116. * Return the server-relative url to this item, eg:
  117. * /gallery3/index.php/BobsWedding?page=2
  118. * /gallery3/index.php/BobsWedding/Eating-Cake.jpg
  119. *
  120. * @param string $query the query string (eg "show=3")
  121. */
  122. public function url($query=null) {
  123. $url = url::site($this->relative_url());
  124. if ($query) {
  125. $url .= "?$query";
  126. }
  127. return $url;
  128. }
  129. /**
  130. * Return the full url to this item, eg:
  131. * http://example.com/gallery3/index.php/BobsWedding?page=2
  132. * http://example.com/gallery3/index.php/BobsWedding/Eating-Cake.jpg
  133. *
  134. * @param string $query the query string (eg "show=3")
  135. */
  136. public function abs_url($query=null) {
  137. $url = url::abs_site($this->relative_url());
  138. if ($query) {
  139. $url .= "?$query";
  140. }
  141. return $url;
  142. }
  143. /**
  144. * album: /var/albums/album1/album2
  145. * photo: /var/albums/album1/album2/photo.jpg
  146. */
  147. public function file_path() {
  148. return VARPATH . "albums/" . urldecode($this->relative_path());
  149. }
  150. /**
  151. * album: http://example.com/gallery3/var/resizes/album1/
  152. * photo: http://example.com/gallery3/var/albums/album1/photo.jpg
  153. */
  154. public function file_url($full_uri=false) {
  155. $relative_path = "var/albums/" . $this->relative_path();
  156. $cache_buster = $this->_cache_buster($this->file_path());
  157. return ($full_uri ? url::abs_file($relative_path) : url::file($relative_path))
  158. . $cache_buster;
  159. }
  160. /**
  161. * album: /var/resizes/album1/.thumb.jpg
  162. * photo: /var/albums/album1/photo.thumb.jpg
  163. */
  164. public function thumb_path() {
  165. $base = VARPATH . "thumbs/" . urldecode($this->relative_path());
  166. if ($this->is_photo()) {
  167. return $base;
  168. } else if ($this->is_album()) {
  169. return $base . "/.album.jpg";
  170. } else if ($this->is_movie()) {
  171. // Replace the extension with jpg
  172. return preg_replace("/...$/", "jpg", $base);
  173. }
  174. }
  175. /**
  176. * Return true if there is a thumbnail for this item.
  177. */
  178. public function has_thumb() {
  179. return $this->thumb_width && $this->thumb_height;
  180. }
  181. /**
  182. * album: http://example.com/gallery3/var/resizes/album1/.thumb.jpg
  183. * photo: http://example.com/gallery3/var/albums/album1/photo.thumb.jpg
  184. */
  185. public function thumb_url($full_uri=false) {
  186. $cache_buster = $this->_cache_buster($this->thumb_path());
  187. $relative_path = "var/thumbs/" . $this->relative_path();
  188. $base = ($full_uri ? url::abs_file($relative_path) : url::file($relative_path));
  189. if ($this->is_photo()) {
  190. return $base . $cache_buster;
  191. } else if ($this->is_album()) {
  192. return $base . "/.album.jpg" . $cache_buster;
  193. } else if ($this->is_movie()) {
  194. // Replace the extension with jpg
  195. $base = preg_replace("/...$/", "jpg", $base);
  196. return $base . $cache_buster;
  197. }
  198. }
  199. /**
  200. * album: /var/resizes/album1/.resize.jpg
  201. * photo: /var/albums/album1/photo.resize.jpg
  202. */
  203. public function resize_path() {
  204. return VARPATH . "resizes/" . urldecode($this->relative_path()) .
  205. ($this->is_album() ? "/.album.jpg" : "");
  206. }
  207. /**
  208. * album: http://example.com/gallery3/var/resizes/album1/.resize.jpg
  209. * photo: http://example.com/gallery3/var/albums/album1/photo.resize.jpg
  210. */
  211. public function resize_url($full_uri=false) {
  212. $relative_path = "var/resizes/" . $this->relative_path();
  213. $cache_buster = $this->_cache_buster($this->resize_path());
  214. return ($full_uri ? url::abs_file($relative_path) : url::file($relative_path)) .
  215. ($this->is_album() ? "/.album.jpg" : "") . $cache_buster;
  216. }
  217. /**
  218. * Rebuild the relative_path_cache and relative_url_cache.
  219. */
  220. private function _build_relative_caches() {
  221. $names = array();
  222. $slugs = array();
  223. foreach (db::build()
  224. ->select(array("name", "slug"))
  225. ->from("items")
  226. ->where("left_ptr", "<=", $this->left_ptr)
  227. ->where("right_ptr", ">=", $this->right_ptr)
  228. ->where("id", "<>", 1)
  229. ->order_by("left_ptr", "ASC")
  230. ->execute() as $row) {
  231. // Don't encode the names segment
  232. $names[] = rawurlencode($row->name);
  233. $slugs[] = rawurlencode($row->slug);
  234. }
  235. $this->relative_path_cache = implode($names, "/");
  236. $this->relative_url_cache = implode($slugs, "/");
  237. return $this;
  238. }
  239. /**
  240. * Return the relative path to this item's file. Note that the components of the path are
  241. * urlencoded so if you want to use this as a filesystem path, you need to call urldecode
  242. * on it.
  243. * @return string
  244. */
  245. public function relative_path() {
  246. if (!$this->loaded()) {
  247. return;
  248. }
  249. if (!isset($this->relative_path_cache)) {
  250. $this->_build_relative_caches()->save();
  251. }
  252. return $this->relative_path_cache;
  253. }
  254. /**
  255. * Return the relative url to this item's file.
  256. * @return string
  257. */
  258. public function relative_url() {
  259. if (!$this->loaded()) {
  260. return;
  261. }
  262. if (!isset($this->relative_url_cache)) {
  263. $this->_build_relative_caches()->save();
  264. }
  265. return $this->relative_url_cache;
  266. }
  267. /**
  268. * @see ORM::__get()
  269. */
  270. public function __get($column) {
  271. if ($column == "owner") {
  272. // This relationship depends on an outside module, which may not be present so handle
  273. // failures gracefully.
  274. try {
  275. return identity::lookup_user($this->owner_id);
  276. } catch (Exception $e) {
  277. return null;
  278. }
  279. } else {
  280. return parent::__get($column);
  281. }
  282. }
  283. /**
  284. * Handle any business logic necessary to create or modify an item.
  285. * @see ORM::save()
  286. *
  287. * @return ORM Item_Model
  288. */
  289. public function save() {
  290. $significant_changes = $this->changed;
  291. unset($significant_changes["view_count"]);
  292. unset($significant_changes["relative_url_cache"]);
  293. unset($significant_changes["relative_path_cache"]);
  294. if ((!empty($this->changed) && $significant_changes) || isset($this->data_file)) {
  295. $this->updated = time();
  296. if (!$this->loaded()) {
  297. // Create a new item.
  298. module::event("item_before_create", $this);
  299. // Set a weight if it's missing. We don't do this in the constructor because it's not a
  300. // simple assignment.
  301. if (empty($this->weight)) {
  302. $this->weight = item::get_max_weight();
  303. }
  304. // Make an url friendly slug from the name, if necessary
  305. if (empty($this->slug)) {
  306. $tmp = pathinfo($this->name, PATHINFO_FILENAME);
  307. $tmp = preg_replace("/[^A-Za-z0-9-_]+/", "-", $tmp);
  308. $this->slug = trim($tmp, "-");
  309. // If the filename is all invalid characters, then the slug may be empty here. Pick a
  310. // random value.
  311. if (empty($this->slug)) {
  312. $this->slug = (string)rand(1000, 9999);
  313. }
  314. }
  315. // Get the width, height and mime type from our data file for photos and movies.
  316. if ($this->is_photo() || $this->is_movie()) {
  317. if ($this->is_photo()) {
  318. list ($this->width, $this->height, $this->mime_type, $extension) =
  319. photo::get_file_metadata($this->data_file);
  320. } else if ($this->is_movie()) {
  321. list ($this->width, $this->height, $this->mime_type, $extension) =
  322. movie::get_file_metadata($this->data_file);
  323. }
  324. // Force an extension onto the name if necessary
  325. $pi = pathinfo($this->data_file);
  326. if (empty($pi["extension"])) {
  327. $this->name = "{$this->name}.$extension";
  328. }
  329. }
  330. $this->_randomize_name_or_slug_on_conflict();
  331. parent::save();
  332. // Build our url caches, then save again. We have to do this after it's already been
  333. // saved once because we use only information from the database to build the paths. If we
  334. // could depend on a save happening later we could defer this 2nd save.
  335. $this->_build_relative_caches();
  336. parent::save();
  337. // Take any actions that we can only do once all our paths are set correctly after saving.
  338. switch ($this->type) {
  339. case "album":
  340. mkdir($this->file_path());
  341. mkdir(dirname($this->thumb_path()));
  342. mkdir(dirname($this->resize_path()));
  343. break;
  344. case "photo":
  345. case "movie":
  346. // The thumb or resize may already exist in the case where a movie and a photo generate
  347. // a thumbnail of the same name (eg, foo.flv movie and foo.jpg photo will generate
  348. // foo.jpg thumbnail). If that happens, randomize and save again.
  349. if (file_exists($this->resize_path()) ||
  350. file_exists($this->thumb_path())) {
  351. $pi = pathinfo($this->name);
  352. $this->name = $pi["filename"] . "-" . random::int() . "." . $pi["extension"];
  353. parent::save();
  354. }
  355. copy($this->data_file, $this->file_path());
  356. break;
  357. }
  358. // This will almost definitely trigger another save, so put it at the end so that we're
  359. // tail recursive. Null out the data file variable first, otherwise the next save will
  360. // trigger an item_updated_data_file event.
  361. $this->data_file = null;
  362. module::event("item_created", $this);
  363. } else {
  364. // Update an existing item
  365. module::event("item_before_update", $item);
  366. // If any significant fields have changed, load up a copy of the original item and
  367. // keep it around.
  368. $original = ORM::factory("item", $this->id);
  369. if (array_intersect($this->changed, array("parent_id", "name", "slug"))) {
  370. $original->_build_relative_caches();
  371. $this->relative_path_cache = null;
  372. $this->relative_url_cache = null;
  373. }
  374. $this->_randomize_name_or_slug_on_conflict();
  375. parent::save();
  376. // Now update the filesystem and any database caches if there were significant value
  377. // changes. If anything past this point fails, then we'll have an inconsistent database
  378. // so this code should be as robust as we can make it.
  379. // Update the MPTT pointers, if necessary. We have to do this before we generate any
  380. // cached paths!
  381. if ($original->parent_id != $this->parent_id) {
  382. parent::move_to($this->parent());
  383. }
  384. if ($original->parent_id != $this->parent_id || $original->name != $this->name) {
  385. // Move all of the items associated data files
  386. @rename($original->file_path(), $this->file_path());
  387. if ($this->is_album()) {
  388. @rename(dirname($original->resize_path()), dirname($this->resize_path()));
  389. @rename(dirname($original->thumb_path()), dirname($this->thumb_path()));
  390. } else {
  391. @rename($original->resize_path(), $this->resize_path());
  392. @rename($original->thumb_path(), $this->thumb_path());
  393. }
  394. if ($original->parent_id != $this->parent_id) {
  395. // This will result in 2 events since we'll still fire the item_updated event below
  396. module::event("item_moved", $this, $original->parent());
  397. }
  398. }
  399. // Changing the name, slug or parent ripples downwards
  400. if ($this->is_album() &&
  401. ($original->name != $this->name ||
  402. $original->slug != $this->slug ||
  403. $original->parent_id != $this->parent_id)) {
  404. db::build()
  405. ->update("items")
  406. ->set("relative_url_cache", null)
  407. ->set("relative_path_cache", null)
  408. ->where("left_ptr", ">", $this->left_ptr)
  409. ->where("right_ptr", "<", $this->right_ptr)
  410. ->execute();
  411. }
  412. // Replace the data file, if requested.
  413. // @todo: we don't handle the case where you swap in a file of a different mime type
  414. // should we prevent that in validation? or in set_data_file()
  415. if ($this->data_file && ($this->is_photo() || $this->is_movie())) {
  416. copy($this->data_file, $this->file_path());
  417. // Get the width, height and mime type from our data file for photos and movies.
  418. if ($this->is_photo()) {
  419. list ($this->width, $this->height) = photo::get_file_metadata($this->file_path());
  420. } else if ($this->is_movie()) {
  421. list ($this->width, $this->height) = movie::get_file_metadata($this->file_path());
  422. }
  423. $this->thumb_dirty = 1;
  424. $this->resize_dirty = 1;
  425. }
  426. module::event("item_updated", $original, $this);
  427. if ($this->data_file) {
  428. // Null out the data file variable here, otherwise this event will trigger another
  429. // save() which will think that we're doing another file move.
  430. $this->data_file = null;
  431. module::event("item_updated_data_file", $this);
  432. }
  433. }
  434. } else if (!empty($this->changed)) {
  435. // Insignificant changes only. Don't fire events or do any special checking to try to keep
  436. // this lightweight.
  437. parent::save();
  438. }
  439. return $this;
  440. }
  441. /**
  442. * Check to see if there's another item that occupies the same name or slug that this item
  443. * intends to use, and if so choose a new name/slug while preserving the extension.
  444. * @todo Improve this. Random numbers are not user friendly
  445. */
  446. private function _randomize_name_or_slug_on_conflict() {
  447. $base_name = pathinfo($this->name, PATHINFO_FILENAME);
  448. $base_ext = pathinfo($this->name, PATHINFO_EXTENSION);
  449. $base_slug = $this->slug;
  450. while (ORM::factory("item")
  451. ->where("parent_id", "=", $this->parent_id)
  452. ->where("id", $this->id ? "<>" : "IS NOT", $this->id)
  453. ->and_open()
  454. ->where("name", "=", $this->name)
  455. ->or_where("slug", "=", $this->slug)
  456. ->close()
  457. ->find()->id) {
  458. $rand = random::int();
  459. if ($base_ext) {
  460. $this->name = "$base_name-$rand.$base_ext";
  461. } else {
  462. $this->name = "$base_name-$rand";
  463. }
  464. $this->slug = "$base_slug-$rand";
  465. }
  466. }
  467. /**
  468. * Return the Item_Model representing the cover for this album.
  469. * @return Item_Model or null if there's no cover
  470. */
  471. public function album_cover() {
  472. if (!$this->is_album()) {
  473. return null;
  474. }
  475. if (empty($this->album_cover_item_id)) {
  476. return null;
  477. }
  478. try {
  479. return model_cache::get("item", $this->album_cover_item_id);
  480. } catch (Exception $e) {
  481. // It's possible (unlikely) that the item was deleted, if so keep going.
  482. return null;
  483. }
  484. }
  485. /**
  486. * Find the position of the given child id in this album. The resulting value is 1-indexed, so
  487. * the first child in the album is at position 1.
  488. *
  489. * This method stands as a backward compatibility for gallery 3.0, and will
  490. * be deprecated in version 3.1.
  491. */
  492. public function get_position($child, $where=array()) {
  493. return item::get_position($child, $where);
  494. }
  495. /**
  496. * Return an <img> tag for the thumbnail.
  497. * @param array $extra_attrs Extra attributes to add to the img tag
  498. * @param int (optional) $max Maximum size of the thumbnail (default: null)
  499. * @param boolean (optional) $center_vertically Center vertically (default: false)
  500. * @return string
  501. */
  502. public function thumb_img($extra_attrs=array(), $max=null, $center_vertically=false) {
  503. list ($height, $width) = $this->scale_dimensions($max);
  504. if ($center_vertically && $max) {
  505. // The constant is divide by 2 to calculate the file and 10 to convert to em
  506. $margin_top = (int)(($max - $height) / 20);
  507. $extra_attrs["style"] = "margin-top: {$margin_top}em";
  508. $extra_attrs["title"] = $this->title;
  509. }
  510. $attrs = array_merge($extra_attrs,
  511. array(
  512. "src" => $this->thumb_url(),
  513. "alt" => $this->title,
  514. "width" => $width,
  515. "height" => $height)
  516. );
  517. // html::image forces an absolute url which we don't want
  518. return "<img" . html::attributes($attrs) . "/>";
  519. }
  520. /**
  521. * Calculate the largest width/height that fits inside the given maximum, while preserving the
  522. * aspect ratio. Don't upscale.
  523. * @param int $max Maximum size of the largest dimension
  524. * @return array
  525. */
  526. public function scale_dimensions($max) {
  527. $width = $this->thumb_width;
  528. $height = $this->thumb_height;
  529. if ($width <= $max && $height <= $max) {
  530. return array($height, $width);
  531. }
  532. if ($height) {
  533. if (isset($max)) {
  534. if ($width > $height) {
  535. $height = (int)($max * $height / $width);
  536. $width = $max;
  537. } else {
  538. $width = (int)($max * $width / $height);
  539. $height = $max;
  540. }
  541. }
  542. } else {
  543. // Missing thumbnail, can happen on albums with no photos yet.
  544. // @todo we should enforce a placeholder for those albums.
  545. $width = 0;
  546. $height = 0;
  547. }
  548. return array($height, $width);
  549. }
  550. /**
  551. * Return an <img> tag for the resize.
  552. * @param array $extra_attrs Extra attributes to add to the img tag
  553. * @return string
  554. */
  555. public function resize_img($extra_attrs) {
  556. $attrs = array_merge($extra_attrs,
  557. array("src" => $this->resize_url(),
  558. "alt" => $this->title,
  559. "width" => $this->resize_width,
  560. "height" => $this->resize_height)
  561. );
  562. // html::image forces an absolute url which we don't want
  563. return "<img" . html::attributes($attrs) . "/>";
  564. }
  565. /**
  566. * Return a flowplayer <script> tag for movies
  567. * @param array $extra_attrs
  568. * @return string
  569. */
  570. public function movie_img($extra_attrs) {
  571. $v = new View("movieplayer.html");
  572. $max_size = module::get_var("gallery", "resize_size", 640);
  573. $width = $this->width;
  574. $height = $this->height;
  575. if ($width > $max_size || $height > $max_size) {
  576. if ($width > $height) {
  577. $height = (int)($height * $max_size / $width);
  578. $width = $max_size;
  579. } else {
  580. $width = (int)($width * $max_size / $height);
  581. $height = $max_size;
  582. }
  583. }
  584. $v->attrs = array_merge($extra_attrs, array("style" => "width:{$width}px;height:{$height}px",
  585. "class" => "g-movie"));
  586. if (empty($v->attrs["id"])) {
  587. $v->attrs["id"] = "g-item-id-{$this->id}";
  588. }
  589. return $v;
  590. }
  591. /**
  592. * Return all of the children of this album. Unless you specify a specific sort order, the
  593. * results will be ordered by this album's sort order.
  594. *
  595. * @chainable
  596. * @param integer SQL limit
  597. * @param integer SQL offset
  598. * @param array additional where clauses
  599. * @param array order_by
  600. * @return array ORM
  601. */
  602. function children($limit=null, $offset=null, $where=array(), $order_by=null) {
  603. if (empty($order_by)) {
  604. $order_by = array($this->sort_column => $this->sort_order);
  605. // Use id as a tie breaker
  606. if ($this->sort_column != "id") {
  607. $order_by["id"] = "ASC";
  608. }
  609. }
  610. return parent::children($limit, $offset, $where, $order_by);
  611. }
  612. /**
  613. * Return the children of this album, and all of it's sub-albums. Unless you specify a specific
  614. * sort order, the results will be ordered by this album's sort order. Note that this
  615. * album's sort order is imposed on all sub-albums, regardless of their sort order.
  616. *
  617. * @chainable
  618. * @param integer SQL limit
  619. * @param integer SQL offset
  620. * @param array additional where clauses
  621. * @return object ORM_Iterator
  622. */
  623. function descendants($limit=null, $offset=null, $where=array(), $order_by=null) {
  624. if (empty($order_by)) {
  625. $order_by = array($this->sort_column => $this->sort_order);
  626. // Use id as a tie breaker
  627. if ($this->sort_column != "id") {
  628. $order_by["id"] = "ASC";
  629. }
  630. }
  631. return parent::descendants($limit, $offset, $where, $order_by);
  632. }
  633. /**
  634. * Specify our rules here so that we have access to the instance of this model.
  635. */
  636. public function validate(Validation $array=null) {
  637. if (!$array) {
  638. $this->rules = array(
  639. "album_cover_item_id" => array("callbacks" => array(array($this, "valid_album_cover"))),
  640. "description" => array("rules" => array("length[0,65535]")),
  641. "mime_type" => array("callbacks" => array(array($this, "valid_field"))),
  642. "name" => array("rules" => array("length[0,255]", "required"),
  643. "callbacks" => array(array($this, "valid_name"))),
  644. "parent_id" => array("callbacks" => array(array($this, "valid_parent"))),
  645. "rand_key" => array("rule" => array("decimal")),
  646. "slug" => array("rules" => array("length[0,255]", "required"),
  647. "callbacks" => array(array($this, "valid_slug"))),
  648. "sort_column" => array("callbacks" => array(array($this, "valid_field"))),
  649. "sort_order" => array("callbacks" => array(array($this, "valid_field"))),
  650. "title" => array("rules" => array("length[0,255]", "required")),
  651. "type" => array("callbacks" => array(array($this, "read_only"),
  652. array($this, "valid_field"))),
  653. );
  654. // Conditional rules
  655. if ($this->id == 1) {
  656. // We don't care about the name and slug for the root album.
  657. $this->rules["name"] = array();
  658. $this->rules["slug"] = array();
  659. }
  660. // Movies and photos must have data files. Verify the data file on new items, or if it has
  661. // been replaced.
  662. if (($this->is_photo() || $this->is_movie()) && $this->data_file) {
  663. $this->rules["name"]["callbacks"][] = array($this, "valid_data_file");
  664. }
  665. }
  666. parent::validate($array);
  667. }
  668. /**
  669. * Validate that the desired slug does not conflict.
  670. */
  671. public function valid_slug(Validation $v, $field) {
  672. if (preg_match("/[^A-Za-z0-9-_]/", $this->slug)) {
  673. $v->add_error("slug", "not_url_safe");
  674. } else if (db::build()
  675. ->from("items")
  676. ->where("parent_id", "=", $this->parent_id)
  677. ->where("id", "<>", $this->id)
  678. ->where("slug", "=", $this->slug)
  679. ->count_records()) {
  680. $v->add_error("slug", "conflict");
  681. }
  682. }
  683. /**
  684. * Validate the item name. It can't conflict with other names, can't contain slashes or
  685. * trailing periods.
  686. */
  687. public function valid_name(Validation $v, $field) {
  688. if (strpos($this->name, "/") !== false) {
  689. $v->add_error("name", "no_slashes");
  690. return;
  691. } else if (rtrim($this->name, ".") !== $this->name) {
  692. $v->add_error("name", "no_trailing_period");
  693. return;
  694. }
  695. if ($this->is_movie() || $this->is_photo()) {
  696. if ($this->loaded()) {
  697. // Existing items can't change their extension
  698. $original = ORM::factory("item", $this->id);
  699. $new_ext = pathinfo($this->name, PATHINFO_EXTENSION);
  700. $old_ext = pathinfo($original->name, PATHINFO_EXTENSION);
  701. if (strcasecmp($new_ext, $old_ext)) {
  702. $v->add_error("name", "illegal_data_file_extension");
  703. return;
  704. }
  705. } else {
  706. // New items must have an extension
  707. $ext = pathinfo($this->name, PATHINFO_EXTENSION);
  708. if (!$ext) {
  709. $v->add_error("name", "illegal_data_file_extension");
  710. return;
  711. }
  712. // rWatcher EDIT: Include other video file types.
  713. if ($this->is_movie() && !preg_match("/^(flv|mp4|m4v|" . implode("|", unserialize(module::get_var("videos", "allowed_extensions"))) . ")$/i", $ext)) {
  714. $v->add_error("name", "illegal_data_file_extension");
  715. } else if ($this->is_photo() && !preg_match("/^(gif|jpg|jpeg|png)$/i", $ext)) {
  716. $v->add_error("name", "illegal_data_file_extension");
  717. }
  718. }
  719. }
  720. if (db::build()
  721. ->from("items")
  722. ->where("parent_id", "=", $this->parent_id)
  723. ->where("name", "=", $this->name)
  724. ->merge_where($this->id ? array(array("id", "<>", $this->id)) : null)
  725. ->count_records()) {
  726. $v->add_error("name", "conflict");
  727. return;
  728. }
  729. }
  730. /**
  731. * Make sure that the data file is well formed (it exists and isn't empty).
  732. */
  733. public function valid_data_file(Validation $v, $field) {
  734. if (!is_file($this->data_file)) {
  735. $v->add_error("name", "bad_data_file_path");
  736. } else if (filesize($this->data_file) == 0) {
  737. $v->add_error("name", "empty_data_file");
  738. }
  739. if ($this->loaded()) {
  740. if ($this->is_photo()) {
  741. list ($a, $b, $mime_type) = photo::get_file_metadata($this->data_file);
  742. } else if ($this->is_movie()) {
  743. list ($a, $b, $mime_type) = movie::get_file_metadata($this->data_file);
  744. }
  745. if ($mime_type != $this->mime_type) {
  746. $v->add_error("name", "cant_change_mime_type");
  747. }
  748. }
  749. }
  750. /**
  751. * Make sure that the parent id refers to an album.
  752. */
  753. public function valid_parent(Validation $v, $field) {
  754. if ($this->id == 1) {
  755. if ($this->parent_id != 0) {
  756. $v->add_error("parent_id", "invalid");
  757. }
  758. } else {
  759. $query = db::build()
  760. ->from("items")
  761. ->where("id", "=", $this->parent_id)
  762. ->where("type", "=", "album");
  763. // If this is an existing item, make sure the new parent is not part of our hierarchy
  764. if ($this->loaded()) {
  765. $query->and_open()
  766. ->where("left_ptr", "<", $this->left_ptr)
  767. ->or_where("right_ptr", ">", $this->right_ptr)
  768. ->close();
  769. }
  770. if ($query->count_records() != 1) {
  771. $v->add_error("parent_id", "invalid");
  772. }
  773. }
  774. }
  775. /**
  776. * Make sure the album cover item id refers to a valid item, or is null.
  777. */
  778. public function valid_album_cover(Validation $v, $field) {
  779. if ($this->id == 1) {
  780. return;
  781. }
  782. if ($this->album_cover_item_id && db::build()
  783. ->from("items")
  784. ->where("id", "=", $this->album_cover_item_id)
  785. ->count_records() != 1) {
  786. $v->add_error("album_cover_item_id", "invalid_item");
  787. }
  788. }
  789. /**
  790. * Make sure that the type is valid.
  791. */
  792. public function valid_field(Validation $v, $field) {
  793. switch($field) {
  794. case "mime_type":
  795. if ($this->is_movie()) {
  796. // rWatcher EDIT.
  797. //$legal_values = array("video/flv", "video/x-flv", "video/mp4");
  798. $legal_values = array("video/flv", "video/x-flv", "video/mp4", "video/avi", "video/msvideo", "video/x-msvideo", "video/mpeg", "video/quicktime", "video/x-ms-wmv", "application/octet-stream", "video/x-ms-asf" );
  799. } if ($this->is_photo()) {
  800. $legal_values = array("image/jpeg", "image/gif", "image/png");
  801. }
  802. break;
  803. case "sort_column":
  804. if (!array_key_exists($this->sort_column, $this->object)) {
  805. $v->add_error($field, "invalid");
  806. }
  807. break;
  808. case "sort_order":
  809. $legal_values = array("ASC", "DESC", "asc", "desc");
  810. break;
  811. case "type":
  812. $legal_values = array("album", "photo", "movie");
  813. break;
  814. default:
  815. $v->add_error($field, "unvalidated_field");
  816. break;
  817. }
  818. if (isset($legal_values) && !in_array($this->$field, $legal_values)) {
  819. $v->add_error($field, "invalid");
  820. }
  821. }
  822. /**
  823. * This field cannot be changed after it's been set.
  824. */
  825. public function read_only(Validation $v, $field) {
  826. if ($this->loaded() && isset($this->changed[$field])) {
  827. $v->add_error($field, "read_only");
  828. }
  829. }
  830. /**
  831. * Same as ORM::as_array() but convert id fields into their RESTful form.
  832. *
  833. * @param array if specified, only return the named fields
  834. */
  835. public function as_restful_array($fields=array()) {
  836. if ($fields) {
  837. $data = array();
  838. foreach ($fields as $field) {
  839. if (isset($this->object[$field])) {
  840. $data[$field] = $this->__get($field);
  841. }
  842. }
  843. $fields = array_flip($fields);
  844. } else {
  845. $data = $this->as_array();
  846. }
  847. // Convert item ids to rest URLs for consistency
  848. if (empty($fields) || isset($fields["parent"])) {
  849. if ($tmp = $this->parent()) {
  850. $data["parent"] = rest::url("item", $tmp);
  851. }
  852. unset($data["parent_id"]);
  853. }
  854. if (empty($fields) || isset($fields["album_cover"])) {
  855. if ($tmp = $this->album_cover()) {
  856. $data["album_cover"] = rest::url("item", $tmp);
  857. }
  858. unset($data["album_cover_item_id"]);
  859. }
  860. if (empty($fields) || isset($fields["web_url"])) {
  861. $data["web_url"] = $this->abs_url();
  862. }
  863. if (!$this->is_album()) {
  864. if (access::can("view_full", $this)) {
  865. if (empty($fields) || isset($fields["file_url"])) {
  866. $data["file_url"] = rest::url("data", $this, "full");
  867. }
  868. if (empty($fields) || isset($fields["file_size"])) {
  869. $data["file_size"] = filesize($this->file_path());
  870. }
  871. if (access::user_can(identity::guest(), "view_full", $this)) {
  872. if (empty($fields) || isset($fields["file_url_public"])) {
  873. $data["file_url_public"] = $this->file_url(true);
  874. }
  875. }
  876. }
  877. }
  878. if ($this->is_photo()) {
  879. if (empty($fields) || isset($fields["resize_url"])) {
  880. $data["resize_url"] = rest::url("data", $this, "resize");
  881. }
  882. if (empty($fields) || isset($fields["resize_size"])) {
  883. $data["resize_size"] = filesize($this->resize_path());
  884. }
  885. if (access::user_can(identity::guest(), "view", $this)) {
  886. if (empty($fields) || isset($fields["resize_url_public"])) {
  887. $data["resize_url_public"] = $this->resize_url(true);
  888. }
  889. }
  890. }
  891. if ($this->has_thumb()) {
  892. if (empty($fields) || isset($fields["thumb_url"])) {
  893. $data["thumb_url"] = rest::url("data", $this, "thumb");
  894. }
  895. if (empty($fields) || isset($fields["thumb_size"])) {
  896. $data["thumb_size"] = filesize($this->thumb_path());
  897. }
  898. if (access::user_can(identity::guest(), "view", $this)) {
  899. if (empty($fields) || isset($fields["thumb_url_public"])) {
  900. $data["thumb_url_public"] = $this->thumb_url(true);
  901. }
  902. }
  903. }
  904. if (empty($fields) || isset($fields["can_edit"])) {
  905. $data["can_edit"] = access::can("edit", $this);
  906. }
  907. // Elide some internal-only data that is going to cause confusion in the client.
  908. foreach (array("relative_path_cache", "relative_url_cache", "left_ptr", "right_ptr",
  909. "thumb_dirty", "resize_dirty", "weight") as $key) {
  910. unset($data[$key]);
  911. }
  912. return $data;
  913. }
  914. /**
  915. * Increments the view counter of this item
  916. * We can't use math in ORM or the query builder, so do this by hand. It's important
  917. * that we do this with math, otherwise concurrent accesses will damage accuracy.
  918. */
  919. public function increment_view_count() {
  920. db::query("UPDATE {items} SET `view_count` = `view_count` + 1 WHERE `id` = $this->id")
  921. ->execute();
  922. }
  923. private function _cache_buster($path) {
  924. return "?m=" . (string)(file_exists($path) ? filemtime($path) : 0);
  925. }
  926. }