TwigExtension.php 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445
  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'], ['needs_context' => true, '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 syntax
  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 array $context
  543. * @param bool $block Block or Line processing
  544. * @return mixed|string
  545. */
  546. public function markdownFunction($context, $string, $block = true)
  547. {
  548. $page = $context['page'] ?? null;
  549. return Utils::processMarkdown($string, $block, $page);
  550. }
  551. /**
  552. * @param string $haystack
  553. * @param string $needle
  554. *
  555. * @return bool
  556. */
  557. public function startsWithFilter($haystack, $needle)
  558. {
  559. return Utils::startsWith($haystack, $needle);
  560. }
  561. /**
  562. * @param string $haystack
  563. * @param string $needle
  564. *
  565. * @return bool
  566. */
  567. public function endsWithFilter($haystack, $needle)
  568. {
  569. return Utils::endsWith($haystack, $needle);
  570. }
  571. /**
  572. * @param mixed $value
  573. * @param null $default
  574. *
  575. * @return null
  576. */
  577. public function definedDefaultFilter($value, $default = null)
  578. {
  579. return null !== $value ? $value : $default;
  580. }
  581. /**
  582. * @param string $value
  583. * @param null $chars
  584. *
  585. * @return string
  586. */
  587. public function rtrimFilter($value, $chars = null)
  588. {
  589. return rtrim($value, $chars);
  590. }
  591. /**
  592. * @param string $value
  593. * @param null $chars
  594. *
  595. * @return string
  596. */
  597. public function ltrimFilter($value, $chars = null)
  598. {
  599. return ltrim($value, $chars);
  600. }
  601. /**
  602. * Casts input to string.
  603. *
  604. * @param mixed $input
  605. * @return string
  606. */
  607. public function stringFilter($input)
  608. {
  609. return (string) $input;
  610. }
  611. /**
  612. * Casts input to int.
  613. *
  614. * @param mixed $input
  615. * @return int
  616. */
  617. public function intFilter($input)
  618. {
  619. return (int) $input;
  620. }
  621. /**
  622. * Casts input to bool.
  623. *
  624. * @param mixed $input
  625. * @return bool
  626. */
  627. public function boolFilter($input)
  628. {
  629. return (bool) $input;
  630. }
  631. /**
  632. * Casts input to float.
  633. *
  634. * @param mixed $input
  635. * @return float
  636. */
  637. public function floatFilter($input)
  638. {
  639. return (float) $input;
  640. }
  641. /**
  642. * Casts input to array.
  643. *
  644. * @param mixed $input
  645. * @return array
  646. */
  647. public function arrayFilter($input)
  648. {
  649. return (array) $input;
  650. }
  651. /**
  652. * @return string
  653. */
  654. public function translate(\Twig_Environment $twig)
  655. {
  656. // shift off the environment
  657. $args = func_get_args();
  658. array_shift($args);
  659. // If admin and tu filter provided, use it
  660. if (isset($this->grav['admin'])) {
  661. $numargs = count($args);
  662. $lang = null;
  663. if (($numargs === 3 && is_array($args[1])) || ($numargs === 2 && !is_array($args[1]))) {
  664. $lang = array_pop($args);
  665. } elseif ($numargs === 2 && is_array($args[1])) {
  666. $subs = array_pop($args);
  667. $args = array_merge($args, $subs);
  668. }
  669. return $this->grav['admin']->translate($args, $lang);
  670. }
  671. // else use the default grav translate functionality
  672. return $this->grav['language']->translate($args);
  673. }
  674. /**
  675. * Translate Strings
  676. *
  677. * @param string|array $args
  678. * @param array|null $languages
  679. * @param bool $array_support
  680. * @param bool $html_out
  681. * @return string
  682. */
  683. public function translateLanguage($args, array $languages = null, $array_support = false, $html_out = false)
  684. {
  685. /** @var Language $language */
  686. $language = $this->grav['language'];
  687. return $language->translate($args, $languages, $array_support, $html_out);
  688. }
  689. /**
  690. * @param string $key
  691. * @param string $index
  692. * @param array|null $lang
  693. * @return string
  694. */
  695. public function translateArray($key, $index, $lang = null)
  696. {
  697. /** @var Language $language */
  698. $language = $this->grav['language'];
  699. return $language->translateArray($key, $index, $lang);
  700. }
  701. /**
  702. * Repeat given string x times.
  703. *
  704. * @param string $input
  705. * @param int $multiplier
  706. *
  707. * @return string
  708. */
  709. public function repeatFunc($input, $multiplier)
  710. {
  711. return str_repeat($input, $multiplier);
  712. }
  713. /**
  714. * Return URL to the resource.
  715. *
  716. * @example {{ url('theme://images/logo.png')|default('http://www.placehold.it/150x100/f4f4f4') }}
  717. *
  718. * @param string $input Resource to be located.
  719. * @param bool $domain True to include domain name.
  720. *
  721. * @return string|null Returns url to the resource or null if resource was not found.
  722. */
  723. public function urlFunc($input, $domain = false)
  724. {
  725. return Utils::url($input, $domain);
  726. }
  727. /**
  728. * This function will evaluate Twig $twig through the $environment, and return its results.
  729. *
  730. * @param array $context
  731. * @param string $twig
  732. * @return mixed
  733. */
  734. public function evaluateTwigFunc($context, $twig ) {
  735. $loader = new \Twig_Loader_Filesystem('.');
  736. $env = new \Twig_Environment($loader);
  737. $template = $env->createTemplate($twig);
  738. return $template->render($context);
  739. }
  740. /**
  741. * This function will evaluate a $string through the $environment, and return its results.
  742. *
  743. * @param array $context
  744. * @param string $string
  745. * @return mixed
  746. */
  747. public function evaluateStringFunc($context, $string )
  748. {
  749. return $this->evaluateTwigFunc($context, "{{ $string }}");
  750. }
  751. /**
  752. * Based on Twig_Extension_Debug / twig_var_dump
  753. * (c) 2011 Fabien Potencier
  754. *
  755. * @param \Twig_Environment $env
  756. * @param string $context
  757. */
  758. public function dump(\Twig_Environment $env, $context)
  759. {
  760. if (!$env->isDebug() || !$this->debugger) {
  761. return;
  762. }
  763. $count = func_num_args();
  764. if (2 === $count) {
  765. $data = [];
  766. foreach ($context as $key => $value) {
  767. if (is_object($value)) {
  768. if (method_exists($value, 'toArray')) {
  769. $data[$key] = $value->toArray();
  770. } else {
  771. $data[$key] = "Object (" . get_class($value) . ")";
  772. }
  773. } else {
  774. $data[$key] = $value;
  775. }
  776. }
  777. $this->debugger->addMessage($data, 'debug');
  778. } else {
  779. for ($i = 2; $i < $count; $i++) {
  780. $this->debugger->addMessage(func_get_arg($i), 'debug');
  781. }
  782. }
  783. }
  784. /**
  785. * Output a Gist
  786. *
  787. * @param string $id
  788. * @param string|bool $file
  789. *
  790. * @return string
  791. */
  792. public function gistFunc($id, $file = false)
  793. {
  794. $url = 'https://gist.github.com/' . $id . '.js';
  795. if ($file) {
  796. $url .= '?file=' . $file;
  797. }
  798. return '<script src="' . $url . '"></script>';
  799. }
  800. /**
  801. * Generate a random string
  802. *
  803. * @param int $count
  804. *
  805. * @return string
  806. */
  807. public function randomStringFunc($count = 5)
  808. {
  809. return Utils::generateRandomString($count);
  810. }
  811. /**
  812. * Pad a string to a certain length with another string
  813. *
  814. * @param string $input
  815. * @param int $pad_length
  816. * @param string $pad_string
  817. * @param int $pad_type
  818. *
  819. * @return string
  820. */
  821. public static function padFilter($input, $pad_length, $pad_string = ' ', $pad_type = STR_PAD_RIGHT)
  822. {
  823. return str_pad($input, (int)$pad_length, $pad_string, $pad_type);
  824. }
  825. /**
  826. * Workaround for twig associative array initialization
  827. * Returns a key => val array
  828. *
  829. * @param string $key key of item
  830. * @param string $val value of item
  831. * @param array $current_array optional array to add to
  832. *
  833. * @return array
  834. */
  835. public function arrayKeyValueFunc($key, $val, $current_array = null)
  836. {
  837. if (empty($current_array)) {
  838. return array($key => $val);
  839. }
  840. $current_array[$key] = $val;
  841. return $current_array;
  842. }
  843. /**
  844. * Wrapper for array_intersect() method
  845. *
  846. * @param array $array1
  847. * @param array $array2
  848. * @return array
  849. */
  850. public function arrayIntersectFunc($array1, $array2)
  851. {
  852. if ($array1 instanceof Collection && $array2 instanceof Collection) {
  853. return $array1->intersect($array2);
  854. }
  855. return array_intersect($array1, $array2);
  856. }
  857. /**
  858. * Returns a string from a value. If the value is array, return it json encoded
  859. *
  860. * @param array|string $value
  861. *
  862. * @return string
  863. */
  864. public function stringFunc($value)
  865. {
  866. if (is_array($value)) { //format the array as a string
  867. return json_encode($value);
  868. }
  869. return $value;
  870. }
  871. /**
  872. * Translate a string
  873. *
  874. * @return string
  875. */
  876. public function translateFunc()
  877. {
  878. return $this->grav['language']->translate(func_get_args());
  879. }
  880. /**
  881. * Authorize an action. Returns true if the user is logged in and
  882. * has the right to execute $action.
  883. *
  884. * @param string|array $action An action or a list of actions. Each
  885. * entry can be a string like 'group.action'
  886. * or without dot notation an associative
  887. * array.
  888. * @return bool Returns TRUE if the user is authorized to
  889. * perform the action, FALSE otherwise.
  890. */
  891. public function authorize($action)
  892. {
  893. /** @var UserInterface|null $user */
  894. $user = $this->grav['user'] ?? null;
  895. if (!$user || !$user->authenticated || (isset($user->authorized) && !$user->authorized)) {
  896. return false;
  897. }
  898. $action = (array) $action;
  899. foreach ($action as $key => $perms) {
  900. $prefix = is_int($key) ? '' : $key . '.';
  901. $perms = $prefix ? (array) $perms : [$perms => true];
  902. foreach ($perms as $action2 => $authenticated) {
  903. if ($user->authorize($prefix . $action2)) {
  904. return $authenticated;
  905. }
  906. }
  907. }
  908. return false;
  909. }
  910. /**
  911. * Used to add a nonce to a form. Call {{ nonce_field('action') }} specifying a string representing the action.
  912. *
  913. * For maximum protection, ensure that the string representing the action is as specific as possible
  914. *
  915. * @param string $action the action
  916. * @param string $nonceParamName a custom nonce param name
  917. *
  918. * @return string the nonce input field
  919. */
  920. public function nonceFieldFunc($action, $nonceParamName = 'nonce')
  921. {
  922. $string = '<input type="hidden" name="' . $nonceParamName . '" value="' . Utils::getNonce($action) . '" />';
  923. return $string;
  924. }
  925. /**
  926. * Decodes string from JSON.
  927. *
  928. * @param string $str
  929. * @param bool $assoc
  930. * @param int $depth
  931. * @param int $options
  932. * @return array
  933. */
  934. public function jsonDecodeFilter($str, $assoc = false, $depth = 512, $options = 0)
  935. {
  936. return json_decode(html_entity_decode($str), $assoc, $depth, $options);
  937. }
  938. /**
  939. * Used to retrieve a cookie value
  940. *
  941. * @param string $key The cookie name to retrieve
  942. *
  943. * @return mixed
  944. */
  945. public function getCookie($key)
  946. {
  947. return filter_input(INPUT_COOKIE, $key, FILTER_SANITIZE_STRING);
  948. }
  949. /**
  950. * Twig wrapper for PHP's preg_replace method
  951. *
  952. * @param mixed $subject the content to perform the replacement on
  953. * @param mixed $pattern the regex pattern to use for matches
  954. * @param mixed $replace the replacement value either as a string or an array of replacements
  955. * @param int $limit the maximum possible replacements for each pattern in each subject
  956. *
  957. * @return string|string[]|null the resulting content
  958. */
  959. public function regexReplace($subject, $pattern, $replace, $limit = -1)
  960. {
  961. return preg_replace($pattern, $replace, $subject, $limit);
  962. }
  963. /**
  964. * Twig wrapper for PHP's preg_grep method
  965. *
  966. * @param array $array
  967. * @param string $regex
  968. * @param int $flags
  969. * @return array
  970. */
  971. public function regexFilter($array, $regex, $flags = 0)
  972. {
  973. return preg_grep($regex, $array, $flags);
  974. }
  975. /**
  976. * redirect browser from twig
  977. *
  978. * @param string $url the url to redirect to
  979. * @param int $statusCode statusCode, default 303
  980. */
  981. public function redirectFunc($url, $statusCode = 303)
  982. {
  983. header('Location: ' . $url, true, $statusCode);
  984. exit();
  985. }
  986. /**
  987. * Generates an array containing a range of elements, optionally stepped
  988. *
  989. * @param int $start Minimum number, default 0
  990. * @param int $end Maximum number, default `getrandmax()`
  991. * @param int $step Increment between elements in the sequence, default 1
  992. *
  993. * @return array
  994. */
  995. public function rangeFunc($start = 0, $end = 100, $step = 1)
  996. {
  997. return range($start, $end, $step);
  998. }
  999. /**
  1000. * Check if HTTP_X_REQUESTED_WITH has been set to xmlhttprequest,
  1001. * in which case we may unsafely assume ajax. Non critical use only.
  1002. *
  1003. * @return bool True if HTTP_X_REQUESTED_WITH exists and has been set to xmlhttprequest
  1004. */
  1005. public function isAjaxFunc()
  1006. {
  1007. return (
  1008. !empty($_SERVER['HTTP_X_REQUESTED_WITH'])
  1009. && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
  1010. }
  1011. /**
  1012. * Get the Exif data for a file
  1013. *
  1014. * @param string $image
  1015. * @param bool $raw
  1016. * @return mixed
  1017. */
  1018. public function exifFunc($image, $raw = false)
  1019. {
  1020. if (isset($this->grav['exif'])) {
  1021. /** @var UniformResourceLocator $locator */
  1022. $locator = $this->grav['locator'];
  1023. if ($locator->isStream($image)) {
  1024. $image = $locator->findResource($image);
  1025. }
  1026. $exif_reader = $this->grav['exif']->getReader();
  1027. if ($image && file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) {
  1028. $exif_data = $exif_reader->read($image);
  1029. if ($exif_data) {
  1030. if ($raw) {
  1031. return $exif_data->getRawData();
  1032. }
  1033. return $exif_data->getData();
  1034. }
  1035. }
  1036. }
  1037. return null;
  1038. }
  1039. /**
  1040. * Simple function to read a file based on a filepath and output it
  1041. *
  1042. * @param string $filepath
  1043. * @return bool|string
  1044. */
  1045. public function readFileFunc($filepath)
  1046. {
  1047. /** @var UniformResourceLocator $locator */
  1048. $locator = $this->grav['locator'];
  1049. if ($locator->isStream($filepath)) {
  1050. $filepath = $locator->findResource($filepath);
  1051. }
  1052. if ($filepath && file_exists($filepath)) {
  1053. return file_get_contents($filepath);
  1054. }
  1055. return false;
  1056. }
  1057. /**
  1058. * Process a folder as Media and return a media object
  1059. *
  1060. * @param string $media_dir
  1061. * @return Media|null
  1062. */
  1063. public function mediaDirFunc($media_dir)
  1064. {
  1065. /** @var UniformResourceLocator $locator */
  1066. $locator = $this->grav['locator'];
  1067. if ($locator->isStream($media_dir)) {
  1068. $media_dir = $locator->findResource($media_dir);
  1069. }
  1070. if ($media_dir && file_exists($media_dir)) {
  1071. return new Media($media_dir);
  1072. }
  1073. return null;
  1074. }
  1075. /**
  1076. * Dump a variable to the browser
  1077. *
  1078. * @param mixed $var
  1079. */
  1080. public function vardumpFunc($var)
  1081. {
  1082. var_dump($var);
  1083. }
  1084. /**
  1085. * Returns a nicer more readable filesize based on bytes
  1086. *
  1087. * @param int $bytes
  1088. * @return string
  1089. */
  1090. public function niceFilesizeFunc($bytes)
  1091. {
  1092. return Utils::prettySize($bytes);
  1093. }
  1094. /**
  1095. * Returns a nicer more readable number
  1096. *
  1097. * @param int|float|string $n
  1098. * @return string|bool
  1099. */
  1100. public function niceNumberFunc($n)
  1101. {
  1102. if (!\is_float($n) && !\is_int($n)) {
  1103. if (!\is_string($n) || $n === '') {
  1104. return false;
  1105. }
  1106. // Strip any thousand formatting and find the first number.
  1107. $list = array_filter(preg_split("/\D+/", str_replace(',', '', $n)));
  1108. $n = reset($list);
  1109. if (!\is_numeric($n)) {
  1110. return false;
  1111. }
  1112. $n = (float)$n;
  1113. }
  1114. // now filter it;
  1115. if ($n > 1000000000000) {
  1116. return round($n/1000000000000, 2).' t';
  1117. }
  1118. if ($n > 1000000000) {
  1119. return round($n/1000000000, 2).' b';
  1120. }
  1121. if ($n > 1000000) {
  1122. return round($n/1000000, 2).' m';
  1123. }
  1124. if ($n > 1000) {
  1125. return round($n/1000, 2).' k';
  1126. }
  1127. return number_format($n);
  1128. }
  1129. /**
  1130. * Get a theme variable
  1131. *
  1132. * @param string $var
  1133. * @param bool $default
  1134. * @return string
  1135. */
  1136. public function themeVarFunc($var, $default = null)
  1137. {
  1138. $header = $this->grav['page']->header();
  1139. $header_classes = $header->{$var} ?? null;
  1140. return $header_classes ?: $this->config->get('theme.' . $var, $default);
  1141. }
  1142. /**
  1143. * takes an array of classes, and if they are not set on body_classes
  1144. * look to see if they are set in theme config
  1145. *
  1146. * @param string|string[] $classes
  1147. * @return string
  1148. */
  1149. public function bodyClassFunc($classes)
  1150. {
  1151. $header = $this->grav['page']->header();
  1152. $body_classes = $header->body_classes ?? '';
  1153. foreach ((array)$classes as $class) {
  1154. if (!empty($body_classes) && Utils::contains($body_classes, $class)) {
  1155. continue;
  1156. }
  1157. $val = $this->config->get('theme.' . $class, false) ? $class : false;
  1158. $body_classes .= $val ? ' ' . $val : '';
  1159. }
  1160. return $body_classes;
  1161. }
  1162. /**
  1163. * Look for a page header variable in an array of pages working its way through until a value is found
  1164. *
  1165. * @param string $var
  1166. * @param string|string[]|null $pages
  1167. * @return mixed
  1168. */
  1169. public function pageHeaderVarFunc($var, $pages = null)
  1170. {
  1171. if ($pages === null) {
  1172. $pages = $this->grav['page'];
  1173. }
  1174. // Make sure pages are an array
  1175. if (!\is_array($pages)) {
  1176. $pages = [$pages];
  1177. }
  1178. // Loop over pages and look for header vars
  1179. foreach ($pages as $page) {
  1180. if (\is_string($page)) {
  1181. $page = $this->grav['pages']->find($page);
  1182. }
  1183. if ($page) {
  1184. $header = $page->header();
  1185. if (isset($header->{$var})) {
  1186. return $header->{$var};
  1187. }
  1188. }
  1189. }
  1190. return null;
  1191. }
  1192. /**
  1193. * Dump/Encode data into YAML format
  1194. *
  1195. * @param array $data
  1196. * @param int $inline integer number of levels of inline syntax
  1197. * @return string
  1198. */
  1199. public function yamlEncodeFilter($data, $inline = 10)
  1200. {
  1201. return Yaml::dump($data, $inline);
  1202. }
  1203. /**
  1204. * Decode/Parse data from YAML format
  1205. *
  1206. * @param string $data
  1207. * @return array
  1208. */
  1209. public function yamlDecodeFilter($data)
  1210. {
  1211. return Yaml::parse($data);
  1212. }
  1213. /**
  1214. * Function/Filter to return the type of variable
  1215. *
  1216. * @param mixed $var
  1217. * @return string
  1218. */
  1219. public function getTypeFunc($var)
  1220. {
  1221. return gettype($var);
  1222. }
  1223. /**
  1224. * Function/Filter to test type of variable
  1225. *
  1226. * @param mixed $var
  1227. * @param string|null $typeTest
  1228. * @param string|null $className
  1229. * @return bool
  1230. */
  1231. public function ofTypeFunc($var, $typeTest=null, $className=null)
  1232. {
  1233. switch ($typeTest)
  1234. {
  1235. default:
  1236. return false;
  1237. break;
  1238. case 'array':
  1239. return is_array($var);
  1240. break;
  1241. case 'bool':
  1242. return is_bool($var);
  1243. break;
  1244. case 'class':
  1245. return is_object($var) === true && get_class($var) === $className;
  1246. break;
  1247. case 'float':
  1248. return is_float($var);
  1249. break;
  1250. case 'int':
  1251. return is_int($var);
  1252. break;
  1253. case 'numeric':
  1254. return is_numeric($var);
  1255. break;
  1256. case 'object':
  1257. return is_object($var);
  1258. break;
  1259. case 'scalar':
  1260. return is_scalar($var);
  1261. break;
  1262. case 'string':
  1263. return is_string($var);
  1264. break;
  1265. }
  1266. }
  1267. }