PageRenderTime 101ms CodeModel.GetById 18ms app.highlight 66ms RepoModel.GetById 2ms 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

Large files files are truncated, but you can click here to view the full file

   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/**
  25* Utility class to parse and construct strings and data structures
  26*/
  27class BUtil extends BClass
  28{
  29    /**
  30    * IV for mcrypt operations
  31    *
  32    * @var string
  33    */
  34    protected static $_mcryptIV;
  35
  36    /**
  37    * Encryption key from configuration (encrypt/key)
  38    *
  39    * @var string
  40    */
  41    protected static $_mcryptKey;
  42
  43    /**
  44    * Default hash algorithm
  45    *
  46    * @var string default sha512 for strength and slowness
  47    */
  48    protected static $_hashAlgo = 'bcrypt';
  49
  50    /**
  51    * Default number of hash iterations
  52    *
  53    * @var int
  54    */
  55    protected static $_hashIter = 3;
  56
  57    /**
  58    * Default full hash string separator
  59    *
  60    * @var string
  61    */
  62    protected static $_hashSep = '$';
  63
  64    /**
  65    * Default character pool for random and sequence strings
  66    *
  67    * Chars "c", "C" are ommited to avoid accidental obscene language
  68    * Chars "0", "1", "I" are removed to avoid leading 0 and ambiguity in print
  69    *
  70    * @var string
  71    */
  72    protected static $_defaultCharPool = '23456789abdefghijklmnopqrstuvwxyzABDEFGHJKLMNOPQRSTUVWXYZ';
  73
  74    /**
  75    * Shortcut to help with IDE autocompletion
  76    *
  77    * @return BUtil
  78    */
  79    public static function i($new=false, array $args=array())
  80    {
  81        return BClassRegistry::i()->instance(__CLASS__, $args, !$new);
  82    }
  83
  84    /**
  85    * Convert any data to JSON string
  86    *
  87    * $data can be BData instance, or array of BModel objects, will be automatically converted to array
  88    *
  89    * @param mixed $data
  90    * @return string
  91    */
  92    public static function toJson($data)
  93    {
  94        if (is_array($data) && is_object(current($data)) && current($data) instanceof BModel) {
  95            $data = BDb::many_as_array($data);
  96        } elseif (is_object($data) && $data instanceof BData) {
  97            $data = $data->as_array(true);
  98        }
  99        return json_encode($data);
 100    }
 101
 102    /**
 103    * Parse JSON into PHP data
 104    *
 105    * @param string $json
 106    * @param bool $asObject if false will attempt to convert to array,
 107    *                       otherwise standard combination of objects and arrays
 108    */
 109    public static function fromJson($json, $asObject=false)
 110    {
 111        $obj = json_decode($json);
 112        return $asObject ? $obj : static::objectToArray($obj);
 113    }
 114
 115    /**
 116    * Indents a flat JSON string to make it more human-readable.
 117    *
 118    * @param string $json The original JSON string to process.
 119    *
 120    * @return string Indented version of the original JSON string.
 121    */
 122    public static function jsonIndent($json)
 123    {
 124
 125        $result      = '';
 126        $pos         = 0;
 127        $strLen      = strlen($json);
 128        $indentStr   = '  ';
 129        $newLine     = "\n";
 130        $prevChar    = '';
 131        $outOfQuotes = true;
 132
 133        for ($i=0; $i<=$strLen; $i++) {
 134
 135            // Grab the next character in the string.
 136            $char = substr($json, $i, 1);
 137
 138            // Are we inside a quoted string?
 139            if ($char == '"' && $prevChar != '\\') {
 140                $outOfQuotes = !$outOfQuotes;
 141
 142            // If this character is the end of an element,
 143            // output a new line and indent the next line.
 144            } else if(($char == '}' || $char == ']') && $outOfQuotes) {
 145                $result .= $newLine;
 146                $pos --;
 147                for ($j=0; $j<$pos; $j++) {
 148                    $result .= $indentStr;
 149                }
 150            }
 151
 152            // Add the character to the result string.
 153            $result .= $char;
 154
 155            // If the last character was the beginning of an element,
 156            // output a new line and indent the next line.
 157            if (($char == ',' || $char == '{' || $char == '[') && $outOfQuotes) {
 158                $result .= $newLine;
 159                if ($char == '{' || $char == '[') {
 160                    $pos ++;
 161                }
 162
 163                for ($j = 0; $j < $pos; $j++) {
 164                    $result .= $indentStr;
 165                }
 166            }
 167
 168            $prevChar = $char;
 169        }
 170
 171        return $result;
 172    }
 173
 174    /**
 175    * Convert data to JavaScript string
 176    *
 177    * Notable difference from toJson: allows raw function callbacks
 178    *
 179    * @param mixed $val
 180    * @return string
 181    */
 182    public static function toJavaScript($val)
 183    {
 184        if (is_null($val)) {
 185            return 'null';
 186        } elseif (is_bool($val)) {
 187            return $val ? 'true' : 'false';
 188        } elseif (is_string($val)) {
 189            if (preg_match('#^\s*function\s*\(#', $val)) {
 190                return $val;
 191            } else {
 192                return "'".addslashes($val)."'";
 193            }
 194        } elseif (is_int($val) || is_float($val)) {
 195            return $val;
 196        } elseif ($val instanceof BValue) {
 197            return $val->toPlain();
 198        } elseif (($isObj = is_object($val)) || is_array($val)) {
 199            $out = array();
 200            if (!empty($val) && ($isObj || array_keys($val) !== range(0, count($val)-1))) { // assoc?
 201                foreach ($val as $k=>$v) {
 202                    $out[] = "'".addslashes($k)."':".static::toJavaScript($v);
 203                }
 204                return '{'.join(',', $out).'}';
 205            } else {
 206                foreach ($val as $k=>$v) {
 207                    $out[] = static::toJavaScript($v);
 208                }
 209                return '['.join(',', $out).']';
 210            }
 211        }
 212        return '"UNSUPPORTED TYPE"';
 213    }
 214
 215    public static function toRss($data)
 216    {
 217        $lang = !empty($data['language']) ? $data['language'] : 'en-us';
 218        $ttl = !empty($data['ttl']) ? (int)$data['ttl'] : 40;
 219        $descr = !empty($data['description']) ? $data['description'] : $data['title'];
 220        $xml = '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel>'
 221.'<title><![CDATA['.$data['title'].']]></title><link><![CDATA['.$data['link'].']]></link>'
 222.'<description><![CDATA['.$descr.']]></description><language><![CDATA['.$lang.']]></language><ttl>'.$ttl.'</ttl>';
 223        foreach ($data['items'] as $item) {
 224            if (!is_numeric($item['pubDate'])) {
 225                $item['pubDate'] =  strtotime($item['pubDate']);
 226            }
 227            if (empty($item['guid'])) {
 228                $item['guid'] = $item['link'];
 229            }
 230            $xml .= '<item><title><![CDATA['.$item['title'].']]></title>'
 231.'<description><![CDATA['.$item['description'].']]></description>'
 232.'<pubDate>'.date('r', $item['pubDate']).'</pubDate>'
 233.'<guid><![CDATA['.$item['guid'].']]></guid><link><![CDATA['.$item['link'].']]></link></item>';
 234        }
 235        $xml .= '</channel></rss>';
 236        return $xml;
 237    }
 238
 239    /**
 240    * Convert object to array recursively
 241    *
 242    * @param object $d
 243    * @return array
 244    */
 245    public static function objectToArray($d)
 246    {
 247        if (is_object($d)) {
 248            $d = get_object_vars($d);
 249        }
 250        if (is_array($d)) {
 251            return array_map('BUtil::objectToArray', $d);
 252        }
 253        return $d;
 254    }
 255
 256    /**
 257    * Convert array to object
 258    *
 259    * @param mixed $d
 260    * @return object
 261    */
 262    public static function arrayToObject($d)
 263    {
 264        if (is_array($d)) {
 265            return (object) array_map('BUtil::objectToArray', $d);
 266        }
 267        return $d;
 268    }
 269
 270    /**
 271     * version of sprintf for cases where named arguments are desired (php syntax)
 272     *
 273     * with sprintf: sprintf('second: %2$s ; first: %1$s', '1st', '2nd');
 274     *
 275     * with sprintfn: sprintfn('second: %second$s ; first: %first$s', array(
 276     *  'first' => '1st',
 277     *  'second'=> '2nd'
 278     * ));
 279     *
 280     * @see http://www.php.net/manual/en/function.sprintf.php#94608
 281     * @param string $format sprintf format string, with any number of named arguments
 282     * @param array $args array of [ 'arg_name' => 'arg value', ... ] replacements to be made
 283     * @return string|false result of sprintf call, or bool false on error
 284     */
 285    public static function sprintfn($format, $args = array())
 286    {
 287        $args = (array)$args;
 288
 289        // map of argument names to their corresponding sprintf numeric argument value
 290        $arg_nums = array_slice(array_flip(array_keys(array(0 => 0) + $args)), 1);
 291
 292        // find the next named argument. each search starts at the end of the previous replacement.
 293        for ($pos = 0; preg_match('/(?<=%)([a-zA-Z_]\w*)(?=\$)/', $format, $match, PREG_OFFSET_CAPTURE, $pos);) {
 294            $arg_pos = $match[0][1];
 295            $arg_len = strlen($match[0][0]);
 296            $arg_key = $match[1][0];
 297
 298            // programmer did not supply a value for the named argument found in the format string
 299            if (! array_key_exists($arg_key, $arg_nums)) {
 300                user_error("sprintfn(): Missing argument '${arg_key}'", E_USER_WARNING);
 301                return false;
 302            }
 303
 304            // replace the named argument with the corresponding numeric one
 305            $format = substr_replace($format, $replace = $arg_nums[$arg_key], $arg_pos, $arg_len);
 306            $pos = $arg_pos + strlen($replace); // skip to end of replacement for next iteration
 307        }
 308
 309        if (!$args) {
 310            $args = array('');
 311        }
 312        return vsprintf($format, array_values($args));
 313    }
 314
 315    /**
 316    * Inject vars into string template
 317    *
 318    * Ex: echo BUtil::injectVars('One :two :three', array('two'=>2, 'three'=>3))
 319    * Result: "One 2 3"
 320    *
 321    * @param string $str
 322    * @param array $vars
 323    * @return string
 324    */
 325    public static function injectVars($str, $vars)
 326    {
 327        $from = array(); $to = array();
 328        foreach ($vars as $k=>$v) {
 329            $from[] = ':'.$k;
 330            $to[] = $v;
 331        }
 332        return str_replace($from, $to, $str);
 333    }
 334
 335    /**
 336     * Merges any number of arrays / parameters recursively, replacing
 337     * entries with string keys with values from latter arrays.
 338     * If the entry or the next value to be assigned is an array, then it
 339     * automagically treats both arguments as an array.
 340     * Numeric entries are appended, not replaced, but only if they are
 341     * unique
 342     *
 343     * calling: result = BUtil::arrayMerge(a1, a2, ... aN)
 344     *
 345     * @param array $array1
 346     * @param array $array2...
 347     * @return array
 348     **/
 349     public static function arrayMerge() {
 350         $arrays = func_get_args();
 351         $base = array_shift($arrays);
 352         if (!is_array($base))  {
 353             $base = empty($base) ? array() : array($base);
 354         }
 355         foreach ($arrays as $append) {
 356             if (!is_array($append)) {
 357                 $append = array($append);
 358             }
 359             foreach ($append as $key => $value) {
 360                 if (is_numeric($key)) {
 361                     if (!in_array($value, $base)) {
 362                        $base[] = $value;
 363                     }
 364                 } elseif (!array_key_exists($key, $base)) {
 365                     $base[$key] = $value;
 366                 } elseif (is_array($value) && is_array($base[$key])) {
 367                     $base[$key] = static::arrayMerge($base[$key], $append[$key]);
 368                 } else {
 369                     $base[$key] = $value;
 370                 }
 371             }
 372         }
 373         return $base;
 374     }
 375
 376    /**
 377    * Compare 2 arrays recursively
 378    *
 379    * @param array $array1
 380    * @param array $array2
 381    */
 382    public static function arrayCompare(array $array1, array $array2)
 383    {
 384        $diff = false;
 385        // Left-to-right
 386        foreach ($array1 as $key => $value) {
 387            if (!array_key_exists($key,$array2)) {
 388                $diff[0][$key] = $value;
 389            } elseif (is_array($value)) {
 390                if (!is_array($array2[$key])) {
 391                    $diff[0][$key] = $value;
 392                    $diff[1][$key] = $array2[$key];
 393                } else {
 394                    $new = static::arrayCompare($value, $array2[$key]);
 395                    if ($new !== false) {
 396                        if (isset($new[0])) $diff[0][$key] = $new[0];
 397                        if (isset($new[1])) $diff[1][$key] = $new[1];
 398                    }
 399                }
 400            } elseif ($array2[$key] !== $value) {
 401                 $diff[0][$key] = $value;
 402                 $diff[1][$key] = $array2[$key];
 403            }
 404        }
 405        // Right-to-left
 406        foreach ($array2 as $key => $value) {
 407            if (!array_key_exists($key,$array1)) {
 408                $diff[1][$key] = $value;
 409            }
 410            // No direct comparsion because matching keys were compared in the
 411            // left-to-right loop earlier, recursively.
 412        }
 413        return $diff;
 414    }
 415
 416    /**
 417    * Walk over array of objects and perform method or callback on each row
 418    *
 419    * @param array $arr
 420    * @param callback $cb
 421    * @param array $args
 422    * @param boolean $ignoreExceptions
 423    * @return array
 424    */
 425    static public function arrayWalk($arr, $cb, $args=array(), $ignoreExceptions=false)
 426    {
 427        $result = array();
 428        foreach ($arr as $i=>$r) {
 429            $callback = is_string($cb) && $cb[0]==='.' ? array($r, substr($cb, 1)) : $cb;
 430            if ($ignoreExceptions) {
 431                try {
 432                    $result[] = call_user_func_array($callback, $args);
 433                } catch (Exception $e) {
 434                    BDebug::warning('EXCEPTION class('.get_class($r).') arrayWalk('.$i.'): '.$e->getMessage());
 435                }
 436            } else {
 437                $result[] = call_user_func_array($callback, $args);
 438            }
 439        }
 440        return $result;
 441    }
 442
 443    /**
 444    * Clean array of ints from empty and non-numeric values
 445    *
 446    * If parameter is a string, splits by comma
 447    *
 448    * @param array|string $arr
 449    * @return array
 450    */
 451    static public function arrayCleanInt($arr)
 452    {
 453        $res = array();
 454        if (is_string($arr)) {
 455            $arr = explode(',', $arr);
 456        }
 457        if (is_array($arr)) {
 458            foreach ($arr as $k=>$v) {
 459                if (is_numeric($v)) {
 460                    $res[$k] = intval($v);
 461                }
 462            }
 463        }
 464        return $res;
 465    }
 466
 467    /**
 468    * Insert 1 or more items into array at specific position
 469    *
 470    * Note: code repetition is for better iteration performance
 471    *
 472    * @param array $array The original container array
 473    * @param array $items Items to be inserted
 474    * @param string $where
 475    *   - start
 476    *   - end
 477    *   - offset==$key
 478    *   - key.(before|after)==$key
 479    *   - obj.(before|after).$object_property==$key
 480    *   - arr.(before|after).$item_array_key==$key
 481    * @return array resulting array
 482    */
 483    static public function arrayInsert($array, $items, $where)
 484    {
 485        $result = array();
 486        $w1 = explode('==', $where, 2);
 487        $w2 = explode('.', $w1[0], 3);
 488
 489        switch ($w2[0]) {
 490        case 'start':
 491            $result = array_merge($items, $array);
 492            break;
 493
 494        case 'end':
 495            $result = array_merge($array, $items);
 496            break;
 497
 498        case 'offset': // for associative only
 499            $key = $w1[1];
 500            $i = 0;
 501            foreach ($array as $k=>$v) {
 502                if ($key===$i++) {
 503                    foreach ($items as $k1=>$v1) {
 504                        $result[$k1] = $v1;
 505                    }
 506                }
 507                $result[$k] = $v;
 508            }
 509            break;
 510
 511        case 'key': // for associative only
 512            $rel = $w2[1];
 513            $key = $w1[1];
 514            foreach ($array as $k=>$v) {
 515                if ($key===$k) {
 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
 531        case 'obj':
 532            $rel = $w2[1];
 533            $f = $w2[2];
 534            $key = $w1[1];
 535            foreach ($array as $k=>$v) {
 536                if ($key===$v->$f) {
 537                    if ($rel==='after') {
 538                        $result[$k] = $v;
 539                    }
 540                    foreach ($items as $k1=>$v1) {
 541                        $result[$k1] = $v1;
 542                    }
 543                    if ($rel==='before') {
 544                        $result[$k] = $v;
 545                    }
 546                } else {
 547                    $result[$k] = $v;
 548                }
 549            }
 550            break;
 551
 552        case 'arr':
 553            $rel = $w2[1];
 554            $f = $w2[2];
 555            $key = $w1[1];
 556            foreach ($array as $k=>$v) {
 557                if ($key===$v[$f]) {
 558                    if ($rel==='after') {
 559                        $result[$k] = $v;
 560                    }
 561                    foreach ($items as $k1=>$v1) {
 562                        $result[$k1] = $v1;
 563                    }
 564                    if ($rel==='before') {
 565                        $result[$k] = $v;
 566                    }
 567                } else {
 568                    $result[$k] = $v;
 569                }
 570            }
 571            break;
 572
 573        default: BDebug::error('Invalid where condition: '.$where);
 574        }
 575
 576        return $result;
 577    }
 578
 579    /**
 580    * Return only specific fields from source array
 581    *
 582    * @param array $source
 583    * @param array|string $fields
 584    * @param boolean $inverse if true, will return anything NOT in $fields
 585    * @param boolean $setNulls fill missing fields with nulls
 586    * @result array
 587    */
 588    static public function arrayMask(array $source, $fields, $inverse=false, $setNulls=true)
 589    {
 590        if (is_string($fields)) {
 591            $fields = explode(',', $fields);
 592            array_walk($fields, 'trim');
 593        }
 594        $result = array();
 595        if (!$inverse) {
 596            foreach ($fields as $k) {
 597                if (isset($source[$k])) {
 598                    $result[$k] = $source[$k];
 599                } elseif ($setNulls) {
 600                    $result[$k] = null;
 601                }
 602            }
 603        } else {
 604            foreach ($source as $k=>$v) {
 605                if (!in_array($k, $fields)) $result[$k] = $v;
 606            }
 607        }
 608        return $result;
 609    }
 610
 611    static public function arrayToOptions($source, $labelField, $keyField=null, $emptyLabel=null)
 612    {
 613        $options = array();
 614        if (!is_null($emptyLabel)) {
 615            $options = array("" => $emptyLabel);
 616        }
 617        if (empty($source)) {
 618            return array();
 619        }
 620        $isObject = is_object(current($source));
 621        foreach ($source as $k=>$item) {
 622            if ($isObject) {
 623                $key = is_null($keyField) ? $k : $item->$keyField;
 624                $label = $labelField[0]==='.' ? $item->{substr($labelField, 1)}() : $item->labelField;
 625                $options[$key] = $label;
 626            } else {
 627                $key = is_null($keyField) ? $k : $item[$keyField];
 628                $options[$key] = $item[$labelField];
 629            }
 630        }
 631        return $options;
 632    }
 633
 634    static public function arrayMakeAssoc($source, $keyField)
 635    {
 636        $isObject = is_object(current($source));
 637        $assocArray = array();
 638        foreach ($source as $k => $item) {
 639            if ($isObject) {
 640                $assocArray[$item->$keyField] = $item;
 641            } else {
 642                $assocArray[$item[$keyField]] = $item;
 643            }
 644        }
 645        return $assocArray;
 646    }
 647
 648    /**
 649    * Create IV for mcrypt operations
 650    *
 651    * @return string
 652    */
 653    static public function mcryptIV()
 654    {
 655        if (!static::$_mcryptIV) {
 656            static::$_mcryptIV = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_DEV_URANDOM);
 657        }
 658        return static::$_mcryptIV;
 659    }
 660
 661    /**
 662    * Fetch default encryption key from config
 663    *
 664    * @return string
 665    */
 666    static public function mcryptKey($key=null, $configPath=null)
 667    {
 668        if (!is_null($key)) {
 669            static::$_mcryptKey = $key;
 670        } elseif (is_null(static::$_mcryptKey) && $configPath) {
 671            static::$_mcryptKey = BConfig::i()->get($configPath);
 672        }
 673        return static::$_mcryptKey;
 674
 675    }
 676
 677    /**
 678    * Encrypt using AES256
 679    *
 680    * Requires PHP extension mcrypt
 681    *
 682    * @param string $value
 683    * @param string $key
 684    * @param boolean $base64
 685    * @return string
 686    */
 687    static public function encrypt($value, $key=null, $base64=true)
 688    {
 689        if (is_null($key)) $key = static::mcryptKey();
 690        $enc = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $value, MCRYPT_MODE_ECB, static::mcryptIV());
 691        return $base64 ? trim(base64_encode($enc)) : $enc;
 692    }
 693
 694    /**
 695    * Decrypt using AES256
 696    *
 697    * Requires PHP extension mcrypt
 698    *
 699    * @param string $value
 700    * @param string $key
 701    * @param boolean $base64
 702    * @return string
 703    */
 704    static public function decrypt($value, $key=null, $base64=true)
 705    {
 706        if (is_null($key)) $key = static::mcryptKey();
 707        $enc = $base64 ? base64_decode($value) : $value;
 708        return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $enc, MCRYPT_MODE_ECB, static::mcryptIV()));
 709    }
 710
 711    /**
 712    * Generate random string
 713    *
 714    * @param int $strLen length of resulting string
 715    * @param string $chars allowed characters to be used
 716    */
 717    public static function randomString($strLen=8, $chars=null)
 718    {
 719        if (is_null($chars)) {
 720            $chars = static::$_defaultCharPool;
 721        }
 722        $charsLen = strlen($chars)-1;
 723        $str = '';
 724        for ($i=0; $i<$strLen; $i++) {
 725            $str .= $chars[mt_rand(0, $charsLen)];
 726        }
 727        return $str;
 728    }
 729
 730    /**
 731    * Generate random string based on pattern
 732    *
 733    * Syntax: {ULD10}-{U5}
 734    * - U: upper case letters
 735    * - L: lower case letters
 736    * - D: digits
 737    *
 738    * @param string $pattern
 739    * @return string
 740    */
 741    public static function randomPattern($pattern)
 742    {
 743        static $chars = array('L'=>'bcdfghjkmnpqrstvwxyz', 'U'=>'BCDFGHJKLMNPQRSTVWXYZ', 'D'=>'123456789');
 744
 745        while (preg_match('#\{([ULD]+)([0-9]+)\}#i', $pattern, $m)) {
 746            for ($i=0, $c=''; $i<strlen($m[1]); $i++) $c .= $chars[$m[1][$i]];
 747            $pattern = preg_replace('#'.preg_quote($m[0]).'#', BUtil::randomString($m[2], $c), $pattern, 1);
 748        }
 749        return $pattern;
 750    }
 751
 752    public static function nextStringValue($string='', $chars=null)
 753    {
 754        if (is_null($chars)) {
 755            $chars = static::$_defaultCharPool; // avoid leading 0
 756        }
 757        $pos = strlen($string);
 758        $lastChar = substr($chars, -1);
 759        while (--$pos>=-1) {
 760            if ($pos==-1) {
 761                $string = $chars[0].$string;
 762                return $string;
 763            } elseif ($string[$pos]===$lastChar) {
 764                $string[$pos] = $chars[0];
 765                continue;
 766            } else {
 767                $string[$pos] = $chars[strpos($chars, $string[$pos])+1];
 768                return $string;
 769            }
 770        }
 771        // should never get here
 772        return $string;
 773    }
 774
 775    /**
 776    * Set or retrieve current hash algorithm
 777    *
 778    * @param string $algo
 779    */
 780    public static function hashAlgo($algo=null)
 781    {
 782        if (is_null($algo)) {
 783            return static::$_hashAlgo;
 784        }
 785        static::$_hashAlgo = $algo;
 786    }
 787
 788    public static function hashIter($iter=null)
 789    {
 790        if (is_null($iter)) {
 791            return static::$_hashIter;
 792        }
 793        static::$iter = $iter;
 794    }
 795
 796    /**
 797    * Generate salted hash
 798    *
 799    * @deprecated by Bcrypt
 800    * @param string $string original text
 801    * @param mixed $salt
 802    * @param mixed $algo
 803    * @return string
 804    */
 805    public static function saltedHash($string, $salt, $algo=null)
 806    {
 807        $algo = !is_null($algo) ? $algo : static::$_hashAlgo;
 808        return hash($algo, $salt.$string);
 809    }
 810
 811    /**
 812    * Generate fully composed salted hash
 813    *
 814    * Ex: $sha512$2$<salt1>$<salt2>$<double-hashed-string-here>
 815    *
 816    * @deprecated by Bcrypt
 817    * @param string $string
 818    * @param string $salt
 819    * @param string $algo
 820    * @param integer $iter
 821    */
 822    public static function fullSaltedHash($string, $salt=null, $algo=null, $iter=null)
 823    {
 824        $algo = !is_null($algo) ? $algo : static::$_hashAlgo;
 825        if ('bcrypt'===$algo) {
 826            return Bcrypt::i()->hash($string);
 827        }
 828        $iter = !is_null($iter) ? $iter : static::$_hashIter;
 829        $s = static::$_hashSep;
 830        $hash = $s.$algo.$s.$iter;
 831        for ($i=0; $i<$iter; $i++) {
 832            $salt1 = !is_null($salt) ? $salt : static::randomString();
 833            $hash .= $s.$salt1;
 834            $string = static::saltedHash($string, $salt1, $algo);
 835        }
 836        return $hash.$s.$string;
 837    }
 838
 839    /**
 840    * Validate salted hash against original text
 841    *
 842    * @deprecated by BUtil::bcrypt()
 843    * @param string $string original text
 844    * @param string $storedHash fully composed salted hash
 845    * @return bool
 846    */
 847    public static function validateSaltedHash($string, $storedHash)
 848    {
 849        if (strpos($storedHash, '$2a$')===0 || strpos($storedHash, '$2y$')===0) {
 850            return Bcrypt::i()->verify($string, $storedHash);
 851        }
 852        if (!$storedHash) {
 853            return false;
 854        }
 855        $sep = $storedHash[0];
 856        $arr = explode($sep, $storedHash);
 857        array_shift($arr);
 858        $algo = array_shift($arr);
 859        $iter = array_shift($arr);
 860        $verifyHash = $string;
 861        for ($i=0; $i<$iter; $i++) {
 862            $salt = array_shift($arr);
 863            $verifyHash = static::saltedHash($verifyHash, $salt, $algo);
 864        }
 865        $knownHash = array_shift($arr);
 866        return $verifyHash===$knownHash;
 867    }
 868
 869    public static function sha512base64($str)
 870    {
 871        return base64_encode(pack('H*', hash('sha512', $str)));
 872    }
 873
 874    static protected $_lastRemoteHttpInfo;
 875    /**
 876    * Send simple POST request to external server and retrieve response
 877    *
 878    * @param string $method
 879    * @param string $url
 880    * @param array $data
 881    * @return string
 882    */
 883    public static function remoteHttp($method, $url, $data = array())
 884    {
 885        $timeout = 5;
 886        $userAgent = 'Mozilla/5.0';
 887        if ($method==='GET' && $data) {
 888            if(is_array($data)){
 889                $request = http_build_query($data, '', '&');
 890            } else {
 891                $request = $data;
 892            }
 893
 894            $url .= (strpos($url, '?')===false ? '?' : '&') . $request;
 895        }
 896
 897        // curl disabled because file upload doesn't work for some reason. TODO: figure out why
 898        if (false && function_exists('curl_init')) {
 899            $curlOpt = array(
 900                CURLOPT_USERAGENT => $userAgent,
 901                CURLOPT_URL => $url,
 902                CURLOPT_ENCODING => '',
 903                CURLOPT_FOLLOWLOCATION => true,
 904                CURLOPT_RETURNTRANSFER => true,
 905                CURLOPT_AUTOREFERER => true,
 906                CURLOPT_SSL_VERIFYPEER => true,
 907                CURLOPT_CAINFO => dirname(__DIR__).'/ssl/ca-bundle.crt',
 908                CURLOPT_SSL_VERIFYHOST => 2,
 909                CURLOPT_CONNECTTIMEOUT => $timeout,
 910                CURLOPT_TIMEOUT => $timeout,
 911                CURLOPT_MAXREDIRS => 10,
 912                CURLOPT_HTTPHEADER, array('Expect:'), //Fixes the HTTP/1.1 417 Expectation Failed
 913                CURLOPT_HEADER => true,
 914            );
 915            if (false) { // TODO: figure out cookies handling
 916                $cookieDir = BConfig::i()->get('fs/storage_dir').'/cache';
 917                BUtil::ensureDir($cookieDir);
 918                $cookie = tempnam($cookieDir, 'CURLCOOKIE');
 919                $curlOpt += array(
 920                    CURLOPT_COOKIEJAR => $cookie,
 921                );
 922            }
 923
 924            if ($method==='POST') {
 925                $curlOpt += array(
 926                    CURLOPT_POSTFIELDS => $data,
 927                    CURLOPT_POST => 1,
 928                );
 929            } elseif ($method==='PUT') {
 930                $curlOpt += array(
 931                    CURLOPT_POSTFIELDS => $data,
 932                    CURLOPT_PUT => 1,
 933                );
 934            }
 935            $ch = curl_init();
 936            curl_setopt_array($ch, $curlOpt);
 937            $rawResponse = curl_exec($ch);
 938            list($response, $headers) = explode("\r\n\r\n", $rawResponse, 2);
 939            static::$_lastRemoteHttpInfo = curl_getinfo($ch);
 940            $respHeaders = explode("\r\n", $headers);
 941            if(curl_errno($ch) != 0){
 942                static::$_lastRemoteHttpInfo['errno'] = curl_errno($ch);
 943                static::$_lastRemoteHttpInfo['error'] = curl_error($ch);
 944            }
 945            curl_close($ch);
 946
 947        } else {
 948            $opts = array('http' => array(
 949                'method' => $method,
 950                'timeout' => $timeout,
 951                'header' => "User-Agent: {$userAgent}\r\n",
 952            ));
 953            if ($method==='POST' || $method==='PUT') {
 954                $multipart = false;
 955                foreach ($data as $k=>$v) {
 956                    if (is_string($v) && $v[0]==='@') {
 957                        $multipart = true;
 958                        break;
 959                    }
 960                }
 961                if (!$multipart) {
 962                    $contentType = 'application/x-www-form-urlencoded';
 963                    $opts['http']['content'] = http_build_query($data);
 964                } else {
 965                    $boundary = '--------------------------'.microtime(true);
 966                    $contentType = 'multipart/form-data; boundary='.$boundary;
 967                    $opts['http']['content'] = '';
 968                    //TODO: implement recursive forms
 969                    foreach ($data as $k =>$v) {
 970                        if (is_string($v) && $v[0]==='@') {
 971                            $filename = substr($v, 1);
 972                            $fileContents = file_get_contents($filename);
 973                            $opts['http']['content'] .= "--{$boundary}\r\n".
 974                                "Content-Disposition: form-data; name=\"{$k}\"; filename=\"".basename($filename)."\"\r\n".
 975                                "Content-Type: application/zip\r\n".
 976                                "\r\n".
 977                                "{$fileContents}\r\n";
 978                        } else {
 979                            $opts['http']['content'] .= "--{$boundary}\r\n".
 980                                "Content-Disposition: form-data; name=\"{$k}\"\r\n".
 981                                "\r\n".
 982                                "{$v}\r\n";
 983                        }
 984                    }
 985                    $opts['http']['content'] .= "--{$boundary}--\r\n";
 986                }
 987                $opts['http']['header'] .= "Content-Type: {$contentType}\r\n";
 988                    //."Content-Length: ".strlen($request)."\r\n";
 989                if (preg_match('#^(ssl|ftps|https):#', $url)) {
 990                    $opts['ssl'] = array(
 991                        'verify_peer' => true,
 992                        'cafile' => dirname(__DIR__).'/ssl/ca-bundle.crt',
 993                        'verify_depth' => 5,
 994                    );
 995                }
 996            }
 997            $response = file_get_contents($url, false, stream_context_create($opts));
 998
 999            static::$_lastRemoteHttpInfo = array(); //TODO: emulate curl data?
1000            $respHeaders = $http_response_header;
1001        }
1002        foreach ($respHeaders as $i => $line) {
1003            if ($i) {
1004                $arr = explode(':', $line, 2);
1005            } else {
1006                $arr = array(0, $line);
1007            }
1008            static::$_lastRemoteHttpInfo['headers'][strtolower($arr[0])] = trim($arr[1]);
1009        }
1010
1011        return $response;
1012    }
1013
1014    public static function lastRemoteHttpInfo()
1015    {
1016        return static::$_lastRemoteHttpInfo;
1017    }
1018
1019    public static function normalizePath($path)
1020    {
1021        $path = str_replace('\\', '/', $path);
1022        if (strpos($path, '/..')!==false) {
1023            $a = explode('/', $path);
1024            $b = array();
1025            foreach ($a as $p) {
1026                if ($p==='..') array_pop($b); else $b[] = $p;
1027            }
1028            $path = join('/', $b);
1029        }
1030        return $path;
1031    }
1032
1033    public static function globRecursive($pattern, $flags=0)
1034    {
1035        $files = glob($pattern, $flags);
1036        if (!$files) $files = array();
1037        $dirs = glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT);
1038        if ($dirs) {
1039            foreach ($dirs as $dir) {
1040                $files = array_merge($files, self::globRecursive($dir.'/'.basename($pattern), $flags));
1041            }
1042        }
1043        return $files;
1044    }
1045
1046    public static function isPathAbsolute($path)
1047    {
1048        return !empty($path) && ($path[0]==='/' || $path[0]==='\\') // starting with / or \
1049            || !empty($path[1]) && $path[1]===':'; // windows drive letter C:
1050    }
1051
1052    public static function isUrlFull($url)
1053    {
1054        return preg_match('#^(https?:)?//#', $url);
1055    }
1056
1057    public static function ensureDir($dir)
1058    {
1059        if (is_file($dir)) {
1060            BDebug::warning($dir.' is a file, directory required');
1061            return;
1062        }
1063        if (!is_dir($dir)) {
1064            @$res = mkdir($dir, 0777, true);
1065            if (!$res) {
1066                BDebug::warning("Can't create directory: ".$dir);
1067            }
1068        }
1069    }
1070
1071    /**
1072    * Put together URL components generated by parse_url() function
1073    *
1074    * @see http://us2.php.net/manual/en/function.parse-url.php#106731
1075    * @param array $p result of parse_url()
1076    * @return string
1077    */
1078    public static function unparseUrl($p)
1079    {
1080        $scheme   = isset($p['scheme'])   ? $p['scheme'] . '://' : '';
1081        $user     = isset($p['user'])     ? $p['user']           : '';
1082        $pass     = isset($p['pass'])     ? ':' . $p['pass']     : '';
1083        $pass     = ($user || $pass)      ? $pass . '@'          : '';
1084        $host     = isset($p['host'])     ? $p['host']           : '';
1085        $port     = isset($p['port'])     ? ':' . $p['port']     : '';
1086        $path     = isset($p['path'])     ? $p['path']           : '';
1087        $query    = isset($p['query'])    ? '?' . $p['query']    : '';
1088        $fragment = isset($p['fragment']) ? '#' . $p['fragment'] : '';
1089        return $scheme.$user.$pass.$host.$port.$path.$query.$fragment;
1090    }
1091
1092    /**
1093    * Add or set URL query parameters
1094    *
1095    * @param string $url
1096    * @param array $params
1097    * @return string
1098    */
1099    public static function setUrlQuery($url, $params)
1100    {
1101        if (true === $url) {
1102            $url = BRequest::currentUrl();
1103        }
1104        $parsed = parse_url($url);
1105        $query = array();
1106        if (!empty($parsed['query'])) {
1107            foreach (explode('&', $parsed['query']) as $q) {
1108                $a = explode('=', $q);
1109                if ($a[0]==='') {
1110                    continue;
1111                }
1112                $a[0] = urldecode($a[0]);
1113                $query[$a[0]] = urldecode($a[1]);
1114            }
1115        }
1116        foreach($params as $k => $v){
1117            if($v === ""){
1118                if(isset($query[$k])){
1119                    unset($query[$k]);
1120                }
1121                unset($params[$k]);
1122            }
1123        }
1124        $query = array_merge($query, $params);
1125        $parsed['query'] = http_build_query($query);
1126        return static::unparseUrl($parsed);
1127    }
1128
1129    public static function paginateSortUrl($url, $state, $field)
1130    {
1131        return static::setUrlQuery($url, array(
1132            's'=>$field,
1133            'sd'=>$state['s']!=$field || $state['sd']=='desc' ? 'asc' : 'desc',
1134        ));
1135    }
1136
1137    public static function paginateSortAttr($url, $state, $field, $class='')
1138    {
1139        return 'href="'.static::paginateSortUrl($url, $state, $field)
1140            .'" class="'.$class.' '.($state['s']==$field ? $state['sd'] : '').'"';
1141    }
1142
1143    /**
1144     * @param string $tag
1145     * @param array  $attrs
1146     * @param null   $content
1147     * @return string
1148     */
1149    public static function tagHtml($tag, $attrs = array(), $content = null)
1150    {
1151        $attrsHtmlArr = array();
1152        foreach ($attrs as $k => $v) {
1153            if ('' === $v || is_null($v) || false === $v) {
1154                continue;
1155            }
1156            if (true === $v) {
1157                $v = $k;
1158            } elseif (is_array($v)) {
1159                switch ($k) {
1160                    case 'class':
1161                        $v = join(' ', $v);
1162                        break;
1163
1164                    case 'style':
1165                        $attrHtmlArr = array();
1166                        foreach ($v as $k1 => $v1) {
1167                            $attrHtmlArr[] = $k1.':'.$v1;
1168                        }
1169                        $v = join('; ', $attrHtmlArr);
1170                        break;
1171
1172                    default:
1173                        $v = join('', $v);
1174                }
1175            }
1176            $attrsHtmlArr[] = $k.'="'.htmlspecialchars($v, ENT_QUOTES, 'UTF-8').'"';
1177        }
1178        return '<'.$tag.' '.join(' ', $attrsHtmlArr).'>'.$content.'</'.$tag.'>';
1179    }
1180
1181    /**
1182     * @param array $options
1183     * @param string $default
1184     * @return string
1185     */
1186    public static function optionsHtml($options, $default = '')
1187    {
1188        if(!is_array($default)){
1189            $default = (string)$default;
1190        }
1191        $htmlArr = array();
1192        foreach ($options as $k => $v) {
1193            $k = (string)$k;
1194            if (is_array($v) && $k!=='' && $k[0] === '@') { // group
1195                $label = trim(substr($k, 1));
1196                $htmlArr[] = BUtil::tagHtml('optgroup', array('label' => $label), static::optionsHtml($v, $default));
1197                continue;
1198            }
1199            if (is_array($v)) {
1200                $attr = $v;
1201                $v = !empty($attr['text']) ? $attr['text'] : '';
1202                unset($attr['text']);
1203            } else {
1204                $attr = array();
1205            }
1206            $attr['value'] = $k;
1207            $attr['selected'] = is_array($default) && in_array($k, $default) || $default === $k;
1208            $htmlArr[] = BUtil::tagHtml('option', $attr, $v);
1209        }
1210
1211        return join("\n", $htmlArr);
1212    }
1213
1214    /**
1215    * Strip html tags and shorten to specified length, to the whole word
1216    *
1217    * @param string $text
1218    * @param integer $limit
1219    */
1220    public static function previewText($text, $limit)
1221    {
1222        $text = strip_tags($text);
1223        if (strlen($text) < $limit) {
1224            return $text;
1225        }
1226        preg_match('/^(.{1,'.$limit.'})\b/', $text, $matches);
1227        return $matches[1];
1228    }
1229
1230    public static function isEmptyDate($date)
1231    {
1232        return preg_replace('#[0 :-]#', '', (string)$date)==='';
1233    }
1234
1235    /**
1236    * Get gravatar image src by email
1237    *
1238    * @param string $email
1239    * @param array $params
1240    *   - size (default 80)
1241    *   - rating (G, PG, R, X)
1242    *   - default
1243    *   - border
1244    */
1245    public static function gravatar($email, $params=array())
1246    {
1247        if (empty($params['default'])) {
1248            $params['default'] = 'identicon';
1249        }
1250        return BRequest::i()->scheme().'://www.gravatar.com/avatar/'.md5(strtolower($email))
1251            .($params ? '?'.http_build_query($params) : '');
1252    }
1253
1254    public static function extCallback($callback)
1255    {
1256        if (is_string($callback)) {
1257            if (strpos($callback, '.')!==false) {
1258                list($class, $method) = explode('.', $callback);
1259            } elseif (strpos($callback, '->')) {
1260                list($class, $method) = explode('->', $callback);
1261            }
1262            if (!empty($class)) {
1263                $callback = array($class::i(), $method);
1264            }
1265        }
1266        return $callback;
1267    }
1268
1269    public static function call($callback, $args=array(), $array=false)
1270    {
1271        $callback = static::extCallback($callback);
1272        if ($array) {
1273            return call_user_func_array($callback, $args);
1274        } else {
1275            return call_user_func($callback, $args);
1276        }
1277    }
1278
1279    public static function formatDateRecursive($source, $format='m/d/Y')
1280    {
1281        foreach ($source as $i=>$val) {
1282            if (is_string($val)) {
1283                // checking only beginning of string for speed, assuming it is a date
1284                if (preg_match('#^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]( |$)#', $val)) {
1285                    $source[$i] = date($format, strtotime($val));
1286                }
1287            } elseif (is_array($val)) {
1288                $source[$i] = static::formatDateRecursive($val, $format);
1289            }
1290        }
1291        return $source;
1292    }
1293
1294    public static function timeAgo($ptime, $now=null, $long=false)
1295    {
1296        if (!is_numeric($ptime)) {
1297            $ptime = strtotime($ptime);
1298        }
1299        if (!$now) {
1300            $now = time();
1301        } elseif (!is_numeric($now)) {
1302            $now = strtotime($now);
1303        }
1304        $etime = $now - $ptime;
1305        if ($etime < 1) {
1306            return $long ? 'less than 1 second' : '0s';
1307        }
1308        $a = array(
1309            12 * 30 * 24 * 60 * 60  =>  array('year', 'y'),
1310            30 * 24 * 60 * 60       =>  array('month', 'mon'),
1311            24 * 60 * 60            =>  array('day', 'd'),
1312            60 * 60                 =>  array('hour', 'h'),
1313            60                      =>  array('minute', 'm'),
1314            1                       =>  array('second', 's'),
1315        );
1316
1317        foreach ($a as $secs => $sa) {
1318            $d = $etime / $secs;
1319            if ($d >= 1) {
1320                $r = round($d);
1321                return $r . ($long ? ' ' . $sa[0] . ($r > 1 ? 's' : '') : $sa[1]);
1322            }
1323        }
1324    }
1325
1326    /**
1327     * Simplify string to allowed characters only
1328     *
1329     * @param string $str input string
1330     * @param string $pattern RegEx pattern to specify not allowed characters
1331     * @param string $filler character to replace not allowed characters with
1332     * @return string
1333     */
1334    static public function simplifyString($str, $pattern='#[^a-z0-9-]+#', $filler='-')
1335    {
1336        return trim(preg_replace($pattern, $filler, strtolower($str)), $filler);
1337    }
1338
1339    /**
1340    * Remove directory recursively
1341    *
1342    * DANGEROUS, I'm afraid to enable it
1343    *
1344    * @param string $dir
1345    */
1346    /*
1347    static public function rmdirRecursive_YesIHaveCheckedThreeTimes($dir, $first=true)
1348    {
1349        if ($first) {
1350            $dir = realpath($dir);
1351        }
1352        if (!file_exists($dir)) {
1353            return true;
1354        }
1355        if (!is_dir($dir) || is_link($dir)) {
1356            return unlink($dir);
1357        }
1358        foreach (scandir($dir) as $item) {
1359            if ($item == '.' || $item == '..') {
1360                continue;
1361            }
1362            if (!static::rmdirRecursive($dir . "/" . $item, false)) {
1363                chmod($dir . "/" . $item, 0777);
1364                if (!static::rmdirRecursive($dir . "/" . $item, false)) return false;
1365            }
1366        }
1367        return rmdir($dir);
1368    }
1369    */
1370
1371    static public function topoSort(array $array, array $args=array())
1372    {
1373        if (empty($array)) {
1374            return array();
1375        }
1376
1377        // nodes listed in 'after' are parents
1378        // nodes listed in 'before' are children
1379        // prepare initial $nodes array
1380        $beforeVar = !empty($args['before']) ? $args['before'] : 'before';
1381        $afterVar = !empty($args['before']) ? $args['after'] : 'after';
1382        $isObject = is_object(current($array));
1383        $nodes = array();
1384        foreach ($array as $k=>$v) {
1385            $before = $isObject ? $v->$beforeVar : $v[$beforeVar];
1386            if (is_string($before)) {
1387                $before = array_walk(explode(',', $before), 'trim');
1388            }
1389            $after = $isObject ? $v->$afterVar : $v[$afterVar];
1390            if (is_string($after)) {
1391                $after = array_walk(explode(',', $after), 'trim');
1392            }
1393            $nodes[$k] = array('key' => $k, 'item' => $v, 'parents' => (array)$after, 'children' => (array)$before);
1394        }
1395
1396        // get nodes without parents
1397        $rootNodes = array();
1398        foreach ($nodes as $k=>$node) {
1399            if (empty($node['parents'])) {
1400                $rootNodes[] = $node;
1401            }
1402        }
1403        // begin algorithm
1404        $sorted = array();
1405        while ($nodes) {
1406            // check for circular reference
1407            if (!$rootNodes) return false;
1408            // remove this node from root nodes and add it to the output
1409            $n = array_pop($rootNodes);
1410            $sorted[$n['key']] = $n['item'];
1411            // for each of its children: queue the new node, finally remove the original
1412            for ($i = count($n['children'])-1; $i>=0; $i--) {
1413                // get child node
1414                $childNode = $nodes[$n['children'][$i]];
1415                // remove child nodes from parent
1416                unset($n['children'][$i]);
1417                // remove parent from child node
1418                unset($childNode['parents'][array_search($n['name'], $childNode['parents'])]);
1419                // check if this child has other parents. if not, add it to the root nodes list
1420                if (!$childNode['parents']) {
1421                    array_push($rootNodes, $childNode);
1422                }
1423            }
1424            // remove processed node from list
1425            unset($nodes[$n['key']]);
1426        }
1427        return $sorted;
1428    }
1429
1430    /**
1431     * Wrapper for ZipArchive::open+extractTo
1432     *
1433     * @param string $filename
1434     * @param string $targetDir
1435     * @return boolean Result
1436     */
1437    static public function zipExtract($filename, $targetDir)
1438    {
1439        if (!class_exists('ZipArchive')) {
1440            throw new BException("Class ZipArchive doesn't exist");
1441        }
1442        $zip = new ZipArchive;
1443        $res = $zip->open($filename);
1444        if (!$res) {
1445            throw new BException("Can't open zip archive for reading: " . $filename);
1446        }
1447        BUtil::ensureDir($targetDir);
1448        $res = $zip->extractTo($targetDir);
1449        $zip->close();
1450        if (!$res) {
1451            throw new BException("Can't extract zip archive: " . $filename . " to " . $targetDir);
1452        }
1453        return true;
1454    }
1455
1456    static public function zipCreateFromDir($filename, $sourceDir)
1457    {
1458        if (!class_exists('ZipArchive')) {
1459            throw new BException("Class ZipArchive doesn't exist");
1460        }
1461        $files = BUtil::globRecursive($sourceDir.'/*');
1462        if (!$files) {
1463            throw new BException('Invalid or empty source dir');
1464        }
1465        $zip = new ZipArchive;
1466        $res = $zip->open($filename, ZipArchive::CREATE);
1467        if (!$res) {
1468            throw new BException("Can't open zip archive for writing: " . $filename);
1469        }
1470        foreach ($files as $file) {
1471            $packedFile = str_replace($sourceDir.'/', '', $file);
1472            if (is_dir($file)) {
1473                $zip->addEmptyDir($packedFile);
1474            } else {
1475                $zip->addFile($file, $packedFile);
1476            }
1477        }
1478        $zip->close();
1479        return true;
1480    }
1481}
1482
1483class BHTML extends BClass
1484{
1485
1486}
1487
1488/**
1489 * @todo Verify license compatibility and integrate with https://github.com/PHPMailer/PHPMailer
1490 */
1491class BEmail extends BClass
1492{
1493    static protected $_handlers = array();
1494    static protected $_defaultHandler = 'default';
1495
1496    public function __construct()
1497    {
1498        $this->addHandler('default', array($this, 'defaultHandler'));
1499    }
1500
1501    public function addHandler($name, $params)
1502    {
1503        if (is_callable($params)) {
1504            $params = array(
1505                'description' => $name,
1506                'callback' => $params,
1507            );
1508        }
1509        static::$_handlers[$name] = $params;
1510    }
1511
1512    public function getHandlers()
1513    {
1514        return static::$_handlers;
1515    }
1516
1517    public function setDefaultHandler($name)
1518    {
1519        static::$_defaultHandler = $name;
1520    }
1521
1522    public function send($data)
1523    {
1524        static $allowedHeadersRegex = '/^(to|from|cc|bcc|reply-to|return-path|content-type|list-unsubscribe|x-.*)$/';
1525
1526        $data = array_change_key_case($data, CASE_LOWER);
1527
1528        $body = trim($data['body']);
1529        unset($data['body']);
1530
1531        $to      = '';
1532        $subject = '';
1533        $headers = array();
1534        $params  = array();
1535        $files   = array();
1536
1537        foreach ($data as $k => $v) {
1538            if ($k == 'subject') {
1539                $subject = $v;
1540
1541            } elseif ($k == 'to') {
1542                $to = $v;
1543
1544            } elseif ($k == 'attach') {
1545                foreach ((array)$v as $file) {
1546                    $files[] = $file;
1547                }
1548
1549            } elseif ($k[0] === '-') {
1550                $params[$k] = $k . ' ' . $v;
1551
1552            } elseif (preg_match($allowedHeadersRegex, $k)) {
1553                if (!empty($v) && $v!=='"" <>') {
1554                    $headers[$k] = $k . ': ' . $v;
1555                }
1556            }
1557        }
1558
1559        $origBody = $body;
1560        if ($files) {
1561            // $body and $headers will be updated
1562            $th…

Large files files are truncated, but you can click here to view the full file