TwigExtension.php 43 KB

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