SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Controllers/FileController.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\Controllers;
  15.  
  16. use Exception;
  17.  
  18. use Kohana;
  19. use Kohana_404_Exception;
  20.  
  21. use Sprout\Helpers\AdminAuth;
  22. use Sprout\Helpers\File;
  23. use Sprout\Helpers\FileConstants;
  24. use Sprout\Helpers\Image;
  25. use Sprout\Helpers\Json;
  26. use Sprout\Helpers\Pdb;
  27. use Sprout\Helpers\Request;
  28. use Sprout\Helpers\Security;
  29. use Sprout\Helpers\Url;
  30. use Sprout\Helpers\View;
  31. use Sprout\Helpers\Sprout;
  32.  
  33.  
  34. /**
  35.  * Provides access to file and image data
  36.  */
  37. class FileController extends Controller
  38. {
  39.  
  40. /**
  41.   * On the fly image resizing
  42.   *
  43.   * The size parameter is the new size.
  44.   * The first character is taken to be the resize type, accepts 'r' or 'c' or 'm':
  45.   * Meaning 'r'esize, 'c'rop or 'm'ax resize (do not scale up).
  46.   * The width and height is specified width . 'x' . height (e.g. 200x100)
  47.   **/
  48. public function resize($size, $filename)
  49. {
  50. $filename = str_replace('/', '', $filename);
  51.  
  52. $cache_hit = $cache_filename = false;
  53. if (is_writable(APPPATH . "cache") and @$_GET['force'] != 1) {
  54. $cache_filename = APPPATH . "cache/resize-{$size}-{$filename}";
  55. }
  56.  
  57. // 404
  58. $modified = File::mtime($filename);
  59. if ($modified === false) {
  60. throw new Kohana_404_Exception($filename);
  61. }
  62.  
  63. // Prevent browser using cached image if it has been deleted and needs re-creation
  64. if (!file_exists($cache_filename)) $modified = PHP_INT_MAX;
  65.  
  66. // If-Modified-Since
  67. $expires = 60 * 60 * 48;
  68. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  69. $since = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
  70. if ($modified <= $since) {
  71. header('HTTP/1.0 304 Not Modified');
  72. header('Pragma: public');
  73. header("Cache-Control: store, cache, maxage={$expires}");
  74. header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');
  75. return;
  76. }
  77. }
  78.  
  79. // Look up image in DB and see if it needs author attribution
  80. $q = "SELECT author, embed_author
  81. FROM ~files
  82. WHERE filename = ?
  83. LIMIT 1";
  84. $rows = Pdb::q($q, [$filename], 'arr');
  85. $row = Sprout::iterableFirstValue($rows);
  86. if (!empty($row['author']) and $row['embed_author']) {
  87. $embed_text = $row['author'];
  88. } else {
  89. $embed_text = false;
  90. }
  91.  
  92. $original = false;
  93. $temp_filename = false;
  94. if ($cache_filename and @filemtime($cache_filename) >= $modified) {
  95. $cache_hit = true;
  96.  
  97. } else {
  98. Security::serverKeyVerify(['filename' => $filename, 'size' => $size], @$_GET['s']);
  99.  
  100. $temp_filename = File::createLocalCopy($filename);
  101. if (! $temp_filename) throw new Exception('Unable to create temporary file');
  102.  
  103. // Resizing, etc
  104. $img = new Image($temp_filename);
  105.  
  106. $parsed_size = File::parseSizeString($size);
  107. if (count($parsed_size) < 5) {
  108. File::cleanupLocalCopy($temp_filename);
  109. throw new Exception('Invalid image resize parameters');
  110. }
  111.  
  112. list($type, $width, $height, $crop_x, $crop_y, $quality) = $parsed_size;
  113.  
  114. $size_limits = Kohana::config('image.max_size');
  115.  
  116. if ($width > $size_limits['width'] or $height > $size_limits['height']) {
  117. File::cleanupLocalCopy($temp_filename);
  118. throw new Exception('Image dimensions exceed the maximum limit.');
  119. }
  120.  
  121. if ($type == 'm') {
  122. // Max size
  123. $file_size = File::imageSize($filename);
  124.  
  125. if ($width == 0) $width = PHP_INT_MAX;
  126. if ($height == 0) $height = PHP_INT_MAX;
  127.  
  128. if ($file_size[0] > $width or $file_size[1] > $height) {
  129. $img->resize($width, $height);
  130. if ($embed_text) $img->addText($embed_text);
  131. } else {
  132. $original = true;
  133. }
  134.  
  135. } else if ($type == 'r') {
  136. // Resize
  137. $img->resize($width, $height);
  138. $resize_dims = $img->calcResizeDims($width, $height, Image::AUTO);
  139. if ($embed_text) $img->addText($embed_text);
  140.  
  141. } else if ($type == 'c') {
  142. // Crop
  143. if ($width / $img->width > $height / $img->height) {
  144. $master = Image::WIDTH;
  145. } else {
  146. $master = Image::HEIGHT;
  147. }
  148.  
  149. // Determine orientation (portrait/square/landscape/panorama)
  150. $ratio = $width / $height;
  151. $orientation = 'panorama';
  152. foreach (FileConstants::$image_ratios as $orient_name => $orient_ratio) {
  153. if ($ratio <= $orient_ratio) {
  154. $orientation = $orient_name;
  155. break;
  156. }
  157. }
  158.  
  159. // Calculate crop position based on focus, if specified
  160. $q = "SELECT focal_points
  161. FROM ~files
  162. WHERE filename = ?
  163. LIMIT 1";
  164. $res = Pdb::q($q, [$filename], 'arr');
  165. $focal_points = @json_decode($res[0]['focal_points'], true);
  166.  
  167. if (isset($focal_points[$orientation])) {
  168. $point = $focal_points[$orientation];
  169. } else {
  170. $point = @$focal_points['default'];
  171. }
  172.  
  173. @list($x, $y) = $point;
  174. if ($x > 0 and $y > 0) {
  175. $full_dims = File::imageSize($filename);
  176.  
  177. if ($master == Image::WIDTH) {
  178. $scale = $width / $img->width;
  179. } else {
  180. $scale = $height / $img->height;
  181. }
  182.  
  183. $scaled_x = round($x * $scale);
  184. $scaled_y = round($y * $scale);
  185.  
  186. // Put focal point as close to center of crop position as possible
  187. if ($master == Image::WIDTH) {
  188. $crop_y = $scaled_y - round($height / 2);
  189. if ($crop_y < 0) $crop_y = 0;
  190.  
  191. if ($crop_y + $height > $img->height * $scale) {
  192. $crop_y = floor($img->height * $scale) - $height;
  193. }
  194. } else {
  195. $crop_x = $scaled_x - round($width / 2);
  196. if ($crop_x < 0) $crop_x = 0;
  197.  
  198. if ($crop_x + $width > $img->width * $scale) {
  199. $crop_x = floor($img->width * $scale) - $width;
  200. }
  201. }
  202. }
  203.  
  204. $img->resize($width, $height, $master);
  205. $img->crop($width, $height, $crop_y, $crop_x);
  206. if ($embed_text) $img->addText($embed_text);
  207.  
  208. } else {
  209. // What?
  210. File::cleanupLocalCopy($temp_filename);
  211. throw new Exception('Incorrect resize type');
  212. }
  213.  
  214. if ($quality) {
  215. $img->quality($quality);
  216. }
  217.  
  218. if ($cache_filename) {
  219. if ($img->save($cache_filename, 0644, true)) $cache_hit = true;
  220. }
  221. }
  222.  
  223. // Content-type
  224. $parts = explode('.', $filename);
  225. $ext = array_pop($parts);
  226. $mime = array(
  227. 'gif' => 'image/gif',
  228. 'jpg' => 'image/jpeg',
  229. 'jpeg' => 'image/jpeg',
  230. 'png' => 'image/png',
  231. );
  232. $mime = $mime[$ext];
  233. if (! $mime) $mime = 'application/octet-stream';
  234.  
  235. // Headers
  236. header('Pragma: public');
  237. header('Content-type: ' . $mime);
  238. header("Cache-Control: store, cache, maxage={$expires}");
  239. header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');
  240. header('Last-modified: ' . gmdate('D, d M Y H:i:s', $modified) . ' GMT');
  241.  
  242. // Image
  243. if ($original) {
  244. header('Content-length: ' . File::size($filename));
  245. File::readfile($filename);
  246.  
  247. } else if ($cache_hit) {
  248. header('Content-length: ' . ((int)@filesize($cache_filename)));
  249. readfile($cache_filename);
  250.  
  251. } else {
  252. $img->render();
  253. }
  254.  
  255. if ($temp_filename) File::cleanupLocalCopy($temp_filename);
  256. }
  257.  
  258.  
  259. /**
  260.   * Redirect to the resize url
  261.   * This allows JS code to use a common URL without needing to be aware of which FilesBackend is in use
  262.   *
  263.   * @param string $size The size you want, e.g. 'c100x100'
  264.   * @param string $filename Original file
  265.   **/
  266. public function redirectResize($size, $filename)
  267. {
  268. Url::redirect(str_replace('SITE/', '', File::resizeUrl($filename, $size)));
  269. }
  270.  
  271.  
  272. /**
  273.   * Outputs an audio player.
  274.   **/
  275. public function playAudio($filename)
  276. {
  277. if (Request::isAjax()) {
  278. $page_view = new View('skin/popup');
  279. } else {
  280. $page_view = new View('skin/inner');
  281. }
  282.  
  283. $view = new View('sprout/audio_player');
  284. $view->filename = File::url($filename);
  285.  
  286. $page_view->page_title = 'Audio player';
  287. $page_view->main_content = $view;
  288. $page_view->controller = 'file';
  289. $page_view->controller_name = $this->getCssClassName();
  290. echo $page_view->render();
  291. }
  292.  
  293.  
  294. /**
  295.   * Renders file contents for viewing or downloading
  296.   *
  297.   * @param int $id ID value from files table
  298.   * @param string $size One of the 'file.image_transformations' config options, e.g. 'small'
  299.   */
  300. public function download($id, $size = '')
  301. {
  302. $id = (int) $id;
  303.  
  304. $q = "SELECT filename FROM ~files WHERE id = ?";
  305. $filename = Pdb::q($q, [$id], 'val');
  306.  
  307. // Incorporate size name if specified, e.g. 'example.jpg' => 'example.small.jpg'
  308. if ($size != '') {
  309. if (!preg_match('/^[a-z_]+$/', $size)) {
  310. throw new Kohana_404_Exception($filename);
  311. }
  312. $filename = File::getResizeFilename($filename, $size);
  313. }
  314.  
  315. $path = DOCROOT . 'files/' . $filename;
  316.  
  317. $modified = File::mtime($filename);
  318. if ($modified === false) {
  319. throw new Kohana_404_Exception($filename);
  320. }
  321.  
  322. // If-Modified-Since
  323. $expires = 60 * 60 * 48;
  324. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  325. $since = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
  326. if ($modified <= $since) {
  327. header('HTTP/1.0 304 Not Modified');
  328. header('Pragma: public');
  329. header("Cache-Control: store, cache, maxage={$expires}");
  330. header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');
  331. return;
  332. }
  333. }
  334.  
  335. $mime_type = File::mimetype($filename);
  336. header('Pragma: public');
  337. header('Content-type: ' . $mime_type);
  338. header("Cache-Control: store, cache, maxage={$expires}");
  339. header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');
  340. header('Last-modified: ' . gmdate('D, d M Y H:i:s', $modified) . ' GMT');
  341. header('Content-length: ' . File::size($filename));
  342. Kohana::closeBuffers();
  343. File::readfile($filename);
  344. }
  345.  
  346.  
  347. /**
  348.   * Looks up filenames for a list of file IDs
  349.   *
  350.   * @post string ids Comma-separated list of IDs
  351.   * @return void Outputs JSON array [file id => file name]
  352.   */
  353. public function nameLookup()
  354. {
  355. AdminAuth::checkLogin();
  356.  
  357. $ids = preg_split('/, */', @$_POST['ids']);
  358. foreach ($ids as $key => &$id) {
  359. $id = (int) $id;
  360. if ($id <= 0) unset($ids[$key]);
  361. }
  362.  
  363. if (count($ids) == 0) Json::out([]);
  364.  
  365. $params = [];
  366. $where = Pdb::buildClause([['id', 'IN', $ids]], $params);
  367. $q = "SELECT id, filename
  368. FROM ~files
  369. WHERE {$where}";
  370. Json::out(Pdb::q($q, $params, 'map'));
  371. }
  372. }
  373.