TwigExtension.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. <?php
  2. namespace Grav\Common\Twig;
  3. use Grav\Common\Grav;
  4. use Grav\Common\Utils;
  5. use Grav\Common\Markdown\Parsedown;
  6. use Grav\Common\Markdown\ParsedownExtra;
  7. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  8. /**
  9. * The Twig extension adds some filters and functions that are useful for Grav
  10. *
  11. * @author RocketTheme
  12. * @license MIT
  13. */
  14. class TwigExtension extends \Twig_Extension
  15. {
  16. protected $grav;
  17. protected $debugger;
  18. protected $config;
  19. public function __construct()
  20. {
  21. $this->grav = Grav::instance();
  22. $this->debugger = isset($this->grav['debugger']) ? $this->grav['debugger'] : null;
  23. $this->config = $this->grav['config'];
  24. }
  25. /**
  26. * Returns extension name.
  27. *
  28. * @return string
  29. */
  30. public function getName()
  31. {
  32. return 'GravTwigExtension';
  33. }
  34. /**
  35. * Register some standard globals
  36. *
  37. * @return array
  38. */
  39. public function getGlobals()
  40. {
  41. return array(
  42. 'grav' => $this->grav,
  43. );
  44. }
  45. /**
  46. * Return a list of all filters.
  47. *
  48. * @return array
  49. */
  50. public function getFilters()
  51. {
  52. return [
  53. new \Twig_SimpleFilter('*ize', [$this,'inflectorFilter']),
  54. new \Twig_SimpleFilter('absolute_url', [$this, 'absoluteUrlFilter']),
  55. new \Twig_SimpleFilter('contains', [$this, 'containsFilter']),
  56. new \Twig_SimpleFilter('defined', [$this, 'definedDefaultFilter']),
  57. new \Twig_SimpleFilter('ends_with', [$this, 'endsWithFilter']),
  58. new \Twig_SimpleFilter('fieldName', [$this,'fieldNameFilter']),
  59. new \Twig_SimpleFilter('ksort', [$this,'ksortFilter']),
  60. new \Twig_SimpleFilter('ltrim', [$this, 'ltrimFilter']),
  61. new \Twig_SimpleFilter('markdown', [$this, 'markdownFilter']),
  62. new \Twig_SimpleFilter('md5', [$this,'md5Filter']),
  63. new \Twig_SimpleFilter('nicetime', [$this, 'nicetimeFilter']),
  64. new \Twig_SimpleFilter('randomize', [$this,'randomizeFilter']),
  65. new \Twig_SimpleFilter('modulus', [$this,'modulusFilter']),
  66. new \Twig_SimpleFilter('rtrim', [$this, 'rtrimFilter']),
  67. new \Twig_SimpleFilter('safe_email', [$this,'safeEmailFilter']),
  68. new \Twig_SimpleFilter('safe_truncate', ['\Grav\Common\Utils','safeTruncate']),
  69. new \Twig_SimpleFilter('safe_truncate_html', ['\Grav\Common\Utils','safeTruncateHTML']),
  70. new \Twig_SimpleFilter('sort_by_key', [$this,'sortByKeyFilter']),
  71. new \Twig_SimpleFilter('starts_with', [$this, 'startsWithFilter']),
  72. new \Twig_SimpleFilter('t', [$this, 'translate']),
  73. new \Twig_SimpleFilter('ta', [$this, 'translateArray']),
  74. new \Twig_SimpleFilter('truncate', ['\Grav\Common\Utils','truncate']),
  75. new \Twig_SimpleFilter('truncate_html', ['\Grav\Common\Utils','truncateHTML']),
  76. ];
  77. }
  78. /**
  79. * Return a list of all functions.
  80. *
  81. * @return array
  82. */
  83. public function getFunctions()
  84. {
  85. return [
  86. new \Twig_SimpleFunction('array', [$this, 'arrayFunc']),
  87. new \Twig_simpleFunction('authorize', [$this, 'authorize']),
  88. new \Twig_SimpleFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
  89. new \Twig_SimpleFunction('dump', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
  90. new \Twig_SimpleFunction('gist', [$this, 'gistFunc']),
  91. new \Twig_simpleFunction('random_string', [$this, 'randomStringFunc']),
  92. new \Twig_SimpleFunction('repeat', [$this, 'repeatFunc']),
  93. new \Twig_SimpleFunction('string', [$this, 'stringFunc']),
  94. new \Twig_simpleFunction('t', [$this, 'translate']),
  95. new \Twig_simpleFunction('ta', [$this, 'translateArray']),
  96. new \Twig_SimpleFunction('url', [$this, 'urlFunc']),
  97. new \Twig_SimpleFunction('evaluate', [$this, 'evaluateFunc']),
  98. ];
  99. }
  100. /**
  101. * Filters field name by changing dot notation into array notation.
  102. *
  103. * @param string $str
  104. * @return string
  105. */
  106. public function fieldNameFilter($str)
  107. {
  108. $path = explode('.', $str);
  109. return array_shift($path) . ($path ? '[' . implode('][', $path) . ']' : '');
  110. }
  111. /**
  112. * Protects email address.
  113. *
  114. * @param string $str
  115. * @return string
  116. */
  117. public function safeEmailFilter($str)
  118. {
  119. $email = '';
  120. $str_len = strlen($str);
  121. for ($i = 0; $i < $str_len; $i++) {
  122. $email .= "&#" . ord($str[$i]). ";";
  123. }
  124. return $email;
  125. }
  126. /**
  127. * Returns array in a random order.
  128. *
  129. * @param array $original
  130. * @param int $offset Can be used to return only slice of the array.
  131. * @return array
  132. */
  133. public function randomizeFilter($original, $offset = 0)
  134. {
  135. if (!is_array($original)) {
  136. return $original;
  137. }
  138. if ($original instanceof \Traversable) {
  139. $original = iterator_to_array($original, false);
  140. }
  141. $sorted = [];
  142. $random = array_slice($original, $offset);
  143. shuffle($random);
  144. $sizeOf = sizeof($original);
  145. for ($x=0; $x < $sizeOf; $x++) {
  146. if ($x < $offset) {
  147. $sorted[] = $original[$x];
  148. } else {
  149. $sorted[] = array_shift($random);
  150. }
  151. }
  152. return $sorted;
  153. }
  154. /**
  155. * Returns the modulus of an integer
  156. *
  157. * @param int $number
  158. * @param int $divider
  159. * @param array $items array of items to select from to return
  160. * @return int
  161. */
  162. public function modulusFilter($number, $divider, $items = null)
  163. {
  164. if (is_string($number)) {
  165. $number = strlen($number);
  166. }
  167. $remainder = $number % $divider;
  168. if (is_array($items)) {
  169. if (isset($items[$remainder])) {
  170. return $items[$remainder];
  171. } else {
  172. return $items[0];
  173. }
  174. }
  175. return $remainder;
  176. }
  177. /**
  178. * Inflector supports following notations:
  179. *
  180. * {{ 'person'|pluralize }} => people
  181. * {{ 'shoes'|singularize }} => shoe
  182. * {{ 'welcome page'|titleize }} => "Welcome Page"
  183. * {{ 'send_email'|camelize }} => SendEmail
  184. * {{ 'CamelCased'|underscorize }} => camel_cased
  185. * {{ 'Something Text'|hyphenize }} => something-text
  186. * {{ 'something_text_to_read'|humanize }} => "Something text to read"
  187. * {{ '181'|monthize }} => 6
  188. * {{ '10'|ordinalize }} => 10th
  189. *
  190. * @param string $action
  191. * @param string $data
  192. * @param int $count
  193. * @return mixed
  194. */
  195. public function inflectorFilter($action, $data, $count = null)
  196. {
  197. $action = $action.'ize';
  198. $inflector = $this->grav['inflector'];
  199. if (in_array(
  200. $action,
  201. ['titleize','camelize','underscorize','hyphenize', 'humanize','ordinalize','monthize']
  202. )) {
  203. return $inflector->$action($data);
  204. } elseif (in_array($action, ['pluralize','singularize'])) {
  205. if ($count) {
  206. return $inflector->$action($data, $count);
  207. } else {
  208. return $inflector->$action($data);
  209. }
  210. } else {
  211. return $data;
  212. }
  213. }
  214. /**
  215. * Return MD5 hash from the input.
  216. *
  217. * @param string $str
  218. * @return string
  219. */
  220. public function md5Filter($str)
  221. {
  222. return md5($str);
  223. }
  224. /**
  225. * Sorts a collection by key
  226. *
  227. * @param array $input
  228. * @param string $filter
  229. * @param array|int $direction
  230. *
  231. * @return string
  232. */
  233. public function sortByKeyFilter(array $input, $filter, $direction = SORT_ASC)
  234. {
  235. $output = [];
  236. if (!$input) {
  237. return $output;
  238. }
  239. foreach ($input as $key => $row) {
  240. $output[$key] = $row[$filter];
  241. }
  242. array_multisort($output, $direction, $input);
  243. return $input;
  244. }
  245. /**
  246. * Return ksorted collection.
  247. *
  248. * @param array $array
  249. * @return array
  250. */
  251. public function ksortFilter(array $array)
  252. {
  253. ksort($array);
  254. return $array;
  255. }
  256. /**
  257. * determine if a string contains another
  258. *
  259. * @param String $haystack
  260. * @param String $needle
  261. *
  262. * @return boolean
  263. */
  264. public function containsFilter($haystack, $needle)
  265. {
  266. return (strpos($haystack, $needle) !== false);
  267. }
  268. /**
  269. * displays a facebook style 'time ago' formatted date/time
  270. *
  271. * @param $date
  272. * @param $long_strings
  273. * @param String
  274. *
  275. * @return boolean
  276. */
  277. public function nicetimeFilter($date, $long_strings = true)
  278. {
  279. if (empty($date)) {
  280. return $this->grav['language']->translate('NICETIME.NO_DATE_PROVIDED', null, true);
  281. }
  282. if ($long_strings) {
  283. $periods = array("NICETIME.SECOND", "NICETIME.MINUTE", "NICETIME.HOUR", "NICETIME.DAY", "NICETIME.WEEK", "NICETIME.MONTH", "NICETIME.YEAR", "NICETIME.DECADE");
  284. } else {
  285. $periods = array("NICETIME.SEC", "NICETIME.MIN", "NICETIME.HR", "NICETIME.DAY", "NICETIME.WK", "NICETIME.MO", "NICETIME.YR", "NICETIME.DEC");
  286. }
  287. $lengths = array("60","60","24","7","4.35","12","10");
  288. $now = time();
  289. // check if unix timestamp
  290. if ((string)(int)$date == $date) {
  291. $unix_date = $date;
  292. } else {
  293. $unix_date = strtotime($date);
  294. }
  295. // check validity of date
  296. if (empty($unix_date)) {
  297. return $this->grav['language']->translate('NICETIME.BAD_DATE', null, true);
  298. }
  299. // is it future date or past date
  300. if ($now > $unix_date) {
  301. $difference = $now - $unix_date;
  302. $tense = $this->grav['language']->translate('NICETIME.AGO', null, true);
  303. } else {
  304. $difference = $unix_date - $now;
  305. $tense = $this->grav['language']->translate('NICETIME.FROM_NOW', null, true);
  306. }
  307. for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths)-1; $j++) {
  308. $difference /= $lengths[$j];
  309. }
  310. $difference = round($difference);
  311. if ($difference != 1) {
  312. $periods[$j] .= '_PLURAL';
  313. }
  314. $periods[$j] = $this->grav['language']->translate($periods[$j], null, true);
  315. return "$difference $periods[$j] {$tense}";
  316. }
  317. public function absoluteUrlFilter($string)
  318. {
  319. $url = $this->grav['uri']->base();
  320. $string = preg_replace('/((?:href|src) *= *[\'"](?!(http|ftp)))/i', "$1$url", $string);
  321. return $string;
  322. }
  323. public function markdownFilter($string)
  324. {
  325. $page = $this->grav['page'];
  326. $defaults = $this->config->get('system.pages.markdown');
  327. // Initialize the preferred variant of Parsedown
  328. if ($defaults['extra']) {
  329. $parsedown = new ParsedownExtra($page, $defaults);
  330. } else {
  331. $parsedown = new Parsedown($page, $defaults);
  332. }
  333. $string = $parsedown->text($string);
  334. return $string;
  335. }
  336. public function startsWithFilter($haystack, $needle)
  337. {
  338. return Utils::startsWith($haystack, $needle);
  339. }
  340. public function endsWithFilter($haystack, $needle)
  341. {
  342. return Utils::endsWith($haystack, $needle);
  343. }
  344. public function definedDefaultFilter($value, $default = null)
  345. {
  346. if (isset($value)) {
  347. return $value;
  348. } else {
  349. return $default;
  350. }
  351. }
  352. public function rtrimFilter($value, $chars = null)
  353. {
  354. return rtrim($value, $chars);
  355. }
  356. public function ltrimFilter($value, $chars = null)
  357. {
  358. return ltrim($value, $chars);
  359. }
  360. public function translate()
  361. {
  362. return $this->grav['language']->translate(func_get_args());
  363. }
  364. public function translateArray($key, $index, $lang = null)
  365. {
  366. return $this->grav['language']->translateArray($key, $index, $lang);
  367. }
  368. /**
  369. * Repeat given string x times.
  370. *
  371. * @param string $input
  372. * @param int $multiplier
  373. * @return string
  374. */
  375. public function repeatFunc($input, $multiplier)
  376. {
  377. return str_repeat($input, $multiplier);
  378. }
  379. /**
  380. * Return URL to the resource.
  381. *
  382. * @example {{ url('theme://images/logo.png')|default('http://www.placehold.it/150x100/f4f4f4') }}
  383. *
  384. * @param string $input Resource to be located.
  385. * @param bool $domain True to include domain name.
  386. * @return string|null Returns url to the resource or null if resource was not found.
  387. */
  388. public function urlFunc($input, $domain = false)
  389. {
  390. if (!trim((string) $input)) {
  391. return false;
  392. }
  393. if (strpos((string) $input, '://')) {
  394. /** @var UniformResourceLocator $locator */
  395. $locator = $this->grav['locator'];
  396. // Get relative path to the resource (or false if not found).
  397. $resource = $locator->findResource((string) $input, false);
  398. } else {
  399. $resource = (string) $input;
  400. }
  401. /** @var Uri $uri */
  402. $uri = $this->grav['uri'];
  403. return $resource ? rtrim($uri->rootUrl($domain), '/') . '/' . $resource : null;
  404. }
  405. /**
  406. * Evaluate a string
  407. *
  408. * @example {{ evaluate('grav.language.getLanguage') }}
  409. *
  410. * @param string $input String to be evaluated
  411. * @return string Returns the evaluated string
  412. */
  413. public function evaluateFunc($input)
  414. {
  415. return $this->grav['twig']->processString("{{ $input }}");
  416. }
  417. /**
  418. * Based on Twig_Extension_Debug / twig_var_dump
  419. * (c) 2011 Fabien Potencier
  420. *
  421. * @param \Twig_Environment $env
  422. * @param $context
  423. */
  424. public function dump(\Twig_Environment $env, $context)
  425. {
  426. if (!$env->isDebug() || !$this->debugger) {
  427. return;
  428. }
  429. $count = func_num_args();
  430. if (2 === $count) {
  431. $data = [];
  432. foreach ($context as $key => $value) {
  433. if (is_object($value)) {
  434. if (method_exists($value, 'toArray')) {
  435. $data[$key] = $value->toArray();
  436. } else {
  437. $data[$key] = "Object (" . get_class($value) . ")";
  438. }
  439. } else {
  440. $data[$key] = $value;
  441. }
  442. }
  443. $this->debugger->addMessage($data, 'debug');
  444. } else {
  445. for ($i = 2; $i < $count; $i++) {
  446. $this->debugger->addMessage(func_get_arg($i), 'debug');
  447. }
  448. }
  449. }
  450. /**
  451. * Output a Gist
  452. *
  453. * @param string $id
  454. * @return string
  455. */
  456. public function gistFunc($id)
  457. {
  458. return '<script src="https://gist.github.com/'.$id.'.js"></script>';
  459. }
  460. /**
  461. * Generate a random string
  462. *
  463. * @param int $count
  464. *
  465. * @return string
  466. */
  467. public function randomStringFunc($count = 5)
  468. {
  469. return Utils::generateRandomString($count);
  470. }
  471. /**
  472. * Cast a value to array
  473. *
  474. * @param $value
  475. *
  476. * @return array
  477. */
  478. public function arrayFunc($value)
  479. {
  480. return (array) $value;
  481. }
  482. /**
  483. * Returns a string from a value. If the value is array, return it json encoded
  484. *
  485. * @param $value
  486. *
  487. * @return string
  488. */
  489. public function stringFunc($value)
  490. {
  491. if (is_array($value)) { //format the array as a string
  492. return json_encode($value);
  493. } else {
  494. return $value;
  495. }
  496. }
  497. /**
  498. * Translate a string
  499. *
  500. * @return string
  501. */
  502. public function translateFunc()
  503. {
  504. return $this->grav['language']->translate(func_get_args());
  505. }
  506. /**
  507. * Authorize an action. Returns true if the user is logged in and has the right to execute $action.
  508. *
  509. * @param string $action
  510. *
  511. * @return bool
  512. */
  513. public function authorize($action)
  514. {
  515. if (!$this->grav['user']->authenticated) {
  516. return false;
  517. }
  518. $action = (array)$action;
  519. foreach ($action as $a) {
  520. if ($this->grav['user']->authorize($a)) {
  521. return true;
  522. }
  523. }
  524. return false;
  525. }
  526. }