Utils.php 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181
  1. <?php
  2. /**
  3. * @package Grav\Common
  4. *
  5. * @copyright Copyright (c) 2015 - 2022 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 DateTimeZone;
  11. use Exception;
  12. use Grav\Common\Flex\Types\Pages\PageObject;
  13. use Grav\Common\Helpers\Truncator;
  14. use Grav\Common\Page\Interfaces\PageInterface;
  15. use Grav\Common\Markdown\Parsedown;
  16. use Grav\Common\Markdown\ParsedownExtra;
  17. use Grav\Common\Page\Markdown\Excerpts;
  18. use Grav\Common\Page\Pages;
  19. use Grav\Framework\Flex\Flex;
  20. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  21. use Grav\Framework\Media\Interfaces\MediaInterface;
  22. use InvalidArgumentException;
  23. use Negotiation\Accept;
  24. use Negotiation\Negotiator;
  25. use RocketTheme\Toolbox\Event\Event;
  26. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  27. use RuntimeException;
  28. use function array_key_exists;
  29. use function array_slice;
  30. use function count;
  31. use function extension_loaded;
  32. use function function_exists;
  33. use function in_array;
  34. use function is_array;
  35. use function is_callable;
  36. use function is_string;
  37. use function strlen;
  38. /**
  39. * Class Utils
  40. * @package Grav\Common
  41. */
  42. abstract class Utils
  43. {
  44. /** @var array */
  45. protected static $nonces = [];
  46. protected const ROOTURL_REGEX = '{^((?:http[s]?:\/\/[^\/]+)|(?:\/\/[^\/]+))(.*)}';
  47. // ^((?:http[s]?:)?[\/]?(?:\/))
  48. /**
  49. * Simple helper method to make getting a Grav URL easier
  50. *
  51. * @param string|object $input
  52. * @param bool $domain
  53. * @param bool $fail_gracefully
  54. * @return string|false
  55. */
  56. public static function url($input, $domain = false, $fail_gracefully = false)
  57. {
  58. if ((!is_string($input) && !is_callable([$input, '__toString'])) || !trim($input)) {
  59. if ($fail_gracefully) {
  60. $input = '/';
  61. } else {
  62. return false;
  63. }
  64. }
  65. $input = (string)$input;
  66. if (Uri::isExternal($input)) {
  67. return $input;
  68. }
  69. $grav = Grav::instance();
  70. /** @var Uri $uri */
  71. $uri = $grav['uri'];
  72. $resource = false;
  73. if (static::contains((string)$input, '://')) {
  74. /** @var UniformResourceLocator $locator */
  75. $locator = $grav['locator'];
  76. $parts = Uri::parseUrl($input);
  77. if (is_array($parts)) {
  78. // Make sure we always have scheme, host, port and path.
  79. $scheme = $parts['scheme'] ?? '';
  80. $host = $parts['host'] ?? '';
  81. $port = $parts['port'] ?? '';
  82. $path = $parts['path'] ?? '';
  83. if ($scheme && !$port) {
  84. // If URL has a scheme, we need to check if it's one of Grav streams.
  85. if (!$locator->schemeExists($scheme)) {
  86. // If scheme does not exists as a stream, assume it's external.
  87. return str_replace(' ', '%20', $input);
  88. }
  89. // Attempt to find the resource (because of parse_url() we need to put host back to path).
  90. $resource = $locator->findResource("{$scheme}://{$host}{$path}", false);
  91. if ($resource === false) {
  92. if (!$fail_gracefully) {
  93. return false;
  94. }
  95. // Return location where the file would be if it was saved.
  96. $resource = $locator->findResource("{$scheme}://{$host}{$path}", false, true);
  97. }
  98. } elseif ($host || $port) {
  99. // If URL doesn't have scheme but has host or port, it is external.
  100. return str_replace(' ', '%20', $input);
  101. }
  102. if (!empty($resource)) {
  103. // Add query string back.
  104. if (isset($parts['query'])) {
  105. $resource .= '?' . $parts['query'];
  106. }
  107. // Add fragment back.
  108. if (isset($parts['fragment'])) {
  109. $resource .= '#' . $parts['fragment'];
  110. }
  111. }
  112. } else {
  113. // Not a valid URL (can still be a stream).
  114. $resource = $locator->findResource($input, false);
  115. }
  116. } else {
  117. $root = preg_quote($uri->rootUrl(), '#');
  118. $pattern = '#(' . $root . '$|' . $root . '/)#';
  119. if (!empty($root) && preg_match($pattern, $input, $matches)) {
  120. $input = static::replaceFirstOccurrence($matches[0], '', $input);
  121. }
  122. $input = ltrim($input, '/');
  123. $resource = $input;
  124. }
  125. if (!$fail_gracefully && $resource === false) {
  126. return false;
  127. }
  128. $domain = $domain ?: $grav['config']->get('system.absolute_urls', false);
  129. return rtrim($uri->rootUrl($domain), '/') . '/' . ($resource ?: '');
  130. }
  131. /**
  132. * Helper method to find the full path to a file, be it a stream, a relative path, or
  133. * already a full path
  134. *
  135. * @param string $path
  136. * @return string
  137. */
  138. public static function fullPath($path)
  139. {
  140. $locator = Grav::instance()['locator'];
  141. if ($locator->isStream($path)) {
  142. $path = $locator->findResource($path, true);
  143. } elseif (!static::startsWith($path, GRAV_ROOT)) {
  144. $base_url = Grav::instance()['base_url'];
  145. $path = GRAV_ROOT . '/' . ltrim(static::replaceFirstOccurrence($base_url, '', $path), '/');
  146. }
  147. return $path;
  148. }
  149. /**
  150. * Check if the $haystack string starts with the substring $needle
  151. *
  152. * @param string $haystack
  153. * @param string|string[] $needle
  154. * @param bool $case_sensitive
  155. * @return bool
  156. */
  157. public static function startsWith($haystack, $needle, $case_sensitive = true)
  158. {
  159. $status = false;
  160. $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos';
  161. foreach ((array)$needle as $each_needle) {
  162. $status = $each_needle === '' || $compare_func($haystack, $each_needle) === 0;
  163. if ($status) {
  164. break;
  165. }
  166. }
  167. return $status;
  168. }
  169. /**
  170. * Check if the $haystack string ends with the substring $needle
  171. *
  172. * @param string $haystack
  173. * @param string|string[] $needle
  174. * @param bool $case_sensitive
  175. * @return bool
  176. */
  177. public static function endsWith($haystack, $needle, $case_sensitive = true)
  178. {
  179. $status = false;
  180. $compare_func = $case_sensitive ? 'mb_strrpos' : 'mb_strripos';
  181. foreach ((array)$needle as $each_needle) {
  182. $expectedPosition = mb_strlen($haystack) - mb_strlen($each_needle);
  183. $status = $each_needle === '' || $compare_func($haystack, $each_needle, 0) === $expectedPosition;
  184. if ($status) {
  185. break;
  186. }
  187. }
  188. return $status;
  189. }
  190. /**
  191. * Check if the $haystack string contains the substring $needle
  192. *
  193. * @param string $haystack
  194. * @param string|string[] $needle
  195. * @param bool $case_sensitive
  196. * @return bool
  197. */
  198. public static function contains($haystack, $needle, $case_sensitive = true)
  199. {
  200. $status = false;
  201. $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos';
  202. foreach ((array)$needle as $each_needle) {
  203. $status = $each_needle === '' || $compare_func($haystack, $each_needle) !== false;
  204. if ($status) {
  205. break;
  206. }
  207. }
  208. return $status;
  209. }
  210. /**
  211. * Function that can match wildcards
  212. *
  213. * match_wildcard('foo*', $test), // TRUE
  214. * match_wildcard('bar*', $test), // FALSE
  215. * match_wildcard('*bar*', $test), // TRUE
  216. * match_wildcard('**blob**', $test), // TRUE
  217. * match_wildcard('*a?d*', $test), // TRUE
  218. * match_wildcard('*etc**', $test) // TRUE
  219. *
  220. * @param string $wildcard_pattern
  221. * @param string $haystack
  222. * @return false|int
  223. */
  224. public static function matchWildcard($wildcard_pattern, $haystack)
  225. {
  226. $regex = str_replace(
  227. array("\*", "\?"), // wildcard chars
  228. array('.*', '.'), // regexp chars
  229. preg_quote($wildcard_pattern, '/')
  230. );
  231. return preg_match('/^' . $regex . '$/is', $haystack);
  232. }
  233. /**
  234. * Render simple template filling up the variables in it. If value is not defined, leave it as it was.
  235. *
  236. * @param string $template Template string
  237. * @param array $variables Variables with values
  238. * @param array $brackets Optional array of opening and closing brackets or symbols
  239. * @return string Final string filled with values
  240. */
  241. public static function simpleTemplate(string $template, array $variables, array $brackets = ['{', '}']): string
  242. {
  243. $opening = $brackets[0] ?? '{';
  244. $closing = $brackets[1] ?? '}';
  245. $expression = '/' . preg_quote($opening, '/') . '(.*?)' . preg_quote($closing, '/') . '/';
  246. $callback = static function ($match) use ($variables) {
  247. return $variables[$match[1]] ?? $match[0];
  248. };
  249. return preg_replace_callback($expression, $callback, $template);
  250. }
  251. /**
  252. * Returns the substring of a string up to a specified needle. if not found, return the whole haystack
  253. *
  254. * @param string $haystack
  255. * @param string $needle
  256. * @param bool $case_sensitive
  257. *
  258. * @return string
  259. */
  260. public static function substrToString($haystack, $needle, $case_sensitive = true)
  261. {
  262. $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos';
  263. if (static::contains($haystack, $needle, $case_sensitive)) {
  264. return mb_substr($haystack, 0, $compare_func($haystack, $needle, $case_sensitive));
  265. }
  266. return $haystack;
  267. }
  268. /**
  269. * Utility method to replace only the first occurrence in a string
  270. *
  271. * @param string $search
  272. * @param string $replace
  273. * @param string $subject
  274. *
  275. * @return string
  276. */
  277. public static function replaceFirstOccurrence($search, $replace, $subject)
  278. {
  279. if (!$search) {
  280. return $subject;
  281. }
  282. $pos = mb_strpos($subject, $search);
  283. if ($pos !== false) {
  284. $subject = static::mb_substr_replace($subject, $replace, $pos, mb_strlen($search));
  285. }
  286. return $subject;
  287. }
  288. /**
  289. * Utility method to replace only the last occurrence in a string
  290. *
  291. * @param string $search
  292. * @param string $replace
  293. * @param string $subject
  294. * @return string
  295. */
  296. public static function replaceLastOccurrence($search, $replace, $subject)
  297. {
  298. $pos = strrpos($subject, $search);
  299. if ($pos !== false) {
  300. $subject = static::mb_substr_replace($subject, $replace, $pos, mb_strlen($search));
  301. }
  302. return $subject;
  303. }
  304. /**
  305. * Multibyte compatible substr_replace
  306. *
  307. * @param string $original
  308. * @param string $replacement
  309. * @param int $position
  310. * @param int $length
  311. * @return string
  312. */
  313. public static function mb_substr_replace($original, $replacement, $position, $length)
  314. {
  315. $startString = mb_substr($original, 0, $position, 'UTF-8');
  316. $endString = mb_substr($original, $position + $length, mb_strlen($original), 'UTF-8');
  317. return $startString . $replacement . $endString;
  318. }
  319. /**
  320. * Merge two objects into one.
  321. *
  322. * @param object $obj1
  323. * @param object $obj2
  324. *
  325. * @return object
  326. */
  327. public static function mergeObjects($obj1, $obj2)
  328. {
  329. return (object)array_merge((array)$obj1, (array)$obj2);
  330. }
  331. /**
  332. * @param array $array
  333. * @return bool
  334. */
  335. public static function isAssoc(array $array)
  336. {
  337. return (array_values($array) !== $array);
  338. }
  339. /**
  340. * Lowercase an entire array. Useful when combined with `in_array()`
  341. *
  342. * @param array $a
  343. * @return array|false
  344. */
  345. public static function arrayLower(array $a)
  346. {
  347. return array_map('mb_strtolower', $a);
  348. }
  349. /**
  350. * Simple function to remove item/s in an array by value
  351. *
  352. * @param array $search
  353. * @param string|array $value
  354. * @return array
  355. */
  356. public static function arrayRemoveValue(array $search, $value)
  357. {
  358. foreach ((array)$value as $val) {
  359. $key = array_search($val, $search);
  360. if ($key !== false) {
  361. unset($search[$key]);
  362. }
  363. }
  364. return $search;
  365. }
  366. /**
  367. * Recursive Merge with uniqueness
  368. *
  369. * @param array $array1
  370. * @param array $array2
  371. * @return array
  372. */
  373. public static function arrayMergeRecursiveUnique($array1, $array2)
  374. {
  375. if (empty($array1)) {
  376. // Optimize the base case
  377. return $array2;
  378. }
  379. foreach ($array2 as $key => $value) {
  380. if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
  381. $value = static::arrayMergeRecursiveUnique($array1[$key], $value);
  382. }
  383. $array1[$key] = $value;
  384. }
  385. return $array1;
  386. }
  387. /**
  388. * Returns an array with the differences between $array1 and $array2
  389. *
  390. * @param array $array1
  391. * @param array $array2
  392. * @return array
  393. */
  394. public static function arrayDiffMultidimensional($array1, $array2)
  395. {
  396. $result = array();
  397. foreach ($array1 as $key => $value) {
  398. if (!is_array($array2) || !array_key_exists($key, $array2)) {
  399. $result[$key] = $value;
  400. continue;
  401. }
  402. if (is_array($value)) {
  403. $recursiveArrayDiff = static::ArrayDiffMultidimensional($value, $array2[$key]);
  404. if (count($recursiveArrayDiff)) {
  405. $result[$key] = $recursiveArrayDiff;
  406. }
  407. continue;
  408. }
  409. if ($value != $array2[$key]) {
  410. $result[$key] = $value;
  411. }
  412. }
  413. return $result;
  414. }
  415. /**
  416. * Array combine but supports different array lengths
  417. *
  418. * @param array $arr1
  419. * @param array $arr2
  420. * @return array|false
  421. */
  422. public static function arrayCombine($arr1, $arr2)
  423. {
  424. $count = min(count($arr1), count($arr2));
  425. return array_combine(array_slice($arr1, 0, $count), array_slice($arr2, 0, $count));
  426. }
  427. /**
  428. * Array is associative or not
  429. *
  430. * @param array $arr
  431. * @return bool
  432. */
  433. public static function arrayIsAssociative($arr)
  434. {
  435. if ([] === $arr) {
  436. return false;
  437. }
  438. return array_keys($arr) !== range(0, count($arr) - 1);
  439. }
  440. /**
  441. * Return the Grav date formats allowed
  442. *
  443. * @return array
  444. */
  445. public static function dateFormats()
  446. {
  447. $now = new DateTime();
  448. $date_formats = [
  449. 'd-m-Y H:i' => 'd-m-Y H:i (e.g. ' . $now->format('d-m-Y H:i') . ')',
  450. 'Y-m-d H:i' => 'Y-m-d H:i (e.g. ' . $now->format('Y-m-d H:i') . ')',
  451. 'm/d/Y h:i a' => 'm/d/Y h:i a (e.g. ' . $now->format('m/d/Y h:i a') . ')',
  452. 'H:i d-m-Y' => 'H:i d-m-Y (e.g. ' . $now->format('H:i d-m-Y') . ')',
  453. 'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. ' . $now->format('h:i a m/d/Y') . ')',
  454. ];
  455. $default_format = Grav::instance()['config']->get('system.pages.dateformat.default');
  456. if ($default_format) {
  457. $date_formats = array_merge([$default_format => $default_format . ' (e.g. ' . $now->format($default_format) . ')'], $date_formats);
  458. }
  459. return $date_formats;
  460. }
  461. /**
  462. * Get current date/time
  463. *
  464. * @param string|null $default_format
  465. * @return string
  466. * @throws Exception
  467. */
  468. public static function dateNow($default_format = null)
  469. {
  470. $now = new DateTime();
  471. if (null === $default_format) {
  472. $default_format = Grav::instance()['config']->get('system.pages.dateformat.default');
  473. }
  474. return $now->format($default_format);
  475. }
  476. /**
  477. * Truncate text by number of characters but can cut off words.
  478. *
  479. * @param string $string
  480. * @param int $limit Max number of characters.
  481. * @param bool $up_to_break truncate up to breakpoint after char count
  482. * @param string $break Break point.
  483. * @param string $pad Appended padding to the end of the string.
  484. * @return string
  485. */
  486. public static function truncate($string, $limit = 150, $up_to_break = false, $break = ' ', $pad = '&hellip;')
  487. {
  488. // return with no change if string is shorter than $limit
  489. if (mb_strlen($string) <= $limit) {
  490. return $string;
  491. }
  492. // is $break present between $limit and the end of the string?
  493. if ($up_to_break && false !== ($breakpoint = mb_strpos($string, $break, $limit))) {
  494. if ($breakpoint < mb_strlen($string) - 1) {
  495. $string = mb_substr($string, 0, $breakpoint) . $pad;
  496. }
  497. } else {
  498. $string = mb_substr($string, 0, $limit) . $pad;
  499. }
  500. return $string;
  501. }
  502. /**
  503. * Truncate text by number of characters in a "word-safe" manor.
  504. *
  505. * @param string $string
  506. * @param int $limit
  507. * @return string
  508. */
  509. public static function safeTruncate($string, $limit = 150)
  510. {
  511. return static::truncate($string, $limit, true);
  512. }
  513. /**
  514. * Truncate HTML by number of characters. not "word-safe"!
  515. *
  516. * @param string $text
  517. * @param int $length in characters
  518. * @param string $ellipsis
  519. * @return string
  520. */
  521. public static function truncateHtml($text, $length = 100, $ellipsis = '...')
  522. {
  523. return Truncator::truncateLetters($text, $length, $ellipsis);
  524. }
  525. /**
  526. * Truncate HTML by number of characters in a "word-safe" manor.
  527. *
  528. * @param string $text
  529. * @param int $length in words
  530. * @param string $ellipsis
  531. * @return string
  532. */
  533. public static function safeTruncateHtml($text, $length = 25, $ellipsis = '...')
  534. {
  535. return Truncator::truncateWords($text, $length, $ellipsis);
  536. }
  537. /**
  538. * Generate a random string of a given length
  539. *
  540. * @param int $length
  541. * @return string
  542. */
  543. public static function generateRandomString($length = 5)
  544. {
  545. return substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length);
  546. }
  547. /**
  548. * Generates a random string with configurable length, prefix and suffix.
  549. * Unlike the built-in `uniqid()`, this string is non-conflicting and safe
  550. *
  551. * @param int $length
  552. * @param array $options
  553. * @return string
  554. * @throws Exception
  555. */
  556. public static function uniqueId(int $length = 13, array $options = []): string
  557. {
  558. $options = array_merge(['prefix' => '', 'suffix' => ''], $options);
  559. $bytes = random_bytes(ceil($length / 2));
  560. return $options['prefix'] . substr(bin2hex($bytes), 0, $length) . $options['suffix'];
  561. }
  562. /**
  563. * Provides the ability to download a file to the browser
  564. *
  565. * @param string $file the full path to the file to be downloaded
  566. * @param bool $force_download as opposed to letting browser choose if to download or render
  567. * @param int $sec Throttling, try 0.1 for some speed throttling of downloads
  568. * @param int $bytes Size of chunks to send in bytes. Default is 1024
  569. * @param array $options Extra options: [mime, download_name, expires]
  570. * @throws Exception
  571. */
  572. public static function download($file, $force_download = true, $sec = 0, $bytes = 1024, array $options = [])
  573. {
  574. if (file_exists($file)) {
  575. // fire download event
  576. Grav::instance()->fireEvent('onBeforeDownload', new Event(['file' => $file, 'options' => &$options]));
  577. $file_parts = static::pathinfo($file);
  578. $mimetype = $options['mime'] ?? static::getMimeByExtension($file_parts['extension']);
  579. $size = filesize($file); // File size
  580. // clean all buffers
  581. while (ob_get_level()) {
  582. ob_end_clean();
  583. }
  584. // required for IE, otherwise Content-Disposition may be ignored
  585. if (ini_get('zlib.output_compression')) {
  586. ini_set('zlib.output_compression', 'Off');
  587. }
  588. header('Content-Type: ' . $mimetype);
  589. header('Accept-Ranges: bytes');
  590. if ($force_download) {
  591. // output the regular HTTP headers
  592. header('Content-Disposition: attachment; filename="' . ($options['download_name'] ?? $file_parts['basename']) . '"');
  593. }
  594. // multipart-download and download resuming support
  595. if (isset($_SERVER['HTTP_RANGE'])) {
  596. [$a, $range] = explode('=', $_SERVER['HTTP_RANGE'], 2);
  597. [$range] = explode(',', $range, 2);
  598. [$range, $range_end] = explode('-', $range);
  599. $range = (int)$range;
  600. if (!$range_end) {
  601. $range_end = $size - 1;
  602. } else {
  603. $range_end = (int)$range_end;
  604. }
  605. $new_length = $range_end - $range + 1;
  606. header('HTTP/1.1 206 Partial Content');
  607. header("Content-Length: {$new_length}");
  608. header("Content-Range: bytes {$range}-{$range_end}/{$size}");
  609. } else {
  610. $range = 0;
  611. $new_length = $size;
  612. header('Content-Length: ' . $size);
  613. if (Grav::instance()['config']->get('system.cache.enabled')) {
  614. $expires = $options['expires'] ?? Grav::instance()['config']->get('system.pages.expires');
  615. if ($expires > 0) {
  616. $expires_date = gmdate('D, d M Y H:i:s T', time() + $expires);
  617. header('Cache-Control: max-age=' . $expires);
  618. header('Expires: ' . $expires_date);
  619. header('Pragma: cache');
  620. }
  621. header('Last-Modified: ' . gmdate('D, d M Y H:i:s T', filemtime($file)));
  622. // Return 304 Not Modified if the file is already cached in the browser
  623. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
  624. strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= filemtime($file)) {
  625. header('HTTP/1.1 304 Not Modified');
  626. exit();
  627. }
  628. }
  629. }
  630. /* output the file itself */
  631. $chunksize = $bytes * 8; //you may want to change this
  632. $bytes_send = 0;
  633. $fp = @fopen($file, 'rb');
  634. if ($fp) {
  635. if ($range) {
  636. fseek($fp, $range);
  637. }
  638. while (!feof($fp) && (!connection_aborted()) && ($bytes_send < $new_length)) {
  639. $buffer = fread($fp, $chunksize);
  640. echo($buffer); //echo($buffer); // is also possible
  641. flush();
  642. usleep($sec * 1000000);
  643. $bytes_send += strlen($buffer);
  644. }
  645. fclose($fp);
  646. } else {
  647. throw new RuntimeException('Error - can not open file.');
  648. }
  649. exit;
  650. }
  651. }
  652. /**
  653. * Returns the output render format, usually the extension provided in the URL. (e.g. `html`, `json`, `xml`, etc).
  654. *
  655. * @return string
  656. */
  657. public static function getPageFormat(): string
  658. {
  659. /** @var Uri $uri */
  660. $uri = Grav::instance()['uri'];
  661. // Set from uri extension
  662. $uri_extension = $uri->extension();
  663. if (is_string($uri_extension) && $uri->isValidExtension($uri_extension)) {
  664. return ($uri_extension);
  665. }
  666. // Use content negotiation via the `accept:` header
  667. $http_accept = $_SERVER['HTTP_ACCEPT'] ?? null;
  668. if (is_string($http_accept)) {
  669. $negotiator = new Negotiator();
  670. $supported_types = static::getSupportPageTypes(['html', 'json']);
  671. $priorities = static::getMimeTypes($supported_types);
  672. $media_type = $negotiator->getBest($http_accept, $priorities);
  673. $mimetype = $media_type instanceof Accept ? $media_type->getValue() : '';
  674. return static::getExtensionByMime($mimetype);
  675. }
  676. return 'html';
  677. }
  678. /**
  679. * Return the mimetype based on filename extension
  680. *
  681. * @param string $extension Extension of file (eg "txt")
  682. * @param string $default
  683. * @return string
  684. */
  685. public static function getMimeByExtension($extension, $default = 'application/octet-stream')
  686. {
  687. $extension = strtolower($extension);
  688. // look for some standard types
  689. switch ($extension) {
  690. case null:
  691. return $default;
  692. case 'json':
  693. return 'application/json';
  694. case 'html':
  695. return 'text/html';
  696. case 'atom':
  697. return 'application/atom+xml';
  698. case 'rss':
  699. return 'application/rss+xml';
  700. case 'xml':
  701. return 'application/xml';
  702. }
  703. $media_types = Grav::instance()['config']->get('media.types');
  704. return $media_types[$extension]['mime'] ?? $default;
  705. }
  706. /**
  707. * Get all the mimetypes for an array of extensions
  708. *
  709. * @param array $extensions
  710. * @return array
  711. */
  712. public static function getMimeTypes(array $extensions)
  713. {
  714. $mimetypes = [];
  715. foreach ($extensions as $extension) {
  716. $mimetype = static::getMimeByExtension($extension, false);
  717. if ($mimetype && !in_array($mimetype, $mimetypes)) {
  718. $mimetypes[] = $mimetype;
  719. }
  720. }
  721. return $mimetypes;
  722. }
  723. /**
  724. * Return all extensions for given mimetype. The first extension is the default one.
  725. *
  726. * @param string $mime Mime type (eg 'image/jpeg')
  727. * @return string[] List of extensions eg. ['jpg', 'jpe', 'jpeg']
  728. */
  729. public static function getExtensionsByMime($mime)
  730. {
  731. $mime = strtolower($mime);
  732. $media_types = (array)Grav::instance()['config']->get('media.types');
  733. $list = [];
  734. foreach ($media_types as $extension => $type) {
  735. if ($extension === '' || $extension === 'defaults') {
  736. continue;
  737. }
  738. if (isset($type['mime']) && $type['mime'] === $mime) {
  739. $list[] = $extension;
  740. }
  741. }
  742. return $list;
  743. }
  744. /**
  745. * Return the mimetype based on filename extension
  746. *
  747. * @param string $mime mime type (eg "text/html")
  748. * @param string $default default value
  749. * @return string
  750. */
  751. public static function getExtensionByMime($mime, $default = 'html')
  752. {
  753. $mime = strtolower($mime);
  754. // look for some standard mime types
  755. switch ($mime) {
  756. case '*/*':
  757. case 'text/*':
  758. case 'text/html':
  759. return 'html';
  760. case 'application/json':
  761. return 'json';
  762. case 'application/atom+xml':
  763. return 'atom';
  764. case 'application/rss+xml':
  765. return 'rss';
  766. case 'application/xml':
  767. return 'xml';
  768. }
  769. $media_types = (array)Grav::instance()['config']->get('media.types');
  770. foreach ($media_types as $extension => $type) {
  771. if ($extension === 'defaults') {
  772. continue;
  773. }
  774. if (isset($type['mime']) && $type['mime'] === $mime) {
  775. return $extension;
  776. }
  777. }
  778. return $default;
  779. }
  780. /**
  781. * Get all the extensions for an array of mimetypes
  782. *
  783. * @param array $mimetypes
  784. * @return array
  785. */
  786. public static function getExtensions(array $mimetypes)
  787. {
  788. $extensions = [];
  789. foreach ($mimetypes as $mimetype) {
  790. $extension = static::getExtensionByMime($mimetype, false);
  791. if ($extension && !in_array($extension, $extensions, true)) {
  792. $extensions[] = $extension;
  793. }
  794. }
  795. return $extensions;
  796. }
  797. /**
  798. * Return the mimetype based on filename
  799. *
  800. * @param string $filename Filename or path to file
  801. * @param string $default default value
  802. * @return string
  803. */
  804. public static function getMimeByFilename($filename, $default = 'application/octet-stream')
  805. {
  806. return static::getMimeByExtension(static::pathinfo($filename, PATHINFO_EXTENSION), $default);
  807. }
  808. /**
  809. * Return the mimetype based on existing local file
  810. *
  811. * @param string $filename Path to the file
  812. * @param string $default
  813. * @return string|bool
  814. */
  815. public static function getMimeByLocalFile($filename, $default = 'application/octet-stream')
  816. {
  817. $type = false;
  818. // For local files we can detect type by the file content.
  819. if (!stream_is_local($filename) || !file_exists($filename)) {
  820. return false;
  821. }
  822. // Prefer using finfo if it exists.
  823. if (extension_loaded('fileinfo')) {
  824. $finfo = finfo_open(FILEINFO_SYMLINK | FILEINFO_MIME_TYPE);
  825. $type = finfo_file($finfo, $filename);
  826. finfo_close($finfo);
  827. } else {
  828. // Fall back to use getimagesize() if it is available (not recommended, but better than nothing)
  829. $info = @getimagesize($filename);
  830. if ($info) {
  831. $type = $info['mime'];
  832. }
  833. }
  834. return $type ?: static::getMimeByFilename($filename, $default);
  835. }
  836. /**
  837. * Returns true if filename is considered safe.
  838. *
  839. * @param string $filename
  840. * @return bool
  841. */
  842. public static function checkFilename($filename)
  843. {
  844. $dangerous_extensions = Grav::instance()['config']->get('security.uploads_dangerous_extensions', []);
  845. $extension = static::pathinfo($filename, PATHINFO_EXTENSION);
  846. return !(
  847. // Empty filenames are not allowed.
  848. !$filename
  849. // Filename should not contain horizontal/vertical tabs, newlines, nils or back/forward slashes.
  850. || strtr($filename, "\t\v\n\r\0\\/", '_______') !== $filename
  851. // Filename should not start or end with dot or space.
  852. || trim($filename, '. ') !== $filename
  853. // File extension should not be part of configured dangerous extensions
  854. || in_array($extension, $dangerous_extensions)
  855. );
  856. }
  857. /**
  858. * Unicode-safe version of PHP’s pathinfo() function.
  859. *
  860. * @link https://www.php.net/manual/en/function.pathinfo.php
  861. *
  862. * @param string $path
  863. * @param int|null $flags
  864. * @return array|string
  865. */
  866. public static function pathinfo($path, int $flags = null)
  867. {
  868. $path = str_replace(['%2F', '%5C'], ['/', '\\'], rawurlencode($path));
  869. if (null === $flags) {
  870. $info = pathinfo($path);
  871. } else {
  872. $info = pathinfo($path, $flags);
  873. }
  874. if (is_array($info)) {
  875. return array_map('rawurldecode', $info);
  876. }
  877. return rawurldecode($info);
  878. }
  879. /**
  880. * Unicode-safe version of the PHP basename() function.
  881. *
  882. * @link https://www.php.net/manual/en/function.basename.php
  883. *
  884. * @param string $path
  885. * @param string $suffix
  886. * @return string
  887. */
  888. public static function basename($path, string $suffix = ''): string
  889. {
  890. return rawurldecode(basename(str_replace(['%2F', '%5C'], '/', rawurlencode($path)), $suffix));
  891. }
  892. /**
  893. * Normalize path by processing relative `.` and `..` syntax and merging path
  894. *
  895. * @param string $path
  896. * @return string
  897. */
  898. public static function normalizePath($path)
  899. {
  900. // Resolve any streams
  901. /** @var UniformResourceLocator $locator */
  902. $locator = Grav::instance()['locator'];
  903. if ($locator->isStream($path)) {
  904. $path = $locator->findResource($path);
  905. }
  906. // Set root properly for any URLs
  907. $root = '';
  908. preg_match(self::ROOTURL_REGEX, $path, $matches);
  909. if ($matches) {
  910. $root = $matches[1];
  911. $path = $matches[2];
  912. }
  913. // Strip off leading / to ensure explode is accurate
  914. if (static::startsWith($path, '/')) {
  915. $root .= '/';
  916. $path = ltrim($path, '/');
  917. }
  918. // If there are any relative paths (..) handle those
  919. if (static::contains($path, '..')) {
  920. $segments = explode('/', trim($path, '/'));
  921. $ret = [];
  922. foreach ($segments as $segment) {
  923. if (($segment === '.') || $segment === '') {
  924. continue;
  925. }
  926. if ($segment === '..') {
  927. array_pop($ret);
  928. } else {
  929. $ret[] = $segment;
  930. }
  931. }
  932. $path = implode('/', $ret);
  933. }
  934. // Stick everything back together
  935. $normalized = $root . $path;
  936. return $normalized;
  937. }
  938. /**
  939. * Check whether a function exists.
  940. *
  941. * Disabled functions count as non-existing functions, just like in PHP 8+.
  942. *
  943. * @param string $function the name of the function to check
  944. * @return bool
  945. */
  946. public static function functionExists($function): bool
  947. {
  948. if (!function_exists($function)) {
  949. return false;
  950. }
  951. // In PHP 7 we need to also exclude disabled methods.
  952. return !static::isFunctionDisabled($function);
  953. }
  954. /**
  955. * Check whether a function is disabled in the PHP settings
  956. *
  957. * @param string $function the name of the function to check
  958. * @return bool
  959. */
  960. public static function isFunctionDisabled($function): bool
  961. {
  962. static $list;
  963. if (null === $list) {
  964. $str = trim(ini_get('disable_functions') . ',' . ini_get('suhosin.executor.func.blacklist'), ',');
  965. $list = $str ? array_flip(preg_split('/\s*,\s*/', $str)) : [];
  966. }
  967. return array_key_exists($function, $list);
  968. }
  969. /**
  970. * Get the formatted timezones list
  971. *
  972. * @return array
  973. */
  974. public static function timezones()
  975. {
  976. $timezones = DateTimeZone::listIdentifiers(DateTimeZone::ALL);
  977. $offsets = [];
  978. $testDate = new DateTime();
  979. foreach ($timezones as $zone) {
  980. $tz = new DateTimeZone($zone);
  981. $offsets[$zone] = $tz->getOffset($testDate);
  982. }
  983. asort($offsets);
  984. $timezone_list = [];
  985. foreach ($offsets as $timezone => $offset) {
  986. $offset_prefix = $offset < 0 ? '-' : '+';
  987. $offset_formatted = gmdate('H:i', abs($offset));
  988. $pretty_offset = "UTC${offset_prefix}${offset_formatted}";
  989. $timezone_list[$timezone] = "(${pretty_offset}) " . str_replace('_', ' ', $timezone);
  990. }
  991. return $timezone_list;
  992. }
  993. /**
  994. * Recursively filter an array, filtering values by processing them through the $fn function argument
  995. *
  996. * @param array $source the Array to filter
  997. * @param callable $fn the function to pass through each array item
  998. * @return array
  999. */
  1000. public static function arrayFilterRecursive(array $source, $fn)
  1001. {
  1002. $result = [];
  1003. foreach ($source as $key => $value) {
  1004. if (is_array($value)) {
  1005. $result[$key] = static::arrayFilterRecursive($value, $fn);
  1006. continue;
  1007. }
  1008. if ($fn($key, $value)) {
  1009. $result[$key] = $value; // KEEP
  1010. continue;
  1011. }
  1012. }
  1013. return $result;
  1014. }
  1015. /**
  1016. * Flatten a multi-dimensional associative array into query params.
  1017. *
  1018. * @param array $array
  1019. * @param string $prepend
  1020. * @return array
  1021. */
  1022. public static function arrayToQueryParams($array, $prepend = '')
  1023. {
  1024. $results = [];
  1025. foreach ($array as $key => $value) {
  1026. $name = $prepend ? $prepend . '[' . $key . ']' : $key;
  1027. if (is_array($value)) {
  1028. $results = array_merge($results, static::arrayToQueryParams($value, $name));
  1029. } else {
  1030. $results[$name] = $value;
  1031. }
  1032. }
  1033. return $results;
  1034. }
  1035. /**
  1036. * Flatten an array
  1037. *
  1038. * @param array $array
  1039. * @return array
  1040. */
  1041. public static function arrayFlatten($array)
  1042. {
  1043. $flatten = [];
  1044. foreach ($array as $key => $inner) {
  1045. if (is_array($inner)) {
  1046. foreach ($inner as $inner_key => $value) {
  1047. $flatten[$inner_key] = $value;
  1048. }
  1049. } else {
  1050. $flatten[$key] = $inner;
  1051. }
  1052. }
  1053. return $flatten;
  1054. }
  1055. /**
  1056. * Flatten a multi-dimensional associative array into dot notation
  1057. *
  1058. * @param array $array
  1059. * @param string $prepend
  1060. * @return array
  1061. */
  1062. public static function arrayFlattenDotNotation($array, $prepend = '')
  1063. {
  1064. $results = array();
  1065. foreach ($array as $key => $value) {
  1066. if (is_array($value)) {
  1067. $results = array_merge($results, static::arrayFlattenDotNotation($value, $prepend . $key . '.'));
  1068. } else {
  1069. $results[$prepend . $key] = $value;
  1070. }
  1071. }
  1072. return $results;
  1073. }
  1074. /**
  1075. * Opposite of flatten, convert flat dot notation array to multi dimensional array.
  1076. *
  1077. * If any of the parent has a scalar value, all children get ignored:
  1078. *
  1079. * admin.pages=true
  1080. * admin.pages.read=true
  1081. *
  1082. * becomes
  1083. *
  1084. * admin:
  1085. * pages: true
  1086. *
  1087. * @param array $array
  1088. * @param string $separator
  1089. * @return array
  1090. */
  1091. public static function arrayUnflattenDotNotation($array, $separator = '.')
  1092. {
  1093. $newArray = [];
  1094. foreach ($array as $key => $value) {
  1095. $dots = explode($separator, $key);
  1096. if (count($dots) > 1) {
  1097. $last = &$newArray[$dots[0]];
  1098. foreach ($dots as $k => $dot) {
  1099. if ($k === 0) {
  1100. continue;
  1101. }
  1102. // Cannot use a scalar value as an array
  1103. if (null !== $last && !is_array($last)) {
  1104. continue 2;
  1105. }
  1106. $last = &$last[$dot];
  1107. }
  1108. // Cannot use a scalar value as an array
  1109. if (null !== $last && !is_array($last)) {
  1110. continue;
  1111. }
  1112. $last = $value;
  1113. } else {
  1114. $newArray[$key] = $value;
  1115. }
  1116. }
  1117. return $newArray;
  1118. }
  1119. /**
  1120. * Checks if the passed path contains the language code prefix
  1121. *
  1122. * @param string $string The path
  1123. *
  1124. * @return bool|string Either false or the language
  1125. *
  1126. */
  1127. public static function pathPrefixedByLangCode($string)
  1128. {
  1129. $languages_enabled = Grav::instance()['config']->get('system.languages.supported', []);
  1130. $parts = explode('/', trim($string, '/'));
  1131. if (count($parts) > 0 && in_array($parts[0], $languages_enabled)) {
  1132. return $parts[0];
  1133. }
  1134. return false;
  1135. }
  1136. /**
  1137. * Get the timestamp of a date
  1138. *
  1139. * @param string $date a String expressed in the system.pages.dateformat.default format, with fallback to a
  1140. * strtotime argument
  1141. * @param string|null $format a date format to use if possible
  1142. * @return int the timestamp
  1143. */
  1144. public static function date2timestamp($date, $format = null)
  1145. {
  1146. $config = Grav::instance()['config'];
  1147. $dateformat = $format ?: $config->get('system.pages.dateformat.default');
  1148. // try to use DateTime and default format
  1149. if ($dateformat) {
  1150. $datetime = DateTime::createFromFormat($dateformat, $date);
  1151. } else {
  1152. $datetime = new DateTime($date);
  1153. }
  1154. // fallback to strtotime() if DateTime approach failed
  1155. if ($datetime !== false) {
  1156. return $datetime->getTimestamp();
  1157. }
  1158. return strtotime($date);
  1159. }
  1160. /**
  1161. * @param array $array
  1162. * @param string $path
  1163. * @param null $default
  1164. * @return mixed
  1165. *
  1166. * @deprecated 1.5 Use ->getDotNotation() method instead.
  1167. */
  1168. public static function resolve(array $array, $path, $default = null)
  1169. {
  1170. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->getDotNotation() method instead', E_USER_DEPRECATED);
  1171. return static::getDotNotation($array, $path, $default);
  1172. }
  1173. /**
  1174. * Checks if a value is positive (true)
  1175. *
  1176. * @param string $value
  1177. * @return bool
  1178. */
  1179. public static function isPositive($value)
  1180. {
  1181. return in_array($value, [true, 1, '1', 'yes', 'on', 'true'], true);
  1182. }
  1183. /**
  1184. * Checks if a value is negative (false)
  1185. *
  1186. * @param string $value
  1187. * @return bool
  1188. */
  1189. public static function isNegative($value)
  1190. {
  1191. return in_array($value, [false, 0, '0', 'no', 'off', 'false'], true);
  1192. }
  1193. /**
  1194. * Generates a nonce string to be hashed. Called by self::getNonce()
  1195. * We removed the IP portion in this version because it causes too many inconsistencies
  1196. * with reverse proxy setups.
  1197. *
  1198. * @param string $action
  1199. * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours)
  1200. * @return string the nonce string
  1201. */
  1202. private static function generateNonceString($action, $previousTick = false)
  1203. {
  1204. $grav = Grav::instance();
  1205. $username = isset($grav['user']) ? $grav['user']->username : '';
  1206. $token = session_id();
  1207. $i = self::nonceTick();
  1208. if ($previousTick) {
  1209. $i--;
  1210. }
  1211. return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . $grav['config']->get('security.salt'));
  1212. }
  1213. /**
  1214. * Get the time-dependent variable for nonce creation.
  1215. *
  1216. * Now a tick lasts a day. Once the day is passed, the nonce is not valid any more. Find a better way
  1217. * to ensure nonces issued near the end of the day do not expire in that small amount of time
  1218. *
  1219. * @return int the time part of the nonce. Changes once every 24 hours
  1220. */
  1221. private static function nonceTick()
  1222. {
  1223. $secondsInHalfADay = 60 * 60 * 12;
  1224. return (int)ceil(time() / $secondsInHalfADay);
  1225. }
  1226. /**
  1227. * Creates a hashed nonce tied to the passed action. Tied to the current user and time. The nonce for a given
  1228. * action is the same for 12 hours.
  1229. *
  1230. * @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage)
  1231. * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours)
  1232. * @return string the nonce
  1233. */
  1234. public static function getNonce($action, $previousTick = false)
  1235. {
  1236. // Don't regenerate this again if not needed
  1237. if (isset(static::$nonces[$action][$previousTick])) {
  1238. return static::$nonces[$action][$previousTick];
  1239. }
  1240. $nonce = md5(self::generateNonceString($action, $previousTick));
  1241. static::$nonces[$action][$previousTick] = $nonce;
  1242. return static::$nonces[$action][$previousTick];
  1243. }
  1244. /**
  1245. * Verify the passed nonce for the give action
  1246. *
  1247. * @param string|string[] $nonce the nonce to verify
  1248. * @param string $action the action to verify the nonce to
  1249. * @return boolean verified or not
  1250. */
  1251. public static function verifyNonce($nonce, $action)
  1252. {
  1253. //Safety check for multiple nonces
  1254. if (is_array($nonce)) {
  1255. $nonce = array_shift($nonce);
  1256. }
  1257. //Nonce generated 0-12 hours ago
  1258. if ($nonce === self::getNonce($action)) {
  1259. return true;
  1260. }
  1261. //Nonce generated 12-24 hours ago
  1262. return $nonce === self::getNonce($action, true);
  1263. }
  1264. /**
  1265. * Simple helper method to get whether or not the admin plugin is active
  1266. *
  1267. * @return bool
  1268. */
  1269. public static function isAdminPlugin()
  1270. {
  1271. return isset(Grav::instance()['admin']);
  1272. }
  1273. /**
  1274. * Get a portion of an array (passed by reference) with dot-notation key
  1275. *
  1276. * @param array $array
  1277. * @param string|int|null $key
  1278. * @param null $default
  1279. * @return mixed
  1280. */
  1281. public static function getDotNotation($array, $key, $default = null)
  1282. {
  1283. if (null === $key) {
  1284. return $array;
  1285. }
  1286. if (isset($array[$key])) {
  1287. return $array[$key];
  1288. }
  1289. foreach (explode('.', $key) as $segment) {
  1290. if (!is_array($array) || !array_key_exists($segment, $array)) {
  1291. return $default;
  1292. }
  1293. $array = $array[$segment];
  1294. }
  1295. return $array;
  1296. }
  1297. /**
  1298. * Set portion of array (passed by reference) for a dot-notation key
  1299. * and set the value
  1300. *
  1301. * @param array $array
  1302. * @param string|int|null $key
  1303. * @param mixed $value
  1304. * @param bool $merge
  1305. *
  1306. * @return mixed
  1307. */
  1308. public static function setDotNotation(&$array, $key, $value, $merge = false)
  1309. {
  1310. if (null === $key) {
  1311. return $array = $value;
  1312. }
  1313. $keys = explode('.', $key);
  1314. while (count($keys) > 1) {
  1315. $key = array_shift($keys);
  1316. if (!isset($array[$key]) || !is_array($array[$key])) {
  1317. $array[$key] = array();
  1318. }
  1319. $array =& $array[$key];
  1320. }
  1321. $key = array_shift($keys);
  1322. if (!$merge || !isset($array[$key])) {
  1323. $array[$key] = $value;
  1324. } else {
  1325. $array[$key] = array_merge($array[$key], $value);
  1326. }
  1327. return $array;
  1328. }
  1329. /**
  1330. * Utility method to determine if the current OS is Windows
  1331. *
  1332. * @return bool
  1333. */
  1334. public static function isWindows()
  1335. {
  1336. return strncasecmp(PHP_OS, 'WIN', 3) === 0;
  1337. }
  1338. /**
  1339. * Utility to determine if the server running PHP is Apache
  1340. *
  1341. * @return bool
  1342. */
  1343. public static function isApache()
  1344. {
  1345. return isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false;
  1346. }
  1347. /**
  1348. * Sort a multidimensional array by another array of ordered keys
  1349. *
  1350. * @param array $array
  1351. * @param array $orderArray
  1352. * @return array
  1353. */
  1354. public static function sortArrayByArray(array $array, array $orderArray)
  1355. {
  1356. $ordered = [];
  1357. foreach ($orderArray as $key) {
  1358. if (array_key_exists($key, $array)) {
  1359. $ordered[$key] = $array[$key];
  1360. unset($array[$key]);
  1361. }
  1362. }
  1363. return $ordered + $array;
  1364. }
  1365. /**
  1366. * Sort an array by a key value in the array
  1367. *
  1368. * @param mixed $array
  1369. * @param string|int $array_key
  1370. * @param int $direction
  1371. * @param int $sort_flags
  1372. * @return array
  1373. */
  1374. public static function sortArrayByKey($array, $array_key, $direction = SORT_DESC, $sort_flags = SORT_REGULAR)
  1375. {
  1376. $output = [];
  1377. if (!is_array($array) || !$array) {
  1378. return $output;
  1379. }
  1380. foreach ($array as $key => $row) {
  1381. $output[$key] = $row[$array_key];
  1382. }
  1383. array_multisort($output, $direction, $sort_flags, $array);
  1384. return $array;
  1385. }
  1386. /**
  1387. * Get relative page path based on a token.
  1388. *
  1389. * @param string $path
  1390. * @param PageInterface|null $page
  1391. * @return string
  1392. * @throws RuntimeException
  1393. */
  1394. public static function getPagePathFromToken($path, PageInterface $page = null)
  1395. {
  1396. return static::getPathFromToken($path, $page);
  1397. }
  1398. /**
  1399. * Get relative path based on a token.
  1400. *
  1401. * Path supports following syntaxes:
  1402. *
  1403. * 'self@', 'self@/path'
  1404. * 'page@:/route', 'page@:/route/filename.ext'
  1405. * 'theme@:', 'theme@:/path'
  1406. *
  1407. * @param string $path
  1408. * @param FlexObjectInterface|PageInterface|null $object
  1409. * @return string
  1410. * @throws RuntimeException
  1411. */
  1412. public static function getPathFromToken($path, $object = null)
  1413. {
  1414. $matches = static::resolveTokenPath($path);
  1415. if (null === $matches) {
  1416. return $path;
  1417. }
  1418. $grav = Grav::instance();
  1419. switch ($matches[0]) {
  1420. case 'self':
  1421. if (!$object instanceof MediaInterface) {
  1422. throw new RuntimeException(sprintf('Page not available for self@ reference: %s', $path));
  1423. }
  1424. if ($matches[2] === '') {
  1425. if ($object->exists()) {
  1426. $route = '/' . $matches[1];
  1427. if ($object instanceof PageInterface) {
  1428. return trim($object->relativePagePath() . $route, '/');
  1429. }
  1430. $folder = $object->getMediaFolder();
  1431. if ($folder) {
  1432. return trim($folder . $route, '/');
  1433. }
  1434. } else {
  1435. return '';
  1436. }
  1437. }
  1438. break;
  1439. case 'page':
  1440. if ($matches[1] === '') {
  1441. $route = '/' . $matches[2];
  1442. // Exclude filename from the page lookup.
  1443. if (static::pathinfo($route, PATHINFO_EXTENSION)) {
  1444. $basename = '/' . static::basename($route);
  1445. $route = \dirname($route);
  1446. } else {
  1447. $basename = '';
  1448. }
  1449. $key = trim($route === '/' ? $grav['config']->get('system.home.alias') : $route, '/');
  1450. if ($object instanceof PageObject) {
  1451. $object = $object->getFlexDirectory()->getObject($key);
  1452. } elseif (static::isAdminPlugin()) {
  1453. /** @var Flex|null $flex */
  1454. $flex = $grav['flex'] ?? null;
  1455. $object = $flex ? $flex->getObject($key, 'pages') : null;
  1456. } else {
  1457. /** @var Pages $pages */
  1458. $pages = $grav['pages'];
  1459. $object = $pages->find($route);
  1460. }
  1461. if ($object instanceof PageInterface) {
  1462. return trim($object->relativePagePath() . $basename, '/');
  1463. }
  1464. }
  1465. break;
  1466. case 'theme':
  1467. if ($matches[1] === '') {
  1468. $route = '/' . $matches[2];
  1469. $theme = $grav['locator']->findResource('theme://', false);
  1470. if (false !== $theme) {
  1471. return trim($theme . $route, '/');
  1472. }
  1473. }
  1474. break;
  1475. }
  1476. throw new RuntimeException(sprintf('Token path not found: %s', $path));
  1477. }
  1478. /**
  1479. * Returns [token, route, path] from '@token/route:/path'. Route and path are optional. If pattern does not match, return null.
  1480. *
  1481. * @param string $path
  1482. * @return string[]|null
  1483. */
  1484. protected static function resolveTokenPath(string $path): ?array
  1485. {
  1486. if (strpos($path, '@') !== false) {
  1487. $regex = '/^(@\w+|\w+@|@\w+@)([^:]*)(.*)$/u';
  1488. if (preg_match($regex, $path, $matches)) {
  1489. return [
  1490. trim($matches[1], '@'),
  1491. trim($matches[2], '/'),
  1492. trim($matches[3], ':/')
  1493. ];
  1494. }
  1495. }
  1496. return null;
  1497. }
  1498. /**
  1499. * @return int
  1500. */
  1501. public static function getUploadLimit()
  1502. {
  1503. static $max_size = -1;
  1504. if ($max_size < 0) {
  1505. $post_max_size = static::parseSize(ini_get('post_max_size'));
  1506. if ($post_max_size > 0) {
  1507. $max_size = $post_max_size;
  1508. } else {
  1509. $max_size = 0;
  1510. }
  1511. $upload_max = static::parseSize(ini_get('upload_max_filesize'));
  1512. if ($upload_max > 0 && $upload_max < $max_size) {
  1513. $max_size = $upload_max;
  1514. }
  1515. }
  1516. return $max_size;
  1517. }
  1518. /**
  1519. * Convert bytes to the unit specified by the $to parameter.
  1520. *
  1521. * @param int $bytes The filesize in Bytes.
  1522. * @param string $to The unit type to convert to. Accepts K, M, or G for Kilobytes, Megabytes, or Gigabytes, respectively.
  1523. * @param int $decimal_places The number of decimal places to return.
  1524. * @return int Returns only the number of units, not the type letter. Returns 0 if the $to unit type is out of scope.
  1525. *
  1526. */
  1527. public static function convertSize($bytes, $to, $decimal_places = 1)
  1528. {
  1529. $formulas = array(
  1530. 'K' => number_format($bytes / 1024, $decimal_places),
  1531. 'M' => number_format($bytes / 1048576, $decimal_places),
  1532. 'G' => number_format($bytes / 1073741824, $decimal_places)
  1533. );
  1534. return $formulas[$to] ?? 0;
  1535. }
  1536. /**
  1537. * Return a pretty size based on bytes
  1538. *
  1539. * @param int $bytes
  1540. * @param int $precision
  1541. * @return string
  1542. */
  1543. public static function prettySize($bytes, $precision = 2)
  1544. {
  1545. $units = array('B', 'KB', 'MB', 'GB', 'TB');
  1546. $bytes = max($bytes, 0);
  1547. $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
  1548. $pow = min($pow, count($units) - 1);
  1549. // Uncomment one of the following alternatives
  1550. $bytes /= 1024 ** $pow;
  1551. // $bytes /= (1 << (10 * $pow));
  1552. return round($bytes, $precision) . ' ' . $units[$pow];
  1553. }
  1554. /**
  1555. * Parse a readable file size and return a value in bytes
  1556. *
  1557. * @param string|int|float $size
  1558. * @return int
  1559. */
  1560. public static function parseSize($size)
  1561. {
  1562. $unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
  1563. $size = (float)preg_replace('/[^0-9\.]/', '', $size);
  1564. if ($unit) {
  1565. $size *= 1024 ** stripos('bkmgtpezy', $unit[0]);
  1566. }
  1567. return (int)abs(round($size));
  1568. }
  1569. /**
  1570. * Multibyte-safe Parse URL function
  1571. *
  1572. * @param string $url
  1573. * @return array
  1574. * @throws InvalidArgumentException
  1575. */
  1576. public static function multibyteParseUrl($url)
  1577. {
  1578. $enc_url = preg_replace_callback(
  1579. '%[^:/@?&=#]+%usD',
  1580. static function ($matches) {
  1581. return urlencode($matches[0]);
  1582. },
  1583. $url
  1584. );
  1585. $parts = parse_url($enc_url);
  1586. if ($parts === false) {
  1587. throw new InvalidArgumentException('Malformed URL: ' . $url);
  1588. }
  1589. foreach ($parts as $name => $value) {
  1590. $parts[$name] = urldecode($value);
  1591. }
  1592. return $parts;
  1593. }
  1594. /**
  1595. * Process a string as markdown
  1596. *
  1597. * @param string $string
  1598. * @param bool $block Block or Line processing
  1599. * @param PageInterface|null $page
  1600. * @return string
  1601. * @throws Exception
  1602. */
  1603. public static function processMarkdown($string, $block = true, $page = null)
  1604. {
  1605. $grav = Grav::instance();
  1606. $page = $page ?? $grav['page'] ?? null;
  1607. $defaults = [
  1608. 'markdown' => $grav['config']->get('system.pages.markdown', []),
  1609. 'images' => $grav['config']->get('system.images', [])
  1610. ];
  1611. $extra = $defaults['markdown']['extra'] ?? false;
  1612. $excerpts = new Excerpts($page, $defaults);
  1613. // Initialize the preferred variant of Parsedown
  1614. if ($extra) {
  1615. $parsedown = new ParsedownExtra($excerpts);
  1616. } else {
  1617. $parsedown = new Parsedown($excerpts);
  1618. }
  1619. if ($block) {
  1620. $string = $parsedown->text($string);
  1621. } else {
  1622. $string = $parsedown->line($string);
  1623. }
  1624. return $string;
  1625. }
  1626. /**
  1627. * Find the subnet of an ip with CIDR prefix size
  1628. *
  1629. * @param string $ip
  1630. * @param int $prefix
  1631. * @return string
  1632. */
  1633. public static function getSubnet($ip, $prefix = 64)
  1634. {
  1635. if (!filter_var($ip, FILTER_VALIDATE_IP)) {
  1636. return $ip;
  1637. }
  1638. // Packed representation of IP
  1639. $ip = (string)inet_pton($ip);
  1640. // Maximum netmask length = same as packed address
  1641. $len = 8 * strlen($ip);
  1642. if ($prefix > $len) {
  1643. $prefix = $len;
  1644. }
  1645. $mask = str_repeat('f', $prefix >> 2);
  1646. switch ($prefix & 3) {
  1647. case 3:
  1648. $mask .= 'e';
  1649. break;
  1650. case 2:
  1651. $mask .= 'c';
  1652. break;
  1653. case 1:
  1654. $mask .= '8';
  1655. break;
  1656. }
  1657. $mask = str_pad($mask, $len >> 2, '0');
  1658. // Packed representation of netmask
  1659. $mask = pack('H*', $mask);
  1660. // Bitwise - Take all bits that are both 1 to generate subnet
  1661. $subnet = inet_ntop($ip & $mask);
  1662. return $subnet;
  1663. }
  1664. /**
  1665. * Wrapper to ensure html, htm in the front of the supported page types
  1666. *
  1667. * @param array|null $defaults
  1668. * @return array
  1669. */
  1670. public static function getSupportPageTypes(array $defaults = null)
  1671. {
  1672. $types = Grav::instance()['config']->get('system.pages.types', $defaults);
  1673. if (!is_array($types)) {
  1674. return [];
  1675. }
  1676. // remove html/htm
  1677. $types = static::arrayRemoveValue($types, ['html', 'htm']);
  1678. // put them back at the front
  1679. $types = array_merge(['html', 'htm'], $types);
  1680. return $types;
  1681. }
  1682. /**
  1683. * @param string $name
  1684. * @return bool
  1685. */
  1686. public static function isDangerousFunction(string $name): bool
  1687. {
  1688. static $commandExecutionFunctions = [
  1689. 'exec',
  1690. 'passthru',
  1691. 'system',
  1692. 'shell_exec',
  1693. 'popen',
  1694. 'proc_open',
  1695. 'pcntl_exec',
  1696. ];
  1697. static $codeExecutionFunctions = [
  1698. 'assert',
  1699. 'preg_replace',
  1700. 'create_function',
  1701. 'include',
  1702. 'include_once',
  1703. 'require',
  1704. 'require_once'
  1705. ];
  1706. static $callbackFunctions = [
  1707. 'ob_start' => 0,
  1708. 'array_diff_uassoc' => -1,
  1709. 'array_diff_ukey' => -1,
  1710. 'array_filter' => 1,
  1711. 'array_intersect_uassoc' => -1,
  1712. 'array_intersect_ukey' => -1,
  1713. 'array_map' => 0,
  1714. 'array_reduce' => 1,
  1715. 'array_udiff_assoc' => -1,
  1716. 'array_udiff_uassoc' => [-1, -2],
  1717. 'array_udiff' => -1,
  1718. 'array_uintersect_assoc' => -1,
  1719. 'array_uintersect_uassoc' => [-1, -2],
  1720. 'array_uintersect' => -1,
  1721. 'array_walk_recursive' => 1,
  1722. 'array_walk' => 1,
  1723. 'assert_options' => 1,
  1724. 'uasort' => 1,
  1725. 'uksort' => 1,
  1726. 'usort' => 1,
  1727. 'preg_replace_callback' => 1,
  1728. 'spl_autoload_register' => 0,
  1729. 'iterator_apply' => 1,
  1730. 'call_user_func' => 0,
  1731. 'call_user_func_array' => 0,
  1732. 'register_shutdown_function' => 0,
  1733. 'register_tick_function' => 0,
  1734. 'set_error_handler' => 0,
  1735. 'set_exception_handler' => 0,
  1736. 'session_set_save_handler' => [0, 1, 2, 3, 4, 5],
  1737. 'sqlite_create_aggregate' => [2, 3],
  1738. 'sqlite_create_function' => 2,
  1739. ];
  1740. static $informationDiscosureFunctions = [
  1741. 'phpinfo',
  1742. 'posix_mkfifo',
  1743. 'posix_getlogin',
  1744. 'posix_ttyname',
  1745. 'getenv',
  1746. 'get_current_user',
  1747. 'proc_get_status',
  1748. 'get_cfg_var',
  1749. 'disk_free_space',
  1750. 'disk_total_space',
  1751. 'diskfreespace',
  1752. 'getcwd',
  1753. 'getlastmo',
  1754. 'getmygid',
  1755. 'getmyinode',
  1756. 'getmypid',
  1757. 'getmyuid'
  1758. ];
  1759. static $otherFunctions = [
  1760. 'extract',
  1761. 'parse_str',
  1762. 'putenv',
  1763. 'ini_set',
  1764. 'mail',
  1765. 'header',
  1766. 'proc_nice',
  1767. 'proc_terminate',
  1768. 'proc_close',
  1769. 'pfsockopen',
  1770. 'fsockopen',
  1771. 'apache_child_terminate',
  1772. 'posix_kill',
  1773. 'posix_mkfifo',
  1774. 'posix_setpgid',
  1775. 'posix_setsid',
  1776. 'posix_setuid',
  1777. ];
  1778. if (in_array($name, $commandExecutionFunctions)) {
  1779. return true;
  1780. }
  1781. if (in_array($name, $codeExecutionFunctions)) {
  1782. return true;
  1783. }
  1784. if (isset($callbackFunctions[$name])) {
  1785. return true;
  1786. }
  1787. if (in_array($name, $informationDiscosureFunctions)) {
  1788. return true;
  1789. }
  1790. if (in_array($name, $otherFunctions)) {
  1791. return true;
  1792. }
  1793. return static::isFilesystemFunction($name);
  1794. }
  1795. /**
  1796. * @param string $name
  1797. * @return bool
  1798. */
  1799. public static function isFilesystemFunction(string $name): bool
  1800. {
  1801. static $fileWriteFunctions = [
  1802. 'fopen',
  1803. 'tmpfile',
  1804. 'bzopen',
  1805. 'gzopen',
  1806. // write to filesystem (partially in combination with reading)
  1807. 'chgrp',
  1808. 'chmod',
  1809. 'chown',
  1810. 'copy',
  1811. 'file_put_contents',
  1812. 'lchgrp',
  1813. 'lchown',
  1814. 'link',
  1815. 'mkdir',
  1816. 'move_uploaded_file',
  1817. 'rename',
  1818. 'rmdir',
  1819. 'symlink',
  1820. 'tempnam',
  1821. 'touch',
  1822. 'unlink',
  1823. 'imagepng',
  1824. 'imagewbmp',
  1825. 'image2wbmp',
  1826. 'imagejpeg',
  1827. 'imagexbm',
  1828. 'imagegif',
  1829. 'imagegd',
  1830. 'imagegd2',
  1831. 'iptcembed',
  1832. 'ftp_get',
  1833. 'ftp_nb_get',
  1834. ];
  1835. static $fileContentFunctions = [
  1836. 'file_get_contents',
  1837. 'file',
  1838. 'filegroup',
  1839. 'fileinode',
  1840. 'fileowner',
  1841. 'fileperms',
  1842. 'glob',
  1843. 'is_executable',
  1844. 'is_uploaded_file',
  1845. 'parse_ini_file',
  1846. 'readfile',
  1847. 'readlink',
  1848. 'realpath',
  1849. 'gzfile',
  1850. 'readgzfile',
  1851. 'stat',
  1852. 'imagecreatefromgif',
  1853. 'imagecreatefromjpeg',
  1854. 'imagecreatefrompng',
  1855. 'imagecreatefromwbmp',
  1856. 'imagecreatefromxbm',
  1857. 'imagecreatefromxpm',
  1858. 'ftp_put',
  1859. 'ftp_nb_put',
  1860. 'hash_update_file',
  1861. 'highlight_file',
  1862. 'show_source',
  1863. 'php_strip_whitespace',
  1864. ];
  1865. static $filesystemFunctions = [
  1866. // read from filesystem
  1867. 'file_exists',
  1868. 'fileatime',
  1869. 'filectime',
  1870. 'filemtime',
  1871. 'filesize',
  1872. 'filetype',
  1873. 'is_dir',
  1874. 'is_file',
  1875. 'is_link',
  1876. 'is_readable',
  1877. 'is_writable',
  1878. 'is_writeable',
  1879. 'linkinfo',
  1880. 'lstat',
  1881. //'pathinfo',
  1882. 'getimagesize',
  1883. 'exif_read_data',
  1884. 'read_exif_data',
  1885. 'exif_thumbnail',
  1886. 'exif_imagetype',
  1887. 'hash_file',
  1888. 'hash_hmac_file',
  1889. 'md5_file',
  1890. 'sha1_file',
  1891. 'get_meta_tags',
  1892. ];
  1893. if (in_array($name, $fileWriteFunctions)) {
  1894. return true;
  1895. }
  1896. if (in_array($name, $fileContentFunctions)) {
  1897. return true;
  1898. }
  1899. if (in_array($name, $filesystemFunctions)) {
  1900. return true;
  1901. }
  1902. return false;
  1903. }
  1904. }