SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/File.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13.  
  14. namespace Sprout\Helpers;
  15.  
  16. use Exception;
  17. use InvalidArgumentException;
  18.  
  19. use Kohana;
  20.  
  21. use karmabunny\pdb\Exceptions\RowMissingException;
  22.  
  23.  
  24. /**
  25.  * Methods for working with files, including images
  26.  */
  27. class File
  28. {
  29. /**
  30.   * Gets the details of a file using its id.
  31.   *
  32.   * Uses a prepared statement for speed when doing repeated queries.
  33.   *
  34.   * N.B. If the file entry doesn't exist, a reference to 'missing.png' is returned
  35.   *
  36.   * @param int $id The ID in the files table
  37.   * @return array Contains keys 'filename' and 'date_file_modified'
  38.   */
  39. public static function getDetails($id)
  40. {
  41. static $prepared_q = null;
  42.  
  43. if (!$prepared_q) {
  44. $q = "SELECT filename, date_file_modified FROM ~files WHERE id = ?";
  45. $prepared_q = Pdb::prepare($q);
  46. }
  47.  
  48. try {
  49. return Pdb::execute($prepared_q, [$id], 'row');
  50. } catch (RowMissingException $ex) {
  51. return ['filename' => 'missing.png', 'date_file_modified' => '1970-01-01 00:00:00'];
  52. }
  53. }
  54.  
  55.  
  56. /**
  57.   * For a given file, returns the filename to use to retrieve a resized version of the file
  58.   *
  59.   * @param string $original Original image name, e.g. 123_example.jpg
  60.   * @param string $size_name One of the 'file.image_transformations' config options, e.g. 'small'
  61.   * @param string $force_ext If set, extension is set to this
  62.   * @return string Name of resized file, e.g. 123_example.small.jpg
  63.   */
  64. static function getResizeFilename($original, $size_name, $force_ext = null)
  65. {
  66. $parts = explode('.', $original);
  67. $ext = array_pop($parts);
  68. $file_noext = implode('.', $parts);
  69.  
  70. if ($force_ext) {
  71. $ext = $force_ext;
  72. }
  73.  
  74. return "{$file_noext}.{$size_name}.{$ext}";
  75. }
  76.  
  77.  
  78. /**
  79.   * Gets the (final) extension from a file name, in lowercase
  80.   * @param string $filename Full filename, e.g. 'image.large.jpg', '/path/to/image.png'
  81.   * @return string Extension, excluding leading dot, e.g. 'jpg', 'png'
  82.   */
  83. static function getExt($filename)
  84. {
  85. $parts = explode('.', $filename);
  86. return strtolower(array_pop($parts));
  87. }
  88.  
  89.  
  90. /**
  91.   * Determines the file type from a file name by examining its extension
  92.   * @param string $filename The file name
  93.   * @return int One of the FileConstants::TYPE_* values, see {@see FileConstants}.
  94.   * If the type couldn't be determined, FileConstants::TYPE_OTHER is returned.
  95.   */
  96. static function getType($filename)
  97. {
  98. $ext = self::getExt($filename);
  99. foreach (FileConstants::$type_exts as $type => $exts) {
  100. if (in_array($ext, $exts)) return $type;
  101. }
  102. return FileConstants::TYPE_OTHER;
  103. }
  104.  
  105.  
  106. /**
  107.   * For a given file, returns the name without an ext
  108.   *
  109.   * @param string Full filename
  110.   * @return string Base part of filename
  111.   **/
  112. static function getNoext($original)
  113. {
  114. $parts = explode('.', $original);
  115. array_pop($parts);
  116. return implode('.', $parts);
  117. }
  118.  
  119.  
  120. /**
  121.   * Converts a file size, in bytes, into a human readable form (with trailing kb, mb, etc)
  122.   *
  123.   * @param int $size Size in bytes
  124.   * @return string
  125.   **/
  126. static function humanSize($size)
  127. {
  128. static $types = array(' bytes', ' kb', ' mb', ' gb', ' tb');
  129.  
  130. $type = 0;
  131. while ($size > 1024) {
  132. $size /= 1024;
  133. $type++;
  134. if ($type > 5) break;
  135. }
  136.  
  137. return round($size, 1) . $types[$type];
  138. }
  139.  
  140.  
  141. /**
  142.   * Make a filename sane - strip lots of characters which create problems
  143.   *
  144.   * @param string $filename Filename which may or may not already be sane
  145.   * @return string Sane filename
  146.   **/
  147. static function filenameMakeSane($filename)
  148. {
  149. $parts = explode('.', $filename);
  150.  
  151. $ext = '';
  152. if (count($parts) > 1) $ext = array_pop($parts);
  153. $filename = implode('', $parts);
  154.  
  155. $filename = preg_replace('![/\\\]!', '', $filename);
  156. $filename = preg_replace('/\s/', '_', $filename);
  157. $filename = strtolower($filename);
  158. $filename = preg_replace('/[^-_a-z0-9.]/', '', $filename);
  159. $filename = preg_replace('/[-_]{2,}/', '_', $filename);
  160. $filename = trim($filename, '_');
  161.  
  162. if ($filename == '') $filename = time();
  163.  
  164. if ($ext) $filename .= '.' . strtolower($ext);
  165.  
  166. return $filename;
  167. }
  168.  
  169.  
  170. /**
  171.   * Return the backend library to use for many file operations
  172.   *
  173.   * @return FilesBackend
  174.   **/
  175. private static function backend()
  176. {
  177. static $backend;
  178.  
  179. if ($backend === null) {
  180. $backend = new FilesBackendDirectory();
  181. }
  182.  
  183. return $backend;
  184. }
  185.  
  186.  
  187. /**
  188.   * Returns the public URL for a given file. Does not include domain.
  189.   *
  190.   * @param string $filename The name of the file in the repository
  191.   * @return string
  192.   **/
  193. public static function url($filename)
  194. {
  195. return self::backend()->absUrl($filename);
  196. }
  197.  
  198.  
  199. /**
  200.   * Returns the relative public URL for a given file.
  201.   * Doesn't contain ROOT/ or domain. Use for content areas.
  202.   *
  203.   * @param string $filename The name of the file in the repository
  204.   * @return string
  205.   **/
  206. public static function relUrl($filename)
  207. {
  208. return self::backend()->relUrl($filename);
  209. }
  210.  
  211.  
  212. /**
  213.   * Returns the public URL for a given file, including domain.
  214.   *
  215.   * @param string $filename The name of the file in the repository
  216.   * @return string
  217.   **/
  218. public static function absUrl($filename)
  219. {
  220. return self::backend()->absUrl($filename);
  221. }
  222.  
  223.  
  224. /**
  225.   * Returns the relative URL for a dynamically resized image.
  226.   *
  227.   * Size formatting is as per {@see File::parseSizeString}, e.g. c400x300
  228.   *
  229.   * @param int $id ID or filename from record in files table
  230.   * @param string $size A code as per {@see File::parseSizeString}
  231.   * @return string HTML-safe relative URL, e.g. file/resize/c400x300/123_example.jpg
  232.   */
  233. public static function resizeUrl($id, $size)
  234. {
  235. return self::backend()->resizeUrl($id, $size);
  236. }
  237.  
  238.  
  239. /**
  240.   * Gets the relative URL for a fixed or dynamically resized image
  241.   *
  242.   * @param int $id ID or filename from record in files table
  243.   * @param string $size_name The size you want, e.g. 'small', 'banner', 'c100x100', etc.
  244.   * The value can either be a size name from the 'file.image_transformations' config option,
  245.   * or be a resize code as per {@see File::parseSizeString}
  246.   * @param string $force_ext Force the ext to a specific value, e.g. 'jpg'
  247.   * @param bool $create_if_missing For numeric size names (e.g. 'c100x100'), causes a resize for any missing files
  248.   * @return string File URL, e.g. 'file/download/123/small' or 'files/123_test.c100x100.jpg'
  249.   */
  250. public static function sizeUrl($id, $size_name, $force_ext = null, $create_if_missing = false)
  251. {
  252. if (preg_match('/^[0-9]+$/', $id)) {
  253. if (preg_match('/^[a-z_]+$/', $size_name)) {
  254. return "file/download/{$id}/{$size_name}";
  255. }
  256. $file_details = File::getDetails($id);
  257. $filename = $file_details['filename'];
  258. } else {
  259. $filename = $id;
  260. if (!self::exists($filename)) {
  261. try {
  262. $filename = File::lookupReplacementName($filename);
  263. } catch (Exception $ex) {
  264. return 'files/missing.png';
  265. }
  266. }
  267. }
  268.  
  269. $url = File::getResizeFilename($filename, $size_name, $force_ext);
  270.  
  271. $pattern = '/^[crm][0-9]+x[0-9]+(?:-[lcr][tcb](?:~[0-9]+)?)?$/';
  272. if ($create_if_missing and preg_match($pattern, $size_name) and !File::exists($url)) {
  273. File::createSize($filename, $size_name, $force_ext);
  274. }
  275.  
  276. return File::relUrl($url);
  277. }
  278.  
  279.  
  280. /**
  281.   * Returns TRUE if the file exists, and FALSE if it does not
  282.   *
  283.   * @param string $filename The name of the file in the repository. Deprecated: an ID value is also accepted in order
  284.   * to support older modules; such modules need to be updated to avoid an extra database lookup.
  285.   * @return bool TRUE if the file exists, and FALSE if it does not
  286.   **/
  287. public static function exists($filename)
  288. {
  289. if (preg_match('/^[0-9]+$/', $filename)) {
  290. $id = $filename;
  291. try {
  292. $filename = Pdb::q('SELECT filename FROM ~files WHERE id = ?', [$id], 'val');
  293. } catch (RowMissingException $ex) {
  294. return false;
  295. }
  296. }
  297. return self::backend()->exists($filename);
  298. }
  299.  
  300.  
  301. /**
  302.   * Returns the size, in bytes, of the specified file
  303.   *
  304.   * @param string $filename The name of the file in the repository
  305.   * @return int File size in bytes
  306.   **/
  307. public static function size($filename)
  308. {
  309. if (!self::exists($filename)) {
  310. try {
  311. $filename = File::lookupReplacementName($filename);
  312. } catch (RowMissingException $ex) {
  313. // No problem, return original (broken) URL
  314. }
  315. }
  316.  
  317. return self::backend()->size($filename);
  318. }
  319.  
  320.  
  321. /**
  322.   * Returns the modified time, in unix timestamp format, of the specified file
  323.   *
  324.   * @param string $filename The name of the file in the repository
  325.   * @return int Modified time as a unix timestamp
  326.   **/
  327. public static function mtime($filename)
  328. {
  329. return self::backend()->mtime($filename);
  330. }
  331.  
  332.  
  333. /**
  334.   * Sets access and modification time of file
  335.   * @return bool True if successful
  336.   **/
  337. public static function touch($filename)
  338. {
  339. return self::backend()->touch($filename);
  340. }
  341.  
  342.  
  343. /**
  344.   * Returns the size of an image, or false on failure.
  345.   *
  346.   * Output format is the same as getimagesize, but will be at a minimum:
  347.   * [0] => width, [1] => height, [2] => type
  348.   *
  349.   * @param string $filename The name of the file in the repository
  350.   * @return array
  351.   **/
  352. public static function imageSize($filename)
  353. {
  354. return self::backend()->imageSize($filename);
  355. }
  356.  
  357.  
  358. /**
  359.   * Delete a file
  360.   * If the file is an image, any resized variants (e.g. 'small', 'medium' etc.) are deleted too
  361.   * @param string $filename The name of the file in the repository, e.g. '123_some_image.jpg'
  362.   * @return bool True if the deletion of the main file succeeded
  363.   */
  364. public static function delete($filename)
  365. {
  366. File::deleteCache($filename);
  367. $ext = File::getExt($filename);
  368. $base = File::getNoExt($filename);
  369. $transforms = Kohana::config('file.image_transformations');
  370. foreach ($transforms as $type => $params) {
  371. self::backend()->delete("{$base}.{$type}.{$ext}");
  372. }
  373. return self::backend()->delete($filename);
  374. }
  375.  
  376.  
  377. /**
  378.   * Delete cached versions of a file
  379.   *
  380.   * @param string $filename The name of the file in the repository
  381.   **/
  382. public static function deleteCache($filename)
  383. {
  384. $filename = preg_replace('![^-_a-z0-9.]!', '', $filename);
  385.  
  386. $files = glob(APPPATH . 'cache/resize-*-' . $filename);
  387. foreach ($files as $f) {
  388. unlink($f);
  389. }
  390. }
  391.  
  392.  
  393. /**
  394.   * Returns all files which match the specified mask.
  395.   * I have a feeling this returns other sizes (e.g. .small) as well - which may not be ideal.
  396.   *
  397.   * @param string $mask Files to find. Supports wildcards such as * and ?
  398.   **/
  399. public static function glob($mask)
  400. {
  401. return self::backend()->glob($mask);
  402. }
  403.  
  404.  
  405. /**
  406.   * This is the equivalent of the php readfile function
  407.   *
  408.   * @param string $filename The name of the file in the repository
  409.   **/
  410. public static function readfile($filename)
  411. {
  412. return self::backend()->readfile($filename);
  413. }
  414.  
  415.  
  416. /**
  417.   * Returns file content as a string. Basically the same as file_get_contents
  418.   *
  419.   * @param string $filename The name of the file in the repository
  420.   * @return string $content The content
  421.   **/
  422. public static function getString($filename)
  423. {
  424. return self::backend()->getString($filename);
  425. }
  426.  
  427.  
  428. /**
  429.   * Saves file content as a string. Basically the same as file_put_contents
  430.   *
  431.   * @param string $filename The name of the file in the repository
  432.   * @param string $content The content
  433.   * @return bool True on success, false on failure
  434.   **/
  435. public static function putString($filename, $content)
  436. {
  437. return self::backend()->putString($filename, $content);
  438. }
  439.  
  440.  
  441. /**
  442.   * Saves file content from a stream. Basically just fopen/stream_copy_to_stream/fclose
  443.   *
  444.   * @param string $filename The name of the file in the repository
  445.   * @param resource $stream The stream to copy content from
  446.   * @return bool True on success, false on failure
  447.   **/
  448. public static function putStream($filename, $stream)
  449. {
  450. return self::backend()->putStream($filename, $stream);
  451. }
  452.  
  453.  
  454. /**
  455.   * Saves file content from an existing file
  456.   *
  457.   * @param string $filename The name of the file in the repository
  458.   * @param string $existing The existing file on disk
  459.   * @return bool True on success, false on failure
  460.   **/
  461. public static function putExisting($filename, $existing)
  462. {
  463. return self::backend()->putExisting($filename, $existing);
  464. }
  465.  
  466.  
  467. /**
  468.   * Moves an uploaded file into the repository.
  469.   * Returns TRUE on success, FALSE on failure.
  470.   *
  471.   * @param string $src Source filename
  472.   * @param string $filename The name of the file in the repository
  473.   * @return bool True on success, false on failure
  474.   **/
  475. public static function moveUpload($src, $filename)
  476. {
  477. return self::backend()->moveUpload($src, $filename);
  478. }
  479.  
  480.  
  481. /**
  482.   * Create a copy of the file in a temporary directory.
  483.   * Don't forget to File::destroy_local_copy($temp_filename) when you're done!
  484.   *
  485.   * @param string $filename The file to copy into a temporary location
  486.   * @return string Temp filename or NULL on error
  487.   **/
  488. public static function createLocalCopy($filename)
  489. {
  490. return self::backend()->createLocalCopy($filename);
  491. }
  492.  
  493.  
  494. /**
  495.   * Remove a local copy of a file
  496.   * Call this once you're done with the local copy
  497.   *
  498.   * @param string $temp_filename The filename returned by createLocalCopy
  499.   **/
  500. public static function cleanupLocalCopy($temp_filename)
  501. {
  502. return self::backend()->cleanupLocalCopy($temp_filename);
  503. }
  504.  
  505.  
  506. /**
  507.   * Searches the whole database to find all records in all columns
  508.   * which contain a given filename.
  509.   *
  510.   * The search looks in VARCHAR columns with more than 200 chars (exact match)
  511.   * and in TEXT columns (contains match)
  512.   *
  513.   * Return value is an array of matches, in the format:
  514.   * [0] => table
  515.   * [1] => record id
  516.   * [2] => record name, if available
  517.   *
  518.   * @param string $filename The name of the file to search
  519.   **/
  520. public static function findUsage($filename)
  521. {
  522. $pf = Pdb::prefix();
  523.  
  524. $all_params = [
  525. 'filename' => $filename,
  526. 'like_filename' => Pdb::likeEscape($filename),
  527. ];
  528.  
  529.  
  530. $size_names = Kohana::config('file.image_transformations');
  531. foreach ($size_names as $size_name => $transform) {
  532. Pdb::validateIdentifier($size_name);
  533. $sizes[] = $size_name;
  534. $all_params["resize_{$size_name}"] = Pdb::likeEscape(File::getResizeFilename($filename, $size_name));
  535. }
  536.  
  537. // Tables to not show results for
  538. $badtables = array(
  539. $pf . 'files',
  540. $pf . 'history_items',
  541. $pf . 'cronjobs',
  542. $pf . 'workerjobs',
  543. $pf . 'pages',
  544. $pf . 'page_revisions',
  545. $pf . 'page_widgets',
  546. $pf . 'exception_log',
  547. );
  548.  
  549. // Iterate the tables
  550. $q = "SHOW TABLE STATUS";
  551. $db_tables = Pdb::q($q, [], 'arr');
  552.  
  553. $queries = array();
  554. foreach ($db_tables as $tbl) {
  555. if (strpos($tbl['Name'], $pf) !== 0) continue;
  556. if (in_array($tbl['Name'], $badtables)) continue;
  557.  
  558. // Grab the columns
  559. $q = "SHOW COLUMNS FROM {$tbl['Name']}";
  560. $db_cols = Pdb::q($q, [], 'arr');
  561.  
  562. // Build a where clause
  563. $cols = [];
  564. $where = [];
  565. $params = [];
  566. foreach ($db_cols as $col) {
  567. if ($col['Field'] === 'id') $cols[] = 'id';
  568. if ($col['Field'] === 'name') $cols[] = 'name';
  569.  
  570. if (preg_match('/VARCHAR\(([0-9]+)\)/i', $col['Type'], $matches) and $matches[1] >= 200) {
  571. $where[] = "{$col['Field']} = :filename";
  572. if (!isset($params['filename'])) $params['filename'] = $all_params['filename'];
  573.  
  574. } else if (preg_match('/TEXT/i', $col['Type'])) {
  575. $where[] = "{$col['Field']} LIKE CONCAT('%', :like_filename, '%')";
  576. if (!isset($params['like_filename'])) $params['like_filename'] = $all_params['like_filename'];
  577.  
  578. foreach ($sizes as $size_name) {
  579. $param_name = "resize_{$size_name}";
  580. $where[] = "{$col['Field']} LIKE CONCAT('%', :{$param_name}, '%')";
  581. if (!isset($params[$param_name])) $params[$param_name] = $all_params[$param_name];
  582. }
  583. }
  584. }
  585.  
  586. if (count($cols) == 0 or count($where) == 0) continue;
  587.  
  588. $q = 'SELECT ' . implode(', ', $cols) . ' FROM ' . $tbl['Name'] . ' WHERE ' . implode(' OR ', $where);
  589. $queries[$tbl['Name']] = [$q, $params];
  590. }
  591.  
  592. // Spekky query for page revisions
  593. $where = [];
  594. $params = $all_params;
  595. unset($params['filename']);
  596.  
  597. $where[] = "widget.settings LIKE CONCAT('%', :like_filename, '%')";
  598. foreach ($sizes as $size_name) {
  599. $param_name = "resize_{$size_name}";
  600. $where[] = "widget.settings LIKE CONCAT('%', :{$param_name}, '%')";
  601. }
  602. $where[] = "page.banner LIKE CONCAT('%', :like_filename, '%')";
  603. $q = "SELECT DISTINCT page.id, page.name
  604. FROM ~page_revisions AS rev
  605. INNER JOIN ~page_widgets AS widget ON rev.id = widget.page_revision_id
  606. AND widget.area_id = 1 AND widget.type = 'RichText'
  607. INNER JOIN ~pages AS page ON rev.page_id = page.id
  608. WHERE (" . implode(' OR ', $where) . ')
  609. AND rev.status = :live';
  610. $params['live'] = 'live';
  611. $queries['sprout_pages'] = [$q, $params];
  612.  
  613. // Spekky query for gallery images
  614. if (Sprout::moduleInstalled('galleries2')) {
  615. $where = [];
  616. $params = $all_params;
  617. unset($params['filename']);
  618. $where[] = 'f.filename LIKE :like_filename';
  619. foreach ($sizes as $size_name) {
  620. $param_name = "resize_{$size_name}";
  621. $where[] = "f.filename LIKE :{$param_name}";
  622. }
  623.  
  624. $q = "SELECT g.id, g.name
  625. FROM ~galleries AS g
  626. INNER JOIN ~gallery_sources AS src ON src.gallery_id = g.id AND src.type = :type_image
  627. INNER JOIN ~files_cat_join AS joiner ON joiner.cat_id = src.category
  628. INNER JOIN ~files AS f ON joiner.file_id = f.id
  629. WHERE (" . implode(' OR ', $where) . ')';
  630. $params['type_image'] = GalleryConstants::SOURCE_FILES_IMAGE;
  631. $queries['sprout_galleries'] = [$q, $params];
  632. }
  633.  
  634. // Run the queries
  635. ksort($queries);
  636. $output = array();
  637. foreach ($queries as $table => $q_and_params) {
  638. list($q, $params) = $q_and_params;
  639. $res = Pdb::q($q, $params, 'pdo');
  640.  
  641. // Save results
  642. foreach ($res as $row) {
  643. $output[] = array(
  644. substr($table, strlen($pf)),
  645. $row['id'],
  646. (isset($row['name']) ? $row['name'] : 'Record #' . $row['id']),
  647. );
  648. }
  649. $res->closeCursor();
  650. }
  651.  
  652. return $output;
  653. }
  654.  
  655.  
  656. /**
  657.   * Return the mimetype for a given filename.
  658.   *
  659.   * Only uses the extension - doesn't actually check the file
  660.   * If you need deep checking, take a look at {@see File::checkFileContentsExtension}
  661.   * If the extension is unrecognised, returns 'application/octet-stream'.
  662.   *
  663.   * @param string $filename
  664.   * @return string E.g. 'image/png'
  665.   **/
  666. public static function mimetype($filename)
  667. {
  668. $ext = File::getExt($filename);
  669. return (isset(Constants::$mimetypes[$ext]) ? Constants::$mimetypes[$ext] : 'application/octet-stream');
  670. }
  671.  
  672.  
  673. /**
  674.   * Checks file contents match the extension
  675.   *
  676.   * @param $filename string The full path/filename of the file to check
  677.   * @param $ext string The supplied file extension
  678.   * @return boolean True if the file matches, false if it doesn't
  679.   * @return null If there isn't a check for the supplied extension
  680.   */
  681. public static function checkFileContentsExtension($filename, $ext)
  682. {
  683. $ext = strtolower(trim($ext));
  684.  
  685. switch ($ext) {
  686. case 'jpg':
  687. case 'jpeg':
  688. case 'jpe':
  689. case 'jif':
  690. case 'jfif':
  691. case 'jfi':
  692. $size = getimagesize($filename);
  693. return ($size[2] == IMAGETYPE_JPEG);
  694.  
  695. case 'png':
  696. $size = getimagesize($filename);
  697. return ($size[2] == IMAGETYPE_PNG);
  698.  
  699. case 'gif':
  700. $size = getimagesize($filename);
  701. return ($size[2] == IMAGETYPE_GIF);
  702.  
  703. case 'pdf':
  704. $fp = fopen($filename, 'r');
  705. $magic = fread($fp, 4);
  706. fclose($fp);
  707. return ($magic == '%PDF');
  708. }
  709.  
  710. return null;
  711. }
  712.  
  713.  
  714. /**
  715.   * Get the content-type of a file using magic mime.
  716.   *
  717.   * This is _NOT_ limited to the whitelist of mime types described in the
  718.   * Constants. Use this with care.
  719.   *
  720.   * Note mime_content_type() inspects file contents and can't always
  721.   * determine css/js files correctly, this is a hack fix for that.
  722.   *
  723.   * https://stackoverflow.com/a/17736797/7694753
  724.   *
  725.   * @param string $path
  726.   * @return string|null null if unknown
  727.   */
  728. public static function mimetypeExtended($path)
  729. {
  730. $extension = pathinfo($path, PATHINFO_EXTENSION);
  731.  
  732. switch($extension) {
  733. case 'css':
  734. return 'text/css; charset=UTF-8';
  735.  
  736. case 'js':
  737. return 'application/javascript; charset=UTF-8';
  738.  
  739. case 'svg':
  740. return 'image/svg+xml; charset=UTF-8';
  741. }
  742.  
  743. $info = finfo_open(FILEINFO_MIME);
  744. if (!$info) return null;
  745.  
  746. return finfo_file($info, $path) ?: null;
  747. }
  748.  
  749.  
  750. /**
  751.   * Prompts a user to download a file, and terminates the script
  752.   * Sets all the right headers and stuff, doesn't set caching/expires/etc headers though.
  753.   *
  754.   * @param string $filename The name of the file in the repository
  755.   * @param string $download_name An optional alternate name to provide to the user
  756.   **/
  757. public static function download($filename, $download_name = null)
  758. {
  759. $size = File::size($filename);
  760. $mime = File::mimetype($filename);
  761.  
  762. // Set some general headers
  763. header('Content-type: ' . $mime);
  764. header('Content-length: ' . $size);
  765. header('Content-disposition: attachment; filename="' . addslashes($download_name ? $download_name : $filename) . '"');
  766.  
  767. // MSIE needs "public" when under SSL - http://support.microsoft.com/kb/316431
  768. header('Pragma: public');
  769. header('Cache-Control: public, max-age=1');
  770.  
  771. Kohana::closeBuffers();
  772. File::readfile($filename);
  773. exit();
  774. }
  775.  
  776.  
  777. /**
  778.   * Parse the size string used in file/resize and some helpers.
  779.   *
  780.   * Syntax: [crm]{number}x{number}(-[lcr][tcb])(~{number})
  781.   * Type Width Height Crop X Y Quality
  782.   *
  783.   * Returns an array.
  784.   * [0] type, either 'r', 'c' or 'm'
  785.   * r = resize, up or down, try to fill the area requested
  786.   * c = crop, resulting file will always be the width and height requested
  787.   * m = resize down only
  788.   * [1] width
  789.   * [2] height
  790.   * [3] x position, 'left', 'center' or 'right'
  791.   * [4] y position, 'top', 'center' or 'bottom'
  792.   * [5] jpeg quality, 0 = worst, 100 = best
  793.   *
  794.   * Returns an empty array on error (so you can use list() safely)
  795.   *
  796.   * @param $str Size string
  797.   * @return array
  798.   **/
  799. public static function parseSizeString($str)
  800. {
  801. $result = preg_match('/^([crm])([0-9]+)x([0-9]+)(?:-([lcr])([tcb]))?(?:~([0-9]+))?$/', $str, $matches);
  802. if (! $result) return array();
  803. array_shift($matches);
  804.  
  805. $matches[1] = (int) $matches[1];
  806. $matches[2] = (int) $matches[2];
  807.  
  808. if (empty($matches[3])) {
  809. $matches[3] = 'center';
  810. $matches[4] = 'center';
  811. } else {
  812. if ($matches[3] == 'l') $matches[3] = 'left';
  813. if ($matches[3] == 'c') $matches[3] = 'center';
  814. if ($matches[3] == 'r') $matches[3] = 'right';
  815. if ($matches[4] == 't') $matches[4] = 'top';
  816. if ($matches[4] == 'c') $matches[4] = 'center';
  817. if ($matches[4] == 'b') $matches[4] = 'bottom';
  818. }
  819.  
  820. if (empty($matches[5])) $matches[5] = null;
  821.  
  822. return $matches;
  823. }
  824.  
  825.  
  826. /**
  827.   * Create a resized version of the specified file at a given size.
  828.   *
  829.   * The size is specified the same as the file/resize method (rXXXxYYY or cXXXxYYY)
  830.   * The output filename will be basename.size.ext
  831.   *
  832.   * The files can be used with `size_url` or `get_resize_filename` on the front-end.
  833.   *
  834.   * @param string $filename The original filename
  835.   * @param string $size The size in on-the-fly resize format
  836.   * @param string $force_ext Force a different ext on save, such as jpg for banners
  837.   * @return bool True on success, false on failure
  838.   **/
  839. public static function createSize($filename, $size, $force_ext = null)
  840. {
  841. if (! File::exists($filename)) {
  842. return false;
  843. }
  844.  
  845. $temp_filename = File::createLocalCopy($filename);
  846. if (! $temp_filename) {
  847. return false;
  848. }
  849.  
  850. $img = new Image($temp_filename);
  851.  
  852. // Determine the size
  853. list($type, $width, $height, $crop_x, $crop_y, $quality) = File::parseSizeString($size);
  854.  
  855. if ($type == 'm') {
  856. // Max size
  857. $file_size = File::imageSize($filename);
  858.  
  859. if ($width == 0) $width = PHP_INT_MAX;
  860. if ($height == 0) $height = PHP_INT_MAX;
  861.  
  862. if ($file_size[0] > $width or $file_size[1] > $height) {
  863. $img->resize($width, $height);
  864. }
  865.  
  866. } else if ($type == 'r') {
  867. // Resize
  868. $img->resize($width, $height);
  869.  
  870. } else if ($type == 'c') {
  871. // Crop
  872. if ($width / $img->width > $height / $img->height) {
  873. $master = Image::WIDTH;
  874. } else {
  875. $master = Image::HEIGHT;
  876. }
  877.  
  878. $img->resize($width, $height, $master);
  879. $img->crop($width, $height, $crop_y, $crop_x);
  880.  
  881. } else {
  882. // What?
  883. File::cleanupLocalCopy($temp_filename);
  884. throw new Exception('Incorrect resize type');
  885. }
  886.  
  887. if ($quality) {
  888. $img->quality($quality);
  889. }
  890.  
  891. $sized_filename = self::getResizeFilename($filename, $size, $force_ext);
  892.  
  893. $result = $img->save();
  894. if (! $result) return false;
  895.  
  896. $result = File::putExisting($sized_filename, $temp_filename);
  897. if (! $result) return false;
  898.  
  899. File::cleanupLocalCopy($temp_filename);
  900.  
  901. $result = Replication::postFileUpdate($sized_filename);
  902. if (! $result) return false;
  903.  
  904. return true;
  905. }
  906.  
  907.  
  908. /**
  909.   * Will we have enough RAM to do the resizes?
  910.   *
  911.   * @throws Exception If we don't
  912.   * @param array $dimensions Dimensions of the original image; 0 = width, 1 => height
  913.   * @return void
  914.   */
  915. public static function calculateResizeRam(array $dimensions)
  916. {
  917. $origin_ram = $dimensions[0] * $dimensions[1] * 4;
  918. $memory_limit = Sprout::getMemoryLimit();
  919.  
  920. $sizes = Kohana::config('file.image_transformations');
  921. foreach ($sizes as $size_name => $transform) {
  922. $size_ram = 0;
  923. foreach ($transform as $t) {
  924. $size_ram += $t->estimateRamRequirement();
  925. }
  926. $total_ram_req = $origin_ram + $size_ram;
  927.  
  928. if ($total_ram_req > $memory_limit) {
  929. $total_ram_req = str_replace('&nbsp;', ' ', File::humanSize($total_ram_req));
  930. $memory_limit = str_replace('&nbsp;', ' ', File::humanSize($memory_limit));
  931.  
  932. throw new Exception(
  933. "Unable to create size '{$size_name}'; expected RAM requirements "
  934. . "of {$total_ram_req} exceeds memory limit of {$memory_limit}"
  935. );
  936. }
  937. }
  938. }
  939.  
  940.  
  941. /**
  942.   * Create default image sizes as per the config parameter 'file.image_transformations'
  943.   *
  944.   * The transformed files get saved onto the server.
  945.   * If any of the transformations in a transform-group fails,
  946.   * the whole group will fail and the file will not be saved.
  947.   *
  948.   * @throw Exception
  949.   * @param string $filename The file to create sizes for
  950.   * @param string $specific_size Optional parameter to process only a single size
  951.   * @return void
  952.   */
  953. public static function createDefaultSizes($filename, $specific_size = null)
  954. {
  955. $parts = explode('.', $filename);
  956. $ext = array_pop($parts);
  957. $file_noext = implode('.', $parts);
  958.  
  959. $sizes = Kohana::config('file.image_transformations');
  960.  
  961. if ($specific_size) {
  962. if (!isset($sizes[$specific_size])) {
  963. throw new Exception('Invalid param $specific_size; size doesn\'t exist.');
  964. }
  965.  
  966. $sizes = array($specific_size => $sizes[$specific_size]);
  967. }
  968.  
  969. // Look up image in DB and see if it needs author attribution
  970. $q = "SELECT author, embed_author
  971. FROM ~files
  972. WHERE filename = ?";
  973. $row = Pdb::q($q, [$filename], 'row');
  974. if ($row['author'] and $row['embed_author']) {
  975. $embed_text = $row['author'];
  976. } else {
  977. $embed_text = false;
  978. }
  979.  
  980. foreach ($sizes as $size_name => $transform) {
  981. $temp_filename = File::createLocalCopy($filename);
  982. if (!$temp_filename) {
  983. throw new Exception('Unable to create temporary copy for processing');
  984. }
  985.  
  986. $img = new Image($temp_filename);
  987.  
  988. $resize_filename = "{$file_noext}.{$size_name}.{$ext}";
  989.  
  990. // Do the transforms
  991. $width = $height = 0;
  992. foreach ($transform as $t) {
  993. $res = $t->transform($img);
  994.  
  995. if ($t instanceof ResizeImageTransform) {
  996. $dims = $t->getDimensions();
  997. $width = $dims['width'];
  998. $height = $dims['height'];
  999. }
  1000.  
  1001. // If an individual transform fails,
  1002. // cancel the transforming for this group
  1003. // The other transform groups will still be processed though
  1004. if ($res == false) {
  1005. File::cleanupLocalCopy($temp_filename);
  1006. continue 2;
  1007. }
  1008. }
  1009.  
  1010. if ($embed_text) $img->addText($embed_text);
  1011.  
  1012. $result = $img->save();
  1013. if (! $result) {
  1014. throw new Exception('Save of new image failed');
  1015. }
  1016.  
  1017. // Import temp file into media repo
  1018. $result = File::putExisting($resize_filename, $temp_filename);
  1019. if (! $result) {
  1020. throw new Exception('Image copy of new file into repository failed');
  1021. }
  1022.  
  1023. File::cleanupLocalCopy($temp_filename);
  1024. }
  1025. }
  1026.  
  1027.  
  1028. /**
  1029.   * Do post-processing after a file upload
  1030.   *
  1031.   * @throw Exception
  1032.   * @param string $filename The name of hte new file
  1033.   * @param int $file_id The ID of the new file
  1034.   * @param int $file_type The new file type - e.g. DOCUMENT or IMAGE; see FileConstants
  1035.   **/
  1036. public static function postUploadProcessing($filename, $file_id, $file_type)
  1037. {
  1038. $file_id = (int) $file_id;
  1039. $file_type = (int) $file_type;
  1040.  
  1041. $update_data = ['filename' => $filename];
  1042. Pdb::update('files', $update_data, ['id' => $file_id]);
  1043.  
  1044. switch ($file_type) {
  1045. case FileConstants::TYPE_DOCUMENT:
  1046. $ext = FileIndexing::getExt($filename);
  1047. if (FileIndexing::isExtSupported($ext)) {
  1048. $update_data = [];
  1049. $update_data['plaintext'] = FileIndexing::getPlaintext($filename, $ext);
  1050. Pdb::update('files', $update_data, ['id' => $file_id]);
  1051. }
  1052. break;
  1053.  
  1054. case FileConstants::TYPE_IMAGE:
  1055. File::createDefaultSizes($filename);
  1056. break;
  1057. }
  1058. }
  1059.  
  1060.  
  1061. /**
  1062.   * Generates a cropped, base-64 encoded thumbnail of an image
  1063.   * @param string $file_path Path to the original image
  1064.   * @param int $width Width to use for thumbnail
  1065.   * @param int $height Height to use for thumbnail
  1066.   * @return array Has the following keys:
  1067.   * 'encoded_thumbnail': Base-64 encoded thumbnail
  1068.   * 'original_width': width of the original image
  1069.   * 'original_height': height of the original image
  1070.   * @return false If file doesn't exist or can't be recognised as an image
  1071.   * @throws Exception if not enough RAM to generate thumbnail
  1072.   */
  1073. public static function base64Thumb($file_path, $width, $height)
  1074. {
  1075. $size = getimagesize($file_path);
  1076. if (!$size) return false;
  1077.  
  1078. list($w, $h) = $size;
  1079. $current_size = new ResizeImageTransform($w, $h);
  1080. $resize = new ResizeImageTransform($width, $height);
  1081. $resize_ram = $current_size->estimateRamRequirement();
  1082. $resize_ram += $resize->estimateRamRequirement();
  1083. if ($resize_ram > Sprout::getMemoryLimit()) {
  1084. throw new Exception('Not enough RAM to generate thumbnail');
  1085. }
  1086. return [
  1087. 'encoded_thumbnail' => Image::base64($file_path, $resize),
  1088. 'original_width' => $w,
  1089. 'original_height' => $h,
  1090. ];
  1091. }
  1092.  
  1093.  
  1094. /**
  1095.   * Checks the database for an updated URL path for a file.
  1096.   * I.e. a file which has been replaced by the admin 'replace file' tool.
  1097.   * @param string $filename Name of file, with no path, e.g. 123_image.jpg
  1098.   * @return string Updated URL (relative to root), e.g. 'files/123_new_image.png' or 'file/download/123'
  1099.   * @throws RowMissingException If no updated path was found
  1100.   * @throws InvalidArgumentException If linkspec in DB invalid
  1101.   */
  1102. public static function lookupReplacementUrl($filename)
  1103. {
  1104. $q = "SELECT destination
  1105. FROM ~redirects
  1106. WHERE path_exact = ?";
  1107. $dest_spec_json = Pdb::q($q, ['files/' . $filename], 'val');
  1108. $dest_spec = json_decode($dest_spec_json, true);
  1109. if ($dest_spec['class'] != '\\Sprout\\Helpers\\LinkSpecInternal') {
  1110. throw new InvalidArgumentException("Link spec doesn't match expected value");
  1111. }
  1112. return $dest_spec['data'];
  1113. }
  1114.  
  1115.  
  1116. /**
  1117.   * Checks the database for an updated name for a file.
  1118.   *
  1119.   * This only works for full-sized images, e.g. 123_example.jpg, not 123_example.small.jpg
  1120.   *
  1121.   * @param string $filename Name of file, with no path, e.g. 123_image.jpg
  1122.   * @return string Updated filename, e.g. '123_new_image.png'
  1123.   * @throws RowMissingException If no updated path was found
  1124.   * @throws InvalidArgumentException If linkspec in DB invalid
  1125.   */
  1126. public static function lookupReplacementName($filename)
  1127. {
  1128. $replacement = self::lookupReplacementUrl($filename);
  1129.  
  1130. if (preg_match('#^file/download/([0-9]+)#', $replacement)) {
  1131. $id = substr($replacement, strlen('file/download/'));
  1132. $file_details = self::getDetails($id);
  1133. return $file_details['filename'];
  1134. } else {
  1135. throw new InvalidArgumentException("Redirect target doesn't match expected value");
  1136. }
  1137. }
  1138.  
  1139.  
  1140. /**
  1141.   * Replaces a set of files to be stored in a single field; this acts as a backend for {@see Fb::chunkedUpload}
  1142.   *
  1143.   * Files are saved as '{prefix}{num}.{ext}', with 'num' starting at 1
  1144.   *
  1145.   * @param string $session_key Session key used for this file field, used for {@see FileUpload::verify}
  1146.   * @param string $field_name Name of the field used on the form, e.g. 'photos'
  1147.   * @param array $exts Allowed file extensions, e.g. ['jpg', 'jpeg', 'png', 'gif']
  1148.   * @param string $prefix The start of the new file name, e.g. 'user-gallery-123-'
  1149.   * @return array List of newly saved filenames
  1150.   */
  1151. public static function replaceSet($session_key, $field_name, $exts, $prefix)
  1152. {
  1153. $files_saved = [];
  1154. $i = 0;
  1155. while (isset($_POST[$field_name][$i])) {
  1156. try {
  1157. $src_file = FileUpload::verify($session_key, $field_name, $i, $exts);
  1158. if (!empty($src_file)) {
  1159. $ext = File::getExt($_POST[$field_name][$i]);
  1160. $dest = $prefix . ($i + 1) . '.' . $ext;
  1161. $res = File::moveUpload($src_file, $dest);
  1162.  
  1163. if (!empty($res)) {
  1164. $files_saved[] = $dest;
  1165. }
  1166. }
  1167. } catch (Exception $ex) {
  1168. }
  1169.  
  1170. ++$i;
  1171. }
  1172.  
  1173. $old_files = File::glob($prefix . '*');
  1174. foreach ($old_files as $file) {
  1175. if (!in_array($file, $files_saved)) {
  1176. File::delete($file);
  1177. }
  1178. }
  1179.  
  1180. return $files_saved;
  1181. }
  1182.  
  1183. }
  1184.  
  1185.