"Fossies" - the Fresh Open Source Software Archive

Member "grav/system/src/Grav/Common/Twig/TwigExtension.php" (1 Sep 2020, 41125 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "TwigExtension.php" see the Fossies "Dox" file reference documentation.

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