PageRenderTime 53ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/models/behaviors/media.php

https://github.com/inast/media
PHP | 462 lines | 271 code | 39 blank | 152 comment | 34 complexity | 5f865a77509e9368d7800fd2b8135978 MD5 | raw file
Possible License(s): MIT
  1. <?php
  2. /**
  3. * Media Behavior File
  4. *
  5. * Copyright (c) 2007-2010 David Persson
  6. *
  7. * Distributed under the terms of the MIT License.
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * PHP version 5
  11. * CakePHP version 1.2
  12. *
  13. * @package media
  14. * @subpackage media.models.behaviors
  15. * @copyright 2007-2010 David Persson <davidpersson@gmx.de>
  16. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  17. * @link http://github.com/davidpersson/media
  18. */
  19. App::import('Vendor', 'Media.MimeType');
  20. App::import('Vendor', 'Media.Medium');
  21. /**
  22. * Media Behavior Class
  23. *
  24. * @package media
  25. * @subpackage media.models.behaviors
  26. */
  27. class MediaBehavior extends ModelBehavior {
  28. /**
  29. * Settings keyed by model alias
  30. *
  31. * @var array
  32. */
  33. var $settings = array();
  34. /**
  35. * Default settings
  36. *
  37. * metadataLevel
  38. * 0 - (disabled) No retrieval of additional metadata
  39. * 1 - (basic) Adds `mime_type` and `size` fields
  40. * 2 - (detailed) Adds Multiple fields dependent on the type of the file e.g. `artist`, `title`
  41. *
  42. * baseDirectory
  43. * An absolute path (with trailing slash) to a directory which will be stripped off the file path
  44. *
  45. * makeVersions
  46. * false - Disable version generation
  47. * true - Creates versions (configured in plugin's `core.php`) of files on create
  48. *
  49. * filterDirectory
  50. * An absolute path (with trailing slash) to a directory to use for storing generated versions
  51. *
  52. * createDirectory
  53. * false - Fail on missing directories
  54. * true - Recursively create missing directories
  55. *
  56. * @var array
  57. */
  58. var $_defaultSettings = array(
  59. 'metadataLevel' => 1,
  60. 'baseDirectory' => MEDIA,
  61. 'makeVersions' => true,
  62. 'filterDirectory' => MEDIA_FILTER,
  63. 'createDirectory' => true,
  64. );
  65. /**
  66. * Holds cached metadata keyed by model alias
  67. *
  68. * @var array
  69. * @access private
  70. */
  71. var $__cached;
  72. /**
  73. * Setup
  74. *
  75. * @param Model $Model
  76. * @param array $config See defaultSettings for configuration options
  77. * @return void
  78. */
  79. function setup(&$Model, $config = null) {
  80. if (!is_array($config)) {
  81. $config = array();
  82. }
  83. /* `base` config option deprecation */
  84. if (isset($config['base'])) {
  85. $message = "MediaBehavior::setup - ";
  86. $message .= "The `base` option has been deprecated in favour of `baseDirectory`.";
  87. trigger_error($message, E_USER_NOTICE);
  88. $config['baseDirectory'] = $config['base'];
  89. unset($config['base']);
  90. }
  91. /* Interact with Transfer Behavior */
  92. if (isset($Model->Behaviors->Transfer)) {
  93. $transferSettings = $Model->Behaviors->Transfer->settings[$Model->alias];
  94. $config['baseDirectory'] = dirname($transferSettings['baseDirectory']) . DS;
  95. $config['createDirectory'] = $transferSettings['createDirectory'];
  96. }
  97. $this->settings[$Model->alias] = $config + $this->_defaultSettings;
  98. $this->__cached[$Model->alias] = Cache::read('media_metadata_' . $Model->alias, '_cake_core_');
  99. }
  100. /**
  101. * Callback
  102. *
  103. * Requires `file` field to be present if a record is created.
  104. *
  105. * Handles deletion of a record and corresponding file if the `delete` field is
  106. * present and has not a value of either `null` or `'0'.`
  107. *
  108. * Prevents `dirname`, `basename`, `checksum` and `delete` fields to be written to
  109. * database.
  110. *
  111. * Parses contents of the `file` field if present and generates a normalized path
  112. * relative to the path set in the `baseDirectory` option.
  113. *
  114. * @param Model $Model
  115. * @return boolean
  116. */
  117. function beforeSave(&$Model) {
  118. if (!$Model->exists()) {
  119. if (!isset($Model->data[$Model->alias]['file'])) {
  120. unset($Model->data[$Model->alias]);
  121. return true;
  122. }
  123. } else {
  124. if (isset($Model->data[$Model->alias]['delete'])
  125. && $Model->data[$Model->alias]['delete'] !== '0') {
  126. $Model->delete();
  127. unset($Model->data[$Model->alias]);
  128. return true;
  129. }
  130. }
  131. $blacklist = array(
  132. 'dirname', 'basename', 'checksum', 'delete'
  133. );
  134. $whitelist = array(
  135. 'id', 'file', 'model', 'foreign_key',
  136. 'created', 'modified', 'alternative'
  137. );
  138. foreach ($Model->data[$Model->alias] as $key => $value) {
  139. if (in_array($key, $whitelist)) {
  140. continue;
  141. }
  142. if (in_array($key, $blacklist)) {
  143. unset($Model->data[$Model->alias][$key]);
  144. }
  145. }
  146. extract($this->settings[$Model->alias]);
  147. if (isset($Model->data[$Model->alias]['file'])) {
  148. $File = new File($Model->data[$Model->alias]['file']);
  149. unset($Model->data[$Model->alias]['file']);
  150. /* `baseDirectory` may equal the file's directory or use backslashes */
  151. $dirname = substr(str_replace(
  152. str_replace('\\', '/', $baseDirectory),
  153. null,
  154. str_replace('\\', '/', Folder::slashTerm($File->Folder->pwd()))
  155. ), 0, -1);
  156. $result = array(
  157. 'checksum' => $File->md5(true),
  158. 'dirname' => $dirname,
  159. 'basename' => $File->name,
  160. );
  161. $Model->data[$Model->alias] = array_merge($Model->data[$Model->alias], $result);
  162. }
  163. return true;
  164. }
  165. /**
  166. * Callback
  167. *
  168. * Triggers `make()` if both `dirname` and `basename` fields are present.
  169. * Otherwise skips and returns `true` to continue the save operation.
  170. *
  171. * @param Model $Model
  172. * @param boolean $created
  173. * @return boolean
  174. */
  175. function afterSave(&$Model, $created) {
  176. extract($this->settings[$Model->alias]);
  177. if (!$created || !$makeVersions) {
  178. return true;
  179. }
  180. $item =& $Model->data[$Model->alias];
  181. if (!isset($item['dirname'], $item['basename'])) {
  182. return true;
  183. }
  184. return $this->make($Model, $item['dirname'] . DS . $item['basename']);
  185. }
  186. /**
  187. * Callback
  188. *
  189. * Adds metadata of corresponding file to each result.
  190. *
  191. * If the corresponding file of a result is not readable it is removed
  192. * from the results array, as it is inconsistent. This can be fixed
  193. * by calling `cake media sync` from the command line.
  194. *
  195. * @param Model $Model
  196. * @param array $results
  197. * @param boolean $primary
  198. * @return array
  199. */
  200. function afterFind(&$Model, $results, $primary = false) {
  201. if (empty($results)) {
  202. return $results;
  203. }
  204. extract($this->settings[$Model->alias]);
  205. foreach ($results as $key => &$result) {
  206. /* Needed during a pre deletion phase */
  207. if (!isset($result[$Model->alias]['dirname'], $result[$Model->alias]['basename'])) {
  208. continue;
  209. }
  210. $file = $result[$Model->alias]['dirname'] . DS . $result[$Model->alias]['basename'];
  211. $metadata = $this->metadata($Model, $file, $metadataLevel);
  212. /* `metadata()` checks if the file is readable */
  213. if ($metadata === false) {
  214. unset($results[$key]);
  215. continue;
  216. }
  217. $result[$Model->alias] = array_merge($result[$Model->alias], $metadata);
  218. }
  219. return $results;
  220. }
  221. /**
  222. * Callback
  223. *
  224. * Deletes file corresponding to record as well as generated versions of that file.
  225. *
  226. * If the file couldn't be deleted the callback won't stop the
  227. * delete operation to continue to delete the record.
  228. *
  229. * @param Model $Model
  230. * @param boolean $cascade
  231. * @return boolean
  232. */
  233. function beforeDelete(&$Model, $cascade = true) {
  234. extract($this->settings[$Model->alias]);
  235. $query = array(
  236. 'conditions' => array('id' => $Model->id),
  237. 'fields' => array('dirname', 'basename'),
  238. 'recursive' => -1,
  239. );
  240. $result = $Model->find('first', $query);
  241. if (empty($result)) {
  242. return false; /* Record did not pass verification? */
  243. }
  244. $file = $baseDirectory;
  245. $file .= $result[$Model->alias]['dirname'];
  246. $file .= DS . $result[$Model->alias]['basename'];
  247. $File = new File($file);
  248. $Folder = new Folder($filterDirectory);
  249. list($versions, ) = $Folder->ls();
  250. foreach ($versions as $version) {
  251. $Folder->cd($filterDirectory . $version . DS . $result[$Model->alias]['dirname'] . DS);
  252. $basenames = $Folder->find($File->name() . '\..*');
  253. if (count($basenames) > 1) {
  254. $message = "MediaBehavior::beforeDelete - Ambiguous filename ";
  255. $message .= "`" . $File->name() . "` in `" . $Folder->pwd() . "`.";
  256. trigger_error($message, E_USER_NOTICE);
  257. continue;
  258. } elseif (!isset($basenames[0])) {
  259. continue;
  260. }
  261. $FilterFile = new File($Folder->pwd() . $basenames[0]);
  262. $FilterFile->delete();
  263. }
  264. $File->delete();
  265. return true;
  266. }
  267. /**
  268. * Parses instruction sets and invokes `Medium::make()` for a file
  269. *
  270. * @param Model $Model
  271. * @param string $file Path to a file relative to `baseDirectory` or an absolute path to a file
  272. * @return boolean
  273. */
  274. function make(&$Model, $file, $overwrite = false) {
  275. extract($this->settings[$Model->alias]);
  276. list($file, $relativeFile) = $this->_file($Model, $file);
  277. $relativeDirectory = DS . rtrim(dirname($relativeFile), '.');
  278. $name = Medium::name($file);
  279. $filter = Configure::read('Media.filter.' . strtolower($name));
  280. $hasCallback = method_exists($Model, 'beforeMake');
  281. foreach ($filter as $version => $instructions) {
  282. $directory = Folder::slashTerm($filterDirectory . $version . $relativeDirectory);
  283. $Folder = new Folder($directory, $createDirectory);
  284. if (!$Folder->pwd()) {
  285. $message = "MediaBehavior::make - Directory `{$directory}` ";
  286. $message .= "could not be created or is not writable. ";
  287. $message .= "Please check the permissions.";
  288. trigger_error($message, E_USER_WARNING);
  289. continue;
  290. }
  291. if ($hasCallback) {
  292. $process = compact('overwrite', 'directory', 'name', 'version', 'instructions');
  293. if ($Model->beforeMake($file, $process)) {
  294. continue;
  295. }
  296. }
  297. if (!$Medium = Medium::make($file, $instructions)) {
  298. $message = "MediaBehavior::make - Failed to make version `{$version}` ";
  299. $message .= "of file `{$file}`. ";
  300. trigger_error($message, E_USER_WARNING);
  301. continue;
  302. }
  303. $Medium->store($directory . basename($file), $overwrite);
  304. }
  305. return true;
  306. }
  307. /**
  308. * Retrieve (cached) metadata of a file
  309. *
  310. * @param Model $Model
  311. * @param string $file Path to a file relative to `baseDirectory` or an absolute path to a file
  312. * @param integer $level level of amount of info to add, `0` disable, `1` for basic, `2` for detailed info
  313. * @return mixed Array with results or false if file is not readable
  314. */
  315. function metadata(&$Model, $file, $level = 1) {
  316. if ($level < 1) {
  317. return array();
  318. }
  319. extract($this->settings[$Model->alias]);
  320. list($file,) = $this->_file($Model, $file);
  321. $File = new File($file);
  322. if (!$File->readable()) {
  323. return false;
  324. }
  325. $checksum = $File->md5(true);
  326. if (isset($this->__cached[$Model->alias][$checksum])) {
  327. $data = $this->__cached[$Model->alias][$checksum];
  328. }
  329. if ($level > 0 && !isset($data[1])) {
  330. $data[1] = array(
  331. 'size' => $File->size(),
  332. 'mime_type' => MimeType::guessType($File->pwd()),
  333. 'checksum' => $checksum,
  334. );
  335. }
  336. if ($level > 1 && !isset($data[2])) {
  337. $Medium = Medium::factory($File->pwd());
  338. if ($Medium->name === 'Audio') {
  339. $data[2] = array(
  340. 'artist' => $Medium->artist(),
  341. 'album' => $Medium->album(),
  342. 'title' => $Medium->title(),
  343. 'track' => $Medium->track(),
  344. 'year' => $Medium->year(),
  345. 'length' => $Medium->duration(),
  346. 'quality' => $Medium->quality(),
  347. 'sampling_rate' => $Medium->samplingRate(),
  348. 'bit_rate' => $Medium->bitRate(),
  349. );
  350. } elseif ($Medium->name === 'Image') {
  351. $data[2] = array(
  352. 'width' => $Medium->width(),
  353. 'height' => $Medium->height(),
  354. 'ratio' => $Medium->ratio(),
  355. 'quality' => $Medium->quality(),
  356. 'megapixel' => $Medium->megapixel(),
  357. );
  358. } elseif ($Medium->name === 'Text') {
  359. $data[2] = array(
  360. 'characters' => $Medium->characters(),
  361. 'syllables' => $Medium->syllables(),
  362. 'sentences' => $Medium->sentences(),
  363. 'words' => $Medium->words(),
  364. 'flesch_score' => $Medium->fleschScore(),
  365. 'lexical_density' => $Medium->lexicalDensity(),
  366. );
  367. } elseif ($Medium->name === 'Video') {
  368. $data[2] = array(
  369. 'title' => $Medium->title(),
  370. 'year' => $Medium->year(),
  371. 'length' => $Medium->duration(),
  372. 'width' => $Medium->width(),
  373. 'height' => $Medium->height(),
  374. 'ratio' => $Medium->ratio(),
  375. 'quality' => $Medium->quality(),
  376. 'bit_rate' => $Medium->bitRate(),
  377. );
  378. } else {
  379. $data[2] = array();
  380. }
  381. }
  382. for ($i = $level, $result = array(); $i > 0; $i--) {
  383. $result = array_merge($result, $data[$i]);
  384. }
  385. $this->__cached[$Model->alias][$checksum] = $data;
  386. return Set::filter($result);
  387. }
  388. /**
  389. * Checks if an alternative text is given only if a file is submitted
  390. *
  391. * @param unknown_type $Model
  392. * @param unknown_type $field
  393. * @return unknown
  394. */
  395. function checkRepresent(&$Model, $field) {
  396. if (!isset($Model->data[$Model->alias]['file'])) {
  397. return true;
  398. }
  399. $value = current($field); /* empty() limitation */
  400. return !empty($value);
  401. }
  402. /**
  403. * Returns relative and absolute path to a file
  404. *
  405. * @param Model $Model
  406. * @param string$file
  407. * @return array
  408. */
  409. function _file(&$Model, $file) {
  410. extract($this->settings[$Model->alias]);
  411. $file = str_replace(array('\\', '/'), DS, $file);
  412. if (!is_file($file)) {
  413. $file = ltrim($file, DS);
  414. $relativeFile = $file;
  415. $file = $baseDirectory . $file;
  416. } else {
  417. $relativeFile = str_replace($baseDirectory, null, $file);
  418. }
  419. return array($file, $relativeFile);
  420. }
  421. }
  422. ?>