MinApp.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. <?php
  2. /**
  3. * Class Minify_Controller_MinApp
  4. * @package Minify
  5. */
  6. /**
  7. * Controller class for requests to /min/index.php
  8. *
  9. * @package Minify
  10. * @author Stephen Clay <steve@mrclay.org>
  11. */
  12. class Minify_Controller_MinApp extends Minify_Controller_Base {
  13. /**
  14. * Set up groups of files as sources
  15. *
  16. * @param array $options controller and Minify options
  17. *
  18. * @return array Minify options
  19. */
  20. public function setupSources($options) {
  21. // PHP insecure by default: realpath() and other FS functions can't handle null bytes.
  22. foreach (array('g', 'b', 'f') as $key) {
  23. if (isset($_GET[$key])) {
  24. $_GET[$key] = str_replace("\x00", '', (string)$_GET[$key]);
  25. }
  26. }
  27. // filter controller options
  28. $cOptions = array_merge(
  29. array(
  30. 'allowDirs' => '//'
  31. ,'groupsOnly' => false
  32. ,'groups' => array()
  33. ,'noMinPattern' => '@[-\\.]min\\.(?:js|css)$@i' // matched against basename
  34. )
  35. ,(isset($options['minApp']) ? $options['minApp'] : array())
  36. );
  37. unset($options['minApp']);
  38. $sources = array();
  39. $this->selectionId = '';
  40. $firstMissingResource = null;
  41. if (isset($_GET['g'])) {
  42. // add group(s)
  43. $this->selectionId .= 'g=' . $_GET['g'];
  44. $keys = explode(',', $_GET['g']);
  45. if ($keys != array_unique($keys)) {
  46. $this->log("Duplicate group key found.");
  47. return $options;
  48. }
  49. foreach ($keys as $key) {
  50. if (! isset($cOptions['groups'][$key])) {
  51. $this->log("A group configuration for \"{$key}\" was not found");
  52. return $options;
  53. }
  54. $files = $cOptions['groups'][$key];
  55. // if $files is a single object, casting will break it
  56. if (is_object($files)) {
  57. $files = array($files);
  58. } elseif (! is_array($files)) {
  59. $files = (array)$files;
  60. }
  61. foreach ($files as $file) {
  62. if ($file instanceof Minify_Source) {
  63. $sources[] = $file;
  64. continue;
  65. }
  66. if (0 === strpos($file, '//')) {
  67. $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1);
  68. }
  69. $realpath = realpath($file);
  70. if ($realpath && is_file($realpath)) {
  71. $sources[] = $this->_getFileSource($realpath, $cOptions);
  72. } else {
  73. $this->log("The path \"{$file}\" (realpath \"{$realpath}\") could not be found (or was not a file)");
  74. if (null === $firstMissingResource) {
  75. $firstMissingResource = basename($file);
  76. continue;
  77. } else {
  78. $secondMissingResource = basename($file);
  79. $this->log("More than one file was missing: '$firstMissingResource', '$secondMissingResource'");
  80. return $options;
  81. }
  82. }
  83. }
  84. if ($sources) {
  85. try {
  86. $this->checkType($sources[0]);
  87. } catch (Exception $e) {
  88. $this->log($e->getMessage());
  89. return $options;
  90. }
  91. }
  92. }
  93. }
  94. if (! $cOptions['groupsOnly'] && isset($_GET['f'])) {
  95. // try user files
  96. // The following restrictions are to limit the URLs that minify will
  97. // respond to.
  98. if (// verify at least one file, files are single comma separated,
  99. // and are all same extension
  100. ! preg_match('/^[^,]+\\.(css|js)(?:,[^,]+\\.\\1)*$/', $_GET['f'], $m)
  101. // no "//"
  102. || strpos($_GET['f'], '//') !== false
  103. // no "\"
  104. || strpos($_GET['f'], '\\') !== false
  105. ) {
  106. $this->log("GET param 'f' was invalid");
  107. return $options;
  108. }
  109. $ext = ".{$m[1]}";
  110. try {
  111. $this->checkType($m[1]);
  112. } catch (Exception $e) {
  113. $this->log($e->getMessage());
  114. return $options;
  115. }
  116. $files = explode(',', $_GET['f']);
  117. if ($files != array_unique($files)) {
  118. $this->log("Duplicate files were specified");
  119. return $options;
  120. }
  121. if (isset($_GET['b'])) {
  122. // check for validity
  123. if (preg_match('@^[^/]+(?:/[^/]+)*$@', $_GET['b'])
  124. && false === strpos($_GET['b'], '..')
  125. && $_GET['b'] !== '.') {
  126. // valid base
  127. $base = "/{$_GET['b']}/";
  128. } else {
  129. $this->log("GET param 'b' was invalid");
  130. return $options;
  131. }
  132. } else {
  133. $base = '/';
  134. }
  135. $allowDirs = array();
  136. foreach ((array)$cOptions['allowDirs'] as $allowDir) {
  137. $allowDirs[] = realpath(str_replace('//', $_SERVER['DOCUMENT_ROOT'] . '/', $allowDir));
  138. }
  139. $basenames = array(); // just for cache id
  140. foreach ($files as $file) {
  141. $uri = $base . $file;
  142. $path = $_SERVER['DOCUMENT_ROOT'] . $uri;
  143. $realpath = realpath($path);
  144. if (false === $realpath || ! is_file($realpath)) {
  145. $this->log("The path \"{$path}\" (realpath \"{$realpath}\") could not be found (or was not a file)");
  146. if (null === $firstMissingResource) {
  147. $firstMissingResource = $uri;
  148. continue;
  149. } else {
  150. $secondMissingResource = $uri;
  151. $this->log("More than one file was missing: '$firstMissingResource', '$secondMissingResource`'");
  152. return $options;
  153. }
  154. }
  155. try {
  156. parent::checkNotHidden($realpath);
  157. parent::checkAllowDirs($realpath, $allowDirs, $uri);
  158. } catch (Exception $e) {
  159. $this->log($e->getMessage());
  160. return $options;
  161. }
  162. $sources[] = $this->_getFileSource($realpath, $cOptions);
  163. $basenames[] = basename($realpath, $ext);
  164. }
  165. if ($this->selectionId) {
  166. $this->selectionId .= '_f=';
  167. }
  168. $this->selectionId .= implode(',', $basenames) . $ext;
  169. }
  170. if ($sources) {
  171. if (null !== $firstMissingResource) {
  172. array_unshift($sources, new Minify_Source(array(
  173. 'id' => 'missingFile'
  174. // should not cause cache invalidation
  175. ,'lastModified' => 0
  176. // due to caching, filename is unreliable.
  177. ,'content' => "/* Minify: at least one missing file. See " . Minify::URL_DEBUG . " */\n"
  178. ,'minifier' => ''
  179. )));
  180. }
  181. $this->sources = $sources;
  182. } else {
  183. $this->log("No sources to serve");
  184. }
  185. return $options;
  186. }
  187. /**
  188. * @param string $file
  189. *
  190. * @param array $cOptions
  191. *
  192. * @return Minify_Source
  193. */
  194. protected function _getFileSource($file, $cOptions)
  195. {
  196. $spec['filepath'] = $file;
  197. if ($cOptions['noMinPattern'] && preg_match($cOptions['noMinPattern'], basename($file))) {
  198. if (preg_match('~\.css$~i', $file)) {
  199. $spec['minifyOptions']['compress'] = false;
  200. } else {
  201. $spec['minifier'] = '';
  202. }
  203. }
  204. return new Minify_Source($spec);
  205. }
  206. protected $_type = null;
  207. /**
  208. * Make sure that only source files of a single type are registered
  209. *
  210. * @param string $sourceOrExt
  211. *
  212. * @throws Exception
  213. */
  214. public function checkType($sourceOrExt)
  215. {
  216. if ($sourceOrExt === 'js') {
  217. $type = Minify::TYPE_JS;
  218. } elseif ($sourceOrExt === 'css') {
  219. $type = Minify::TYPE_CSS;
  220. } elseif ($sourceOrExt->contentType !== null) {
  221. $type = $sourceOrExt->contentType;
  222. } else {
  223. return;
  224. }
  225. if ($this->_type === null) {
  226. $this->_type = $type;
  227. } elseif ($this->_type !== $type) {
  228. throw new Exception('Content-Type mismatch');
  229. }
  230. }
  231. }