Utils.php 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512
  1. <?php
  2. /**
  3. * @package Grav\Common
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common;
  9. use Grav\Common\Helpers\Truncator;
  10. use Grav\Common\Page\Interfaces\PageInterface;
  11. use Grav\Common\Markdown\Parsedown;
  12. use Grav\Common\Markdown\ParsedownExtra;
  13. use RocketTheme\Toolbox\Event\Event;
  14. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  15. abstract class Utils
  16. {
  17. protected static $nonces = [];
  18. protected const ROOTURL_REGEX = '{^((?:http[s]?:\/\/[^\/]+)|(?:\/\/[^\/]+))(.*)}';
  19. // ^((?:http[s]?:)?[\/]?(?:\/))
  20. /**
  21. * Simple helper method to make getting a Grav URL easier
  22. *
  23. * @param string $input
  24. * @param bool $domain
  25. * @return bool|null|string
  26. */
  27. public static function url($input, $domain = false)
  28. {
  29. if (!trim((string)$input)) {
  30. $input = '/';
  31. }
  32. if (Grav::instance()['config']->get('system.absolute_urls', false)) {
  33. $domain = true;
  34. }
  35. if (Grav::instance()['uri']->isExternal($input)) {
  36. return $input;
  37. }
  38. /** @var Uri $uri */
  39. $uri = Grav::instance()['uri'];
  40. $root = $uri->rootUrl();
  41. $input = Utils::replaceFirstOccurrence($root, '', $input);
  42. $input = ltrim((string)$input, '/');
  43. if (Utils::contains((string)$input, '://')) {
  44. /** @var UniformResourceLocator $locator */
  45. $locator = Grav::instance()['locator'];
  46. $parts = Uri::parseUrl($input);
  47. if ($parts) {
  48. $resource = $locator->findResource("{$parts['scheme']}://{$parts['host']}{$parts['path']}", false);
  49. if (isset($parts['query'])) {
  50. $resource = $resource . '?' . $parts['query'];
  51. }
  52. } else {
  53. // Not a valid URL (can still be a stream).
  54. $resource = $locator->findResource($input, false);
  55. }
  56. } else {
  57. $resource = $input;
  58. }
  59. return rtrim($uri->rootUrl($domain), '/') . '/' . ($resource ?? '');
  60. }
  61. /**
  62. * Check if the $haystack string starts with the substring $needle
  63. *
  64. * @param string $haystack
  65. * @param string|string[] $needle
  66. * @param bool $case_sensitive
  67. *
  68. * @return bool
  69. */
  70. public static function startsWith($haystack, $needle, $case_sensitive = true)
  71. {
  72. $status = false;
  73. $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos';
  74. foreach ((array)$needle as $each_needle) {
  75. $status = $each_needle === '' || $compare_func($haystack, $each_needle) === 0;
  76. if ($status) {
  77. break;
  78. }
  79. }
  80. return $status;
  81. }
  82. /**
  83. * Check if the $haystack string ends with the substring $needle
  84. *
  85. * @param string $haystack
  86. * @param string|string[] $needle
  87. * @param bool $case_sensitive
  88. *
  89. * @return bool
  90. */
  91. public static function endsWith($haystack, $needle, $case_sensitive = true)
  92. {
  93. $status = false;
  94. $compare_func = $case_sensitive ? 'mb_strrpos' : 'mb_strripos';
  95. foreach ((array)$needle as $each_needle) {
  96. $expectedPosition = mb_strlen($haystack) - mb_strlen($each_needle);
  97. $status = $each_needle === '' || $compare_func($haystack, $each_needle, 0) === $expectedPosition;
  98. if ($status) {
  99. break;
  100. }
  101. }
  102. return $status;
  103. }
  104. /**
  105. * Check if the $haystack string contains the substring $needle
  106. *
  107. * @param string $haystack
  108. * @param string|string[] $needle
  109. * @param bool $case_sensitive
  110. *
  111. * @return bool
  112. */
  113. public static function contains($haystack, $needle, $case_sensitive = true)
  114. {
  115. $status = false;
  116. $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos';
  117. foreach ((array)$needle as $each_needle) {
  118. $status = $each_needle === '' || $compare_func($haystack, $each_needle) !== false;
  119. if ($status) {
  120. break;
  121. }
  122. }
  123. return $status;
  124. }
  125. /**
  126. * Function that can match wildcards
  127. *
  128. * match_wildcard('foo*', $test), // TRUE
  129. * match_wildcard('bar*', $test), // FALSE
  130. * match_wildcard('*bar*', $test), // TRUE
  131. * match_wildcard('**blob**', $test), // TRUE
  132. * match_wildcard('*a?d*', $test), // TRUE
  133. * match_wildcard('*etc**', $test) // TRUE
  134. *
  135. * @param string $wildcard_pattern
  136. * @param string $haystack
  137. * @return false|int
  138. */
  139. public static function matchWildcard($wildcard_pattern, $haystack) {
  140. $regex = str_replace(
  141. array("\*", "\?"), // wildcard chars
  142. array('.*','.'), // regexp chars
  143. preg_quote($wildcard_pattern, '/')
  144. );
  145. return preg_match('/^'.$regex.'$/is', $haystack);
  146. }
  147. /**
  148. * Returns the substring of a string up to a specified needle. if not found, return the whole haystack
  149. *
  150. * @param string $haystack
  151. * @param string $needle
  152. * @param bool $case_sensitive
  153. *
  154. * @return string
  155. */
  156. public static function substrToString($haystack, $needle, $case_sensitive = true)
  157. {
  158. $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos';
  159. if (static::contains($haystack, $needle, $case_sensitive)) {
  160. return mb_substr($haystack, 0, $compare_func($haystack, $needle, $case_sensitive));
  161. }
  162. return $haystack;
  163. }
  164. /**
  165. * Utility method to replace only the first occurrence in a string
  166. *
  167. * @param string $search
  168. * @param string $replace
  169. * @param string $subject
  170. *
  171. * @return string
  172. */
  173. public static function replaceFirstOccurrence($search, $replace, $subject)
  174. {
  175. if (!$search) {
  176. return $subject;
  177. }
  178. $pos = mb_strpos($subject, $search);
  179. if ($pos !== false) {
  180. $subject = static::mb_substr_replace($subject, $replace, $pos, mb_strlen($search));
  181. }
  182. return $subject;
  183. }
  184. /**
  185. * Utility method to replace only the last occurrence in a string
  186. *
  187. * @param string $search
  188. * @param string $replace
  189. * @param string $subject
  190. * @return string
  191. */
  192. public static function replaceLastOccurrence($search, $replace, $subject)
  193. {
  194. $pos = strrpos($subject, $search);
  195. if($pos !== false)
  196. {
  197. $subject = static::mb_substr_replace($subject, $replace, $pos, mb_strlen($search));
  198. }
  199. return $subject;
  200. }
  201. /**
  202. * Multibyte compatible substr_replace
  203. *
  204. * @param string $original
  205. * @param string $replacement
  206. * @param int $position
  207. * @param int $length
  208. * @return string
  209. */
  210. public static function mb_substr_replace($original, $replacement, $position, $length)
  211. {
  212. $startString = mb_substr($original, 0, $position, "UTF-8");
  213. $endString = mb_substr($original, $position + $length, mb_strlen($original), "UTF-8");
  214. return $startString . $replacement . $endString;
  215. }
  216. /**
  217. * Merge two objects into one.
  218. *
  219. * @param object $obj1
  220. * @param object $obj2
  221. *
  222. * @return object
  223. */
  224. public static function mergeObjects($obj1, $obj2)
  225. {
  226. return (object)array_merge((array)$obj1, (array)$obj2);
  227. }
  228. /**
  229. * Recursive Merge with uniqueness
  230. *
  231. * @param array $array1
  232. * @param array $array2
  233. * @return array
  234. */
  235. public static function arrayMergeRecursiveUnique($array1, $array2)
  236. {
  237. if (empty($array1)) {
  238. // Optimize the base case
  239. return $array2;
  240. }
  241. foreach ($array2 as $key => $value) {
  242. if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
  243. $value = static::arrayMergeRecursiveUnique($array1[$key], $value);
  244. }
  245. $array1[$key] = $value;
  246. }
  247. return $array1;
  248. }
  249. /**
  250. * Returns an array with the differences between $array1 and $array2
  251. *
  252. * @param array $array1
  253. * @param array $array2
  254. * @return array
  255. */
  256. public static function arrayDiffMultidimensional($array1, $array2)
  257. {
  258. $result = array();
  259. foreach ($array1 as $key => $value) {
  260. if (!is_array($array2) || !array_key_exists($key, $array2)) {
  261. $result[$key] = $value;
  262. continue;
  263. }
  264. if (is_array($value)) {
  265. $recursiveArrayDiff = static::ArrayDiffMultidimensional($value, $array2[$key]);
  266. if (count($recursiveArrayDiff)) {
  267. $result[$key] = $recursiveArrayDiff;
  268. }
  269. continue;
  270. }
  271. if ($value != $array2[$key]) {
  272. $result[$key] = $value;
  273. }
  274. }
  275. return $result;
  276. }
  277. /**
  278. * Array combine but supports different array lengths
  279. *
  280. * @param array $arr1
  281. * @param array $arr2
  282. * @return array|false
  283. */
  284. public static function arrayCombine($arr1, $arr2)
  285. {
  286. $count = min(count($arr1), count($arr2));
  287. return array_combine(array_slice($arr1, 0, $count), array_slice($arr2, 0, $count));
  288. }
  289. /**
  290. * Array is associative or not
  291. *
  292. * @param array $arr
  293. * @return bool
  294. */
  295. public static function arrayIsAssociative($arr)
  296. {
  297. if ([] === $arr) {
  298. return false;
  299. }
  300. return array_keys($arr) !== range(0, count($arr) - 1);
  301. }
  302. /**
  303. * Return the Grav date formats allowed
  304. *
  305. * @return array
  306. */
  307. public static function dateFormats()
  308. {
  309. $now = new \DateTime();
  310. $date_formats = [
  311. 'd-m-Y H:i' => 'd-m-Y H:i (e.g. '.$now->format('d-m-Y H:i').')',
  312. 'Y-m-d H:i' => 'Y-m-d H:i (e.g. '.$now->format('Y-m-d H:i').')',
  313. 'm/d/Y h:i a' => 'm/d/Y h:i a (e.g. '.$now->format('m/d/Y h:i a').')',
  314. 'H:i d-m-Y' => 'H:i d-m-Y (e.g. '.$now->format('H:i d-m-Y').')',
  315. 'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. '.$now->format('h:i a m/d/Y').')',
  316. ];
  317. $default_format = Grav::instance()['config']->get('system.pages.dateformat.default');
  318. if ($default_format) {
  319. $date_formats = array_merge([$default_format => $default_format.' (e.g. '.$now->format($default_format).')'], $date_formats);
  320. }
  321. return $date_formats;
  322. }
  323. /**
  324. * Get current date/time
  325. *
  326. * @param string|null $default_format
  327. * @return string
  328. * @throws \Exception
  329. */
  330. public static function dateNow($default_format = null)
  331. {
  332. $now = new \DateTime();
  333. if (is_null($default_format)) {
  334. $default_format = Grav::instance()['config']->get('system.pages.dateformat.default');
  335. }
  336. return $now->format($default_format);
  337. }
  338. /**
  339. * Truncate text by number of characters but can cut off words.
  340. *
  341. * @param string $string
  342. * @param int $limit Max number of characters.
  343. * @param bool $up_to_break truncate up to breakpoint after char count
  344. * @param string $break Break point.
  345. * @param string $pad Appended padding to the end of the string.
  346. *
  347. * @return string
  348. */
  349. public static function truncate($string, $limit = 150, $up_to_break = false, $break = ' ', $pad = '&hellip;')
  350. {
  351. // return with no change if string is shorter than $limit
  352. if (mb_strlen($string) <= $limit) {
  353. return $string;
  354. }
  355. // is $break present between $limit and the end of the string?
  356. if ($up_to_break && false !== ($breakpoint = mb_strpos($string, $break, $limit))) {
  357. if ($breakpoint < mb_strlen($string) - 1) {
  358. $string = mb_substr($string, 0, $breakpoint) . $pad;
  359. }
  360. } else {
  361. $string = mb_substr($string, 0, $limit) . $pad;
  362. }
  363. return $string;
  364. }
  365. /**
  366. * Truncate text by number of characters in a "word-safe" manor.
  367. *
  368. * @param string $string
  369. * @param int $limit
  370. *
  371. * @return string
  372. */
  373. public static function safeTruncate($string, $limit = 150)
  374. {
  375. return static::truncate($string, $limit, true);
  376. }
  377. /**
  378. * Truncate HTML by number of characters. not "word-safe"!
  379. *
  380. * @param string $text
  381. * @param int $length in characters
  382. * @param string $ellipsis
  383. *
  384. * @return string
  385. */
  386. public static function truncateHtml($text, $length = 100, $ellipsis = '...')
  387. {
  388. return Truncator::truncateLetters($text, $length, $ellipsis);
  389. }
  390. /**
  391. * Truncate HTML by number of characters in a "word-safe" manor.
  392. *
  393. * @param string $text
  394. * @param int $length in words
  395. * @param string $ellipsis
  396. *
  397. * @return string
  398. */
  399. public static function safeTruncateHtml($text, $length = 25, $ellipsis = '...')
  400. {
  401. return Truncator::truncateWords($text, $length, $ellipsis);
  402. }
  403. /**
  404. * Generate a random string of a given length
  405. *
  406. * @param int $length
  407. *
  408. * @return string
  409. */
  410. public static function generateRandomString($length = 5)
  411. {
  412. return substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length);
  413. }
  414. /**
  415. * Provides the ability to download a file to the browser
  416. *
  417. * @param string $file the full path to the file to be downloaded
  418. * @param bool $force_download as opposed to letting browser choose if to download or render
  419. * @param int $sec Throttling, try 0.1 for some speed throttling of downloads
  420. * @param int $bytes Size of chunks to send in bytes. Default is 1024
  421. * @throws \Exception
  422. */
  423. public static function download($file, $force_download = true, $sec = 0, $bytes = 1024)
  424. {
  425. if (file_exists($file)) {
  426. // fire download event
  427. Grav::instance()->fireEvent('onBeforeDownload', new Event(['file' => $file]));
  428. $file_parts = pathinfo($file);
  429. $mimetype = static::getMimeByExtension($file_parts['extension']);
  430. $size = filesize($file); // File size
  431. // clean all buffers
  432. while (ob_get_level()) {
  433. ob_end_clean();
  434. }
  435. // required for IE, otherwise Content-Disposition may be ignored
  436. if (ini_get('zlib.output_compression')) {
  437. ini_set('zlib.output_compression', 'Off');
  438. }
  439. header('Content-Type: ' . $mimetype);
  440. header('Accept-Ranges: bytes');
  441. if ($force_download) {
  442. // output the regular HTTP headers
  443. header('Content-Disposition: attachment; filename="' . $file_parts['basename'] . '"');
  444. }
  445. // multipart-download and download resuming support
  446. if (isset($_SERVER['HTTP_RANGE'])) {
  447. list($a, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
  448. list($range) = explode(',', $range, 2);
  449. list($range, $range_end) = explode('-', $range);
  450. $range = (int)$range;
  451. if (!$range_end) {
  452. $range_end = $size - 1;
  453. } else {
  454. $range_end = (int)$range_end;
  455. }
  456. $new_length = $range_end - $range + 1;
  457. header('HTTP/1.1 206 Partial Content');
  458. header("Content-Length: {$new_length}");
  459. header("Content-Range: bytes {$range}-{$range_end}/{$size}");
  460. } else {
  461. $range = 0;
  462. $new_length = $size;
  463. header('Content-Length: ' . $size);
  464. if (Grav::instance()['config']->get('system.cache.enabled')) {
  465. $expires = Grav::instance()['config']->get('system.pages.expires');
  466. if ($expires > 0) {
  467. $expires_date = gmdate('D, d M Y H:i:s T', time() + $expires);
  468. header('Cache-Control: max-age=' . $expires);
  469. header('Expires: ' . $expires_date);
  470. header('Pragma: cache');
  471. }
  472. header('Last-Modified: ' . gmdate('D, d M Y H:i:s T', filemtime($file)));
  473. // Return 304 Not Modified if the file is already cached in the browser
  474. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
  475. strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= filemtime($file))
  476. {
  477. header('HTTP/1.1 304 Not Modified');
  478. exit();
  479. }
  480. }
  481. }
  482. /* output the file itself */
  483. $chunksize = $bytes * 8; //you may want to change this
  484. $bytes_send = 0;
  485. $fp = @fopen($file, 'rb');
  486. if ($fp) {
  487. if ($range) {
  488. fseek($fp, $range);
  489. }
  490. while (!feof($fp) && (!connection_aborted()) && ($bytes_send < $new_length) ) {
  491. $buffer = fread($fp, $chunksize);
  492. echo($buffer); //echo($buffer); // is also possible
  493. flush();
  494. usleep($sec * 1000000);
  495. $bytes_send += strlen($buffer);
  496. }
  497. fclose($fp);
  498. } else {
  499. throw new \RuntimeException('Error - can not open file.');
  500. }
  501. exit;
  502. }
  503. }
  504. /**
  505. * Return the mimetype based on filename extension
  506. *
  507. * @param string $extension Extension of file (eg "txt")
  508. * @param string $default
  509. *
  510. * @return string
  511. */
  512. public static function getMimeByExtension($extension, $default = 'application/octet-stream')
  513. {
  514. $extension = strtolower($extension);
  515. // look for some standard types
  516. switch ($extension) {
  517. case null:
  518. return $default;
  519. case 'json':
  520. return 'application/json';
  521. case 'html':
  522. return 'text/html';
  523. case 'atom':
  524. return 'application/atom+xml';
  525. case 'rss':
  526. return 'application/rss+xml';
  527. case 'xml':
  528. return 'application/xml';
  529. }
  530. $media_types = Grav::instance()['config']->get('media.types');
  531. if (isset($media_types[$extension])) {
  532. if (isset($media_types[$extension]['mime'])) {
  533. return $media_types[$extension]['mime'];
  534. }
  535. }
  536. return $default;
  537. }
  538. /**
  539. * Get all the mimetypes for an array of extensions
  540. *
  541. * @param array $extensions
  542. * @return array
  543. */
  544. public static function getMimeTypes(array $extensions)
  545. {
  546. $mimetypes = [];
  547. foreach ($extensions as $extension) {
  548. $mimetype = static::getMimeByExtension($extension, false);
  549. if ($mimetype && !in_array($mimetype, $mimetypes)) {
  550. $mimetypes[] = $mimetype;
  551. }
  552. }
  553. return $mimetypes;
  554. }
  555. /**
  556. * Return the mimetype based on filename extension
  557. *
  558. * @param string $mime mime type (eg "text/html")
  559. * @param string $default default value
  560. *
  561. * @return string
  562. */
  563. public static function getExtensionByMime($mime, $default = 'html')
  564. {
  565. $mime = strtolower($mime);
  566. // look for some standard mime types
  567. switch ($mime) {
  568. case '*/*':
  569. case 'text/*':
  570. case 'text/html':
  571. return 'html';
  572. case 'application/json':
  573. return 'json';
  574. case 'application/atom+xml':
  575. return 'atom';
  576. case 'application/rss+xml':
  577. return 'rss';
  578. case 'application/xml':
  579. return 'xml';
  580. }
  581. $media_types = (array)Grav::instance()['config']->get('media.types');
  582. foreach ($media_types as $extension => $type) {
  583. if ($extension === 'defaults') {
  584. continue;
  585. }
  586. if (isset($type['mime']) && $type['mime'] === $mime) {
  587. return $extension;
  588. }
  589. }
  590. return $default;
  591. }
  592. /**
  593. * Get all the extensions for an array of mimetypes
  594. *
  595. * @param array $mimetypes
  596. * @return array
  597. */
  598. public static function getExtensions(array $mimetypes)
  599. {
  600. $extensions = [];
  601. foreach ($mimetypes as $mimetype) {
  602. $extension = static::getExtensionByMime($mimetype, false);
  603. if ($extension && !\in_array($extension, $extensions, true)) {
  604. $extensions[] = $extension;
  605. }
  606. }
  607. return $extensions;
  608. }
  609. /**
  610. * Return the mimetype based on filename
  611. *
  612. * @param string $filename Filename or path to file
  613. * @param string $default default value
  614. *
  615. * @return string
  616. */
  617. public static function getMimeByFilename($filename, $default = 'application/octet-stream')
  618. {
  619. return static::getMimeByExtension(pathinfo($filename, PATHINFO_EXTENSION), $default);
  620. }
  621. /**
  622. * Return the mimetype based on existing local file
  623. *
  624. * @param string $filename Path to the file
  625. *
  626. * @return string|bool
  627. */
  628. public static function getMimeByLocalFile($filename, $default = 'application/octet-stream')
  629. {
  630. $type = false;
  631. // For local files we can detect type by the file content.
  632. if (!stream_is_local($filename) || !file_exists($filename)) {
  633. return false;
  634. }
  635. // Prefer using finfo if it exists.
  636. if (\extension_loaded('fileinfo')) {
  637. $finfo = finfo_open(FILEINFO_SYMLINK | FILEINFO_MIME_TYPE);
  638. $type = finfo_file($finfo, $filename);
  639. finfo_close($finfo);
  640. } else {
  641. // Fall back to use getimagesize() if it is available (not recommended, but better than nothing)
  642. $info = @getimagesize($filename);
  643. if ($info) {
  644. $type = $info['mime'];
  645. }
  646. }
  647. return $type ?: static::getMimeByFilename($filename, $default);
  648. }
  649. /**
  650. * Returns true if filename is considered safe.
  651. *
  652. * @param string $filename
  653. * @return bool
  654. */
  655. public static function checkFilename($filename)
  656. {
  657. $dangerous_extensions = Grav::instance()['config']->get('security.uploads_dangerous_extensions', []);
  658. array_walk($dangerous_extensions, function(&$val) {
  659. $val = '.' . $val;
  660. });
  661. $extension = '.' . pathinfo($filename, PATHINFO_EXTENSION);
  662. return !(
  663. // Empty filenames are not allowed.
  664. !$filename
  665. // Filename should not contain horizontal/vertical tabs, newlines, nils or back/forward slashes.
  666. || strtr($filename, "\t\v\n\r\0\\/", '_______') !== $filename
  667. // Filename should not start or end with dot or space.
  668. || trim($filename, '. ') !== $filename
  669. // Filename should not contain .php in it.
  670. || static::contains($extension, $dangerous_extensions)
  671. );
  672. }
  673. /**
  674. * Normalize path by processing relative `.` and `..` syntax and merging path
  675. *
  676. * @param string $path
  677. *
  678. * @return string
  679. */
  680. public static function normalizePath($path)
  681. {
  682. // Resolve any streams
  683. /** @var UniformResourceLocator $locator */
  684. $locator = Grav::instance()['locator'];
  685. if ($locator->isStream($path)) {
  686. $path = $locator->findResource($path);
  687. }
  688. // Set root properly for any URLs
  689. $root = '';
  690. preg_match(self::ROOTURL_REGEX, $path, $matches);
  691. if ($matches) {
  692. $root = $matches[1];
  693. $path = $matches[2];
  694. }
  695. // Strip off leading / to ensure explode is accurate
  696. if (Utils::startsWith($path,'/')) {
  697. $root .= '/';
  698. $path = ltrim($path, '/');
  699. }
  700. // If there are any relative paths (..) handle those
  701. if (Utils::contains($path, '..')) {
  702. $segments = explode('/', trim($path, '/'));
  703. $ret = [];
  704. foreach ($segments as $segment) {
  705. if (($segment === '.') || $segment === '') {
  706. continue;
  707. }
  708. if ($segment === '..') {
  709. array_pop($ret);
  710. } else {
  711. $ret[] = $segment;
  712. }
  713. }
  714. $path = implode('/', $ret);
  715. }
  716. // Stick everything back together
  717. $normalized = $root . $path;
  718. return $normalized;
  719. }
  720. /**
  721. * Check whether a function is disabled in the PHP settings
  722. *
  723. * @param string $function the name of the function to check
  724. *
  725. * @return bool
  726. */
  727. public static function isFunctionDisabled($function)
  728. {
  729. return \in_array($function, explode(',', ini_get('disable_functions')), true);
  730. }
  731. /**
  732. * Get the formatted timezones list
  733. *
  734. * @return array
  735. */
  736. public static function timezones()
  737. {
  738. $timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL);
  739. $offsets = [];
  740. $testDate = new \DateTime();
  741. foreach ($timezones as $zone) {
  742. $tz = new \DateTimeZone($zone);
  743. $offsets[$zone] = $tz->getOffset($testDate);
  744. }
  745. asort($offsets);
  746. $timezone_list = [];
  747. foreach ($offsets as $timezone => $offset) {
  748. $offset_prefix = $offset < 0 ? '-' : '+';
  749. $offset_formatted = gmdate('H:i', abs($offset));
  750. $pretty_offset = "UTC${offset_prefix}${offset_formatted}";
  751. $timezone_list[$timezone] = "(${pretty_offset}) ".str_replace('_', ' ', $timezone);
  752. }
  753. return $timezone_list;
  754. }
  755. /**
  756. * Recursively filter an array, filtering values by processing them through the $fn function argument
  757. *
  758. * @param array $source the Array to filter
  759. * @param callable $fn the function to pass through each array item
  760. *
  761. * @return array
  762. */
  763. public static function arrayFilterRecursive(Array $source, $fn)
  764. {
  765. $result = [];
  766. foreach ($source as $key => $value) {
  767. if (is_array($value)) {
  768. $result[$key] = static::arrayFilterRecursive($value, $fn);
  769. continue;
  770. }
  771. if ($fn($key, $value)) {
  772. $result[$key] = $value; // KEEP
  773. continue;
  774. }
  775. }
  776. return $result;
  777. }
  778. /**
  779. * Flatten an array
  780. *
  781. * @param array $array
  782. * @return array
  783. */
  784. public static function arrayFlatten($array)
  785. {
  786. $flatten = array();
  787. foreach ($array as $key => $inner) {
  788. if (is_array($inner)) {
  789. foreach ($inner as $inner_key => $value) {
  790. $flatten[$inner_key] = $value;
  791. }
  792. } else {
  793. $flatten[$key] = $inner;
  794. }
  795. }
  796. return $flatten;
  797. }
  798. /**
  799. * Flatten a multi-dimensional associative array into dot notation
  800. *
  801. * @param array $array
  802. * @param string $prepend
  803. * @return array
  804. */
  805. public static function arrayFlattenDotNotation($array, $prepend = '')
  806. {
  807. $results = array();
  808. foreach ($array as $key => $value) {
  809. if (is_array($value)) {
  810. $results = array_merge($results, static::arrayFlattenDotNotation($value, $prepend.$key.'.'));
  811. } else {
  812. $results[$prepend.$key] = $value;
  813. }
  814. }
  815. return $results;
  816. }
  817. /**
  818. * Opposite of flatten, convert flat dot notation array to multi dimensional array
  819. *
  820. * @param array $array
  821. * @param string $separator
  822. * @return array
  823. */
  824. public static function arrayUnflattenDotNotation($array, $separator = '.')
  825. {
  826. $newArray = [];
  827. foreach ($array as $key => $value) {
  828. $dots = explode($separator, $key);
  829. if (\count($dots) > 1) {
  830. $last = &$newArray[$dots[0]];
  831. foreach ($dots as $k => $dot) {
  832. if ($k === 0) {
  833. continue;
  834. }
  835. $last = &$last[$dot];
  836. }
  837. $last = $value;
  838. } else {
  839. $newArray[$key] = $value;
  840. }
  841. }
  842. return $newArray;
  843. }
  844. /**
  845. * Checks if the passed path contains the language code prefix
  846. *
  847. * @param string $string The path
  848. *
  849. * @return bool
  850. */
  851. public static function pathPrefixedByLangCode($string)
  852. {
  853. if (strlen($string) <= 3) {
  854. return false;
  855. }
  856. $languages_enabled = Grav::instance()['config']->get('system.languages.supported', []);
  857. return $string[0] === '/' && $string[3] === '/' && \in_array(substr($string, 1, 2), $languages_enabled, true);
  858. }
  859. /**
  860. * Get the timestamp of a date
  861. *
  862. * @param string $date a String expressed in the system.pages.dateformat.default format, with fallback to a
  863. * strtotime argument
  864. * @param string $format a date format to use if possible
  865. * @return int the timestamp
  866. */
  867. public static function date2timestamp($date, $format = null)
  868. {
  869. $config = Grav::instance()['config'];
  870. $dateformat = $format ?: $config->get('system.pages.dateformat.default');
  871. // try to use DateTime and default format
  872. if ($dateformat) {
  873. $datetime = \DateTime::createFromFormat($dateformat, $date);
  874. } else {
  875. $datetime = new \DateTime($date);
  876. }
  877. // fallback to strtotime() if DateTime approach failed
  878. if ($datetime !== false) {
  879. return $datetime->getTimestamp();
  880. }
  881. return strtotime($date);
  882. }
  883. /**
  884. * @param array $array
  885. * @param string $path
  886. * @param null $default
  887. * @return mixed
  888. *
  889. * @deprecated 1.5 Use ->getDotNotation() method instead.
  890. */
  891. public static function resolve(array $array, $path, $default = null)
  892. {
  893. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->getDotNotation() method instead', E_USER_DEPRECATED);
  894. return static::getDotNotation($array, $path, $default);
  895. }
  896. /**
  897. * Checks if a value is positive
  898. *
  899. * @param string $value
  900. *
  901. * @return boolean
  902. */
  903. public static function isPositive($value)
  904. {
  905. return in_array($value, [true, 1, '1', 'yes', 'on', 'true'], true);
  906. }
  907. /**
  908. * Generates a nonce string to be hashed. Called by self::getNonce()
  909. * We removed the IP portion in this version because it causes too many inconsistencies
  910. * with reverse proxy setups.
  911. *
  912. * @param string $action
  913. * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours)
  914. *
  915. * @return string the nonce string
  916. */
  917. private static function generateNonceString($action, $previousTick = false)
  918. {
  919. $username = '';
  920. if (isset(Grav::instance()['user'])) {
  921. $user = Grav::instance()['user'];
  922. $username = $user->username;
  923. }
  924. $token = session_id();
  925. $i = self::nonceTick();
  926. if ($previousTick) {
  927. $i--;
  928. }
  929. return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . Grav::instance()['config']->get('security.salt'));
  930. }
  931. /**
  932. * Get the time-dependent variable for nonce creation.
  933. *
  934. * Now a tick lasts a day. Once the day is passed, the nonce is not valid any more. Find a better way
  935. * to ensure nonces issued near the end of the day do not expire in that small amount of time
  936. *
  937. * @return int the time part of the nonce. Changes once every 24 hours
  938. */
  939. private static function nonceTick()
  940. {
  941. $secondsInHalfADay = 60 * 60 * 12;
  942. return (int)ceil(time() / $secondsInHalfADay);
  943. }
  944. /**
  945. * Creates a hashed nonce tied to the passed action. Tied to the current user and time. The nonce for a given
  946. * action is the same for 12 hours.
  947. *
  948. * @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage)
  949. * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours)
  950. *
  951. * @return string the nonce
  952. */
  953. public static function getNonce($action, $previousTick = false)
  954. {
  955. // Don't regenerate this again if not needed
  956. if (isset(static::$nonces[$action][$previousTick])) {
  957. return static::$nonces[$action][$previousTick];
  958. }
  959. $nonce = md5(self::generateNonceString($action, $previousTick));
  960. static::$nonces[$action][$previousTick] = $nonce;
  961. return static::$nonces[$action][$previousTick];
  962. }
  963. /**
  964. * Verify the passed nonce for the give action
  965. *
  966. * @param string|string[] $nonce the nonce to verify
  967. * @param string $action the action to verify the nonce to
  968. *
  969. * @return boolean verified or not
  970. */
  971. public static function verifyNonce($nonce, $action)
  972. {
  973. //Safety check for multiple nonces
  974. if (is_array($nonce)) {
  975. $nonce = array_shift($nonce);
  976. }
  977. //Nonce generated 0-12 hours ago
  978. if ($nonce === self::getNonce($action)) {
  979. return true;
  980. }
  981. //Nonce generated 12-24 hours ago
  982. $previousTick = true;
  983. return $nonce === self::getNonce($action, $previousTick);
  984. }
  985. /**
  986. * Simple helper method to get whether or not the admin plugin is active
  987. *
  988. * @return bool
  989. */
  990. public static function isAdminPlugin()
  991. {
  992. if (isset(Grav::instance()['admin'])) {
  993. return true;
  994. }
  995. return false;
  996. }
  997. /**
  998. * Get a portion of an array (passed by reference) with dot-notation key
  999. *
  1000. * @param array $array
  1001. * @param string|int $key
  1002. * @param null $default
  1003. * @return mixed
  1004. */
  1005. public static function getDotNotation($array, $key, $default = null)
  1006. {
  1007. if (null === $key) {
  1008. return $array;
  1009. }
  1010. if (isset($array[$key])) {
  1011. return $array[$key];
  1012. }
  1013. foreach (explode('.', $key) as $segment) {
  1014. if (!is_array($array) || !array_key_exists($segment, $array)) {
  1015. return $default;
  1016. }
  1017. $array = $array[$segment];
  1018. }
  1019. return $array;
  1020. }
  1021. /**
  1022. * Set portion of array (passed by reference) for a dot-notation key
  1023. * and set the value
  1024. *
  1025. * @param array $array
  1026. * @param string|int $key
  1027. * @param mixed $value
  1028. * @param bool $merge
  1029. *
  1030. * @return mixed
  1031. */
  1032. public static function setDotNotation(&$array, $key, $value, $merge = false)
  1033. {
  1034. if (null === $key) {
  1035. return $array = $value;
  1036. }
  1037. $keys = explode('.', $key);
  1038. while (count($keys) > 1) {
  1039. $key = array_shift($keys);
  1040. if ( ! isset($array[$key]) || ! is_array($array[$key]))
  1041. {
  1042. $array[$key] = array();
  1043. }
  1044. $array =& $array[$key];
  1045. }
  1046. $key = array_shift($keys);
  1047. if (!$merge || !isset($array[$key])) {
  1048. $array[$key] = $value;
  1049. } else {
  1050. $array[$key] = array_merge($array[$key], $value);
  1051. }
  1052. return $array;
  1053. }
  1054. /**
  1055. * Utility method to determine if the current OS is Windows
  1056. *
  1057. * @return bool
  1058. */
  1059. public static function isWindows()
  1060. {
  1061. return strncasecmp(PHP_OS, 'WIN', 3) === 0;
  1062. }
  1063. /**
  1064. * Utility to determine if the server running PHP is Apache
  1065. *
  1066. * @return bool
  1067. */
  1068. public static function isApache() {
  1069. return isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false;
  1070. }
  1071. /**
  1072. * Sort a multidimensional array by another array of ordered keys
  1073. *
  1074. * @param array $array
  1075. * @param array $orderArray
  1076. * @return array
  1077. */
  1078. public static function sortArrayByArray(array $array, array $orderArray)
  1079. {
  1080. $ordered = array();
  1081. foreach ($orderArray as $key) {
  1082. if (array_key_exists($key, $array)) {
  1083. $ordered[$key] = $array[$key];
  1084. unset($array[$key]);
  1085. }
  1086. }
  1087. return $ordered + $array;
  1088. }
  1089. /**
  1090. * Sort an array by a key value in the array
  1091. *
  1092. * @param mixed $array
  1093. * @param string|int $array_key
  1094. * @param int $direction
  1095. * @param int $sort_flags
  1096. * @return array
  1097. */
  1098. public static function sortArrayByKey($array, $array_key, $direction = SORT_DESC, $sort_flags = SORT_REGULAR)
  1099. {
  1100. $output = [];
  1101. if (!is_array($array) || !$array) {
  1102. return $output;
  1103. }
  1104. foreach ($array as $key => $row) {
  1105. $output[$key] = $row[$array_key];
  1106. }
  1107. array_multisort($output, $direction, $sort_flags, $array);
  1108. return $array;
  1109. }
  1110. /**
  1111. * Get's path based on a token
  1112. *
  1113. * @param string $path
  1114. * @param PageInterface|null $page
  1115. * @return string
  1116. * @throws \RuntimeException
  1117. */
  1118. public static function getPagePathFromToken($path, PageInterface $page = null)
  1119. {
  1120. $path_parts = pathinfo($path);
  1121. $grav = Grav::instance();
  1122. $basename = '';
  1123. if (isset($path_parts['extension'])) {
  1124. $basename = '/' . $path_parts['basename'];
  1125. $path = rtrim($path_parts['dirname'], ':');
  1126. }
  1127. $regex = '/(@self|self@)|((?:@page|page@):(?:.*))|((?:@theme|theme@):(?:.*))/';
  1128. preg_match($regex, $path, $matches);
  1129. if ($matches) {
  1130. if ($matches[1]) {
  1131. if (null === $page) {
  1132. throw new \RuntimeException('Page not available for this self@ reference');
  1133. }
  1134. } elseif ($matches[2]) {
  1135. // page@
  1136. $parts = explode(':', $path);
  1137. $route = $parts[1];
  1138. $page = $grav['page']->find($route);
  1139. } elseif ($matches[3]) {
  1140. // theme@
  1141. $parts = explode(':', $path);
  1142. $route = $parts[1];
  1143. $theme = str_replace(ROOT_DIR, '', $grav['locator']->findResource("theme://"));
  1144. return $theme . $route . $basename;
  1145. }
  1146. } else {
  1147. return $path . $basename;
  1148. }
  1149. if (!$page) {
  1150. throw new \RuntimeException('Page route not found: ' . $path);
  1151. }
  1152. $path = str_replace($matches[0], rtrim($page->relativePagePath(), '/'), $path);
  1153. return $path . $basename;
  1154. }
  1155. public static function getUploadLimit()
  1156. {
  1157. static $max_size = -1;
  1158. if ($max_size < 0) {
  1159. $post_max_size = static::parseSize(ini_get('post_max_size'));
  1160. if ($post_max_size > 0) {
  1161. $max_size = $post_max_size;
  1162. }
  1163. $upload_max = static::parseSize(ini_get('upload_max_filesize'));
  1164. if ($upload_max > 0 && $upload_max < $max_size) {
  1165. $max_size = $upload_max;
  1166. }
  1167. }
  1168. return $max_size;
  1169. }
  1170. /**
  1171. * Convert bytes to the unit specified by the $to parameter.
  1172. *
  1173. * @param int $bytes The filesize in Bytes.
  1174. * @param string $to The unit type to convert to. Accepts K, M, or G for Kilobytes, Megabytes, or Gigabytes, respectively.
  1175. * @param int $decimal_places The number of decimal places to return.
  1176. *
  1177. * @return int Returns only the number of units, not the type letter. Returns 0 if the $to unit type is out of scope.
  1178. *
  1179. */
  1180. public static function convertSize($bytes, $to, $decimal_places = 1)
  1181. {
  1182. $formulas = array(
  1183. 'K' => number_format($bytes / 1024, $decimal_places),
  1184. 'M' => number_format($bytes / 1048576, $decimal_places),
  1185. 'G' => number_format($bytes / 1073741824, $decimal_places)
  1186. );
  1187. return $formulas[$to] ?? 0;
  1188. }
  1189. /**
  1190. * Return a pretty size based on bytes
  1191. *
  1192. * @param int $bytes
  1193. * @param int $precision
  1194. * @return string
  1195. */
  1196. public static function prettySize($bytes, $precision = 2)
  1197. {
  1198. $units = array('B', 'KB', 'MB', 'GB', 'TB');
  1199. $bytes = max($bytes, 0);
  1200. $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
  1201. $pow = min($pow, count($units) - 1);
  1202. // Uncomment one of the following alternatives
  1203. $bytes /= pow(1024, $pow);
  1204. // $bytes /= (1 << (10 * $pow));
  1205. return round($bytes, $precision) . ' ' . $units[$pow];
  1206. }
  1207. /**
  1208. * Parse a readable file size and return a value in bytes
  1209. *
  1210. * @param string|int $size
  1211. * @return int
  1212. */
  1213. public static function parseSize($size)
  1214. {
  1215. $unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
  1216. $size = preg_replace('/[^0-9\.]/', '', $size);
  1217. if ($unit) {
  1218. return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
  1219. } else {
  1220. return round($size);
  1221. }
  1222. }
  1223. /**
  1224. * Multibyte-safe Parse URL function
  1225. *
  1226. * @param string $url
  1227. * @return array
  1228. * @throws \InvalidArgumentException
  1229. */
  1230. public static function multibyteParseUrl($url)
  1231. {
  1232. $enc_url = preg_replace_callback(
  1233. '%[^:/@?&=#]+%usD',
  1234. function ($matches) {
  1235. return urlencode($matches[0]);
  1236. },
  1237. $url
  1238. );
  1239. $parts = parse_url($enc_url);
  1240. if($parts === false) {
  1241. throw new \InvalidArgumentException('Malformed URL: ' . $url);
  1242. }
  1243. foreach($parts as $name => $value) {
  1244. $parts[$name] = urldecode($value);
  1245. }
  1246. return $parts;
  1247. }
  1248. /**
  1249. * Process a string as markdown
  1250. *
  1251. * @param string $string
  1252. *
  1253. * @param bool $block Block or Line processing
  1254. * @return string
  1255. */
  1256. public static function processMarkdown($string, $block = true)
  1257. {
  1258. $page = Grav::instance()['page'] ?? null;
  1259. $defaults = Grav::instance()['config']->get('system.pages.markdown');
  1260. // Initialize the preferred variant of Parsedown
  1261. if ($defaults['extra']) {
  1262. $parsedown = new ParsedownExtra($page, $defaults);
  1263. } else {
  1264. $parsedown = new Parsedown($page, $defaults);
  1265. }
  1266. if ($block) {
  1267. $string = $parsedown->text($string);
  1268. } else {
  1269. $string = $parsedown->line($string);
  1270. }
  1271. return $string;
  1272. }
  1273. /**
  1274. * Find the subnet of an ip with CIDR prefix size
  1275. *
  1276. * @param string $ip
  1277. * @param int $prefix
  1278. *
  1279. * @return string
  1280. * @throws \InvalidArgumentException if provided an invalid IP
  1281. */
  1282. public static function getSubnet($ip, $prefix = 64)
  1283. {
  1284. if (!filter_var($ip, FILTER_VALIDATE_IP)) {
  1285. throw new \InvalidArgumentException('Invalid IP: ' . $ip);
  1286. }
  1287. // Packed representation of IP
  1288. $ip = inet_pton($ip);
  1289. // Maximum netmask length = same as packed address
  1290. $len = 8*strlen($ip);
  1291. if ($prefix > $len) $prefix = $len;
  1292. $mask = str_repeat('f', $prefix>>2);
  1293. switch($prefix & 3)
  1294. {
  1295. case 3: $mask .= 'e'; break;
  1296. case 2: $mask .= 'c'; break;
  1297. case 1: $mask .= '8'; break;
  1298. }
  1299. $mask = str_pad($mask, $len>>2, '0');
  1300. // Packed representation of netmask
  1301. $mask = pack('H*', $mask);
  1302. // Bitwise - Take all bits that are both 1 to generate subnet
  1303. $subnet = inet_ntop($ip & $mask);
  1304. return $subnet;
  1305. }
  1306. }