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

Last change on this file since 359 was 359, checked in by quinn, 15 years ago

Modified logMsg throttle so as not to create thousands of lock files.

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