"Fossies" - the Fresh Open Source Software Archive

Member "grav/system/src/Grav/Common/Utils.php" (1 Sep 2020, 48014 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 "Utils.php" see the Fossies "Dox" file reference documentation.

    1 <?php
    2 
    3 /**
    4  * @package    Grav\Common
    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;
   11 
   12 use Grav\Common\Helpers\Truncator;
   13 use Grav\Common\Page\Interfaces\PageInterface;
   14 use Grav\Common\Markdown\Parsedown;
   15 use Grav\Common\Markdown\ParsedownExtra;
   16 use Grav\Common\Page\Markdown\Excerpts;
   17 use RocketTheme\Toolbox\Event\Event;
   18 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
   19 
   20 abstract class Utils
   21 {
   22     protected static $nonces = [];
   23 
   24     protected const ROOTURL_REGEX = '{^((?:http[s]?:\/\/[^\/]+)|(?:\/\/[^\/]+))(.*)}';
   25 
   26     // ^((?:http[s]?:)?[\/]?(?:\/))
   27 
   28     /**
   29      * Simple helper method to make getting a Grav URL easier
   30      *
   31      * @param string|object $input
   32      * @param bool $domain
   33      * @param bool $fail_gracefully
   34      * @return bool|null|string
   35      */
   36     public static function url($input, $domain = false, $fail_gracefully = false)
   37     {
   38         if ((!is_string($input) && !method_exists($input, '__toString')) || !trim($input)) {
   39             if ($fail_gracefully) {
   40                 $input = '/';
   41             } else {
   42                 return false;
   43             }
   44         }
   45 
   46         $input = (string)$input;
   47 
   48         if (Uri::isExternal($input)) {
   49             return $input;
   50         }
   51 
   52         $grav = Grav::instance();
   53 
   54         /** @var Uri $uri */
   55         $uri = $grav['uri'];
   56 
   57         if (static::contains((string)$input, '://')) {
   58             /** @var UniformResourceLocator $locator */
   59             $locator = $grav['locator'];
   60 
   61             $parts = Uri::parseUrl($input);
   62 
   63             if (is_array($parts)) {
   64                 // Make sure we always have scheme, host, port and path.
   65                 $scheme = $parts['scheme'] ?? '';
   66                 $host = $parts['host'] ?? '';
   67                 $port = $parts['port'] ?? '';
   68                 $path = $parts['path'] ?? '';
   69 
   70                 if ($scheme && !$port) {
   71                     // If URL has a scheme, we need to check if it's one of Grav streams.
   72                     if (!$locator->schemeExists($scheme)) {
   73                         // If scheme does not exists as a stream, assume it's external.
   74                         return str_replace(' ', '%20', $input);
   75                     }
   76 
   77                     // Attempt to find the resource (because of parse_url() we need to put host back to path).
   78                     $resource = $locator->findResource("{$scheme}://{$host}{$path}", false);
   79 
   80                     if ($resource === false) {
   81                         if (!$fail_gracefully) {
   82                             return false;
   83                         }
   84 
   85                         // Return location where the file would be if it was saved.
   86                         $resource = $locator->findResource("{$scheme}://{$host}{$path}", false, true);
   87                     }
   88 
   89                 } elseif ($host || $port) {
   90                     // If URL doesn't have scheme but has host or port, it is external.
   91                     return str_replace(' ', '%20', $input);
   92                 }
   93 
   94                 if (!empty($resource)) {
   95                     // Add query string back.
   96                     if (isset($parts['query'])) {
   97                         $resource .= '?' . $parts['query'];
   98                     }
   99 
  100                     // Add fragment back.
  101                     if (isset($parts['fragment'])) {
  102                         $resource .= '#' . $parts['fragment'];
  103                     }
  104                 }
  105 
  106             } else {
  107                 // Not a valid URL (can still be a stream).
  108                 $resource = $locator->findResource($input, false);
  109             }
  110 
  111         } else {
  112             $root = $uri->rootUrl();
  113 
  114             if (static::startsWith($input, $root)) {
  115                 $input = static::replaceFirstOccurrence($root, '', $input);
  116             }
  117 
  118             $input = ltrim($input, '/');
  119 
  120             $resource = $input;
  121         }
  122 
  123         if (!$fail_gracefully && $resource === false) {
  124             return false;
  125         }
  126 
  127         $domain = $domain ?: $grav['config']->get('system.absolute_urls', false);
  128 
  129         return rtrim($uri->rootUrl($domain), '/') . '/' . ($resource ?? '');
  130     }
  131 
  132     /**
  133      * Check if the $haystack string starts with the substring $needle
  134      *
  135      * @param  string $haystack
  136      * @param  string|string[] $needle
  137      * @param bool $case_sensitive
  138      *
  139      * @return bool
  140      */
  141     public static function startsWith($haystack, $needle, $case_sensitive = true)
  142     {
  143         $status = false;
  144 
  145         $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos';
  146 
  147         foreach ((array)$needle as $each_needle) {
  148             $status = $each_needle === '' || $compare_func($haystack, $each_needle) === 0;
  149             if ($status) {
  150                 break;
  151             }
  152         }
  153 
  154         return $status;
  155     }
  156 
  157     /**
  158      * Check if the $haystack string ends with the substring $needle
  159      *
  160      * @param  string $haystack
  161      * @param  string|string[] $needle
  162      * @param bool $case_sensitive
  163      *
  164      * @return bool
  165      */
  166     public static function endsWith($haystack, $needle, $case_sensitive = true)
  167     {
  168         $status = false;
  169 
  170         $compare_func = $case_sensitive ? 'mb_strrpos' : 'mb_strripos';
  171 
  172         foreach ((array)$needle as $each_needle) {
  173             $expectedPosition = mb_strlen($haystack) - mb_strlen($each_needle);
  174             $status = $each_needle === '' || $compare_func($haystack, $each_needle, 0) === $expectedPosition;
  175             if ($status) {
  176                 break;
  177             }
  178         }
  179 
  180         return $status;
  181     }
  182 
  183     /**
  184      * Check if the $haystack string contains the substring $needle
  185      *
  186      * @param  string $haystack
  187      * @param  string|string[] $needle
  188      * @param  bool $case_sensitive
  189      *
  190      * @return bool
  191      */
  192     public static function contains($haystack, $needle, $case_sensitive = true)
  193     {
  194         $status = false;
  195 
  196         $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos';
  197 
  198         foreach ((array)$needle as $each_needle) {
  199             $status = $each_needle === '' || $compare_func($haystack, $each_needle) !== false;
  200             if ($status) {
  201                 break;
  202             }
  203         }
  204 
  205         return $status;
  206     }
  207 
  208     /**
  209      * Function that can match wildcards
  210      *
  211      * match_wildcard('foo*', $test),      // TRUE
  212      * match_wildcard('bar*', $test),      // FALSE
  213      * match_wildcard('*bar*', $test),     // TRUE
  214      * match_wildcard('**blob**', $test),  // TRUE
  215      * match_wildcard('*a?d*', $test),     // TRUE
  216      * match_wildcard('*etc**', $test)     // TRUE
  217      *
  218      * @param string $wildcard_pattern
  219      * @param string $haystack
  220      * @return false|int
  221      */
  222     public static function matchWildcard($wildcard_pattern, $haystack) {
  223         $regex = str_replace(
  224             array("\*", "\?"), // wildcard chars
  225             array('.*','.'),   // regexp chars
  226             preg_quote($wildcard_pattern, '/')
  227         );
  228 
  229         return preg_match('/^'.$regex.'$/is', $haystack);
  230     }
  231 
  232     /**
  233      * Returns the substring of a string up to a specified needle.  if not found, return the whole haystack
  234      *
  235      * @param string $haystack
  236      * @param string $needle
  237      * @param bool $case_sensitive
  238      *
  239      * @return string
  240      */
  241     public static function substrToString($haystack, $needle, $case_sensitive = true)
  242     {
  243         $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos';
  244 
  245         if (static::contains($haystack, $needle, $case_sensitive)) {
  246             return mb_substr($haystack, 0, $compare_func($haystack, $needle, $case_sensitive));
  247         }
  248 
  249         return $haystack;
  250     }
  251 
  252     /**
  253      * Utility method to replace only the first occurrence in a string
  254      *
  255      * @param string $search
  256      * @param string $replace
  257      * @param string $subject
  258      *
  259      * @return string
  260      */
  261     public static function replaceFirstOccurrence($search, $replace, $subject)
  262     {
  263         if (!$search) {
  264             return $subject;
  265         }
  266 
  267         $pos = mb_strpos($subject, $search);
  268         if ($pos !== false) {
  269             $subject = static::mb_substr_replace($subject, $replace, $pos, mb_strlen($search));
  270         }
  271 
  272 
  273         return $subject;
  274     }
  275 
  276     /**
  277      * Utility method to replace only the last occurrence in a string
  278      *
  279      * @param string $search
  280      * @param string $replace
  281      * @param string $subject
  282      * @return string
  283      */
  284     public static function replaceLastOccurrence($search, $replace, $subject)
  285     {
  286         $pos = strrpos($subject, $search);
  287 
  288         if($pos !== false)
  289         {
  290             $subject = static::mb_substr_replace($subject, $replace, $pos, mb_strlen($search));
  291         }
  292 
  293         return $subject;
  294     }
  295 
  296     /**
  297      * Multibyte compatible substr_replace
  298      *
  299      * @param string $original
  300      * @param string $replacement
  301      * @param int $position
  302      * @param int $length
  303      * @return string
  304      */
  305     public static function mb_substr_replace($original, $replacement, $position, $length)
  306     {
  307         $startString = mb_substr($original, 0, $position, "UTF-8");
  308         $endString = mb_substr($original, $position + $length, mb_strlen($original), "UTF-8");
  309 
  310         return $startString . $replacement . $endString;
  311     }
  312 
  313     /**
  314      * Merge two objects into one.
  315      *
  316      * @param  object $obj1
  317      * @param  object $obj2
  318      *
  319      * @return object
  320      */
  321     public static function mergeObjects($obj1, $obj2)
  322     {
  323         return (object)array_merge((array)$obj1, (array)$obj2);
  324     }
  325 
  326     /**
  327      * Lowercase an entire array. Useful when combined with `in_array()`
  328      *
  329      * @param array $a
  330      * @return array|false
  331      */
  332     public static function arrayLower(Array $a)
  333     {
  334         return array_map('mb_strtolower', $a);
  335     }
  336 
  337     /**
  338      * Simple function to remove item/s in an array by value
  339      *
  340      * @param $search array
  341      * @param $value string|array
  342      * @return array
  343      */
  344     public static function arrayRemoveValue(Array $search, $value)
  345     {
  346         foreach ((array) $value as $val) {
  347             $key = array_search($val, $search);
  348             if ($key !== false) {
  349                 unset($search[$key]);
  350             }
  351         }
  352         return $search;
  353     }
  354 
  355     /**
  356      * Recursive Merge with uniqueness
  357      *
  358      * @param array $array1
  359      * @param array $array2
  360      * @return array
  361      */
  362     public static function arrayMergeRecursiveUnique($array1, $array2)
  363     {
  364         if (empty($array1)) {
  365             // Optimize the base case
  366             return $array2;
  367         }
  368 
  369         foreach ($array2 as $key => $value) {
  370             if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
  371                 $value = static::arrayMergeRecursiveUnique($array1[$key], $value);
  372             }
  373             $array1[$key] = $value;
  374         }
  375 
  376         return $array1;
  377     }
  378 
  379     /**
  380      * Returns an array with the differences between $array1 and $array2
  381      *
  382      * @param array $array1
  383      * @param array $array2
  384      * @return array
  385      */
  386     public static function arrayDiffMultidimensional($array1, $array2)
  387     {
  388         $result = array();
  389         foreach ($array1 as $key => $value) {
  390             if (!is_array($array2) || !array_key_exists($key, $array2)) {
  391                 $result[$key] = $value;
  392                 continue;
  393             }
  394             if (is_array($value)) {
  395                 $recursiveArrayDiff = static::ArrayDiffMultidimensional($value, $array2[$key]);
  396                 if (count($recursiveArrayDiff)) {
  397                     $result[$key] = $recursiveArrayDiff;
  398                 }
  399                 continue;
  400             }
  401             if ($value != $array2[$key]) {
  402                 $result[$key] = $value;
  403             }
  404         }
  405 
  406         return $result;
  407     }
  408 
  409     /**
  410      * Array combine but supports different array lengths
  411      *
  412      * @param  array $arr1
  413      * @param  array $arr2
  414      * @return array|false
  415      */
  416     public static function arrayCombine($arr1, $arr2)
  417     {
  418         $count = min(count($arr1), count($arr2));
  419 
  420         return array_combine(array_slice($arr1, 0, $count), array_slice($arr2, 0, $count));
  421     }
  422 
  423     /**
  424      * Array is associative or not
  425      *
  426      * @param  array $arr
  427      * @return bool
  428      */
  429     public static function arrayIsAssociative($arr)
  430     {
  431         if ([] === $arr) {
  432             return false;
  433         }
  434 
  435         return array_keys($arr) !== range(0, count($arr) - 1);
  436     }
  437 
  438     /**
  439      * Return the Grav date formats allowed
  440      *
  441      * @return array
  442      */
  443     public static function dateFormats()
  444     {
  445         $now = new \DateTime();
  446 
  447         $date_formats = [
  448             'd-m-Y H:i' => 'd-m-Y H:i (e.g. '.$now->format('d-m-Y H:i').')',
  449             'Y-m-d H:i' => 'Y-m-d H:i (e.g. '.$now->format('Y-m-d H:i').')',
  450             'm/d/Y h:i a' => 'm/d/Y h:i a (e.g. '.$now->format('m/d/Y h:i a').')',
  451             'H:i d-m-Y' => 'H:i d-m-Y (e.g. '.$now->format('H:i d-m-Y').')',
  452             'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. '.$now->format('h:i a m/d/Y').')',
  453             ];
  454         $default_format = Grav::instance()['config']->get('system.pages.dateformat.default');
  455         if ($default_format) {
  456             $date_formats = array_merge([$default_format => $default_format.' (e.g. '.$now->format($default_format).')'], $date_formats);
  457         }
  458 
  459         return $date_formats;
  460     }
  461 
  462     /**
  463      * Get current date/time
  464      *
  465      * @param string|null $default_format
  466      * @return string
  467      * @throws \Exception
  468      */
  469     public static function dateNow($default_format = null)
  470     {
  471         $now = new \DateTime();
  472 
  473         if (is_null($default_format)) {
  474             $default_format = Grav::instance()['config']->get('system.pages.dateformat.default');
  475         }
  476 
  477         return $now->format($default_format);
  478     }
  479 
  480     /**
  481      * Truncate text by number of characters but can cut off words.
  482      *
  483      * @param  string $string
  484      * @param  int    $limit       Max number of characters.
  485      * @param  bool   $up_to_break truncate up to breakpoint after char count
  486      * @param  string $break       Break point.
  487      * @param  string $pad         Appended padding to the end of the string.
  488      *
  489      * @return string
  490      */
  491     public static function truncate($string, $limit = 150, $up_to_break = false, $break = ' ', $pad = '&hellip;')
  492     {
  493         // return with no change if string is shorter than $limit
  494         if (mb_strlen($string) <= $limit) {
  495             return $string;
  496         }
  497 
  498         // is $break present between $limit and the end of the string?
  499         if ($up_to_break && false !== ($breakpoint = mb_strpos($string, $break, $limit))) {
  500             if ($breakpoint < mb_strlen($string) - 1) {
  501                 $string = mb_substr($string, 0, $breakpoint) . $pad;
  502             }
  503         } else {
  504             $string = mb_substr($string, 0, $limit) . $pad;
  505         }
  506 
  507         return $string;
  508     }
  509 
  510     /**
  511      * Truncate text by number of characters in a "word-safe" manor.
  512      *
  513      * @param string $string
  514      * @param int    $limit
  515      *
  516      * @return string
  517      */
  518     public static function safeTruncate($string, $limit = 150)
  519     {
  520         return static::truncate($string, $limit, true);
  521     }
  522 
  523 
  524     /**
  525      * Truncate HTML by number of characters. not "word-safe"!
  526      *
  527      * @param  string $text
  528      * @param  int $length in characters
  529      * @param  string $ellipsis
  530      *
  531      * @return string
  532      */
  533     public static function truncateHtml($text, $length = 100, $ellipsis = '...')
  534     {
  535         return Truncator::truncateLetters($text, $length, $ellipsis);
  536     }
  537 
  538     /**
  539      * Truncate HTML by number of characters in a "word-safe" manor.
  540      *
  541      * @param  string $text
  542      * @param  int    $length in words
  543      * @param  string $ellipsis
  544      *
  545      * @return string
  546      */
  547     public static function safeTruncateHtml($text, $length = 25, $ellipsis = '...')
  548     {
  549         return Truncator::truncateWords($text, $length, $ellipsis);
  550     }
  551 
  552     /**
  553      * Generate a random string of a given length
  554      *
  555      * @param int $length
  556      *
  557      * @return string
  558      */
  559     public static function generateRandomString($length = 5)
  560     {
  561         return substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length);
  562     }
  563 
  564     /**
  565      * Provides the ability to download a file to the browser
  566      *
  567      * @param string $file the full path to the file to be downloaded
  568      * @param bool $force_download as opposed to letting browser choose if to download or render
  569      * @param int $sec      Throttling, try 0.1 for some speed throttling of downloads
  570      * @param int $bytes    Size of chunks to send in bytes. Default is 1024
  571      * @throws \Exception
  572      */
  573     public static function download($file, $force_download = true, $sec = 0, $bytes = 1024)
  574     {
  575         if (file_exists($file)) {
  576             // fire download event
  577             Grav::instance()->fireEvent('onBeforeDownload', new Event(['file' => $file]));
  578 
  579             $file_parts = pathinfo($file);
  580             $mimetype = static::getMimeByExtension($file_parts['extension']);
  581             $size   = filesize($file); // File size
  582 
  583             // clean all buffers
  584             while (ob_get_level()) {
  585                 ob_end_clean();
  586             }
  587 
  588             // required for IE, otherwise Content-Disposition may be ignored
  589             if (ini_get('zlib.output_compression')) {
  590                 ini_set('zlib.output_compression', 'Off');
  591             }
  592 
  593             header('Content-Type: ' . $mimetype);
  594             header('Accept-Ranges: bytes');
  595 
  596             if ($force_download) {
  597                 // output the regular HTTP headers
  598                 header('Content-Disposition: attachment; filename="' . $file_parts['basename'] . '"');
  599             }
  600 
  601             // multipart-download and download resuming support
  602             if (isset($_SERVER['HTTP_RANGE'])) {
  603                 list($a, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
  604                 list($range) = explode(',', $range, 2);
  605                 list($range, $range_end) = explode('-', $range);
  606                 $range = (int)$range;
  607                 if (!$range_end) {
  608                     $range_end = $size - 1;
  609                 } else {
  610                     $range_end = (int)$range_end;
  611                 }
  612                 $new_length = $range_end - $range + 1;
  613                 header('HTTP/1.1 206 Partial Content');
  614                 header("Content-Length: {$new_length}");
  615                 header("Content-Range: bytes {$range}-{$range_end}/{$size}");
  616             } else {
  617                 $range = 0;
  618                 $new_length = $size;
  619                 header('Content-Length: ' . $size);
  620 
  621                 if (Grav::instance()['config']->get('system.cache.enabled')) {
  622                     $expires = Grav::instance()['config']->get('system.pages.expires');
  623                     if ($expires > 0) {
  624                         $expires_date = gmdate('D, d M Y H:i:s T', time() + $expires);
  625                         header('Cache-Control: max-age=' . $expires);
  626                         header('Expires: ' . $expires_date);
  627                         header('Pragma: cache');
  628                     }
  629                     header('Last-Modified: ' . gmdate('D, d M Y H:i:s T', filemtime($file)));
  630 
  631                     // Return 304 Not Modified if the file is already cached in the browser
  632                     if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
  633                         strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= filemtime($file))
  634                     {
  635                         header('HTTP/1.1 304 Not Modified');
  636                         exit();
  637                     }
  638                 }
  639             }
  640 
  641             /* output the file itself */
  642             $chunksize = $bytes * 8; //you may want to change this
  643             $bytes_send = 0;
  644 
  645             $fp = @fopen($file, 'rb');
  646             if ($fp) {
  647                 if ($range) {
  648                     fseek($fp, $range);
  649                 }
  650                 while (!feof($fp) && (!connection_aborted()) && ($bytes_send < $new_length) ) {
  651                     $buffer = fread($fp, $chunksize);
  652                     echo($buffer); //echo($buffer); // is also possible
  653                     flush();
  654                     usleep($sec * 1000000);
  655                     $bytes_send += strlen($buffer);
  656                 }
  657                 fclose($fp);
  658             } else {
  659                 throw new \RuntimeException('Error - can not open file.');
  660             }
  661 
  662             exit;
  663         }
  664     }
  665 
  666     /**
  667      * Return the mimetype based on filename extension
  668      *
  669      * @param string $extension Extension of file (eg "txt")
  670      * @param string $default
  671      *
  672      * @return string
  673      */
  674     public static function getMimeByExtension($extension, $default = 'application/octet-stream')
  675     {
  676         $extension = strtolower($extension);
  677 
  678         // look for some standard types
  679         switch ($extension) {
  680             case null:
  681                 return $default;
  682             case 'json':
  683                 return 'application/json';
  684             case 'html':
  685                 return 'text/html';
  686             case 'atom':
  687                 return 'application/atom+xml';
  688             case 'rss':
  689                 return 'application/rss+xml';
  690             case 'xml':
  691                 return 'application/xml';
  692         }
  693 
  694         $media_types = Grav::instance()['config']->get('media.types');
  695 
  696         if (isset($media_types[$extension])) {
  697             if (isset($media_types[$extension]['mime'])) {
  698                 return $media_types[$extension]['mime'];
  699             }
  700         }
  701 
  702         return $default;
  703     }
  704 
  705     /**
  706      * Get all the mimetypes for an array of extensions
  707      *
  708      * @param array $extensions
  709      * @return array
  710      */
  711     public static function getMimeTypes(array $extensions)
  712     {
  713         $mimetypes = [];
  714         foreach ($extensions as $extension) {
  715             $mimetype = static::getMimeByExtension($extension, false);
  716             if ($mimetype && !in_array($mimetype, $mimetypes)) {
  717                 $mimetypes[] = $mimetype;
  718             }
  719         }
  720         return $mimetypes;
  721     }
  722 
  723 
  724     /**
  725      * Return the mimetype based on filename extension
  726      *
  727      * @param string $mime mime type (eg "text/html")
  728      * @param string $default default value
  729      *
  730      * @return string
  731      */
  732     public static function getExtensionByMime($mime, $default = 'html')
  733     {
  734         $mime = strtolower($mime);
  735 
  736         // look for some standard mime types
  737         switch ($mime) {
  738             case '*/*':
  739             case 'text/*':
  740             case 'text/html':
  741                 return 'html';
  742             case 'application/json':
  743                 return 'json';
  744             case 'application/atom+xml':
  745                 return 'atom';
  746             case 'application/rss+xml':
  747                 return 'rss';
  748             case 'application/xml':
  749                 return 'xml';
  750         }
  751 
  752         $media_types = (array)Grav::instance()['config']->get('media.types');
  753 
  754         foreach ($media_types as $extension => $type) {
  755             if ($extension === 'defaults') {
  756                 continue;
  757             }
  758             if (isset($type['mime']) && $type['mime'] === $mime) {
  759                 return $extension;
  760             }
  761         }
  762 
  763         return $default;
  764     }
  765 
  766     /**
  767      * Get all the extensions for an array of mimetypes
  768      *
  769      * @param array $mimetypes
  770      * @return array
  771      */
  772     public static function getExtensions(array $mimetypes)
  773     {
  774         $extensions = [];
  775         foreach ($mimetypes as $mimetype) {
  776             $extension = static::getExtensionByMime($mimetype, false);
  777             if ($extension && !\in_array($extension, $extensions, true)) {
  778                 $extensions[] = $extension;
  779             }
  780         }
  781 
  782         return $extensions;
  783     }
  784 
  785     /**
  786      * Return the mimetype based on filename
  787      *
  788      * @param string $filename Filename or path to file
  789      * @param string $default default value
  790      *
  791      * @return string
  792      */
  793     public static function getMimeByFilename($filename, $default = 'application/octet-stream')
  794     {
  795         return static::getMimeByExtension(pathinfo($filename, PATHINFO_EXTENSION), $default);
  796     }
  797 
  798     /**
  799      * Return the mimetype based on existing local file
  800      *
  801      * @param string $filename Path to the file
  802      *
  803      * @return string|bool
  804      */
  805     public static function getMimeByLocalFile($filename, $default = 'application/octet-stream')
  806     {
  807         $type = false;
  808 
  809         // For local files we can detect type by the file content.
  810         if (!stream_is_local($filename) || !file_exists($filename)) {
  811             return false;
  812         }
  813 
  814         // Prefer using finfo if it exists.
  815         if (\extension_loaded('fileinfo')) {
  816             $finfo = finfo_open(FILEINFO_SYMLINK | FILEINFO_MIME_TYPE);
  817             $type = finfo_file($finfo, $filename);
  818             finfo_close($finfo);
  819         } else {
  820             // Fall back to use getimagesize() if it is available (not recommended, but better than nothing)
  821             $info = @getimagesize($filename);
  822             if ($info) {
  823                 $type = $info['mime'];
  824             }
  825         }
  826 
  827         return $type ?: static::getMimeByFilename($filename, $default);
  828     }
  829 
  830 
  831     /**
  832      * Returns true if filename is considered safe.
  833      *
  834      * @param string $filename
  835      * @return bool
  836      */
  837     public static function checkFilename($filename)
  838     {
  839         $dangerous_extensions = Grav::instance()['config']->get('security.uploads_dangerous_extensions', []);
  840         array_walk($dangerous_extensions, function(&$val) {
  841             $val = '.' . $val;
  842         });
  843 
  844         $extension = '.' . pathinfo($filename, PATHINFO_EXTENSION);
  845 
  846         return !(
  847             // Empty filenames are not allowed.
  848             !$filename
  849             // Filename should not contain horizontal/vertical tabs, newlines, nils or back/forward slashes.
  850             || strtr($filename, "\t\v\n\r\0\\/", '_______') !== $filename
  851             // Filename should not start or end with dot or space.
  852             || trim($filename, '. ') !== $filename
  853             // Filename should not contain .php in it.
  854             || static::contains($extension, $dangerous_extensions)
  855         );
  856     }
  857 
  858     /**
  859      * Normalize path by processing relative `.` and `..` syntax and merging path
  860      *
  861      * @param string $path
  862      *
  863      * @return string
  864      */
  865     public static function normalizePath($path)
  866     {
  867         // Resolve any streams
  868         /** @var UniformResourceLocator $locator */
  869         $locator = Grav::instance()['locator'];
  870         if ($locator->isStream($path)) {
  871             $path = $locator->findResource($path);
  872         }
  873 
  874         // Set root properly for any URLs
  875         $root = '';
  876         preg_match(self::ROOTURL_REGEX, $path, $matches);
  877         if ($matches) {
  878             $root = $matches[1];
  879             $path = $matches[2];
  880         }
  881 
  882         // Strip off leading / to ensure explode is accurate
  883         if (Utils::startsWith($path,'/')) {
  884             $root .= '/';
  885             $path = ltrim($path, '/');
  886         }
  887 
  888         // If there are any relative paths (..) handle those
  889         if (Utils::contains($path, '..')) {
  890             $segments = explode('/', trim($path, '/'));
  891             $ret = [];
  892             foreach ($segments as $segment) {
  893                 if (($segment === '.') || $segment === '') {
  894                     continue;
  895                 }
  896                 if ($segment === '..') {
  897                     array_pop($ret);
  898                 } else {
  899                     $ret[] = $segment;
  900                 }
  901             }
  902             $path = implode('/', $ret);
  903         }
  904 
  905         // Stick everything back together
  906         $normalized = $root . $path;
  907         return $normalized;
  908     }
  909 
  910     /**
  911      * Check whether a function is disabled in the PHP settings
  912      *
  913      * @param string $function the name of the function to check
  914      *
  915      * @return bool
  916      */
  917     public static function isFunctionDisabled($function)
  918     {
  919         return \in_array($function, explode(',', ini_get('disable_functions')), true);
  920     }
  921 
  922     /**
  923      * Get the formatted timezones list
  924      *
  925      * @return array
  926      */
  927     public static function timezones()
  928     {
  929         $timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL);
  930         $offsets = [];
  931         $testDate = new \DateTime();
  932 
  933         foreach ($timezones as $zone) {
  934             $tz = new \DateTimeZone($zone);
  935             $offsets[$zone] = $tz->getOffset($testDate);
  936         }
  937 
  938         asort($offsets);
  939 
  940         $timezone_list = [];
  941         foreach ($offsets as $timezone => $offset) {
  942             $offset_prefix = $offset < 0 ? '-' : '+';
  943             $offset_formatted = gmdate('H:i', abs($offset));
  944 
  945             $pretty_offset = "UTC${offset_prefix}${offset_formatted}";
  946 
  947             $timezone_list[$timezone] = "(${pretty_offset}) ".str_replace('_', ' ', $timezone);
  948         }
  949 
  950         return $timezone_list;
  951     }
  952 
  953     /**
  954      * Recursively filter an array, filtering values by processing them through the $fn function argument
  955      *
  956      * @param array    $source the Array to filter
  957      * @param callable $fn     the function to pass through each array item
  958      *
  959      * @return array
  960      */
  961     public static function arrayFilterRecursive(Array $source, $fn)
  962     {
  963         $result = [];
  964         foreach ($source as $key => $value) {
  965             if (is_array($value)) {
  966                 $result[$key] = static::arrayFilterRecursive($value, $fn);
  967                 continue;
  968             }
  969             if ($fn($key, $value)) {
  970                 $result[$key] = $value; // KEEP
  971                 continue;
  972             }
  973         }
  974 
  975         return $result;
  976     }
  977 
  978     /**
  979      * Flatten an array
  980      *
  981      * @param array $array
  982      * @return array
  983      */
  984     public static function arrayFlatten($array)
  985     {
  986         $flatten = array();
  987         foreach ($array as $key => $inner) {
  988             if (is_array($inner)) {
  989                 foreach ($inner as $inner_key => $value) {
  990                     $flatten[$inner_key] = $value;
  991                 }
  992             } else {
  993                 $flatten[$key] = $inner;
  994             }
  995         }
  996 
  997         return $flatten;
  998     }
  999 
 1000     /**
 1001      * Flatten a multi-dimensional associative array into dot notation
 1002      *
 1003      * @param  array   $array
 1004      * @param  string  $prepend
 1005      * @return array
 1006      */
 1007     public static function arrayFlattenDotNotation($array, $prepend = '')
 1008     {
 1009         $results = array();
 1010         foreach ($array as $key => $value) {
 1011             if (is_array($value)) {
 1012                 $results = array_merge($results, static::arrayFlattenDotNotation($value, $prepend.$key.'.'));
 1013             } else {
 1014                 $results[$prepend.$key] = $value;
 1015             }
 1016         }
 1017 
 1018         return $results;
 1019     }
 1020 
 1021     /**
 1022      * Opposite of flatten, convert flat dot notation array to multi dimensional array
 1023      *
 1024      * @param array $array
 1025      * @param string $separator
 1026      * @return array
 1027      */
 1028     public static function arrayUnflattenDotNotation($array, $separator = '.')
 1029     {
 1030         $newArray = [];
 1031         foreach ($array as $key => $value) {
 1032             $dots = explode($separator, $key);
 1033             if (\count($dots) > 1) {
 1034                 $last = &$newArray[$dots[0]];
 1035                 foreach ($dots as $k => $dot) {
 1036                     if ($k === 0) {
 1037                         continue;
 1038                     }
 1039                     $last = &$last[$dot];
 1040                 }
 1041                 $last = $value;
 1042             } else {
 1043                 $newArray[$key] = $value;
 1044             }
 1045         }
 1046 
 1047         return $newArray;
 1048     }
 1049 
 1050     /**
 1051      * Checks if the passed path contains the language code prefix
 1052      *
 1053      * @param string $string The path
 1054      *
 1055      * @return bool|string Either false or the language
 1056      *
 1057      */
 1058     public static function pathPrefixedByLangCode($string)
 1059     {
 1060         $languages_enabled = Grav::instance()['config']->get('system.languages.supported', []);
 1061         $parts = explode('/', trim($string, '/'));
 1062 
 1063         if (count($parts) > 0 && in_array($parts[0], $languages_enabled)) {
 1064             return $parts[0];
 1065         }
 1066 
 1067         return false;
 1068     }
 1069 
 1070     /**
 1071      * Get the timestamp of a date
 1072      *
 1073      * @param string $date a String expressed in the system.pages.dateformat.default format, with fallback to a
 1074      *                     strtotime argument
 1075      * @param string $format a date format to use if possible
 1076      * @return int the timestamp
 1077      */
 1078     public static function date2timestamp($date, $format = null)
 1079     {
 1080         $config = Grav::instance()['config'];
 1081         $dateformat = $format ?: $config->get('system.pages.dateformat.default');
 1082 
 1083         // try to use DateTime and default format
 1084         if ($dateformat) {
 1085             $datetime = \DateTime::createFromFormat($dateformat, $date);
 1086         } else {
 1087             $datetime = new \DateTime($date);
 1088         }
 1089 
 1090         // fallback to strtotime() if DateTime approach failed
 1091         if ($datetime !== false) {
 1092             return $datetime->getTimestamp();
 1093         }
 1094 
 1095         return strtotime($date);
 1096     }
 1097 
 1098     /**
 1099      * @param array $array
 1100      * @param string $path
 1101      * @param null $default
 1102      * @return mixed
 1103      *
 1104      * @deprecated 1.5 Use ->getDotNotation() method instead.
 1105      */
 1106     public static function resolve(array $array, $path, $default = null)
 1107     {
 1108         user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->getDotNotation() method instead', E_USER_DEPRECATED);
 1109 
 1110         return static::getDotNotation($array, $path, $default);
 1111     }
 1112 
 1113     /**
 1114      * Checks if a value is positive
 1115      *
 1116      * @param string $value
 1117      *
 1118      * @return boolean
 1119      */
 1120     public static function isPositive($value)
 1121     {
 1122         return in_array($value, [true, 1, '1', 'yes', 'on', 'true'], true);
 1123     }
 1124 
 1125     /**
 1126      * Generates a nonce string to be hashed. Called by self::getNonce()
 1127      * We removed the IP portion in this version because it causes too many inconsistencies
 1128      * with reverse proxy setups.
 1129      *
 1130      * @param string $action
 1131      * @param bool   $previousTick if true, generates the token for the previous tick (the previous 12 hours)
 1132      *
 1133      * @return string the nonce string
 1134      */
 1135     private static function generateNonceString($action, $previousTick = false)
 1136     {
 1137         $grav = Grav::instance();
 1138 
 1139         $username = isset($grav['user']) ? $grav['user']->username : '';
 1140         $token = session_id();
 1141         $i = self::nonceTick();
 1142 
 1143         if ($previousTick) {
 1144             $i--;
 1145         }
 1146 
 1147         return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . $grav['config']->get('security.salt'));
 1148     }
 1149 
 1150     /**
 1151      * Get the time-dependent variable for nonce creation.
 1152      *
 1153      * Now a tick lasts a day. Once the day is passed, the nonce is not valid any more. Find a better way
 1154      *       to ensure nonces issued near the end of the day do not expire in that small amount of time
 1155      *
 1156      * @return int the time part of the nonce. Changes once every 24 hours
 1157      */
 1158     private static function nonceTick()
 1159     {
 1160         $secondsInHalfADay = 60 * 60 * 12;
 1161 
 1162         return (int)ceil(time() / $secondsInHalfADay);
 1163     }
 1164 
 1165     /**
 1166      * Creates a hashed nonce tied to the passed action. Tied to the current user and time. The nonce for a given
 1167      * action is the same for 12 hours.
 1168      *
 1169      * @param string $action      the action the nonce is tied to (e.g. save-user-admin or move-page-homepage)
 1170      * @param bool   $previousTick if true, generates the token for the previous tick (the previous 12 hours)
 1171      *
 1172      * @return string the nonce
 1173      */
 1174     public static function getNonce($action, $previousTick = false)
 1175     {
 1176         // Don't regenerate this again if not needed
 1177         if (isset(static::$nonces[$action][$previousTick])) {
 1178             return static::$nonces[$action][$previousTick];
 1179         }
 1180         $nonce = md5(self::generateNonceString($action, $previousTick));
 1181         static::$nonces[$action][$previousTick] = $nonce;
 1182 
 1183         return static::$nonces[$action][$previousTick];
 1184     }
 1185 
 1186     /**
 1187      * Verify the passed nonce for the give action
 1188      *
 1189      * @param string|string[] $nonce  the nonce to verify
 1190      * @param string $action the action to verify the nonce to
 1191      *
 1192      * @return boolean verified or not
 1193      */
 1194     public static function verifyNonce($nonce, $action)
 1195     {
 1196         //Safety check for multiple nonces
 1197         if (is_array($nonce)) {
 1198             $nonce = array_shift($nonce);
 1199         }
 1200 
 1201         //Nonce generated 0-12 hours ago
 1202         if ($nonce === self::getNonce($action)) {
 1203             return true;
 1204         }
 1205 
 1206         //Nonce generated 12-24 hours ago
 1207         $previousTick = true;
 1208 
 1209         return $nonce === self::getNonce($action, $previousTick);
 1210     }
 1211 
 1212     /**
 1213      * Simple helper method to get whether or not the admin plugin is active
 1214      *
 1215      * @return bool
 1216      */
 1217     public static function isAdminPlugin()
 1218     {
 1219         if (isset(Grav::instance()['admin'])) {
 1220             return true;
 1221         }
 1222 
 1223         return false;
 1224     }
 1225 
 1226     /**
 1227      * Get a portion of an array (passed by reference) with dot-notation key
 1228      *
 1229      * @param array $array
 1230      * @param string|int $key
 1231      * @param null $default
 1232      * @return mixed
 1233      */
 1234     public static function getDotNotation($array, $key, $default = null)
 1235     {
 1236         if (null === $key) {
 1237             return $array;
 1238         }
 1239 
 1240         if (isset($array[$key])) {
 1241             return $array[$key];
 1242         }
 1243 
 1244         foreach (explode('.', $key) as $segment) {
 1245             if (!is_array($array) || !array_key_exists($segment, $array)) {
 1246                 return $default;
 1247             }
 1248 
 1249             $array = $array[$segment];
 1250         }
 1251 
 1252         return $array;
 1253     }
 1254 
 1255     /**
 1256      * Set portion of array (passed by reference) for a dot-notation key
 1257      * and set the value
 1258      *
 1259      * @param array      $array
 1260      * @param string|int $key
 1261      * @param mixed      $value
 1262      * @param bool       $merge
 1263      *
 1264      * @return mixed
 1265      */
 1266     public static function setDotNotation(&$array, $key, $value, $merge = false)
 1267     {
 1268         if (null === $key) {
 1269             return $array = $value;
 1270         }
 1271 
 1272         $keys = explode('.', $key);
 1273 
 1274         while (count($keys) > 1) {
 1275             $key = array_shift($keys);
 1276 
 1277             if ( ! isset($array[$key]) || ! is_array($array[$key]))
 1278             {
 1279                 $array[$key] = array();
 1280             }
 1281 
 1282             $array =& $array[$key];
 1283         }
 1284 
 1285         $key = array_shift($keys);
 1286 
 1287         if (!$merge || !isset($array[$key])) {
 1288             $array[$key] = $value;
 1289         } else {
 1290             $array[$key] = array_merge($array[$key], $value);
 1291         }
 1292 
 1293         return $array;
 1294     }
 1295 
 1296     /**
 1297      * Utility method to determine if the current OS is Windows
 1298      *
 1299      * @return bool
 1300      */
 1301     public static function isWindows()
 1302     {
 1303         return strncasecmp(PHP_OS, 'WIN', 3) === 0;
 1304     }
 1305 
 1306     /**
 1307      * Utility to determine if the server running PHP is Apache
 1308      *
 1309      * @return bool
 1310      */
 1311     public static function isApache() {
 1312         return isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false;
 1313     }
 1314 
 1315     /**
 1316      * Sort a multidimensional array  by another array of ordered keys
 1317      *
 1318      * @param array $array
 1319      * @param array $orderArray
 1320      * @return array
 1321      */
 1322     public static function sortArrayByArray(array $array, array $orderArray)
 1323     {
 1324         $ordered = array();
 1325         foreach ($orderArray as $key) {
 1326             if (array_key_exists($key, $array)) {
 1327                 $ordered[$key] = $array[$key];
 1328                 unset($array[$key]);
 1329             }
 1330         }
 1331         return $ordered + $array;
 1332     }
 1333 
 1334     /**
 1335      * Sort an array by a key value in the array
 1336      *
 1337      * @param mixed $array
 1338      * @param string|int $array_key
 1339      * @param int $direction
 1340      * @param int $sort_flags
 1341      * @return array
 1342      */
 1343     public static function sortArrayByKey($array, $array_key, $direction = SORT_DESC, $sort_flags = SORT_REGULAR)
 1344     {
 1345         $output = [];
 1346 
 1347         if (!is_array($array) || !$array) {
 1348             return $output;
 1349         }
 1350 
 1351         foreach ($array as $key => $row) {
 1352             $output[$key] = $row[$array_key];
 1353         }
 1354 
 1355         array_multisort($output, $direction, $sort_flags, $array);
 1356 
 1357         return $array;
 1358     }
 1359 
 1360     /**
 1361      * Get path based on a token
 1362      *
 1363      * @param string $path
 1364      * @param PageInterface|null $page
 1365      * @return string
 1366      * @throws \RuntimeException
 1367      */
 1368     public static function getPagePathFromToken($path, PageInterface $page = null)
 1369     {
 1370         $path_parts = pathinfo($path);
 1371         $grav       = Grav::instance();
 1372 
 1373         $basename = '';
 1374         if (isset($path_parts['extension'])) {
 1375             $basename = '/' . $path_parts['basename'];
 1376             $path     = rtrim($path_parts['dirname'], ':');
 1377         }
 1378 
 1379         $regex = '/(@self|self@)|((?:@page|page@):(?:.*))|((?:@theme|theme@):(?:.*))/';
 1380         preg_match($regex, $path, $matches);
 1381 
 1382         if ($matches) {
 1383             if ($matches[1]) {
 1384                 if (null === $page) {
 1385                     throw new \RuntimeException('Page not available for this self@ reference');
 1386                 }
 1387             } elseif ($matches[2]) {
 1388                 // page@
 1389                 $parts = explode(':', $path);
 1390                 $route = $parts[1];
 1391                 $page  = $grav['page']->find($route);
 1392             } elseif ($matches[3]) {
 1393                 // theme@
 1394                 $parts = explode(':', $path);
 1395                 $route = $parts[1];
 1396                 $theme = str_replace(ROOT_DIR, '', $grav['locator']->findResource("theme://"));
 1397 
 1398                 return $theme . $route . $basename;
 1399             }
 1400         } else {
 1401             return $path . $basename;
 1402         }
 1403 
 1404         if (!$page) {
 1405             throw new \RuntimeException('Page route not found: ' . $path);
 1406         }
 1407 
 1408         $path = str_replace($matches[0], rtrim($page->relativePagePath(), '/'), $path);
 1409 
 1410         return $path . $basename;
 1411     }
 1412 
 1413     public static function getUploadLimit()
 1414     {
 1415         static $max_size = -1;
 1416 
 1417         if ($max_size < 0) {
 1418             $post_max_size = static::parseSize(ini_get('post_max_size'));
 1419             if ($post_max_size > 0) {
 1420                 $max_size = $post_max_size;
 1421             } else {
 1422                 $max_size = 0;
 1423             }
 1424 
 1425             $upload_max = static::parseSize(ini_get('upload_max_filesize'));
 1426             if ($upload_max > 0 && $upload_max < $max_size) {
 1427                 $max_size = $upload_max;
 1428             }
 1429         }
 1430 
 1431         return $max_size;
 1432     }
 1433 
 1434     /**
 1435      * Convert bytes to the unit specified by the $to parameter.
 1436      *
 1437      * @param int $bytes The filesize in Bytes.
 1438      * @param string $to The unit type to convert to. Accepts K, M, or G for Kilobytes, Megabytes, or Gigabytes, respectively.
 1439      * @param int $decimal_places The number of decimal places to return.
 1440      *
 1441      * @return int Returns only the number of units, not the type letter. Returns 0 if the $to unit type is out of scope.
 1442      *
 1443      */
 1444     public static function convertSize($bytes, $to, $decimal_places = 1)
 1445     {
 1446         $formulas = array(
 1447             'K' => number_format($bytes / 1024, $decimal_places),
 1448             'M' => number_format($bytes / 1048576, $decimal_places),
 1449             'G' => number_format($bytes / 1073741824, $decimal_places)
 1450         );
 1451         return $formulas[$to] ?? 0;
 1452     }
 1453 
 1454     /**
 1455      * Return a pretty size based on bytes
 1456      *
 1457      * @param int $bytes
 1458      * @param int $precision
 1459      * @return string
 1460      */
 1461     public static function prettySize($bytes, $precision = 2)
 1462     {
 1463         $units = array('B', 'KB', 'MB', 'GB', 'TB');
 1464 
 1465         $bytes = max($bytes, 0);
 1466         $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
 1467         $pow = min($pow, count($units) - 1);
 1468 
 1469         // Uncomment one of the following alternatives
 1470         $bytes /= 1024 ** $pow;
 1471         // $bytes /= (1 << (10 * $pow));
 1472 
 1473         return round($bytes, $precision) . ' ' . $units[$pow];
 1474     }
 1475 
 1476     /**
 1477      * Parse a readable file size and return a value in bytes
 1478      *
 1479      * @param string|int $size
 1480      * @return int
 1481      */
 1482     public static function parseSize($size)
 1483     {
 1484         $unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
 1485         $size = preg_replace('/[^0-9\.]/', '', $size);
 1486 
 1487         if ($unit) {
 1488             $size = $size * pow(1024, stripos('bkmgtpezy', $unit[0]));
 1489         }
 1490 
 1491         return (int) abs(round($size));
 1492     }
 1493 
 1494     /**
 1495      * Multibyte-safe Parse URL function
 1496      *
 1497      * @param string $url
 1498      * @return array
 1499      * @throws \InvalidArgumentException
 1500      */
 1501     public static function multibyteParseUrl($url)
 1502     {
 1503         $enc_url = preg_replace_callback(
 1504             '%[^:/@?&=#]+%usD',
 1505             function ($matches) {
 1506                 return urlencode($matches[0]);
 1507             },
 1508             $url
 1509         );
 1510 
 1511         $parts = parse_url($enc_url);
 1512 
 1513         if($parts === false) {
 1514             throw new \InvalidArgumentException('Malformed URL: ' . $url);
 1515         }
 1516 
 1517         foreach($parts as $name => $value) {
 1518             $parts[$name] = urldecode($value);
 1519         }
 1520 
 1521         return $parts;
 1522     }
 1523 
 1524     /**
 1525      * Process a string as markdown
 1526      *
 1527      * @param string $string
 1528      *
 1529      * @param bool $block Block or Line processing
 1530      * @param null $page
 1531      * @return string
 1532      * @throws \Exception
 1533      */
 1534     public static function processMarkdown($string, $block = true, $page = null)
 1535     {
 1536         $grav = Grav::instance();
 1537         $page     = $page ?? $grav['page'] ?? null;
 1538         $defaults = [
 1539             'markdown' => $grav['config']->get('system.pages.markdown', []),
 1540             'images' => $grav['config']->get('system.images', [])
 1541         ];
 1542         $extra = $defaults['markdown']['extra'] ?? false;
 1543 
 1544         $excerpts = new Excerpts($page, $defaults);
 1545 
 1546         // Initialize the preferred variant of Parsedown
 1547         if ($extra) {
 1548             $parsedown = new ParsedownExtra($excerpts);
 1549         } else {
 1550             $parsedown = new Parsedown($excerpts);
 1551         }
 1552 
 1553         if ($block) {
 1554             $string = $parsedown->text($string);
 1555         } else {
 1556             $string = $parsedown->line($string);
 1557         }
 1558 
 1559         return $string;
 1560     }
 1561 
 1562     /**
 1563      * Find the subnet of an ip with CIDR prefix size
 1564      *
 1565      * @param string $ip
 1566      * @param int $prefix
 1567      *
 1568      * @return string
 1569      */
 1570     public static function getSubnet($ip, $prefix = 64)
 1571     {
 1572         if (!filter_var($ip, FILTER_VALIDATE_IP)) {
 1573             return $ip;
 1574         }
 1575 
 1576         // Packed representation of IP
 1577         $ip = inet_pton($ip);
 1578 
 1579         // Maximum netmask length = same as packed address
 1580         $len = 8*strlen($ip);
 1581         if ($prefix > $len) $prefix = $len;
 1582 
 1583         $mask  = str_repeat('f', $prefix>>2);
 1584 
 1585         switch($prefix & 3)
 1586         {
 1587             case 3: $mask .= 'e'; break;
 1588             case 2: $mask .= 'c'; break;
 1589             case 1: $mask .= '8'; break;
 1590         }
 1591         $mask = str_pad($mask, $len>>2, '0');
 1592 
 1593         // Packed representation of netmask
 1594         $mask = pack('H*', $mask);
 1595         // Bitwise - Take all bits that are both 1 to generate subnet
 1596         $subnet = inet_ntop($ip & $mask);
 1597 
 1598         return $subnet;
 1599     }
 1600 
 1601     /**
 1602      * Wrapper to ensure html, htm in the front of the supported page types
 1603      *
 1604      * @param array|null $defaults
 1605      * @return array|mixed
 1606      */
 1607     public static function getSupportPageTypes(array $defaults = null)
 1608     {
 1609         $types = Grav::instance()['config']->get('system.pages.types', $defaults);
 1610 
 1611         // remove html/htm
 1612         $types = static::arrayRemoveValue($types, ['html', 'htm']);
 1613 
 1614         // put them back at the front
 1615         $types = array_merge(['html', 'htm'], $types);
 1616 
 1617         return $types;
 1618     }
 1619 }