SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Sprout.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13.  
  14. namespace Sprout\Helpers;
  15.  
  16. use Exception;
  17. use InvalidArgumentException;
  18. use ReflectionClass;
  19.  
  20. use Kohana;
  21.  
  22. use karmabunny\pdb\Exceptions\QueryException;
  23.  
  24.  
  25. /**
  26. * Useful functions for sprout in general
  27. **/
  28. class Sprout
  29. {
  30.  
  31.  
  32. /**
  33.   * Determines the file path for a class, usually for autoloading
  34.   * @param string $class The class, including namespace
  35.   * @return string|false path or false if it couldn't be found
  36.   */
  37. public static function determineFilePath($class)
  38. {
  39. $sep = DIRECTORY_SEPARATOR;
  40. $sprout_ns = 'Sprout\\';
  41. $sprout_ns_len = strlen($sprout_ns);
  42. $modules_ns = 'SproutModules\\';
  43. $modules_ns_len = strlen($modules_ns);
  44. $file = false;
  45.  
  46. // Load Sprout core
  47. if (substr($class, 0, $sprout_ns_len) == $sprout_ns) {
  48. $file = substr($class, $sprout_ns_len);
  49. $dir = realpath(__DIR__ . $sep . '..') . $sep;
  50.  
  51. // Load modules
  52. } else if (substr($class, 0, $modules_ns_len) == $modules_ns) {
  53. $file = substr($class, $modules_ns_len);
  54.  
  55. // Strip vendor name to get directory within modules/
  56. // e.g. SproutModules\Karmabunny\Pages => Pages
  57. $slash_pos = strpos($file, '\\');
  58. if ($slash_pos === false) return;
  59. $file = substr($file, $slash_pos + 1);
  60.  
  61. $dir = realpath(__DIR__ . str_repeat($sep . '..', 2)) . $sep;
  62. $dir .= 'modules' . $sep;
  63.  
  64. } else {
  65. $dir = DOCROOT . 'vendor/';
  66. $file = $class;
  67. }
  68.  
  69. if (!$file) return false;
  70.  
  71. return $dir . str_replace('\\', $sep, $file) . '.php';
  72. }
  73.  
  74.  
  75. /**
  76.   * Removes the namespaces from a class-like entity,
  77.   * e.g. Sprout\Helpers\Text => Text
  78.   * @param string $classlike
  79.   * @return string
  80.   */
  81. public static function removeNs($classlike)
  82. {
  83. $pos = strrpos($classlike, '\\');
  84. if ($pos !== false) {
  85. return substr($classlike, $pos + 1);
  86. }
  87. return $classlike;
  88. }
  89.  
  90.  
  91. /**
  92.   * Creates an object of a class specified by a string name, with a list of possible namespaces to lookup if the
  93.   * specified class name doesn't contain a namespace
  94.   * @param string $class The class to instantiate
  95.   * @param array $possible_nses Possible namespaces to try
  96.   * @return object
  97.   * @throws Exception if the class lookup failed
  98.   */
  99. public static function nsNew($class, array $possible_nses)
  100. {
  101. if (strpos($class, '\\') !== false) {
  102. if (class_exists($class)) {
  103. return new $class;
  104. } else {
  105. throw new Exception("Unable to load class {$class}");
  106. }
  107. }
  108. foreach ($possible_nses as $ns) {
  109. $full_class = $ns . '\\' . $class;
  110. if (class_exists($full_class)) {
  111. return new $full_class;
  112. }
  113. }
  114. throw new Exception("Class lookup failed: {$class}");
  115. }
  116.  
  117.  
  118. /**
  119.   * Gets the full class name (including namespace) for a specified class, with a list of namespaces to search
  120.   * @param string $class The class to instantiate, e.g. 'Fb'
  121.   * @param array $possible_nses Possible namespaces to try, e.g. ['Sprout\Helpers']
  122.   * @return string
  123.   * @throws Exception if the class lookup failed
  124.   */
  125. public static function nsClass($class, array $possible_nses)
  126. {
  127. if (strpos($class, '\\') !== false) {
  128. if (class_exists($class)) return $class;
  129. }
  130. foreach ($possible_nses as $ns) {
  131. $full_class = $ns . '\\' . $class;
  132. if (class_exists($full_class)) return $full_class;
  133. }
  134. throw new Exception("Class lookup failed: {$class}");
  135. }
  136.  
  137.  
  138. /**
  139.   * Gets the full name (including namespaced class) for a specified function
  140.   * @param string $func The function to find, e.g. 'Fb::dropdown'
  141.   * @param array $possible_nses Possible namespaces to try, e.g. ['Sprout\Helpers']
  142.   * @return string
  143.   * @throws Exception if the function lookup failed
  144.   */
  145. public static function nsFunc($func, array $possible_nses)
  146. {
  147. $class = '';
  148. if (strpos($func, '::') !== false) {
  149. list($class, $func) = explode('::', $func, 2);
  150. }
  151.  
  152. if (strpos($func, '\\') !== false) {
  153. if ($class) {
  154. if (method_exists($class, $func)) return "{$class}::{$func}";
  155. }
  156. if (function_exists($func)) return $func;
  157. }
  158. foreach ($possible_nses as $ns) {
  159. if ($class) {
  160. $full_class = $ns . '\\' . $class;
  161. if (method_exists($full_class, $func)) return "{$full_class}::{$func}";
  162. } else {
  163. $full_fn = $ns . '\\' . $func;
  164. if (function_exists($full_fn)) return $full_fn;
  165. }
  166. }
  167. throw new Exception("Function lookup failed: {$func}");
  168. }
  169.  
  170.  
  171. /**
  172.   * Construct a new instance of a class with a given name
  173.   *
  174.   * @example
  175.   * $inst = Sprout::instance($widget_class, 'Sprout\\Widgets\\Widget');
  176.   *
  177.   * @param string $class_name The name of the class
  178.   * @param string|array $base_class_name The base class or interface which the class must extend/implement.
  179.   * Can be a string for a single check, or an array for multiple checks.
  180.   * NULL disables this check.
  181.   * @throws InvalidArgumentException If the class does not exist
  182.   * @return mixed The new instance
  183.   */
  184. public static function instance($class_name, $base_class_name = null)
  185. {
  186. if (!$class_name or !class_exists($class_name)) {
  187. throw new InvalidArgumentException("Class <{$class_name}> does not exist");
  188. }
  189.  
  190. // Check the class isn't abstract
  191. $class = new ReflectionClass($class_name);
  192. if ($class->isAbstract()) {
  193. throw new InvalidArgumentException("Class <{$class_name}> is abstract");
  194. }
  195.  
  196. // Check that the class extends/implements everything it's required to
  197. if (!empty($base_class_name)) {
  198. if (!is_array($base_class_name)) {
  199. $base_class_name = [$base_class_name];
  200. }
  201. foreach ($base_class_name as $chk) {
  202. if (!$class->isSubclassOf($chk)) {
  203. throw new InvalidArgumentException("Class <{$class_name}> is not a sub-class of <{$chk}>");
  204. }
  205. }
  206. }
  207.  
  208. // Interesting little way to report an instantiation error even if display_errors is off.
  209. // On fatal errors, the output buffer gets flushed to the beowser, reporting our message.
  210. // On success, we just clear the buffer.
  211. // On the test server, we just let it die naturally.
  212. if (IN_PRODUCTION) {
  213. echo '<p><b>FATAL ERROR:</b><br>Unable to instance class "' . Enc::html($class_name) . '".</p>';
  214. $inst = @new $class_name;
  215. } else {
  216. $inst = new $class_name;
  217. }
  218.  
  219. return $inst;
  220. }
  221.  
  222.  
  223. /**
  224.   * Returns the current version of sprout
  225.   *
  226.   * @param bool $git_version Optional flag to return git version, returns branding version by default
  227.   */
  228. public static function getVersion($git_version = false)
  229. {
  230. if (!empty($git_version)) return Kohana::config('core.version');
  231. return Kohana::config('core.version_brand');
  232. }
  233.  
  234. /**
  235.   * Returns true if the specified module is currently installed, false otherwise
  236.   **/
  237. public static function moduleInstalled($module_name)
  238. {
  239. return in_array($module_name, Register::getModules());
  240. }
  241.  
  242.  
  243. /**
  244.   * Gets a simplified backtrace with fewer elements and no recursion
  245.   * @param array $trace If empty, the trace is automatically determined
  246.   * @return array
  247.   */
  248. public static function simpleBacktrace(array $trace = [])
  249. {
  250. if (count($trace) == 0) {
  251. $trace = debug_backtrace();
  252.  
  253. // Remove this and its caller
  254. array_shift($trace);
  255. array_shift($trace);
  256. }
  257.  
  258. $simple_trace = [];
  259. foreach ($trace as $call) {
  260. $simple_call = [];
  261. if (isset($call['file'])) {
  262. $file = $call['file'];
  263. if (IN_PRODUCTION or @$_SERVER['SERVER_ADDR'] != @$_SERVER['REMOTE_ADDR']) {
  264. if (substr($file, 0, strlen(DOCROOT)) == DOCROOT) {
  265. $file = substr($file, strlen(DOCROOT));
  266. }
  267. }
  268. $simple_call['file'] = $file;
  269. }
  270. if (isset($call['line'])) {
  271. $simple_call['line'] = $call['line'];
  272. }
  273. if (!empty($call['function'])) {
  274. $call_func = $call['function'];
  275. if (isset($call['class'])) {
  276. $class = $call['class'];
  277. if (isset($call['type'])) {
  278. $class .= $call['type'];
  279. } else {
  280. $class .= '-??-';
  281. }
  282. $call_func = $class . $call_func;
  283. }
  284. $simple_call['function'] = $call_func;
  285. }
  286. if (!empty($call['args'])) {
  287. $args = [];
  288. foreach ($call['args'] as $akey => $aval) {
  289. if (is_object($aval)) {
  290. $args[$akey] = get_class($aval);
  291. } else if (is_array($aval)) {
  292. $len = count($aval);
  293. $args[$akey] = "array({$len}): " . self::condenseArray($aval);
  294. } else {
  295. $args[$akey] = self::readableVar($aval);
  296. }
  297. }
  298. unset($call['args']);
  299. $simple_call['args'] = $args;
  300. }
  301. $simple_trace[] = $simple_call;
  302. }
  303. return $simple_trace;
  304. }
  305.  
  306.  
  307. /**
  308.   * Converts a variable into something human readable
  309.   * @param mixed $var
  310.   * @return string
  311.   */
  312. public static function readableVar($var)
  313. {
  314. if (is_array($var)) return self::condenseArray($var);
  315. if (is_bool($var)) return $var? 'true': 'false';
  316. if (is_null($var)) return 'null';
  317. if (is_int($var) or is_float($var)) return $var;
  318. if (is_string($var)) {
  319. return "'" . str_replace("'", "\\'", $var) . "'";
  320. }
  321. if (is_resource($var)) return 'resource';
  322. if (is_object($var)) return get_class($var);
  323. return 'unknown';
  324. }
  325.  
  326.  
  327. /**
  328.   * Condenses an array into a string
  329.   */
  330. public static function condenseArray(array $arr)
  331. {
  332. $keys = array_keys($arr);
  333. $int_keys = true;
  334. foreach ($keys as $key) {
  335. if (!is_int($key)) {
  336. $int_keys = false;
  337. break;
  338. }
  339. }
  340.  
  341. $str = '[';
  342. $arg_num = 0;
  343. foreach ($arr as $key => $val) {
  344. if (++$arg_num != 1) $str .= ', ';
  345. if (!$int_keys) $str .= self::readableVar($key) . ' => ';
  346. $str .= self::readableVar($val);
  347. }
  348. $str .= ']';
  349. return $str;
  350. }
  351.  
  352.  
  353. /**
  354.   * Checks a URL that is to be used for redirection is valid.
  355.   *
  356.   * Will allow remote URLs beginning with 'http://' and local URLs beginning with '/'
  357.   **/
  358. public static function checkRedirect($text)
  359. {
  360. $text = Enc::cleanfunky($text);
  361. if (preg_match('!^http(s?)://[a-z]!', $text)) return true;
  362. if (preg_match('!^/[a-z]!', $text)) return true;
  363.  
  364. return false;
  365. }
  366.  
  367.  
  368. /**
  369.   * Returns an absolute URL for the web root of this server
  370.   *
  371.   * Example: 'http://thejosh.info/sprout_test/'
  372.   *
  373.   * @param string $protocol Protocol to use. 'http' or 'https'.
  374.   * Defaults to server config option, which if not set, uses current request protocol.
  375.   **/
  376. public static function absRoot($protocol = '')
  377. {
  378. if ($protocol == '') {
  379. $protocol = Kohana::config('config.site_protocol');
  380. }
  381. if ($protocol == '') {
  382. $protocol = Request::protocol();
  383. }
  384. if ($protocol == '') {
  385. $protocol = 'http';
  386. }
  387.  
  388. return Url::base(true, $protocol);
  389. }
  390.  
  391.  
  392. /**
  393.   * Takes a mysql DATETIME value (will probably also work with a TIME or DATE value)
  394.   * and formats it according to the format codes specified by the PHP date() function.
  395.   *
  396.   * The format is optional, if omittted, uses 'd/m/Y g:i a' = '7/11/2010 5:27 pm'
  397.   **/
  398. public static function formatMysqlDatetime($date, $format = 'd/m/Y g:i a')
  399. {
  400. return date($format, strtotime($date));
  401. }
  402.  
  403.  
  404. /**
  405.   * Returns a string of random numbers and letters
  406.   **/
  407. public static function randStr($length = 16, $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890')
  408. {
  409. $num_chars = strlen($chars) - 1;
  410.  
  411. $string = '';
  412. for ($i = 0; $i < $length; $i++) {
  413. $string .= $chars[mt_rand(0, $num_chars)];
  414. }
  415.  
  416. return $string;
  417. }
  418.  
  419.  
  420. /**
  421.   * Returns a time in 'x minutes ago' format.
  422.   *
  423.   * Very small times (0, 1 seconds) are considered 'Just now'.
  424.   * Times are represneted in seconds, minutes, hours or days.
  425.   *
  426.   * @param int $timediff Amount of time that has passed, in seconds.
  427.   **/
  428. public static function timeAgo($timediff)
  429. {
  430. $timediff = (int) $timediff;
  431.  
  432. if ($timediff < 2) return 'Just now';
  433.  
  434. if ($timediff >= 86400) {
  435. $unit = ' day';
  436. $time = floor($timediff / 86400);
  437.  
  438. } else if ($timediff >= 3600) {
  439. $unit = ' hour';
  440. $time = floor($timediff / 3600);
  441.  
  442. } else if ($timediff >= 60) {
  443. $unit = ' minute';
  444. $time = floor($timediff / 60);
  445.  
  446. } else {
  447. $unit = ' second';
  448. $time = $timediff;
  449.  
  450. }
  451.  
  452. return $time . $unit . ($time == 1 ? ' ago' : 's ago');
  453. }
  454.  
  455.  
  456. /**
  457.   * Load the text for an extra page.
  458.   * Returns NULL on error.
  459.   *
  460.   * @param int $type The page type, should be one of the type constants
  461.   **/
  462. public static function extraPage($type)
  463. {
  464. $subsite_id = SubsiteSelector::$content_id;
  465.  
  466. $q = "SELECT text
  467. FROM ~extra_pages
  468. WHERE subsite_id = ? AND type = ?
  469. ORDER BY id
  470. LIMIT 1";
  471. try {
  472. $row = Pdb::q($q, [$subsite_id, $type], 'row');
  473. } catch (QueryException $ex) {
  474. return null;
  475. }
  476.  
  477. return $row['text'];
  478. }
  479.  
  480.  
  481. /**
  482.   * Attempts to put the handbrake on a script which is doing malicious inserts to the database
  483.   *
  484.   * @param string $table The table name, not prefixed
  485.   * @param string $column The column to check
  486.   * @param string $value The value to check
  487.   * @param string $limit The number of inserts allowed in the provided time
  488.   * @param string $time The amount of time the limit applies for, in seconds. Default = 1 hour
  489.   * @param array $conds Additional conditions for the WHERE clause, formatted as per {@see Pdb::buildClause}
  490.   * @return bool True if the insert rate is OK
  491.   **/
  492. public static function checkInsertRate($table, $column, $value, $limit, $time = 3600, array $conds = [])
  493. {
  494. Pdb::validateIdentifier($table);
  495. Pdb::validateIdentifier($column);
  496.  
  497. $params = [$value, (int)$time];
  498. $clause = Pdb::buildClause($conds, $params);
  499. if ($clause) $clause = 'AND ' . $clause;
  500.  
  501. $q = "SELECT COUNT(id) AS C
  502. FROM ~{$table}
  503. WHERE {$column} LIKE ?
  504. AND date_added > DATE_SUB(NOW(), INTERVAL ? SECOND)
  505. {$clause}";
  506. $count = Pdb::q($q, $params, 'val');
  507.  
  508. if ($count >= $limit) return false;
  509.  
  510. return true;
  511. }
  512.  
  513.  
  514. /**
  515.   * Back-end for link-checking tool
  516.   **/
  517. public static function linkChecker()
  518. {
  519. throw new Exception('Not in use any more; use the worker "WorkerLinkChecker".');
  520. }
  521.  
  522.  
  523. /**
  524.   * Takes two strings of text (which will be stripped of HTML tags)
  525.   * and returns HTML which is a table showing the differences
  526.   * in a nice colourful way
  527.   **/
  528. public static function colorisedDiff($orig, $new)
  529. {
  530. $tmp_name1 = tempnam('/tmp', 'dif');
  531. file_put_contents($tmp_name1, trim(strip_tags($orig)) . "\n");
  532.  
  533. $tmp_name2 = tempnam('/tmp', 'dif');
  534. file_put_contents($tmp_name2, trim(strip_tags($new)) . "\n");
  535.  
  536. $diff = shell_exec("diff -yat --left-column --width=3004 {$tmp_name1} {$tmp_name2}");
  537.  
  538. unlink($tmp_name1);
  539. unlink($tmp_name2);
  540.  
  541. // Colorise diff
  542. $diff = explode("\n", $diff);
  543. $out = '<table cellpadding="5" cellspacing="3">';
  544. $out .= '<tr><td>&nbsp;</td>';
  545. $out .= '<th style="width: 420px;" bgcolor="#CECECE">Old revision (paragraph-by-paragraph)</th>';
  546. $out .= '<th style="width: 420px;" bgcolor="#CECECE">New revision (paragraph-by-paragraph)</th>';
  547. $out .= '</tr>';
  548.  
  549. foreach ($diff as &$line) {
  550. if (! preg_match('/^(.{1,1500}) (.) ? ?(.{1,1500})?$/', $line, $matches)) continue;
  551. @list($nop, $left, $char, $right) = $matches;
  552.  
  553. if ($left == '' and $right == '') continue;
  554.  
  555. $line = $left . '<b>' . $char . '</b>' . $right;
  556.  
  557. if (strlen($left) >= 1500) $left .= '...';
  558. if (strlen($right) >= 1500) $right .= '...';
  559.  
  560. switch ($char) {
  561. case '(':
  562. //$out .= '<tr><td><b>Not changed</b></td><td>' . $left . '</td><td>' . $left . '</td></tr>';
  563. break;
  564.  
  565. case '|':
  566. $out .= '<tr><td><b>Changed</b></td><td bgcolor="#D8F1FF">' . $left . '</td><td bgcolor="#D8F1FF">' . $right . '</td></tr>';
  567. break;
  568.  
  569. case '<':
  570. $out .= '<tr><td><b>Removed</b></td><td bgcolor="#FCA7AE">' . $left . '</td><td bgcolor="#FFDDDF">&nbsp;</td></tr>';
  571. break;
  572.  
  573. case '>':
  574. $out .= '<tr><td><b>Added</b></td><td bgcolor="#E6FADD">&nbsp;</td><td bgcolor="#C9FFB3">' . $right . '</td></tr>';
  575. break;
  576. }
  577. }
  578. $out .= '</table>';
  579.  
  580. return $out;
  581. }
  582.  
  583.  
  584. /**
  585.   * Set the etag header, and some expiry headers.
  586.   * Checks if the etag matches - if it does, terminates the script with '304 Not Modified'.
  587.   *
  588.   * ETag should be specified as a string.
  589.   * Expires should be specified as a number of seconds, after that time the URL will expire.
  590.   *
  591.   * ETags should be something which is unique for that version of the URL. They should use
  592.   * something which is collission-resistant, such as MD5. They should vary based on the
  593.   * Accept-Encoding header, or any other 'accept' headers, if you are supporting them.
  594.   **/
  595. public static function etag($etag, $expires)
  596. {
  597. header('ETag: "' . $etag . '"');
  598. header('Pragma: public');
  599. header('Cache-Control: store, cache, must-revalidate, max-age=' . $expires);
  600. header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');
  601.  
  602. if ($_SERVER['HTTP_IF_NONE_MATCH']) {
  603. $match = str_replace('"', '', $_SERVER['HTTP_IF_NONE_MATCH']);
  604. if ($match == $etag) {
  605. header('HTTP/1.0 304 Not Modified');
  606. }
  607. }
  608. }
  609.  
  610.  
  611. /**
  612.   * Translate an array which may contain a page_id, filename or url into the final URL to use
  613.   **/
  614. public static function translateLink($row)
  615. {
  616. if ($row['page_id']) {
  617. $root = Navigation::getRootNode();
  618. $page = $root->findNodeValue('id', $row['page_id']);
  619. if ($page) {
  620. return $page->getFriendlyUrl();
  621. }
  622. }
  623.  
  624. if ($row['filename']) {
  625. return File::absUrl($row['filename']);
  626. }
  627.  
  628. if ($row['url']) {
  629. return $row['url'];
  630. }
  631.  
  632. return null;
  633. }
  634.  
  635.  
  636. /**
  637.   * Return the last-modified date of all pages on the (sub-)site
  638.   * Returns NULL on error.
  639.   *
  640.   * The date is formatted using the php date function.
  641.   * The default date format is "d/m/Y".
  642.   *
  643.   * @param string $date_format The date format to return the date in
  644.   * @return string Last modified date
  645.   * @return null On error
  646.   **/
  647. public static function lastModified($date_format = 'd/m/Y')
  648. {
  649. try {
  650. $q = "SELECT date_modified
  651. FROM ~pages
  652. WHERE subsite_id = ?
  653. ORDER BY date_modified DESC
  654. LIMIT 1";
  655. $date = Pdb::query($q, [SubsiteSelector::$content_id], 'val');
  656. return date($date_format, strtotime($date));
  657.  
  658. } catch (QueryException $ex) {
  659. return null;
  660. }
  661. }
  662.  
  663.  
  664. /**
  665.   * Adds classes, analytics and target-blank to file links.
  666.   * Also adds a random string, which prevents caching, solving some problems we were having with some clients.
  667.   **/
  668. public static function specialFileLinks($html)
  669. {
  670. // Grabs <a> links, with href containing:
  671. // - optional something
  672. // - "files/"
  673. // - something
  674. // - "."
  675. // - some letters (a-z)
  676. // and the A must only have non HTML content (doesn't contain < or >)
  677. //
  678. '!<a[^>]+href="([^"]*)files/([^"]+\.([a-z]+))"[^>]*>([^<>]+)</a>!',
  679.  
  680. function($matches) {
  681. $matches[1] = html_entity_decode($matches[1]);
  682. $matches[2] = html_entity_decode($matches[2]);
  683. $matches[3] = html_entity_decode($matches[3]);
  684. $matches[4] = html_entity_decode($matches[4]);
  685.  
  686. // Only mangle local URLs; leave remote URLs alone
  687. $http_pattern = '#^(?:https?:)?//([^/]*)#';
  688. $link_matches = [];
  689. $link_matches_pattern = preg_match($http_pattern, $matches[1], $link_matches);
  690. $own_domain_matches = [];
  691. $url_base = Subsites::getAbsRoot(SubsiteSelector::$subsite_id);
  692. $own_domain_matches_pattern = preg_match($http_pattern, $url_base, $own_domain_matches);
  693.  
  694. // Local URLs
  695. $url = File::relUrl($matches[2]) . '?v=' . mt_rand(100, 999);
  696.  
  697. // Remote URLs
  698. if ($link_matches_pattern and $own_domain_matches_pattern) {
  699. $link_domain = preg_replace('/^www\./', '', $link_matches[1]);
  700. $own_domain = preg_replace('/^www\./', '', $own_domain_matches[1]);
  701. if ($link_domain != $own_domain) {
  702. return $matches[0];
  703. }
  704. }
  705.  
  706. $class = 'document document-' . $matches[3];
  707. $onclick = "ga('send', 'event', 'Document', 'Download', '" . Enc::js($matches[2]) . "');";
  708.  
  709. if (preg_match('!class="([^"]+)"!', $matches[0], $m)) {
  710. $class .= ' ' . html_entity_decode($m[1]);
  711. }
  712.  
  713. $out = '<a href="' . Enc::html($url) . '"';
  714. $out .= ' class="' . Enc::html(trim($class)) . '"';
  715. $out .= ' target="_blank"';
  716. $out .= ' data-ext="' . Enc::html($matches[3]) . '"';
  717. $out .= ' data-size="' . Enc::html(File::humanSize(File::size($matches[2]))) . '"';
  718. $out .= ' onclick="' . Enc::html($onclick) . '">';
  719. $out .= Enc::html($matches[4]);
  720. $out .= '</a>';
  721.  
  722. return $out;
  723. },
  724.  
  725. $html
  726. );
  727. }
  728.  
  729.  
  730. /**
  731.   * Return true if the browser supports drag-and-drop uploads.
  732.   **/
  733. public static function browserDragdropUploads()
  734. {
  735. $supported = array(
  736. 'Firefox' => '4.0.0',
  737. 'Internet Explorer' => '10.0',
  738. 'Chrome' => '13.0.0',
  739. 'Safari' => '6.0.0',
  740. );
  741.  
  742. if (! isset($supported[Kohana::userAgent('browser')])) {
  743. return false;
  744. }
  745.  
  746. $min_version = $supported[Kohana::userAgent('browser')];
  747.  
  748. return version_compare(Kohana::userAgent('version'), $min_version, '>=');
  749. }
  750.  
  751.  
  752. /**
  753.   * @deprecated Use {@see Security::passwordComplexity} instead
  754.   **/
  755. public static function passwordComplexity($str)
  756. {
  757. $errs = Security::passwordComplexity($str, 8, 0, false);
  758. if (count($errs) == 0) return true;
  759. return $errs;
  760. }
  761.  
  762.  
  763. /**
  764.   * Return a list of admins to send emails to.
  765.   *
  766.   * The return value is an array of arrays.
  767.   * The inner arrays contains the keys "name" and "email".
  768.   **/
  769. public static function adminEmails()
  770. {
  771. $out = array();
  772.  
  773. $ops = AdminPerms::getOperatorsWithAccess('access_reportemail');
  774. foreach ($ops as $row) {
  775. $out[] = array(
  776. 'name' => $row['name'],
  777. 'email' => $row['email'],
  778. );
  779. }
  780.  
  781. return $out;
  782. }
  783.  
  784.  
  785. /**
  786.   * Check an IP against a list of IP addresses, with logic for CIDR ranges
  787.   *
  788.   * @return bool True if the IP is in the list, false if it's not
  789.   **/
  790. public static function ipaddressInArray($needle, $haystack)
  791. {
  792. foreach ($haystack as $check) {
  793. $parts = explode('/', $check, 2);
  794.  
  795. if (count($parts) == 1) {
  796. // Plain IP
  797. if ($needle == $parts[0]) return true;
  798.  
  799. } else {
  800. // CIDR
  801. list($subnet, $mask) = $parts;
  802. $mask = ~((1 << (32 - $mask)) - 1);
  803.  
  804. // Correctly handle unaligned subnets
  805. $subnet = ip2long($subnet) & $mask;
  806. if ((ip2long($needle) & $mask) === $subnet) {
  807. return true;
  808. }
  809. }
  810. }
  811.  
  812. return false;
  813. }
  814.  
  815.  
  816. /**
  817.   * Returns the memory limit in bytes. If there is no limit, returns INT_MAX.
  818.   *
  819.   * @return int Bytes
  820.   */
  821. public static function getMemoryLimit()
  822. {
  823. $memory_limit = ini_get('memory_limit');
  824.  
  825. if ($memory_limit == -1) return INT_MAX;
  826.  
  827. if (preg_match('/^(\d+)(.)$/', $memory_limit, $matches)) {
  828. $matches[2] = strtoupper($matches[2]);
  829. if ($matches[2] == 'G') return $matches[1] * 1024 * 1024 * 1024;
  830. if ($matches[2] == 'M') return $matches[1] * 1024 * 1024;
  831. if ($matches[2] == 'K') return $matches[1] * 1024;
  832. } else {
  833. return $memory_limit;
  834. }
  835. }
  836.  
  837. /**
  838.   * Gets the first key value pair of an iterable
  839.   *
  840.   * This is to replace `reset` which has been deprecated in 7.2. While this lacks the
  841.   * stateful behaviour of the original (i.e. changing the internal pointer) it does
  842.   * recreate the most used feature: fetching the first element without knowing its key.
  843.   *
  844.   * @param iterable $iter An array or Traversable
  845.   * @return array|null An array of [key, value] or null if the iterable is empty
  846.   * @example
  847.   * list ($key, $value) = Sprout::iterableFirst(['an' => 'array']);
  848.   */
  849. public static function iterableFirst($iter)
  850. {
  851. foreach ($iter as $k => $v) {
  852. return [$k, $v];
  853. }
  854.  
  855. return null;
  856. }
  857.  
  858. /**
  859.   * Gets the first key of an iterable
  860.   *
  861.   * @param iterable $iter An array or Traversable
  862.   * @return mixed|null The value or null if the iterable is emtpy
  863.   */
  864. public static function iterableFirstKey($iter)
  865. {
  866. return @static::iterableFirst($iter)[0];
  867. }
  868.  
  869. /**
  870.   * Gets the first value of an iterable
  871.   *
  872.   * @param iterable $iter An array or Traversable
  873.   * @return mixed|null The value or null if the iterable is empty
  874.   */
  875. public static function iterableFirstValue($iter)
  876. {
  877. return @static::iterableFirst($iter)[1];
  878. }
  879. }
  880.