PageRenderTime 66ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/symphony/lib/toolkit/class.general.php

https://github.com/vlad-ghita/symphony-2
PHP | 1437 lines | 581 code | 186 blank | 670 comment | 152 complexity | 0013e5833d4b915ae5ff8b64883b5ffe MD5 | raw file
Possible License(s): BSD-3-Clause-No-Nuclear-License-2014
  1. <?php
  2. /**
  3. * @package toolkit
  4. */
  5. define_safe('CDATA_BEGIN', '<![CDATA[');
  6. define_safe('CDATA_END', ']]>');
  7. /**
  8. * General is a utility class that offers a number miscellaneous of
  9. * functions that are used throughout Symphony.
  10. */
  11. Class General{
  12. /**
  13. * Convert any special characters into their entity equivalents. Since
  14. * Symphony 2.3, this function assumes UTF-8 and will not double
  15. * encode strings.
  16. *
  17. * @param string $source
  18. * a string to operate on.
  19. * @return string
  20. * the encoded version of the string.
  21. */
  22. public static function sanitize($source) {
  23. $source = htmlspecialchars($source, ENT_COMPAT, 'UTF-8', false);
  24. return $source;
  25. }
  26. /**
  27. * Revert any html entities to their character equivalents.
  28. *
  29. * @param string $str
  30. * a string to operate on
  31. * @return string
  32. * the decoded version of the string
  33. */
  34. public static function reverse_sanitize($str){
  35. return htmlspecialchars_decode($str, ENT_COMPAT);
  36. }
  37. /**
  38. * Validate a string against a set of regular expressions.
  39. *
  40. * @param array|string $string
  41. * string to operate on
  42. * @param array|string $rule
  43. * a single rule or array of rules
  44. * @return boolean
  45. * false if any of the rules in $rule do not match any of the strings in
  46. * `$string`, return true otherwise.
  47. */
  48. public static function validateString($string, $rule){
  49. if(!is_array($rule) && $rule == '') return true;
  50. if(!is_array($string) && $string == '') return true;
  51. if(!is_array($rule)) $rule = array($rule);
  52. if(!is_array($string)) $string = array($string);
  53. foreach($rule as $r){
  54. foreach($string as $s){
  55. if(!preg_match($r, $s)) return false;
  56. }
  57. }
  58. return true;
  59. }
  60. /**
  61. * Replace the tabs with spaces in the input string.
  62. *
  63. * @param string $string
  64. * the string in which to replace the tabs with spaces.
  65. * @param integer $spaces (optional)
  66. * the number of spaces to replace each tab with. This argument is optional
  67. * with a default of 4.
  68. * @return string
  69. * the resulting string.
  70. */
  71. public static function tabsToSpaces($string, $spaces=4){
  72. return str_replace("\t", str_pad(NULL, $spaces), $string);
  73. }
  74. /**
  75. * Checks an xml document for well-formedness.
  76. *
  77. * @param string $data
  78. * filename, xml document as a string, or arbitrary string
  79. * @param pointer &$errors
  80. * pointer to an array which will contain any validation errors
  81. * @param boolean $isFile (optional)
  82. * if this is true, the method will attempt to read from a file, `$data`
  83. * instead.
  84. * @param mixed $xsltProcessor (optional)
  85. * if set, the validation will be done using this XSLT processor rather
  86. * than the built in XML parser. the default is null.
  87. * @param string $encoding (optional)
  88. * if no XML header is expected, than this should be set to match the
  89. * encoding of the XML
  90. * @return boolean
  91. * true if there are no errors in validating the XML, false otherwise.
  92. */
  93. public static function validateXML($data, &$errors, $isFile=true, $xsltProcessor=NULL, $encoding='UTF-8') {
  94. $_parser = null;
  95. $_data = null;
  96. $_vals = array();
  97. $_index = array();
  98. $_data = ($isFile) ? file_get_contents($data) : $data;
  99. $_data = preg_replace('/<!DOCTYPE[-.:"\'\/\\w\\s]+>/', NULL, $_data);
  100. if(strpos($_data, '<?xml') === false){
  101. $_data = '<?xml version="1.0" encoding="'.$encoding.'"?><rootelement>'.$_data.'</rootelement>';
  102. }
  103. if(is_object($xsltProcessor)){
  104. $xsl = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  105. <xsl:template match="/"></xsl:template>
  106. </xsl:stylesheet>';
  107. $xsltProcessor->process($_data, $xsl, array());
  108. if($xsltProcessor->isErrors()) {
  109. $errors = $xsltProcessor->getError(true);
  110. return false;
  111. }
  112. }
  113. else{
  114. $_parser = xml_parser_create();
  115. xml_parser_set_option($_parser, XML_OPTION_SKIP_WHITE, 0);
  116. xml_parser_set_option($_parser, XML_OPTION_CASE_FOLDING, 0);
  117. if(!xml_parse($_parser, $_data)){
  118. $errors = array('error' => xml_get_error_code($_parser) . ': ' . xml_error_string(xml_get_error_code($_parser)),
  119. 'col' => xml_get_current_column_number($_parser),
  120. 'line' => (xml_get_current_line_number($_parser) - 2));
  121. return false;
  122. }
  123. xml_parser_free($_parser);
  124. }
  125. return true;
  126. }
  127. /**
  128. * Check that a string is a valid URL.
  129. *
  130. * @param string $url
  131. * string to operate on
  132. * @return string
  133. * a blank string or a valid URL
  134. */
  135. public static function validateURL($url = null){
  136. $url = trim($url);
  137. if(is_null($url) || $url == '') return $url;
  138. if(!preg_match('#^http[s]?:\/\/#i', $url)){
  139. $url = 'http://' . $url;
  140. }
  141. include(TOOLKIT . '/util.validators.php');
  142. if(!preg_match($validators['URI'], $url)){
  143. $url = '';
  144. }
  145. return $url;
  146. }
  147. /**
  148. * Strip any slashes from all array values.
  149. *
  150. * @param array &$arr
  151. * Pointer to an array to operate on. Can be multi-dimensional.
  152. */
  153. public static function cleanArray(array &$arr) {
  154. foreach($arr as $k => $v){
  155. if(is_array($v))
  156. self::cleanArray($arr[$k]);
  157. else
  158. $arr[$k] = stripslashes($v);
  159. }
  160. }
  161. /**
  162. * Flatten the input array. Any elements of the input array that are
  163. * themselves arrays will be removed and the contents of the removed array
  164. * inserted in its place. The keys for the inserted values will be the
  165. * concatenation of the keys in the original arrays in which it was embedded.
  166. * The elements of the path are separated by periods (.). For example,
  167. * given the following nested array structure:
  168. * `
  169. * array(1 =>
  170. * array('key' => 'value'),
  171. * 2 =>
  172. * array('key2' => 'value2', 'key3' => 'value3')
  173. * )
  174. * `
  175. * will flatten to:
  176. * `array('1.key' => 'value', '2.key2' => 'value2', '2.key3' => 'value3')`
  177. *
  178. * @param array &$source
  179. * The array to flatten, passed by reference
  180. * @param array &$output (optional)
  181. * The array in which to store the flattened input, passed by reference.
  182. * if this is not provided then a new array will be created.
  183. * @param string $path (optional)
  184. * the current prefix of the keys to insert into the output array. this
  185. * defaults to null.
  186. */
  187. public static function flattenArray(array &$source, &$output = null, $path = null) {
  188. if (is_null($output)) $output = array();
  189. foreach ($source as $key => $value) {
  190. if (is_int($key)) $key = (string)($key + 1);
  191. if (!is_null($path)) $key = $path . '.' . (string)$key;
  192. if (is_array($value)) self::flattenArray($value, $output, $key);
  193. else $output[$key] = $value;
  194. }
  195. $source = $output;
  196. }
  197. /**
  198. * Flatten the input array. Any elements of the input array that are
  199. * themselves arrays will be removed and the contents of the removed array
  200. * inserted in its place. The keys for the inserted values will be the
  201. * concatenation of the keys in the original arrays in which it was embedded.
  202. * The elements of the path are separated by colons (:). For example, given
  203. * the following nested array structure:
  204. * `
  205. * array(1 =>
  206. * array('key' => 'value'),
  207. * 2 =>
  208. * array('key2' => 'value2', 'key3' => 'value3')
  209. * )
  210. * `
  211. * will flatten to:
  212. * `array('1:key' => 'value', '2:key2' => 'value2', '2:key3' => 'value3')`
  213. *
  214. *
  215. * @param array &$output
  216. * The array in which to store the flattened input, passed by reference.
  217. * @param array &$source
  218. * The array to flatten, passed by reference
  219. * @param string $path
  220. * the current prefix of the keys to insert into the output array.
  221. */
  222. protected static function flattenArraySub(array &$output, array &$source, $path) {
  223. foreach ($source as $key => $value) {
  224. $key = $path . ':' . $key;
  225. if (is_array($value)) self::flattenArraySub($output, $value, $key);
  226. else $output[$key] = $value;
  227. }
  228. }
  229. /**
  230. * Given a string, this will clean it for use as a Symphony handle. Preserves multi-byte characters.
  231. *
  232. * @since Symphony 2.2.1
  233. * @param string $string
  234. * String to be cleaned up
  235. * @param integer $max_length
  236. * The maximum number of characters in the handle
  237. * @param string $delim
  238. * All non-valid characters will be replaced with this
  239. * @param boolean $uriencode
  240. * Force the resultant string to be uri encoded making it safe for URLs
  241. * @param array $additional_rule_set
  242. * An array of REGEX patterns that should be applied to the `$string`. This
  243. * occurs after the string has been trimmed and joined with the `$delim`
  244. * @return string
  245. * Returns resultant handle
  246. */
  247. public static function createHandle($string, $max_length=255, $delim='-', $uriencode=false, $additional_rule_set=NULL) {
  248. $max_length = intval($max_length);
  249. // Strip out any tag
  250. $string = strip_tags($string);
  251. // Remove punctuation
  252. $string = preg_replace('/[\\.\'"]+/', NULL, $string);
  253. // Trim it
  254. if($max_length > 0) $string = General::limitWords($string, $max_length);
  255. // Replace spaces (tab, newline etc) with the delimiter
  256. $string = preg_replace('/[\s]+/', $delim, $string);
  257. // Find all legal characters
  258. preg_match_all('/[^<>?@:!-\/\[-`;‘’…]+/u', $string, $matches);
  259. // Join only legal character with the $delim
  260. $string = implode($delim, $matches[0]);
  261. // Allow for custom rules
  262. if(is_array($additional_rule_set) && !empty($additional_rule_set)) {
  263. foreach($additional_rule_set as $rule => $replacement) $string = preg_replace($rule, $replacement, $string);
  264. }
  265. // Remove leading or trailing delim characters
  266. $string = trim($string, $delim);
  267. // Encode it for URI use
  268. if($uriencode) $string = urlencode($string);
  269. // Make it lowercase
  270. $string = strtolower($string);
  271. return $string;
  272. }
  273. /**
  274. * Given a string, this will clean it for use as a filename. Preserves multi-byte characters.
  275. *
  276. * @since Symphony 2.2.1
  277. * @param string $string
  278. * String to be cleaned up
  279. * @param string $delim
  280. * All non-valid characters will be replaced with this
  281. * @return string
  282. * Returns created filename
  283. */
  284. public static function createFilename($string, $delim='-') {
  285. // Strip out any tag
  286. $string = strip_tags($string);
  287. // Find all legal characters
  288. $count = preg_match_all('/[\p{L}\w:;.,+=~]+/u', $string, $matches);
  289. if($count <= 0 || $count == false) {
  290. preg_match_all('/[\w:;.,+=~]+/', $string, $matches);
  291. }
  292. // Join only legal character with the $delim
  293. $string = implode($delim, $matches[0]);
  294. // Remove leading or trailing delim characters
  295. $string = trim($string, $delim);
  296. // Make it lowercase
  297. $string = strtolower($string);
  298. return $string;
  299. }
  300. /**
  301. * Extract the first `$val` characters of the input string. If `$val`
  302. * is larger than the length of the input string then the original
  303. * input string is returned.
  304. *
  305. * @param string $str
  306. * the string to operate on
  307. * @param integer $val
  308. * the number to compare lengths with
  309. * @return string|boolean
  310. * the resulting string or false on failure.
  311. */
  312. public static function substrmin($str, $val){
  313. return(substr($str, 0, min(strlen($str), $val)));
  314. }
  315. /**
  316. * Extract the first `$val` characters of the input string. If
  317. * `$val` is larger than the length of the input string then
  318. * the original input string is returned
  319. *
  320. * @param string $str
  321. * the string to operate on
  322. * @param integer $val
  323. * the number to compare lengths with
  324. * @return string|boolean
  325. * the resulting string or false on failure.
  326. */
  327. public static function substrmax($str, $val){
  328. return(substr($str, 0, max(strlen($str), $val)));
  329. }
  330. /**
  331. * Extract the last `$num` characters from a string.
  332. *
  333. * @param string $str
  334. * the string to extract the characters from.
  335. * @param integer $num
  336. * the number of characters to extract.
  337. * @return string|boolean
  338. * a string containing the last `$num` characters of the
  339. * input string, or false on failure.
  340. */
  341. public static function right($str, $num){
  342. $str = substr($str, strlen($str)-$num, $num);
  343. return $str;
  344. }
  345. /**
  346. * Extract the first `$num` characters from a string.
  347. *
  348. * @param string $str
  349. * the string to extract the characters from.
  350. * @param integer $num
  351. * the number of characters to extract.
  352. * @return string|boolean
  353. * a string containing the last `$num` characters of the
  354. * input string, or false on failure.
  355. */
  356. public static function left($str, $num){
  357. $str = substr($str, 0, $num);
  358. return $str;
  359. }
  360. /**
  361. * Create all the directories as specified by the input path. If the current
  362. * directory already exists, this function will return true.
  363. *
  364. * @param string $path
  365. * the path containing the directories to create.
  366. * @param integer $mode (optional)
  367. * the permissions (in octal) of the directories to create. Defaults to 0755
  368. * @param boolean $silent (optional)
  369. * true if an exception should be raised if an error occurs, false
  370. * otherwise. this defaults to true.
  371. * @throws Exception
  372. * @return boolean
  373. */
  374. public static function realiseDirectory($path, $mode = 0755, $silent = true){
  375. if(is_dir($path)) return true;
  376. try {
  377. $current_umask = umask(0);
  378. $success = @mkdir($path, intval($mode, 8), true);
  379. umask($current_umask);
  380. return $success;
  381. }
  382. catch(Exception $ex) {
  383. if($silent === false){
  384. throw new Exception(__('Unable to create path - %s', array($path)));
  385. }
  386. return false;
  387. }
  388. }
  389. /**
  390. * Recursively deletes all files and directories given a directory. This
  391. * function has two path. This function optionally takes a `$silent` parameter,
  392. * which when `false` will throw an `Exception` if there is an error deleting a file
  393. * or folder.
  394. *
  395. * @since Symphony 2.3
  396. * @param string $dir
  397. * the path of the directory to delete
  398. * @param boolean $silent (optional)
  399. * true if an exception should be raised if an error occurs, false
  400. * otherwise. this defaults to true.
  401. * @throws Exception
  402. * @return boolean
  403. */
  404. public static function deleteDirectory($dir, $silent = true) {
  405. try {
  406. if (!file_exists($dir)) return true;
  407. if (!is_dir($dir)) return unlink($dir);
  408. foreach (scandir($dir) as $item) {
  409. if ($item == '.' || $item == '..') continue;
  410. if (!self::deleteDirectory($dir.DIRECTORY_SEPARATOR.$item)) return false;
  411. }
  412. return rmdir($dir);
  413. }
  414. catch(Exception $ex) {
  415. if($silent === false){
  416. throw new Exception(__('Unable to remove - %s', array($dir)));
  417. }
  418. return false;
  419. }
  420. }
  421. /**
  422. * Search a multi-dimensional array for a value.
  423. *
  424. * @param mixed $needle
  425. * the value to search for.
  426. * @param array $haystack
  427. * the multi-dimensional array to search.
  428. * @return boolean
  429. * true if `$needle` is found in `$haystack`.
  430. * true if `$needle` == `$haystack`.
  431. * true if `$needle` is found in any of the arrays contained within `$haystack`.
  432. * false otherwise.
  433. */
  434. public static function in_array_multi($needle, $haystack){
  435. if($needle == $haystack) return true;
  436. if(is_array($haystack)){
  437. foreach($haystack as $key => $val){
  438. if(is_array($val)){
  439. if(self::in_array_multi($needle, $val)) return true;
  440. }
  441. elseif(!strcmp($needle, $key) || !strcmp($needle, $val)){
  442. return true;
  443. }
  444. }
  445. }
  446. return false;
  447. }
  448. /**
  449. * Search an array for multiple values.
  450. *
  451. * @param array $needles
  452. * the values to search the `$haystack` for.
  453. * @param array $haystack
  454. * the in which to search for the `$needles`
  455. * @return boolean
  456. * true if any of the `$needles` are in `$haystack`,
  457. * false otherwise.
  458. */
  459. public static function in_array_all($needles, $haystack){
  460. foreach($needles as $n){
  461. if(!in_array($n, $haystack)) return false;
  462. }
  463. return true;
  464. }
  465. /**
  466. * Transform a multi-dimensional array to a flat array. The input array
  467. * is expected to conform to the structure of the `$_FILES` variable.
  468. *
  469. * @param array $filedata
  470. * the raw `$_FILES` data structured array
  471. * @return array
  472. * the flattened array.
  473. */
  474. public static function processFilePostData($filedata){
  475. $result = array();
  476. foreach($filedata as $key => $data){
  477. foreach($data as $handle => $value){
  478. if(is_array($value)){
  479. foreach($value as $index => $pair){
  480. if(!is_array($result[$handle][$index])) $result[$handle][$index] = array();
  481. if(!is_array($pair)) $result[$handle][$index][$key] = $pair;
  482. else $result[$handle][$index][array_pop(array_keys($pair))][$key] = array_pop(array_values($pair));
  483. }
  484. }
  485. else $result[$handle][$key] = $value;
  486. }
  487. }
  488. return $result;
  489. }
  490. /**
  491. * Merge `$_POST` with `$_FILES` to produce a flat array of the contents
  492. * of both. If there is no merge_file_post_data function defined then
  493. * such a function is created. This is necessary to overcome PHP's ability
  494. * to handle forms. This overcomes PHP's convoluted `$_FILES` structure
  495. * to make it simpler to access `multi-part/formdata`.
  496. *
  497. * @return array
  498. * a flat array containing the flattened contents of both `$_POST` and
  499. * `$_FILES`.
  500. */
  501. public static function getPostData() {
  502. if (!function_exists('merge_file_post_data')) {
  503. function merge_file_post_data($type, array $file, &$post) {
  504. foreach ($file as $key => $value) {
  505. if (!isset($post[$key])) $post[$key] = array();
  506. if (is_array($value)) merge_file_post_data($type, $value, $post[$key]);
  507. else $post[$key][$type] = $value;
  508. }
  509. }
  510. }
  511. $files = array(
  512. 'name' => array(),
  513. 'type' => array(),
  514. 'tmp_name' => array(),
  515. 'error' => array(),
  516. 'size' => array()
  517. );
  518. $post = $_POST;
  519. if(is_array($_FILES) && !empty($_FILES)){
  520. foreach ($_FILES as $key_a => $data_a) {
  521. if(!is_array($data_a)) continue;
  522. foreach ($data_a as $key_b => $data_b) {
  523. $files[$key_b][$key_a] = $data_b;
  524. }
  525. }
  526. }
  527. foreach ($files as $type => $data) {
  528. merge_file_post_data($type, $data, $post);
  529. }
  530. return $post;
  531. }
  532. /**
  533. * Find the next available index in an array. Works best with numeric keys.
  534. * The next available index is the minimum integer such that the array does
  535. * not have a mapping for that index. Uses the increment operator on the
  536. * index type of the input array, whatever that may do.
  537. *
  538. * @param array $array
  539. * the array to find the next index for.
  540. * @param mixed $seed (optional)
  541. * the object with which the search for an empty index is initialized. this
  542. * defaults to null.
  543. * @return integer
  544. * the minimum empty index into the input array.
  545. */
  546. public static function array_find_available_index($array, $seed=NULL){
  547. if(!is_null($seed)) $index = $seed;
  548. else{
  549. $keys = array_keys($array);
  550. sort($keys);
  551. $index = array_pop($keys);
  552. }
  553. if(isset($array[$index])){
  554. do{
  555. $index++;
  556. }while(isset($array[$index]));
  557. }
  558. return $index;
  559. }
  560. /**
  561. * Filter the duplicate values from an array into a new array, optionally
  562. * ignoring the case of the values (assuming they are strings?). A new array
  563. * is returned, the input array is left unchanged.
  564. *
  565. * @param array $array
  566. * the array to filter.
  567. * @param boolean $ignore_case
  568. * true if the case of the values in the array should be ignored, false otherwise.
  569. * @return array
  570. * a new array containing only the unique elements of the input array.
  571. */
  572. public static function array_remove_duplicates(array $array, $ignore_case=false){
  573. return ($ignore_case == true ? self::array_iunique($array) : array_unique($array));
  574. }
  575. /**
  576. * Test whether a value is in an array based on string comparison, ignoring
  577. * the case of the values.
  578. *
  579. * @param mixed $needle
  580. * the object to search the array for.
  581. * @param array $haystack
  582. * the array to search for the `$needle`.
  583. * @return boolean
  584. * true if the `$needle` is in the `$haystack`, false otherwise.
  585. */
  586. public static function in_iarray($needle, array $haystack){
  587. foreach($haystack as $key => $value){
  588. if(strcasecmp($value, $needle) == 0) return true;
  589. }
  590. return false;
  591. }
  592. /**
  593. * Filter the input array for duplicates, treating each element in the array
  594. * as a string and comparing them using a case insensitive comparison function.
  595. *
  596. * @param array $array
  597. * the array to filter.
  598. * @return array
  599. * a new array containing only the unique elements of the input array.
  600. */
  601. public static function array_iunique(array $array){
  602. $tmp = array();
  603. foreach($array as $key => $value){
  604. if(!self::in_iarray($value, $tmp)){
  605. $tmp[$key] = $value;
  606. }
  607. }
  608. return $tmp;
  609. }
  610. /**
  611. * Function recursively apply a function to an array's values.
  612. * This will not touch the keys, just the values.
  613. *
  614. * @since Symphony 2.2
  615. * @param string $function
  616. * @param array $array
  617. * @return array
  618. * a new array with all the values passed through the given `$function`
  619. */
  620. public static function array_map_recursive($function, array $array) {
  621. $tmp = array();
  622. foreach($array as $key => $value) {
  623. if(is_array($value)) {
  624. $tmp[$key] = self::array_map_recursive($function, $value);
  625. }
  626. else {
  627. $tmp[$key] = call_user_func($function, $value);
  628. }
  629. }
  630. return $tmp;
  631. }
  632. /**
  633. * Convert an array into an XML fragment and append it to an existing
  634. * XML element. Any arrays contained as elements in the input array will
  635. * also be recursively formatted and appended to the input XML fragment.
  636. * The input XML element will be modified as a result of calling this.
  637. *
  638. * @param XMLElement $parent
  639. * the XML element to append the formatted array data to.
  640. * @param array $data
  641. * the array to format and append to the XML fragment.
  642. * @param boolean $validate
  643. * true if the formatted array data should be validated as it is
  644. * constructed, false otherwise.
  645. */
  646. public static function array_to_xml(XMLElement $parent, array $data, $validate=false) {
  647. foreach ($data as $element_name => $value) {
  648. if (empty($value)) continue;
  649. if (is_int($element_name)) {
  650. $child = new XMLElement('item');
  651. $child->setAttribute('index', $element_name + 1);
  652. }
  653. else {
  654. $child = new XMLElement($element_name, null, array(), true);
  655. }
  656. if(is_array($value)) {
  657. self::array_to_xml($child, $value);
  658. if($child->getNumberOfChildren() == 0) continue;
  659. }
  660. else if($validate == true && !self::validateXML(self::sanitize($value), $errors, false, new XSLTProcess)) {
  661. continue;
  662. }
  663. else {
  664. $child->setValue(self::sanitize($value));
  665. }
  666. $parent->appendChild($child);
  667. }
  668. }
  669. /**
  670. * Create a file at the input path with the (optional) input permissions
  671. * with the input content. This function will ignore errors in opening,
  672. * writing, closing and changing the permissions of the resulting file.
  673. * If opening or writing the file fail then this will return false.
  674. *
  675. * @param string $file
  676. * the path of the file to write.
  677. * @param mixed $data
  678. * the data to write to the file.
  679. * @param integer|null $perm (optional)
  680. * the permissions as an octal number to set set on the resulting file.
  681. * this defaults to 0644 (if omitted or set to null)
  682. * @param string $mode (optional)
  683. * the mode that the file should be opened with, defaults to 'w'. See modes
  684. * at http://php.net/manual/en/function.fopen.php
  685. * @param boolean $trim (optional)
  686. * removes tripple linebreaks
  687. * @return boolean
  688. * true if the file is successfully opened, written to, closed and has the
  689. * required permissions set. false, otherwise.
  690. */
  691. public static function writeFile($file, $data, $perm = 0644, $mode = 'w', $trim = false){
  692. if(
  693. (!is_writable(dirname($file)) || !is_readable(dirname($file))) // Folder
  694. || (file_exists($file) && (!is_readable($file) || !is_writable($file))) // File
  695. ) {
  696. return false;
  697. }
  698. if(!$handle = fopen($file, $mode)) {
  699. return false;
  700. }
  701. if($trim === true) {
  702. $data = preg_replace("/(" . PHP_EOL . "(\t+)?){2,}" . PHP_EOL . "/", PHP_EOL . PHP_EOL, trim($data));
  703. }
  704. if(fwrite($handle, $data, strlen($data)) === false) {
  705. return false;
  706. }
  707. fclose($handle);
  708. try {
  709. if(is_null($perm)) $perm = 0644;
  710. chmod($file, intval($perm, 8));
  711. }
  712. catch(Exception $ex) {
  713. // If we can't chmod the file, this is probably because our host is
  714. // running PHP with a different user to that of the file. Although we
  715. // can delete the file, create a new one and then chmod it, we run the
  716. // risk of losing the file as we aren't saving it anywhere. For the immediate
  717. // future, atomic saving isn't needed by Symphony and it's recommended that
  718. // if your extension require this logic, it uses it's own function rather
  719. // than this 'General' one.
  720. return true;
  721. }
  722. return true;
  723. }
  724. /**
  725. * Delete a file at a given path, silently ignoring errors depending
  726. * on the value of the input variable $silent.
  727. *
  728. * @param string $file
  729. * the path of the file to delete
  730. * @param boolean $silent (optional)
  731. * true if an exception should be raised if an error occurs, false
  732. * otherwise. this defaults to true.
  733. * @throws Exception
  734. * @return boolean
  735. * true if the file is successfully unlinked, if the unlink fails and
  736. * silent is set to true then an exception is thrown. if the unlink
  737. * fails and silent is set to false then this returns false.
  738. */
  739. public static function deleteFile($file, $silent=true){
  740. try {
  741. return unlink($file);
  742. }
  743. catch(Exception $ex) {
  744. if($silent == false){
  745. throw new Exception(__('Unable to remove file - %s', array($file)));
  746. }
  747. return false;
  748. }
  749. }
  750. /**
  751. * Extract the file extension from the input file path.
  752. *
  753. * @param string $file
  754. * the path of the file to extract the extension of.
  755. * @return array
  756. * an array with a single key 'extension' and a value of the extension
  757. * of the input path.
  758. */
  759. public static function getExtension($file){
  760. return pathinfo($file, PATHINFO_EXTENSION);
  761. }
  762. /**
  763. * Gets mime type of a file.
  764. *
  765. * For email attachments, the mime type is very important.
  766. * Uses the PHP 5.3 function `finfo_open` when available, otherwise falls
  767. * back to using a mapping of known of common mimetypes. If no matches
  768. * are found `application/octet-stream` will be returned.
  769. *
  770. * @author Michael Eichelsdoerfer
  771. * @author Huib Keemink
  772. * @param string $file
  773. * @return string MIMEtype
  774. */
  775. public function getMimeType($file) {
  776. if (!empty($file)) {
  777. // in PHP 5.3 we can use 'finfo'
  778. if (PHP_VERSION_ID >= 50300 && function_exists('finfo_open')) {
  779. $finfo = finfo_open(FILEINFO_MIME_TYPE);
  780. $mime_type = finfo_file($finfo, $file);
  781. finfo_close($finfo);
  782. }
  783. /**
  784. * fallback
  785. * this may be removed when Symphony requires PHP 5.3
  786. */
  787. else{
  788. // A few mimetypes to "guess" using the file extension.
  789. $mimetypes = array(
  790. 'txt' => 'text/plain',
  791. 'csv' => 'text/csv',
  792. 'pdf' => 'application/pdf',
  793. 'doc' => 'application/msword',
  794. 'docx' => 'application/msword',
  795. 'xls' => 'application/vnd.ms-excel',
  796. 'ppt' => 'application/vnd.ms-powerpoint',
  797. 'eps' => 'application/postscript',
  798. 'zip' => 'application/zip',
  799. 'gif' => 'image/gif',
  800. 'jpg' => 'image/jpeg',
  801. 'jpeg' => 'image/jpeg',
  802. 'png' => 'image/png',
  803. 'mp3' => 'audio/mpeg',
  804. 'mp4a' => 'audio/mp4',
  805. 'aac' => 'audio/x-aac',
  806. 'aif' => 'audio/x-aiff',
  807. 'aiff' => 'audio/x-aiff',
  808. 'wav' => 'audio/x-wav',
  809. 'wma' => 'audio/x-ms-wma',
  810. 'mpeg' => 'video/mpeg',
  811. 'mpg' => 'video/mpeg',
  812. 'mp4' => 'video/mp4',
  813. 'mov' => 'video/quicktime',
  814. 'avi' => 'video/x-msvideo',
  815. 'wmv' => 'video/x-ms-wmv',
  816. );
  817. $extension = substr(strrchr($file, '.'), 1);
  818. if($mimetypes[strtolower($extension)] != null){
  819. $mime_type = $mimetypes[$extension];
  820. }
  821. else{
  822. $mime_type = 'application/octet-stream';
  823. }
  824. }
  825. return $mime_type;
  826. }
  827. return false;
  828. }
  829. /**
  830. * Construct a multi-dimensional array that reflects the directory
  831. * structure of a given path.
  832. *
  833. * @param string $dir (optional)
  834. * the path of the directory to construct the multi-dimensional array
  835. * for. this defaults to '.'.
  836. * @param string $filter (optional)
  837. * A regular expression to filter the directories. This is positive filter, ie.
  838. * if the filter matches, the directory is included. Defaults to null.
  839. * @param boolean $recurse (optional)
  840. * true if sub-directories should be traversed and reflected in the
  841. * resulting array, false otherwise.
  842. * @param mixed $strip_root (optional)
  843. * If null, the full path to the file will be returned, otherwise the value
  844. * of `strip_root` will be removed from the file path.
  845. * @param array $exclude (optional)
  846. * ignore directories listed in this array. this defaults to an empty array.
  847. * @param boolean $ignore_hidden (optional)
  848. * ignore hidden directory (i.e.directories that begin with a period). this defaults
  849. * to true.
  850. * @return null|array
  851. * return the array structure reflecting the input directory or null if
  852. * the input directory is not actually a directory.
  853. */
  854. public static function listDirStructure($dir = '.', $filter = null, $recurse = true, $strip_root = null, $exclude = array(), $ignore_hidden = true) {
  855. if (!is_dir($dir)) return null;
  856. $files = array();
  857. foreach (scandir($dir) as $file) {
  858. if (
  859. ($file == '.' or $file == '..')
  860. or ($ignore_hidden and $file{0} == '.')
  861. or !is_dir("$dir/$file")
  862. or in_array($file, $exclude)
  863. or in_array("$dir/$file", $exclude)
  864. ) continue;
  865. if(!is_null($filter)) {
  866. if(!preg_match($filter, $file)) continue;
  867. }
  868. $files[] = rtrim(str_replace($strip_root, '', $dir), '/') ."/$file/";
  869. if ($recurse) {
  870. $files = @array_merge($files, self::listDirStructure("$dir/$file", $filter, $recurse, $strip_root, $exclude, $ignore_hidden));
  871. }
  872. }
  873. return $files;
  874. }
  875. /**
  876. * Construct a multi-dimensional array that reflects the directory
  877. * structure of a given path grouped into directory and file keys
  878. * matching any input constraints.
  879. *
  880. * @param string $dir (optional)
  881. * the path of the directory to construct the multi-dimensional array
  882. * for. this defaults to '.'.
  883. * @param array|string $filters (optional)
  884. * either a regular expression to filter the files by or an array of
  885. * files to include.
  886. * @param boolean $recurse (optional)
  887. * true if sub-directories should be traversed and reflected in the
  888. * resulting array, false otherwise.
  889. * @param string $sort (optional)
  890. * 'asc' if the resulting filelist array should be sorted, anything else otherwise.
  891. * this defaults to 'asc'.
  892. * @param mixed $strip_root (optional)
  893. * If null, the full path to the file will be returned, otherwise the value
  894. * of `strip_root` will be removed from the file path.
  895. * @param array $exclude (optional)
  896. * ignore files listed in this array. this defaults to an empty array.
  897. * @param boolean $ignore_hidden (optional)
  898. * ignore hidden files (i.e. files that begin with a period). this defaults
  899. * to true.
  900. * @return null|array
  901. * return the array structure reflecting the input directory or null if
  902. * the input directory is not actually a directory.
  903. */
  904. public static function listStructure($dir=".", $filters=array(), $recurse=true, $sort="asc", $strip_root=NULL, $exclude=array(), $ignore_hidden=true){
  905. if(!is_dir($dir)) return null;
  906. // Check to see if $filters is a string containing a regex, or an array of file types
  907. if(is_array($filters) && !empty($filters)) {
  908. $filter_type = 'file';
  909. }
  910. else if(is_string($filters)) {
  911. $filter_type = 'regex';
  912. }
  913. else {
  914. $filter_type = null;
  915. }
  916. $files = array();
  917. $prefix = str_replace($strip_root, '', $dir);
  918. if($prefix != "" && substr($prefix, -1) != "/") {
  919. $prefix .= "/";
  920. }
  921. $files['dirlist'] = array();
  922. $files['filelist'] = array();
  923. foreach(scandir($dir) as $file) {
  924. if (
  925. ($file == '.' or $file == '..')
  926. or ($ignore_hidden and $file{0} == '.')
  927. or in_array($file, $exclude)
  928. or in_array("$dir/$file", $exclude)
  929. ) continue;
  930. $dir = rtrim($dir, '/');
  931. if(is_dir("$dir/$file")) {
  932. if($recurse) {
  933. $files["$prefix$file/"] = self::listStructure("$dir/$file", $filters, $recurse, $sort, $strip_root, $exclude, $ignore_hidden);
  934. }
  935. $files['dirlist'][] = "$prefix$file/";
  936. }
  937. else if($filter_type == 'regex') {
  938. if(preg_match($filters, $file)){
  939. $files['filelist'][] = "$prefix$file";
  940. }
  941. }
  942. else if($filter_type == 'file') {
  943. if(in_array(self::getExtension($file), $filters)) {
  944. $files['filelist'][] = "$prefix$file";
  945. }
  946. }
  947. else if(is_null($filter_type)){
  948. $files['filelist'][] = "$prefix$file";
  949. }
  950. }
  951. if(is_array($files['filelist'])) {
  952. ($sort == 'desc') ? rsort($files['filelist']) : sort($files['filelist']);
  953. }
  954. return $files;
  955. }
  956. /**
  957. * Count the number of words in a string. Words are delimited by "spaces".
  958. * The characters included in the set of "spaces" are:
  959. * '&#x2002;', '&#x2003;', '&#x2004;', '&#x2005;',
  960. * '&#x2006;', '&#x2007;', '&#x2009;', '&#x200a;',
  961. * '&#x200b;', '&#x2002f;', '&#x205f;'
  962. * Any html/xml tags are first removed by strip_tags() and any included html
  963. * entities are decoded. The resulting string is then split by the above set
  964. * of spaces and the resulting size of the resulting array returned.
  965. *
  966. * @param string $string
  967. * the string from which to count the contained words.
  968. * @return integer
  969. * the number of words contained in the input string.
  970. */
  971. public static function countWords($string){
  972. $string = strip_tags($string);
  973. // Strip spaces:
  974. $string = html_entity_decode($string, ENT_NOQUOTES, 'UTF-8');
  975. $spaces = array(
  976. '&#x2002;', '&#x2003;', '&#x2004;', '&#x2005;',
  977. '&#x2006;', '&#x2007;', '&#x2009;', '&#x200a;',
  978. '&#x200b;', '&#x2002f;', '&#x205f;'
  979. );
  980. foreach ($spaces as &$space) {
  981. $space = html_entity_decode($space, ENT_NOQUOTES, 'UTF-8');
  982. }
  983. $string = str_replace($spaces, ' ', $string);
  984. $string = preg_replace('/[^\w\s]/i', '', $string);
  985. return str_word_count($string);
  986. }
  987. /**
  988. * Truncate a string to a given length. Newlines are replaced with `<br />`
  989. * html elements and html tags are removed from the string. If the resulting
  990. * string contains only spaces then null is returned. If the resulting string
  991. * is less than the input length then it is returned. If the option to
  992. * truncate the string to a space character is provided then the string is
  993. * truncated to the character prior to the last space in the string. Words
  994. * (contiguous non-' ' characters) are then removed from the end of the string
  995. * until the length of resulting string is within the input bound. Initial
  996. * and trailing spaces are removed. Provided the user requested an
  997. * ellipsis suffix and the resulting string is shorter than the input string
  998. * then the ellipses are appended to the result which is then returned.
  999. *
  1000. * @param string $string
  1001. * the string to truncate.
  1002. * @param integer maxChars (optional)
  1003. * the maximum length of the string to truncate the input string to. this
  1004. * defaults to 200 characters.
  1005. * @param boolean $appendHellip (optional)
  1006. * true if the ellipses should be appended to the result in circumstances
  1007. * where the result is shorter than the input string. false otherwise. this
  1008. * defaults to false.
  1009. * @return null|string
  1010. * if the resulting string contains only spaces then null is returned. otherwise
  1011. * a string that satisfies the input constraints.
  1012. */
  1013. public static function limitWords($string, $maxChars=200, $appendHellip=false) {
  1014. if($appendHellip) $maxChars -= 1;
  1015. $string = trim(strip_tags(nl2br($string)));
  1016. $original_length = strlen($string);
  1017. if($original_length == 0) return null;
  1018. elseif($original_length < $maxChars) return $string;
  1019. $string = trim(substr($string, 0, $maxChars));
  1020. $array = explode(' ', $string);
  1021. $result = '';
  1022. $length = 0;
  1023. while(!empty($array) && $length > $maxChars){
  1024. $length += strlen(array_pop($array)) + 1;
  1025. }
  1026. $result = implode(' ', $array);
  1027. if($appendHellip && strlen($result) < $original_length)
  1028. $result .= "&#8230;";
  1029. return($result);
  1030. }
  1031. /**
  1032. * Move a file from the source path to the destination path and name and
  1033. * set its permissions to the input permissions. This will ignore errors
  1034. * in the `is_uploaded_file()`, `move_uploaded_file()` and `chmod()` functions.
  1035. *
  1036. * @param string $dest_path
  1037. * the file path to which the source file is to be moved.
  1038. * @param string #dest_name
  1039. * the file name within the file path to which the source file is to be moved.
  1040. * @param string $tmp_name
  1041. * the full path name of the source file to move.
  1042. * @param integer $perm (optional)
  1043. * the permissions to apply to the moved file. this defaults to 0777.
  1044. * @return boolean
  1045. * true if the file was moved and its permissions set as required. false otherwise.
  1046. */
  1047. public static function uploadFile($dest_path, $dest_name, $tmp_name, $perm=0777){
  1048. // Upload the file
  1049. if(@is_uploaded_file($tmp_name)) {
  1050. $dest_path = rtrim($dest_path, '/') . '/';
  1051. // Try place the file in the correction location
  1052. if(@move_uploaded_file($tmp_name, $dest_path . $dest_name)){
  1053. chmod($dest_path . $dest_name, intval($perm, 8));
  1054. return true;
  1055. }
  1056. }
  1057. // Could not move the file
  1058. return false;
  1059. }
  1060. /**
  1061. * Format a number of bytes in human readable format. This will append MB as
  1062. * appropriate for values greater than 1,024*1,024, KB for values between
  1063. * 1,024 and 1,024*1,024-1 and bytes for values between 0 and 1,024.
  1064. *
  1065. * @param integer $file_size
  1066. * the number to format.
  1067. * @return string
  1068. * the formatted number.
  1069. */
  1070. public static function formatFilesize($file_size){
  1071. $file_size = intval($file_size);
  1072. if($file_size >= (1024 * 1024)) $file_size = number_format($file_size * (1 / (1024 * 1024)), 2) . ' MB';
  1073. elseif($file_size >= 1024) $file_size = intval($file_size * (1/1024)) . ' KB';
  1074. else $file_size = intval($file_size) . ' bytes';
  1075. return $file_size;
  1076. }
  1077. /**
  1078. * Construct an XML fragment that reflects the structure of the input timestamp.
  1079. *
  1080. * @param integer $timestamp
  1081. * the timestamp to construct the XML element from.
  1082. * @param string $element (optional)
  1083. * the name of the element to append to the namespace of the constructed XML.
  1084. * this defaults to "date".
  1085. * @param string $date_format (optional)
  1086. * the format to apply to the date, defaults to `Y-m-d`
  1087. * @param string $time_format (optional)
  1088. * the format to apply to the date, defaults to `H:i`
  1089. * @param string $namespace (optional)
  1090. * the namespace in which the resulting XML entity will reside. this defaults
  1091. * to null.
  1092. * @return boolean|XMLElement
  1093. * false if there is no XMLElement class on the system, the constructed XML element
  1094. * otherwise.
  1095. */
  1096. public static function createXMLDateObject($timestamp, $element='date', $date_format = 'Y-m-d', $time_format = 'H:i', $namespace = null){
  1097. if(!class_exists('XMLElement')) return false;
  1098. $xDate = new XMLElement(
  1099. (!is_null($namespace) ? $namespace . ':' : '') . $element,
  1100. DateTimeObj::get($date_format, $timestamp),
  1101. array(
  1102. 'iso' => DateTimeObj::get('c', $timestamp),
  1103. 'time' => DateTimeObj::get($time_format, $timestamp),
  1104. 'weekday' => DateTimeObj::get('N', $timestamp),
  1105. 'offset' => DateTimeObj::get('O', $timestamp)
  1106. )
  1107. );
  1108. return $xDate;
  1109. }
  1110. /**
  1111. * Construct an XML fragment that describes a pagination structure.
  1112. *
  1113. * @param integer $total_entries (optional)
  1114. * the total number of entries that this structure is paginating. this
  1115. * defaults to 0.
  1116. * @param integer $total_pages (optional)
  1117. * the total number of pages within the pagination structure. this defaults
  1118. * to 0.
  1119. * @param integer $entries_per_page (optional)
  1120. * the number of entries per page. this defaults to 1.
  1121. * @param integer $current_page (optional)
  1122. * the current page within the total number of pages within this pagination
  1123. * structure. this defaults to 1.
  1124. * @return XMLElement
  1125. * the constructed XML fragment.
  1126. */
  1127. public static function buildPaginationElement($total_entries=0, $total_pages=0, $entries_per_page=1, $current_page=1){
  1128. $pageinfo = new XMLElement('pagination');
  1129. $pageinfo->setAttribute('total-entries', $total_entries);
  1130. $pageinfo->setAttribute('total-pages', $total_pages);
  1131. $pageinfo->setAttribute('entries-per-page', $entries_per_page);
  1132. $pageinfo->setAttribute('current-page', $current_page);
  1133. return $pageinfo;
  1134. }
  1135. /**
  1136. * Uses `SHA1` or `MD5` to create a hash based on some input
  1137. * This function is currently very basic, but would allow
  1138. * future expansion. Salting the hash comes to mind.
  1139. *
  1140. * @param string $input
  1141. * the string to be hashed
  1142. * @param string $algorithm
  1143. * This function supports 'md5', 'sha1' and 'pbkdf2'. Any
  1144. * other algorithm will default to 'pbkdf2'.
  1145. * @return string
  1146. * the hashed string
  1147. */
  1148. public static function hash($input, $algorithm='sha1') {
  1149. switch($algorithm) {
  1150. case 'sha1':
  1151. return SHA1::hash($input);
  1152. case 'md5':
  1153. return MD5::hash($input);
  1154. case 'pbkdf2':
  1155. default:
  1156. return Crytography::hash($input, $algorithm);
  1157. }
  1158. }
  1159. /**
  1160. * Helper to cut down on variables' type check.
  1161. * Currently known types are the PHP defaults.
  1162. * Uses `is_XXX()` functions internally.
  1163. *
  1164. * @since Symphony 2.3
  1165. *
  1166. * @param array $params - an array of arrays containing variables info
  1167. *
  1168. * Array[
  1169. * $key1 => $value1
  1170. * $key2 => $value2
  1171. * ...
  1172. * ]
  1173. *
  1174. * $key = the name of the variable
  1175. * $value = Array[
  1176. * 'var' => the variable to check
  1177. * 'type' => enforced type. Must match the XXX part from an `is_XXX()` function
  1178. * 'optional' => boolean. If this is set, the default value of the variable must be null
  1179. * ]
  1180. *
  1181. * @throws InvalidArgumentException if validator doesn't exist.
  1182. * @throws InvalidArgumentException if variable type validation fails.
  1183. *
  1184. * @example
  1185. * $color = 'red';
  1186. * $foo = null;
  1187. * $bar = 21;
  1188. *
  1189. * General::ensureType(array(
  1190. * 'color' => array('var' => $color, 'type'=> 'string'), // success
  1191. * 'foo' => array('var' => $foo, 'type'=> 'int', 'optional' => true), // success
  1192. * 'bar' => array('var' => $bar, 'type'=> 'string') // fail
  1193. * ));
  1194. */
  1195. public static function ensureType(array $params){
  1196. foreach( $params as $name => $param ){
  1197. if( isset($param['optional']) && ($param['optional'] === true) ){
  1198. if( is_null($param['var']) ) continue;
  1199. // if not null, check it's type
  1200. }
  1201. // validate the validator
  1202. $validator = 'is_'.$param['type'];
  1203. if( !function_exists($validator) ){
  1204. throw new InvalidArgumentException(__('Enforced type `%1$s` for argument `$%2$s` does not match any known variable types.', array($param['type'], $name)));
  1205. }
  1206. // validate variable type
  1207. if( !call_user_func($validator, $param['var']) ){
  1208. throw new InvalidArgumentException(__('Argument `$%1$s` is not of type `%2$s`, given `%3$s`.', array($name, $param['type'], gettype($param['var']))));
  1209. }
  1210. }
  1211. }
  1212. /**
  1213. * Wrap a value in CDATA tags for XSL output of non encoded data, only
  1214. * if not already wrapped.
  1215. *
  1216. * @since Symphony 2.3.2
  1217. *
  1218. * @param string $value
  1219. * The string to wrap in CDATA
  1220. * @return string
  1221. * The wrapped string
  1222. */
  1223. public static function wrapInCDATA($value) {
  1224. if (empty($value)) {
  1225. return $value;
  1226. }
  1227. $startRegExp = '/^' . preg_quote(CDATA_BEGIN) . '/';
  1228. $endRegExp = '/' . preg_quote(CDATA_END) . '$/';
  1229. if (!preg_match($startRegExp, $value)) {
  1230. $value = CDATA_BEGIN . $value;
  1231. }
  1232. if (!preg_match($endRegExp, $value)) {
  1233. $value .= CDATA_END;
  1234. }
  1235. return $value;
  1236. }
  1237. /**
  1238. * Unwrap a value from CDATA tags to return the raw string
  1239. *
  1240. * @since Symphony 2.3.4
  1241. * @param string $value
  1242. * The string to unwrap from CDATA
  1243. * @return string
  1244. * The unwrapped string
  1245. */
  1246. public static function unwrapCDATA($value) {
  1247. return str_replace(array(CDATA_BEGIN, CDATA_END), '', $value);
  1248. }
  1249. }