GravExtension.php 47 KB

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