source: trunk/lib/App.inc.php @ 341

Last change on this file since 341 was 341, checked in by dan, 15 years ago

Added throttling to the logMsg function for SMS and EMAIL alearts. No need to send the same error to a human multiple times per minute. log_mutiple_timeout is used to cotrolhow long to time a message out for. Default is 60 seconds. So multiple emails and sms's will not be sent if the same msg happens more offten than every minute.

File size: 48.7 KB
Line 
1<?php
2/**
3 * App.inc.php
4 * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
5 *
6 * Primary application framework class.
7 *
8 * @author  Quinn Comendant <quinn@strangecode.com>
9 * @version 2.1
10 */
11
12// Message Types.
13define('MSG_ERR', 1);
14define('MSG_ERROR', MSG_ERR);
15define('MSG_WARNING', 2);
16define('MSG_NOTICE', 4);
17define('MSG_SUCCESS', 8);
18define('MSG_ALL', MSG_SUCCESS | MSG_NOTICE | MSG_WARNING | MSG_ERROR);
19
20require_once dirname(__FILE__) . '/Utilities.inc.php';
21
22class App {
23
24    // Namespace of this application instance.
25    var $_ns;
26
27    // If $app->start has run successfully.
28    var $running = false;
29
30    // Instance of database object.
31    var $db;
32
33    // Array of query arguments will be carried persistently between requests.
34    var $_carry_queries = array();
35
36    // Dictionary of global application parameters.
37    var $_params = array();
38
39    // Default parameters.
40    var $_param_defaults = array(
41
42        // Public name and email address for this application.
43        'site_name' => null,
44        'site_email' => null,
45        'site_url' => '', // URL automatically determined by _SERVER['HTTP_HOST'] if not set here.
46        'images_path' => '', // Location for codebase-generated interface widgets (ex: "/admin/i").
47
48        // The location the user will go if the system doesn't know where else to send them.
49        'redirect_home_url' => '/',
50
51        // SSL URL used when redirecting with $app->sslOn().
52        'ssl_domain' => null,
53        'ssl_enabled' => false,
54
55        // Character set for page output. Used in the Content-Type header and the HTML <meta content-type> tag.
56        'character_set' => 'utf-8',
57
58        // Human-readable format used to display dates.
59        'date_format' => 'd M Y',
60        'time_format' => 'h:i:s A',
61        'sql_date_format' => '%e %b %Y',
62        'sql_time_format' => '%k:%i',
63
64        // Use php sessions?
65        'enable_session' => false,
66        'session_name' => '_session',
67        'session_use_cookies' => true,
68       
69        // Pass the session-id through URLs if cookies are not enabled?
70        // Disable this to prevent session ID theft.
71        'session_use_trans_sid' => false,
72
73        // Use database?
74        'enable_db' => false,
75
76        // Use db-based sessions?
77        'enable_db_session_handler' => false,
78
79        // DB passwords should be set as apache environment variables in httpd.conf, readable only by root.
80        'db_server' => 'localhost',
81        'db_name' => null,
82        'db_user' => null,
83        'db_pass' => null,
84
85        // Database debugging.
86        'db_always_debug' => false, // TRUE = display all SQL queries.
87        'db_debug' => false, // TRUE = display db errors.
88        'db_die_on_failure' => false, // TRUE = script stops on db error.
89
90        // For classes that require db tables, do we check that a table exists and create if missing?
91        'db_create_tables' => true,
92
93        // The level of error reporting. Don't change this to suppress messages, instead use display_errors to control display.
94        'error_reporting' => E_ALL,
95
96        // Don't display errors by default; it is preferable to log them to a file.
97        'display_errors' => false,
98
99        // Directory in which to store log files.
100        'log_directory' => '',
101
102        // PHP error log.
103        'php_error_log' => 'php_error_log',
104
105        // General application log.
106        'log_filename' => 'app_log',
107
108        //Don't email or sms duplicate messages that happen more often than this value in sceonds
109        'log_multiple_timeout' => 60,
110
111        // Logging priority can be any of the following, or false to deactivate:
112        // LOG_EMERG     system is unusable
113        // LOG_ALERT     action must be taken immediately
114        // LOG_CRIT      critical conditions
115        // LOG_ERR       error conditions
116        // LOG_WARNING   warning conditions
117        // LOG_NOTICE    normal, but significant, condition
118        // LOG_INFO      informational message
119        // LOG_DEBUG     debug-level message
120        'log_file_priority' => LOG_INFO,
121        'log_email_priority' => LOG_INFO,
122        'log_sms_priority' => false,
123        'log_screen_priority' => false,
124
125        // Email address to receive log event emails.
126        'log_to_email_address' => 'dan@strangecode.com',
127
128        // SMS Email address to receive log event SMS messages.
129        'log_to_sms_address' => null,
130
131        // A key for calculating simple cryptographic signatures. Set using as an environment variables in the httpd.conf with 'SetEnv SIGNING_KEY <key>'.
132        // Existing password hashes rely on the same key/salt being used to compare encryptions.
133        // Don't change this unless you know existing hashes or signatures will not be affected!
134        'signing_key' => 'aae6abd6209d82a691a9f96384a7634a',
135    );
136
137    /**
138     * This method enforces the singleton pattern for this class. Only one application is running at a time.
139     *
140     * $param   string  $namespace  Name of this application.
141     * @return  object  Reference to the global Cache object.
142     * @access  public
143     * @static
144     */
145    function &getInstance($namespace='')
146    {
147        static $instance = null;
148
149        if ($instance === null) {
150            $instance = new App($namespace);
151        }
152
153        return $instance;
154    }
155
156    /**
157     * Constructor.
158     */
159    function App($namespace='')
160    {
161        // Set namespace of application instance.
162        $this->_ns = $namespace;
163
164        // Initialize default parameters.
165        $this->_params = array_merge($this->_params, $this->_param_defaults);
166       
167        // Begin timing script.
168        require_once dirname(__FILE__) . '/ScriptTimer.inc.php';
169        $this->timer = new ScriptTimer();
170        $this->timer->start('_app');
171    }
172
173    /**
174     * Set (or overwrite existing) parameters by passing an array of new parameters.
175     *
176     * @access public
177     * @param  array    $param     Array of parameters (key => val pairs).
178     */
179    function setParam($param=null)
180    {
181        if (isset($param) && is_array($param)) {
182            // Merge new parameters with old overriding only those passed.
183            $this->_params = array_merge($this->_params, $param);
184        }
185    }
186
187    /**
188     * Return the value of a parameter.
189     *
190     * @access  public
191     * @param   string  $param      The key of the parameter to return.
192     * @return  mixed               Parameter value, or null if not existing.
193     */
194    function getParam($param=null)
195    {
196        if ($param === null) {
197            return $this->_params;
198        } else if (isset($this->_params[$param])) {
199            return $this->_params[$param];
200        } else {
201            trigger_error(sprintf('Parameter is not set: %s', $param), E_USER_NOTICE);
202            return null;
203        }
204    }
205
206    /**
207     * Begin running this application.
208     *
209     * @access  public
210     * @author  Quinn Comendant <quinn@strangecode.com>
211     * @since   15 Jul 2005 00:32:21
212     */
213    function start()
214    {
215        if ($this->running) {
216            return false;
217        }
218
219        // Error reporting.
220        ini_set('error_reporting', $this->getParam('error_reporting'));
221        ini_set('display_errors', $this->getParam('display_errors'));
222        ini_set('log_errors', true);
223        if (is_dir($this->getParam('log_directory')) && is_writable($this->getParam('log_directory'))) {
224            ini_set('error_log', $this->getParam('log_directory') . '/' . $this->getParam('php_error_log'));
225        }
226
227        // Set character set to use for multi-byte string functions.
228        mb_internal_encoding($this->getParam('character_set'));
229        switch (mb_strtolower($this->getParam('character_set'))) {
230        case 'utf-8' :
231            mb_language('uni');
232            break;
233
234        case 'iso-2022-jp' :
235            mb_language('ja');
236            break;
237
238        case 'iso-8859-1' :
239        default :
240            mb_language('en');
241            break;
242        }
243
244        /**
245         * 1. Start Database.
246         */
247
248        if (true === $this->getParam('enable_db')) {
249
250            // DB connection parameters taken from environment variables in the httpd.conf file, readable only by root.
251            if (!empty($_SERVER['DB_SERVER'])) {
252                $this->setParam(array('db_server' => $_SERVER['DB_SERVER']));
253            }
254            if (!empty($_SERVER['DB_NAME'])) {
255                $this->setParam(array('db_name' => $_SERVER['DB_NAME']));
256            }
257            if (!empty($_SERVER['DB_USER'])) {
258                $this->setParam(array('db_user' => $_SERVER['DB_USER']));
259            }
260            if (!empty($_SERVER['DB_PASS'])) {
261                $this->setParam(array('db_pass' => $_SERVER['DB_PASS']));
262            }
263
264            // There will ever only be one instance of the DB object, and here is where it is instantiated.
265            require_once dirname(__FILE__) . '/DB.inc.php';
266            $this->db =& DB::getInstance();
267            $this->db->setParam(array(
268                'db_server' => $this->getParam('db_server'),
269                'db_name' => $this->getParam('db_name'),
270                'db_user' => $this->getParam('db_user'),
271                'db_pass' => $this->getParam('db_pass'),
272                'db_always_debug' => $this->getParam('db_always_debug'),
273                'db_debug' => $this->getParam('db_debug'),
274                'db_die_on_failure' => $this->getParam('db_die_on_failure'),
275            ));
276
277            // Connect to database.
278            $this->db->connect();
279        }
280
281
282        /**
283         * 2. Start PHP session.
284         */
285
286        // Skip session for some user agents.
287        if (preg_match('/Atomz|ApacheBench|Wget/i', getenv('HTTP_USER_AGENT'))) {
288            $this->setParam(array('enable_session' => false));
289        }
290
291        if (true === $this->getParam('enable_session')) {
292
293            if (true === $this->getParam('enable_db_session_handler') && true === $this->getParam('enable_db')) {
294                // Database session handling.
295                require_once dirname(__FILE__) . '/DBSessionHandler.inc.php';
296                $db_save_handler = new DBSessionHandler($this->db, array(
297                    'db_table' => 'session_tbl',
298                    'create_table' => $this->getParam('db_create_tables'),
299                ));
300            }
301
302            // Session parameters.
303            ini_set('session.use_cookies', $this->getParam('session_use_cookies'));
304            ini_set('session.use_trans_sid', false);
305            ini_set('session.entropy_file', '/dev/urandom');
306            ini_set('session.entropy_length', '512');
307            session_name($this->getParam('session_name'));
308
309            // Start the session.
310            session_start();
311
312            if (!isset($_SESSION['_app'][$this->_ns])) {
313                // Access session data using: $_SESSION['...'].
314                // Initialize here _after_ session has started.
315                $_SESSION['_app'][$this->_ns] = array(
316                    'messages' => array(),
317                    'boomerang' => array('url'),
318                );
319            }
320        }
321
322
323        /**
324         * 3. Misc setup.
325         */
326
327        // Script URI will be something like http://host.name.tld (no ending slash)
328        // and is used whenever a URL need be used to the current site.
329        // Not available on cli scripts obviously.
330        if (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST'] && '' == $this->getParam('site_url')) {
331            $this->setParam(array('site_url' => sprintf('%s://%s', ('on' == getenv('HTTPS') ? 'https' : 'http'), getenv('HTTP_HOST'))));
332        }
333
334        // A key for calculating simple cryptographic signatures.
335        if (isset($_SERVER['SIGNING_KEY'])) {
336            $this->setParam(array('signing_key' => $_SERVER['SIGNING_KEY']));
337        }
338
339        // Character set. This should also be printed in the html header template.
340        header('Content-type: text/html; charset=' . $this->getParam('character_set'));
341       
342        // Set the version of the codebase we're using.
343        $codebase_version_file = dirname(__FILE__) . '/../docs/version.txt';
344        if (is_readable($codebase_version_file)) {
345            $codebase_version = trim(file_get_contents($codebase_version_file));
346            $this->setParam(array('codebase_version' => $codebase_version));
347            header('X-Codebase-Version: ' . $codebase_version);
348        }
349
350        $this->running = true;
351    }
352
353    /**
354     * Stop running this application.
355     *
356     * @access  public
357     * @author  Quinn Comendant <quinn@strangecode.com>
358     * @since   17 Jul 2005 17:20:18
359     */
360    function stop()
361    {
362        session_write_close();
363        restore_include_path();
364        $this->running = false;
365        $num_queries = 0;
366        if (true === $this->getParam('enable_db')) {
367            $num_queries = $this->db->numQueries();
368            $this->db->close();
369        }
370        $this->timer->stop('_app');
371        $this->logMsg(sprintf('Script ended gracefully. Execution time: %s. Number of db queries: %s.', $this->timer->getTime('_app'), $num_queries), LOG_DEBUG, __FILE__, __LINE__);
372    }
373
374
375    /**
376     * Add a message to the session, which is printed in the header.
377     * Just a simple way to print messages to the user.
378     *
379     * @access public
380     *
381     * @param string $message The text description of the message.
382     * @param int    $type    The type of message: MSG_NOTICE,
383     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
384     * @param string $file    __FILE__.
385     * @param string $line    __LINE__.
386     */
387    function raiseMsg($message, $type=MSG_NOTICE, $file=null, $line=null)
388    {
389        $message = trim($message);
390
391        if (!$this->running) {
392            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
393            return false;
394        }
395
396        if ('' == trim($message)) {
397            $this->logMsg(sprintf('Raised message is an empty string.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
398            return false;
399        }
400
401        // Save message in session under unique key to avoid duplicate messages.
402        $msg_id = md5($type . $message);
403        if (!isset($_SESSION['_app'][$this->_ns]['messages'][$msg_id])) {
404            $_SESSION['_app'][$this->_ns]['messages'][$msg_id] = array(
405                'type'    => $type,
406                'message' => $message,
407                'file'    => $file,
408                'line'    => $line,
409                'count'   => (isset($_SESSION['_app'][$this->_ns]['messages'][$msg_id]['count']) ? (1 + $_SESSION['_app'][$this->_ns]['messages'][$msg_id]['count']) : 1)
410            );
411        }
412
413        if (!in_array($type, array(MSG_NOTICE, MSG_SUCCESS, MSG_WARNING, MSG_ERR))) {
414            $this->logMsg(sprintf('Invalid MSG_* type: %s', $type), LOG_NOTICE, __FILE__, __LINE__);
415        }
416    }
417   
418    /**
419     * Returns an array of the raised messages.
420     *
421     * @access  public
422     * @return  array   List of messages in FIFO order.
423     * @author  Quinn Comendant <quinn@strangecode.com>
424     * @since   21 Dec 2005 13:09:20
425     */
426    function getRaisedMessages()
427    {
428        if (!$this->running) {
429            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
430            return false;
431        }
432
433        return isset($_SESSION['_app'][$this->_ns]['messages']) ? $_SESSION['_app'][$this->_ns]['messages'] : array();
434    }
435   
436    /**
437     * Resets the message list.
438     *
439     * @access  public
440     * @author  Quinn Comendant <quinn@strangecode.com>
441     * @since   21 Dec 2005 13:21:54
442     */
443    function clearRaisedMessages()
444    {
445        if (!$this->running) {
446            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
447            return false;
448        }
449       
450        $_SESSION['_app'][$this->_ns]['messages'] = array();
451    }
452
453    /**
454     * Prints the HTML for displaying raised messages.
455     *
456     * @access  public
457     * @author  Quinn Comendant <quinn@strangecode.com>
458     * @since   15 Jul 2005 01:39:14
459     */
460    function printRaisedMessages()
461    {
462        if (!$this->running) {
463            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
464            return false;
465        }
466       
467        $messages = $this->getRaisedMessages();
468        if (!empty($messages)) {
469            ?><div id="sc-msg" class="sc-msg"><?php
470            foreach ($messages as $m) {
471                if (error_reporting() > 0 && $this->getParam('display_errors') && isset($m['file']) && isset($m['line'])) {
472                    echo "\n<!-- [" . $m['file'] . ' : ' . $m['line'] . '] -->';
473                }
474                switch ($m['type']) {
475                case MSG_ERR:
476                    echo '<div class="sc-msg-error">' . $m['message'] . '</div>';
477                    break;
478
479                case MSG_WARNING:
480                    echo '<div class="sc-msg-warning">' . $m['message'] . '</div>';
481                    break;
482
483                case MSG_SUCCESS:
484                    echo '<div class="sc-msg-success">' . $m['message'] . '</div>';
485                    break;
486
487                case MSG_NOTICE:
488                default:
489                    echo '<div class="sc-msg-notice">' . $m['message'] . '</div>';
490                    break;
491
492                }
493            }
494            ?></div><?php
495        }
496        $this->clearRaisedMessages();
497    }
498
499    /**
500     * Logs messages to defined channels: file, email, sms, and screen. Repeated messages are
501     * not repeated but printed once with count.
502     *
503     * @access public
504     * @param string $message   The text description of the message.
505     * @param int    $priority  The type of message priority (in descending order):
506     *                          LOG_EMERG     system is unusable
507     *                          LOG_ALERT     action must be taken immediately
508     *                          LOG_CRIT      critical conditions
509     *                          LOG_ERR       error conditions
510     *                          LOG_WARNING   warning conditions
511     *                          LOG_NOTICE    normal, but significant, condition
512     *                          LOG_INFO      informational message
513     *                          LOG_DEBUG     debug-level message
514     * @param string $file      The file where the log event occurs.
515     * @param string $line      The line of the file where the log event occurs.
516     */
517    function logMsg($message, $priority=LOG_INFO, $file=null, $line=null)
518    {
519        static $previous_events = array();
520
521        // If priority is not specified, assume the worst.
522        if (!$this->logPriorityToString($priority)) {
523            $this->logMsg(sprintf('Log priority %s not defined. (Message: %s)', $priority, $message), LOG_EMERG, $file, $line);
524            $priority = LOG_EMERG;
525        }
526
527        // If log file is not specified, don't log to a file.
528        if (!$this->getParam('log_directory') || !$this->getParam('log_filename') || !is_dir($this->getParam('log_directory')) || !is_writable($this->getParam('log_directory'))) {
529            $this->setParam(array('log_file_priority' => false));
530            // We must use trigger_error to report this problem rather than calling $app->logMsg, which might lead to an infinite loop.
531            trigger_error(sprintf('Codebase error: log directory (%s) not found or writable.', $this->getParam('log_directory')), E_USER_NOTICE);
532        }
533
534        // Make sure to log in the system's locale.
535        $locale = setlocale(LC_TIME, 0);
536        setlocale(LC_TIME, 'C');
537
538        // Strip HTML tags except any with more than 7 characters because that's probably not a HTML tag, e.g. <email@address.com>.
539        preg_match_all('/(<[^>\s]{7,})[^>]*>/', $message, $strip_tags_allow);
540        $message = strip_tags(preg_replace('/\s+/', ' ', $message), (!empty($strip_tags_allow[1]) ? join('> ', $strip_tags_allow[1]) . '>' : null));
541
542        // Store this event under a unique key, counting each time it occurs so that it only gets reported a limited number of times.
543        $msg_id = md5($message . $priority . $file . $line);
544        if (isset($previous_events[$msg_id])) {
545            $previous_events[$msg_id]++;
546            if ($previous_events[$msg_id] == 2) {
547                $this->logMsg(sprintf('%s (Event repeated %s or more times)', $message, $previous_events[$msg_id]), $priority, $file, $line);
548            }
549            return false;
550        } else {
551            $previous_events[$msg_id] = 1;
552        }
553
554        // Create tmp files so that we don't email and sms redundantly.
555        $site_hash = md5(SITE_BASE);
556        $temp_dir = "/tmp/codebase_msgs_$site_hash/";
557        $temp_file = $temp_dir . $msg_id;
558        if (!is_dir($temp_dir)) {
559            mkdir($temp_dir);
560        }
561        $send_notifications = true;
562        if (is_file($temp_file)) {
563            $msg_last_sent = filectime($temp_file);
564            //Has this message been sent more recently than the timeout?
565            if ((time() - $msg_last_sent) < $this->getParam('log_multiple_timeout')) {
566                //this message was alreay sent recently
567                $send_notifications = false;
568            } else {
569                //timeout has expired go ahead and send notifications again
570                unlink($temp_file);
571            }
572        } else {
573            touch($temp_file);
574        }
575       
576       
577        // Data to be stored for a log event.
578        $event = array(
579            'date'      => date('Y-m-d H:i:s'),
580            'remote ip' => getRemoteAddr(),
581            'pid'       => (mb_substr(PHP_OS, 0, 3) != 'WIN' ? posix_getpid() : ''),
582            'type'      => $this->logPriorityToString($priority),
583            'file:line' => "$file : $line",
584            'url'       => mb_substr(isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '', 0, 128),
585            'message'   => $message
586        );
587
588        // FILE ACTION
589        if ($this->getParam('log_file_priority') && $priority <= $this->getParam('log_file_priority')) {
590            $event_str = '[' . join('] [', $event) . ']';
591            error_log(mb_substr($event_str, 0, 1024) . "\n", 3, $this->getParam('log_directory') . '/' . $this->getParam('log_filename'));
592        }
593
594        // NOTIFY SOMEONE
595        if ($send_notifications) {
596
597            // EMAIL ACTION
598            if ($this->getParam('log_email_priority') && $priority <= $this->getParam('log_email_priority')) {
599                $subject = sprintf('[%s %s] %s', getenv('HTTP_HOST'), $event['type'], $message);
600                $email_msg = sprintf("A %s log event occured on %s\n\n", $event['type'], getenv('HTTP_HOST'));
601                $headers = "From: codebase@strangecode.com";
602                foreach ($event as $k=>$v) {
603                    $email_msg .= sprintf("%-11s%s\n", $k, $v);
604                }
605                mb_send_mail($this->getParam('log_to_email_address'), $subject, $email_msg, $headers, '-f codebase@strangecode.com');
606            }
607   
608            // SMS ACTION
609            if ($this->getParam('log_sms_priority') && $priority <= $this->getParam('log_sms_priority')) {
610                $subject = sprintf('[%s %s]', getenv('HTTP_HOST'), $priority);
611                $sms_msg = sprintf('%s [%s:%s]', mb_substr($event['message'], 0, 64), basename($file), $line);
612                $headers = "From: codebase@strangecode.com";
613                mb_send_mail($this->getParam('log_to_sms_address'), $subject, $sms_msg, $headers, '-f codebase@strangecode.com');
614            }
615
616        }
617   
618        // SCREEN ACTION
619        if ($this->getParam('log_screen_priority') && $priority <= $this->getParam('log_screen_priority')) {
620            echo "[{$event['type']}] [{$event['message']}]\n";
621        }
622
623        // Restore original locale.
624        setlocale(LC_TIME, $locale);
625    }
626
627    /**
628     * Returns the string representation of a LOG_* integer constant.
629     *
630     * @param int  $priority  The LOG_* integer constant.
631     *
632     * @return                The string representation of $priority.
633     */
634    function logPriorityToString($priority) {
635        $priorities = array(
636            LOG_EMERG   => 'emergency',
637            LOG_ALERT   => 'alert',
638            LOG_CRIT    => 'critical',
639            LOG_ERR     => 'error',
640            LOG_WARNING => 'warning',
641            LOG_NOTICE  => 'notice',
642            LOG_INFO    => 'info',
643            LOG_DEBUG   => 'debug'
644        );
645        if (isset($priorities[$priority])) {
646            return $priorities[$priority];
647        } else {
648            return false;
649        }
650    }
651
652    /**
653     * Forcefully set a query argument even if one currently exists in the request.
654     * Values in the _carry_queries array will be copied to URLs (via $app->url()) and
655     * to hidden input values (via printHiddenSession()).
656     *
657     * @access  public
658     * @param   mixed   $query_key  The key (or keys, as an array) of the query argument to save.
659     * @param   mixed   $val        The new value of the argument key.
660     * @author  Quinn Comendant <quinn@strangecode.com>
661     * @since   13 Oct 2007 11:34:51
662     */
663    function setQuery($query_key, $val)
664    {
665        if (!is_array($query_key)) {
666            $query_key = array($query_key);
667        }
668        foreach ($query_key as $k) {
669            // Set the value of the specified query argument into the _carry_queries array.
670            $this->_carry_queries[$k] = $val;
671        }
672    }
673
674    /**
675     * Specify which query arguments will be carried persistently between requests.
676     * Values in the _carry_queries array will be copied to URLs (via $app->url()) and
677     * to hidden input values (via printHiddenSession()).
678     *
679     * @access  public
680     * @param   mixed   $query_key   The key (or keys, as an array) of the query argument to save.
681     * @param   mixed   $default    If the key is not available, set to this default value.
682     * @author  Quinn Comendant <quinn@strangecode.com>
683     * @since   14 Nov 2005 19:24:52
684     */
685    function carryQuery($query_key, $default=false)
686    {
687        if (!is_array($query_key)) {
688            $query_key = array($query_key);
689        }
690        foreach ($query_key as $k) {
691            // If not already set, and there is a non-empty value provided in the request...
692            if (!isset($this->_carry_queries[$k]) && false !== getFormData($k, $default)) {
693                // Copy the value of the specified query argument into the _carry_queries array.
694                $this->_carry_queries[$k] = getFormData($k, $default);
695                $this->logMsg(sprintf('Carrying query: %s => %s', $k, truncate(getDump($this->_carry_queries[$k], true), 128, 'end')), LOG_DEBUG, __FILE__, __LINE__);
696            }
697        }
698    }
699
700    /**
701     * dropQuery() is the opposite of carryQuery(). The specified value will not appear in
702     * url()/ohref()/printHiddenSession() modified URLs unless explicitly written in.
703     *
704     * @access  public
705     * @param   mixed   $query_key  The key (or keys, as an array) of the query argument to remove.
706     * @author  Quinn Comendant <quinn@strangecode.com>
707     * @since   18 Jun 2007 20:57:29
708     */
709    function dropQuery($query_key, $unset=false)
710    {
711        if (!is_array($query_key)) {
712            $query_key = array($query_key);
713        }
714        foreach ($query_key as $k) {
715            if (isset($this->_carry_queries[$k])) {
716                // Remove the value of the specified query argument from the _carry_queries array.
717                $this->logMsg(sprintf('Dropping carried query: %s => %s', $k, $this->_carry_queries[$k]), LOG_DEBUG, __FILE__, __LINE__);
718                unset($this->_carry_queries[$k]);
719            }
720            if ($unset && isset($_REQUEST[$k])) {
721                unset($_REQUEST[$k], $_GET[$k], $_POST[$k], $_COOKIE[$k]);
722            }
723        }
724    }
725
726    /**
727     * Outputs a fully qualified URL with a query of all the used (ie: not empty)
728     * keys and values, including optional queries. This allows mindless retention
729     * of query arguments across page requests. If cookies are not
730     * used, the session id will be propagated in the URL.
731     *
732     * @param  string $url              The initial url
733     * @param  mixed  $carry_args       Additional url arguments to carry in the query,
734     *                                  or FALSE to prevent carrying queries. Can be any of the following formats:
735     *                                      array('key1', key2', key3')  <-- to save these keys if in the form data.
736     *                                      array('key1'=>'value', key2'='value')  <-- to set keys to default values if not present in form data.
737     *                                      false  <-- To not carry any queries. If URL already has queries those will be retained.
738     *
739     * @param  mixed  $always_include_sid  Always add the session id, even if using_trans_sid = true. This is required when
740     *                                     URL starts with http, since PHP using_trans_sid doesn't do those and also for
741     *                                     header('Location...') redirections.
742     *
743     * @return string url with attached queries and, if not using cookies, the session id
744     */
745    function url($url, $carry_args=null, $always_include_sid=false)
746    {
747        if (!$this->running) {
748            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
749            return false;
750        }
751
752        // Get any provided query arguments to include in the final URL.
753        // If FALSE is a provided here, DO NOT carry the queries.
754        $do_carry_queries = true;
755        $one_time_carry_queries = array();
756        if (!is_null($carry_args)) {
757            if (is_array($carry_args) && !empty($carry_args)) {
758                foreach ($carry_args as $key=>$arg) {
759                    // Get query from appropriate source.
760                    if (false === $arg) {
761                        $do_carry_queries = false;
762                    } else if (false !== getFormData($arg, false)) {
763                        $one_time_carry_queries[$arg] = getFormData($arg); // Set arg to form data if available.
764                    } else if (!is_numeric($key) && '' != $arg) {
765                        $one_time_carry_queries[$key] = getFormData($key, $arg); // Set to arg to default if specified (overwritten by form data).
766                    }
767                }
768            } else if (false !== getFormData($carry_args, false)) {
769                $one_time_carry_queries[$carry_args] = getFormData($carry_args);
770            } else if (false === $carry_args) {
771                $do_carry_queries = false;
772            }
773        }
774
775        // Get the first delimiter that is needed in the url.
776        $delim = mb_strpos($url, '?') !== false ? ini_get('arg_separator.output') : '?';
777
778        $q = '';
779        if ($do_carry_queries) {
780            // Join the global _carry_queries and local one_time_carry_queries.
781            $query_args = urlEncodeArray(array_merge($this->_carry_queries, $one_time_carry_queries));
782            foreach ($query_args as $key=>$val) {
783                // Check value is set and value does not already exist in the url.
784                if (!preg_match('/[?&]' . preg_quote($key) . '=/', $url)) {
785                    $q .= $delim . $key . '=' . $val;
786                    $delim = ini_get('arg_separator.output');
787                }
788            }
789        }
790
791        // Include the necessary SID if the following is true:
792        // - no cookie in http request OR cookies disabled in App
793        // - sessions are enabled
794        // - the link stays on our site
795        // - transparent SID propagation with session.use_trans_sid is not being used OR url begins with protocol (using_trans_sid has no effect here)
796        // OR
797        // - we must include the SID because we say so (it's used in a context where cookies will not be effective, ie. moving from http to https)
798        // AND
799        // - the SID is not already in the query.
800        if (
801            (
802                (
803                    (
804                        !isset($_COOKIE[session_name()])
805                        || !$this->getParam('session_use_cookies')
806                    )
807                    && $this->getParam('session_use_trans_sid')
808                    && $this->getParam('enable_session')
809                    && isMyDomain($url)
810                    && (
811                        !ini_get('session.use_trans_sid')
812                        || preg_match('!^(http|https)://!i', $url)
813                    )
814                )
815                || $always_include_sid
816            )
817            && !preg_match('/[?&]' . preg_quote(session_name()) . '=/', $url)
818        ) {
819            $url .= $q . $delim . session_name() . '=' . session_id();
820            return $url;
821        } else {
822            $url .= $q;
823            return $url;
824        }
825    }
826
827    /**
828     * Returns a HTML-friendly URL processed with $app->url and & replaced with &amp;
829     *
830     * @access  public
831     * @param   string  $url    Input URL to parse.
832     * @return  string          URL passed through $app->url() and then & turned to $amp;.
833     * @author  Quinn Comendant <quinn@strangecode.com>
834     * @since   09 Dec 2005 17:58:45
835     */
836    function oHREF($url, $carry_args=null, $always_include_sid=false)
837    {
838        $url = $this->url($url, $carry_args, $always_include_sid);
839
840        // Replace any & not followed by an html or unicode entity with it's &amp; equivalent.
841        $url = preg_replace('/&(?![\w\d#]{1,10};)/', '&amp;', $url);
842
843        return $url;
844    }
845
846    /**
847     * Prints a hidden form element with the PHPSESSID when cookies are not used, as well
848     * as hidden form elements for GET_VARS that might be in use.
849     *
850     * @param  mixed  $carry_args        Additional url arguments to carry in the query,
851     *                                   or FALSE to prevent carrying queries. Can be any of the following formats:
852     *                                      array('key1', key2', key3')  <-- to save these keys if in the form data.
853     *                                      array('key1'=>'value', key2'='value')  <-- to set keys to default values if not present in form data.
854     *                                      false  <-- To not carry any queries. If URL already has queries those will be retained.
855     */
856    function printHiddenSession($carry_args=null)
857    {
858        if (!$this->running) {
859            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
860            return false;
861        }
862
863        // Get any provided query arguments to include in the final hidden form data.
864        // If FALSE is a provided here, DO NOT carry the queries.
865        $do_carry_queries = true;
866        $one_time_carry_queries = array();
867        if (!is_null($carry_args)) {
868            if (is_array($carry_args) && !empty($carry_args)) {
869                foreach ($carry_args as $key=>$arg) {
870                    // Get query from appropriate source.
871                    if (false === $arg) {
872                        $do_carry_queries = false;
873                    } else if (false !== getFormData($arg, false)) {
874                        $one_time_carry_queries[$arg] = getFormData($arg); // Set arg to form data if available.
875                    } else if (!is_numeric($key) && '' != $arg) {
876                        $one_time_carry_queries[$key] = getFormData($key, $arg); // Set to arg to default if specified (overwritten by form data).
877                    }
878                }
879            } else if (false !== getFormData($carry_args, false)) {
880                $one_time_carry_queries[$carry_args] = getFormData($carry_args);
881            } else if (false === $carry_args) {
882                $do_carry_queries = false;
883            }
884        }
885
886        // For each existing request value, we create a hidden input to carry it through a form.
887        if ($do_carry_queries) {
888            // Join the global _carry_queries and local one_time_carry_queries.
889            // urlencode is not used here, not for form data!
890            $query_args = array_merge($this->_carry_queries, $one_time_carry_queries);
891            foreach ($query_args as $key=>$val) {
892                printf('<input type="hidden" name="%s" value="%s" />', $key, $val);
893            }
894        }
895
896        // Include the SID if cookies are disabled.
897        if (!isset($_COOKIE[session_name()]) && !ini_get('session.use_trans_sid')) {
898            printf('<input type="hidden" name="%s" value="%s" />', session_name(), session_id());
899        }
900    }
901
902    /**
903     * Uses an http header to redirect the client to the given $url. If sessions are not used
904     * and the session is not already defined in the given $url, the SID is appended as a URI query.
905     * As with all header generating functions, make sure this is called before any other output.
906     *
907     * @param   string  $url                    The URL the client will be redirected to.
908     * @param   mixed   $carry_args             Additional url arguments to carry in the query,
909     *                                          or FALSE to prevent carrying queries. Can be any of the following formats:
910     *                                          -array('key1', key2', key3')  <-- to save these keys if in the form data.
911     *                                          -array('key1' => 'value', key2' => 'value')  <-- to set keys to default values if not present in form data.
912     *                                          -false  <-- To not carry any queries. If URL already has queries those will be retained.
913     * @param   bool    $always_include_sid     Force session id to be added to Location header.
914     */
915    function dieURL($url, $carry_args=null, $always_include_sid=false)
916    {
917        if (!$this->running) {
918            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
919            return false;
920        }
921
922        if ('' == $url) {
923            // If URL is not specified, use the redirect_home_url.
924            $url = $this->getParam('redirect_home_url');
925        }
926
927        if (preg_match('!^/!', $url)) {
928            // If relative URL is given, prepend correct local hostname.
929            $scheme = 'on' == getenv('HTTPS') ? 'https' : 'http';
930            $host = getenv('HTTP_HOST');
931            $url = sprintf('%s://%s%s', $scheme, $host, $url);
932        }
933
934        $url = $this->url($url, $carry_args, $always_include_sid);
935
936        // Should we send a "303 See Other" header here instead of relying on the 302 sent automatically by PHP?
937        header(sprintf('Location: %s', $url));
938        $this->logMsg(sprintf('dieURL: %s', $url), LOG_DEBUG, __FILE__, __LINE__);
939
940        // End application.
941        // Recommended, although I'm not sure it's necessary: http://cn2.php.net/session_write_close
942        $this->stop();
943        die;
944    }
945
946    /*
947    * Redirects a user by calling $app->dieURL(). It will use:
948    * 1. the stored boomerang URL, it it exists
949    * 2. a specified $default_url, it it exists
950    * 3. the referring URL, it it exists.
951    * 4. redirect_home_url configuration variable.
952    *
953    * @access   public
954    * @param    string  $id             Identifier for this script.
955    * @param    mixed   $carry_args     Additional arguments to carry in the URL automatically (see $app->oHREF()).
956    * @param    string  $default_url    A default URL if there is not a valid specified boomerang URL.
957    * @param    bool    $queryless_referrer_comparison   Exclude the URL query from the refererIsMe() comparison.
958    * @return   bool                    False if the session is not running. No return otherwise.
959    * @author   Quinn Comendant <quinn@strangecode.com>
960    * @since    31 Mar 2006 19:17:00
961    */
962    function dieBoomerangURL($id=null, $carry_args=null, $default_url=null, $queryless_referrer_comparison=false)
963    {
964        if (!$this->running) {
965            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
966            return false;
967        }
968
969        // Get URL from stored boomerang. Allow non specific URL if ID not valid.
970        if ($this->validBoomerangURL($id, true)) {
971            if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
972                $url = $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id];
973                $this->logMsg(sprintf('dieBoomerangURL(%s) found: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
974            } else {
975                $url = end($_SESSION['_app'][$this->_ns]['boomerang']['url']);
976                $this->logMsg(sprintf('dieBoomerangURL(%s) using: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
977            }
978            // Delete stored boomerang.
979            $this->deleteBoomerangURL($id);
980        } else if (isset($default_url)) {
981            $url = $default_url;
982        } else if (!refererIsMe(true === $queryless_referrer_comparison)) {
983            // Ensure that the redirecting page is not also the referrer.
984            $url = getenv('HTTP_REFERER');
985            $this->logMsg(sprintf('dieBoomerangURL(%s) using referrer: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
986        } else {
987            // If URL is not specified, use the redirect_home_url.
988            $url = $this->getParam('redirect_home_url');
989            $this->logMsg(sprintf('dieBoomerangURL(%s) using redirect_home_url: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
990        }
991
992        // A redirection will never happen immediately twice.
993        // Set the time so ensure this doesn't happen.
994        $_SESSION['_app'][$this->_ns]['boomerang']['time'] = time();
995        $this->dieURL($url, $carry_args);
996    }
997
998    /**
999     * Set the URL to return to when $app->dieBoomerangURL() is called.
1000     *
1001     * @param string  $url  A fully validated URL.
1002     * @param bool  $id     An identification tag for this url.
1003     * FIXME: url garbage collection?
1004     */
1005    function setBoomerangURL($url=null, $id=null)
1006    {
1007        if (!$this->running) {
1008            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1009            return false;
1010        }
1011        // A redirection will never happen immediately after setting the boomerangURL.
1012        // Set the time so ensure this doesn't happen. See $app->validBoomerangURL for more.
1013
1014        if ('' != $url && is_string($url)) {
1015            // Delete any boomerang request keys in the query string (along with any trailing delimiters after the deletion).
1016            $url = preg_replace(array('/([&?])boomerang=\w+[&?]?/', '/[&?]$/'), array('$1', ''), $url);
1017
1018            if (isset($_SESSION['_app'][$this->_ns]['boomerang']['url']) && is_array($_SESSION['_app'][$this->_ns]['boomerang']['url']) && !empty($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1019                // If the URL currently exists in the boomerang array, delete.
1020                while ($existing_key = array_search($url, $_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1021                    unset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$existing_key]);
1022                }
1023            }
1024
1025            if (isset($id)) {
1026                $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id] = $url;
1027            } else {
1028                $_SESSION['_app'][$this->_ns]['boomerang']['url'][] = $url;
1029            }
1030            $this->logMsg(sprintf('setBoomerangURL(%s): %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1031            return true;
1032        } else {
1033            $this->logMsg(sprintf('setBoomerangURL(%s) is empty!', $id, $url), LOG_NOTICE, __FILE__, __LINE__);
1034            return false;
1035        }
1036    }
1037
1038    /**
1039     * Return the URL set for the specified $id, or an empty string if one isn't set.
1040     *
1041     * @param string  $id     An identification tag for this url.
1042     */
1043    function getBoomerangURL($id=null)
1044    {
1045        if (!$this->running) {
1046            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1047            return false;
1048        }
1049
1050        if (isset($id)) {
1051            if (isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1052                return $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id];
1053            } else {
1054                return '';
1055            }
1056        } else if (is_array($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1057            return end($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1058        } else {
1059            return false;
1060        }
1061    }
1062
1063    /**
1064     * Delete the URL set for the specified $id.
1065     *
1066     * @param string  $id     An identification tag for this url.
1067     */
1068    function deleteBoomerangURL($id=null)
1069    {
1070        if (!$this->running) {
1071            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1072            return false;
1073        }
1074
1075        $this->logMsg(sprintf('deleteBoomerangURL(%s): %s', $id, $this->getBoomerangURL($id)), LOG_DEBUG, __FILE__, __LINE__);
1076
1077        if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1078            unset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id]);
1079        } else if (is_array($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1080            array_pop($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1081        }
1082    }
1083
1084    /**
1085     * Check if a valid boomerang URL value has been set. A boomerang URL is considered
1086     * valid if: 1) it is not empty, 2) it is not the current URL, and 3) has not been accessed within n seconds.
1087     *
1088     * @return bool  True if it is set and valid, false otherwise.
1089     */
1090    function validBoomerangURL($id=null, $use_nonspecificboomerang=false)
1091    {
1092        if (!$this->running) {
1093            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1094            return false;
1095        }
1096
1097        if (!isset($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1098            $this->logMsg(sprintf('validBoomerangURL(%s) no boomerang URL set.', $id), LOG_DEBUG, __FILE__, __LINE__);
1099            return false;
1100        }
1101
1102        // Time is the time stamp of a boomerangURL redirection, or setting of a boomerangURL.
1103        // a boomerang redirection will always occur at least several seconds after the last boomerang redirect
1104        // or a boomerang being set.
1105        $boomerang_time = isset($_SESSION['_app'][$this->_ns]['boomerang']['time']) ? $_SESSION['_app'][$this->_ns]['boomerang']['time'] : 0;
1106
1107        $url = '';
1108        if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1109            $url = $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id];
1110        } else if (!isset($id) || $use_nonspecificboomerang) {
1111            // Use non specific boomerang if available.
1112            $url = end($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1113        }
1114
1115        $this->logMsg(sprintf('validBoomerangURL(%s) testing: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1116
1117        if ('' == $url) {
1118            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, empty!', $id), LOG_DEBUG, __FILE__, __LINE__);
1119            return false;
1120        }
1121        if ($url == absoluteMe()) {
1122            // The URL we are directing to is the current page.
1123            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, same as absoluteMe: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1124            return false;
1125        }
1126        if ($boomerang_time >= (time() - 2)) {
1127            // Last boomerang direction was less than 2 seconds ago.
1128            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, boomerang_time too short: %s seconds', $id, time() - $boomerang_time), LOG_DEBUG, __FILE__, __LINE__);
1129            return false;
1130        }
1131
1132        $this->logMsg(sprintf('validBoomerangURL(%s) is valid: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1133        return true;
1134    }
1135
1136    /**
1137     * Force the user to connect via https (port 443) by redirecting them to
1138     * the same page but with https.
1139     */
1140    function sslOn()
1141    {
1142        if (function_exists('apache_get_modules')) {
1143            $modules = apache_get_modules();
1144        } else {
1145            // It's safe to assume we have mod_ssl if we can't determine otherwise.
1146            $modules = array('mod_ssl');
1147        }
1148
1149        if ('' == getenv('HTTPS') && $this->getParam('ssl_enabled') && in_array('mod_ssl', $modules)) {
1150            $this->raiseMsg(sprintf(_("Secure SSL connection made to %s"), $this->getParam('ssl_domain')), MSG_NOTICE, __FILE__, __LINE__);
1151            // Always append session because some browsers do not send cookie when crossing to SSL URL.
1152            $this->dieURL('https://' . $this->getParam('ssl_domain') . getenv('REQUEST_URI'), null, true);
1153        }
1154    }
1155
1156
1157    /**
1158     * to enforce the user to connect via http (port 80) by redirecting them to
1159     * a http version of the current url.
1160     */
1161    function sslOff()
1162    {
1163        if ('' != getenv('HTTPS')) {
1164            $this->dieURL('http://' . getenv('HTTP_HOST') . getenv('REQUEST_URI'), null, true);
1165        }
1166    }
1167
1168
1169} // End.
1170
1171?>
Note: See TracBrowser for help on using the repository browser.