PageRenderTime 64ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/com/misc.php

http://github.com/unirgy/buckyball
PHP | 3899 lines | 3041 code | 219 blank | 639 comment | 297 complexity | c856600b7d0b0471aede00c3723d951b MD5 | raw file
Possible License(s): LGPL-2.1, BSD-3-Clause
  1. <?php
  2. /**
  3. * Copyright 2011 Unirgy LLC
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. *
  17. * @package BuckyBall
  18. * @link http://github.com/unirgy/buckyball
  19. * @author Boris Gurvich <boris@unirgy.com>
  20. * @copyright (c) 2010-2012 Boris Gurvich
  21. * @license http://www.apache.org/licenses/LICENSE-2.0.html
  22. */
  23. /**
  24. * Utility class to parse and construct strings and data structures
  25. */
  26. class BUtil extends BClass
  27. {
  28. /**
  29. * IV for mcrypt operations
  30. *
  31. * @var string
  32. */
  33. protected static $_mcryptIV;
  34. /**
  35. * Encryption key from configuration (encrypt/key)
  36. *
  37. * @var string
  38. */
  39. protected static $_mcryptKey;
  40. /**
  41. * Default hash algorithm
  42. *
  43. * @var string default sha512 for strength and slowness
  44. */
  45. protected static $_hashAlgo = 'bcrypt';
  46. /**
  47. * Default number of hash iterations
  48. *
  49. * @var int
  50. */
  51. protected static $_hashIter = 3;
  52. /**
  53. * Default full hash string separator
  54. *
  55. * @var string
  56. */
  57. protected static $_hashSep = '$';
  58. /**
  59. * Default character pool for random and sequence strings
  60. *
  61. * Chars "c", "C" are ommited to avoid accidental obscene language
  62. * Chars "0", "1", "I" are removed to avoid leading 0 and ambiguity in print
  63. *
  64. * @var string
  65. */
  66. protected static $_defaultCharPool = '23456789abdefghijklmnopqrstuvwxyzABDEFGHJKLMNOPQRSTUVWXYZ';
  67. /**
  68. * Shortcut to help with IDE autocompletion
  69. *
  70. * @return BUtil
  71. */
  72. public static function i($new=false, array $args=array())
  73. {
  74. return BClassRegistry::i()->instance(__CLASS__, $args, !$new);
  75. }
  76. /**
  77. * Convert any data to JSON string
  78. *
  79. * $data can be BData instance, or array of BModel objects, will be automatically converted to array
  80. *
  81. * @param mixed $data
  82. * @return string
  83. */
  84. public static function toJson($data)
  85. {
  86. if (is_array($data) && is_object(current($data)) && current($data) instanceof BModel) {
  87. $data = BDb::many_as_array($data);
  88. } elseif (is_object($data) && $data instanceof BData) {
  89. $data = $data->as_array(true);
  90. }
  91. return json_encode($data);
  92. }
  93. /**
  94. * Parse JSON into PHP data
  95. *
  96. * @param string $json
  97. * @param bool $asObject if false will attempt to convert to array,
  98. * otherwise standard combination of objects and arrays
  99. */
  100. public static function fromJson($json, $asObject=false)
  101. {
  102. $obj = json_decode($json);
  103. return $asObject ? $obj : static::objectToArray($obj);
  104. }
  105. /**
  106. * Indents a flat JSON string to make it more human-readable.
  107. *
  108. * @param string $json The original JSON string to process.
  109. *
  110. * @return string Indented version of the original JSON string.
  111. */
  112. public static function jsonIndent($json)
  113. {
  114. $result = '';
  115. $pos = 0;
  116. $strLen = strlen($json);
  117. $indentStr = ' ';
  118. $newLine = "\n";
  119. $prevChar = '';
  120. $outOfQuotes = true;
  121. for ($i=0; $i<=$strLen; $i++) {
  122. // Grab the next character in the string.
  123. $char = substr($json, $i, 1);
  124. // Are we inside a quoted string?
  125. if ($char == '"' && $prevChar != '\\') {
  126. $outOfQuotes = !$outOfQuotes;
  127. // If this character is the end of an element,
  128. // output a new line and indent the next line.
  129. } else if(($char == '}' || $char == ']') && $outOfQuotes) {
  130. $result .= $newLine;
  131. $pos --;
  132. for ($j=0; $j<$pos; $j++) {
  133. $result .= $indentStr;
  134. }
  135. }
  136. // Add the character to the result string.
  137. $result .= $char;
  138. // If the last character was the beginning of an element,
  139. // output a new line and indent the next line.
  140. if (($char == ',' || $char == '{' || $char == '[') && $outOfQuotes) {
  141. $result .= $newLine;
  142. if ($char == '{' || $char == '[') {
  143. $pos ++;
  144. }
  145. for ($j = 0; $j < $pos; $j++) {
  146. $result .= $indentStr;
  147. }
  148. }
  149. $prevChar = $char;
  150. }
  151. return $result;
  152. }
  153. /**
  154. * Convert data to JavaScript string
  155. *
  156. * Notable difference from toJson: allows raw function callbacks
  157. *
  158. * @param mixed $val
  159. * @return string
  160. */
  161. public static function toJavaScript($val)
  162. {
  163. if (is_null($val)) {
  164. return 'null';
  165. } elseif (is_bool($val)) {
  166. return $val ? 'true' : 'false';
  167. } elseif (is_string($val)) {
  168. if (preg_match('#^\s*function\s*\(#', $val)) {
  169. return $val;
  170. } else {
  171. return "'".addslashes($val)."'";
  172. }
  173. } elseif (is_int($val) || is_float($val)) {
  174. return $val;
  175. } elseif ($val instanceof BValue) {
  176. return $val->toPlain();
  177. } elseif (($isObj = is_object($val)) || is_array($val)) {
  178. $out = array();
  179. if (!empty($val) && ($isObj || array_keys($val) !== range(0, count($val)-1))) { // assoc?
  180. foreach ($val as $k=>$v) {
  181. $out[] = "'".addslashes($k)."':".static::toJavaScript($v);
  182. }
  183. return '{'.join(',', $out).'}';
  184. } else {
  185. foreach ($val as $k=>$v) {
  186. $out[] = static::toJavaScript($v);
  187. }
  188. return '['.join(',', $out).']';
  189. }
  190. }
  191. return '"UNSUPPORTED TYPE"';
  192. }
  193. public static function toRss($data)
  194. {
  195. $lang = !empty($data['language']) ? $data['language'] : 'en-us';
  196. $ttl = !empty($data['ttl']) ? (int)$data['ttl'] : 40;
  197. $descr = !empty($data['description']) ? $data['description'] : $data['title'];
  198. $xml = '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel>'
  199. .'<title><![CDATA['.$data['title'].']]></title><link><![CDATA['.$data['link'].']]></link>'
  200. .'<description><![CDATA['.$descr.']]></description><language><![CDATA['.$lang.']]></language><ttl>'.$ttl.'</ttl>';
  201. foreach ($data['items'] as $item) {
  202. if (!is_numeric($item['pubDate'])) {
  203. $item['pubDate'] = strtotime($item['pubDate']);
  204. }
  205. if (empty($item['guid'])) {
  206. $item['guid'] = $item['link'];
  207. }
  208. $xml .= '<item><title><![CDATA['.$item['title'].']]></title>'
  209. .'<description><![CDATA['.$item['description'].']]></description>'
  210. .'<pubDate>'.date('r', $item['pubDate']).'</pubDate>'
  211. .'<guid><![CDATA['.$item['guid'].']]></guid><link><![CDATA['.$item['link'].']]></link></item>';
  212. }
  213. $xml .= '</channel></rss>';
  214. return $xml;
  215. }
  216. /**
  217. * Convert object to array recursively
  218. *
  219. * @param object $d
  220. * @return array
  221. */
  222. public static function objectToArray($d)
  223. {
  224. if (is_object($d)) {
  225. $d = get_object_vars($d);
  226. }
  227. if (is_array($d)) {
  228. return array_map('BUtil::objectToArray', $d);
  229. }
  230. return $d;
  231. }
  232. /**
  233. * Convert array to object
  234. *
  235. * @param mixed $d
  236. * @return object
  237. */
  238. public static function arrayToObject($d)
  239. {
  240. if (is_array($d)) {
  241. return (object) array_map('BUtil::objectToArray', $d);
  242. }
  243. return $d;
  244. }
  245. /**
  246. * version of sprintf for cases where named arguments are desired (php syntax)
  247. *
  248. * with sprintf: sprintf('second: %2$s ; first: %1$s', '1st', '2nd');
  249. *
  250. * with sprintfn: sprintfn('second: %second$s ; first: %first$s', array(
  251. * 'first' => '1st',
  252. * 'second'=> '2nd'
  253. * ));
  254. *
  255. * @see http://www.php.net/manual/en/function.sprintf.php#94608
  256. * @param string $format sprintf format string, with any number of named arguments
  257. * @param array $args array of [ 'arg_name' => 'arg value', ... ] replacements to be made
  258. * @return string|false result of sprintf call, or bool false on error
  259. */
  260. public static function sprintfn($format, $args = array())
  261. {
  262. $args = (array)$args;
  263. // map of argument names to their corresponding sprintf numeric argument value
  264. $arg_nums = array_slice(array_flip(array_keys(array(0 => 0) + $args)), 1);
  265. // find the next named argument. each search starts at the end of the previous replacement.
  266. for ($pos = 0; preg_match('/(?<=%)([a-zA-Z_]\w*)(?=\$)/', $format, $match, PREG_OFFSET_CAPTURE, $pos);) {
  267. $arg_pos = $match[0][1];
  268. $arg_len = strlen($match[0][0]);
  269. $arg_key = $match[1][0];
  270. // programmer did not supply a value for the named argument found in the format string
  271. if (! array_key_exists($arg_key, $arg_nums)) {
  272. user_error("sprintfn(): Missing argument '${arg_key}'", E_USER_WARNING);
  273. return false;
  274. }
  275. // replace the named argument with the corresponding numeric one
  276. $format = substr_replace($format, $replace = $arg_nums[$arg_key], $arg_pos, $arg_len);
  277. $pos = $arg_pos + strlen($replace); // skip to end of replacement for next iteration
  278. }
  279. if (!$args) {
  280. $args = array('');
  281. }
  282. return vsprintf($format, array_values($args));
  283. }
  284. /**
  285. * Inject vars into string template
  286. *
  287. * Ex: echo BUtil::injectVars('One :two :three', array('two'=>2, 'three'=>3))
  288. * Result: "One 2 3"
  289. *
  290. * @param string $str
  291. * @param array $vars
  292. * @return string
  293. */
  294. public static function injectVars($str, $vars)
  295. {
  296. $from = array(); $to = array();
  297. foreach ($vars as $k=>$v) {
  298. $from[] = ':'.$k;
  299. $to[] = $v;
  300. }
  301. return str_replace($from, $to, $str);
  302. }
  303. /**
  304. * Merges any number of arrays / parameters recursively, replacing
  305. * entries with string keys with values from latter arrays.
  306. * If the entry or the next value to be assigned is an array, then it
  307. * automagically treats both arguments as an array.
  308. * Numeric entries are appended, not replaced, but only if they are
  309. * unique
  310. *
  311. * calling: result = BUtil::arrayMerge(a1, a2, ... aN)
  312. *
  313. * @param array $array1
  314. * @param array $array2...
  315. * @return array
  316. **/
  317. public static function arrayMerge() {
  318. $arrays = func_get_args();
  319. $base = array_shift($arrays);
  320. if (!is_array($base)) {
  321. $base = empty($base) ? array() : array($base);
  322. }
  323. foreach ($arrays as $append) {
  324. if (!is_array($append)) {
  325. $append = array($append);
  326. }
  327. foreach ($append as $key => $value) {
  328. if (is_numeric($key)) {
  329. if (!in_array($value, $base)) {
  330. $base[] = $value;
  331. }
  332. } elseif (!array_key_exists($key, $base)) {
  333. $base[$key] = $value;
  334. } elseif (is_array($value) && is_array($base[$key])) {
  335. $base[$key] = static::arrayMerge($base[$key], $append[$key]);
  336. } else {
  337. $base[$key] = $value;
  338. }
  339. }
  340. }
  341. return $base;
  342. }
  343. /**
  344. * Compare 2 arrays recursively
  345. *
  346. * @param array $array1
  347. * @param array $array2
  348. */
  349. public static function arrayCompare(array $array1, array $array2)
  350. {
  351. $diff = false;
  352. // Left-to-right
  353. foreach ($array1 as $key => $value) {
  354. if (!array_key_exists($key,$array2)) {
  355. $diff[0][$key] = $value;
  356. } elseif (is_array($value)) {
  357. if (!is_array($array2[$key])) {
  358. $diff[0][$key] = $value;
  359. $diff[1][$key] = $array2[$key];
  360. } else {
  361. $new = static::arrayCompare($value, $array2[$key]);
  362. if ($new !== false) {
  363. if (isset($new[0])) $diff[0][$key] = $new[0];
  364. if (isset($new[1])) $diff[1][$key] = $new[1];
  365. }
  366. }
  367. } elseif ($array2[$key] !== $value) {
  368. $diff[0][$key] = $value;
  369. $diff[1][$key] = $array2[$key];
  370. }
  371. }
  372. // Right-to-left
  373. foreach ($array2 as $key => $value) {
  374. if (!array_key_exists($key,$array1)) {
  375. $diff[1][$key] = $value;
  376. }
  377. // No direct comparsion because matching keys were compared in the
  378. // left-to-right loop earlier, recursively.
  379. }
  380. return $diff;
  381. }
  382. /**
  383. * Walk over array of objects and perform method or callback on each row
  384. *
  385. * @param array $arr
  386. * @param callback $cb
  387. * @param array $args
  388. * @param boolean $ignoreExceptions
  389. * @return array
  390. */
  391. static public function arrayWalk($arr, $cb, $args=array(), $ignoreExceptions=false)
  392. {
  393. $result = array();
  394. foreach ($arr as $i=>$r) {
  395. $callback = is_string($cb) && $cb[0]==='.' ? array($r, substr($cb, 1)) : $cb;
  396. if ($ignoreExceptions) {
  397. try {
  398. $result[] = call_user_func_array($callback, $args);
  399. } catch (Exception $e) {
  400. BDebug::warning('EXCEPTION class('.get_class($r).') arrayWalk('.$i.'): '.$e->getMessage());
  401. }
  402. } else {
  403. $result[] = call_user_func_array($callback, $args);
  404. }
  405. }
  406. return $result;
  407. }
  408. /**
  409. * Clean array of ints from empty and non-numeric values
  410. *
  411. * If parameter is a string, splits by comma
  412. *
  413. * @param array|string $arr
  414. * @return array
  415. */
  416. static public function arrayCleanInt($arr)
  417. {
  418. $res = array();
  419. if (is_string($arr)) {
  420. $arr = explode(',', $arr);
  421. }
  422. if (is_array($arr)) {
  423. foreach ($arr as $k=>$v) {
  424. if (is_numeric($v)) {
  425. $res[$k] = intval($v);
  426. }
  427. }
  428. }
  429. return $res;
  430. }
  431. /**
  432. * Insert 1 or more items into array at specific position
  433. *
  434. * Note: code repetition is for better iteration performance
  435. *
  436. * @param array $array The original container array
  437. * @param array $items Items to be inserted
  438. * @param string $where
  439. * - start
  440. * - end
  441. * - offset==$key
  442. * - key.(before|after)==$key
  443. * - obj.(before|after).$object_property==$key
  444. * - arr.(before|after).$item_array_key==$key
  445. * @return array resulting array
  446. */
  447. static public function arrayInsert($array, $items, $where)
  448. {
  449. $result = array();
  450. $w1 = explode('==', $where, 2);
  451. $w2 = explode('.', $w1[0], 3);
  452. switch ($w2[0]) {
  453. case 'start':
  454. $result = array_merge($items, $array);
  455. break;
  456. case 'end':
  457. $result = array_merge($array, $items);
  458. break;
  459. case 'offset': // for associative only
  460. $key = $w1[1];
  461. $i = 0;
  462. foreach ($array as $k=>$v) {
  463. if ($key===$i++) {
  464. foreach ($items as $k1=>$v1) {
  465. $result[$k1] = $v1;
  466. }
  467. }
  468. $result[$k] = $v;
  469. }
  470. break;
  471. case 'key': // for associative only
  472. $rel = $w2[1];
  473. $key = $w1[1];
  474. foreach ($array as $k=>$v) {
  475. if ($key===$k) {
  476. if ($rel==='after') {
  477. $result[$k] = $v;
  478. }
  479. foreach ($items as $k1=>$v1) {
  480. $result[$k1] = $v1;
  481. }
  482. if ($rel==='before') {
  483. $result[$k] = $v;
  484. }
  485. } else {
  486. $result[$k] = $v;
  487. }
  488. }
  489. break;
  490. case 'obj':
  491. $rel = $w2[1];
  492. $f = $w2[2];
  493. $key = $w1[1];
  494. foreach ($array as $k=>$v) {
  495. if ($key===$v->$f) {
  496. if ($rel==='after') {
  497. $result[$k] = $v;
  498. }
  499. foreach ($items as $k1=>$v1) {
  500. $result[$k1] = $v1;
  501. }
  502. if ($rel==='before') {
  503. $result[$k] = $v;
  504. }
  505. } else {
  506. $result[$k] = $v;
  507. }
  508. }
  509. break;
  510. case 'arr':
  511. $rel = $w2[1];
  512. $f = $w2[2];
  513. $key = $w1[1];
  514. foreach ($array as $k=>$v) {
  515. if ($key===$v[$f]) {
  516. if ($rel==='after') {
  517. $result[$k] = $v;
  518. }
  519. foreach ($items as $k1=>$v1) {
  520. $result[$k1] = $v1;
  521. }
  522. if ($rel==='before') {
  523. $result[$k] = $v;
  524. }
  525. } else {
  526. $result[$k] = $v;
  527. }
  528. }
  529. break;
  530. default: BDebug::error('Invalid where condition: '.$where);
  531. }
  532. return $result;
  533. }
  534. /**
  535. * Return only specific fields from source array
  536. *
  537. * @param array $source
  538. * @param array|string $fields
  539. * @param boolean $inverse if true, will return anything NOT in $fields
  540. * @param boolean $setNulls fill missing fields with nulls
  541. * @result array
  542. */
  543. static public function arrayMask(array $source, $fields, $inverse=false, $setNulls=true)
  544. {
  545. if (is_string($fields)) {
  546. $fields = explode(',', $fields);
  547. array_walk($fields, 'trim');
  548. }
  549. $result = array();
  550. if (!$inverse) {
  551. foreach ($fields as $k) {
  552. if (isset($source[$k])) {
  553. $result[$k] = $source[$k];
  554. } elseif ($setNulls) {
  555. $result[$k] = null;
  556. }
  557. }
  558. } else {
  559. foreach ($source as $k=>$v) {
  560. if (!in_array($k, $fields)) $result[$k] = $v;
  561. }
  562. }
  563. return $result;
  564. }
  565. static public function arrayToOptions($source, $labelField, $keyField=null, $emptyLabel=null)
  566. {
  567. $options = array();
  568. if (!is_null($emptyLabel)) {
  569. $options = array("" => $emptyLabel);
  570. }
  571. if (empty($source)) {
  572. return array();
  573. }
  574. $isObject = is_object(current($source));
  575. foreach ($source as $k=>$item) {
  576. if ($isObject) {
  577. $key = is_null($keyField) ? $k : $item->$keyField;
  578. $label = $labelField[0]==='.' ? $item->{substr($labelField, 1)}() : $item->labelField;
  579. $options[$key] = $label;
  580. } else {
  581. $key = is_null($keyField) ? $k : $item[$keyField];
  582. $options[$key] = $item[$labelField];
  583. }
  584. }
  585. return $options;
  586. }
  587. static public function arrayMakeAssoc($source, $keyField)
  588. {
  589. $isObject = is_object(current($source));
  590. $assocArray = array();
  591. foreach ($source as $k => $item) {
  592. if ($isObject) {
  593. $assocArray[$item->$keyField] = $item;
  594. } else {
  595. $assocArray[$item[$keyField]] = $item;
  596. }
  597. }
  598. return $assocArray;
  599. }
  600. /**
  601. * Create IV for mcrypt operations
  602. *
  603. * @return string
  604. */
  605. static public function mcryptIV()
  606. {
  607. if (!static::$_mcryptIV) {
  608. static::$_mcryptIV = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_DEV_URANDOM);
  609. }
  610. return static::$_mcryptIV;
  611. }
  612. /**
  613. * Fetch default encryption key from config
  614. *
  615. * @return string
  616. */
  617. static public function mcryptKey($key=null, $configPath=null)
  618. {
  619. if (!is_null($key)) {
  620. static::$_mcryptKey = $key;
  621. } elseif (is_null(static::$_mcryptKey) && $configPath) {
  622. static::$_mcryptKey = BConfig::i()->get($configPath);
  623. }
  624. return static::$_mcryptKey;
  625. }
  626. /**
  627. * Encrypt using AES256
  628. *
  629. * Requires PHP extension mcrypt
  630. *
  631. * @param string $value
  632. * @param string $key
  633. * @param boolean $base64
  634. * @return string
  635. */
  636. static public function encrypt($value, $key=null, $base64=true)
  637. {
  638. if (is_null($key)) $key = static::mcryptKey();
  639. $enc = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $value, MCRYPT_MODE_ECB, static::mcryptIV());
  640. return $base64 ? trim(base64_encode($enc)) : $enc;
  641. }
  642. /**
  643. * Decrypt using AES256
  644. *
  645. * Requires PHP extension mcrypt
  646. *
  647. * @param string $value
  648. * @param string $key
  649. * @param boolean $base64
  650. * @return string
  651. */
  652. static public function decrypt($value, $key=null, $base64=true)
  653. {
  654. if (is_null($key)) $key = static::mcryptKey();
  655. $enc = $base64 ? base64_decode($value) : $value;
  656. return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $enc, MCRYPT_MODE_ECB, static::mcryptIV()));
  657. }
  658. /**
  659. * Generate random string
  660. *
  661. * @param int $strLen length of resulting string
  662. * @param string $chars allowed characters to be used
  663. */
  664. public static function randomString($strLen=8, $chars=null)
  665. {
  666. if (is_null($chars)) {
  667. $chars = static::$_defaultCharPool;
  668. }
  669. $charsLen = strlen($chars)-1;
  670. $str = '';
  671. for ($i=0; $i<$strLen; $i++) {
  672. $str .= $chars[mt_rand(0, $charsLen)];
  673. }
  674. return $str;
  675. }
  676. /**
  677. * Generate random string based on pattern
  678. *
  679. * Syntax: {ULD10}-{U5}
  680. * - U: upper case letters
  681. * - L: lower case letters
  682. * - D: digits
  683. *
  684. * @param string $pattern
  685. * @return string
  686. */
  687. public static function randomPattern($pattern)
  688. {
  689. static $chars = array('L'=>'bcdfghjkmnpqrstvwxyz', 'U'=>'BCDFGHJKLMNPQRSTVWXYZ', 'D'=>'123456789');
  690. while (preg_match('#\{([ULD]+)([0-9]+)\}#i', $pattern, $m)) {
  691. for ($i=0, $c=''; $i<strlen($m[1]); $i++) $c .= $chars[$m[1][$i]];
  692. $pattern = preg_replace('#'.preg_quote($m[0]).'#', BUtil::randomString($m[2], $c), $pattern, 1);
  693. }
  694. return $pattern;
  695. }
  696. public static function nextStringValue($string='', $chars=null)
  697. {
  698. if (is_null($chars)) {
  699. $chars = static::$_defaultCharPool; // avoid leading 0
  700. }
  701. $pos = strlen($string);
  702. $lastChar = substr($chars, -1);
  703. while (--$pos>=-1) {
  704. if ($pos==-1) {
  705. $string = $chars[0].$string;
  706. return $string;
  707. } elseif ($string[$pos]===$lastChar) {
  708. $string[$pos] = $chars[0];
  709. continue;
  710. } else {
  711. $string[$pos] = $chars[strpos($chars, $string[$pos])+1];
  712. return $string;
  713. }
  714. }
  715. // should never get here
  716. return $string;
  717. }
  718. /**
  719. * Set or retrieve current hash algorithm
  720. *
  721. * @param string $algo
  722. */
  723. public static function hashAlgo($algo=null)
  724. {
  725. if (is_null($algo)) {
  726. return static::$_hashAlgo;
  727. }
  728. static::$_hashAlgo = $algo;
  729. }
  730. public static function hashIter($iter=null)
  731. {
  732. if (is_null($iter)) {
  733. return static::$_hashIter;
  734. }
  735. static::$iter = $iter;
  736. }
  737. /**
  738. * Generate salted hash
  739. *
  740. * @deprecated by Bcrypt
  741. * @param string $string original text
  742. * @param mixed $salt
  743. * @param mixed $algo
  744. * @return string
  745. */
  746. public static function saltedHash($string, $salt, $algo=null)
  747. {
  748. $algo = !is_null($algo) ? $algo : static::$_hashAlgo;
  749. return hash($algo, $salt.$string);
  750. }
  751. /**
  752. * Generate fully composed salted hash
  753. *
  754. * Ex: $sha512$2$<salt1>$<salt2>$<double-hashed-string-here>
  755. *
  756. * @deprecated by Bcrypt
  757. * @param string $string
  758. * @param string $salt
  759. * @param string $algo
  760. * @param integer $iter
  761. */
  762. public static function fullSaltedHash($string, $salt=null, $algo=null, $iter=null)
  763. {
  764. $algo = !is_null($algo) ? $algo : static::$_hashAlgo;
  765. if ('bcrypt'===$algo) {
  766. return Bcrypt::i()->hash($string);
  767. }
  768. $iter = !is_null($iter) ? $iter : static::$_hashIter;
  769. $s = static::$_hashSep;
  770. $hash = $s.$algo.$s.$iter;
  771. for ($i=0; $i<$iter; $i++) {
  772. $salt1 = !is_null($salt) ? $salt : static::randomString();
  773. $hash .= $s.$salt1;
  774. $string = static::saltedHash($string, $salt1, $algo);
  775. }
  776. return $hash.$s.$string;
  777. }
  778. /**
  779. * Validate salted hash against original text
  780. *
  781. * @deprecated by BUtil::bcrypt()
  782. * @param string $string original text
  783. * @param string $storedHash fully composed salted hash
  784. * @return bool
  785. */
  786. public static function validateSaltedHash($string, $storedHash)
  787. {
  788. if (strpos($storedHash, '$2a$')===0 || strpos($storedHash, '$2y$')===0) {
  789. return Bcrypt::i()->verify($string, $storedHash);
  790. }
  791. if (!$storedHash) {
  792. return false;
  793. }
  794. $sep = $storedHash[0];
  795. $arr = explode($sep, $storedHash);
  796. array_shift($arr);
  797. $algo = array_shift($arr);
  798. $iter = array_shift($arr);
  799. $verifyHash = $string;
  800. for ($i=0; $i<$iter; $i++) {
  801. $salt = array_shift($arr);
  802. $verifyHash = static::saltedHash($verifyHash, $salt, $algo);
  803. }
  804. $knownHash = array_shift($arr);
  805. return $verifyHash===$knownHash;
  806. }
  807. public static function sha512base64($str)
  808. {
  809. return base64_encode(pack('H*', hash('sha512', $str)));
  810. }
  811. static protected $_lastRemoteHttpInfo;
  812. /**
  813. * Send simple POST request to external server and retrieve response
  814. *
  815. * @param string $method
  816. * @param string $url
  817. * @param array $data
  818. * @return string
  819. */
  820. public static function remoteHttp($method, $url, $data = array())
  821. {
  822. $timeout = 5;
  823. $userAgent = 'Mozilla/5.0';
  824. if ($method==='GET' && $data) {
  825. if(is_array($data)){
  826. $request = http_build_query($data, '', '&');
  827. } else {
  828. $request = $data;
  829. }
  830. $url .= (strpos($url, '?')===false ? '?' : '&') . $request;
  831. }
  832. // curl disabled because file upload doesn't work for some reason. TODO: figure out why
  833. if (false && function_exists('curl_init')) {
  834. $curlOpt = array(
  835. CURLOPT_USERAGENT => $userAgent,
  836. CURLOPT_URL => $url,
  837. CURLOPT_ENCODING => '',
  838. CURLOPT_FOLLOWLOCATION => true,
  839. CURLOPT_RETURNTRANSFER => true,
  840. CURLOPT_AUTOREFERER => true,
  841. CURLOPT_SSL_VERIFYPEER => true,
  842. CURLOPT_CAINFO => dirname(__DIR__).'/ssl/ca-bundle.crt',
  843. CURLOPT_SSL_VERIFYHOST => 2,
  844. CURLOPT_CONNECTTIMEOUT => $timeout,
  845. CURLOPT_TIMEOUT => $timeout,
  846. CURLOPT_MAXREDIRS => 10,
  847. CURLOPT_HTTPHEADER, array('Expect:'), //Fixes the HTTP/1.1 417 Expectation Failed
  848. CURLOPT_HEADER => true,
  849. );
  850. if (false) { // TODO: figure out cookies handling
  851. $cookieDir = BConfig::i()->get('fs/storage_dir').'/cache';
  852. BUtil::ensureDir($cookieDir);
  853. $cookie = tempnam($cookieDir, 'CURLCOOKIE');
  854. $curlOpt += array(
  855. CURLOPT_COOKIEJAR => $cookie,
  856. );
  857. }
  858. if ($method==='POST') {
  859. $curlOpt += array(
  860. CURLOPT_POSTFIELDS => $data,
  861. CURLOPT_POST => 1,
  862. );
  863. } elseif ($method==='PUT') {
  864. $curlOpt += array(
  865. CURLOPT_POSTFIELDS => $data,
  866. CURLOPT_PUT => 1,
  867. );
  868. }
  869. $ch = curl_init();
  870. curl_setopt_array($ch, $curlOpt);
  871. $rawResponse = curl_exec($ch);
  872. list($response, $headers) = explode("\r\n\r\n", $rawResponse, 2);
  873. static::$_lastRemoteHttpInfo = curl_getinfo($ch);
  874. $respHeaders = explode("\r\n", $headers);
  875. if(curl_errno($ch) != 0){
  876. static::$_lastRemoteHttpInfo['errno'] = curl_errno($ch);
  877. static::$_lastRemoteHttpInfo['error'] = curl_error($ch);
  878. }
  879. curl_close($ch);
  880. } else {
  881. $opts = array('http' => array(
  882. 'method' => $method,
  883. 'timeout' => $timeout,
  884. 'header' => "User-Agent: {$userAgent}\r\n",
  885. ));
  886. if ($method==='POST' || $method==='PUT') {
  887. $multipart = false;
  888. foreach ($data as $k=>$v) {
  889. if (is_string($v) && $v[0]==='@') {
  890. $multipart = true;
  891. break;
  892. }
  893. }
  894. if (!$multipart) {
  895. $contentType = 'application/x-www-form-urlencoded';
  896. $opts['http']['content'] = http_build_query($data);
  897. } else {
  898. $boundary = '--------------------------'.microtime(true);
  899. $contentType = 'multipart/form-data; boundary='.$boundary;
  900. $opts['http']['content'] = '';
  901. //TODO: implement recursive forms
  902. foreach ($data as $k =>$v) {
  903. if (is_string($v) && $v[0]==='@') {
  904. $filename = substr($v, 1);
  905. $fileContents = file_get_contents($filename);
  906. $opts['http']['content'] .= "--{$boundary}\r\n".
  907. "Content-Disposition: form-data; name=\"{$k}\"; filename=\"".basename($filename)."\"\r\n".
  908. "Content-Type: application/zip\r\n".
  909. "\r\n".
  910. "{$fileContents}\r\n";
  911. } else {
  912. $opts['http']['content'] .= "--{$boundary}\r\n".
  913. "Content-Disposition: form-data; name=\"{$k}\"\r\n".
  914. "\r\n".
  915. "{$v}\r\n";
  916. }
  917. }
  918. $opts['http']['content'] .= "--{$boundary}--\r\n";
  919. }
  920. $opts['http']['header'] .= "Content-Type: {$contentType}\r\n";
  921. //."Content-Length: ".strlen($request)."\r\n";
  922. if (preg_match('#^(ssl|ftps|https):#', $url)) {
  923. $opts['ssl'] = array(
  924. 'verify_peer' => true,
  925. 'cafile' => dirname(__DIR__).'/ssl/ca-bundle.crt',
  926. 'verify_depth' => 5,
  927. );
  928. }
  929. }
  930. $response = file_get_contents($url, false, stream_context_create($opts));
  931. static::$_lastRemoteHttpInfo = array(); //TODO: emulate curl data?
  932. $respHeaders = $http_response_header;
  933. }
  934. foreach ($respHeaders as $i => $line) {
  935. if ($i) {
  936. $arr = explode(':', $line, 2);
  937. } else {
  938. $arr = array(0, $line);
  939. }
  940. static::$_lastRemoteHttpInfo['headers'][strtolower($arr[0])] = trim($arr[1]);
  941. }
  942. return $response;
  943. }
  944. public static function lastRemoteHttpInfo()
  945. {
  946. return static::$_lastRemoteHttpInfo;
  947. }
  948. public static function normalizePath($path)
  949. {
  950. $path = str_replace('\\', '/', $path);
  951. if (strpos($path, '/..')!==false) {
  952. $a = explode('/', $path);
  953. $b = array();
  954. foreach ($a as $p) {
  955. if ($p==='..') array_pop($b); else $b[] = $p;
  956. }
  957. $path = join('/', $b);
  958. }
  959. return $path;
  960. }
  961. public static function globRecursive($pattern, $flags=0)
  962. {
  963. $files = glob($pattern, $flags);
  964. if (!$files) $files = array();
  965. $dirs = glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT);
  966. if ($dirs) {
  967. foreach ($dirs as $dir) {
  968. $files = array_merge($files, self::globRecursive($dir.'/'.basename($pattern), $flags));
  969. }
  970. }
  971. return $files;
  972. }
  973. public static function isPathAbsolute($path)
  974. {
  975. return !empty($path) && ($path[0]==='/' || $path[0]==='\\') // starting with / or \
  976. || !empty($path[1]) && $path[1]===':'; // windows drive letter C:
  977. }
  978. public static function isUrlFull($url)
  979. {
  980. return preg_match('#^(https?:)?//#', $url);
  981. }
  982. public static function ensureDir($dir)
  983. {
  984. if (is_file($dir)) {
  985. BDebug::warning($dir.' is a file, directory required');
  986. return;
  987. }
  988. if (!is_dir($dir)) {
  989. @$res = mkdir($dir, 0777, true);
  990. if (!$res) {
  991. BDebug::warning("Can't create directory: ".$dir);
  992. }
  993. }
  994. }
  995. /**
  996. * Put together URL components generated by parse_url() function
  997. *
  998. * @see http://us2.php.net/manual/en/function.parse-url.php#106731
  999. * @param array $p result of parse_url()
  1000. * @return string
  1001. */
  1002. public static function unparseUrl($p)
  1003. {
  1004. $scheme = isset($p['scheme']) ? $p['scheme'] . '://' : '';
  1005. $user = isset($p['user']) ? $p['user'] : '';
  1006. $pass = isset($p['pass']) ? ':' . $p['pass'] : '';
  1007. $pass = ($user || $pass) ? $pass . '@' : '';
  1008. $host = isset($p['host']) ? $p['host'] : '';
  1009. $port = isset($p['port']) ? ':' . $p['port'] : '';
  1010. $path = isset($p['path']) ? $p['path'] : '';
  1011. $query = isset($p['query']) ? '?' . $p['query'] : '';
  1012. $fragment = isset($p['fragment']) ? '#' . $p['fragment'] : '';
  1013. return $scheme.$user.$pass.$host.$port.$path.$query.$fragment;
  1014. }
  1015. /**
  1016. * Add or set URL query parameters
  1017. *
  1018. * @param string $url
  1019. * @param array $params
  1020. * @return string
  1021. */
  1022. public static function setUrlQuery($url, $params)
  1023. {
  1024. if (true === $url) {
  1025. $url = BRequest::currentUrl();
  1026. }
  1027. $parsed = parse_url($url);
  1028. $query = array();
  1029. if (!empty($parsed['query'])) {
  1030. foreach (explode('&', $parsed['query']) as $q) {
  1031. $a = explode('=', $q);
  1032. if ($a[0]==='') {
  1033. continue;
  1034. }
  1035. $a[0] = urldecode($a[0]);
  1036. $query[$a[0]] = urldecode($a[1]);
  1037. }
  1038. }
  1039. foreach($params as $k => $v){
  1040. if($v === ""){
  1041. if(isset($query[$k])){
  1042. unset($query[$k]);
  1043. }
  1044. unset($params[$k]);
  1045. }
  1046. }
  1047. $query = array_merge($query, $params);
  1048. $parsed['query'] = http_build_query($query);
  1049. return static::unparseUrl($parsed);
  1050. }
  1051. public static function paginateSortUrl($url, $state, $field)
  1052. {
  1053. return static::setUrlQuery($url, array(
  1054. 's'=>$field,
  1055. 'sd'=>$state['s']!=$field || $state['sd']=='desc' ? 'asc' : 'desc',
  1056. ));
  1057. }
  1058. public static function paginateSortAttr($url, $state, $field, $class='')
  1059. {
  1060. return 'href="'.static::paginateSortUrl($url, $state, $field)
  1061. .'" class="'.$class.' '.($state['s']==$field ? $state['sd'] : '').'"';
  1062. }
  1063. /**
  1064. * @param string $tag
  1065. * @param array $attrs
  1066. * @param null $content
  1067. * @return string
  1068. */
  1069. public static function tagHtml($tag, $attrs = array(), $content = null)
  1070. {
  1071. $attrsHtmlArr = array();
  1072. foreach ($attrs as $k => $v) {
  1073. if ('' === $v || is_null($v) || false === $v) {
  1074. continue;
  1075. }
  1076. if (true === $v) {
  1077. $v = $k;
  1078. } elseif (is_array($v)) {
  1079. switch ($k) {
  1080. case 'class':
  1081. $v = join(' ', $v);
  1082. break;
  1083. case 'style':
  1084. $attrHtmlArr = array();
  1085. foreach ($v as $k1 => $v1) {
  1086. $attrHtmlArr[] = $k1.':'.$v1;
  1087. }
  1088. $v = join('; ', $attrHtmlArr);
  1089. break;
  1090. default:
  1091. $v = join('', $v);
  1092. }
  1093. }
  1094. $attrsHtmlArr[] = $k.'="'.htmlspecialchars($v, ENT_QUOTES, 'UTF-8').'"';
  1095. }
  1096. return '<'.$tag.' '.join(' ', $attrsHtmlArr).'>'.$content.'</'.$tag.'>';
  1097. }
  1098. /**
  1099. * @param array $options
  1100. * @param string $default
  1101. * @return string
  1102. */
  1103. public static function optionsHtml($options, $default = '')
  1104. {
  1105. if(!is_array($default)){
  1106. $default = (string)$default;
  1107. }
  1108. $htmlArr = array();
  1109. foreach ($options as $k => $v) {
  1110. $k = (string)$k;
  1111. if (is_array($v) && $k!=='' && $k[0] === '@') { // group
  1112. $label = trim(substr($k, 1));
  1113. $htmlArr[] = BUtil::tagHtml('optgroup', array('label' => $label), static::optionsHtml($v, $default));
  1114. continue;
  1115. }
  1116. if (is_array($v)) {
  1117. $attr = $v;
  1118. $v = !empty($attr['text']) ? $attr['text'] : '';
  1119. unset($attr['text']);
  1120. } else {
  1121. $attr = array();
  1122. }
  1123. $attr['value'] = $k;
  1124. $attr['selected'] = is_array($default) && in_array($k, $default) || $default === $k;
  1125. $htmlArr[] = BUtil::tagHtml('option', $attr, $v);
  1126. }
  1127. return join("\n", $htmlArr);
  1128. }
  1129. /**
  1130. * Strip html tags and shorten to specified length, to the whole word
  1131. *
  1132. * @param string $text
  1133. * @param integer $limit
  1134. */
  1135. public static function previewText($text, $limit)
  1136. {
  1137. $text = strip_tags($text);
  1138. if (strlen($text) < $limit) {
  1139. return $text;
  1140. }
  1141. preg_match('/^(.{1,'.$limit.'})\b/', $text, $matches);
  1142. return $matches[1];
  1143. }
  1144. public static function isEmptyDate($date)
  1145. {
  1146. return preg_replace('#[0 :-]#', '', (string)$date)==='';
  1147. }
  1148. /**
  1149. * Get gravatar image src by email
  1150. *
  1151. * @param string $email
  1152. * @param array $params
  1153. * - size (default 80)
  1154. * - rating (G, PG, R, X)
  1155. * - default
  1156. * - border
  1157. */
  1158. public static function gravatar($email, $params=array())
  1159. {
  1160. if (empty($params['default'])) {
  1161. $params['default'] = 'identicon';
  1162. }
  1163. return BRequest::i()->scheme().'://www.gravatar.com/avatar/'.md5(strtolower($email))
  1164. .($params ? '?'.http_build_query($params) : '');
  1165. }
  1166. public static function extCallback($callback)
  1167. {
  1168. if (is_string($callback)) {
  1169. if (strpos($callback, '.')!==false) {
  1170. list($class, $method) = explode('.', $callback);
  1171. } elseif (strpos($callback, '->')) {
  1172. list($class, $method) = explode('->', $callback);
  1173. }
  1174. if (!empty($class)) {
  1175. $callback = array($class::i(), $method);
  1176. }
  1177. }
  1178. return $callback;
  1179. }
  1180. public static function call($callback, $args=array(), $array=false)
  1181. {
  1182. $callback = static::extCallback($callback);
  1183. if ($array) {
  1184. return call_user_func_array($callback, $args);
  1185. } else {
  1186. return call_user_func($callback, $args);
  1187. }
  1188. }
  1189. public static function formatDateRecursive($source, $format='m/d/Y')
  1190. {
  1191. foreach ($source as $i=>$val) {
  1192. if (is_string($val)) {
  1193. // checking only beginning of string for speed, assuming it is a date
  1194. if (preg_match('#^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]( |$)#', $val)) {
  1195. $source[$i] = date($format, strtotime($val));
  1196. }
  1197. } elseif (is_array($val)) {
  1198. $source[$i] = static::formatDateRecursive($val, $format);
  1199. }
  1200. }
  1201. return $source;
  1202. }
  1203. public static function timeAgo($ptime, $now=null, $long=false)
  1204. {
  1205. if (!is_numeric($ptime)) {
  1206. $ptime = strtotime($ptime);
  1207. }
  1208. if (!$now) {
  1209. $now = time();
  1210. } elseif (!is_numeric($now)) {
  1211. $now = strtotime($now);
  1212. }
  1213. $etime = $now - $ptime;
  1214. if ($etime < 1) {
  1215. return $long ? 'less than 1 second' : '0s';
  1216. }
  1217. $a = array(
  1218. 12 * 30 * 24 * 60 * 60 => array('year', 'y'),
  1219. 30 * 24 * 60 * 60 => array('month', 'mon'),
  1220. 24 * 60 * 60 => array('day', 'd'),
  1221. 60 * 60 => array('hour', 'h'),
  1222. 60 => array('minute', 'm'),
  1223. 1 => array('second', 's'),
  1224. );
  1225. foreach ($a as $secs => $sa) {
  1226. $d = $etime / $secs;
  1227. if ($d >= 1) {
  1228. $r = round($d);
  1229. return $r . ($long ? ' ' . $sa[0] . ($r > 1 ? 's' : '') : $sa[1]);
  1230. }
  1231. }
  1232. }
  1233. /**
  1234. * Simplify string to allowed characters only
  1235. *
  1236. * @param string $str input string
  1237. * @param string $pattern RegEx pattern to specify not allowed characters
  1238. * @param string $filler character to replace not allowed characters with
  1239. * @return string
  1240. */
  1241. static public function simplifyString($str, $pattern='#[^a-z0-9-]+#', $filler='-')
  1242. {
  1243. return trim(preg_replace($pattern, $filler, strtolower($str)), $filler);
  1244. }
  1245. /**
  1246. * Remove directory recursively
  1247. *
  1248. * DANGEROUS, I'm afraid to enable it
  1249. *
  1250. * @param string $dir
  1251. */
  1252. /*
  1253. static public function rmdirRecursive_YesIHaveCheckedThreeTimes($dir, $first=true)
  1254. {
  1255. if ($first) {
  1256. $dir = realpath($dir);
  1257. }
  1258. if (!file_exists($dir)) {
  1259. return true;
  1260. }
  1261. if (!is_dir($dir) || is_link($dir)) {
  1262. return unlink($dir);
  1263. }
  1264. foreach (scandir($dir) as $item) {
  1265. if ($item == '.' || $item == '..') {
  1266. continue;
  1267. }
  1268. if (!static::rmdirRecursive($dir . "/" . $item, false)) {
  1269. chmod($dir . "/" . $item, 0777);
  1270. if (!static::rmdirRecursive($dir . "/" . $item, false)) return false;
  1271. }
  1272. }
  1273. return rmdir($dir);
  1274. }
  1275. */
  1276. static public function topoSort(array $array, array $args=array())
  1277. {
  1278. if (empty($array)) {
  1279. return array();
  1280. }
  1281. // nodes listed in 'after' are parents
  1282. // nodes listed in 'before' are children
  1283. // prepare initial $nodes array
  1284. $beforeVar = !empty($args['before']) ? $args['before'] : 'before';
  1285. $afterVar = !empty($args['before']) ? $args['after'] : 'after';
  1286. $isObject = is_object(current($array));
  1287. $nodes = array();
  1288. foreach ($array as $k=>$v) {
  1289. $before = $isObject ? $v->$beforeVar : $v[$beforeVar];
  1290. if (is_string($before)) {
  1291. $before = array_walk(explode(',', $before), 'trim');
  1292. }
  1293. $after = $isObject ? $v->$afterVar : $v[$afterVar];
  1294. if (is_string($after)) {
  1295. $after = array_walk(explode(',', $after), 'trim');
  1296. }
  1297. $nodes[$k] = array('key' => $k, 'item' => $v, 'parents' => (array)$after, 'children' => (array)$before);
  1298. }
  1299. // get nodes without parents
  1300. $rootNodes = array();
  1301. foreach ($nodes as $k=>$node) {
  1302. if (empty($node['parents'])) {
  1303. $rootNodes[] = $node;
  1304. }
  1305. }
  1306. // begin algorithm
  1307. $sorted = array();
  1308. while ($nodes) {
  1309. // check for circular reference
  1310. if (!$rootNodes) return false;
  1311. // remove this node from root nodes and add it to the output
  1312. $n = array_pop($rootNodes);
  1313. $sorted[$n['key']] = $n['item'];
  1314. // for each of its children: queue the new node, finally remove the original
  1315. for ($i = count($n['children'])-1; $i>=0; $i--) {
  1316. // get child node
  1317. $childNode = $nodes[$n['children'][$i]];
  1318. // remove child nodes from parent
  1319. unset($n['children'][$i]);
  1320. // remove parent from child node
  1321. unset($childNode['parents'][array_search($n['name'], $childNode['parents'])]);
  1322. // check if this child has other parents. if not, add it to the root nodes list
  1323. if (!$childNode['parents']) {
  1324. array_push($rootNodes, $childNode);
  1325. }
  1326. }
  1327. // remove processed node from list
  1328. unset($nodes[$n['key']]);
  1329. }
  1330. return $sorted;
  1331. }
  1332. /**
  1333. * Wrapper for ZipArchive::open+extractTo
  1334. *
  1335. * @param string $filename
  1336. * @param string $targetDir
  1337. * @return boolean Result
  1338. */
  1339. static public function zipExtract($filename, $targetDir)
  1340. {
  1341. if (!class_exists('ZipArchive')) {
  1342. throw new BException("Class ZipArchive doesn't exist");
  1343. }
  1344. $zip = new ZipArchive;
  1345. $res = $zip->open($filename);
  1346. if (!$res) {
  1347. throw new BException("Can't open zip archive for reading: " . $filename);
  1348. }
  1349. BUtil::ensureDir($targetDir);
  1350. $res = $zip->extractTo($targetDir);
  1351. $zip->close();
  1352. if (!$res) {
  1353. throw new BException("Can't extract zip archive: " . $filename . " to " . $targetDir);
  1354. }
  1355. return true;
  1356. }
  1357. static public function zipCreateFromDir($filename, $sourceDir)
  1358. {
  1359. if (!class_exists('ZipArchive')) {
  1360. throw new BException("Class ZipArchive doesn't exist");
  1361. }
  1362. $files = BUtil::globRecursive($sourceDir.'/*');
  1363. if (!$files) {
  1364. throw new BException('Invalid or empty source dir');
  1365. }
  1366. $zip = new ZipArchive;
  1367. $res = $zip->open($filename, ZipArchive::CREATE);
  1368. if (!$res) {
  1369. throw new BException("Can't open zip archive for writing: " . $filename);
  1370. }
  1371. foreach ($files as $file) {
  1372. $packedFile = str_replace($sourceDir.'/', '', $file);
  1373. if (is_dir($file)) {
  1374. $zip->addEmptyDir($packedFile);
  1375. } else {
  1376. $zip->addFile($file, $packedFile);
  1377. }
  1378. }
  1379. $zip->close();
  1380. return true;
  1381. }
  1382. }
  1383. class BHTML extends BClass
  1384. {
  1385. }
  1386. /**
  1387. * @todo Verify license compatibility and integrate with https://github.com/PHPMailer/PHPMailer
  1388. */
  1389. class BEmail extends BClass
  1390. {
  1391. static protected $_handlers = array();
  1392. static protected $_defaultHandler = 'default';
  1393. public function __construct()
  1394. {
  1395. $this->addHandler('default', array($this, 'defaultHandler'));
  1396. }
  1397. public function addHandler($name, $params)
  1398. {
  1399. if (is_callable($params)) {
  1400. $params = array(
  1401. 'description' => $name,
  1402. 'callback' => $params,
  1403. );
  1404. }
  1405. static::$_handlers[$name] = $params;
  1406. }
  1407. public function getHandlers()
  1408. {
  1409. return static::$_handlers;
  1410. }
  1411. public function setDefaultHandler($name)
  1412. {
  1413. static::$_defaultHandler = $name;
  1414. }
  1415. public function send($data)
  1416. {
  1417. static $allowedHeadersRegex = '/^(to|from|cc|bcc|reply-to|return-path|content-type|list-unsubscribe|x-.*)$/';
  1418. $data = array_change_key_case($data, CASE_LOWER);
  1419. $body = trim($data['body']);
  1420. unset($data['body']);
  1421. $to = '';
  1422. $subject = '';
  1423. $headers = array();
  1424. $params = array();
  1425. $files = array();
  1426. foreach ($data as $k => $v) {
  1427. if ($k == 'subject') {
  1428. $subject = $v;
  1429. } elseif ($k == 'to') {
  1430. $to = $v;
  1431. } elseif ($k == 'attach') {
  1432. foreach ((array)$v as $file) {
  1433. $files[] = $file;
  1434. }
  1435. } elseif ($k[0] === '-') {
  1436. $params[$k] = $k . ' ' . $v;
  1437. } elseif (preg_match($allowedHeadersRegex, $k)) {
  1438. if (!empty($v) && $v!=='"" <>') {
  1439. $headers[$k] = $k . ': ' . $v;
  1440. }
  1441. }
  1442. }
  1443. $origBody = $body;
  1444. if ($files) {
  1445. // $body and $headers will be updated
  1446. $this->_addAttachment($files, $headers, $body);
  1447. }
  1448. $emailData = array(
  1449. 'to' => &$to,
  1450. 'subject' => &$subject,
  1451. 'orig_body' => &$origBody,
  1452. 'body' => &$body,
  1453. 'headers' => &$headers,
  1454. 'params' => &$params,
  1455. 'files' => &$files,
  1456. 'orig_data' => $data,
  1457. );
  1458. return $this->_dispatch($emailData);
  1459. }
  1460. protected function _dispatch($emailData)
  1461. {
  1462. try {
  1463. $flags = BEvents::i()->fire('BEmail::send:before', array('email_data' => $emailData));
  1464. if ($flags===false) {
  1465. return false;
  1466. } elseif (is_array($flags)) {
  1467. foreach ($flags as $f) {
  1468. if ($f===false) {
  1469. return false;
  1470. }
  1471. }
  1472. }
  1473. } catch (BException $e) {
  1474. BDebug::warning($e->getMessage());
  1475. return false;
  1476. }
  1477. $callback = static::$_handlers[static::$_defaultHandler]['callback'];
  1478. if (is_callable($callback)) {
  1479. $result = call_user_func($callback, $emailData);
  1480. } else {
  1481. BDebug::warning('Default email handler is not callable');
  1482. $result = false;
  1483. }
  1484. $emailData['result'] = $result;
  1485. BEvents::i()->fire('BEmail::send:after', array('email_data' => $emailData));
  1486. return $result;
  1487. }
  1488. /**
  1489. * Add email attachment
  1490. *
  1491. * @param $files
  1492. * @param $mailheaders
  1493. * @param $body
  1494. */
  1495. protected function _addAttachment($files, &$mailheaders, &$body)
  1496. {
  1497. $body = trim($body);
  1498. //$headers = array();
  1499. // boundary
  1500. $semi_rand = md5(microtime());
  1501. $mime_boundary = "==Multipart_Boundary_x{$semi_rand}x";
  1502. // headers for attachment
  1503. $headers = $mailheaders;
  1504. $headers[] = "MIME-Version: 1.0";
  1505. $headers[] = "Content-Type: multipart/mixed;";
  1506. $headers[] = " boundary=\"{$mime_boundary}\"";
  1507. //headers and message for text
  1508. $message = "--{$mime_boundary}\n\n" . $body . "\n\n";
  1509. // preparing attachments
  1510. foreach ($files as $file) {
  1511. if (is_file($file)) {
  1512. $data = chunk_split(base64_encode(file_get_contents($file)));
  1513. $name = basename($file);
  1514. $message .= "--{$mime_boundary}\n" .
  1515. "Content-Type: application/octet-stream; name=\"" . $name . "\"\n" .
  1516. "Content-Description: " . $name . "\n" .
  1517. "Content-Disposition: attachment;\n" . " filename=\"" . $name . "\"; size=" . filesize($files[$i]) . ";\n" .
  1518. "Content-Transfer-Encoding: base64\n\n" . $data . "\n\n";
  1519. }
  1520. }
  1521. $message .= "--{$mime_boundary}--";
  1522. $body = $message;
  1523. $mailheaders = $headers;
  1524. return true;
  1525. }
  1526. public function defaultHandler($data)
  1527. {
  1528. return mail($data['to'], $data['subject'], $data['body'],
  1529. join("\r\n", $data['headers']), join(' ', $data['params']));
  1530. }
  1531. }
  1532. /**
  1533. * Helper class to designate a variable a custom type
  1534. */
  1535. class BValue
  1536. {
  1537. public $content;
  1538. public $type;
  1539. public function __construct($content, $type='string')
  1540. {
  1541. $this->content = $content;
  1542. $this->type = $type;
  1543. }
  1544. public function toPlain()
  1545. {
  1546. return $this->content;
  1547. }
  1548. public function __toString()
  1549. {
  1550. return (string)$this->toPlain();
  1551. }
  1552. }
  1553. /**
  1554. * @deprecated
  1555. */
  1556. class BType extends BValue {}
  1557. class BData extends BClass implements ArrayAccess
  1558. {
  1559. protected $_data;
  1560. public function __construct($data, $recursive = false)
  1561. {
  1562. if (!is_array($data)) {
  1563. $data = array(); // not sure for here, should we try to convert data to array or do empty array???
  1564. }
  1565. if ($recursive) {
  1566. foreach ($data as $k => $v) {
  1567. if (is_array($v)) {
  1568. $data[$k] = new BData($v, true);
  1569. }
  1570. }
  1571. }
  1572. $this->_data = $data;
  1573. }
  1574. public function as_array($recursive=false)
  1575. {
  1576. $data = $this->_data;
  1577. if ($recursive) {
  1578. foreach ($data as $k => $v) {
  1579. if (is_object($v) && $v instanceof BData) {
  1580. $data[$k] = $v->as_array();
  1581. }
  1582. }
  1583. }
  1584. return $data;
  1585. }
  1586. public function __get($name)
  1587. {
  1588. return isset($this->_data[$name]) ? $this->_data[$name] : null;
  1589. }
  1590. public function __set($name, $value)
  1591. {
  1592. $this->_data[$name] = $value;
  1593. }
  1594. public function offsetSet($offset, $value)
  1595. {
  1596. if (is_null($offset)) {
  1597. $this->_data[] = $value;
  1598. } else {
  1599. $this->_data[$offset] = $value;
  1600. }
  1601. }
  1602. public function offsetExists($offset) {
  1603. return isset($this->_data[$offset]);
  1604. }
  1605. public function offsetUnset($offset)
  1606. {
  1607. unset($this->_data[$offset]);
  1608. }
  1609. public function offsetGet($offset)
  1610. {
  1611. return isset($this->_data[$offset]) ? $this->_data[$offset] : null;
  1612. }
  1613. public function get($name)
  1614. {
  1615. return isset($this->_data[$name]) ? $this->_data[$name] : null;
  1616. }
  1617. public function set($name, $value)
  1618. {
  1619. $this->_data[$name] = $value;
  1620. return $this;
  1621. }
  1622. }
  1623. class BErrorException extends Exception
  1624. {
  1625. public $context;
  1626. public $stackPop;
  1627. public function __construct($code, $message, $file, $line, $context=null, $stackPop=1)
  1628. {
  1629. parent::__construct($message, $code);
  1630. $this->file = $file;
  1631. $this->line = $line;
  1632. $this->context = $context;
  1633. $this->stackPop = $stackPop;
  1634. }
  1635. }
  1636. /**
  1637. * Facility to log errors and events for development and debugging
  1638. *
  1639. * @todo move all debugging into separate plugin, and override core classes
  1640. */
  1641. class BDebug extends BClass
  1642. {
  1643. const EMERGENCY = 0,
  1644. ALERT = 1,
  1645. CRITICAL = 2,
  1646. ERROR = 3,
  1647. WARNING = 4,
  1648. NOTICE = 5,
  1649. INFO = 6,
  1650. DEBUG = 7;
  1651. static protected $_levelLabels = array(
  1652. self::EMERGENCY => 'EMERGENCY',
  1653. self::ALERT => 'ALERT',
  1654. self::CRITICAL => 'CRITICAL',
  1655. self::ERROR => 'ERROR',
  1656. self::WARNING => 'WARNING',
  1657. self::NOTICE => 'NOTICE',
  1658. self::INFO => 'INFO',
  1659. self::DEBUG => 'DEBUG',
  1660. );
  1661. const MEMORY = 0,
  1662. FILE = 1,
  1663. SYSLOG = 2,
  1664. EMAIL = 4,
  1665. OUTPUT = 8,
  1666. EXCEPTION = 16,
  1667. STOP = 4096;
  1668. const MODE_DEBUG = 'DEBUG',
  1669. MODE_DEVELOPMENT = 'DEVELOPMENT',
  1670. MODE_STAGING = 'STAGING',
  1671. MODE_PRODUCTION = 'PRODUCTION',
  1672. MODE_MIGRATION = 'MIGRATION',
  1673. MODE_INSTALLATION = 'INSTALLATION',
  1674. MODE_RECOVERY = 'RECOVERY',
  1675. MODE_DISABLED = 'DISABLED'
  1676. ;
  1677. /**
  1678. * Trigger levels for different actions
  1679. *
  1680. * - memory: remember in immediate script memory
  1681. * - file: write to debug log file
  1682. * - email: send email notification to admin
  1683. * - output: display error in output
  1684. * - exception: stop script execution by throwing exception
  1685. *
  1686. * Default are production values
  1687. *
  1688. * @var array
  1689. */
  1690. static protected $_level;
  1691. static protected $_levelPreset = array(
  1692. self::MODE_PRODUCTION => array(
  1693. self::MEMORY => false,
  1694. self::SYSLOG => false,
  1695. self::FILE => self::WARNING,
  1696. self::EMAIL => false,//self::ERROR,
  1697. self::OUTPUT => self::CRITICAL,
  1698. self::EXCEPTION => self::ERROR,
  1699. self::STOP => self::CRITICAL,
  1700. ),
  1701. self::MODE_STAGING => array(
  1702. self::MEMORY => false,
  1703. self::SYSLOG => false,
  1704. self::FILE => self::WARNING,
  1705. self::EMAIL => false,//self::ERROR,
  1706. self::OUTPUT => self::CRITICAL,
  1707. self::EXCEPTION => self::ERROR,
  1708. self::STOP => self::CRITICAL,
  1709. ),
  1710. self::MODE_DEVELOPMENT => array(
  1711. self::MEMORY => self::INFO,
  1712. self::SYSLOG => false,
  1713. self::FILE => self::WARNING,
  1714. self::EMAIL => false,//self::CRITICAL,
  1715. self::OUTPUT => self::NOTICE,
  1716. self::EXCEPTION => self::ERROR,
  1717. self::STOP => self::CRITICAL,
  1718. ),
  1719. self::MODE_DEBUG => array(
  1720. self::MEMORY => self::DEBUG,
  1721. self::SYSLOG => false,
  1722. self::FILE => self::WARNING,
  1723. self::EMAIL => false,//self::CRITICAL,
  1724. self::OUTPUT => self::NOTICE,
  1725. self::EXCEPTION => self::ERROR,
  1726. self::STOP => self::CRITICAL,
  1727. ),
  1728. self::MODE_RECOVERY => array(
  1729. self::MEMORY => self::DEBUG,
  1730. self::SYSLOG => false,
  1731. self::FILE => self::WARNING,
  1732. self::EMAIL => false,//self::CRITICAL,
  1733. self::OUTPUT => self::NOTICE,
  1734. self::EXCEPTION => self::ERROR,
  1735. self::STOP => self::CRITICAL,
  1736. ),
  1737. self::MODE_MIGRATION => array(
  1738. self::MEMORY => self::DEBUG,
  1739. self::SYSLOG => false,
  1740. self::FILE => self::WARNING,
  1741. self::EMAIL => false,//self::CRITICAL,
  1742. self::OUTPUT => self::NOTICE,
  1743. self::EXCEPTION => self::ERROR,
  1744. self::STOP => self::CRITICAL,
  1745. ),
  1746. self::MODE_INSTALLATION => array(
  1747. self::MEMORY => self::DEBUG,
  1748. self::SYSLOG => false,
  1749. self::FILE => self::WARNING,
  1750. self::EMAIL => false,//self::CRITICAL,
  1751. self::OUTPUT => self::NOTICE,
  1752. self::EXCEPTION => self::ERROR,
  1753. self::STOP => self::CRITICAL,
  1754. ),
  1755. self::MODE_DISABLED => array(
  1756. self::MEMORY => false,
  1757. self::SYSLOG => false,
  1758. self::FILE => false,
  1759. self::EMAIL => false,
  1760. self::OUTPUT => false,
  1761. self::EXCEPTION => false,
  1762. self::STOP => false,
  1763. ),
  1764. );
  1765. static protected $_modules = array();
  1766. static protected $_mode = 'PRODUCTION';
  1767. static protected $_startTime;
  1768. static protected $_events = array();
  1769. static protected $_logDir = null;
  1770. static protected $_logFile = array(
  1771. self::EMERGENCY => 'error.log',
  1772. self::ALERT => 'error.log',
  1773. self::CRITICAL => 'error.log',
  1774. self::ERROR => 'error.log',
  1775. self::WARNING => 'debug.log',
  1776. self::NOTICE => 'debug.log',
  1777. self::INFO => 'debug.log',
  1778. self::DEBUG => 'debug.log',
  1779. );
  1780. static protected $_adminEmail = null;
  1781. static protected $_phpErrorMap = array(
  1782. E_ERROR => self::ERROR,
  1783. E_WARNING => self::WARNING,
  1784. E_NOTICE => self::NOTICE,
  1785. E_USER_ERROR => self::ERROR,
  1786. E_USER_WARNING => self::WARNING,
  1787. E_USER_NOTICE => self::NOTICE,
  1788. E_STRICT => self::NOTICE,
  1789. E_RECOVERABLE_ERROR => self::ERROR,
  1790. );
  1791. static protected $_verboseBacktrace = array();
  1792. static protected $_collectedErrors = array();
  1793. static protected $_errorHandlerLog = array();
  1794. /**
  1795. * Constructor, remember script start time for delta timestamps
  1796. *
  1797. * @return BDebug
  1798. */
  1799. public function __construct()
  1800. {
  1801. self::$_startTime = microtime(true);
  1802. BEvents::i()->on('BResponse::output:after', 'BDebug::afterOutput');
  1803. }
  1804. /**
  1805. * Shortcut to help with IDE autocompletion
  1806. *
  1807. * @param bool $new
  1808. * @param array $args
  1809. * @return BDebug
  1810. */
  1811. public static function i($new=false, array $args=array())
  1812. {
  1813. return BClassRegistry::i()->instance(__CLASS__, $args, !$new);
  1814. }
  1815. public static function registerErrorHandlers()
  1816. {
  1817. set_error_handler('BDebug::errorHandler');
  1818. set_exception_handler('BDebug::exceptionHandler');
  1819. register_shutdown_function('BDebug::shutdownHandler');
  1820. }
  1821. public static function startErrorLogger()
  1822. {
  1823. static::$_errorHandlerLog = array();
  1824. set_error_handler('BDebug::errorHandlerLogger');
  1825. }
  1826. public static function stopErrorLogger()
  1827. {
  1828. set_error_handler('BDebug::errorHandler');
  1829. return static::$_errorHandlerLog;
  1830. }
  1831. public static function errorHandlerLogger($code, $message, $file, $line, $context=null)
  1832. {
  1833. return static::$_errorHandlerLog[] = compact('code', 'message', 'file', 'line', 'context');
  1834. }
  1835. public static function errorHandler($code, $message, $file, $line, $context=null)
  1836. {
  1837. if (!(error_reporting() & $code)) {
  1838. return;
  1839. }
  1840. static::trigger(self::$_phpErrorMap[$code], $message, 1);
  1841. //throw new BErrorException(self::$_phpErrorMap[$code], $message, $file, $line, $context);
  1842. }
  1843. public static function exceptionHandler($e)
  1844. {
  1845. //static::trigger($e->getCode(), $e->getMessage(), $e->stackPop+1);
  1846. static::trigger(self::ERROR, $e);
  1847. }
  1848. public static function shutdownHandler()
  1849. {
  1850. $e = error_get_last();
  1851. if ($e && ($e['type']===E_ERROR || $e['type']===E_PARSE || $e['type']===E_COMPILE_ERROR || $e['type']===E_COMPILE_WARNING)) {
  1852. static::trigger(self::CRITICAL, $e['file'].':'.$e['line'].': '.$e['message'], 1);
  1853. }
  1854. }
  1855. public static function level($type, $level=null)
  1856. {
  1857. if (!isset(static::$_level[$type])) {
  1858. throw new BException('Invalid debug level type');
  1859. }
  1860. if (is_null($level)) {
  1861. if (is_null(static::$_level)) {
  1862. static::$_level = static::$_levelPreset[self::$_mode];
  1863. }
  1864. return static::$_level[$type];
  1865. }
  1866. static::$_level[$type] = $level;
  1867. }
  1868. public static function logDir($dir)
  1869. {
  1870. BUtil::ensureDir($dir);
  1871. static::$_logDir = $dir;
  1872. }
  1873. public static function log($msg, $file='debug.log')
  1874. {
  1875. error_log($msg."\n", 3, static::$_logDir.'/'.$file);
  1876. }
  1877. public static function logException($e)
  1878. {
  1879. static::log(print_r($e, 1), 'exceptions.log');
  1880. }
  1881. public static function adminEmail($email)
  1882. {
  1883. self::$_adminEmail = $email;
  1884. }
  1885. public static function mode($mode=null, $setLevels=true)
  1886. {
  1887. if (is_null($mode)) {
  1888. return static::$_mode;
  1889. }
  1890. self::$_mode = $mode;
  1891. if ($setLevels && !empty(static::$_levelPreset[$mode])) {
  1892. static::$_level = static::$_levelPreset[$mode];
  1893. }
  1894. }
  1895. public static function backtraceOn($msg)
  1896. {
  1897. foreach ((array)$msg as $m) {
  1898. static::$_verboseBacktrace[$m] = true;
  1899. }
  1900. }
  1901. public static function trigger($level, $msg, $stackPop=0)
  1902. {
  1903. if (is_scalar($msg)) {
  1904. $e = array('msg'=>$msg);
  1905. } elseif (is_object($msg) && $msg instanceof Exception) {
  1906. $e = array('msg'=>$msg->getMessage());
  1907. } elseif (is_array($msg)) {
  1908. $e = $msg;
  1909. } else {
  1910. throw new Exception('Invalid message type: '.print_r($msg, 1));
  1911. }
  1912. //$stackPop++;
  1913. $bt = debug_backtrace(true);
  1914. $e['level'] = self::$_levelLabels[$level];
  1915. if (isset($bt[$stackPop]['file'])) $e['file'] = $bt[$stackPop]['file'];
  1916. if (isset($bt[$stackPop]['line'])) $e['line'] = $bt[$stackPop]['line'];
  1917. //$o = $bt[$stackPop]['object'];
  1918. //$e['object'] = is_object($o) ? get_class($o) : $o;
  1919. $e['ts'] = BDb::now();
  1920. $e['t'] = microtime(true)-self::$_startTime;
  1921. $e['d'] = null;
  1922. $e['c'] = null;
  1923. $e['mem'] = memory_get_usage();
  1924. if (!empty(static::$_verboseBacktrace[$e['msg']])) {
  1925. foreach ($bt as $t) {
  1926. $e['msg'] .= "\n".$t['file'].':'.$t['line'];
  1927. }
  1928. }
  1929. $message = "{$e['level']}: {$e['msg']}".(isset($e['file'])?" ({$e['file']}:{$e['line']})":'');
  1930. if (($moduleName = BModuleRegistry::i()->currentModuleName())) {
  1931. $e['module'] = $moduleName;
  1932. }
  1933. if (is_null(static::$_level) && !empty(static::$_levelPreset[self::$_mode])) {
  1934. static::$_level = static::$_levelPreset[self::$_mode];
  1935. }
  1936. $l = self::$_level[self::MEMORY];
  1937. if (false!==$l && (is_array($l) && in_array($level, $l) || $l>=$level)) {
  1938. self::$_events[] = $e;
  1939. $id = sizeof(self::$_events)-1;
  1940. }
  1941. $l = self::$_level[self::SYSLOG];
  1942. if (false!==$l && (is_array($l) && in_array($level, $l) || $l>=$level)) {
  1943. error_log($message, 0, self::$_logDir);
  1944. }
  1945. if (!is_null(self::$_logDir)) { // require explicit enable of file log
  1946. $l = self::$_level[self::FILE];
  1947. if (false!==$l && (is_array($l) && in_array($level, $l) || $l>=$level)) {
  1948. /*
  1949. if (is_null(self::$_logDir)) {
  1950. self::$_logDir = sys_get_temp_dir();
  1951. }
  1952. */
  1953. $file = self::$_logDir.'/'.self::$_logFile[$level];
  1954. if (is_writable(self::$_logDir) || is_writable($file)) {
  1955. error_log("{$e['ts']} {$message}\n", 3, $file);
  1956. } else {
  1957. //TODO: anything needs to be done here?
  1958. }
  1959. }
  1960. }
  1961. if (!is_null(self::$_adminEmail)) { // require explicit enable of email
  1962. $l = self::$_level[self::EMAIL];
  1963. if (false!==$l && (is_array($l) && in_array($level, $l) || $l>=$level)) {
  1964. error_log(print_r($e, 1), 1, self::$_adminEmail);
  1965. }
  1966. }
  1967. $l = self::$_level[self::OUTPUT];
  1968. if (false!==$l && (is_array($l) && in_array($level, $l) || $l>=$level)) {
  1969. echo '<xmp style="text-align:left; border:solid 1px red; font-family:monospace;">';
  1970. //ob_start();
  1971. echo $message."\n";
  1972. debug_print_backtrace();
  1973. //echo ob_get_clean();
  1974. echo '</xmp>';
  1975. }
  1976. /*
  1977. $l = self::$_level[self::EXCEPTION];
  1978. if (false!==$l && (is_array($l) && in_array($level, $l) || $l>=$level)) {
  1979. if (is_object($msg) && $msg instanceof Exception) {
  1980. throw $msg;
  1981. } else {
  1982. throw new Exception($msg);
  1983. }
  1984. }
  1985. */
  1986. $l = self::$_level[self::STOP];
  1987. if (false!==$l && (is_array($l) && in_array($level, $l) || $l>=$level)) {
  1988. static::dumpLog();
  1989. die;
  1990. }
  1991. return isset($id) ? $id : null;
  1992. }
  1993. public static function alert($msg, $stackPop=0)
  1994. {
  1995. self::i()->collectError($msg);
  1996. return self::trigger(self::ALERT, $msg, $stackPop+1);
  1997. }
  1998. public static function critical($msg, $stackPop=0)
  1999. {
  2000. self::i()->collectError($msg);
  2001. return self::trigger(self::CRITICAL, $msg, $stackPop+1);
  2002. }
  2003. public static function error($msg, $stackPop=0)
  2004. {
  2005. self::i()->collectError($msg);
  2006. return self::trigger(self::ERROR, $msg, $stackPop+1);
  2007. }
  2008. public static function warning($msg, $stackPop=0)
  2009. {
  2010. self::i()->collectError($msg);
  2011. return self::trigger(self::WARNING, $msg, $stackPop+1);
  2012. }
  2013. public static function notice($msg, $stackPop=0)
  2014. {
  2015. self::i()->collectError($msg);
  2016. return self::trigger(self::NOTICE, $msg, $stackPop+1);
  2017. }
  2018. public static function info($msg, $stackPop=0)
  2019. {
  2020. self::i()->collectError($msg);
  2021. return self::trigger(self::INFO, $msg, $stackPop+1);
  2022. }
  2023. public function collectError($msg, $type=self::ERROR)
  2024. {
  2025. self::$_collectedErrors[$type][] = $msg;
  2026. }
  2027. public function getCollectedErrors($type=self::ERROR)
  2028. {
  2029. if (!empty(self::$_collectedErrors[$type])) {
  2030. return self::$_collectedErrors[$type];
  2031. }
  2032. }
  2033. public static function debug($msg, $stackPop=0)
  2034. {
  2035. if ('DEBUG'!==self::$_mode) return; // to speed things up
  2036. return self::trigger(self::DEBUG, $msg, $stackPop+1);
  2037. }
  2038. public static function profile($id)
  2039. {
  2040. if ($id && !empty(self::$_events[$id])) {
  2041. self::$_events[$id]['d'] = microtime(true)-self::$_startTime-self::$_events[$id]['t'];
  2042. self::$_events[$id]['c']++;
  2043. }
  2044. }
  2045. public static function is($modes)
  2046. {
  2047. if (is_string($modes)) $modes = explode(',', $modes);
  2048. return in_array(self::$_mode, $modes);
  2049. }
  2050. public static function dumpLog($return=false)
  2051. {
  2052. if ((self::$_mode!==self::MODE_DEBUG && self::$_mode!==self::MODE_DEVELOPMENT)
  2053. || BResponse::i()->contentType()!=='text/html'
  2054. || BRequest::i()->xhr()
  2055. ) {
  2056. return;
  2057. }
  2058. ob_start();
  2059. ?><style>
  2060. #buckyball-debug-trigger { position:fixed; top:0; right:0; font:normal 10px Verdana; cursor:pointer; z-index:999999; background:#ffc; }
  2061. #buckyball-debug-console { position:fixed; overflow:auto; top:10px; left:10px; bottom:10px; right:10px; border:solid 2px #f00; padding:4px; text-align:left; opacity:1; background:#FFC; font:normal 10px Verdana; z-index:20000; }
  2062. #buckyball-debug-console table { border-collapse: collapse; }
  2063. #buckyball-debug-console th, #buckyball-debug-console td { font:normal 10px Verdana; border: solid 1px #ccc; padding:2px 5px;}
  2064. #buckyball-debug-console th { font-weight:bold; }
  2065. </style>
  2066. <div id="buckyball-debug-trigger" onclick="var el=document.getElementById('buckyball-debug-console');el.style.display=el.style.display?'':'none'">[DBG]</div>
  2067. <div id="buckyball-debug-console" style="display:none"><?php
  2068. echo "DELTA: ".BDebug::i()->delta().', PEAK: '.memory_get_peak_usage(true).', EXIT: '.memory_get_usage(true);
  2069. echo "<pre>";
  2070. print_r(BORM::get_query_log());
  2071. //BEvents::i()->debug();
  2072. echo "</pre>";
  2073. //print_r(self::$_events);
  2074. ?><table cellspacing="0"><tr><th>Message</th><th>Rel.Time</th><th>Profile</th><th>Memory</th><th>Level</th><th>Relevant Location</th><th>Module</th></tr><?php
  2075. foreach (self::$_events as $e) {
  2076. if (empty($e['file'])) { $e['file'] = ''; $e['line'] = ''; }
  2077. $profile = $e['d'] ? number_format($e['d'], 6).($e['c']>1 ? ' ('.$e['c'].')' : '') : '';
  2078. echo "<tr><td><xmp style='margin:0'>".$e['msg']."</xmp></td><td>".number_format($e['t'], 6)."</td><td>".$profile."</td><td>".number_format($e['mem'], 0)."</td><td>{$e['level']}</td><td>{$e['file']}:{$e['line']}</td><td>".(!empty($e['module'])?$e['module']:'')."</td></tr>";
  2079. }
  2080. ?></table></div><?php
  2081. $html = ob_get_clean();
  2082. if ($return) {
  2083. return $html;
  2084. } else {
  2085. echo $html;
  2086. }
  2087. }
  2088. /**
  2089. * Delta time from start
  2090. *
  2091. * @return float
  2092. */
  2093. public static function delta()
  2094. {
  2095. return microtime(true)-self::$_startTime;
  2096. }
  2097. public static function dump($var)
  2098. {
  2099. if (is_array($var) && current($var) instanceof Model) {
  2100. foreach ($var as $k=>$v) {
  2101. echo '<hr>'.$k.':';
  2102. static::dump($v);
  2103. }
  2104. } elseif ($var instanceof Model) {
  2105. echo '<pre>'; print_r($var->as_array()); echo '</pre>';
  2106. } else {
  2107. echo '<pre>'; print_r($var); echo '</pre>';
  2108. }
  2109. }
  2110. public static function afterOutput($args)
  2111. {
  2112. static::dumpLog();
  2113. //$args['content'] = str_replace('</body>', static::dumpLog(true).'</body>', $args['content']);
  2114. }
  2115. }
  2116. /**
  2117. * Facility to handle l10n and i18n
  2118. */
  2119. class BLocale extends BClass
  2120. {
  2121. static protected $_domainPrefix = 'fulleron/';
  2122. static protected $_domainStack = array();
  2123. static protected $_defaultLanguage = 'en_US';
  2124. static protected $_currentLanguage;
  2125. static protected $_transliterateMap = array(
  2126. '&amp;' => 'and', '@' => 'at', '©' => 'c', '®' => 'r', 'À' => 'a',
  2127. 'Á' => 'a', 'Â' => 'a', 'Ä' => 'a', 'Å' => 'a', 'Æ' => 'ae','Ç' => 'c',
  2128. 'È' => 'e', 'É' => 'e', 'Ë' => 'e', 'Ì' => 'i', 'Í' => 'i', 'Î' => 'i',
  2129. 'Ï' => 'i', 'Ò' => 'o', 'Ó' => 'o', 'Ô' => 'o', 'Õ' => 'o', 'Ö' => 'o',
  2130. 'Ø' => 'o', 'Ù' => 'u', 'Ú' => 'u', 'Û' => 'u', 'Ü' => 'u', 'Ý' => 'y',
  2131. 'ß' => 'ss','à' => 'a', 'á' => 'a', 'â' => 'a', 'ä' => 'a', 'å' => 'a',
  2132. 'æ' => 'ae','ç' => 'c', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
  2133. 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ò' => 'o', 'ó' => 'o',
  2134. 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ø' => 'o', 'ù' => 'u', 'ú' => 'u',
  2135. 'û' => 'u', 'ü' => 'u', 'ý' => 'y', 'þ' => 'p', 'ÿ' => 'y', 'Ā' => 'a',
  2136. 'ā' => 'a', 'Ă' => 'a', 'ă' => 'a', 'Ą' => 'a', 'ą' => 'a', 'Ć' => 'c',
  2137. 'ć' => 'c', 'Ĉ' => 'c', 'ĉ' => 'c', 'Ċ' => 'c', 'ċ' => 'c', 'Č' => 'c',
  2138. 'č' => 'c', 'Ď' => 'd', 'ď' => 'd', 'Đ' => 'd', 'đ' => 'd', 'Ē' => 'e',
  2139. 'ē' => 'e', 'Ĕ' => 'e', 'ĕ' => 'e', 'Ė' => 'e', 'ė' => 'e', 'Ę' => 'e',
  2140. 'ę' => 'e', 'Ě' => 'e', 'ě' => 'e', 'Ĝ' => 'g', 'ĝ' => 'g', 'Ğ' => 'g',
  2141. 'ğ' => 'g', 'Ġ' => 'g', 'ġ' => 'g', 'Ģ' => 'g', 'ģ' => 'g', 'Ĥ' => 'h',
  2142. 'ĥ' => 'h', 'Ħ' => 'h', 'ħ' => 'h', 'Ĩ' => 'i', 'ĩ' => 'i', 'Ī' => 'i',
  2143. 'ī' => 'i', 'Ĭ' => 'i', 'ĭ' => 'i', 'Į' => 'i', 'į' => 'i', 'İ' => 'i',
  2144. 'ı' => 'i', 'IJ' => 'ij','ij' => 'ij','Ĵ' => 'j', 'ĵ' => 'j', 'Ķ' => 'k',
  2145. 'ķ' => 'k', 'ĸ' => 'k', 'Ĺ' => 'l', 'ĺ' => 'l', 'Ļ' => 'l', 'ļ' => 'l',
  2146. 'Ľ' => 'l', 'ľ' => 'l', 'Ŀ' => 'l', 'ŀ' => 'l', 'Ł' => 'l', 'ł' => 'l',
  2147. 'Ń' => 'n', 'ń' => 'n', 'Ņ' => 'n', 'ņ' => 'n', 'Ň' => 'n', 'ň' => 'n',
  2148. 'ʼn' => 'n', 'Ŋ' => 'n', 'ŋ' => 'n', 'Ō' => 'o', 'ō' => 'o', 'Ŏ' => 'o',
  2149. 'ŏ' => 'o', 'Ő' => 'o', 'ő' => 'o', 'Œ' => 'oe','œ' => 'oe','Ŕ' => 'r',
  2150. 'ŕ' => 'r', 'Ŗ' => 'r', 'ŗ' => 'r', 'Ř' => 'r', 'ř' => 'r', 'Ś' => 's',
  2151. 'ś' => 's', 'Ŝ' => 's', 'ŝ' => 's', 'Ş' => 's', 'ş' => 's', 'Š' => 's',
  2152. 'š' => 's', 'Ţ' => 't', 'ţ' => 't', 'Ť' => 't', 'ť' => 't', 'Ŧ' => 't',
  2153. 'ŧ' => 't', 'Ũ' => 'u', 'ũ' => 'u', 'Ū' => 'u', 'ū' => 'u', 'Ŭ' => 'u',
  2154. 'ŭ' => 'u', 'Ů' => 'u', 'ů' => 'u', 'Ű' => 'u', 'ű' => 'u', 'Ų' => 'u',
  2155. 'ų' => 'u', 'Ŵ' => 'w', 'ŵ' => 'w', 'Ŷ' => 'y', 'ŷ' => 'y', 'Ÿ' => 'y',
  2156. 'Ź' => 'z', 'ź' => 'z', 'Ż' => 'z', 'ż' => 'z', 'Ž' => 'z', 'ž' => 'z',
  2157. 'ſ' => 'z', 'Ə' => 'e', 'ƒ' => 'f', 'Ơ' => 'o', 'ơ' => 'o', 'Ư' => 'u',
  2158. 'ư' => 'u', 'Ǎ' => 'a', 'ǎ' => 'a', 'Ǐ' => 'i', 'ǐ' => 'i', 'Ǒ' => 'o',
  2159. 'ǒ' => 'o', 'Ǔ' => 'u', 'ǔ' => 'u', 'Ǖ' => 'u', 'ǖ' => 'u', 'Ǘ' => 'u',
  2160. 'ǘ' => 'u', 'Ǚ' => 'u', 'ǚ' => 'u', 'Ǜ' => 'u', 'ǜ' => 'u', 'Ǻ' => 'a',
  2161. 'ǻ' => 'a', 'Ǽ' => 'ae','ǽ' => 'ae','Ǿ' => 'o', 'ǿ' => 'o', 'ə' => 'e',
  2162. 'Ё' => 'jo','Є' => 'e', 'І' => 'i', 'Ї' => 'i', 'А' => 'a', 'Б' => 'b',
  2163. 'В' => 'v', 'Г' => 'g', 'Д' => 'd', 'Е' => 'e', 'Ж' => 'zh','З' => 'z',
  2164. 'И' => 'i', 'Й' => 'j', 'К' => 'k', 'Л' => 'l', 'М' => 'm', 'Н' => 'n',
  2165. 'О' => 'o', 'П' => 'p', 'Р' => 'r', 'С' => 's', 'Т' => 't', 'У' => 'u',
  2166. 'Ф' => 'f', 'Х' => 'h', 'Ц' => 'c', 'Ч' => 'ch','Ш' => 'sh','Щ' => 'sch',
  2167. 'Ъ' => '-', 'Ы' => 'y', 'Ь' => '-', 'Э' => 'je','Ю' => 'ju','Я' => 'ja',
  2168. 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e',
  2169. 'ж' => 'zh','з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l',
  2170. 'м' => 'm', 'н' => 'n', 'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's',
  2171. 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c', 'ч' => 'ch',
  2172. 'ш' => 'sh','щ' => 'sch','ъ' => '-','ы' => 'y', 'ь' => '-', 'э' => 'je',
  2173. 'ю' => 'ju','я' => 'ja','ё' => 'jo','є' => 'e', 'і' => 'i', 'ї' => 'i',
  2174. 'Ґ' => 'g', 'ґ' => 'g', 'א' => 'a', 'ב' => 'b', 'ג' => 'g', 'ד' => 'd',
  2175. 'ה' => 'h', 'ו' => 'v', 'ז' => 'z', 'ח' => 'h', 'ט' => 't', 'י' => 'i',
  2176. 'ך' => 'k', 'כ' => 'k', 'ל' => 'l', 'ם' => 'm', 'מ' => 'm', 'ן' => 'n',
  2177. 'נ' => 'n', 'ס' => 's', 'ע' => 'e', 'ף' => 'p', 'פ' => 'p', 'ץ' => 'C',
  2178. 'צ' => 'c', 'ק' => 'q', 'ר' => 'r', 'ש' => 'w', 'ת' => 't', '™' => 'tm',
  2179. );
  2180. /**
  2181. * Default timezone
  2182. *
  2183. * @var string
  2184. */
  2185. protected $_defaultTz = 'America/Los_Angeles';
  2186. /**
  2187. * Default locale
  2188. *
  2189. * @var string
  2190. */
  2191. protected $_defaultLocale = 'en_US';
  2192. /**
  2193. * Cache for DateTimeZone objects
  2194. *
  2195. * @var DateTimeZone
  2196. */
  2197. protected $_tzCache = array();
  2198. /**
  2199. * Translations tree
  2200. *
  2201. * static::$_tr = array(
  2202. * 'STRING1' => 'DEFAULT TRANSLATION',
  2203. * 'STRING2' => array(
  2204. * '_' => 'DEFAULT TRANSLATION',
  2205. * 'Module1' => 'MODULE1 TRANSLATION',
  2206. * 'Module2' => 'MODULE2 TRANSLATION',
  2207. * ...
  2208. * ),
  2209. * );
  2210. *
  2211. * @var array
  2212. */
  2213. protected static $_tr;
  2214. /**
  2215. * Shortcut to help with IDE autocompletion
  2216. *
  2217. * @param bool $new
  2218. * @param array $args
  2219. * @return BLocale
  2220. */
  2221. public static function i($new=false, array $args=array())
  2222. {
  2223. return BClassRegistry::i()->instance(__CLASS__, $args, !$new);
  2224. }
  2225. /**
  2226. * Constructor, set default timezone and locale
  2227. *
  2228. */
  2229. public function __construct()
  2230. {
  2231. date_default_timezone_set($this->_defaultTz);
  2232. setlocale(LC_ALL, $this->_defaultLocale);
  2233. $this->_tzCache['GMT'] = new DateTimeZone('GMT');
  2234. }
  2235. public static function transliterate($str, $filler='-')
  2236. {
  2237. return strtolower(trim(preg_replace('#[^0-9a-z]+#i', $filler,
  2238. strtr($str, static::$_transliterateMap)), $filler));
  2239. }
  2240. public static function setCurrentLanguage($lang)
  2241. {
  2242. self::$_currentLanguage = $lang;
  2243. }
  2244. public static function getCurrentLanguage()
  2245. {
  2246. if (empty(static::$_currentLanguage)) {
  2247. static::$_currentLanguage = static::$_defaultLanguage;
  2248. }
  2249. return static::$_currentLanguage;
  2250. }
  2251. /**
  2252. * Import translations to the tree
  2253. *
  2254. * @todo make more flexible with file location
  2255. * @todo YAML
  2256. * @param mixed $data array or file name string
  2257. */
  2258. public static function importTranslations($data, $params=array())
  2259. {
  2260. $module = !empty($params['_module']) ? $params['_module'] : BModuleRegistry::i()->currentModuleName();
  2261. if (is_string($data)) {
  2262. if (!BUtil::isPathAbsolute($data)) {
  2263. $data = BApp::m($module)->root_dir.'/i18n/'.$data;
  2264. }
  2265. if (is_readable($data)) {
  2266. $extension = !empty($params['extension']) ? $params['extension'] : 'csv';
  2267. switch ($extension) {
  2268. case 'csv':
  2269. $fp = fopen($data, 'r');
  2270. while (($r = fgetcsv($fp, 2084))) {
  2271. static::addTranslation($r, $module);
  2272. }
  2273. fclose($fp);
  2274. break;
  2275. case 'json':
  2276. $content = file_get_contents($data);
  2277. $translations = BUtil::fromJson($content);
  2278. foreach ($translations as $word => $tr) {
  2279. static::addTranslation(array($word,$tr), $module);
  2280. }
  2281. break;
  2282. case 'php':
  2283. $translations = include $data;
  2284. foreach ($translations as $word => $tr) {
  2285. static::addTranslation(array($word,$tr), $module);
  2286. }
  2287. break;
  2288. case 'po':
  2289. //TODO: implement https://github.com/clinisbut/PHP-po-parser
  2290. $contentLines = file($data);
  2291. $translations = array();
  2292. $mode = null;
  2293. foreach ($contentLines as $line) {
  2294. $line = trim($line);
  2295. if ($line[0]==='"') {
  2296. $cmd = '+'.$mode;
  2297. $str = $line;
  2298. } else {
  2299. list($cmd, $str) = explode(' ', $line, 2);
  2300. }
  2301. $str = preg_replace('/(^\s*"|"\s*$)/', '', $str);
  2302. switch ($cmd) {
  2303. case 'msgid': $msgid = $str; $mode = $cmd; $translations[$msgid] = ''; break;
  2304. case '+msgid': $msgid .= $str; break;
  2305. case 'msgstr': $mode = $cmd; $translations[$msgid] = $str; break;
  2306. case '+msgstr': $translations[$msgid] .= $str; break;
  2307. }
  2308. }
  2309. break;
  2310. }
  2311. } else {
  2312. BDebug::warning('Could not load translation file: '.$data);
  2313. return;
  2314. }
  2315. } elseif (is_array($data)) {
  2316. foreach ($data as $r) {
  2317. static::addTranslation($r, $module);
  2318. }
  2319. }
  2320. }
  2321. /**
  2322. * Collect all translation keys & values start from $rootDir and save into $targetFile
  2323. * @param string $rootDir - start directory to look for translation calls BLocale::_
  2324. * @param string $targetFile - output file which contain translation values
  2325. * @return boolean - TRUE on success
  2326. * @example BLocale::collectTranslations('/www/unirgy/fulleron/FCom/Disqus', '/www/unirgy/fulleron/FCom/Disqus/tr.csv');
  2327. */
  2328. static public function collectTranslations($rootDir, $targetFile)
  2329. {
  2330. //find files recursively
  2331. $files = self::getFilesFromDir($rootDir);
  2332. if (empty($files)) {
  2333. return true;
  2334. }
  2335. //find all BLocale::_ calls and extract first parameter - translation key
  2336. $keys = array();
  2337. foreach($files as $file) {
  2338. $source = file_get_contents($file);
  2339. $tokens = token_get_all($source);
  2340. $func = 0;
  2341. $class = 0;
  2342. $sep = 0;
  2343. foreach($tokens as $token) {
  2344. if (empty($token[1])){
  2345. continue;
  2346. }
  2347. if ($token[1] =='BLocale') {
  2348. $class = 1;
  2349. continue;
  2350. }
  2351. if ($class && $token[1] == '::') {
  2352. $class = 0;
  2353. $sep = 1;
  2354. continue;
  2355. }
  2356. if ($sep && $token[1] == '_') {
  2357. $sep = 0;
  2358. $func = 1;
  2359. continue;
  2360. }
  2361. if($func) {
  2362. $token[1] = trim($token[1], "'");
  2363. $keys[$token[1]] = '';
  2364. $func = 0;
  2365. continue;
  2366. }
  2367. }
  2368. }
  2369. //import translation from $targetFile
  2370. self::$_tr = '';
  2371. self::addTranslationsFile($targetFile);
  2372. $translations = self::getTranslations();
  2373. //find undefined translations
  2374. foreach ($keys as $key => $v) {
  2375. if(isset($translations[$key])) {
  2376. unset($keys[$key]);
  2377. }
  2378. }
  2379. //add undefined translation to $targetFile
  2380. $newtranslations = array();
  2381. if ($translations) {
  2382. foreach($translations as $trkey => $tr){
  2383. list(,$newtranslations[$trkey]) = each($tr);
  2384. }
  2385. }
  2386. $newtranslations = array_merge($newtranslations, $keys);
  2387. $ext = strtolower(pathinfo($targetFile, PATHINFO_EXTENSION));
  2388. switch ($ext) {
  2389. case 'php':
  2390. static::saveToPHP($targetFile, $newtranslations);
  2391. break;
  2392. case 'csv':
  2393. static::saveToCSV($targetFile, $newtranslations);
  2394. break;
  2395. case 'json':
  2396. static::saveToJSON($targetFile, $newtranslations);
  2397. break;
  2398. case 'po':
  2399. static::saveToJSON($targetFile, $newtranslations);
  2400. default:
  2401. throw new Exception("Undefined format of translation targetFile. Possible formats are: json/csv/php");
  2402. }
  2403. }
  2404. static protected function saveToPHP($targetFile, $array)
  2405. {
  2406. $code = '';
  2407. foreach($array as $k => $v) {
  2408. if (!empty($code)) {
  2409. $code .= ','."\n";
  2410. }
  2411. $code .= "'{$k}' => '".addslashes($v)."'";
  2412. }
  2413. $code = "<?php return array({$code});";
  2414. file_put_contents($targetFile, $code);
  2415. }
  2416. static protected function saveToJSON($targetFile, $array)
  2417. {
  2418. $json = json_encode($array);
  2419. file_put_contents($targetFile, $json);
  2420. }
  2421. static protected function saveToCSV($targetFile, $array)
  2422. {
  2423. $handle = fopen($targetFile, "w");
  2424. foreach ($array as $k => $v) {
  2425. $k = trim($k, '"');
  2426. fputcsv($handle, array($k, $v));
  2427. }
  2428. fclose($handle);
  2429. }
  2430. static protected function saveToPO($targetFile, $array)
  2431. {
  2432. $handle = fopen($targetFile, "w");
  2433. foreach ($array as $k => $v) {
  2434. $v = str_replace("\n", '\n', $v);
  2435. fwrite($handle, "msgid \"{$k}\"\nmsgstr \"{$v}\"\n\n");
  2436. }
  2437. fclose($handle);
  2438. }
  2439. static public function getFilesFromDir($dir)
  2440. {
  2441. $files = array();
  2442. if (false !== ($handle = opendir($dir))) {
  2443. while (false !== ($file = readdir($handle))) {
  2444. if ($file != "." && $file != "..") {
  2445. if(is_dir($dir.'/'.$file)) {
  2446. $dir2 = $dir.'/'.$file;
  2447. $files = array_merge($files, self::getFilesFromDir($dir2));
  2448. }
  2449. else {
  2450. $files[] = $dir.'/'.$file;
  2451. }
  2452. }
  2453. }
  2454. closedir($handle);
  2455. }
  2456. return $files;
  2457. }
  2458. static public function addTranslationsFile($file)
  2459. {
  2460. $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
  2461. if (empty($ext)) {
  2462. return;
  2463. }
  2464. $params['extension'] = $ext;
  2465. self::importTranslations($file, $params);
  2466. }
  2467. protected static function addTranslation($r, $module=null)
  2468. {
  2469. if (empty($r[1])) {
  2470. BDebug::debug('No translation specified for '.$r[0]);
  2471. return;
  2472. }
  2473. // short and quick way
  2474. static::$_tr[ $r[0] ][ !empty($module) ? $module : '_' ] = $r[1];
  2475. /*
  2476. // a bit of memory saving way
  2477. list($from, $to) = $r;
  2478. if (!empty($module)) { // module supplied
  2479. if (!empty(static::$_tr[$from]) && is_string(static::$_tr[$from])) { // only default translation present
  2480. static::$_tr[$from] = array('_'=>static::$_tr[$from]); // convert to array
  2481. }
  2482. static::$_tr[$from][$module] = $to; // save module specific translation
  2483. } else { // no module, default translation
  2484. if (!empty(static::$_tr[$from]) && is_array(static::$_tr[$from])) { // modular translations present
  2485. static::$_tr[$from]['_'] = $to; // play nice
  2486. } else {
  2487. static::$_tr[$from] = $to; // simple
  2488. }
  2489. }
  2490. */
  2491. }
  2492. public static function cacheSave()
  2493. {
  2494. }
  2495. public static function cacheLoad()
  2496. {
  2497. }
  2498. public static function _($string, $params=array(), $module=null)
  2499. {
  2500. if (empty(static::$_tr[$string])) { // if no translation at all
  2501. $tr = $string; // return original string
  2502. } else { // if some translation present
  2503. $arr = static::$_tr[$string];
  2504. if (!empty($module) && !empty($arr[$module])) { // if module requested and translation for it present
  2505. $tr = $arr[$module]; // use it
  2506. } elseif (!empty($arr['_'])) { // otherwise, if there's default translation
  2507. $tr = $arr['_']; // use it
  2508. } else { // otherwise
  2509. reset($arr); // find the first available translation
  2510. $tr = current($arr); // and use it
  2511. }
  2512. }
  2513. return BUtil::sprintfn($tr, $params);
  2514. }
  2515. /*
  2516. public function language($lang=null)
  2517. {
  2518. if (is_null($lang)) {
  2519. return $this->_curLang;
  2520. }
  2521. putenv('LANGUAGE='.$lang);
  2522. putenv('LANG='.$lang);
  2523. setlocale(LC_ALL, $lang.'.utf8', $lang.'.UTF8', $lang.'.utf-8', $lang.'.UTF-8');
  2524. return $this;
  2525. }
  2526. public function module($domain, $file=null)
  2527. {
  2528. if (is_null($file)) {
  2529. if (!is_null($domain)) {
  2530. $domain = static::$_domainPrefix.$domain;
  2531. $oldDomain = textdomain(null);
  2532. if ($oldDomain) {
  2533. array_push(static::$_domainStack, $domain!==$oldDomain ? $domain : false);
  2534. }
  2535. } else {
  2536. $domain = array_pop(static::$_domainStack);
  2537. }
  2538. if ($domain) {
  2539. textdomain($domain);
  2540. }
  2541. } else {
  2542. $domain = static::$_domainPrefix.$domain;
  2543. bindtextdomain($domain, $file);
  2544. bind_textdomain_codeset($domain, "UTF-8");
  2545. }
  2546. return $this;
  2547. }
  2548. */
  2549. /**
  2550. * Translate a string and inject optionally named arguments
  2551. *
  2552. * @param string $string
  2553. * @param array $args
  2554. * @return string|false
  2555. */
  2556. /*
  2557. public function translate($string, $args=array(), $domain=null)
  2558. {
  2559. if (!is_null($domain)) {
  2560. $string = dgettext($domain, $string);
  2561. } else {
  2562. $string = gettext($string);
  2563. }
  2564. return BUtil::sprintfn($string, $args);
  2565. }
  2566. */
  2567. /**
  2568. * Get server timezone
  2569. *
  2570. * @return string
  2571. */
  2572. public function serverTz()
  2573. {
  2574. return date('e'); // Examples: UTC, GMT, Atlantic/Azores
  2575. }
  2576. /**
  2577. * Get timezone offset in seconds
  2578. *
  2579. * @param stirng|null $tz If null, return server timezone offset
  2580. * @return int
  2581. */
  2582. public function tzOffset($tz=null)
  2583. {
  2584. if (is_null($tz)) { // Server timezone
  2585. return date('O') * 36; // x/100*60*60; // Seconds from GMT
  2586. }
  2587. if (empty($this->_tzCache[$tz])) {
  2588. $this->_tzCache[$tz] = new DateTimeZone($tz);
  2589. }
  2590. return $this->_tzCache[$tz]->getOffset($this->_tzCache['GMT']);
  2591. }
  2592. /**
  2593. * Convert local datetime to DB (GMT)
  2594. *
  2595. * @param string $value
  2596. * @return string
  2597. */
  2598. public function datetimeLocalToDb($value)
  2599. {
  2600. if (is_array($value)) {
  2601. return array_map(array($this, __METHOD__), $value);
  2602. }
  2603. if (!$value) return $value;
  2604. return gmstrftime('%F %T', strtotime($value));
  2605. }
  2606. /**
  2607. * Parse user formatted dates into db style within object or array
  2608. *
  2609. * @param array|object $request fields to be parsed
  2610. * @param null|string|array $fields if null, all fields will be parsed, if string, will be split by comma
  2611. * @return array|object clone of $request with parsed dates
  2612. */
  2613. public function parseRequestDates($request, $fields=null)
  2614. {
  2615. if (is_string($fields)) $fields = explode(',', $fields);
  2616. $isObject = is_object($request);
  2617. if ($isObject) $result = clone $request;
  2618. foreach ($request as $k=>$v) {
  2619. if (is_null($fields) || in_array($k, $fields)) {
  2620. $r = $this->datetimeLocalToDb($v);
  2621. } else {
  2622. $r = $v;
  2623. }
  2624. if ($isObject) $result->$k = $r; else $result[$k] = $r;
  2625. }
  2626. return $result;
  2627. }
  2628. /**
  2629. * Convert DB datetime (GMT) to local
  2630. *
  2631. * @param string $value
  2632. * @param bool $full Full format or short
  2633. * @return string
  2634. */
  2635. public function datetimeDbToLocal($value, $full=false)
  2636. {
  2637. return strftime($full ? '%c' : '%x', strtotime($value));
  2638. }
  2639. static public function getTranslations()
  2640. {
  2641. return self::$_tr;
  2642. }
  2643. static protected $_currencySymbolMap = array(
  2644. 'USD' => '$',
  2645. 'EUR' => '',
  2646. 'GBP' => '',
  2647. );
  2648. static protected $_currencyCode = 'USD';
  2649. static protected $_currencySymbol = '$';
  2650. static public function setCurrency($code, $symbol = null)
  2651. {
  2652. static::$_currencyCode = $code;
  2653. if (is_null($symbol)) {
  2654. if (!empty(static::$_currencySymbolMap[$code])) {
  2655. $symbol = static::$_currencySymbolMap[$code];
  2656. } else {
  2657. $symbol = $code.' ';
  2658. }
  2659. }
  2660. static::$_currencySymbol = $symbol;
  2661. }
  2662. static public function currency($value, $decimals = 2)
  2663. {
  2664. return sprintf('%s%s', static::$_currencySymbol, number_format($value, $decimals));
  2665. }
  2666. }
  2667. class BFtpClient extends BClass
  2668. {
  2669. protected $_ftpDirMode = 0775;
  2670. protected $_ftpFileMode = 0664;
  2671. protected $_ftpHost = '';
  2672. protected $_ftpPort = 21;
  2673. protected $_ftpUsername = '';
  2674. protected $_ftpPassword = '';
  2675. public function __construct($config)
  2676. {
  2677. if (!empty($config['hostname'])) {
  2678. $this->_ftpHost = $config['hostname'];
  2679. }
  2680. if (!empty($config['port'])) {
  2681. $this->_ftpPort = $config['port'];
  2682. }
  2683. if (!empty($config['username'])) {
  2684. $this->_ftpUsername = $config['username'];
  2685. }
  2686. if (!empty($config['password'])) {
  2687. $this->_ftpPassword = $config['password'];
  2688. }
  2689. }
  2690. public function upload($from, $to)
  2691. {
  2692. if (!extension_loaded('ftp')) {
  2693. new BException('FTP PHP extension is not installed');
  2694. }
  2695. if (!($conn = ftp_connect($this->_ftpHost, $this->_ftpPort))) {
  2696. throw new BException('Could not connect to FTP host');
  2697. }
  2698. if (!@ftp_login($conn, $this->_ftpUsername, $this->_ftpPassword)) {
  2699. ftp_close($conn);
  2700. throw new BException('Could not login to FTP host');
  2701. }
  2702. if (!ftp_chdir($conn, $to)) {
  2703. ftp_close($conn);
  2704. throw new BException('Could not navigate to '. $to);
  2705. }
  2706. $errors = $this->uploadDir($conn, $from.'/');
  2707. ftp_close($conn);
  2708. return $errors;
  2709. }
  2710. public function uploadDir($conn, $source, $ftpPath='')
  2711. {
  2712. $errors = array();
  2713. $dir = opendir($source);
  2714. while ($file = readdir($dir)) {
  2715. if ($file=='.' || $file=="..") {
  2716. continue;
  2717. }
  2718. if (!is_dir($source.$file)) {
  2719. if (@ftp_put($conn, $file, $source.$file, FTP_BINARY)) {
  2720. // all is good
  2721. #ftp_chmod($conn, $this->_ftpFileMode, $file);
  2722. } else {
  2723. $errors[] = ftp_pwd($conn).'/'.$file;
  2724. }
  2725. continue;
  2726. }
  2727. if (@ftp_chdir($conn, $file)) {
  2728. // all is good
  2729. } elseif (@ftp_mkdir($conn, $file)) {
  2730. ftp_chmod($conn, $this->_ftpDirMode, $file);
  2731. ftp_chdir($conn, $file);
  2732. } else {
  2733. $errors[] = ftp_pwd($conn).'/'.$file.'/';
  2734. continue;
  2735. }
  2736. $errors += $this->uploadDir($conn, $source.$file.'/', $ftpPath.$file.'/');
  2737. ftp_chdir($conn, '..');
  2738. }
  2739. return $errors;
  2740. }
  2741. }
  2742. /**
  2743. * Throttle invalid login attempts and potentially notify user and admin
  2744. *
  2745. * Usage:
  2746. * - BEFORE AUTH: BLoginThrottle::i()->init('FCom_Customer_Model_Customer', $username);
  2747. * - ON FAILURE: BLoginThrottle::i()->failure();
  2748. * - ON SUCCESS: BloginThrottle::i()->success();
  2749. */
  2750. class BLoginThrottle extends BClass
  2751. {
  2752. protected $_all;
  2753. protected $_area;
  2754. protected $_username;
  2755. protected $_rec;
  2756. protected $_config;
  2757. protected $_blockedIPs = array();
  2758. protected $_cachePrefix = 'BLoginThrottle/';
  2759. /**
  2760. * Shortcut to help with IDE autocompletion
  2761. *
  2762. * @return BLoginThrottle
  2763. */
  2764. public static function i($new=false, array $args=array())
  2765. {
  2766. return BClassRegistry::i()->instance(__CLASS__, $args, !$new);
  2767. }
  2768. public function __construct()
  2769. {
  2770. $c = BConfig::i()->get('modules/BLoginThrottle');
  2771. if (empty($c['sleep_sec'])) $c['sleep_sec'] = 2; // lock record for 2 secs after failed login
  2772. if (empty($c['brute_attempts_max'])) $c['brute_attempts_max'] = 3; // after 3 fast attempts do something
  2773. if (empty($c['reset_time'])) $c['reset_time'] = 10; // after 10 secs reset record
  2774. $this->_config = $c;
  2775. }
  2776. public function config($config)
  2777. {
  2778. $this->_config = BUtil::arrayMerge($this->_config, $config);
  2779. }
  2780. public function init($area, $username)
  2781. {
  2782. $now = time();
  2783. $c = $this->_config;
  2784. $this->_area = $area;
  2785. $this->_username = $username;
  2786. $this->_rec = $this->_load();
  2787. if ($this->_rec) {
  2788. if ($this->_rec['status'] === 'FAILED') {
  2789. if (empty($this->_rec['brute_attempts_cnt'])) {
  2790. $this->_rec['brute_attempts_cnt'] = 1;
  2791. } else {
  2792. $this->_rec['brute_attempts_cnt']++;
  2793. }
  2794. $this->_save();
  2795. $this->_fire('init:brute');
  2796. if ($this->_rec['brute_attempts_cnt'] == $c['brute_attempts_max']) {
  2797. $this->_fire('init:brute_max');
  2798. }
  2799. return false; // currently locked
  2800. }
  2801. }
  2802. return true; // init OK
  2803. }
  2804. public function success()
  2805. {
  2806. $this->_fire('success');
  2807. $this->_reset();
  2808. return true;
  2809. }
  2810. public function failure()
  2811. {
  2812. $username = $this->_username;
  2813. $now = time();
  2814. $c = $this->_config;
  2815. $this->_fire('fail:before');
  2816. if (empty($this->_rec['attempt_cnt'])) {
  2817. $this->_rec['attempt_cnt'] = 1;
  2818. } else {
  2819. $this->_rec['attempt_cnt']++;
  2820. }
  2821. $this->_rec['last_attempt'] = $now;
  2822. $this->_rec['status'] = 'FAILED';
  2823. $this->_save();
  2824. $this->_fire('fail:wait');
  2825. $this->_gc();
  2826. sleep($c['sleep_sec']);
  2827. $this->_rec['status'] = '';
  2828. $this->_save();
  2829. $this->_fire('fail:after');
  2830. return true; // normal response
  2831. }
  2832. protected function _fire($event)
  2833. {
  2834. BEvents::i()->fire('BLoginThrottle::'.$event, array(
  2835. 'area' => $this->_area,
  2836. 'username' => $this->_username,
  2837. 'rec' => $this->_rec,
  2838. 'config' => $this->_config,
  2839. ));
  2840. }
  2841. protected function _load()
  2842. {
  2843. $key = $this->_area.'/'.$this->_username;
  2844. return BCache::i()->load($this->_cachePrefix.$key);
  2845. }
  2846. protected function _save()
  2847. {
  2848. $key = $this->_area.'/'.$this->_username;
  2849. return BCache::i()->save($this->_cachePrefix.$key, $this->_rec, $this->_config['reset_time']);
  2850. }
  2851. protected function _reset()
  2852. {
  2853. $key = $this->_area.'/'.$this->_username;
  2854. return BCache::i()->delete($key);
  2855. }
  2856. protected function _gc()
  2857. {
  2858. return true;
  2859. }
  2860. }
  2861. /**
  2862. * Falls back to pecl extensions: yaml, syck
  2863. * Uses libraries: spyc, symphony\yaml (not included)
  2864. */
  2865. class BYAML extends BCLass
  2866. {
  2867. static protected $_peclYaml = null;
  2868. static protected $_peclSyck = null;
  2869. static public function bootstrap()
  2870. {
  2871. }
  2872. static public function load($filename, $cache=true)
  2873. {
  2874. $filename1 = realpath($filename);
  2875. if (!$filename1) {
  2876. BDebug::debug('BCache load: file does not exist: '.$filename);
  2877. return false;
  2878. }
  2879. $filename = $filename1;
  2880. $filemtime = filemtime($filename);
  2881. if ($cache) {
  2882. $cacheData = BCache::i()->load('BYAML--'.$filename);
  2883. if (!empty($cacheData) && !empty($cacheData['v']) && $cacheData['v'] === $filemtime) {
  2884. return $cacheData['d'];
  2885. }
  2886. }
  2887. $yamlData = file_get_contents($filename);
  2888. $yamlData = str_replace("\t", ' ', $yamlData); //TODO: make configurable tab size
  2889. $arrayData = static::parse($yamlData);
  2890. if ($cache) {
  2891. BCache::i()->save('BYAML--'.$filename, array('v'=>$filemtime, 'd'=>$arrayData), false);
  2892. }
  2893. return $arrayData;
  2894. }
  2895. static public function init()
  2896. {
  2897. if (is_null(static::$_peclYaml)) {
  2898. static::$_peclYaml = function_exists('yaml_parse');
  2899. if (!static::$_peclYaml) {
  2900. static::$_peclSyck = function_exists('syck_load');
  2901. }
  2902. if (!static::$_peclYaml && !static::$_peclSyck) {
  2903. require_once(__DIR__.'/lib/spyc.php');
  2904. /*
  2905. require_once(__DIR__.'/Yaml/Exception/ExceptionInterface.php');
  2906. require_once(__DIR__.'/Yaml/Exception/RuntimeException.php');
  2907. require_once(__DIR__.'/Yaml/Exception/DumpException.php');
  2908. require_once(__DIR__.'/Yaml/Exception/ParseException.php');
  2909. require_once(__DIR__.'/Yaml/Yaml.php');
  2910. require_once(__DIR__.'/Yaml/Parser.php');
  2911. require_once(__DIR__.'/Yaml/Dumper.php');
  2912. require_once(__DIR__.'/Yaml/Escaper.php');
  2913. require_once(__DIR__.'/Yaml/Inline.php');
  2914. require_once(__DIR__.'/Yaml/Unescaper.php');
  2915. */
  2916. }
  2917. }
  2918. return true;
  2919. }
  2920. static public function parse($yamlData)
  2921. {
  2922. static::init();
  2923. if (static::$_peclYaml) {
  2924. return yaml_parse($yamlData);
  2925. } elseif (static::$_peclSyck) {
  2926. return syck_load($yamlData);
  2927. }
  2928. if (class_exists('Spyc', false)) {
  2929. return Spyc::YAMLLoadString($yamlData);
  2930. } else {
  2931. return Symfony\Component\Yaml\Yaml::parse($yamlData);
  2932. }
  2933. }
  2934. static public function dump($arrayData)
  2935. {
  2936. static::init();
  2937. if (static::$_peclYaml) {
  2938. return yaml_emit($arrayData);
  2939. } elseif (static::$_peclSyck) {
  2940. return syck_dump($arrayData);
  2941. }
  2942. if (class_exists('Spyc', false)) {
  2943. return Spyc::YAMLDump($arrayData);
  2944. } else {
  2945. return Symfony\Component\Yaml\Yaml::dump($arrayData);
  2946. }
  2947. }
  2948. }
  2949. class BValidate extends BClass
  2950. {
  2951. protected $_reRegex = '#^([/\#~&,%])(.*)(\1)[imsxADSUXJu]*$#';
  2952. protected $_defaultRules = array(
  2953. 'required' => array(
  2954. 'rule' => 'BValidate::ruleRequired',
  2955. 'message' => 'Missing field: :field',
  2956. ),
  2957. 'url' => array(
  2958. 'rule' => '#(([\w]+:)?//)?(([\d\w]|%[a-fA-f\d]{2,2})+(:([\d\w]|%[a-fA-f\d]{2,2})+)?@)?([\d\w][-\d\w]{0,253}[\d\w]\.)+[\w]{2,4}(:[\d]+)?(/([-+_~.\d\w]|%[a-fA-f\d]{2,2})*)*(\?(&?([-+_~.\d\w]|%[a-fA-f\d]{2,2})=?)*)?(\#([-+_~.\d\w]|%[a-fA-f\d]{2,2})*)?#',
  2959. 'message' => 'Invalid URL',
  2960. ),
  2961. 'email' => array(
  2962. 'rule' => '/^([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/',
  2963. 'message' => 'Invalid Email',
  2964. ),
  2965. 'numeric' => array(
  2966. 'rule' => '/^([+-]?)([0-9 ]+)(\.?,?)([0-9]*)$/',
  2967. 'message' => 'Invalid number: :field',
  2968. ),
  2969. 'integer' => array(
  2970. 'rule' => '/^[+-][0-9]+$/',
  2971. 'message' => 'Invalid integer: :field',
  2972. ),
  2973. 'alphanum' => array(
  2974. 'rule' => '/^[a-zA-Z0-9 ]+$/',
  2975. 'message' => 'Invalid alphanumeric: :field',
  2976. ),
  2977. 'alpha' => array(
  2978. 'rule' => '/^[a-zA-Z ]+$/',
  2979. 'message' => 'Invalid alphabet field: :field',
  2980. ),
  2981. 'password_confirm' => array(
  2982. 'rule' => 'BValidate::rulePasswordConfirm',
  2983. 'message' => 'Password confirmation does not match',
  2984. 'args' => array('original' => 'password'),
  2985. ),
  2986. );
  2987. protected $_defaultMessage = "Validation failed for: :field";
  2988. protected $_expandedRules = array();
  2989. protected $_validateErrors = array();
  2990. public function addValidator($name, $rule)
  2991. {
  2992. $this->_defaultRules[$name] = $rule;
  2993. return $this;
  2994. }
  2995. protected function _expandRules($rules)
  2996. {
  2997. $this->_expandedRules = array();
  2998. foreach ($rules as $rule) {
  2999. if (!empty($rule[0]) && !empty($rule[1])) {
  3000. $r = $rule;
  3001. $rule = array('field' => $r[0], 'rule' => $r[1]);
  3002. if (isset($r[2])) $rule['message'] = $r[2];
  3003. if (isset($r[3])) $rule['args'] = $r[3];
  3004. if (isset($rule['args']) && is_string($rule['args'])) {
  3005. $rule['args'] = array($rule['args'] => true);
  3006. }
  3007. }
  3008. if (is_string($rule['rule']) && $rule['rule'][0] === '@') {
  3009. $ruleName = substr($rule['rule'], 1);
  3010. if (empty($this->_defaultRules[$ruleName])) {
  3011. throw new BException('Invalid rule name: ' . $ruleName);
  3012. }
  3013. $defRule = $this->_defaultRules[$ruleName];
  3014. $rule = BUtil::arrayMerge($defRule, $rule);
  3015. $rule['rule'] = $defRule['rule'];
  3016. }
  3017. if(empty($rule['message'])) $rule['message'] = $this->_defaultMessage;
  3018. $this->_expandedRules[] = $rule;
  3019. }
  3020. }
  3021. protected function _validateRules($data)
  3022. {
  3023. $this->_validateErrors = array();
  3024. foreach ($this->_expandedRules as $r) {
  3025. $args = !empty($r['args']) ? $r['args'] : array();
  3026. $r['args']['field'] = $r['field']; // for callback and message vars
  3027. if (is_string($r['rule']) && preg_match($this->_reRegex, $r['rule'], $m)) {
  3028. $result = empty($data[$r['field']]) || preg_match($m[0], $data[$r['field']]);
  3029. } elseif($r['rule'] instanceof Closure){
  3030. $result = $r['rule']($data, $r['args']);
  3031. } elseif (is_callable($r['rule'])) {
  3032. $result = BUtil::call($r['rule'], array($data, $r['args']), true);
  3033. } else {
  3034. throw new BException('Invalid rule: '.print_r($r['rule'], 1));
  3035. }
  3036. if (!$result) {
  3037. $this->_validateErrors[$r['field']][] = BUtil::injectVars($r['message'], $r['args']);
  3038. if (!empty($r['args']['break'])) {
  3039. break;
  3040. }
  3041. }
  3042. }
  3043. }
  3044. /**
  3045. * Validate passed data
  3046. *
  3047. * $data is an array of key value pairs.
  3048. * Keys will be matched against rules.
  3049. * <code>
  3050. * // data
  3051. * array (
  3052. * 'firstname' => 'John',
  3053. * 'lastname' => 'Doe',
  3054. * 'email' => 'test@example.com',
  3055. * 'url' => 'http://example.com/test?foo=bar#baz',
  3056. * 'password' => '12345678',
  3057. * 'password_confirm' => '12345678',
  3058. * );
  3059. *
  3060. * // rules in format: ['field', 'rule', ['message'], [ 'break' | 'arg1' => 'val1' ] ]
  3061. * $rules = array(
  3062. * array('email', '@required'),
  3063. * array('email', '@email'),
  3064. * array('url', '@url'),
  3065. * array('firstname', '@required', 'Missing First Name'),
  3066. * array('firstname', '/^[A-Za-z]+$/', 'Invalid First Name', 'break'),
  3067. * array('password', '@required', 'Missing Password'),
  3068. * array('password_confirm', '@password_confirm'),
  3069. * );
  3070. * </code>
  3071. *
  3072. * Rule can be either string that resolves to callback, regular expression or closure.
  3073. * Allowed pattern delimiters for regular expression are: /\#~&,%
  3074. * Allowed regular expression modifiers are: i m s x A D S U X J u
  3075. * e and E modifiers are NOT allowed. Any exptression using them will not work.
  3076. *
  3077. * Callbacks can be either: Class::method for static method call or Class.method | Class->method for instance call
  3078. *
  3079. * @param array $data
  3080. * @param array $rules
  3081. * @param null $formName
  3082. * @return bool
  3083. */
  3084. public function validateInput($data, $rules, $formName = null)
  3085. {
  3086. $this->_expandRules($rules);
  3087. $this->_validateRules($data);
  3088. if ($this->_validateErrors && $formName) {
  3089. BSession::i()->set('validator-data:' . $formName, $data);
  3090. foreach ($this->_validateErrors as $field => $errors) {
  3091. foreach ($errors as $error) {
  3092. $msg = compact('error', 'field');
  3093. BSession::i()->addMessage($msg, 'error', 'validator-errors:' . $formName);
  3094. }
  3095. }
  3096. }
  3097. return $this->_validateErrors ? false : true;
  3098. }
  3099. public function validateErrors()
  3100. {
  3101. return $this->_validateErrors;
  3102. }
  3103. static public function ruleRequired($data, $args)
  3104. {
  3105. return !empty($data[$args['field']]);
  3106. }
  3107. static public function rulePasswordConfirm($data, $args)
  3108. {
  3109. return empty($data[$args['original']])
  3110. || !empty($data[$args['field']]) && $data[$args['field']] === $data[$args['original']];
  3111. }
  3112. }
  3113. /**
  3114. * Class BValidateViewHelper
  3115. *
  3116. *
  3117. */
  3118. class BValidateViewHelper extends BClass
  3119. {
  3120. protected $_errors = array();
  3121. protected $_data = array();
  3122. public function __construct($args)
  3123. {
  3124. if(!isset($args['form'])){
  3125. return;
  3126. }
  3127. if (isset($args['data'])) {
  3128. if (is_object($args['data'])) {
  3129. $args['data'] = $args['data']->as_array();
  3130. }
  3131. $this->_data = $args['data'];
  3132. }
  3133. $sessionHlp = BSession::i();
  3134. $errors = $sessionHlp->messages('validator-errors:' . $args['form']);
  3135. $formData = $sessionHlp->get('validator-data:' . $args['form']);
  3136. $this->_data = BUtil::arrayMerge($this->_data, $formData);
  3137. $sessionHlp->set('validator-data:' . $args['form'], null);
  3138. foreach ($errors as $error) {
  3139. $field = $error['msg']['field'];
  3140. $error['value'] = !empty($formData[$field]) ? $formData[$field] : null;
  3141. $this->_errors[$field] = $error;
  3142. }
  3143. }
  3144. public function fieldClass($field)
  3145. {
  3146. if (empty($this->_errors[$field]['type'])) {
  3147. return '';
  3148. }
  3149. return $this->_errors[$field]['type'];
  3150. }
  3151. public function fieldValue($field)
  3152. {
  3153. return !empty($this->_data[$field]) ? $this->_data[$field] : null;
  3154. }
  3155. public function messageClass($field)
  3156. {
  3157. if (empty($this->_errors[$field]['type'])) {
  3158. return '';
  3159. }
  3160. return $this->_errors[$field]['type'];
  3161. }
  3162. public function messageText($field)
  3163. {
  3164. if (empty($this->_errors[$field]['msg']['error'])) {
  3165. return '';
  3166. }
  3167. return BLocale::_($this->_errors[$field]['msg']['error']);
  3168. }
  3169. /**
  3170. * @param string $field form field name
  3171. * @param string $fieldId form field ID
  3172. * @return string
  3173. */
  3174. public function errorHtml($field, $fieldId)
  3175. {
  3176. $html = '';
  3177. if(!empty($this->_errors[$field]['type'])){
  3178. $html .= BUtil::tagHtml('label', array('for' => $fieldId, 'class' => $this->messageClass($field)),$this->messageText($field));
  3179. }
  3180. return $html;
  3181. }
  3182. }
  3183. /**
  3184. * If FISMA/FIPS/NIST compliance required, use PBKDF2
  3185. *
  3186. * @see http://stackoverflow.com/questions/4795385/how-do-you-use-bcrypt-for-hashing-passwords-in-php
  3187. */
  3188. class Bcrypt extends BClass
  3189. {
  3190. public function __construct()
  3191. {
  3192. if (CRYPT_BLOWFISH != 1) {
  3193. throw new Exception("bcrypt not supported in this installation. See http://php.net/crypt");
  3194. }
  3195. }
  3196. public function hash($input)
  3197. {
  3198. $hash = crypt($input, $this->getSalt());
  3199. return strlen($hash) > 13 ? $hash : false;
  3200. }
  3201. public function verify($input, $existingHash)
  3202. {
  3203. // md5 for protection against timing side channel attack (needed)
  3204. return md5(crypt($input, $existingHash)) === md5($existingHash);
  3205. }
  3206. private function getSalt()
  3207. {
  3208. // The security weakness between 5.3.7 affects password with 8-bit characters only
  3209. // @see: http://php.net/security/crypt_blowfish.php
  3210. $salt = '$' . (version_compare(phpversion(), '5.3.7', '>=') ? '2y' : '2a') . '$12$';
  3211. $salt .= $this->encodeBytes($this->getRandomBytes(16));
  3212. return $salt;
  3213. }
  3214. private $randomState;
  3215. private function getRandomBytes($count)
  3216. {
  3217. $bytes = '';
  3218. if (function_exists('openssl_random_pseudo_bytes') &&
  3219. (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win
  3220. $bytes = openssl_random_pseudo_bytes($count);
  3221. }
  3222. if ($bytes === '' && is_readable('/dev/urandom') &&
  3223. ($hRand = @fopen('/dev/urandom', 'rb')) !== FALSE) {
  3224. $bytes = fread($hRand, $count);
  3225. fclose($hRand);
  3226. }
  3227. if (strlen($bytes) < $count) {
  3228. $bytes = '';
  3229. if ($this->randomState === null) {
  3230. $this->randomState = microtime();
  3231. if (function_exists('getmypid')) {
  3232. $this->randomState .= getmypid();
  3233. }
  3234. }
  3235. for ($i = 0; $i < $count; $i += 16) {
  3236. $this->randomState = md5(microtime() . $this->randomState);
  3237. $bytes .= md5($this->randomState, true);
  3238. }
  3239. $bytes = substr($bytes, 0, $count);
  3240. }
  3241. return $bytes;
  3242. }
  3243. private function encodeBytes($input)
  3244. {
  3245. // The following is code from the PHP Password Hashing Framework
  3246. $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  3247. $output = '';
  3248. $i = 0;
  3249. do {
  3250. $c1 = ord($input[$i++]);
  3251. $output .= $itoa64[$c1 >> 2];
  3252. $c1 = ($c1 & 0x03) << 4;
  3253. if ($i >= 16) {
  3254. $output .= $itoa64[$c1];
  3255. break;
  3256. }
  3257. $c2 = ord($input[$i++]);
  3258. $c1 |= $c2 >> 4;
  3259. $output .= $itoa64[$c1];
  3260. $c1 = ($c2 & 0x0f) << 2;
  3261. $c2 = ord($input[$i++]);
  3262. $c1 |= $c2 >> 6;
  3263. $output .= $itoa64[$c1];
  3264. $output .= $itoa64[$c2 & 0x3f];
  3265. } while (1);
  3266. return $output;
  3267. }
  3268. }
  3269. class BRSA extends BClass
  3270. {
  3271. protected $_configPath = 'modules/BRSA';
  3272. protected $_config = array();
  3273. protected $_publicKey;
  3274. protected $_privateKey;
  3275. protected $_cache = array();
  3276. public function __construct()
  3277. {
  3278. if (!function_exists('openssl_pkey_new')) {
  3279. // TODO: integrate Crypt_RSA ?
  3280. throw new BException('RSA encryption requires openssl module installed');
  3281. }
  3282. $defConf = array(
  3283. "digest_alg" => "sha512",
  3284. "private_key_bits" => 4096,
  3285. "private_key_type" => OPENSSL_KEYTYPE_RSA,
  3286. );
  3287. $conf = BConfig::i()->get($this->_configPath);
  3288. $this->_config = array_merge($defConf, $conf);
  3289. }
  3290. public function generateKey()
  3291. {
  3292. $config = BUtil::arrayMask($this->_config, 'digest_alg,x509_extensions,req_extensions,'
  3293. . 'private_key_bits,private_key_type,encrypt_key,encrypt_key_cipher');
  3294. $res = openssl_pkey_new($config);
  3295. openssl_pkey_export($res, $this->_privateKey); // private key
  3296. $pubKey = openssl_pkey_get_details($res); // public key
  3297. $this->_publicKey = $pubKey["key"];
  3298. BConfig::i()->set($this->_configPath.'/public_key', $this->_publicKey, false, true);
  3299. file_put_contents($this->_getPrivateKeyFileName(), $this->_privateKey);
  3300. return $this;
  3301. }
  3302. protected function _getPublicKey()
  3303. {
  3304. if (!$this->_publicKey) {
  3305. $this->_publicKey = BConfig::i()->get($this->_configPath.'/public_key');
  3306. if (!$this->_publicKey) {
  3307. throw new BException('No public key defined');
  3308. }
  3309. }
  3310. return $this->_publicKey;
  3311. }
  3312. protected function _getPrivateKeyFileName()
  3313. {
  3314. $configDir = BConfig::i()->get('fs/config_dir');
  3315. if (!$configDir) {
  3316. $configDir = '.';
  3317. }
  3318. return $configDir . '/private-' . md5($this->_getPublicKey()) . '.key';
  3319. }
  3320. protected function _getPrivateKey()
  3321. {
  3322. if (!$this->_privateKey) {
  3323. $filepath = $this->_getPrivateKeyFileName();
  3324. if (!is_readable($filepath)) {
  3325. throw new BException('No private key file found');
  3326. }
  3327. $this->_privateKey = file_get_contents($filepath);
  3328. }
  3329. return $this->_privateKey;
  3330. }
  3331. public function setPublicKey()
  3332. {
  3333. $this->_publicKey = $key;
  3334. return $this;
  3335. }
  3336. public function setPrivateKey()
  3337. {
  3338. $this->_privateKey = $key;
  3339. return $this;
  3340. }
  3341. public function encrypt($plain)
  3342. {
  3343. openssl_public_encrypt($plain, $encrypted, $this->_getPublicKey());
  3344. return $encrypted;
  3345. }
  3346. /**
  3347. * Decrypt data
  3348. *
  3349. * Use buckyball/ssl/offsite-decrypt.php script for much improved security
  3350. *
  3351. * @param string $encrypted
  3352. * @return string
  3353. */
  3354. public function decrypt($encrypted)
  3355. {
  3356. $hash = sha1($encrypted);
  3357. if (!empty($this->_cache[$hash])) {
  3358. return $this->_cache[$hash];
  3359. }
  3360. // even though decrypt_url can potentially be overridden by extension, only encrypted data is sent over
  3361. if (!empty($this->_config['decrypt_url'])) {
  3362. $data = array('encrypted' => base64_encode($encrypted));
  3363. $result = BUtil::remoteHttp('GET', $this->_config['decrypt_url'], $data);
  3364. $decrypted = base64_decode($result);
  3365. if (!empty($result['decrypted'])) {
  3366. $decrypted = $result['decrypted'];
  3367. } else {
  3368. //TODO: handle exceptions
  3369. }
  3370. } else {
  3371. openssl_private_decrypt($encrypted, $decrypted, $this->_getPrivateKey());
  3372. }
  3373. $this->_cache[$hash] = $decrypted;
  3374. return $decrypted;
  3375. }
  3376. }
  3377. if( !function_exists( 'xmlentities' ) ) {
  3378. /**
  3379. * @see http://www.php.net/manual/en/function.htmlentities.php#106535
  3380. */
  3381. function xmlentities( $string ) {
  3382. $not_in_list = "A-Z0-9a-z\s_-";
  3383. return preg_replace_callback( "/[^{$not_in_list}]/" , 'get_xml_entity_at_index_0' , $string );
  3384. }
  3385. function get_xml_entity_at_index_0( $CHAR ) {
  3386. if( !is_string( $CHAR[0] ) || ( strlen( $CHAR[0] ) > 1 ) ) {
  3387. die( "function: 'get_xml_entity_at_index_0' requires data type: 'char' (single character). '{$CHAR[0]}' does not match this type." );
  3388. }
  3389. switch( $CHAR[0] ) {
  3390. case "'": case '"': case '&': case '<': case '>':
  3391. return htmlspecialchars( $CHAR[0], ENT_QUOTES ); break;
  3392. default:
  3393. return numeric_entity_4_char($CHAR[0]); break;
  3394. }
  3395. }
  3396. function numeric_entity_4_char( $char ) {
  3397. return "&#".str_pad(ord($char), 3, '0', STR_PAD_LEFT).";";
  3398. }
  3399. }
  3400. if (!function_exists('password_hash')) {
  3401. /**
  3402. * If FISMA/FIPS/NIST compliance required, use PBKDF2
  3403. *
  3404. * @see http://stackoverflow.com/questions/4795385/how-do-you-use-bcrypt-for-hashing-passwords-in-php
  3405. */
  3406. function password_hash($password)
  3407. {
  3408. return Bcrypt::i()->hash($password);
  3409. }
  3410. function password_verify($password, $hash)
  3411. {
  3412. return Bcrypt::i()->verify($password, $hash);
  3413. }
  3414. }
  3415. if (!function_exists('hash_hmac')) {
  3416. /**
  3417. * HMAC hash, works if hash extension is not installed
  3418. *
  3419. * Supports SHA1 and MD5 algos
  3420. *
  3421. * @see http://www.php.net/manual/en/function.hash-hmac.php#93440
  3422. *
  3423. * @param string $data Data to be hashed.
  3424. * @param string $key Hash key.
  3425. * @param boolean $raw_output Return raw or hex
  3426. *
  3427. * @access public
  3428. * @static
  3429. *
  3430. * @return string Hash
  3431. */
  3432. function hash_hmac($algo, $data, $key, $raw_output = false)
  3433. {
  3434. $algo = strtolower($algo);
  3435. $pack = 'H'.strlen($algo('test'));
  3436. $size = 64;
  3437. $opad = str_repeat(chr(0x5C), $size);
  3438. $ipad = str_repeat(chr(0x36), $size);
  3439. if (strlen($key) > $size) {
  3440. $key = str_pad(pack($pack, $algo($key)), $size, chr(0x00));
  3441. } else {
  3442. $key = str_pad($key, $size, chr(0x00));
  3443. }
  3444. for ($i = 0; $i < strlen($key) - 1; $i++) {
  3445. $opad[$i] = $opad[$i] ^ $key[$i];
  3446. $ipad[$i] = $ipad[$i] ^ $key[$i];
  3447. }
  3448. $output = $algo($opad.pack($pack, $algo($ipad.$data)));
  3449. return ($raw_output) ? pack($pack, $output) : $output;
  3450. }
  3451. }
  3452. if (!function_exists('hash_pbkdf2')) {
  3453. /**
  3454. * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
  3455. *
  3456. * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
  3457. *
  3458. * This implementation of PBKDF2 was originally created by defuse.ca
  3459. * With improvements by variations-of-shadow.com
  3460. *
  3461. * @see http://www.php.net/manual/en/function.hash-hmac.php#109260
  3462. *
  3463. * @param string $algorithm - The hash algorithm to use. Recommended: SHA256
  3464. * @param string $password - The password.
  3465. * @param string $salt - A salt that is unique to the password.
  3466. * @param integer $count - Iteration count. Higher is better, but slower. Recommended: At least 1024.
  3467. * @param integer $key_length - The length of the derived key in bytes.
  3468. * @param boolean $raw_output - If true, the key is returned in raw binary format. Hex encoded otherwise.
  3469. * @return A $key_length-byte key derived from the password and salt.
  3470. */
  3471. function hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
  3472. {
  3473. if (function_exists('openssl_pbkdf2')) {
  3474. return openssl_pbkdf2($password, $salt, $key_length, $count, $algorithm);
  3475. }
  3476. $algorithm = strtolower($algorithm);
  3477. if(!in_array($algorithm, hash_algos(), true))
  3478. die('PBKDF2 ERROR: Invalid hash algorithm.');
  3479. if($count <= 0 || $key_length <= 0)
  3480. die('PBKDF2 ERROR: Invalid parameters.');
  3481. $hash_length = strlen(hash($algorithm, "", true));
  3482. $block_count = ceil($key_length / $hash_length);
  3483. $output = "";
  3484. for($i = 1; $i <= $block_count; $i++) {
  3485. // $i encoded as 4 bytes, big endian.
  3486. $last = $salt . pack("N", $i);
  3487. // first iteration
  3488. $last = $xorsum = hash_hmac($algorithm, $last, $password, true);
  3489. // perform the other $count - 1 iterations
  3490. for ($j = 1; $j < $count; $j++) {
  3491. $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
  3492. }
  3493. $output .= $xorsum;
  3494. }
  3495. if($raw_output)
  3496. return substr($output, 0, $key_length);
  3497. else
  3498. return bin2hex(substr($output, 0, $key_length));
  3499. }
  3500. }
  3501. if (!function_exists('oath_hotp')) {
  3502. /**
  3503. * Yet another OATH HOTP function. Has a 64 bit counter.
  3504. *
  3505. * @see http://www.php.net/manual/en/function.hash-hmac.php#108978
  3506. *
  3507. * @param string $secret Shared secret
  3508. * @param string $crt Counter
  3509. * @param integer $len OTP length
  3510. * @return string
  3511. */
  3512. function oath_hotp($secret, $counter, $len = 8)
  3513. {
  3514. $binctr = pack ('NNC*', $counter>>32, $counter & 0xFFFFFFFF);
  3515. $hash = hash_hmac ("sha1", $binctr, $secret);
  3516. // This is where hashing stops and truncation begins
  3517. $ofs = 2*hexdec (substr ($hash, 39, 1));
  3518. $int = hexdec (substr ($hash, $ofs, 8)) & 0x7FFFFFFF;
  3519. $pin = substr ($int, -$len);
  3520. $pin = str_pad ($pin, $len, "0", STR_PAD_LEFT);
  3521. return $pin;
  3522. }
  3523. }