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

/theme/image.php

https://gitlab.com/unofficial-mirrors/moodle
PHP | 324 lines | 200 code | 44 blank | 80 comment | 48 complexity | c90e945c5f8e36439bb999cef00b3127 MD5 | raw file
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * This file is responsible for serving the one theme and plugin images.
  18. *
  19. * @package core
  20. * @copyright 2009 Petr Skoda (skodak) {@link http://skodak.org}
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. // disable moodle specific debug messages and any errors in output,
  24. // comment out when debugging or better look into error log!
  25. define('NO_DEBUG_DISPLAY', true);
  26. // we need just the values from config.php and minlib.php
  27. define('ABORT_AFTER_CONFIG', true);
  28. require('../config.php'); // this stops immediately at the beginning of lib/setup.php
  29. if ($slashargument = min_get_slash_argument()) {
  30. $slashargument = ltrim($slashargument, '/');
  31. if (substr_count($slashargument, '/') < 3) {
  32. image_not_found();
  33. }
  34. if (strpos($slashargument, '_s/') === 0) {
  35. // Can't use SVG
  36. $slashargument = substr($slashargument, 3);
  37. $usesvg = false;
  38. } else {
  39. $usesvg = true;
  40. }
  41. // image must be last because it may contain "/"
  42. list($themename, $component, $rev, $image) = explode('/', $slashargument, 4);
  43. $themename = min_clean_param($themename, 'SAFEDIR');
  44. $component = min_clean_param($component, 'SAFEDIR');
  45. $rev = min_clean_param($rev, 'INT');
  46. $image = min_clean_param($image, 'SAFEPATH');
  47. } else {
  48. $themename = min_optional_param('theme', 'standard', 'SAFEDIR');
  49. $component = min_optional_param('component', 'core', 'SAFEDIR');
  50. $rev = min_optional_param('rev', -1, 'INT');
  51. $image = min_optional_param('image', '', 'SAFEPATH');
  52. $usesvg = (bool)min_optional_param('svg', '1', 'INT');
  53. }
  54. if (empty($component) or $component === 'moodle' or $component === 'core') {
  55. $component = 'core';
  56. }
  57. if (empty($image)) {
  58. image_not_found();
  59. }
  60. if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
  61. // exists
  62. } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) {
  63. // exists
  64. } else {
  65. image_not_found();
  66. }
  67. $candidatelocation = "$CFG->localcachedir/theme/$rev/$themename/pix/$component";
  68. $etag = sha1("$rev/$themename/$component/$image");
  69. if ($rev > 0) {
  70. if (file_exists("$candidatelocation/$image.error")) {
  71. // This is a major speedup if there are multiple missing images,
  72. // the only problem is that random requests may pollute our cache.
  73. image_not_found();
  74. }
  75. $cacheimage = false;
  76. if ($usesvg && file_exists("$candidatelocation/$image.svg")) {
  77. $cacheimage = "$candidatelocation/$image.svg";
  78. $ext = 'svg';
  79. } else if (file_exists("$candidatelocation/$image.png")) {
  80. $cacheimage = "$candidatelocation/$image.png";
  81. $ext = 'png';
  82. } else if (file_exists("$candidatelocation/$image.gif")) {
  83. $cacheimage = "$candidatelocation/$image.gif";
  84. $ext = 'gif';
  85. } else if (file_exists("$candidatelocation/$image.jpg")) {
  86. $cacheimage = "$candidatelocation/$image.jpg";
  87. $ext = 'jpg';
  88. } else if (file_exists("$candidatelocation/$image.jpeg")) {
  89. $cacheimage = "$candidatelocation/$image.jpeg";
  90. $ext = 'jpeg';
  91. } else if (file_exists("$candidatelocation/$image.ico")) {
  92. $cacheimage = "$candidatelocation/$image.ico";
  93. $ext = 'ico';
  94. }
  95. if ($cacheimage) {
  96. if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  97. // We do not actually need to verify the etag value because our files
  98. // never change in cache because we increment the rev parameter.
  99. // 90 days only - based on Moodle point release cadence being every 3 months.
  100. $lifetime = 60 * 60 * 24 * 90;
  101. $mimetype = get_contenttype_from_ext($ext);
  102. header('HTTP/1.1 304 Not Modified');
  103. header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
  104. header('Cache-Control: public, max-age='.$lifetime.', no-transform');
  105. header('Content-Type: '.$mimetype);
  106. header('Etag: "'.$etag.'"');
  107. die;
  108. }
  109. send_cached_image($cacheimage, $etag);
  110. }
  111. }
  112. //=================================================================================
  113. // ok, now we need to start normal moodle script, we need to load all libs and $DB
  114. define('ABORT_AFTER_CONFIG_CANCEL', true);
  115. define('NO_MOODLE_COOKIES', true); // Session not used here
  116. define('NO_UPGRADE_CHECK', true); // Ignore upgrade check
  117. require("$CFG->dirroot/lib/setup.php");
  118. $theme = theme_config::load($themename);
  119. $themerev = theme_get_revision();
  120. if ($themerev <= 0 or $rev != $themerev) {
  121. // Do not send caching headers if they do not request current revision,
  122. // we do not want to pollute browser caches with outdated images.
  123. $imagefile = $theme->resolve_image_location($image, $component, $usesvg);
  124. if (empty($imagefile) or !is_readable($imagefile)) {
  125. image_not_found();
  126. }
  127. send_uncached_image($imagefile);
  128. }
  129. make_localcache_directory('theme', false);
  130. // At this stage caching is enabled, and either:
  131. // * we have no cached copy of the image in any format (either SVG, or non-SVG); or
  132. // * we have a cached copy of the SVG, but the non-SVG was requested by the browser.
  133. //
  134. // Because of the way in which the cache return code works above:
  135. // * if we are allowed to return SVG, we do not need to cache the non-SVG version; however
  136. // * if the browser has requested the non-SVG version, we *must* cache _both_ the SVG, and the non-SVG versions.
  137. // First get all copies - including, potentially, the SVG version.
  138. $imagefile = $theme->resolve_image_location($image, $component, true);
  139. if (empty($imagefile) || !is_readable($imagefile)) {
  140. // Unable to find a copy of the image file in any format.
  141. // We write a .error file for the image now - this will be used above when searching for cached copies to prevent
  142. // trying to find the image in the future.
  143. if (!file_exists($candidatelocation)) {
  144. @mkdir($candidatelocation, $CFG->directorypermissions, true);
  145. }
  146. // Make note we can not find this file.
  147. $cacheimage = "$candidatelocation/$image.error";
  148. $fp = fopen($cacheimage, 'w');
  149. fclose($fp);
  150. image_not_found();
  151. }
  152. // The image was found, and it is readable.
  153. $pathinfo = pathinfo($imagefile);
  154. // Attempt to cache it if necessary.
  155. // We don't really want to overwrite any existing cache items just for the sake of it.
  156. $cacheimage = "$candidatelocation/$image.{$pathinfo['extension']}";
  157. if (!file_exists($cacheimage)) {
  158. // We don't already hold a cached copy of this image. Cache it now.
  159. $cacheimage = cache_image($image, $imagefile, $candidatelocation);
  160. }
  161. if (!$usesvg && $pathinfo['extension'] === 'svg') {
  162. // The browser has requested that a non-SVG version be returned.
  163. // The version found so far is the SVG version - try and find the non-SVG version.
  164. $imagefile = $theme->resolve_image_location($image, $component, false);
  165. if (empty($imagefile) || !is_readable($imagefile)) {
  166. // A non-SVG file could not be found at all.
  167. // The browser has requested a non-SVG version, so we must return image_not_found().
  168. // We must *not* write an .error file because the SVG is available.
  169. image_not_found();
  170. }
  171. // An non-SVG version of image was found - cache it.
  172. // This will be used below in the image serving code.
  173. $cacheimage = cache_image($image, $imagefile, $candidatelocation);
  174. }
  175. if (connection_aborted()) {
  176. // Request was cancelled - do not send anything.
  177. die;
  178. }
  179. // Make sure nothing failed.
  180. clearstatcache();
  181. if (file_exists($cacheimage)) {
  182. // The cached copy was found, and is accessible. Serve it.
  183. send_cached_image($cacheimage, $etag);
  184. }
  185. send_uncached_image($imagefile);
  186. //=================================================================================
  187. //=== utility functions ==
  188. // we are not using filelib because we need to fine tune all header
  189. // parameters to get the best performance.
  190. function send_cached_image($imagepath, $etag) {
  191. global $CFG;
  192. require("$CFG->dirroot/lib/xsendfilelib.php");
  193. // 90 days only - based on Moodle point release cadence being every 3 months.
  194. $lifetime = 60 * 60 * 24 * 90;
  195. $pathinfo = pathinfo($imagepath);
  196. $imagename = $pathinfo['filename'].'.'.$pathinfo['extension'];
  197. $mimetype = get_contenttype_from_ext($pathinfo['extension']);
  198. header('Etag: "'.$etag.'"');
  199. header('Content-Disposition: inline; filename="'.$imagename.'"');
  200. header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($imagepath)) .' GMT');
  201. header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
  202. header('Pragma: ');
  203. header('Cache-Control: public, max-age='.$lifetime.', no-transform, immutable');
  204. header('Accept-Ranges: none');
  205. header('Content-Type: '.$mimetype);
  206. if (xsendfile($imagepath)) {
  207. die;
  208. }
  209. if ($mimetype === 'image/svg+xml') {
  210. // SVG format is a text file. So we can compress SVG files.
  211. if (!min_enable_zlib_compression()) {
  212. header('Content-Length: '.filesize($imagepath));
  213. }
  214. } else {
  215. // No need to compress other image formats.
  216. header('Content-Length: '.filesize($imagepath));
  217. }
  218. readfile($imagepath);
  219. die;
  220. }
  221. function send_uncached_image($imagepath) {
  222. $pathinfo = pathinfo($imagepath);
  223. $imagename = $pathinfo['filename'].'.'.$pathinfo['extension'];
  224. $mimetype = get_contenttype_from_ext($pathinfo['extension']);
  225. header('Content-Disposition: inline; filename="'.$imagename.'"');
  226. header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
  227. header('Expires: '. gmdate('D, d M Y H:i:s', time() + 15) .' GMT');
  228. header('Pragma: ');
  229. header('Accept-Ranges: none');
  230. header('Content-Type: '.$mimetype);
  231. header('Content-Length: '.filesize($imagepath));
  232. readfile($imagepath);
  233. die;
  234. }
  235. function image_not_found() {
  236. header('HTTP/1.0 404 not found');
  237. die('Image was not found, sorry.');
  238. }
  239. function get_contenttype_from_ext($ext) {
  240. switch ($ext) {
  241. case 'svg':
  242. return 'image/svg+xml';
  243. case 'png':
  244. return 'image/png';
  245. case 'gif':
  246. return 'image/gif';
  247. case 'jpg':
  248. case 'jpeg':
  249. return 'image/jpeg';
  250. case 'ico':
  251. return 'image/vnd.microsoft.icon';
  252. }
  253. return 'document/unknown';
  254. }
  255. /**
  256. * Caches a given image file.
  257. *
  258. * @param string $image The name of the image that was requested.
  259. * @param string $imagefile The location of the image file we want to cache.
  260. * @param string $candidatelocation The location to cache it in.
  261. * @return string The path to the cached image.
  262. */
  263. function cache_image($image, $imagefile, $candidatelocation) {
  264. global $CFG;
  265. $pathinfo = pathinfo($imagefile);
  266. $cacheimage = "$candidatelocation/$image.".$pathinfo['extension'];
  267. clearstatcache();
  268. if (!file_exists(dirname($cacheimage))) {
  269. @mkdir(dirname($cacheimage), $CFG->directorypermissions, true);
  270. }
  271. // Prevent serving of incomplete file from concurrent request,
  272. // the rename() should be more atomic than copy().
  273. ignore_user_abort(true);
  274. if (@copy($imagefile, $cacheimage.'.tmp')) {
  275. rename($cacheimage.'.tmp', $cacheimage);
  276. @chmod($cacheimage, $CFG->filepermissions);
  277. @unlink($cacheimage.'.tmp'); // just in case anything fails
  278. }
  279. return $cacheimage;
  280. }