GravExtension.php 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755
  1. <?php
  2. /**
  3. * @package Grav\Common\Twig
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Twig\Extension;
  9. use CallbackFilterIterator;
  10. use Cron\CronExpression;
  11. use Grav\Common\Config\Config;
  12. use Grav\Common\Data\Data;
  13. use Grav\Common\Debugger;
  14. use Grav\Common\Grav;
  15. use Grav\Common\Inflector;
  16. use Grav\Common\Language\Language;
  17. use Grav\Common\Page\Collection;
  18. use Grav\Common\Page\Interfaces\PageInterface;
  19. use Grav\Common\Page\Media;
  20. use Grav\Common\Scheduler\Cron;
  21. use Grav\Common\Security;
  22. use Grav\Common\Twig\TokenParser\TwigTokenParserCache;
  23. use Grav\Common\Twig\TokenParser\TwigTokenParserLink;
  24. use Grav\Common\Twig\TokenParser\TwigTokenParserRender;
  25. use Grav\Common\Twig\TokenParser\TwigTokenParserScript;
  26. use Grav\Common\Twig\TokenParser\TwigTokenParserStyle;
  27. use Grav\Common\Twig\TokenParser\TwigTokenParserSwitch;
  28. use Grav\Common\Twig\TokenParser\TwigTokenParserThrow;
  29. use Grav\Common\Twig\TokenParser\TwigTokenParserTryCatch;
  30. use Grav\Common\Twig\TokenParser\TwigTokenParserMarkdown;
  31. use Grav\Common\User\Interfaces\UserInterface;
  32. use Grav\Common\Utils;
  33. use Grav\Common\Yaml;
  34. use Grav\Common\Helpers\Base32;
  35. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  36. use Grav\Framework\Psr7\Response;
  37. use Iterator;
  38. use JsonSerializable;
  39. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  40. use Traversable;
  41. use Twig\Environment;
  42. use Twig\Error\RuntimeError;
  43. use Twig\Extension\AbstractExtension;
  44. use Twig\Extension\GlobalsInterface;
  45. use Twig\Loader\FilesystemLoader;
  46. use Twig\Markup;
  47. use Twig\TwigFilter;
  48. use Twig\TwigFunction;
  49. use function array_slice;
  50. use function count;
  51. use function func_get_args;
  52. use function func_num_args;
  53. use function get_class;
  54. use function gettype;
  55. use function in_array;
  56. use function is_array;
  57. use function is_bool;
  58. use function is_float;
  59. use function is_int;
  60. use function is_numeric;
  61. use function is_object;
  62. use function is_scalar;
  63. use function is_string;
  64. use function strlen;
  65. /**
  66. * Class GravExtension
  67. * @package Grav\Common\Twig\Extension
  68. */
  69. class GravExtension extends AbstractExtension implements GlobalsInterface
  70. {
  71. /** @var Grav */
  72. protected $grav;
  73. /** @var Debugger|null */
  74. protected $debugger;
  75. /** @var Config */
  76. protected $config;
  77. /**
  78. * GravExtension constructor.
  79. */
  80. public function __construct()
  81. {
  82. $this->grav = Grav::instance();
  83. $this->debugger = $this->grav['debugger'] ?? null;
  84. $this->config = $this->grav['config'];
  85. }
  86. /**
  87. * Register some standard globals
  88. *
  89. * @return array
  90. */
  91. public function getGlobals(): array
  92. {
  93. return [
  94. 'grav' => $this->grav,
  95. ];
  96. }
  97. /**
  98. * Return a list of all filters.
  99. *
  100. * @return array
  101. */
  102. public function getFilters(): array
  103. {
  104. return [
  105. new TwigFilter('*ize', [$this, 'inflectorFilter']),
  106. new TwigFilter('absolute_url', [$this, 'absoluteUrlFilter']),
  107. new TwigFilter('contains', [$this, 'containsFilter']),
  108. new TwigFilter('chunk_split', [$this, 'chunkSplitFilter']),
  109. new TwigFilter('nicenumber', [$this, 'niceNumberFunc']),
  110. new TwigFilter('nicefilesize', [$this, 'niceFilesizeFunc']),
  111. new TwigFilter('nicetime', [$this, 'nicetimeFunc']),
  112. new TwigFilter('defined', [$this, 'definedDefaultFilter']),
  113. new TwigFilter('ends_with', [$this, 'endsWithFilter']),
  114. new TwigFilter('fieldName', [$this, 'fieldNameFilter']),
  115. new TwigFilter('parent_field', [$this, 'fieldParentFilter']),
  116. new TwigFilter('ksort', [$this, 'ksortFilter']),
  117. new TwigFilter('ltrim', [$this, 'ltrimFilter']),
  118. new TwigFilter('markdown', [$this, 'markdownFunction'], ['needs_context' => true, 'is_safe' => ['html']]),
  119. new TwigFilter('md5', [$this, 'md5Filter']),
  120. new TwigFilter('base32_encode', [$this, 'base32EncodeFilter']),
  121. new TwigFilter('base32_decode', [$this, 'base32DecodeFilter']),
  122. new TwigFilter('base64_encode', [$this, 'base64EncodeFilter']),
  123. new TwigFilter('base64_decode', [$this, 'base64DecodeFilter']),
  124. new TwigFilter('randomize', [$this, 'randomizeFilter']),
  125. new TwigFilter('modulus', [$this, 'modulusFilter']),
  126. new TwigFilter('rtrim', [$this, 'rtrimFilter']),
  127. new TwigFilter('pad', [$this, 'padFilter']),
  128. new TwigFilter('regex_replace', [$this, 'regexReplace']),
  129. new TwigFilter('safe_email', [$this, 'safeEmailFilter'], ['is_safe' => ['html']]),
  130. new TwigFilter('safe_truncate', [Utils::class, 'safeTruncate']),
  131. new TwigFilter('safe_truncate_html', [Utils::class, 'safeTruncateHTML']),
  132. new TwigFilter('sort_by_key', [$this, 'sortByKeyFilter']),
  133. new TwigFilter('starts_with', [$this, 'startsWithFilter']),
  134. new TwigFilter('truncate', [Utils::class, 'truncate']),
  135. new TwigFilter('truncate_html', [Utils::class, 'truncateHTML']),
  136. new TwigFilter('json_decode', [$this, 'jsonDecodeFilter']),
  137. new TwigFilter('array_unique', 'array_unique'),
  138. new TwigFilter('basename', 'basename'),
  139. new TwigFilter('dirname', 'dirname'),
  140. new TwigFilter('print_r', [$this, 'print_r']),
  141. new TwigFilter('yaml_encode', [$this, 'yamlEncodeFilter']),
  142. new TwigFilter('yaml_decode', [$this, 'yamlDecodeFilter']),
  143. new TwigFilter('nicecron', [$this, 'niceCronFilter']),
  144. new TwigFilter('replace_last', [$this, 'replaceLastFilter']),
  145. // Translations
  146. new TwigFilter('t', [$this, 'translate'], ['needs_environment' => true]),
  147. new TwigFilter('tl', [$this, 'translateLanguage']),
  148. new TwigFilter('ta', [$this, 'translateArray']),
  149. // Casting values
  150. new TwigFilter('string', [$this, 'stringFilter']),
  151. new TwigFilter('int', [$this, 'intFilter'], ['is_safe' => ['all']]),
  152. new TwigFilter('bool', [$this, 'boolFilter']),
  153. new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]),
  154. new TwigFilter('array', [$this, 'arrayFilter']),
  155. new TwigFilter('yaml', [$this, 'yamlFilter']),
  156. // Object Types
  157. new TwigFilter('get_type', [$this, 'getTypeFunc']),
  158. new TwigFilter('of_type', [$this, 'ofTypeFunc']),
  159. // PHP methods
  160. new TwigFilter('count', 'count'),
  161. new TwigFilter('array_diff', 'array_diff'),
  162. // Security fixes
  163. new TwigFilter('filter', [$this, 'filterFunc'], ['needs_environment' => true]),
  164. new TwigFilter('map', [$this, 'mapFunc'], ['needs_environment' => true]),
  165. new TwigFilter('reduce', [$this, 'reduceFunc'], ['needs_environment' => true]),
  166. ];
  167. }
  168. /**
  169. * Return a list of all functions.
  170. *
  171. * @return array
  172. */
  173. public function getFunctions(): array
  174. {
  175. return [
  176. new TwigFunction('array', [$this, 'arrayFilter']),
  177. new TwigFunction('array_key_value', [$this, 'arrayKeyValueFunc']),
  178. new TwigFunction('array_key_exists', 'array_key_exists'),
  179. new TwigFunction('array_unique', 'array_unique'),
  180. new TwigFunction('array_intersect', [$this, 'arrayIntersectFunc']),
  181. new TwigFunction('array_diff', 'array_diff'),
  182. new TwigFunction('authorize', [$this, 'authorize']),
  183. new TwigFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
  184. new TwigFunction('dump', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
  185. new TwigFunction('vardump', [$this, 'vardumpFunc']),
  186. new TwigFunction('print_r', [$this, 'print_r']),
  187. new TwigFunction('http_response_code', 'http_response_code'),
  188. new TwigFunction('evaluate', [$this, 'evaluateStringFunc'], ['needs_context' => true]),
  189. new TwigFunction('evaluate_twig', [$this, 'evaluateTwigFunc'], ['needs_context' => true]),
  190. new TwigFunction('gist', [$this, 'gistFunc']),
  191. new TwigFunction('nonce_field', [$this, 'nonceFieldFunc']),
  192. new TwigFunction('pathinfo', 'pathinfo'),
  193. new TwigFunction('parseurl', 'parse_url'),
  194. new TwigFunction('random_string', [$this, 'randomStringFunc']),
  195. new TwigFunction('repeat', [$this, 'repeatFunc']),
  196. new TwigFunction('regex_replace', [$this, 'regexReplace']),
  197. new TwigFunction('regex_filter', [$this, 'regexFilter']),
  198. new TwigFunction('regex_match', [$this, 'regexMatch']),
  199. new TwigFunction('regex_split', [$this, 'regexSplit']),
  200. new TwigFunction('string', [$this, 'stringFilter']),
  201. new TwigFunction('url', [$this, 'urlFunc']),
  202. new TwigFunction('json_decode', [$this, 'jsonDecodeFilter']),
  203. new TwigFunction('get_cookie', [$this, 'getCookie']),
  204. new TwigFunction('redirect_me', [$this, 'redirectFunc']),
  205. new TwigFunction('range', [$this, 'rangeFunc']),
  206. new TwigFunction('isajaxrequest', [$this, 'isAjaxFunc']),
  207. new TwigFunction('exif', [$this, 'exifFunc']),
  208. new TwigFunction('media_directory', [$this, 'mediaDirFunc']),
  209. new TwigFunction('body_class', [$this, 'bodyClassFunc'], ['needs_context' => true]),
  210. new TwigFunction('theme_var', [$this, 'themeVarFunc'], ['needs_context' => true]),
  211. new TwigFunction('header_var', [$this, 'pageHeaderVarFunc'], ['needs_context' => true]),
  212. new TwigFunction('read_file', [$this, 'readFileFunc']),
  213. new TwigFunction('nicenumber', [$this, 'niceNumberFunc']),
  214. new TwigFunction('nicefilesize', [$this, 'niceFilesizeFunc']),
  215. new TwigFunction('nicetime', [$this, 'nicetimeFunc']),
  216. new TwigFunction('cron', [$this, 'cronFunc']),
  217. new TwigFunction('svg_image', [$this, 'svgImageFunction']),
  218. new TwigFunction('xss', [$this, 'xssFunc']),
  219. new TwigFunction('unique_id', [$this, 'uniqueId']),
  220. // Translations
  221. new TwigFunction('t', [$this, 'translate'], ['needs_environment' => true]),
  222. new TwigFunction('tl', [$this, 'translateLanguage']),
  223. new TwigFunction('ta', [$this, 'translateArray']),
  224. // Object Types
  225. new TwigFunction('get_type', [$this, 'getTypeFunc']),
  226. new TwigFunction('of_type', [$this, 'ofTypeFunc']),
  227. // PHP methods
  228. new TwigFunction('is_numeric', 'is_numeric'),
  229. new TwigFunction('is_iterable', 'is_iterable'),
  230. new TwigFunction('is_countable', 'is_countable'),
  231. new TwigFunction('is_null', 'is_null'),
  232. new TwigFunction('is_string', 'is_string'),
  233. new TwigFunction('is_array', 'is_array'),
  234. new TwigFunction('is_object', 'is_object'),
  235. new TwigFunction('count', 'count'),
  236. new TwigFunction('array_diff', 'array_diff'),
  237. new TwigFunction('parse_url', 'parse_url'),
  238. // Security fixes
  239. new TwigFunction('filter', [$this, 'filterFunc'], ['needs_environment' => true]),
  240. new TwigFunction('map', [$this, 'mapFunc'], ['needs_environment' => true]),
  241. new TwigFunction('reduce', [$this, 'reduceFunc'], ['needs_environment' => true]),
  242. ];
  243. }
  244. /**
  245. * @return array
  246. */
  247. public function getTokenParsers(): array
  248. {
  249. return [
  250. new TwigTokenParserRender(),
  251. new TwigTokenParserThrow(),
  252. new TwigTokenParserTryCatch(),
  253. new TwigTokenParserScript(),
  254. new TwigTokenParserStyle(),
  255. new TwigTokenParserLink(),
  256. new TwigTokenParserMarkdown(),
  257. new TwigTokenParserSwitch(),
  258. new TwigTokenParserCache(),
  259. ];
  260. }
  261. /**
  262. * @param mixed $var
  263. * @return string
  264. */
  265. public function print_r($var)
  266. {
  267. return print_r($var, true);
  268. }
  269. /**
  270. * Filters field name by changing dot notation into array notation.
  271. *
  272. * @param string $str
  273. * @return string
  274. */
  275. public function fieldNameFilter($str)
  276. {
  277. $path = explode('.', rtrim($str, '.'));
  278. return array_shift($path) . ($path ? '[' . implode('][', $path) . ']' : '');
  279. }
  280. /**
  281. * Filters field name by changing dot notation into array notation.
  282. *
  283. * @param string $str
  284. * @return string
  285. */
  286. public function fieldParentFilter($str)
  287. {
  288. $path = explode('.', rtrim($str, '.'));
  289. array_pop($path);
  290. return implode('.', $path);
  291. }
  292. /**
  293. * Protects email address.
  294. *
  295. * @param string $str
  296. * @return string
  297. */
  298. public function safeEmailFilter($str)
  299. {
  300. static $list = [
  301. '"' => '&#34;',
  302. "'" => '&#39;',
  303. '&' => '&amp;',
  304. '<' => '&lt;',
  305. '>' => '&gt;',
  306. '@' => '&#64;'
  307. ];
  308. $characters = mb_str_split($str, 1, 'UTF-8');
  309. $encoded = '';
  310. foreach ($characters as $chr) {
  311. $encoded .= $list[$chr] ?? (random_int(0, 1) ? '&#' . mb_ord($chr) . ';' : $chr);
  312. }
  313. return $encoded;
  314. }
  315. /**
  316. * Returns array in a random order.
  317. *
  318. * @param array|Traversable $original
  319. * @param int $offset Can be used to return only slice of the array.
  320. * @return array
  321. */
  322. public function randomizeFilter($original, $offset = 0)
  323. {
  324. if ($original instanceof Traversable) {
  325. $original = iterator_to_array($original, false);
  326. }
  327. if (!is_array($original)) {
  328. return $original;
  329. }
  330. $sorted = [];
  331. $random = array_slice($original, $offset);
  332. shuffle($random);
  333. $sizeOf = count($original);
  334. for ($x = 0; $x < $sizeOf; $x++) {
  335. if ($x < $offset) {
  336. $sorted[] = $original[$x];
  337. } else {
  338. $sorted[] = array_shift($random);
  339. }
  340. }
  341. return $sorted;
  342. }
  343. /**
  344. * Returns the modulus of an integer
  345. *
  346. * @param string|int $number
  347. * @param int $divider
  348. * @param array|null $items array of items to select from to return
  349. * @return int
  350. */
  351. public function modulusFilter($number, $divider, $items = null)
  352. {
  353. if (is_string($number)) {
  354. $number = strlen($number);
  355. }
  356. $remainder = $number % $divider;
  357. if (is_array($items)) {
  358. return $items[$remainder] ?? $items[0];
  359. }
  360. return $remainder;
  361. }
  362. /**
  363. * Inflector supports following notations:
  364. *
  365. * `{{ 'person'|pluralize }} => people`
  366. * `{{ 'shoes'|singularize }} => shoe`
  367. * `{{ 'welcome page'|titleize }} => "Welcome Page"`
  368. * `{{ 'send_email'|camelize }} => SendEmail`
  369. * `{{ 'CamelCased'|underscorize }} => camel_cased`
  370. * `{{ 'Something Text'|hyphenize }} => something-text`
  371. * `{{ 'something_text_to_read'|humanize }} => "Something text to read"`
  372. * `{{ '181'|monthize }} => 5`
  373. * `{{ '10'|ordinalize }} => 10th`
  374. *
  375. * @param string $action
  376. * @param string $data
  377. * @param int|null $count
  378. * @return string
  379. */
  380. public function inflectorFilter($action, $data, $count = null)
  381. {
  382. $action .= 'ize';
  383. /** @var Inflector $inflector */
  384. $inflector = $this->grav['inflector'];
  385. if (in_array(
  386. $action,
  387. ['titleize', 'camelize', 'underscorize', 'hyphenize', 'humanize', 'ordinalize', 'monthize'],
  388. true
  389. )) {
  390. return $inflector->{$action}($data);
  391. }
  392. if (in_array($action, ['pluralize', 'singularize'], true)) {
  393. return $count ? $inflector->{$action}($data, $count) : $inflector->{$action}($data);
  394. }
  395. return $data;
  396. }
  397. /**
  398. * Return MD5 hash from the input.
  399. *
  400. * @param string $str
  401. * @return string
  402. */
  403. public function md5Filter($str)
  404. {
  405. return md5($str);
  406. }
  407. /**
  408. * Return Base32 encoded string
  409. *
  410. * @param string $str
  411. * @return string
  412. */
  413. public function base32EncodeFilter($str)
  414. {
  415. return Base32::encode($str);
  416. }
  417. /**
  418. * Return Base32 decoded string
  419. *
  420. * @param string $str
  421. * @return string
  422. */
  423. public function base32DecodeFilter($str)
  424. {
  425. return Base32::decode($str);
  426. }
  427. /**
  428. * Return Base64 encoded string
  429. *
  430. * @param string $str
  431. * @return string
  432. */
  433. public function base64EncodeFilter($str)
  434. {
  435. return base64_encode((string) $str);
  436. }
  437. /**
  438. * Return Base64 decoded string
  439. *
  440. * @param string $str
  441. * @return string|false
  442. */
  443. public function base64DecodeFilter($str)
  444. {
  445. return base64_decode($str);
  446. }
  447. /**
  448. * Sorts a collection by key
  449. *
  450. * @param array $input
  451. * @param string $filter
  452. * @param int $direction
  453. * @param int $sort_flags
  454. * @return array
  455. */
  456. public function sortByKeyFilter($input, $filter, $direction = SORT_ASC, $sort_flags = SORT_REGULAR)
  457. {
  458. return Utils::sortArrayByKey($input, $filter, $direction, $sort_flags);
  459. }
  460. /**
  461. * Return ksorted collection.
  462. *
  463. * @param array|null $array
  464. * @return array
  465. */
  466. public function ksortFilter($array)
  467. {
  468. if (null === $array) {
  469. $array = [];
  470. }
  471. ksort($array);
  472. return $array;
  473. }
  474. /**
  475. * Wrapper for chunk_split() function
  476. *
  477. * @param string $value
  478. * @param int $chars
  479. * @param string $split
  480. * @return string
  481. */
  482. public function chunkSplitFilter($value, $chars, $split = '-')
  483. {
  484. return chunk_split($value, $chars, $split);
  485. }
  486. /**
  487. * determine if a string contains another
  488. *
  489. * @param string $haystack
  490. * @param string $needle
  491. * @return string|bool
  492. * @todo returning $haystack here doesn't make much sense
  493. */
  494. public function containsFilter($haystack, $needle)
  495. {
  496. if (empty($needle)) {
  497. return $haystack;
  498. }
  499. return (strpos($haystack, (string) $needle) !== false);
  500. }
  501. /**
  502. * Gets a human readable output for cron syntax
  503. *
  504. * @param string $at
  505. * @return string
  506. */
  507. public function niceCronFilter($at)
  508. {
  509. $cron = new Cron($at);
  510. return $cron->getText('en');
  511. }
  512. /**
  513. * @param string|mixed $str
  514. * @param string $search
  515. * @param string $replace
  516. * @return string|mixed
  517. */
  518. public function replaceLastFilter($str, $search, $replace)
  519. {
  520. if (is_string($str) && ($pos = mb_strrpos($str, $search)) !== false) {
  521. $str = mb_substr($str, 0, $pos) . $replace . mb_substr($str, $pos + mb_strlen($search));
  522. }
  523. return $str;
  524. }
  525. /**
  526. * Get Cron object for a crontab 'at' format
  527. *
  528. * @param string $at
  529. * @return CronExpression
  530. */
  531. public function cronFunc($at)
  532. {
  533. return CronExpression::factory($at);
  534. }
  535. /**
  536. * displays a facebook style 'time ago' formatted date/time
  537. *
  538. * @param string $date
  539. * @param bool $long_strings
  540. * @param bool $show_tense
  541. * @return string
  542. */
  543. public function nicetimeFunc($date, $long_strings = true, $show_tense = true)
  544. {
  545. if (empty($date)) {
  546. return $this->grav['language']->translate('GRAV.NICETIME.NO_DATE_PROVIDED');
  547. }
  548. if ($long_strings) {
  549. $periods = [
  550. 'NICETIME.SECOND',
  551. 'NICETIME.MINUTE',
  552. 'NICETIME.HOUR',
  553. 'NICETIME.DAY',
  554. 'NICETIME.WEEK',
  555. 'NICETIME.MONTH',
  556. 'NICETIME.YEAR',
  557. 'NICETIME.DECADE'
  558. ];
  559. } else {
  560. $periods = [
  561. 'NICETIME.SEC',
  562. 'NICETIME.MIN',
  563. 'NICETIME.HR',
  564. 'NICETIME.DAY',
  565. 'NICETIME.WK',
  566. 'NICETIME.MO',
  567. 'NICETIME.YR',
  568. 'NICETIME.DEC'
  569. ];
  570. }
  571. $lengths = ['60', '60', '24', '7', '4.35', '12', '10'];
  572. $now = time();
  573. // check if unix timestamp
  574. if ((string)(int)$date === (string)$date) {
  575. $unix_date = $date;
  576. } else {
  577. $unix_date = strtotime($date);
  578. }
  579. // check validity of date
  580. if (empty($unix_date)) {
  581. return $this->grav['language']->translate('GRAV.NICETIME.BAD_DATE');
  582. }
  583. // is it future date or past date
  584. if ($now > $unix_date) {
  585. $difference = $now - $unix_date;
  586. $tense = $this->grav['language']->translate('GRAV.NICETIME.AGO');
  587. } elseif ($now == $unix_date) {
  588. $difference = $now - $unix_date;
  589. $tense = $this->grav['language']->translate('GRAV.NICETIME.JUST_NOW');
  590. } else {
  591. $difference = $unix_date - $now;
  592. $tense = $this->grav['language']->translate('GRAV.NICETIME.FROM_NOW');
  593. }
  594. for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths) - 1; $j++) {
  595. $difference /= $lengths[$j];
  596. }
  597. $difference = round($difference);
  598. if ($difference != 1) {
  599. $periods[$j] .= '_PLURAL';
  600. }
  601. if ($this->grav['language']->getTranslation(
  602. $this->grav['language']->getLanguage(),
  603. $periods[$j] . '_MORE_THAN_TWO'
  604. )
  605. ) {
  606. if ($difference > 2) {
  607. $periods[$j] .= '_MORE_THAN_TWO';
  608. }
  609. }
  610. $periods[$j] = $this->grav['language']->translate('GRAV.'.$periods[$j]);
  611. if ($now == $unix_date) {
  612. return $tense;
  613. }
  614. $time = "{$difference} {$periods[$j]}";
  615. $time .= $show_tense ? " {$tense}" : '';
  616. return $time;
  617. }
  618. /**
  619. * Allow quick check of a string for XSS Vulnerabilities
  620. *
  621. * @param string|array $data
  622. * @return bool|string|array
  623. */
  624. public function xssFunc($data)
  625. {
  626. if (!is_array($data)) {
  627. return Security::detectXss($data);
  628. }
  629. $results = Security::detectXssFromArray($data);
  630. $results_parts = array_map(static function ($value, $key) {
  631. return $key.': \''.$value . '\'';
  632. }, array_values($results), array_keys($results));
  633. return implode(', ', $results_parts);
  634. }
  635. /**
  636. * Generates a random string with configurable length, prefix and suffix.
  637. * Unlike the built-in `uniqid()`, this string is non-conflicting and safe
  638. *
  639. * @param int $length
  640. * @param array $options
  641. * @return string
  642. * @throws \Exception
  643. */
  644. public function uniqueId(int $length = 9, array $options = ['prefix' => '', 'suffix' => '']): string
  645. {
  646. return Utils::uniqueId($length, $options);
  647. }
  648. /**
  649. * @param string $string
  650. * @return string
  651. */
  652. public function absoluteUrlFilter($string)
  653. {
  654. $url = $this->grav['uri']->base();
  655. $string = preg_replace('/((?:href|src) *= *[\'"](?!(http|ftp)))/i', "$1$url", $string);
  656. return $string;
  657. }
  658. /**
  659. * @param array $context
  660. * @param string $string
  661. * @param bool $block Block or Line processing
  662. * @return string
  663. */
  664. public function markdownFunction($context, $string, $block = true)
  665. {
  666. $page = $context['page'] ?? null;
  667. return Utils::processMarkdown($string, $block, $page);
  668. }
  669. /**
  670. * @param string $haystack
  671. * @param string $needle
  672. * @return bool
  673. */
  674. public function startsWithFilter($haystack, $needle)
  675. {
  676. return Utils::startsWith($haystack, $needle);
  677. }
  678. /**
  679. * @param string $haystack
  680. * @param string $needle
  681. * @return bool
  682. */
  683. public function endsWithFilter($haystack, $needle)
  684. {
  685. return Utils::endsWith($haystack, $needle);
  686. }
  687. /**
  688. * @param mixed $value
  689. * @param null $default
  690. * @return mixed|null
  691. */
  692. public function definedDefaultFilter($value, $default = null)
  693. {
  694. return $value ?? $default;
  695. }
  696. /**
  697. * @param string $value
  698. * @param string|null $chars
  699. * @return string
  700. */
  701. public function rtrimFilter($value, $chars = null)
  702. {
  703. return null !== $chars ? rtrim($value, $chars) : rtrim($value);
  704. }
  705. /**
  706. * @param string $value
  707. * @param string|null $chars
  708. * @return string
  709. */
  710. public function ltrimFilter($value, $chars = null)
  711. {
  712. return null !== $chars ? ltrim($value, $chars) : ltrim($value);
  713. }
  714. /**
  715. * Returns a string from a value. If the value is array, return it json encoded
  716. *
  717. * @param mixed $value
  718. * @return string
  719. */
  720. public function stringFilter($value)
  721. {
  722. // Format the array as a string
  723. if (is_array($value)) {
  724. return json_encode($value);
  725. }
  726. // Boolean becomes '1' or '0'
  727. if (is_bool($value)) {
  728. $value = (int)$value;
  729. }
  730. // Cast the other values to string.
  731. return (string)$value;
  732. }
  733. /**
  734. * Casts input to int.
  735. *
  736. * @param mixed $input
  737. * @return int
  738. */
  739. public function intFilter($input)
  740. {
  741. return (int) $input;
  742. }
  743. /**
  744. * Casts input to bool.
  745. *
  746. * @param mixed $input
  747. * @return bool
  748. */
  749. public function boolFilter($input)
  750. {
  751. return (bool) $input;
  752. }
  753. /**
  754. * Casts input to float.
  755. *
  756. * @param mixed $input
  757. * @return float
  758. */
  759. public function floatFilter($input)
  760. {
  761. return (float) $input;
  762. }
  763. /**
  764. * Casts input to array.
  765. *
  766. * @param mixed $input
  767. * @return array
  768. */
  769. public function arrayFilter($input)
  770. {
  771. if (is_array($input)) {
  772. return $input;
  773. }
  774. if (is_object($input)) {
  775. if (method_exists($input, 'toArray')) {
  776. return $input->toArray();
  777. }
  778. if ($input instanceof Iterator) {
  779. return iterator_to_array($input);
  780. }
  781. }
  782. return (array)$input;
  783. }
  784. /**
  785. * @param array|object $value
  786. * @param int|null $inline
  787. * @param int|null $indent
  788. * @return string
  789. */
  790. public function yamlFilter($value, $inline = null, $indent = null): string
  791. {
  792. return Yaml::dump($value, $inline, $indent);
  793. }
  794. /**
  795. * @param Environment $twig
  796. * @return string
  797. */
  798. public function translate(Environment $twig, ...$args)
  799. {
  800. // If admin and tu filter provided, use it
  801. if (isset($this->grav['admin'])) {
  802. $numargs = count($args);
  803. $lang = null;
  804. if (($numargs === 3 && is_array($args[1])) || ($numargs === 2 && !is_array($args[1]))) {
  805. $lang = array_pop($args);
  806. /** @var Language $language */
  807. $language = $this->grav['language'];
  808. if (is_string($lang) && !$language->getLanguageCode($lang)) {
  809. $args[] = $lang;
  810. $lang = null;
  811. }
  812. } elseif ($numargs === 2 && is_array($args[1])) {
  813. $subs = array_pop($args);
  814. $args = array_merge($args, $subs);
  815. }
  816. return $this->grav['admin']->translate($args, $lang);
  817. }
  818. $translation = $this->grav['language']->translate($args);
  819. if ($this->config->get('system.languages.debug', false)) {
  820. return new Markup("<span class=\"translate-debug\" data-toggle=\"tooltip\" title=\"" . $args[0] . "\">$translation</span>", 'UTF-8');
  821. } else {
  822. return $translation;
  823. }
  824. }
  825. /**
  826. * Translate Strings
  827. *
  828. * @param string|array $args
  829. * @param array|null $languages
  830. * @param bool $array_support
  831. * @param bool $html_out
  832. * @return string
  833. */
  834. public function translateLanguage($args, array $languages = null, $array_support = false, $html_out = false)
  835. {
  836. /** @var Language $language */
  837. $language = $this->grav['language'];
  838. return $language->translate($args, $languages, $array_support, $html_out);
  839. }
  840. /**
  841. * @param string $key
  842. * @param string $index
  843. * @param array|null $lang
  844. * @return string
  845. */
  846. public function translateArray($key, $index, $lang = null)
  847. {
  848. /** @var Language $language */
  849. $language = $this->grav['language'];
  850. return $language->translateArray($key, $index, $lang);
  851. }
  852. /**
  853. * Repeat given string x times.
  854. *
  855. * @param string $input
  856. * @param int $multiplier
  857. *
  858. * @return string
  859. */
  860. public function repeatFunc($input, $multiplier)
  861. {
  862. return str_repeat($input, (int) $multiplier);
  863. }
  864. /**
  865. * Return URL to the resource.
  866. *
  867. * @example {{ url('theme://images/logo.png')|default('http://www.placehold.it/150x100/f4f4f4') }}
  868. *
  869. * @param string $input Resource to be located.
  870. * @param bool $domain True to include domain name.
  871. * @param bool $failGracefully If true, return URL even if the file does not exist.
  872. * @return string|false Returns url to the resource or null if resource was not found.
  873. */
  874. public function urlFunc($input, $domain = false, $failGracefully = false)
  875. {
  876. return Utils::url($input, $domain, $failGracefully);
  877. }
  878. /**
  879. * This function will evaluate Twig $twig through the $environment, and return its results.
  880. *
  881. * @param array $context
  882. * @param string $twig
  883. * @return mixed
  884. */
  885. public function evaluateTwigFunc($context, $twig)
  886. {
  887. $loader = new FilesystemLoader('.');
  888. $env = new Environment($loader);
  889. $env->addExtension($this);
  890. $template = $env->createTemplate($twig);
  891. return $template->render($context);
  892. }
  893. /**
  894. * This function will evaluate a $string through the $environment, and return its results.
  895. *
  896. * @param array $context
  897. * @param string $string
  898. * @return mixed
  899. */
  900. public function evaluateStringFunc($context, $string)
  901. {
  902. return $this->evaluateTwigFunc($context, "{{ $string }}");
  903. }
  904. /**
  905. * Based on Twig\Extension\Debug / twig_var_dump
  906. * (c) 2011 Fabien Potencier
  907. *
  908. * @param Environment $env
  909. * @param array $context
  910. */
  911. public function dump(Environment $env, $context)
  912. {
  913. if (!$env->isDebug() || !$this->debugger) {
  914. return;
  915. }
  916. $count = func_num_args();
  917. if (2 === $count) {
  918. $data = [];
  919. foreach ($context as $key => $value) {
  920. if (is_object($value)) {
  921. if (method_exists($value, 'toArray')) {
  922. $data[$key] = $value->toArray();
  923. } else {
  924. $data[$key] = 'Object (' . get_class($value) . ')';
  925. }
  926. } else {
  927. $data[$key] = $value;
  928. }
  929. }
  930. $this->debugger->addMessage($data, 'debug');
  931. } else {
  932. for ($i = 2; $i < $count; $i++) {
  933. $var = func_get_arg($i);
  934. $this->debugger->addMessage($var, 'debug');
  935. }
  936. }
  937. }
  938. /**
  939. * Output a Gist
  940. *
  941. * @param string $id
  942. * @param string|false $file
  943. * @return string
  944. */
  945. public function gistFunc($id, $file = false)
  946. {
  947. $url = 'https://gist.github.com/' . $id . '.js';
  948. if ($file) {
  949. $url .= '?file=' . $file;
  950. }
  951. return '<script src="' . $url . '"></script>';
  952. }
  953. /**
  954. * Generate a random string
  955. *
  956. * @param int $count
  957. * @return string
  958. */
  959. public function randomStringFunc($count = 5)
  960. {
  961. return Utils::generateRandomString($count);
  962. }
  963. /**
  964. * Pad a string to a certain length with another string
  965. *
  966. * @param string $input
  967. * @param int $pad_length
  968. * @param string $pad_string
  969. * @param int $pad_type
  970. * @return string
  971. */
  972. public static function padFilter($input, $pad_length, $pad_string = ' ', $pad_type = STR_PAD_RIGHT)
  973. {
  974. return str_pad($input, (int)$pad_length, $pad_string, $pad_type);
  975. }
  976. /**
  977. * Workaround for twig associative array initialization
  978. * Returns a key => val array
  979. *
  980. * @param string $key key of item
  981. * @param string $val value of item
  982. * @param array|null $current_array optional array to add to
  983. * @return array
  984. */
  985. public function arrayKeyValueFunc($key, $val, $current_array = null)
  986. {
  987. if (empty($current_array)) {
  988. return array($key => $val);
  989. }
  990. $current_array[$key] = $val;
  991. return $current_array;
  992. }
  993. /**
  994. * Wrapper for array_intersect() method
  995. *
  996. * @param array|Collection $array1
  997. * @param array|Collection $array2
  998. * @return array|Collection
  999. */
  1000. public function arrayIntersectFunc($array1, $array2)
  1001. {
  1002. if ($array1 instanceof Collection && $array2 instanceof Collection) {
  1003. return $array1->intersect($array2)->toArray();
  1004. }
  1005. return array_intersect($array1, $array2);
  1006. }
  1007. /**
  1008. * Translate a string
  1009. *
  1010. * @return string
  1011. */
  1012. public function translateFunc()
  1013. {
  1014. return $this->grav['language']->translate(func_get_args());
  1015. }
  1016. /**
  1017. * Authorize an action. Returns true if the user is logged in and
  1018. * has the right to execute $action.
  1019. *
  1020. * @param string|array $action An action or a list of actions. Each
  1021. * entry can be a string like 'group.action'
  1022. * or without dot notation an associative
  1023. * array.
  1024. * @return bool Returns TRUE if the user is authorized to
  1025. * perform the action, FALSE otherwise.
  1026. */
  1027. public function authorize($action)
  1028. {
  1029. // Admin can use Flex users even if the site does not; make sure we use the right version of the user.
  1030. $admin = $this->grav['admin'] ?? null;
  1031. if ($admin) {
  1032. $user = $admin->user;
  1033. } else {
  1034. /** @var UserInterface|null $user */
  1035. $user = $this->grav['user'] ?? null;
  1036. }
  1037. if (!$user) {
  1038. return false;
  1039. }
  1040. if (is_array($action)) {
  1041. if (Utils::isAssoc($action)) {
  1042. // Handle nested access structure.
  1043. $actions = Utils::arrayFlattenDotNotation($action);
  1044. } else {
  1045. // Handle simple access list.
  1046. $actions = array_combine($action, array_fill(0, count($action), true));
  1047. }
  1048. } else {
  1049. // Handle single action.
  1050. $actions = [(string)$action => true];
  1051. }
  1052. $count = count($actions);
  1053. foreach ($actions as $act => $authenticated) {
  1054. // Ignore 'admin.super' if it's not the only value to be checked.
  1055. if ($act === 'admin.super' && $count > 1 && $user instanceof FlexObjectInterface) {
  1056. continue;
  1057. }
  1058. $auth = $user->authorize($act) ?? false;
  1059. if (is_bool($auth) && $auth === Utils::isPositive($authenticated)) {
  1060. return true;
  1061. }
  1062. }
  1063. return false;
  1064. }
  1065. /**
  1066. * Used to add a nonce to a form. Call {{ nonce_field('action') }} specifying a string representing the action.
  1067. *
  1068. * For maximum protection, ensure that the string representing the action is as specific as possible
  1069. *
  1070. * @param string $action the action
  1071. * @param string $nonceParamName a custom nonce param name
  1072. * @return string the nonce input field
  1073. */
  1074. public function nonceFieldFunc($action, $nonceParamName = 'nonce')
  1075. {
  1076. $string = '<input type="hidden" name="' . $nonceParamName . '" value="' . Utils::getNonce($action) . '" />';
  1077. return $string;
  1078. }
  1079. /**
  1080. * Decodes string from JSON.
  1081. *
  1082. * @param string $str
  1083. * @param bool $assoc
  1084. * @param int $depth
  1085. * @param int $options
  1086. * @return array
  1087. */
  1088. public function jsonDecodeFilter($str, $assoc = false, $depth = 512, $options = 0)
  1089. {
  1090. if ($str === null) {
  1091. $str = '';
  1092. }
  1093. return json_decode(html_entity_decode($str, ENT_COMPAT | ENT_HTML401, 'UTF-8'), $assoc, $depth, $options);
  1094. }
  1095. /**
  1096. * Used to retrieve a cookie value
  1097. *
  1098. * @param string $key The cookie name to retrieve
  1099. * @return string
  1100. */
  1101. public function getCookie($key)
  1102. {
  1103. $cookie_value = filter_input(INPUT_COOKIE, $key);
  1104. if ($cookie_value === null) {
  1105. return null;
  1106. }
  1107. return htmlspecialchars(strip_tags($cookie_value), ENT_QUOTES, 'UTF-8');
  1108. }
  1109. /**
  1110. * Twig wrapper for PHP's preg_replace method
  1111. *
  1112. * @param string|string[] $subject the content to perform the replacement on
  1113. * @param string|string[] $pattern the regex pattern to use for matches
  1114. * @param string|string[] $replace the replacement value either as a string or an array of replacements
  1115. * @param int $limit the maximum possible replacements for each pattern in each subject
  1116. * @return string|string[]|null the resulting content
  1117. */
  1118. public function regexReplace($subject, $pattern, $replace, $limit = -1)
  1119. {
  1120. return preg_replace($pattern, $replace, $subject, $limit);
  1121. }
  1122. /**
  1123. * Twig wrapper for PHP's preg_grep method
  1124. *
  1125. * @param array $array
  1126. * @param string $regex
  1127. * @param int $flags
  1128. * @return array
  1129. */
  1130. public function regexFilter($array, $regex, $flags = 0)
  1131. {
  1132. return preg_grep($regex, $array, $flags);
  1133. }
  1134. /**
  1135. * Twig wrapper for PHP's preg_match method
  1136. *
  1137. * @param string $subject the content to perform the match on
  1138. * @param string $pattern the regex pattern to use for match
  1139. * @param int $flags
  1140. * @param int $offset
  1141. * @return array|false returns the matches if there is at least one match in the subject for a given pattern or null if not.
  1142. */
  1143. public function regexMatch($subject, $pattern, $flags = 0, $offset = 0)
  1144. {
  1145. if (preg_match($pattern, $subject, $matches, $flags, $offset) === false) {
  1146. return false;
  1147. }
  1148. return $matches;
  1149. }
  1150. /**
  1151. * Twig wrapper for PHP's preg_split method
  1152. *
  1153. * @param string $subject the content to perform the split on
  1154. * @param string $pattern the regex pattern to use for split
  1155. * @param int $limit the maximum possible splits for the given pattern
  1156. * @param int $flags
  1157. * @return array|false the resulting array after performing the split operation
  1158. */
  1159. public function regexSplit($subject, $pattern, $limit = -1, $flags = 0)
  1160. {
  1161. return preg_split($pattern, $subject, $limit, $flags);
  1162. }
  1163. /**
  1164. * redirect browser from twig
  1165. *
  1166. * @param string $url the url to redirect to
  1167. * @param int $statusCode statusCode, default 303
  1168. * @return void
  1169. */
  1170. public function redirectFunc($url, $statusCode = 303)
  1171. {
  1172. $response = new Response($statusCode, ['location' => $url]);
  1173. $this->grav->close($response);
  1174. }
  1175. /**
  1176. * Generates an array containing a range of elements, optionally stepped
  1177. *
  1178. * @param int $start Minimum number, default 0
  1179. * @param int $end Maximum number, default `getrandmax()`
  1180. * @param int $step Increment between elements in the sequence, default 1
  1181. * @return array
  1182. */
  1183. public function rangeFunc($start = 0, $end = 100, $step = 1)
  1184. {
  1185. return range($start, $end, $step);
  1186. }
  1187. /**
  1188. * Check if HTTP_X_REQUESTED_WITH has been set to xmlhttprequest,
  1189. * in which case we may unsafely assume ajax. Non critical use only.
  1190. *
  1191. * @return bool True if HTTP_X_REQUESTED_WITH exists and has been set to xmlhttprequest
  1192. */
  1193. public function isAjaxFunc()
  1194. {
  1195. return (
  1196. !empty($_SERVER['HTTP_X_REQUESTED_WITH'])
  1197. && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
  1198. }
  1199. /**
  1200. * Get the Exif data for a file
  1201. *
  1202. * @param string $image
  1203. * @param bool $raw
  1204. * @return mixed
  1205. */
  1206. public function exifFunc($image, $raw = false)
  1207. {
  1208. if (isset($this->grav['exif'])) {
  1209. /** @var UniformResourceLocator $locator */
  1210. $locator = $this->grav['locator'];
  1211. if ($locator->isStream($image)) {
  1212. $image = $locator->findResource($image);
  1213. }
  1214. $exif_reader = $this->grav['exif']->getReader();
  1215. if ($image && file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) {
  1216. $exif_data = $exif_reader->read($image);
  1217. if ($exif_data) {
  1218. if ($raw) {
  1219. return $exif_data->getRawData();
  1220. }
  1221. return $exif_data->getData();
  1222. }
  1223. }
  1224. }
  1225. return null;
  1226. }
  1227. /**
  1228. * Simple function to read a file based on a filepath and output it
  1229. *
  1230. * @param string $filepath
  1231. * @return bool|string
  1232. */
  1233. public function readFileFunc($filepath)
  1234. {
  1235. /** @var UniformResourceLocator $locator */
  1236. $locator = $this->grav['locator'];
  1237. if ($locator->isStream($filepath)) {
  1238. $filepath = $locator->findResource($filepath);
  1239. }
  1240. if ($filepath && file_exists($filepath)) {
  1241. return file_get_contents($filepath);
  1242. }
  1243. return false;
  1244. }
  1245. /**
  1246. * Process a folder as Media and return a media object
  1247. *
  1248. * @param string $media_dir
  1249. * @return Media|null
  1250. */
  1251. public function mediaDirFunc($media_dir)
  1252. {
  1253. /** @var UniformResourceLocator $locator */
  1254. $locator = $this->grav['locator'];
  1255. if ($locator->isStream($media_dir)) {
  1256. $media_dir = $locator->findResource($media_dir);
  1257. }
  1258. if ($media_dir && file_exists($media_dir)) {
  1259. return new Media($media_dir);
  1260. }
  1261. return null;
  1262. }
  1263. /**
  1264. * Dump a variable to the browser
  1265. *
  1266. * @param mixed $var
  1267. * @return void
  1268. */
  1269. public function vardumpFunc($var)
  1270. {
  1271. dump($var);
  1272. }
  1273. /**
  1274. * Returns a nicer more readable filesize based on bytes
  1275. *
  1276. * @param int $bytes
  1277. * @return string
  1278. */
  1279. public function niceFilesizeFunc($bytes)
  1280. {
  1281. return Utils::prettySize($bytes);
  1282. }
  1283. /**
  1284. * Returns a nicer more readable number
  1285. *
  1286. * @param int|float|string $n
  1287. * @return string|bool
  1288. */
  1289. public function niceNumberFunc($n)
  1290. {
  1291. if (!is_float($n) && !is_int($n)) {
  1292. if (!is_string($n) || $n === '') {
  1293. return false;
  1294. }
  1295. // Strip any thousand formatting and find the first number.
  1296. $list = array_filter(preg_split("/\D+/", str_replace(',', '', $n)));
  1297. $n = reset($list);
  1298. if (!is_numeric($n)) {
  1299. return false;
  1300. }
  1301. $n = (float)$n;
  1302. }
  1303. // now filter it;
  1304. if ($n > 1000000000000) {
  1305. return round($n/1000000000000, 2).' t';
  1306. }
  1307. if ($n > 1000000000) {
  1308. return round($n/1000000000, 2).' b';
  1309. }
  1310. if ($n > 1000000) {
  1311. return round($n/1000000, 2).' m';
  1312. }
  1313. if ($n > 1000) {
  1314. return round($n/1000, 2).' k';
  1315. }
  1316. return number_format($n);
  1317. }
  1318. /**
  1319. * Get a theme variable
  1320. * Will try to get the variable for the current page, if not found, it tries it's parent page on up to root.
  1321. * If still not found, will use the theme's configuration value,
  1322. * If still not found, will use the $default value passed in
  1323. *
  1324. * @param array $context Twig Context
  1325. * @param string $var variable to be found (using dot notation)
  1326. * @param null $default the default value to be used as last resort
  1327. * @param PageInterface|null $page an optional page to use for the current page
  1328. * @param bool $exists toggle to simply return the page where the variable is set, else null
  1329. * @return mixed
  1330. */
  1331. public function themeVarFunc($context, $var, $default = null, $page = null, $exists = false)
  1332. {
  1333. $page = $page ?? $context['page'] ?? Grav::instance()['page'] ?? null;
  1334. // Try to find var in the page headers
  1335. if ($page instanceof PageInterface && $page->exists()) {
  1336. // Loop over pages and look for header vars
  1337. while ($page && !$page->root()) {
  1338. $header = new Data((array)$page->header());
  1339. $value = $header->get($var);
  1340. if (isset($value)) {
  1341. if ($exists) {
  1342. return $page;
  1343. }
  1344. return $value;
  1345. }
  1346. $page = $page->parent();
  1347. }
  1348. }
  1349. if ($exists) {
  1350. return false;
  1351. }
  1352. return Grav::instance()['config']->get('theme.' . $var, $default);
  1353. }
  1354. /**
  1355. * Look for a page header variable in an array of pages working its way through until a value is found
  1356. *
  1357. * @param array $context
  1358. * @param string $var the variable to look for in the page header
  1359. * @param string|string[]|null $pages array of pages to check (current page upwards if not null)
  1360. * @return mixed
  1361. * @deprecated 1.7 Use themeVarFunc() instead
  1362. */
  1363. public function pageHeaderVarFunc($context, $var, $pages = null)
  1364. {
  1365. if (is_array($pages)) {
  1366. $page = array_shift($pages);
  1367. } else {
  1368. $page = null;
  1369. }
  1370. return $this->themeVarFunc($context, $var, null, $page);
  1371. }
  1372. /**
  1373. * takes an array of classes, and if they are not set on body_classes
  1374. * look to see if they are set in theme config
  1375. *
  1376. * @param array $context
  1377. * @param string|string[] $classes
  1378. * @return string
  1379. */
  1380. public function bodyClassFunc($context, $classes)
  1381. {
  1382. $header = $context['page']->header();
  1383. $body_classes = $header->body_classes ?? '';
  1384. foreach ((array)$classes as $class) {
  1385. if (!empty($body_classes) && Utils::contains($body_classes, $class)) {
  1386. continue;
  1387. }
  1388. $val = $this->config->get('theme.' . $class, false) ? $class : false;
  1389. $body_classes .= $val ? ' ' . $val : '';
  1390. }
  1391. return $body_classes;
  1392. }
  1393. /**
  1394. * Returns the content of an SVG image and adds extra classes as needed
  1395. *
  1396. * @param string $path
  1397. * @param string|null $classes
  1398. * @return string|string[]|null
  1399. */
  1400. public static function svgImageFunction($path, $classes = null, $strip_style = false)
  1401. {
  1402. $path = Utils::fullPath($path);
  1403. $classes = $classes ?: '';
  1404. if (file_exists($path) && !is_dir($path)) {
  1405. $svg = file_get_contents($path);
  1406. $classes = " inline-block $classes";
  1407. $matched = false;
  1408. //Remove xml tag if it exists
  1409. $svg = preg_replace('/^<\?xml.*\?>/','', $svg);
  1410. //Strip style if needed
  1411. if ($strip_style) {
  1412. $svg = preg_replace('/<style.*<\/style>/s', '', $svg);
  1413. }
  1414. //Look for existing class
  1415. $svg = preg_replace_callback('/^<svg[^>]*(class=\"([^"]*)\")[^>]*>/', function($matches) use ($classes, &$matched) {
  1416. if (isset($matches[2])) {
  1417. $new_classes = $matches[2] . $classes;
  1418. $matched = true;
  1419. return str_replace($matches[1], "class=\"$new_classes\"", $matches[0]);
  1420. }
  1421. return $matches[0];
  1422. }, $svg
  1423. );
  1424. // no matches found just add the class
  1425. if (!$matched) {
  1426. $classes = trim($classes);
  1427. $svg = str_replace('<svg ', "<svg class=\"$classes\" ", $svg);
  1428. }
  1429. return trim($svg);
  1430. }
  1431. return null;
  1432. }
  1433. /**
  1434. * Dump/Encode data into YAML format
  1435. *
  1436. * @param array|object $data
  1437. * @param int $inline integer number of levels of inline syntax
  1438. * @return string
  1439. */
  1440. public function yamlEncodeFilter($data, $inline = 10)
  1441. {
  1442. if (!is_array($data)) {
  1443. if ($data instanceof JsonSerializable) {
  1444. $data = $data->jsonSerialize();
  1445. } elseif (method_exists($data, 'toArray')) {
  1446. $data = $data->toArray();
  1447. } else {
  1448. $data = json_decode(json_encode($data), true);
  1449. }
  1450. }
  1451. return Yaml::dump($data, $inline);
  1452. }
  1453. /**
  1454. * Decode/Parse data from YAML format
  1455. *
  1456. * @param string $data
  1457. * @return array
  1458. */
  1459. public function yamlDecodeFilter($data)
  1460. {
  1461. return Yaml::parse($data);
  1462. }
  1463. /**
  1464. * Function/Filter to return the type of variable
  1465. *
  1466. * @param mixed $var
  1467. * @return string
  1468. */
  1469. public function getTypeFunc($var)
  1470. {
  1471. return gettype($var);
  1472. }
  1473. /**
  1474. * Function/Filter to test type of variable
  1475. *
  1476. * @param mixed $var
  1477. * @param string|null $typeTest
  1478. * @param string|null $className
  1479. * @return bool
  1480. */
  1481. public function ofTypeFunc($var, $typeTest = null, $className = null)
  1482. {
  1483. switch ($typeTest) {
  1484. default:
  1485. return false;
  1486. case 'array':
  1487. return is_array($var);
  1488. case 'bool':
  1489. return is_bool($var);
  1490. case 'class':
  1491. return is_object($var) === true && get_class($var) === $className;
  1492. case 'float':
  1493. return is_float($var);
  1494. case 'int':
  1495. return is_int($var);
  1496. case 'numeric':
  1497. return is_numeric($var);
  1498. case 'object':
  1499. return is_object($var);
  1500. case 'scalar':
  1501. return is_scalar($var);
  1502. case 'string':
  1503. return is_string($var);
  1504. }
  1505. }
  1506. /**
  1507. * @param Environment $env
  1508. * @param array $array
  1509. * @param callable|string $arrow
  1510. * @return array|CallbackFilterIterator
  1511. * @throws RuntimeError
  1512. */
  1513. function filterFunc(Environment $env, $array, $arrow)
  1514. {
  1515. if (!$arrow instanceof \Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) {
  1516. throw new RuntimeError('Twig |filter("' . $arrow . '") is not allowed.');
  1517. }
  1518. return twig_array_filter($env, $array, $arrow);
  1519. }
  1520. /**
  1521. * @param Environment $env
  1522. * @param array $array
  1523. * @param callable|string $arrow
  1524. * @return array|CallbackFilterIterator
  1525. * @throws RuntimeError
  1526. */
  1527. function mapFunc(Environment $env, $array, $arrow)
  1528. {
  1529. if (!$arrow instanceof \Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) {
  1530. throw new RuntimeError('Twig |map("' . $arrow . '") is not allowed.');
  1531. }
  1532. return twig_array_map($env, $array, $arrow);
  1533. }
  1534. /**
  1535. * @param Environment $env
  1536. * @param array $array
  1537. * @param callable|string $arrow
  1538. * @return array|CallbackFilterIterator
  1539. * @throws RuntimeError
  1540. */
  1541. function reduceFunc(Environment $env, $array, $arrow)
  1542. {
  1543. if (!$arrow instanceof \Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) {
  1544. throw new RuntimeError('Twig |reduce("' . $arrow . '") is not allowed.');
  1545. }
  1546. return twig_array_map($env, $array, $arrow);
  1547. }
  1548. }