1<?php
2// Authors:
3// - cadorn, Christoph Dorn <christoph@christophdorn.com>, Copyright 2007, New BSD License
4// - qbbr, Sokolov Innokenty <sokolov.innokenty@gmail.com>, Copyright 2011, New BSD License
5// - cadorn, Christoph Dorn <christoph@christophdorn.com>, Copyright 2011, MIT License
6
7/**
8 * *** BEGIN LICENSE BLOCK *****
9 *
10 * [MIT License](http://www.opensource.org/licenses/mit-license.php)
11 *
12 * Copyright (c) 2007+ [Christoph Dorn](http://www.christophdorn.com/)
13 *
14 * Permission is hereby granted, free of charge, to any person obtaining a copy
15 * of this software and associated documentation files (the "Software"), to deal
16 * in the Software without restriction, including without limitation the rights
17 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18 * copies of the Software, and to permit persons to whom the Software is
19 * furnished to do so, subject to the following conditions:
20 *
21 * The above copyright notice and this permission notice shall be included in
22 * all copies or substantial portions of the Software.
23 *
24 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30 * THE SOFTWARE.
31 *
32 * ***** END LICENSE BLOCK *****
33 *
34 * @copyright       Copyright (C) 2007+ Christoph Dorn
35 * @author          Christoph Dorn <christoph@christophdorn.com>
36 * @license         [MIT License](http://www.opensource.org/licenses/mit-license.php)
37 * @package         FirePHPCore
38 */
39
40/**
41 * @see http://code.google.com/p/firephp/issues/detail?id=112
42 */
43if (!defined('E_STRICT')) {
44    define('E_STRICT', 2048);
45}
46if (!defined('E_RECOVERABLE_ERROR')) {
47    define('E_RECOVERABLE_ERROR', 4096);
48}
49if (!defined('E_DEPRECATED')) {
50    define('E_DEPRECATED', 8192);
51}
52if (!defined('E_USER_DEPRECATED')) {
53    define('E_USER_DEPRECATED', 16384);
54}
55
56/**
57 * Sends the given data to the FirePHP Firefox Extension.
58 * The data can be displayed in the Firebug Console or in the
59 * "Server" request tab.
60 *
61 * For more information see: http://www.firephp.org/
62 *
63 * @copyright       Copyright (C) 2007+ Christoph Dorn
64 * @author          Christoph Dorn <christoph@christophdorn.com>
65 * @license         [MIT License](http://www.opensource.org/licenses/mit-license.php)
66 * @package         FirePHPCore
67 *
68 * @deprecated 2.3 This will be removed in Minify 3.0
69 */
70class FirePHP {
71
72    /**
73     * FirePHP version
74     *
75     * @var string
76     */
77    const VERSION = '0.3';    // @pinf replace '0.3' with '%%VERSION%%'
78
79    /**
80     * Firebug LOG level
81     *
82     * Logs a message to firebug console.
83     *
84     * @var string
85     */
86    const LOG = 'LOG';
87
88    /**
89     * Firebug INFO level
90     *
91     * Logs a message to firebug console and displays an info icon before the message.
92     *
93     * @var string
94     */
95    const INFO = 'INFO';
96
97    /**
98     * Firebug WARN level
99     *
100     * Logs a message to firebug console, displays an warning icon before the message and colors the line turquoise.
101     *
102     * @var string
103     */
104    const WARN = 'WARN';
105
106    /**
107     * Firebug ERROR level
108     *
109     * Logs a message to firebug console, displays an error icon before the message and colors the line yellow. Also increments the firebug error count.
110     *
111     * @var string
112     */
113    const ERROR = 'ERROR';
114
115    /**
116     * Dumps a variable to firebug's server panel
117     *
118     * @var string
119     */
120    const DUMP = 'DUMP';
121
122    /**
123     * Displays a stack trace in firebug console
124     *
125     * @var string
126     */
127    const TRACE = 'TRACE';
128
129    /**
130     * Displays an exception in firebug console
131     *
132     * Increments the firebug error count.
133     *
134     * @var string
135     */
136    const EXCEPTION = 'EXCEPTION';
137
138    /**
139     * Displays an table in firebug console
140     *
141     * @var string
142     */
143    const TABLE = 'TABLE';
144
145    /**
146     * Starts a group in firebug console
147     *
148     * @var string
149     */
150    const GROUP_START = 'GROUP_START';
151
152    /**
153     * Ends a group in firebug console
154     *
155     * @var string
156     */
157    const GROUP_END = 'GROUP_END';
158
159    /**
160     * Singleton instance of FirePHP
161     *
162     * @var FirePHP
163     */
164    protected static $instance = null;
165
166    /**
167     * Flag whether we are logging from within the exception handler
168     *
169     * @var boolean
170     */
171    protected $inExceptionHandler = false;
172
173    /**
174     * Flag whether to throw PHP errors that have been converted to ErrorExceptions
175     *
176     * @var boolean
177     */
178    protected $throwErrorExceptions = true;
179
180    /**
181     * Flag whether to convert PHP assertion errors to Exceptions
182     *
183     * @var boolean
184     */
185    protected $convertAssertionErrorsToExceptions = true;
186
187    /**
188     * Flag whether to throw PHP assertion errors that have been converted to Exceptions
189     *
190     * @var boolean
191     */
192    protected $throwAssertionExceptions = false;
193
194    /**
195     * Wildfire protocol message index
196     *
197     * @var integer
198     */
199    protected $messageIndex = 1;
200
201    /**
202     * Options for the library
203     *
204     * @var array
205     */
206    protected $options = array('maxDepth' => 10,
207                               'maxObjectDepth' => 5,
208                               'maxArrayDepth' => 5,
209                               'useNativeJsonEncode' => true,
210                               'includeLineNumbers' => true);
211
212    /**
213     * Filters used to exclude object members when encoding
214     *
215     * @var array
216     */
217    protected $objectFilters = array(
218        'firephp' => array('objectStack', 'instance', 'json_objectStack'),
219        'firephp_test_class' => array('objectStack', 'instance', 'json_objectStack')
220    );
221
222    /**
223     * A stack of objects used to detect recursion during object encoding
224     *
225     * @var object
226     */
227    protected $objectStack = array();
228
229    /**
230     * Flag to enable/disable logging
231     *
232     * @var boolean
233     */
234    protected $enabled = true;
235
236    /**
237     * The insight console to log to if applicable
238     *
239     * @var object
240     */
241    protected $logToInsightConsole = null;
242
243    /**
244     * When the object gets serialized only include specific object members.
245     *
246     * @return array
247     */
248    public function __sleep()
249    {
250        return array('options', 'objectFilters', 'enabled');
251    }
252
253    /**
254     * Gets singleton instance of FirePHP
255     *
256     * @param boolean $autoCreate
257     * @return FirePHP
258     */
259    public static function getInstance($autoCreate = false)
260    {
261        if ($autoCreate === true && !self::$instance) {
262            self::init();
263        }
264        return self::$instance;
265    }
266
267    /**
268     * Creates FirePHP object and stores it for singleton access
269     *
270     * @return FirePHP
271     */
272    public static function init()
273    {
274        return self::setInstance(new self());
275    }
276
277    /**
278     * Set the instance of the FirePHP singleton
279     *
280     * @param FirePHP $instance The FirePHP object instance
281     * @return FirePHP
282     */
283    public static function setInstance($instance)
284    {
285        return self::$instance = $instance;
286    }
287
288    /**
289     * Set an Insight console to direct all logging calls to
290     *
291     * @param object $console The console object to log to
292     * @return void
293     */
294    public function setLogToInsightConsole($console)
295    {
296        if (is_string($console)) {
297            if (get_class($this) != 'FirePHP_Insight' && !is_subclass_of($this, 'FirePHP_Insight')) {
298                throw new Exception('FirePHP instance not an instance or subclass of FirePHP_Insight!');
299            }
300            $this->logToInsightConsole = $this->to('request')->console($console);
301        } else {
302            $this->logToInsightConsole = $console;
303        }
304    }
305
306    /**
307     * Enable and disable logging to Firebug
308     *
309     * @param boolean $enabled TRUE to enable, FALSE to disable
310     * @return void
311     */
312    public function setEnabled($enabled)
313    {
314       $this->enabled = $enabled;
315    }
316
317    /**
318     * Check if logging is enabled
319     *
320     * @return boolean TRUE if enabled
321     */
322    public function getEnabled()
323    {
324        return $this->enabled;
325    }
326
327    /**
328     * Specify a filter to be used when encoding an object
329     *
330     * Filters are used to exclude object members.
331     *
332     * @param string $class The class name of the object
333     * @param array $filter An array of members to exclude
334     * @return void
335     */
336    public function setObjectFilter($class, $filter)
337    {
338        $this->objectFilters[strtolower($class)] = $filter;
339    }
340
341    /**
342     * Set some options for the library
343     *
344     * Options:
345     *  - maxDepth: The maximum depth to traverse (default: 10)
346     *  - maxObjectDepth: The maximum depth to traverse objects (default: 5)
347     *  - maxArrayDepth: The maximum depth to traverse arrays (default: 5)
348     *  - useNativeJsonEncode: If true will use json_encode() (default: true)
349     *  - includeLineNumbers: If true will include line numbers and filenames (default: true)
350     *
351     * @param array $options The options to be set
352     * @return void
353     */
354    public function setOptions($options)
355    {
356        $this->options = array_merge($this->options, $options);
357    }
358
359    /**
360     * Get options from the library
361     *
362     * @return array The currently set options
363     */
364    public function getOptions()
365    {
366        return $this->options;
367    }
368
369    /**
370     * Set an option for the library
371     *
372     * @param string $name
373     * @param mixed $value
374     * @return void
375     * @throws Exception
376     */
377    public function setOption($name, $value)
378    {
379        if (!isset($this->options[$name])) {
380            throw $this->newException('Unknown option: ' . $name);
381        }
382        $this->options[$name] = $value;
383    }
384
385    /**
386     * Get an option from the library
387     *
388     * @param string $name
389     * @return mixed
390     * @throws Exception
391     */
392    public function getOption($name)
393    {
394        if (!isset($this->options[$name])) {
395            throw $this->newException('Unknown option: ' . $name);
396        }
397        return $this->options[$name];
398    }
399
400    /**
401     * Register FirePHP as your error handler
402     *
403     * Will throw exceptions for each php error.
404     *
405     * @return mixed Returns a string containing the previously defined error handler (if any)
406     */
407    public function registerErrorHandler($throwErrorExceptions = false)
408    {
409        //NOTE: The following errors will not be caught by this error handler:
410        //      E_ERROR, E_PARSE, E_CORE_ERROR,
411        //      E_CORE_WARNING, E_COMPILE_ERROR,
412        //      E_COMPILE_WARNING, E_STRICT
413
414        $this->throwErrorExceptions = $throwErrorExceptions;
415
416        return set_error_handler(array($this, 'errorHandler'));
417    }
418
419    /**
420     * FirePHP's error handler
421     *
422     * Throws exception for each php error that will occur.
423     *
424     * @param integer $errno
425     * @param string $errstr
426     * @param string $errfile
427     * @param integer $errline
428     * @param array $errcontext
429     */
430    public function errorHandler($errno, $errstr, $errfile, $errline, $errcontext)
431    {
432        // Don't throw exception if error reporting is switched off
433        if (error_reporting() == 0) {
434            return;
435        }
436        // Only throw exceptions for errors we are asking for
437        if (error_reporting() & $errno) {
438
439            $exception = new ErrorException($errstr, 0, $errno, $errfile, $errline);
440            if ($this->throwErrorExceptions) {
441                throw $exception;
442            } else {
443                $this->fb($exception);
444            }
445        }
446    }
447
448    /**
449     * Register FirePHP as your exception handler
450     *
451     * @return mixed Returns the name of the previously defined exception handler,
452     *               or NULL on error.
453     *               If no previous handler was defined, NULL is also returned.
454     */
455    public function registerExceptionHandler()
456    {
457        return set_exception_handler(array($this, 'exceptionHandler'));
458    }
459
460    /**
461     * FirePHP's exception handler
462     *
463     * Logs all exceptions to your firebug console and then stops the script.
464     *
465     * @param Exception $exception
466     * @throws Exception
467     */
468    function exceptionHandler($exception)
469    {
470        $this->inExceptionHandler = true;
471
472        header('HTTP/1.1 500 Internal Server Error');
473
474        try {
475            $this->fb($exception);
476        } catch (Exception $e) {
477            echo 'We had an exception: ' . $e;
478        }
479
480        $this->inExceptionHandler = false;
481    }
482
483    /**
484     * Register FirePHP driver as your assert callback
485     *
486     * @param boolean $convertAssertionErrorsToExceptions
487     * @param boolean $throwAssertionExceptions
488     * @return mixed Returns the original setting or FALSE on errors
489     */
490    public function registerAssertionHandler($convertAssertionErrorsToExceptions = true, $throwAssertionExceptions = false)
491    {
492        $this->convertAssertionErrorsToExceptions = $convertAssertionErrorsToExceptions;
493        $this->throwAssertionExceptions = $throwAssertionExceptions;
494
495        if ($throwAssertionExceptions && !$convertAssertionErrorsToExceptions) {
496            throw $this->newException('Cannot throw assertion exceptions as assertion errors are not being converted to exceptions!');
497        }
498
499        return assert_options(ASSERT_CALLBACK, array($this, 'assertionHandler'));
500    }
501
502    /**
503     * FirePHP's assertion handler
504     *
505     * Logs all assertions to your firebug console and then stops the script.
506     *
507     * @param string $file File source of assertion
508     * @param integer $line Line source of assertion
509     * @param mixed $code Assertion code
510     */
511    public function assertionHandler($file, $line, $code)
512    {
513        if ($this->convertAssertionErrorsToExceptions) {
514
515          $exception = new ErrorException('Assertion Failed - Code[ ' . $code . ' ]', 0, null, $file, $line);
516
517          if ($this->throwAssertionExceptions) {
518              throw $exception;
519          } else {
520              $this->fb($exception);
521          }
522
523        } else {
524            $this->fb($code, 'Assertion Failed', FirePHP::ERROR, array('File' => $file, 'Line' => $line));
525        }
526    }
527
528    /**
529     * Start a group for following messages.
530     *
531     * Options:
532     *   Collapsed: [true|false]
533     *   Color:     [#RRGGBB|ColorName]
534     *
535     * @param string $name
536     * @param array $options OPTIONAL Instructions on how to log the group
537     * @return true
538     * @throws Exception
539     */
540    public function group($name, $options = null)
541    {
542
543        if ( !isset($name) ) {
544            throw $this->newException('You must specify a label for the group!');
545        }
546
547        if ($options) {
548            if (!is_array($options)) {
549                throw $this->newException('Options must be defined as an array!');
550            }
551            if (array_key_exists('Collapsed', $options)) {
552                $options['Collapsed'] = ($options['Collapsed']) ? 'true' : 'false';
553            }
554        }
555
556        return $this->fb(null, $name, FirePHP::GROUP_START, $options);
557    }
558
559    /**
560     * Ends a group you have started before
561     *
562     * @return true
563     * @throws Exception
564     */
565    public function groupEnd()
566    {
567        return $this->fb(null, null, FirePHP::GROUP_END);
568    }
569
570    /**
571     * Log object with label to firebug console
572     *
573     * @see FirePHP::LOG
574     * @param mixes $object
575     * @param string $label
576     * @return true
577     * @throws Exception
578     */
579    public function log($object, $label = null, $options = array())
580    {
581        return $this->fb($object, $label, FirePHP::LOG, $options);
582    }
583
584    /**
585     * Log object with label to firebug console
586     *
587     * @see FirePHP::INFO
588     * @param mixes $object
589     * @param string $label
590     * @return true
591     * @throws Exception
592     */
593    public function info($object, $label = null, $options = array())
594    {
595        return $this->fb($object, $label, FirePHP::INFO, $options);
596    }
597
598    /**
599     * Log object with label to firebug console
600     *
601     * @see FirePHP::WARN
602     * @param mixes $object
603     * @param string $label
604     * @return true
605     * @throws Exception
606     */
607    public function warn($object, $label = null, $options = array())
608    {
609        return $this->fb($object, $label, FirePHP::WARN, $options);
610    }
611
612    /**
613     * Log object with label to firebug console
614     *
615     * @see FirePHP::ERROR
616     * @param mixes $object
617     * @param string $label
618     * @return true
619     * @throws Exception
620     */
621    public function error($object, $label = null, $options = array())
622    {
623        return $this->fb($object, $label, FirePHP::ERROR, $options);
624    }
625
626    /**
627     * Dumps key and variable to firebug server panel
628     *
629     * @see FirePHP::DUMP
630     * @param string $key
631     * @param mixed $variable
632     * @return true
633     * @throws Exception
634     */
635    public function dump($key, $variable, $options = array())
636    {
637        if (!is_string($key)) {
638            throw $this->newException('Key passed to dump() is not a string');
639        }
640        if (strlen($key) > 100) {
641            throw $this->newException('Key passed to dump() is longer than 100 characters');
642        }
643        if (!preg_match_all('/^[a-zA-Z0-9-_\.:]*$/', $key, $m)) {
644            throw $this->newException('Key passed to dump() contains invalid characters [a-zA-Z0-9-_\.:]');
645        }
646        return $this->fb($variable, $key, FirePHP::DUMP, $options);
647    }
648
649    /**
650     * Log a trace in the firebug console
651     *
652     * @see FirePHP::TRACE
653     * @param string $label
654     * @return true
655     * @throws Exception
656     */
657    public function trace($label)
658    {
659        return $this->fb($label, FirePHP::TRACE);
660    }
661
662    /**
663     * Log a table in the firebug console
664     *
665     * @see FirePHP::TABLE
666     * @param string $label
667     * @param string $table
668     * @return true
669     * @throws Exception
670     */
671    public function table($label, $table, $options = array())
672    {
673        return $this->fb($table, $label, FirePHP::TABLE, $options);
674    }
675
676    /**
677     * Insight API wrapper
678     *
679     * @see Insight_Helper::to()
680     */
681    public static function to()
682    {
683        $instance = self::getInstance();
684        if (!method_exists($instance, '_to')) {
685            throw new Exception('FirePHP::to() implementation not loaded');
686        }
687        $args = func_get_args();
688        return call_user_func_array(array($instance, '_to'), $args);
689    }
690
691    /**
692     * Insight API wrapper
693     *
694     * @see Insight_Helper::plugin()
695     */
696    public static function plugin()
697    {
698        $instance = self::getInstance();
699        if (!method_exists($instance, '_plugin')) {
700            throw new Exception('FirePHP::plugin() implementation not loaded');
701        }
702        $args = func_get_args();
703        return call_user_func_array(array($instance, '_plugin'), $args);
704    }
705
706    /**
707     * Check if FirePHP is installed on client
708     *
709     * @return boolean
710     */
711    public function detectClientExtension()
712    {
713        // Check if FirePHP is installed on client via User-Agent header
714        if (@preg_match_all('/\sFirePHP\/([\.\d]*)\s?/si', $this->getUserAgent(), $m) &&
715           version_compare($m[1][0], '0.0.6', '>=')) {
716            return true;
717        } else
718        // Check if FirePHP is installed on client via X-FirePHP-Version header
719        if (@preg_match_all('/^([\.\d]*)$/si', $this->getRequestHeader('X-FirePHP-Version'), $m) &&
720           version_compare($m[1][0], '0.0.6', '>=')) {
721            return true;
722        }
723        return false;
724    }
725
726    /**
727     * Log varible to Firebug
728     *
729     * @see http://www.firephp.org/Wiki/Reference/Fb
730     * @param mixed $object The variable to be logged
731     * @return boolean Return TRUE if message was added to headers, FALSE otherwise
732     * @throws Exception
733     */
734    public function fb($object)
735    {
736        if ($this instanceof FirePHP_Insight && method_exists($this, '_logUpgradeClientMessage')) {
737            if (!FirePHP_Insight::$upgradeClientMessageLogged) { // avoid infinite recursion as _logUpgradeClientMessage() logs a message
738                $this->_logUpgradeClientMessage();
739            }
740        }
741
742        static $insightGroupStack = array();
743
744        if (!$this->getEnabled()) {
745            return false;
746        }
747
748        if ($this->headersSent($filename, $linenum)) {
749            // If we are logging from within the exception handler we cannot throw another exception
750            if ($this->inExceptionHandler) {
751                // Simply echo the error out to the page
752                echo '<div style="border: 2px solid red; font-family: Arial; font-size: 12px; background-color: lightgray; padding: 5px;"><span style="color: red; font-weight: bold;">FirePHP ERROR:</span> Headers already sent in <b>' . $filename . '</b> on line <b>' . $linenum . '</b>. Cannot send log data to FirePHP. You must have Output Buffering enabled via ob_start() or output_buffering ini directive.</div>';
753            } else {
754                throw $this->newException('Headers already sent in ' . $filename . ' on line ' . $linenum . '. Cannot send log data to FirePHP. You must have Output Buffering enabled via ob_start() or output_buffering ini directive.');
755            }
756        }
757
758        $type = null;
759        $label = null;
760        $options = array();
761
762        if (func_num_args() == 1) {
763        } else if (func_num_args() == 2) {
764            switch (func_get_arg(1)) {
765                case self::LOG:
766                case self::INFO:
767                case self::WARN:
768                case self::ERROR:
769                case self::DUMP:
770                case self::TRACE:
771                case self::EXCEPTION:
772                case self::TABLE:
773                case self::GROUP_START:
774                case self::GROUP_END:
775                    $type = func_get_arg(1);
776                    break;
777                default:
778                    $label = func_get_arg(1);
779                    break;
780            }
781        } else if (func_num_args() == 3) {
782            $type = func_get_arg(2);
783            $label = func_get_arg(1);
784        } else if (func_num_args() == 4) {
785            $type = func_get_arg(2);
786            $label = func_get_arg(1);
787            $options = func_get_arg(3);
788        } else {
789            throw $this->newException('Wrong number of arguments to fb() function!');
790        }
791
792        // Get folder name where firephp is located.
793        $parentFolder = basename(dirname(__FILE__));
794        $parentFolderLength = strlen( $parentFolder );
795        $fbLength = 7 + $parentFolderLength;
796        $fireClassLength = 18 + $parentFolderLength;
797
798        if ($this->logToInsightConsole !== null && (get_class($this) == 'FirePHP_Insight' || is_subclass_of($this, 'FirePHP_Insight'))) {
799            $trace = debug_backtrace();
800            if (!$trace) return false;
801            for ($i = 0; $i < sizeof($trace); $i++) {
802                if (isset($trace[$i]['class'])) {
803                    if ($trace[$i]['class'] == 'FirePHP' || $trace[$i]['class'] == 'FB') {
804                        continue;
805                    }
806                }
807                if (isset($trace[$i]['file'])) {
808                    $path = $this->_standardizePath($trace[$i]['file']);
809                    if (substr($path, -1*$fbLength, $fbLength) == $parentFolder.'/fb.php' || substr($path, -1*$fireClassLength, $fireClassLength) == $parentFolder.'/FirePHP.class.php') {
810                        continue;
811                    }
812                }
813                if (isset($trace[$i]['function']) && $trace[$i]['function'] == 'fb' &&
814                        isset($trace[$i - 1]['file']) && substr($this->_standardizePath($trace[$i - 1]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') {
815                    continue;
816                }
817                if (isset($trace[$i]['class']) && $trace[$i]['class'] == 'FB' &&
818                        isset($trace[$i - 1]['file']) && substr($this->_standardizePath($trace[$i - 1]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') {
819                    continue;
820                }
821                break;
822            }
823            // adjust trace offset
824            $msg = $this->logToInsightConsole->option('encoder.trace.offsetAdjustment', $i);
825
826            if ($object instanceof Exception) {
827                $type = self::EXCEPTION;
828            }
829            if ($label && $type != self::TABLE && $type != self::GROUP_START) {
830                $msg = $msg->label($label);
831            }
832            switch ($type) {
833                case self::DUMP:
834                case self::LOG:
835                    return $msg->log($object);
836                case self::INFO:
837                    return $msg->info($object);
838                case self::WARN:
839                    return $msg->warn($object);
840                case self::ERROR:
841                    return $msg->error($object);
842                case self::TRACE:
843                    return $msg->trace($object);
844                case self::EXCEPTION:
845                    return $this->plugin('error')->handleException($object, $msg);
846                case self::TABLE:
847                    if (isset($object[0]) && !is_string($object[0]) && $label) {
848                        $object = array($label, $object);
849                    }
850                    return $msg->table($object[0], array_slice($object[1], 1), $object[1][0]);
851                case self::GROUP_START:
852                    $insightGroupStack[] = $msg->group(md5($label))->open();
853                    return $msg->log($label);
854                case self::GROUP_END:
855                    if (count($insightGroupStack) == 0) {
856                        throw new Error('Too many groupEnd() as opposed to group() calls!');
857                    }
858                    $group = array_pop($insightGroupStack);
859                    return $group->close();
860                default:
861                    return $msg->log($object);
862            }
863        }
864
865        if (!$this->detectClientExtension()) {
866            return false;
867        }
868
869        $meta = array();
870        $skipFinalObjectEncode = false;
871
872        if ($object instanceof Exception) {
873
874            $meta['file'] = $this->_escapeTraceFile($object->getFile());
875            $meta['line'] = $object->getLine();
876
877            $trace = $object->getTrace();
878            if ($object instanceof ErrorException
879               && isset($trace[0]['function'])
880               && $trace[0]['function'] == 'errorHandler'
881               && isset($trace[0]['class'])
882               && $trace[0]['class'] == 'FirePHP') {
883
884                $severity = false;
885                switch ($object->getSeverity()) {
886                    case E_WARNING:
887                        $severity = 'E_WARNING';
888                        break;
889
890                    case E_NOTICE:
891                        $severity = 'E_NOTICE';
892                        break;
893
894                    case E_USER_ERROR:
895                        $severity = 'E_USER_ERROR';
896                        break;
897
898                    case E_USER_WARNING:
899                        $severity = 'E_USER_WARNING';
900                        break;
901
902                    case E_USER_NOTICE:
903                        $severity = 'E_USER_NOTICE';
904                        break;
905
906                    case E_STRICT:
907                        $severity = 'E_STRICT';
908                        break;
909
910                    case E_RECOVERABLE_ERROR:
911                        $severity = 'E_RECOVERABLE_ERROR';
912                        break;
913
914                    case E_DEPRECATED:
915                        $severity = 'E_DEPRECATED';
916                        break;
917
918                    case E_USER_DEPRECATED:
919                        $severity = 'E_USER_DEPRECATED';
920                        break;
921                }
922
923                $object = array('Class' => get_class($object),
924                                'Message' => $severity . ': ' . $object->getMessage(),
925                                'File' => $this->_escapeTraceFile($object->getFile()),
926                                'Line' => $object->getLine(),
927                                'Type' => 'trigger',
928                                'Trace' => $this->_escapeTrace(array_splice($trace, 2)));
929                $skipFinalObjectEncode = true;
930            } else {
931                $object = array('Class' => get_class($object),
932                                'Message' => $object->getMessage(),
933                                'File' => $this->_escapeTraceFile($object->getFile()),
934                                'Line' => $object->getLine(),
935                                'Type' => 'throw',
936                                'Trace' => $this->_escapeTrace($trace));
937                $skipFinalObjectEncode = true;
938            }
939            $type = self::EXCEPTION;
940
941        } else if ($type == self::TRACE) {
942
943            $trace = debug_backtrace();
944            if (!$trace) return false;
945            for ($i = 0; $i < sizeof($trace); $i++) {
946
947                if (isset($trace[$i]['class'])
948                   && isset($trace[$i]['file'])
949                   && ($trace[$i]['class'] == 'FirePHP'
950                       || $trace[$i]['class'] == 'FB')
951                   && (substr($this->_standardizePath($trace[$i]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php'
952                       || substr($this->_standardizePath($trace[$i]['file']), -1*$fireClassLength, $fireClassLength) == $parentFolder.'/FirePHP.class.php')) {
953                    /* Skip - FB::trace(), FB::send(), $firephp->trace(), $firephp->fb() */
954                } else
955                if (isset($trace[$i]['class'])
956                   && isset($trace[$i+1]['file'])
957                   && $trace[$i]['class'] == 'FirePHP'
958                   && substr($this->_standardizePath($trace[$i + 1]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') {
959                    /* Skip fb() */
960                } else
961                if ($trace[$i]['function'] == 'fb'
962                   || $trace[$i]['function'] == 'trace'
963                   || $trace[$i]['function'] == 'send') {
964
965                    $object = array('Class' => isset($trace[$i]['class']) ? $trace[$i]['class'] : '',
966                                    'Type' => isset($trace[$i]['type']) ? $trace[$i]['type'] : '',
967                                    'Function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : '',
968                                    'Message' => $trace[$i]['args'][0],
969                                    'File' => isset($trace[$i]['file']) ? $this->_escapeTraceFile($trace[$i]['file']) : '',
970                                    'Line' => isset($trace[$i]['line']) ? $trace[$i]['line'] : '',
971                                    'Args' => isset($trace[$i]['args']) ? $this->encodeObject($trace[$i]['args']) : '',
972                                    'Trace' => $this->_escapeTrace(array_splice($trace, $i + 1)));
973
974                    $skipFinalObjectEncode = true;
975                    $meta['file'] = isset($trace[$i]['file']) ? $this->_escapeTraceFile($trace[$i]['file']) : '';
976                    $meta['line'] = isset($trace[$i]['line']) ? $trace[$i]['line'] : '';
977                    break;
978                }
979            }
980
981        } else
982        if ($type == self::TABLE) {
983
984            if (isset($object[0]) && is_string($object[0])) {
985                $object[1] = $this->encodeTable($object[1]);
986            } else {
987                $object = $this->encodeTable($object);
988            }
989
990            $skipFinalObjectEncode = true;
991
992        } else if ($type == self::GROUP_START) {
993
994            if (!$label) {
995                throw $this->newException('You must specify a label for the group!');
996            }
997
998        } else {
999            if ($type === null) {
1000                $type = self::LOG;
1001            }
1002        }
1003
1004        if ($this->options['includeLineNumbers']) {
1005            if (!isset($meta['file']) || !isset($meta['line'])) {
1006
1007                $trace = debug_backtrace();
1008                for ($i = 0; $trace && $i < sizeof($trace); $i++) {
1009
1010                    if (isset($trace[$i]['class'])
1011                       && isset($trace[$i]['file'])
1012                       && ($trace[$i]['class'] == 'FirePHP'
1013                           || $trace[$i]['class'] == 'FB')
1014                       && (substr($this->_standardizePath($trace[$i]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php'
1015                           || substr($this->_standardizePath($trace[$i]['file']), -1*$fireClassLength, $fireClassLength) == $parentFolder.'/FirePHP.class.php')) {
1016                        /* Skip - FB::trace(), FB::send(), $firephp->trace(), $firephp->fb() */
1017                    } else
1018                    if (isset($trace[$i]['class'])
1019                       && isset($trace[$i + 1]['file'])
1020                       && $trace[$i]['class'] == 'FirePHP'
1021                       && substr($this->_standardizePath($trace[$i + 1]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') {
1022                        /* Skip fb() */
1023                    } else
1024                    if (isset($trace[$i]['file'])
1025                       && substr($this->_standardizePath($trace[$i]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') {
1026                        /* Skip FB::fb() */
1027                    } else {
1028                        $meta['file'] = isset($trace[$i]['file']) ? $this->_escapeTraceFile($trace[$i]['file']) : '';
1029                        $meta['line'] = isset($trace[$i]['line']) ? $trace[$i]['line'] : '';
1030                        break;
1031                    }
1032                }
1033            }
1034        } else {
1035            unset($meta['file']);
1036            unset($meta['line']);
1037        }
1038
1039        $this->setHeader('X-Wf-Protocol-1', 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2');
1040        $this->setHeader('X-Wf-1-Plugin-1', 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/' . self::VERSION);
1041
1042        $structureIndex = 1;
1043        if ($type == self::DUMP) {
1044            $structureIndex = 2;
1045            $this->setHeader('X-Wf-1-Structure-2', 'http://meta.firephp.org/Wildfire/Structure/FirePHP/Dump/0.1');
1046        } else {
1047            $this->setHeader('X-Wf-1-Structure-1', 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1');
1048        }
1049
1050        if ($type == self::DUMP) {
1051            $msg = '{"' . $label . '":' . $this->jsonEncode($object, $skipFinalObjectEncode) . '}';
1052        } else {
1053            $msgMeta = $options;
1054            $msgMeta['Type'] = $type;
1055            if ($label !== null) {
1056                $msgMeta['Label'] = $label;
1057            }
1058            if (isset($meta['file']) && !isset($msgMeta['File'])) {
1059                $msgMeta['File'] = $meta['file'];
1060            }
1061            if (isset($meta['line']) && !isset($msgMeta['Line'])) {
1062                $msgMeta['Line'] = $meta['line'];
1063            }
1064            $msg = '[' . $this->jsonEncode($msgMeta) . ',' . $this->jsonEncode($object, $skipFinalObjectEncode) . ']';
1065        }
1066
1067        $parts = explode("\n", chunk_split($msg, 5000, "\n"));
1068
1069        for ($i = 0; $i < count($parts); $i++) {
1070
1071            $part = $parts[$i];
1072            if ($part) {
1073
1074                if (count($parts) > 2) {
1075                    // Message needs to be split into multiple parts
1076                    $this->setHeader('X-Wf-1-' . $structureIndex . '-' . '1-' . $this->messageIndex,
1077                                     (($i == 0) ? strlen($msg) : '')
1078                                     . '|' . $part . '|'
1079                                     . (($i < count($parts) - 2) ? '\\' : ''));
1080                } else {
1081                    $this->setHeader('X-Wf-1-' . $structureIndex . '-' . '1-' . $this->messageIndex,
1082                                     strlen($part) . '|' . $part . '|');
1083                }
1084
1085                $this->messageIndex++;
1086
1087                if ($this->messageIndex > 99999) {
1088                    throw $this->newException('Maximum number (99,999) of messages reached!');
1089                }
1090            }
1091        }
1092
1093        $this->setHeader('X-Wf-1-Index', $this->messageIndex - 1);
1094
1095        return true;
1096    }
1097
1098    /**
1099     * Standardizes path for windows systems.
1100     *
1101     * @param string $path
1102     * @return string
1103     */
1104    protected function _standardizePath($path)
1105    {
1106        return preg_replace('/\\\\+/', '/', $path);
1107    }
1108
1109    /**
1110     * Escape trace path for windows systems
1111     *
1112     * @param array $trace
1113     * @return array
1114     */
1115    protected function _escapeTrace($trace)
1116    {
1117        if (!$trace) return $trace;
1118        for ($i = 0; $i < sizeof($trace); $i++) {
1119            if (isset($trace[$i]['file'])) {
1120                $trace[$i]['file'] = $this->_escapeTraceFile($trace[$i]['file']);
1121            }
1122            if (isset($trace[$i]['args'])) {
1123                $trace[$i]['args'] = $this->encodeObject($trace[$i]['args']);
1124            }
1125        }
1126        return $trace;
1127    }
1128
1129    /**
1130     * Escape file information of trace for windows systems
1131     *
1132     * @param string $file
1133     * @return string
1134     */
1135    protected function _escapeTraceFile($file)
1136    {
1137        /* Check if we have a windows filepath */
1138        if (strpos($file, '\\')) {
1139            /* First strip down to single \ */
1140
1141            $file = preg_replace('/\\\\+/', '\\', $file);
1142
1143            return $file;
1144        }
1145        return $file;
1146    }
1147
1148    /**
1149     * Check if headers have already been sent
1150     *
1151     * @param string $filename
1152     * @param integer $linenum
1153     */
1154    protected function headersSent(&$filename, &$linenum)
1155    {
1156        return headers_sent($filename, $linenum);
1157    }
1158
1159    /**
1160     * Send header
1161     *
1162     * @param string $name
1163     * @param string $value
1164     */
1165    protected function setHeader($name, $value)
1166    {
1167        return header($name . ': ' . $value);
1168    }
1169
1170    /**
1171     * Get user agent
1172     *
1173     * @return string|false
1174     */
1175    protected function getUserAgent()
1176    {
1177        if (!isset($_SERVER['HTTP_USER_AGENT'])) return false;
1178        return $_SERVER['HTTP_USER_AGENT'];
1179    }
1180
1181    /**
1182     * Get all request headers
1183     *
1184     * @return array
1185     */
1186    public static function getAllRequestHeaders()
1187    {
1188        static $_cachedHeaders = false;
1189        if ($_cachedHeaders !== false) {
1190            return $_cachedHeaders;
1191        }
1192        $headers = array();
1193        if (function_exists('getallheaders')) {
1194            foreach (getallheaders() as $name => $value) {
1195                $headers[strtolower($name)] = $value;
1196            }
1197        } else {
1198            foreach ($_SERVER as $name => $value) {
1199                if (substr($name, 0, 5) == 'HTTP_') {
1200                    $headers[strtolower(str_replace(' ', '-', str_replace('_', ' ', substr($name, 5))))] = $value;
1201                }
1202            }
1203        }
1204        return $_cachedHeaders = $headers;
1205    }
1206
1207    /**
1208     * Get a request header
1209     *
1210     * @return string|false
1211     */
1212    protected function getRequestHeader($name)
1213    {
1214        $headers = self::getAllRequestHeaders();
1215        if (isset($headers[strtolower($name)])) {
1216            return $headers[strtolower($name)];
1217        }
1218        return false;
1219    }
1220
1221    /**
1222     * Returns a new exception
1223     *
1224     * @param string $message
1225     * @return Exception
1226     */
1227    protected function newException($message)
1228    {
1229        return new Exception($message);
1230    }
1231
1232    /**
1233     * Encode an object into a JSON string
1234     *
1235     * Uses PHP's jeson_encode() if available
1236     *
1237     * @param object $object The object to be encoded
1238     * @param boolean $skipObjectEncode
1239     * @return string The JSON string
1240     */
1241    public function jsonEncode($object, $skipObjectEncode = false)
1242    {
1243        if (!$skipObjectEncode) {
1244            $object = $this->encodeObject($object);
1245        }
1246
1247        if (function_exists('json_encode')
1248           && $this->options['useNativeJsonEncode'] != false) {
1249
1250            return json_encode($object);
1251        } else {
1252            return $this->json_encode($object);
1253        }
1254    }
1255
1256    /**
1257     * Encodes a table by encoding each row and column with encodeObject()
1258     *
1259     * @param array $table The table to be encoded
1260     * @return array
1261     */
1262    protected function encodeTable($table)
1263    {
1264        if (!$table) return $table;
1265
1266        $newTable = array();
1267        foreach ($table as $row) {
1268
1269            if (is_array($row)) {
1270                $newRow = array();
1271
1272                foreach ($row as $item) {
1273                    $newRow[] = $this->encodeObject($item);
1274                }
1275
1276                $newTable[] = $newRow;
1277            }
1278        }
1279
1280        return $newTable;
1281    }
1282
1283    /**
1284     * Encodes an object including members with
1285     * protected and private visibility
1286     *
1287     * @param object $object The object to be encoded
1288     * @param integer $Depth The current traversal depth
1289     * @return array All members of the object
1290     */
1291    protected function encodeObject($object, $objectDepth = 1, $arrayDepth = 1, $maxDepth = 1)
1292    {
1293        if ($maxDepth > $this->options['maxDepth']) {
1294            return '** Max Depth (' . $this->options['maxDepth'] . ') **';
1295        }
1296
1297        $return = array();
1298
1299        //#2801 is_resource reports false for closed resources https://bugs.php.net/bug.php?id=28016
1300        if (is_resource($object) || gettype($object) === "unknown type") {
1301
1302            return '** ' . (string) $object . ' **';
1303
1304        } else if (is_object($object)) {
1305
1306            if ($objectDepth > $this->options['maxObjectDepth']) {
1307                return '** Max Object Depth (' . $this->options['maxObjectDepth'] . ') **';
1308            }
1309
1310            foreach ($this->objectStack as $refVal) {
1311                if ($refVal === $object) {
1312                    return '** Recursion (' . get_class($object) . ') **';
1313                }
1314            }
1315            array_push($this->objectStack, $object);
1316
1317            $return['__className'] = $class = get_class($object);
1318            $classLower = strtolower($class);
1319
1320            $reflectionClass = new ReflectionClass($class);
1321            $properties = array();
1322            foreach ($reflectionClass->getProperties() as $property) {
1323                $properties[$property->getName()] = $property;
1324            }
1325
1326            $members = (array)$object;
1327
1328            foreach ($properties as $plainName => $property) {
1329
1330                $name = $rawName = $plainName;
1331                if ($property->isStatic()) {
1332                    $name = 'static:' . $name;
1333                }
1334                if ($property->isPublic()) {
1335                    $name = 'public:' . $name;
1336                } else if ($property->isPrivate()) {
1337                    $name = 'private:' . $name;
1338                    $rawName = "\0" . $class . "\0" . $rawName;
1339                } else if ($property->isProtected()) {
1340                    $name = 'protected:' . $name;
1341                    $rawName = "\0" . '*' . "\0" . $rawName;
1342                }
1343
1344                if (!(isset($this->objectFilters[$classLower])
1345                     && is_array($this->objectFilters[$classLower])
1346                     && in_array($plainName, $this->objectFilters[$classLower]))) {
1347
1348                    if (array_key_exists($rawName, $members) && !$property->isStatic()) {
1349                        $return[$name] = $this->encodeObject($members[$rawName], $objectDepth + 1, 1, $maxDepth + 1);
1350                    } else {
1351                        if (method_exists($property, 'setAccessible')) {
1352                            $property->setAccessible(true);
1353                            $return[$name] = $this->encodeObject($property->getValue($object), $objectDepth + 1, 1, $maxDepth + 1);
1354                        } else
1355                        if ($property->isPublic()) {
1356                            $return[$name] = $this->encodeObject($property->getValue($object), $objectDepth + 1, 1, $maxDepth + 1);
1357                        } else {
1358                            $return[$name] = '** Need PHP 5.3 to get value **';
1359                        }
1360                    }
1361                } else {
1362                    $return[$name] = '** Excluded by Filter **';
1363                }
1364            }
1365
1366            // Include all members that are not defined in the class
1367            // but exist in the object
1368            foreach ($members as $rawName => $value) {
1369
1370                $name = $rawName;
1371
1372                if ($name{0} == "\0") {
1373                    $parts = explode("\0", $name);
1374                    $name = $parts[2];
1375                }
1376
1377                $plainName = $name;
1378
1379                if (!isset($properties[$name])) {
1380                    $name = 'undeclared:' . $name;
1381
1382                    if (!(isset($this->objectFilters[$classLower])
1383                         && is_array($this->objectFilters[$classLower])
1384                         && in_array($plainName, $this->objectFilters[$classLower]))) {
1385
1386                        $return[$name] = $this->encodeObject($value, $objectDepth + 1, 1, $maxDepth + 1);
1387                    } else {
1388                        $return[$name] = '** Excluded by Filter **';
1389                    }
1390                }
1391            }
1392
1393            array_pop($this->objectStack);
1394
1395        } elseif (is_array($object)) {
1396
1397            if ($arrayDepth > $this->options['maxArrayDepth']) {
1398                return '** Max Array Depth (' . $this->options['maxArrayDepth'] . ') **';
1399            }
1400
1401            foreach ($object as $key => $val) {
1402
1403                // Encoding the $GLOBALS PHP array causes an infinite loop
1404                // if the recursion is not reset here as it contains
1405                // a reference to itself. This is the only way I have come up
1406                // with to stop infinite recursion in this case.
1407                if ($key == 'GLOBALS'
1408                   && is_array($val)
1409                   && array_key_exists('GLOBALS', $val)) {
1410                    $val['GLOBALS'] = '** Recursion (GLOBALS) **';
1411                }
1412
1413                if (!$this->is_utf8($key)) {
1414                    $key = utf8_encode($key);
1415                }
1416
1417                $return[$key] = $this->encodeObject($val, 1, $arrayDepth + 1, $maxDepth + 1);
1418            }
1419		} elseif ( is_bool($object) ) {
1420			return $object;
1421		} elseif ( is_null($object) ) {
1422			return $object;
1423		} elseif ( is_numeric($object) ) {
1424			return $object;
1425		} else {
1426            if ($this->is_utf8($object)) {
1427                return $object;
1428            } else {
1429                return utf8_encode($object);
1430            }
1431        }
1432        return $return;
1433    }
1434
1435    /**
1436     * Returns true if $string is valid UTF-8 and false otherwise.
1437     *
1438     * @param mixed $str String to be tested
1439     * @return boolean
1440     */
1441    protected function is_utf8($str)
1442    {
1443        if (function_exists('mb_detect_encoding')) {
1444            return (
1445                mb_detect_encoding($str, 'UTF-8', true) == 'UTF-8' &&
1446                ($str === null || $this->jsonEncode($str, true) !== 'null')
1447            );
1448        }
1449        $c = 0;
1450        $b = 0;
1451        $bits = 0;
1452        $len = strlen($str);
1453        for ($i = 0; $i < $len; $i++) {
1454            $c = ord($str[$i]);
1455            if ($c > 128) {
1456                if (($c >= 254)) return false;
1457                elseif ($c >= 252) $bits = 6;
1458                elseif ($c >= 248) $bits = 5;
1459                elseif ($c >= 240) $bits = 4;
1460                elseif ($c >= 224) $bits = 3;
1461                elseif ($c >= 192) $bits = 2;
1462                else return false;
1463                if (($i + $bits) > $len) return false;
1464                while($bits > 1) {
1465                    $i++;
1466                    $b = ord($str[$i]);
1467                    if ($b < 128 || $b > 191) return false;
1468                    $bits--;
1469                }
1470            }
1471        }
1472        return ($str === null || $this->jsonEncode($str, true) !== 'null');
1473    }
1474
1475    /**
1476     * Converts to and from JSON format.
1477     *
1478     * JSON (JavaScript Object Notation) is a lightweight data-interchange
1479     * format. It is easy for humans to read and write. It is easy for machines
1480     * to parse and generate. It is based on a subset of the JavaScript
1481     * Programming Language, Standard ECMA-262 3rd Edition - December 1999.
1482     * This feature can also be found in  Python. JSON is a text format that is
1483     * completely language independent but uses conventions that are familiar
1484     * to programmers of the C-family of languages, including C, C++, C#, Java,
1485     * JavaScript, Perl, TCL, and many others. These properties make JSON an
1486     * ideal data-interchange language.
1487     *
1488     * This package provides a simple encoder and decoder for JSON notation. It
1489     * is intended for use with client-side Javascript applications that make
1490     * use of HTTPRequest to perform server communication functions - data can
1491     * be encoded into JSON notation for use in a client-side javascript, or
1492     * decoded from incoming Javascript requests. JSON format is native to
1493     * Javascript, and can be directly eval()'ed with no further parsing
1494     * overhead
1495     *
1496     * All strings should be in ASCII or UTF-8 format!
1497     *
1498     * LICENSE: Redistribution and use in source and binary forms, with or
1499     * without modification, are permitted provided that the following
1500     * conditions are met: Redistributions of source code must retain the
1501     * above copyright notice, this list of conditions and the following
1502     * disclaimer. Redistributions in binary form must reproduce the above
1503     * copyright notice, this list of conditions and the following disclaimer
1504     * in the documentation and/or other materials provided with the
1505     * distribution.
1506     *
1507     * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
1508     * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
1509     * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
1510     * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
1511     * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
1512     * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
1513     * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
1514     * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
1515     * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
1516     * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
1517     * DAMAGE.
1518     *
1519     * @category
1520     * @package     Services_JSON
1521     * @author      Michal Migurski <mike-json@teczno.com>
1522     * @author      Matt Knapp <mdknapp[at]gmail[dot]com>
1523     * @author      Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
1524     * @author      Christoph Dorn <christoph@christophdorn.com>
1525     * @copyright   2005 Michal Migurski
1526     * @version     CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $
1527     * @license     http://www.opensource.org/licenses/bsd-license.php
1528     * @link        http://pear.php.net/pepr/pepr-proposal-show.php?id=198
1529     */
1530
1531
1532    /**
1533     * Keep a list of objects as we descend into the array so we can detect recursion.
1534     */
1535    private $json_objectStack = array();
1536
1537
1538   /**
1539    * convert a string from one UTF-8 char to one UTF-16 char
1540    *
1541    * Normally should be handled by mb_convert_encoding, but
1542    * provides a slower PHP-only method for installations
1543    * that lack the multibye string extension.
1544    *
1545    * @param    string  $utf8   UTF-8 character
1546    * @return   string  UTF-16 character
1547    * @access   private
1548    */
1549    private function json_utf82utf16($utf8)
1550    {
1551        // oh please oh please oh please oh please oh please
1552        if (function_exists('mb_convert_encoding')) {
1553            return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
1554        }
1555
1556        switch (strlen($utf8)) {
1557            case 1:
1558                // this case should never be reached, because we are in ASCII range
1559                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
1560                return $utf8;
1561
1562            case 2:
1563                // return a UTF-16 character from a 2-byte UTF-8 char
1564                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
1565                return chr(0x07 & (ord($utf8{0}) >> 2))
1566                       . chr((0xC0 & (ord($utf8{0}) << 6))
1567                       | (0x3F & ord($utf8{1})));
1568
1569            case 3:
1570                // return a UTF-16 character from a 3-byte UTF-8 char
1571                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
1572                return chr((0xF0 & (ord($utf8{0}) << 4))
1573                       | (0x0F & (ord($utf8{1}) >> 2)))
1574                       . chr((0xC0 & (ord($utf8{1}) << 6))
1575                       | (0x7F & ord($utf8{2})));
1576        }
1577
1578        // ignoring UTF-32 for now, sorry
1579        return '';
1580    }
1581
1582   /**
1583    * encodes an arbitrary variable into JSON format
1584    *
1585    * @param    mixed   $var    any number, boolean, string, array, or object to be encoded.
1586    *                           see argument 1 to Services_JSON() above for array-parsing behavior.
1587    *                           if var is a strng, note that encode() always expects it
1588    *                           to be in ASCII or UTF-8 format!
1589    *
1590    * @return   mixed   JSON string representation of input var or an error if a problem occurs
1591    * @access   public
1592    */
1593    private function json_encode($var)
1594    {
1595        if (is_object($var)) {
1596            if (in_array($var, $this->json_objectStack)) {
1597                return '"** Recursion **"';
1598            }
1599        }
1600
1601        switch (gettype($var)) {
1602            case 'boolean':
1603                return $var ? 'true' : 'false';
1604
1605            case 'NULL':
1606                return 'null';
1607
1608            case 'integer':
1609                return (int) $var;
1610
1611            case 'double':
1612            case 'float':
1613                return (float) $var;
1614
1615            case 'string':
1616                // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
1617                $ascii = '';
1618                $strlen_var = strlen($var);
1619
1620               /*
1621                * Iterate over every character in the string,
1622                * escaping with a slash or encoding to UTF-8 where necessary
1623                */
1624                for ($c = 0; $c < $strlen_var; ++$c) {
1625
1626                    $ord_var_c = ord($var{$c});
1627
1628                    switch (true) {
1629                        case $ord_var_c == 0x08:
1630                            $ascii .= '\b';
1631                            break;
1632                        case $ord_var_c == 0x09:
1633                            $ascii .= '\t';
1634                            break;
1635                        case $ord_var_c == 0x0A:
1636                            $ascii .= '\n';
1637                            break;
1638                        case $ord_var_c == 0x0C:
1639                            $ascii .= '\f';
1640                            break;
1641                        case $ord_var_c == 0x0D:
1642                            $ascii .= '\r';
1643                            break;
1644
1645                        case $ord_var_c == 0x22:
1646                        case $ord_var_c == 0x2F:
1647                        case $ord_var_c == 0x5C:
1648                            // double quote, slash, slosh
1649                            $ascii .= '\\' . $var{$c};
1650                            break;
1651
1652                        case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
1653                            // characters U-00000000 - U-0000007F (same as ASCII)
1654                            $ascii .= $var{$c};
1655                            break;
1656
1657                        case (($ord_var_c & 0xE0) == 0xC0):
1658                            // characters U-00000080 - U-000007FF, mask 110XXXXX
1659                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
1660                            $char = pack('C*', $ord_var_c, ord($var{$c + 1}));
1661                            $c += 1;
1662                            $utf16 = $this->json_utf82utf16($char);
1663                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
1664                            break;
1665
1666                        case (($ord_var_c & 0xF0) == 0xE0):
1667                            // characters U-00000800 - U-0000FFFF, mask 1110XXXX
1668                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
1669                            $char = pack('C*', $ord_var_c,
1670                                         ord($var{$c + 1}),
1671                                         ord($var{$c + 2}));
1672                            $c += 2;
1673                            $utf16 = $this->json_utf82utf16($char);
1674                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
1675                            break;
1676
1677                        case (($ord_var_c & 0xF8) == 0xF0):
1678                            // characters U-00010000 - U-001FFFFF, mask 11110XXX
1679                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
1680                            $char = pack('C*', $ord_var_c,
1681                                         ord($var{$c + 1}),
1682                                         ord($var{$c + 2}),
1683                                         ord($var{$c + 3}));
1684                            $c += 3;
1685                            $utf16 = $this->json_utf82utf16($char);
1686                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
1687                            break;
1688
1689                        case (($ord_var_c & 0xFC) == 0xF8):
1690                            // characters U-00200000 - U-03FFFFFF, mask 111110XX
1691                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
1692                            $char = pack('C*', $ord_var_c,
1693                                         ord($var{$c + 1}),
1694                                         ord($var{$c + 2}),
1695                                         ord($var{$c + 3}),
1696                                         ord($var{$c + 4}));
1697                            $c += 4;
1698                            $utf16 = $this->json_utf82utf16($char);
1699                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
1700                            break;
1701
1702                        case (($ord_var_c & 0xFE) == 0xFC):
1703                            // characters U-04000000 - U-7FFFFFFF, mask 1111110X
1704                            // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
1705                            $char = pack('C*', $ord_var_c,
1706                                         ord($var{$c + 1}),
1707                                         ord($var{$c + 2}),
1708                                         ord($var{$c + 3}),
1709                                         ord($var{$c + 4}),
1710                                         ord($var{$c + 5}));
1711                            $c += 5;
1712                            $utf16 = $this->json_utf82utf16($char);
1713                            $ascii .= sprintf('\u%04s', bin2hex($utf16));
1714                            break;
1715                    }
1716                }
1717
1718                return '"' . $ascii . '"';
1719
1720            case 'array':
1721                /*
1722                 * As per JSON spec if any array key is not an integer
1723                 * we must treat the the whole array as an object. We
1724                 * also try to catch a sparsely populated associative
1725                 * array with numeric keys here because some JS engines
1726                 * will create an array with empty indexes up to
1727                 * max_index which can cause memory issues and because
1728                 * the keys, which may be relevant, will be remapped
1729                 * otherwise.
1730                 *
1731                 * As per the ECMA and JSON specification an object may
1732                 * have any string as a property. Unfortunately due to
1733                 * a hole in the ECMA specification if the key is a
1734                 * ECMA reserved word or starts with a digit the
1735                 * parameter is only accessible using ECMAScript's
1736                 * bracket notation.
1737                 */
1738
1739                // treat as a JSON object
1740                if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
1741
1742                    $this->json_objectStack[] = $var;
1743
1744                    $properties = array_map(array($this, 'json_name_value'),
1745                                            array_keys($var),
1746                                            array_values($var));
1747
1748                    array_pop($this->json_objectStack);
1749
1750                    foreach ($properties as $property) {
1751                        if ($property instanceof Exception) {
1752                            return $property;
1753                        }
1754                    }
1755
1756                    return '{' . join(',', $properties) . '}';
1757                }
1758
1759                $this->json_objectStack[] = $var;
1760
1761                // treat it like a regular array
1762                $elements = array_map(array($this, 'json_encode'), $var);
1763
1764                array_pop($this->json_objectStack);
1765
1766                foreach ($elements as $element) {
1767                    if ($element instanceof Exception) {
1768                        return $element;
1769                    }
1770                }
1771
1772                return '[' . join(',', $elements) . ']';
1773
1774            case 'object':
1775                $vars = self::encodeObject($var);
1776
1777                $this->json_objectStack[] = $var;
1778
1779                $properties = array_map(array($this, 'json_name_value'),
1780                                        array_keys($vars),
1781                                        array_values($vars));
1782
1783                array_pop($this->json_objectStack);
1784
1785                foreach ($properties as $property) {
1786                    if ($property instanceof Exception) {
1787                        return $property;
1788                    }
1789                }
1790
1791                return '{' . join(',', $properties) . '}';
1792
1793            default:
1794                return null;
1795        }
1796    }
1797
1798   /**
1799    * array-walking function for use in generating JSON-formatted name-value pairs
1800    *
1801    * @param    string  $name   name of key to use
1802    * @param    mixed   $value  reference to an array element to be encoded
1803    *
1804    * @return   string  JSON-formatted name-value pair, like '"name":value'
1805    * @access   private
1806    */
1807    private function json_name_value($name, $value)
1808    {
1809        // Encoding the $GLOBALS PHP array causes an infinite loop
1810        // if the recursion is not reset here as it contains
1811        // a reference to itself. This is the only way I have come up
1812        // with to stop infinite recursion in this case.
1813        if ($name == 'GLOBALS'
1814           && is_array($value)
1815           && array_key_exists('GLOBALS', $value)) {
1816            $value['GLOBALS'] = '** Recursion **';
1817        }
1818
1819        $encodedValue = $this->json_encode($value);
1820
1821        if ($encodedValue instanceof Exception) {
1822            return $encodedValue;
1823        }
1824
1825        return $this->json_encode(strval($name)) . ':' . $encodedValue;
1826    }
1827
1828    /**
1829     * @deprecated
1830     */
1831    public function setProcessorUrl($URL)
1832    {
1833        trigger_error('The FirePHP::setProcessorUrl() method is no longer supported', E_USER_DEPRECATED);
1834    }
1835
1836    /**
1837     * @deprecated
1838     */
1839    public function setRendererUrl($URL)
1840    {
1841        trigger_error('The FirePHP::setRendererUrl() method is no longer supported', E_USER_DEPRECATED);
1842    }
1843}