TwigExtension.php 40 KB

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