Utils.php 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075
  1. <?php
  2. /**
  3. * @package Grav.Common
  4. *
  5. * @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common;
  9. use DateTime;
  10. use Grav\Common\Helpers\Truncator;
  11. use Grav\Common\Page\Page;
  12. use RocketTheme\Toolbox\Event\Event;
  13. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  14. abstract class Utils
  15. {
  16. protected static $nonces = [];
  17. /**
  18. * Simple helper method to make getting a Grav URL easier
  19. *
  20. * @param $input
  21. * @param bool $domain
  22. * @return bool|null|string
  23. */
  24. public static function url($input, $domain = false)
  25. {
  26. if (!trim((string)$input)) {
  27. return false;
  28. }
  29. if (Grav::instance()['config']->get('system.absolute_urls', false)) {
  30. $domain = true;
  31. }
  32. if (Grav::instance()['uri']->isExternal($input)) {
  33. return $input;
  34. }
  35. $input = ltrim((string)$input, '/');
  36. if (Utils::contains((string)$input, '://')) {
  37. /** @var UniformResourceLocator $locator */
  38. $locator = Grav::instance()['locator'];
  39. $parts = Uri::parseUrl($input);
  40. if ($parts) {
  41. $resource = $locator->findResource("{$parts['scheme']}://{$parts['host']}{$parts['path']}", false);
  42. if (isset($parts['query'])) {
  43. $resource = $resource . '?' . $parts['query'];
  44. }
  45. } else {
  46. // Not a valid URL (can still be a stream).
  47. $resource = $locator->findResource($input, false);
  48. }
  49. } else {
  50. $resource = $input;
  51. }
  52. /** @var Uri $uri */
  53. $uri = Grav::instance()['uri'];
  54. return $resource ? rtrim($uri->rootUrl($domain), '/') . '/' . $resource : null;
  55. }
  56. /**
  57. * Check if the $haystack string starts with the substring $needle
  58. *
  59. * @param string $haystack
  60. * @param string|string[] $needle
  61. *
  62. * @return bool
  63. */
  64. public static function startsWith($haystack, $needle)
  65. {
  66. $status = false;
  67. foreach ((array)$needle as $each_needle) {
  68. $status = $each_needle === '' || strpos($haystack, $each_needle) === 0;
  69. if ($status) {
  70. break;
  71. }
  72. }
  73. return $status;
  74. }
  75. /**
  76. * Check if the $haystack string ends with the substring $needle
  77. *
  78. * @param string $haystack
  79. * @param string|string[] $needle
  80. *
  81. * @return bool
  82. */
  83. public static function endsWith($haystack, $needle)
  84. {
  85. $status = false;
  86. foreach ((array)$needle as $each_needle) {
  87. $status = $each_needle === '' || substr($haystack, -strlen($each_needle)) === $each_needle;
  88. if ($status) {
  89. break;
  90. }
  91. }
  92. return $status;
  93. }
  94. /**
  95. * Check if the $haystack string contains the substring $needle
  96. *
  97. * @param string $haystack
  98. * @param string|string[] $needle
  99. *
  100. * @return bool
  101. */
  102. public static function contains($haystack, $needle)
  103. {
  104. $status = false;
  105. foreach ((array)$needle as $each_needle) {
  106. $status = $each_needle === '' || strpos($haystack, $each_needle) !== false;
  107. if ($status) {
  108. break;
  109. }
  110. }
  111. return $status;
  112. }
  113. /**
  114. * Returns the substring of a string up to a specified needle. if not found, return the whole haystack
  115. *
  116. * @param $haystack
  117. * @param $needle
  118. *
  119. * @return string
  120. */
  121. public static function substrToString($haystack, $needle)
  122. {
  123. if (static::contains($haystack, $needle)) {
  124. return substr($haystack, 0, strpos($haystack, $needle));
  125. }
  126. return $haystack;
  127. }
  128. /**
  129. * Utility method to replace only the first occurrence in a string
  130. *
  131. * @param $search
  132. * @param $replace
  133. * @param $subject
  134. * @return mixed
  135. */
  136. public static function replaceFirstOccurrence($search, $replace, $subject)
  137. {
  138. if (!$search) {
  139. return $subject;
  140. }
  141. $pos = strpos($subject, $search);
  142. if ($pos !== false) {
  143. $subject = substr_replace($subject, $replace, $pos, strlen($search));
  144. }
  145. return $subject;
  146. }
  147. /**
  148. * Utility method to replace only the last occurrence in a string
  149. *
  150. * @param $search
  151. * @param $replace
  152. * @param $subject
  153. * @return mixed
  154. */
  155. public static function replaceLastOccurrence($search, $replace, $subject)
  156. {
  157. $pos = strrpos($subject, $search);
  158. if($pos !== false)
  159. {
  160. $subject = substr_replace($subject, $replace, $pos, strlen($search));
  161. }
  162. return $subject;
  163. }
  164. /**
  165. * Merge two objects into one.
  166. *
  167. * @param object $obj1
  168. * @param object $obj2
  169. *
  170. * @return object
  171. */
  172. public static function mergeObjects($obj1, $obj2)
  173. {
  174. return (object)array_merge((array)$obj1, (array)$obj2);
  175. }
  176. /**
  177. * Recursive Merge with uniqueness
  178. *
  179. * @param $array1
  180. * @param $array2
  181. * @return mixed
  182. */
  183. public static function arrayMergeRecursiveUnique($array1, $array2)
  184. {
  185. if (empty($array1)) {
  186. // Optimize the base case
  187. return $array2;
  188. }
  189. foreach ($array2 as $key => $value) {
  190. if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
  191. $value = static::arrayMergeRecursiveUnique($array1[$key], $value);
  192. }
  193. $array1[$key] = $value;
  194. }
  195. return $array1;
  196. }
  197. /**
  198. * Return the Grav date formats allowed
  199. *
  200. * @return array
  201. */
  202. public static function dateFormats()
  203. {
  204. $now = new DateTime();
  205. $date_formats = [
  206. 'd-m-Y H:i' => 'd-m-Y H:i (e.g. '.$now->format('d-m-Y H:i').')',
  207. 'Y-m-d H:i' => 'Y-m-d H:i (e.g. '.$now->format('Y-m-d H:i').')',
  208. 'm/d/Y h:i a' => 'm/d/Y h:i a (e.g. '.$now->format('m/d/Y h:i a').')',
  209. 'H:i d-m-Y' => 'H:i d-m-Y (e.g. '.$now->format('H:i d-m-Y').')',
  210. 'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. '.$now->format('h:i a m/d/Y').')',
  211. ];
  212. $default_format = Grav::instance()['config']->get('system.pages.dateformat.default');
  213. if ($default_format) {
  214. $date_formats = array_merge([$default_format => $default_format.' (e.g. '.$now->format($default_format).')'], $date_formats);
  215. }
  216. return $date_formats;
  217. }
  218. /**
  219. * Truncate text by number of characters but can cut off words.
  220. *
  221. * @param string $string
  222. * @param int $limit Max number of characters.
  223. * @param bool $up_to_break truncate up to breakpoint after char count
  224. * @param string $break Break point.
  225. * @param string $pad Appended padding to the end of the string.
  226. *
  227. * @return string
  228. */
  229. public static function truncate($string, $limit = 150, $up_to_break = false, $break = " ", $pad = "&hellip;")
  230. {
  231. // return with no change if string is shorter than $limit
  232. if (mb_strlen($string) <= $limit) {
  233. return $string;
  234. }
  235. // is $break present between $limit and the end of the string?
  236. if ($up_to_break && false !== ($breakpoint = mb_strpos($string, $break, $limit))) {
  237. if ($breakpoint < mb_strlen($string) - 1) {
  238. $string = mb_substr($string, 0, $breakpoint) . $pad;
  239. }
  240. } else {
  241. $string = mb_substr($string, 0, $limit) . $pad;
  242. }
  243. return $string;
  244. }
  245. /**
  246. * Truncate text by number of characters in a "word-safe" manor.
  247. *
  248. * @param string $string
  249. * @param int $limit
  250. *
  251. * @return string
  252. */
  253. public static function safeTruncate($string, $limit = 150)
  254. {
  255. return static::truncate($string, $limit, true);
  256. }
  257. /**
  258. * Truncate HTML by number of characters. not "word-safe"!
  259. *
  260. * @param string $text
  261. * @param int $length in characters
  262. * @param string $ellipsis
  263. *
  264. * @return string
  265. */
  266. public static function truncateHtml($text, $length = 100, $ellipsis = '...')
  267. {
  268. return Truncator::truncateLetters($text, $length, $ellipsis);
  269. }
  270. /**
  271. * Truncate HTML by number of characters in a "word-safe" manor.
  272. *
  273. * @param string $text
  274. * @param int $length in words
  275. * @param string $ellipsis
  276. *
  277. * @return string
  278. */
  279. public static function safeTruncateHtml($text, $length = 25, $ellipsis = '...')
  280. {
  281. return Truncator::truncateWords($text, $length, $ellipsis);
  282. }
  283. /**
  284. * Generate a random string of a given length
  285. *
  286. * @param int $length
  287. *
  288. * @return string
  289. */
  290. public static function generateRandomString($length = 5)
  291. {
  292. return substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length);
  293. }
  294. /**
  295. * Provides the ability to download a file to the browser
  296. *
  297. * @param string $file the full path to the file to be downloaded
  298. * @param bool $force_download as opposed to letting browser choose if to download or render
  299. * @param int $sec Throttling, try 0.1 for some speed throttling of downloads
  300. * @param int $bytes Size of chunks to send in bytes. Default is 1024
  301. * @throws \Exception
  302. */
  303. public static function download($file, $force_download = true, $sec = 0, $bytes = 1024)
  304. {
  305. if (file_exists($file)) {
  306. // fire download event
  307. Grav::instance()->fireEvent('onBeforeDownload', new Event(['file' => $file]));
  308. $file_parts = pathinfo($file);
  309. $mimetype = static::getMimeByExtension($file_parts['extension']);
  310. $size = filesize($file); // File size
  311. // clean all buffers
  312. while (ob_get_level()) {
  313. ob_end_clean();
  314. }
  315. // required for IE, otherwise Content-Disposition may be ignored
  316. if (ini_get('zlib.output_compression')) {
  317. ini_set('zlib.output_compression', 'Off');
  318. }
  319. header('Content-Type: ' . $mimetype);
  320. header('Accept-Ranges: bytes');
  321. if ($force_download) {
  322. // output the regular HTTP headers
  323. header('Content-Disposition: attachment; filename="' . $file_parts['basename'] . '"');
  324. }
  325. // multipart-download and download resuming support
  326. if (isset($_SERVER['HTTP_RANGE'])) {
  327. list($a, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
  328. list($range) = explode(',', $range, 2);
  329. list($range, $range_end) = explode('-', $range);
  330. $range = (int)$range;
  331. if (!$range_end) {
  332. $range_end = $size - 1;
  333. } else {
  334. $range_end = (int)$range_end;
  335. }
  336. $new_length = $range_end - $range + 1;
  337. header('HTTP/1.1 206 Partial Content');
  338. header("Content-Length: {$new_length}");
  339. header("Content-Range: bytes {$range}-{$range_end}/{$size}");
  340. } else {
  341. $range = 0;
  342. $new_length = $size;
  343. header('Content-Length: ' . $size);
  344. if (Grav::instance()['config']->get('system.cache.enabled')) {
  345. $expires = Grav::instance()['config']->get('system.pages.expires');
  346. if ($expires > 0) {
  347. $expires_date = gmdate('D, d M Y H:i:s T', time() + $expires);
  348. header('Cache-Control: max-age=' . $expires);
  349. header('Expires: ' . $expires_date);
  350. header('Pragma: cache');
  351. }
  352. header('Last-Modified: ' . gmdate('D, d M Y H:i:s T', filemtime($file)));
  353. // Return 304 Not Modified if the file is already cached in the browser
  354. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
  355. strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= filemtime($file))
  356. {
  357. header('HTTP/1.1 304 Not Modified');
  358. exit();
  359. }
  360. }
  361. }
  362. /* output the file itself */
  363. $chunksize = $bytes * 8; //you may want to change this
  364. $bytes_send = 0;
  365. $fp = @fopen($file, 'rb');
  366. if ($fp) {
  367. if ($range) {
  368. fseek($fp, $range);
  369. }
  370. while (!feof($fp) && (!connection_aborted()) && ($bytes_send < $new_length) ) {
  371. $buffer = fread($fp, $chunksize);
  372. echo($buffer); //echo($buffer); // is also possible
  373. flush();
  374. usleep($sec * 1000000);
  375. $bytes_send += strlen($buffer);
  376. }
  377. fclose($fp);
  378. } else {
  379. throw new \RuntimeException('Error - can not open file.');
  380. }
  381. exit;
  382. }
  383. }
  384. /**
  385. * Return the mimetype based on filename extension
  386. *
  387. * @param string $extension Extension of file (eg "txt")
  388. * @param string $default
  389. *
  390. * @return string
  391. */
  392. public static function getMimeByExtension($extension, $default = 'application/octet-stream')
  393. {
  394. $extension = strtolower($extension);
  395. // look for some standard types
  396. switch ($extension) {
  397. case null:
  398. return $default;
  399. case 'json':
  400. return 'application/json';
  401. case 'html':
  402. return 'text/html';
  403. case 'atom':
  404. return 'application/atom+xml';
  405. case 'rss':
  406. return 'application/rss+xml';
  407. case 'xml':
  408. return 'application/xml';
  409. }
  410. $media_types = Grav::instance()['config']->get('media.types');
  411. if (isset($media_types[$extension])) {
  412. if (isset($media_types[$extension]['mime'])) {
  413. return $media_types[$extension]['mime'];
  414. }
  415. }
  416. return $default;
  417. }
  418. /**
  419. * Return the mimetype based on filename extension
  420. *
  421. * @param string $mime mime type (eg "text/html")
  422. * @param string $default default value
  423. *
  424. * @return string
  425. */
  426. public static function getExtensionByMime($mime, $default = 'html')
  427. {
  428. $mime = strtolower($mime);
  429. // look for some standard mime types
  430. switch ($mime) {
  431. case '*/*':
  432. case 'text/*':
  433. case 'text/html':
  434. return 'html';
  435. case 'application/json':
  436. return 'json';
  437. case 'application/atom+xml':
  438. return 'atom';
  439. case 'application/rss+xml':
  440. return 'rss';
  441. case 'application/xml':
  442. return 'xml';
  443. }
  444. $media_types = (array)Grav::instance()['config']->get('media.types');
  445. foreach ($media_types as $extension => $type) {
  446. if ($extension === 'defaults') {
  447. continue;
  448. }
  449. if (isset($type['mime']) && $type['mime'] === $mime) {
  450. return $extension;
  451. }
  452. }
  453. return $default;
  454. }
  455. /**
  456. * Normalize path by processing relative `.` and `..` syntax and merging path
  457. *
  458. * @param string $path
  459. *
  460. * @return string
  461. */
  462. public static function normalizePath($path)
  463. {
  464. $root = ($path[0] === '/') ? '/' : '';
  465. $segments = explode('/', trim($path, '/'));
  466. $ret = [];
  467. foreach ($segments as $segment) {
  468. if (($segment === '.') || $segment === '') {
  469. continue;
  470. }
  471. if ($segment === '..') {
  472. array_pop($ret);
  473. } else {
  474. $ret[] = $segment;
  475. }
  476. }
  477. return $root . implode('/', $ret);
  478. }
  479. /**
  480. * Check whether a function is disabled in the PHP settings
  481. *
  482. * @param string $function the name of the function to check
  483. *
  484. * @return bool
  485. */
  486. public static function isFunctionDisabled($function)
  487. {
  488. return in_array($function, explode(',', ini_get('disable_functions')), true);
  489. }
  490. /**
  491. * Get the formatted timezones list
  492. *
  493. * @return array
  494. */
  495. public static function timezones()
  496. {
  497. $timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL);
  498. $offsets = [];
  499. $testDate = new \DateTime;
  500. foreach ($timezones as $zone) {
  501. $tz = new \DateTimeZone($zone);
  502. $offsets[$zone] = $tz->getOffset($testDate);
  503. }
  504. asort($offsets);
  505. $timezone_list = [];
  506. foreach ($offsets as $timezone => $offset) {
  507. $offset_prefix = $offset < 0 ? '-' : '+';
  508. $offset_formatted = gmdate('H:i', abs($offset));
  509. $pretty_offset = "UTC${offset_prefix}${offset_formatted}";
  510. $timezone_list[$timezone] = "(${pretty_offset}) ".str_replace('_', ' ', $timezone);
  511. }
  512. return $timezone_list;
  513. }
  514. /**
  515. * Recursively filter an array, filtering values by processing them through the $fn function argument
  516. *
  517. * @param array $source the Array to filter
  518. * @param callable $fn the function to pass through each array item
  519. *
  520. * @return array
  521. */
  522. public static function arrayFilterRecursive(Array $source, $fn)
  523. {
  524. $result = [];
  525. foreach ($source as $key => $value) {
  526. if (is_array($value)) {
  527. $result[$key] = static::arrayFilterRecursive($value, $fn);
  528. continue;
  529. }
  530. if ($fn($key, $value)) {
  531. $result[$key] = $value; // KEEP
  532. continue;
  533. }
  534. }
  535. return $result;
  536. }
  537. /**
  538. * Flatten an array
  539. *
  540. * @param array $array
  541. * @return array
  542. */
  543. public static function arrayFlatten($array)
  544. {
  545. $flatten = array();
  546. foreach ($array as $key => $inner){
  547. if (is_array($inner)) {
  548. foreach ($inner as $inner_key => $value) {
  549. $flatten[$inner_key] = $value;
  550. }
  551. } else {
  552. $flatten[$key] = $inner;
  553. }
  554. }
  555. return $flatten;
  556. }
  557. /**
  558. * Checks if the passed path contains the language code prefix
  559. *
  560. * @param string $string The path
  561. *
  562. * @return bool
  563. */
  564. public static function pathPrefixedByLangCode($string)
  565. {
  566. if (strlen($string) <= 3) {
  567. return false;
  568. }
  569. $languages_enabled = Grav::instance()['config']->get('system.languages.supported', []);
  570. if ($string[0] === '/' && $string[3] === '/' && in_array(substr($string, 1, 2), $languages_enabled)) {
  571. return true;
  572. }
  573. return false;
  574. }
  575. /**
  576. * Get the timestamp of a date
  577. *
  578. * @param string $date a String expressed in the system.pages.dateformat.default format, with fallback to a
  579. * strtotime argument
  580. * @param string $format a date format to use if possible
  581. * @return int the timestamp
  582. */
  583. public static function date2timestamp($date, $format = null)
  584. {
  585. $config = Grav::instance()['config'];
  586. $dateformat = $format ?: $config->get('system.pages.dateformat.default');
  587. // try to use DateTime and default format
  588. if ($dateformat) {
  589. $datetime = DateTime::createFromFormat($dateformat, $date);
  590. } else {
  591. $datetime = new DateTime($date);
  592. }
  593. // fallback to strtotime() if DateTime approach failed
  594. if ($datetime !== false) {
  595. return $datetime->getTimestamp();
  596. }
  597. return strtotime($date);
  598. }
  599. /**
  600. * @param array $array
  601. * @param string $path
  602. * @param null $default
  603. * @return mixed
  604. *
  605. * @deprecated Use getDotNotation() method instead
  606. */
  607. public static function resolve(array $array, $path, $default = null)
  608. {
  609. return static::getDotNotation($array, $path, $default);
  610. }
  611. /**
  612. * Checks if a value is positive
  613. *
  614. * @param string $value
  615. *
  616. * @return boolean
  617. */
  618. public static function isPositive($value)
  619. {
  620. return in_array($value, [true, 1, '1', 'yes', 'on', 'true'], true);
  621. }
  622. /**
  623. * Generates a nonce string to be hashed. Called by self::getNonce()
  624. * We removed the IP portion in this version because it causes too many inconsistencies
  625. * with reverse proxy setups.
  626. *
  627. * @param string $action
  628. * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours)
  629. *
  630. * @return string the nonce string
  631. */
  632. private static function generateNonceString($action, $previousTick = false)
  633. {
  634. $username = '';
  635. if (isset(Grav::instance()['user'])) {
  636. $user = Grav::instance()['user'];
  637. $username = $user->username;
  638. }
  639. $token = session_id();
  640. $i = self::nonceTick();
  641. if ($previousTick) {
  642. $i--;
  643. }
  644. return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . Grav::instance()['config']->get('security.salt'));
  645. }
  646. /**
  647. * Get the time-dependent variable for nonce creation.
  648. *
  649. * Now a tick lasts a day. Once the day is passed, the nonce is not valid any more. Find a better way
  650. * to ensure nonces issued near the end of the day do not expire in that small amount of time
  651. *
  652. * @return int the time part of the nonce. Changes once every 24 hours
  653. */
  654. private static function nonceTick()
  655. {
  656. $secondsInHalfADay = 60 * 60 * 12;
  657. return (int)ceil(time() / $secondsInHalfADay);
  658. }
  659. /**
  660. * Creates a hashed nonce tied to the passed action. Tied to the current user and time. The nonce for a given
  661. * action is the same for 12 hours.
  662. *
  663. * @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage)
  664. * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours)
  665. *
  666. * @return string the nonce
  667. */
  668. public static function getNonce($action, $previousTick = false)
  669. {
  670. // Don't regenerate this again if not needed
  671. if (isset(static::$nonces[$action][$previousTick])) {
  672. return static::$nonces[$action][$previousTick];
  673. }
  674. $nonce = md5(self::generateNonceString($action, $previousTick));
  675. static::$nonces[$action][$previousTick] = $nonce;
  676. return static::$nonces[$action][$previousTick];
  677. }
  678. /**
  679. * Verify the passed nonce for the give action
  680. *
  681. * @param string|string[] $nonce the nonce to verify
  682. * @param string $action the action to verify the nonce to
  683. *
  684. * @return boolean verified or not
  685. */
  686. public static function verifyNonce($nonce, $action)
  687. {
  688. //Safety check for multiple nonces
  689. if (is_array($nonce)) {
  690. $nonce = array_shift($nonce);
  691. }
  692. //Nonce generated 0-12 hours ago
  693. if ($nonce === self::getNonce($action)) {
  694. return true;
  695. }
  696. //Nonce generated 12-24 hours ago
  697. $previousTick = true;
  698. if ($nonce === self::getNonce($action, $previousTick)) {
  699. return true;
  700. }
  701. //Invalid nonce
  702. return false;
  703. }
  704. /**
  705. * Simple helper method to get whether or not the admin plugin is active
  706. *
  707. * @return bool
  708. */
  709. public static function isAdminPlugin()
  710. {
  711. if (isset(Grav::instance()['admin'])) {
  712. return true;
  713. }
  714. return false;
  715. }
  716. /**
  717. * Get a portion of an array (passed by reference) with dot-notation key
  718. *
  719. * @param $array
  720. * @param $key
  721. * @param null $default
  722. * @return mixed
  723. */
  724. public static function getDotNotation($array, $key, $default = null)
  725. {
  726. if (null === $key) {
  727. return $array;
  728. }
  729. if (isset($array[$key])) {
  730. return $array[$key];
  731. }
  732. foreach (explode('.', $key) as $segment) {
  733. if (!is_array($array) || !array_key_exists($segment, $array)) {
  734. return $default;
  735. }
  736. $array = $array[$segment];
  737. }
  738. return $array;
  739. }
  740. /**
  741. * Set portion of array (passed by reference) for a dot-notation key
  742. * and set the value
  743. *
  744. * @param $array
  745. * @param $key
  746. * @param $value
  747. * @param bool $merge
  748. *
  749. * @return mixed
  750. */
  751. public static function setDotNotation(&$array, $key, $value, $merge = false)
  752. {
  753. if (null === $key) {
  754. return $array = $value;
  755. }
  756. $keys = explode('.', $key);
  757. while (count($keys) > 1) {
  758. $key = array_shift($keys);
  759. if ( ! isset($array[$key]) || ! is_array($array[$key]))
  760. {
  761. $array[$key] = array();
  762. }
  763. $array =& $array[$key];
  764. }
  765. $key = array_shift($keys);
  766. if (!$merge || !isset($array[$key])) {
  767. $array[$key] = $value;
  768. } else {
  769. $array[$key] = array_merge($array[$key], $value);
  770. }
  771. return $array;
  772. }
  773. /**
  774. * Utility method to determine if the current OS is Windows
  775. *
  776. * @return bool
  777. */
  778. public static function isWindows()
  779. {
  780. return strncasecmp(PHP_OS, 'WIN', 3) === 0;
  781. }
  782. /**
  783. * Utility to determine if the server running PHP is Apache
  784. *
  785. * @return bool
  786. */
  787. public static function isApache() {
  788. return isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false;
  789. }
  790. /**
  791. * Sort a multidimensional array by another array of ordered keys
  792. *
  793. * @param array $array
  794. * @param array $orderArray
  795. * @return array
  796. */
  797. public static function sortArrayByArray(array $array, array $orderArray)
  798. {
  799. $ordered = array();
  800. foreach ($orderArray as $key) {
  801. if (array_key_exists($key, $array)) {
  802. $ordered[$key] = $array[$key];
  803. unset($array[$key]);
  804. }
  805. }
  806. return $ordered + $array;
  807. }
  808. /**
  809. * Sort an array by a key value in the array
  810. *
  811. * @param $array
  812. * @param $array_key
  813. * @param int $direction
  814. * @param int $sort_flags
  815. * @return array
  816. */
  817. public static function sortArrayByKey($array, $array_key, $direction = SORT_DESC, $sort_flags = SORT_REGULAR )
  818. {
  819. $output = [];
  820. if (!is_array($array) || !$array) {
  821. return $output;
  822. }
  823. foreach ($array as $key => $row) {
  824. $output[$key] = $row[$array_key];
  825. }
  826. array_multisort($output, $direction, $sort_flags, $array);
  827. return $array;
  828. }
  829. /**
  830. * Get's path based on a token
  831. *
  832. * @param $path
  833. * @param Page|null $page
  834. * @return string
  835. * @throws \RuntimeException
  836. */
  837. public static function getPagePathFromToken($path, $page = null)
  838. {
  839. $path_parts = pathinfo($path);
  840. $grav = Grav::instance();
  841. $basename = '';
  842. if (isset($path_parts['extension'])) {
  843. $basename = '/' . $path_parts['basename'];
  844. $path = rtrim($path_parts['dirname'], ':');
  845. }
  846. $regex = '/(@self|self@)|((?:@page|page@):(?:.*))|((?:@theme|theme@):(?:.*))/';
  847. preg_match($regex, $path, $matches);
  848. if ($matches) {
  849. if ($matches[1]) {
  850. if (null === $page) {
  851. throw new \RuntimeException('Page not available for this self@ reference');
  852. }
  853. } elseif ($matches[2]) {
  854. // page@
  855. $parts = explode(':', $path);
  856. $route = $parts[1];
  857. $page = $grav['page']->find($route);
  858. } elseif ($matches[3]) {
  859. // theme@
  860. $parts = explode(':', $path);
  861. $route = $parts[1];
  862. $theme = str_replace(ROOT_DIR, '', $grav['locator']->findResource("theme://"));
  863. return $theme . $route . $basename;
  864. }
  865. } else {
  866. return $path . $basename;
  867. }
  868. if (!$page) {
  869. throw new \RuntimeException('Page route not found: ' . $path);
  870. }
  871. $path = str_replace($matches[0], rtrim($page->relativePagePath(), '/'), $path);
  872. return $path . $basename;
  873. }
  874. public static function getUploadLimit()
  875. {
  876. static $max_size = -1;
  877. if ($max_size < 0) {
  878. $post_max_size = static::parseSize(ini_get('post_max_size'));
  879. if ($post_max_size > 0) {
  880. $max_size = $post_max_size;
  881. }
  882. $upload_max = static::parseSize(ini_get('upload_max_filesize'));
  883. if ($upload_max > 0 && $upload_max < $max_size) {
  884. $max_size = $upload_max;
  885. }
  886. }
  887. return $max_size;
  888. }
  889. /**
  890. * Parse a readable file size and return a value in bytes
  891. *
  892. * @param $size
  893. * @return int
  894. */
  895. public static function parseSize($size)
  896. {
  897. $unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
  898. $size = preg_replace('/[^0-9\.]/', '', $size);
  899. if ($unit) {
  900. return (int)($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
  901. }
  902. return (int)$size;
  903. }
  904. /**
  905. * Multibyte-safe Parse URL function
  906. *
  907. * @param $url
  908. * @return mixed
  909. * @throws \InvalidArgumentException
  910. */
  911. public static function multibyteParseUrl($url)
  912. {
  913. $enc_url = preg_replace_callback(
  914. '%[^:/@?&=#]+%usD',
  915. function ($matches) {
  916. return urlencode($matches[0]);
  917. },
  918. $url
  919. );
  920. $parts = parse_url($enc_url);
  921. if($parts === false) {
  922. throw new \InvalidArgumentException('Malformed URL: ' . $url);
  923. }
  924. foreach($parts as $name => $value) {
  925. $parts[$name] = urldecode($value);
  926. }
  927. return $parts;
  928. }
  929. }