modphpthumb.class.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. <?php
  2. /*
  3. * This file is part of MODX Revolution.
  4. *
  5. * Copyright (c) MODX, LLC. All Rights Reserved.
  6. *
  7. * For complete copyright and license information, see the COPYRIGHT and LICENSE
  8. * files found in the top-level directory of this distribution.
  9. */
  10. require_once MODX_CORE_PATH . 'model/phpthumb/phpthumb.class.php';
  11. /**
  12. * Helper class to extend phpThumb and simplify thumbnail generation process
  13. * since phpThumb class is overly convoluted and doesn't do enough.
  14. *
  15. * @package modx
  16. * @subpackage phpthumb
  17. */
  18. class modPhpThumb extends phpThumb
  19. {
  20. public $modx;
  21. public $config = array();
  22. /**
  23. * modPhpThumb constructor.
  24. * @param modX $modx
  25. * @param array $config
  26. */
  27. public function __construct(modX &$modx, array $config = array())
  28. {
  29. $this->modx =& $modx;
  30. $this->config = $config;
  31. if (version_compare(PHP_VERSION, '5.6.25', '<')
  32. || (version_compare(PHP_VERSION, '7.0.0', '>=')
  33. && version_compare(PHP_VERSION, '7.0.10', '<'))) {
  34. // The constant IMG_WEBP is available as of PHP 5.6.25 and PHP 7.0.10, respectively.
  35. define('IMG_WEBP', 32);
  36. }
  37. parent::__construct();
  38. }
  39. /**
  40. * Setup some site-wide phpthumb options from modx config
  41. */
  42. public function initialize()
  43. {
  44. $cachePath = $this->modx->getOption('core_path',null,MODX_CORE_PATH).'cache/phpthumb/';
  45. if (!is_dir($cachePath)) {
  46. $this->modx->cacheManager->writeTree($cachePath);
  47. }
  48. $this->setParameter('config_cache_directory', $cachePath);
  49. $this->setParameter('config_temp_directory', $cachePath);
  50. $this->setCacheDirectory();
  51. $this->setParameter('config_allow_src_above_docroot',(boolean)$this->modx->getOption('phpthumb_allow_src_above_docroot',$this->config,false));
  52. $this->setParameter('config_cache_maxage',(float)$this->modx->getOption('phpthumb_cache_maxage',$this->config,30) * 86400);
  53. $this->setParameter('config_cache_maxsize',(float)$this->modx->getOption('phpthumb_cache_maxsize',$this->config,100) * 1024 * 1024);
  54. $this->setParameter('config_cache_maxfiles',(int)$this->modx->getOption('phpthumb_cache_maxfiles',$this->config,10000));
  55. $this->setParameter('config_error_bgcolor',(string)$this->modx->getOption('phpthumb_error_bgcolor',$this->config,'CCCCFF'));
  56. $this->setParameter('config_error_textcolor',(string)$this->modx->getOption('phpthumb_error_textcolor',$this->config,'FF0000'));
  57. $this->setParameter('config_error_fontsize',(int)$this->modx->getOption('phpthumb_error_fontsize',$this->config,1));
  58. $this->setParameter('config_nohotlink_enabled',(boolean)$this->modx->getOption('phpthumb_nohotlink_enabled',$this->config,true));
  59. $this->setParameter('config_nohotlink_valid_domains',explode(',', $this->modx->getOption('phpthumb_nohotlink_valid_domains',$this->config,$this->modx->getOption('http_host'))));
  60. $this->setParameter('config_nohotlink_erase_image',(boolean)$this->modx->getOption('phpthumb_nohotlink_erase_image',$this->config,true));
  61. $this->setParameter('config_nohotlink_text_message',(string)$this->modx->getOption('phpthumb_nohotlink_text_message',$this->config,'Off-server thumbnailing is not allowed'));
  62. $this->setParameter('config_nooffsitelink_enabled',(boolean)$this->modx->getOption('phpthumb_nooffsitelink_enabled',$this->config,false));
  63. $this->setParameter('config_nooffsitelink_valid_domains',explode(',', $this->modx->getOption('phpthumb_nooffsitelink_valid_domains',$this->config,$this->modx->getOption('http_host'))));
  64. $this->setParameter('config_nooffsitelink_require_refer',(boolean)$this->modx->getOption('phpthumb_nooffsitelink_require_refer',$this->config,false));
  65. $this->setParameter('config_nooffsitelink_erase_image',(boolean)$this->modx->getOption('phpthumb_nooffsitelink_erase_image',$this->config,true));
  66. $this->setParameter('config_nooffsitelink_watermark_src',(string)$this->modx->getOption('phpthumb_nooffsitelink_watermark_src',$this->config,''));
  67. $this->setParameter('config_nooffsitelink_text_message',(string)$this->modx->getOption('phpthumb_nooffsitelink_text_message',$this->config,'Off-server linking is not allowed'));
  68. $this->setParameter('config_ttf_directory', (string)$this->modx->getOption('core_path', $this->config, MODX_CORE_PATH) . 'model/phpthumb/fonts/');
  69. $this->setParameter('config_imagemagick_path', (string)$this->modx->getOption('phpthumb_imagemagick_path', $this->config, null));
  70. $this->setParameter('cache_source_enabled',(boolean)$this->modx->getOption('phpthumb_cache_source_enabled',$this->config,false));
  71. $this->setParameter('cache_source_directory',$cachePath.'source/');
  72. $this->setParameter('allow_local_http_src',true);
  73. $this->setParameter('zc',$this->modx->getOption('zc',$_REQUEST,$this->modx->getOption('phpthumb_zoomcrop',$this->config,0)));
  74. $this->setParameter('far',$this->modx->getOption('far',$_REQUEST,$this->modx->getOption('phpthumb_far',$this->config,'C')));
  75. $this->setParameter('cache_directory_depth',4);
  76. $documentRoot = $this->modx->getOption('phpthumb_document_root',$this->config, '');
  77. if ($documentRoot == '') $documentRoot = $this->modx->getOption('base_path', null, '');
  78. if (!empty($documentRoot)) {
  79. $this->setParameter('config_document_root',$documentRoot);
  80. }
  81. // Only public parameters of phpThumb should be allowed to pass from user input.
  82. // List properties between START PARAMETERS and START PARAMETERS in src/core/model/phpthumb/phpthumb.class.php
  83. $allowed = array(
  84. 'src', 'new', 'w', 'h', 'wp', 'hp', 'wl', 'hl', 'ws', 'hs',
  85. 'f', 'q', 'sx', 'sy', 'sw', 'sh', 'zc', 'bc', 'bg', 'fltr',
  86. 'goto', 'err', 'xto', 'ra', 'ar', 'aoe', 'far', 'iar', 'maxb', 'down',
  87. 'md5s', 'sfn', 'dpi', 'sia', 'phpThumbDebug'
  88. );
  89. /* iterate through properties */
  90. foreach ($this->config as $property => $value) {
  91. if (!in_array($property, $allowed, true)) {
  92. $this->modx->log(modX::LOG_LEVEL_WARN,"Detected attempt of using private parameter `$property` (for internal usage) of phpThumb that not allowed and insecure");
  93. continue;
  94. }
  95. $this->setParameter($property, $value);
  96. }
  97. return true;
  98. }
  99. /**
  100. * Sets the source image
  101. */
  102. public function set($src) {
  103. $src = rawurldecode($src);
  104. if (empty($src)) return '';
  105. // Detecting URL's and explicitly replacing spaces with %20 for phpThumb to work correctly
  106. if (preg_match('#^[a-z0-9]+://#i', $src)) {
  107. $src = str_replace(' ', '%20', $src);
  108. }
  109. return $this->setSourceFilename($src);
  110. }
  111. /**
  112. * Check to see if cached file already exists
  113. */
  114. public function checkForCachedFile() {
  115. $this->SetCacheFilename();
  116. if (file_exists($this->cache_filename) && is_readable($this->cache_filename)) {
  117. return true;
  118. }
  119. return false;
  120. }
  121. /**
  122. * Load cached file
  123. */
  124. public function loadCache() {
  125. $this->RedirectToCachedFile();
  126. }
  127. /**
  128. * Cache the generated thumbnail.
  129. */
  130. public function cache() {
  131. phpthumb_functions::EnsureDirectoryExists(dirname($this->cache_filename));
  132. if ((file_exists($this->cache_filename) && is_writable($this->cache_filename)) || is_writable(dirname($this->cache_filename))) {
  133. $this->CleanUpCacheDirectory();
  134. if ($this->RenderToFile($this->cache_filename) && is_readable($this->cache_filename)) {
  135. chmod($this->cache_filename, 0644);
  136. $this->RedirectToCachedFile();
  137. }
  138. }
  139. }
  140. /**
  141. * Generate a thumbnail
  142. */
  143. public function generate() {
  144. if (!$this->GenerateThumbnail()) {
  145. $this->modx->log(modX::LOG_LEVEL_ERROR,'phpThumb was unable to generate a thumbnail for: '.$this->cache_filename);
  146. return false;
  147. }
  148. return true;
  149. }
  150. /**
  151. * Output a thumbnail.
  152. */
  153. public function output() {
  154. $output = $this->OutputThumbnail();
  155. if (!$output) {
  156. $this->modx->log(modX::LOG_LEVEL_ERROR,'Error outputting thumbnail:'."\n".$this->debugmessages[(count($this->debugmessages) - 1)]);
  157. }
  158. return $output;
  159. }
  160. /** PHPTHUMB HELPER METHODS **/
  161. public function RedirectToCachedFile() {
  162. $nice_cachefile = str_replace(DIRECTORY_SEPARATOR, '/', $this->cache_filename);
  163. $nice_docroot = str_replace(DIRECTORY_SEPARATOR, '/', rtrim($this->config_document_root, '/\\'));
  164. $parsed_url = phpthumb_functions::ParseURLbetter(@$_SERVER['HTTP_REFERER']);
  165. $nModified = filemtime($this->cache_filename);
  166. if ($this->config_nooffsitelink_enabled && @$_SERVER['HTTP_REFERER'] && !in_array(@$parsed_url['host'], $this->config_nooffsitelink_valid_domains)) {
  167. $this->DebugMessage('Would have used cached (image/'.$this->thumbnailFormat.') file "'.$this->cache_filename.'" (Last-Modified: '.gmdate('D, d M Y H:i:s', $nModified).' GMT), but skipping because $_SERVER[HTTP_REFERER] ('.@$_SERVER['HTTP_REFERER'].') is not in $this->config_nooffsitelink_valid_domains ('.implode(';', $this->config_nooffsitelink_valid_domains).')', __FILE__, __LINE__);
  168. } elseif ($this->phpThumbDebug) {
  169. $this->DebugTimingMessage('skipped using cached image', __FILE__, __LINE__);
  170. $this->DebugMessage('Would have used cached file, but skipping due to phpThumbDebug', __FILE__, __LINE__);
  171. $this->DebugMessage('* Would have sent headers (1): Last-Modified: '.gmdate('D, d M Y H:i:s', $nModified).' GMT', __FILE__, __LINE__);
  172. $getimagesize = @getimagesize($this->cache_filename);
  173. if ($getimagesize) {
  174. $this->DebugMessage('* Would have sent headers (2): Content-Type: '.phpthumb_functions::ImageTypeToMIMEtype($getimagesize[2]), __FILE__, __LINE__);
  175. }
  176. if (preg_match('/^'.preg_quote($nice_docroot, '/').'(.*)$/', $nice_cachefile, $matches)) {
  177. $this->DebugMessage('* Would have sent headers (3): Location: '.dirname($matches[1]).'/'.urlencode(basename($matches[1])), __FILE__, __LINE__);
  178. } else {
  179. $this->DebugMessage('* Would have sent data: readfile('.$this->cache_filename.')', __FILE__, __LINE__);
  180. }
  181. } else {
  182. /*
  183. if (headers_sent()) {
  184. $this->ErrorImage('Headers already sent ('.basename(__FILE__).' line '.__LINE__.')');
  185. exit;
  186. }*/
  187. $this->SendSaveAsFileHeaderIfNeeded();
  188. header('Last-Modified: '.gmdate('D, d M Y H:i:s', $nModified).' GMT');
  189. if (@$_SERVER['HTTP_IF_MODIFIED_SINCE'] && ($nModified == strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) && @$_SERVER['SERVER_PROTOCOL']) {
  190. header($_SERVER['SERVER_PROTOCOL'].' 304 Not Modified');
  191. exit;
  192. }
  193. $getimagesize = @getimagesize($this->cache_filename);
  194. if ($getimagesize) {
  195. header('Content-Type: '.phpthumb_functions::ImageTypeToMIMEtype($getimagesize[2]));
  196. } elseif (preg_match('#\.ico$#i', $this->cache_filename)) {
  197. header('Content-Type: image/x-icon');
  198. }
  199. if (!$this->config_cache_force_passthru && preg_match('#^'.preg_quote($nice_docroot, '/').'(.*)$#', $nice_cachefile, $matches)) {
  200. header('Location: '.dirname($matches[1]).'/'.urlencode(basename($matches[1])));
  201. } else {
  202. @readfile($this->cache_filename);
  203. }
  204. session_write_close();
  205. exit;
  206. }
  207. return true;
  208. }
  209. public function SendSaveAsFileHeaderIfNeeded() {
  210. if (headers_sent()) {
  211. return false;
  212. }
  213. $downloadfilename = phpthumb_functions::SanitizeFilename(@$_GET['sia'] ? $_GET['sia'] : (@$_GET['down'] ? $_GET['down'] : 'phpThumb_generated_thumbnail'.(@$_GET['f'] ? $_GET['f'] : 'jpg')));
  214. if (@$downloadfilename) {
  215. $this->DebugMessage('SendSaveAsFileHeaderIfNeeded() sending header: Content-Disposition: '.(@$_GET['down'] ? 'attachment' : 'inline').'; filename="'.$downloadfilename.'"', __FILE__, __LINE__);
  216. header('Content-Disposition: '.(@$_GET['down'] ? 'attachment' : 'inline').'; filename="'.$downloadfilename.'"');
  217. }
  218. return true;
  219. }
  220. function ResolveFilenameToAbsolute($filename) {
  221. if (empty($filename)) {
  222. return false;
  223. }
  224. if (preg_match('#^[a-z0-9]+\:/{1,2}#i', $filename)) {
  225. // eg: http://host/path/file.jpg (HTTP URL)
  226. // eg: ftp://host/path/file.jpg (FTP URL)
  227. // eg: data1:/path/file.jpg (Netware path)
  228. //$AbsoluteFilename = $filename;
  229. return $filename;
  230. } elseif ($this->iswindows && isset($filename{1}) && ($filename{1} == ':')) {
  231. // absolute pathname (Windows)
  232. $AbsoluteFilename = $filename;
  233. } elseif ($this->iswindows && ((substr($filename, 0, 2) == '//') || (substr($filename, 0, 2) == '\\\\'))) {
  234. // absolute pathname (Windows)
  235. $AbsoluteFilename = $filename;
  236. } elseif ($filename{0} == '/') {
  237. if (@is_readable($filename) && !@is_readable($this->config_document_root.$filename)) {
  238. // absolute filename (*nix)
  239. $AbsoluteFilename = $filename;
  240. } elseif (isset($filename{1}) && ($filename{1} == '~')) {
  241. // /~user/path
  242. if ($ApacheLookupURIarray = phpthumb_functions::ApacheLookupURIarray($filename)) {
  243. $AbsoluteFilename = $ApacheLookupURIarray['filename'];
  244. } else {
  245. $AbsoluteFilename = realpath($filename);
  246. if (@is_readable($AbsoluteFilename)) {
  247. $this->DebugMessage('phpthumb_functions::ApacheLookupURIarray() failed for "'.$filename.'", but the correct filename ('.$AbsoluteFilename.') seems to have been resolved with realpath($filename)', __FILE__, __LINE__);
  248. } elseif (is_dir(dirname($AbsoluteFilename))) {
  249. $this->DebugMessage('phpthumb_functions::ApacheLookupURIarray() failed for "'.dirname($filename).'", but the correct directory ('.dirname($AbsoluteFilename).') seems to have been resolved with realpath(.)', __FILE__, __LINE__);
  250. } else {
  251. return $this->ErrorImage('phpthumb_functions::ApacheLookupURIarray() failed for "'.$filename.'". This has been known to fail on Apache2 - try using the absolute filename for the source image (ex: "/home/user/httpdocs/image.jpg" instead of "/~user/image.jpg")');
  252. }
  253. }
  254. } else {
  255. // relative filename (any OS)
  256. if (preg_match('#^'.preg_quote($this->config_document_root).'#', $filename)) {
  257. $AbsoluteFilename = $filename;
  258. $this->DebugMessage('ResolveFilenameToAbsolute() NOT prepending $this->config_document_root ('.$this->config_document_root.') to $filename ('.$filename.') resulting in ($AbsoluteFilename = "'.$AbsoluteFilename.'")', __FILE__, __LINE__);
  259. } else {
  260. $AbsoluteFilename = $this->config_document_root.$filename;
  261. $this->DebugMessage('ResolveFilenameToAbsolute() prepending $this->config_document_root ('.$this->config_document_root.') to $filename ('.$filename.') resulting in ($AbsoluteFilename = "'.$AbsoluteFilename.'")', __FILE__, __LINE__);
  262. }
  263. }
  264. } else {
  265. // relative to current directory (any OS)
  266. $AbsoluteFilename = $this->config_document_root.preg_replace('#[/\\\\]#', DIRECTORY_SEPARATOR, dirname(@$_SERVER['PHP_SELF'])).DIRECTORY_SEPARATOR.preg_replace('#[/\\\\]#', DIRECTORY_SEPARATOR, $filename);
  267. // $AbsoluteFilename = dirname(__FILE__).DIRECTORY_SEPARATOR.preg_replace('#[/\\\\]#', DIRECTORY_SEPARATOR, $filename);
  268. $AbsoluteFilename = preg_replace('~[\/]+~', DIRECTORY_SEPARATOR, $AbsoluteFilename);
  269. //if (!@file_exists($AbsoluteFilename) && @file_exists(realpath($this->DotPadRelativeDirectoryPath($filename)))) {
  270. // $AbsoluteFilename = realpath($this->DotPadRelativeDirectoryPath($filename));
  271. //}
  272. if (substr(dirname(@$_SERVER['PHP_SELF']), 0, 2) == '/~') {
  273. if ($ApacheLookupURIarray = phpthumb_functions::ApacheLookupURIarray(dirname(@$_SERVER['PHP_SELF']))) {
  274. $AbsoluteFilename = $ApacheLookupURIarray['filename'].DIRECTORY_SEPARATOR.$filename;
  275. } else {
  276. $AbsoluteFilename = realpath('.').DIRECTORY_SEPARATOR.$filename;
  277. if (@is_readable($AbsoluteFilename)) {
  278. $this->DebugMessage('phpthumb_functions::ApacheLookupURIarray() failed for "'.dirname(@$_SERVER['PHP_SELF']).'", but the correct filename ('.$AbsoluteFilename.') seems to have been resolved with realpath(.)/$filename', __FILE__, __LINE__);
  279. } elseif (is_dir(dirname($AbsoluteFilename))) {
  280. $this->DebugMessage('phpthumb_functions::ApacheLookupURIarray() failed for "'.dirname(@$_SERVER['PHP_SELF']).'", but the correct directory ('.dirname($AbsoluteFilename).') seems to have been resolved with realpath(.)', __FILE__, __LINE__);
  281. } else {
  282. return $this->ErrorImage('phpthumb_functions::ApacheLookupURIarray() failed for "'.dirname(@$_SERVER['PHP_SELF']).'". This has been known to fail on Apache2 - try using the absolute filename for the source image');
  283. }
  284. }
  285. }
  286. }
  287. if (is_link($AbsoluteFilename)) {
  288. $this->DebugMessage('is_link()==true, changing "'.$AbsoluteFilename.'" to "'.readlink($AbsoluteFilename).'"', __FILE__, __LINE__);
  289. $AbsoluteFilename = readlink($AbsoluteFilename);
  290. }
  291. if (realpath($AbsoluteFilename)) {
  292. $AbsoluteFilename = realpath($AbsoluteFilename);
  293. }
  294. if ($this->iswindows) {
  295. $AbsoluteFilename = preg_replace('#^'.preg_quote(realpath($this->config_document_root)).'#i', realpath($this->config_document_root), $AbsoluteFilename);
  296. $AbsoluteFilename = str_replace(DIRECTORY_SEPARATOR, '/', $AbsoluteFilename);
  297. }
  298. if (!$this->config_allow_src_above_docroot && !preg_match('#^'.preg_quote(str_replace(DIRECTORY_SEPARATOR, '/', realpath($this->config_document_root))).'#', $AbsoluteFilename)) {
  299. $this->DebugMessage('!$this->config_allow_src_above_docroot therefore setting "'.$AbsoluteFilename.'" (outside "'.realpath($this->config_document_root).'") to null', __FILE__, __LINE__);
  300. return false;
  301. }
  302. if (!$this->config_allow_src_above_phpthumb && !preg_match('#^'.preg_quote(str_replace(DIRECTORY_SEPARATOR, '/', dirname(__FILE__))).'#', $AbsoluteFilename)) {
  303. $this->DebugMessage('!$this->config_allow_src_above_phpthumb therefore setting "'.$AbsoluteFilename.'" (outside "'.dirname(__FILE__).'") to null', __FILE__, __LINE__);
  304. return false;
  305. }
  306. return $AbsoluteFilename;
  307. }
  308. }