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

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

updated tests to work. updated email validation regex to include quote marks around name part. changed logmsg tmp dir name to use script_filename.

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 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        // Create tmp files so that we don't email and sms redundantly.
558        $site_hash = md5($_SERVER['SCRIPT_NAME']);
559        $temp_dir = $this->getParam('tmp_dir') . "/codebase_msgs_$site_hash/";
560        $temp_file = $temp_dir . $msg_id;
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                unlink($temp_file);
574            }
575        } else {
576            touch($temp_file);
577        }
578       
579       
580        // Data to be stored for a log event.
581        $event = array(
582            'date'      => date('Y-m-d H:i:s'),
583            'remote ip' => getRemoteAddr(),
584            'pid'       => (mb_substr(PHP_OS, 0, 3) != 'WIN' ? posix_getpid() : ''),
585            'type'      => $this->logPriorityToString($priority),
586            'file:line' => "$file : $line",
587            'url'       => mb_substr(isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '', 0, 128),
588            'message'   => $message
589        );
590
591        // FILE ACTION
592        if ($this->getParam('log_file_priority') && $priority <= $this->getParam('log_file_priority')) {
593            $event_str = '[' . join('] [', $event) . ']';
594            error_log(mb_substr($event_str, 0, 1024) . "\n", 3, $this->getParam('log_directory') . '/' . $this->getParam('log_filename'));
595        }
596
597        // NOTIFY SOMEONE
598        if ($send_notifications) {
599            // EMAIL ACTION
600            if ($this->getParam('log_email_priority') && $priority <= $this->getParam('log_email_priority')) {
601                $subject = sprintf('[%s %s] %s', getenv('HTTP_HOST'), $event['type'], $message);
602                $email_msg = sprintf("A %s log event occured on %s\n\n", $event['type'], getenv('HTTP_HOST'));
603                $headers = "From: codebase@strangecode.com";
604                foreach ($event as $k=>$v) {
605                    $email_msg .= sprintf("%-11s%s\n", $k, $v);
606                }
607                mb_send_mail($this->getParam('log_to_email_address'), $subject, $email_msg, $headers);
608            }
609   
610            // SMS ACTION
611            if ($this->getParam('log_sms_priority') && $priority <= $this->getParam('log_sms_priority')) {
612                $subject = sprintf('[%s %s]', getenv('HTTP_HOST'), $priority);
613                $sms_msg = sprintf('%s [%s:%s]', mb_substr($event['message'], 0, 64), basename($file), $line);
614                $headers = "From: codebase@strangecode.com";
615                mb_send_mail($this->getParam('log_to_sms_address'), $subject, $sms_msg, $headers);
616            }
617        }
618   
619        // SCREEN ACTION
620        if ($this->getParam('log_screen_priority') && $priority <= $this->getParam('log_screen_priority')) {
621            echo "[{$event['type']}] [{$event['message']}]\n";
622        }
623
624        // Restore original locale.
625        setlocale(LC_TIME, $locale);
626    }
627
628    /**
629     * Returns the string representation of a LOG_* integer constant.
630     *
631     * @param int  $priority  The LOG_* integer constant.
632     *
633     * @return                The string representation of $priority.
634     */
635    function logPriorityToString($priority) {
636        $priorities = array(
637            LOG_EMERG   => 'emergency',
638            LOG_ALERT   => 'alert',
639            LOG_CRIT    => 'critical',
640            LOG_ERR     => 'error',
641            LOG_WARNING => 'warning',
642            LOG_NOTICE  => 'notice',
643            LOG_INFO    => 'info',
644            LOG_DEBUG   => 'debug'
645        );
646        if (isset($priorities[$priority])) {
647            return $priorities[$priority];
648        } else {
649            return false;
650        }
651    }
652
653    /**
654     * Forcefully set a query argument even if one currently exists in the request.
655     * Values in the _carry_queries array will be copied to URLs (via $app->url()) and
656     * to hidden input values (via printHiddenSession()).
657     *
658     * @access  public
659     * @param   mixed   $query_key  The key (or keys, as an array) of the query argument to save.
660     * @param   mixed   $val        The new value of the argument key.
661     * @author  Quinn Comendant <quinn@strangecode.com>
662     * @since   13 Oct 2007 11:34:51
663     */
664    function setQuery($query_key, $val)
665    {
666        if (!is_array($query_key)) {
667            $query_key = array($query_key);
668        }
669        foreach ($query_key as $k) {
670            // Set the value of the specified query argument into the _carry_queries array.
671            $this->_carry_queries[$k] = $val;
672        }
673    }
674
675    /**
676     * Specify which query arguments will be carried persistently between requests.
677     * Values in the _carry_queries array will be copied to URLs (via $app->url()) and
678     * to hidden input values (via printHiddenSession()).
679     *
680     * @access  public
681     * @param   mixed   $query_key   The key (or keys, as an array) of the query argument to save.
682     * @param   mixed   $default    If the key is not available, set to this default value.
683     * @author  Quinn Comendant <quinn@strangecode.com>
684     * @since   14 Nov 2005 19:24:52
685     */
686    function carryQuery($query_key, $default=false)
687    {
688        if (!is_array($query_key)) {
689            $query_key = array($query_key);
690        }
691        foreach ($query_key as $k) {
692            // If not already set, and there is a non-empty value provided in the request...
693            if (!isset($this->_carry_queries[$k]) && false !== getFormData($k, $default)) {
694                // Copy the value of the specified query argument into the _carry_queries array.
695                $this->_carry_queries[$k] = getFormData($k, $default);
696                $this->logMsg(sprintf('Carrying query: %s => %s', $k, truncate(getDump($this->_carry_queries[$k], true), 128, 'end')), LOG_DEBUG, __FILE__, __LINE__);
697            }
698        }
699    }
700
701    /**
702     * dropQuery() is the opposite of carryQuery(). The specified value will not appear in
703     * url()/ohref()/printHiddenSession() modified URLs unless explicitly written in.
704     *
705     * @access  public
706     * @param   mixed   $query_key  The key (or keys, as an array) of the query argument to remove.
707     * @author  Quinn Comendant <quinn@strangecode.com>
708     * @since   18 Jun 2007 20:57:29
709     */
710    function dropQuery($query_key, $unset=false)
711    {
712        if (!is_array($query_key)) {
713            $query_key = array($query_key);
714        }
715        foreach ($query_key as $k) {
716            if (isset($this->_carry_queries[$k])) {
717                // Remove the value of the specified query argument from the _carry_queries array.
718                $this->logMsg(sprintf('Dropping carried query: %s => %s', $k, $this->_carry_queries[$k]), LOG_DEBUG, __FILE__, __LINE__);
719                unset($this->_carry_queries[$k]);
720            }
721            if ($unset && isset($_REQUEST[$k])) {
722                unset($_REQUEST[$k], $_GET[$k], $_POST[$k], $_COOKIE[$k]);
723            }
724        }
725    }
726
727    /**
728     * Outputs a fully qualified URL with a query of all the used (ie: not empty)
729     * keys and values, including optional queries. This allows mindless retention
730     * of query arguments across page requests. If cookies are not
731     * used, the session id will be propagated in the URL.
732     *
733     * @param  string $url              The initial url
734     * @param  mixed  $carry_args       Additional url arguments to carry in the query,
735     *                                  or FALSE to prevent carrying queries. Can be any of the following formats:
736     *                                      array('key1', key2', key3')  <-- to save these keys if in the form data.
737     *                                      array('key1'=>'value', key2'='value')  <-- to set keys to default values if not present in form data.
738     *                                      false  <-- To not carry any queries. If URL already has queries those will be retained.
739     *
740     * @param  mixed  $always_include_sid  Always add the session id, even if using_trans_sid = true. This is required when
741     *                                     URL starts with http, since PHP using_trans_sid doesn't do those and also for
742     *                                     header('Location...') redirections.
743     *
744     * @return string url with attached queries and, if not using cookies, the session id
745     */
746    function url($url, $carry_args=null, $always_include_sid=false)
747    {
748        if (!$this->running) {
749            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
750            return false;
751        }
752
753        // Get any provided query arguments to include in the final URL.
754        // If FALSE is a provided here, DO NOT carry the queries.
755        $do_carry_queries = true;
756        $one_time_carry_queries = array();
757        if (!is_null($carry_args)) {
758            if (is_array($carry_args) && !empty($carry_args)) {
759                foreach ($carry_args as $key=>$arg) {
760                    // Get query from appropriate source.
761                    if (false === $arg) {
762                        $do_carry_queries = false;
763                    } else if (false !== getFormData($arg, false)) {
764                        $one_time_carry_queries[$arg] = getFormData($arg); // Set arg to form data if available.
765                    } else if (!is_numeric($key) && '' != $arg) {
766                        $one_time_carry_queries[$key] = getFormData($key, $arg); // Set to arg to default if specified (overwritten by form data).
767                    }
768                }
769            } else if (false !== getFormData($carry_args, false)) {
770                $one_time_carry_queries[$carry_args] = getFormData($carry_args);
771            } else if (false === $carry_args) {
772                $do_carry_queries = false;
773            }
774        }
775
776        // Get the first delimiter that is needed in the url.
777        $delim = mb_strpos($url, '?') !== false ? ini_get('arg_separator.output') : '?';
778
779        $q = '';
780        if ($do_carry_queries) {
781            // Join the global _carry_queries and local one_time_carry_queries.
782            $query_args = urlEncodeArray(array_merge($this->_carry_queries, $one_time_carry_queries));
783            foreach ($query_args as $key=>$val) {
784                // Check value is set and value does not already exist in the url.
785                if (!preg_match('/[?&]' . preg_quote($key) . '=/', $url)) {
786                    $q .= $delim . $key . '=' . $val;
787                    $delim = ini_get('arg_separator.output');
788                }
789            }
790        }
791
792        // Include the necessary SID if the following is true:
793        // - no cookie in http request OR cookies disabled in App
794        // - sessions are enabled
795        // - the link stays on our site
796        // - transparent SID propagation with session.use_trans_sid is not being used OR url begins with protocol (using_trans_sid has no effect here)
797        // OR
798        // - 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)
799        // AND
800        // - the SID is not already in the query.
801        if (
802            (
803                (
804                    (
805                        !isset($_COOKIE[session_name()])
806                        || !$this->getParam('session_use_cookies')
807                    )
808                    && $this->getParam('session_use_trans_sid')
809                    && $this->getParam('enable_session')
810                    && isMyDomain($url)
811                    && (
812                        !ini_get('session.use_trans_sid')
813                        || preg_match('!^(http|https)://!i', $url)
814                    )
815                )
816                || $always_include_sid
817            )
818            && !preg_match('/[?&]' . preg_quote(session_name()) . '=/', $url)
819        ) {
820            $url .= $q . $delim . session_name() . '=' . session_id();
821            return $url;
822        } else {
823            $url .= $q;
824            return $url;
825        }
826    }
827
828    /**
829     * Returns a HTML-friendly URL processed with $app->url and & replaced with &amp;
830     *
831     * @access  public
832     * @param   string  $url    Input URL to parse.
833     * @return  string          URL passed through $app->url() and then & turned to $amp;.
834     * @author  Quinn Comendant <quinn@strangecode.com>
835     * @since   09 Dec 2005 17:58:45
836     */
837    function oHREF($url, $carry_args=null, $always_include_sid=false)
838    {
839        $url = $this->url($url, $carry_args, $always_include_sid);
840
841        // Replace any & not followed by an html or unicode entity with it's &amp; equivalent.
842        $url = preg_replace('/&(?![\w\d#]{1,10};)/', '&amp;', $url);
843
844        return $url;
845    }
846
847    /**
848     * Prints a hidden form element with the PHPSESSID when cookies are not used, as well
849     * as hidden form elements for GET_VARS that might be in use.
850     *
851     * @param  mixed  $carry_args        Additional url arguments to carry in the query,
852     *                                   or FALSE to prevent carrying queries. Can be any of the following formats:
853     *                                      array('key1', key2', key3')  <-- to save these keys if in the form data.
854     *                                      array('key1'=>'value', key2'='value')  <-- to set keys to default values if not present in form data.
855     *                                      false  <-- To not carry any queries. If URL already has queries those will be retained.
856     */
857    function printHiddenSession($carry_args=null)
858    {
859        if (!$this->running) {
860            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
861            return false;
862        }
863
864        // Get any provided query arguments to include in the final hidden form data.
865        // If FALSE is a provided here, DO NOT carry the queries.
866        $do_carry_queries = true;
867        $one_time_carry_queries = array();
868        if (!is_null($carry_args)) {
869            if (is_array($carry_args) && !empty($carry_args)) {
870                foreach ($carry_args as $key=>$arg) {
871                    // Get query from appropriate source.
872                    if (false === $arg) {
873                        $do_carry_queries = false;
874                    } else if (false !== getFormData($arg, false)) {
875                        $one_time_carry_queries[$arg] = getFormData($arg); // Set arg to form data if available.
876                    } else if (!is_numeric($key) && '' != $arg) {
877                        $one_time_carry_queries[$key] = getFormData($key, $arg); // Set to arg to default if specified (overwritten by form data).
878                    }
879                }
880            } else if (false !== getFormData($carry_args, false)) {
881                $one_time_carry_queries[$carry_args] = getFormData($carry_args);
882            } else if (false === $carry_args) {
883                $do_carry_queries = false;
884            }
885        }
886
887        // For each existing request value, we create a hidden input to carry it through a form.
888        if ($do_carry_queries) {
889            // Join the global _carry_queries and local one_time_carry_queries.
890            // urlencode is not used here, not for form data!
891            $query_args = array_merge($this->_carry_queries, $one_time_carry_queries);
892            foreach ($query_args as $key=>$val) {
893                printf('<input type="hidden" name="%s" value="%s" />', $key, $val);
894            }
895        }
896
897        // Include the SID if cookies are disabled.
898        if (!isset($_COOKIE[session_name()]) && !ini_get('session.use_trans_sid')) {
899            printf('<input type="hidden" name="%s" value="%s" />', session_name(), session_id());
900        }
901    }
902
903    /**
904     * Uses an http header to redirect the client to the given $url. If sessions are not used
905     * and the session is not already defined in the given $url, the SID is appended as a URI query.
906     * As with all header generating functions, make sure this is called before any other output.
907     *
908     * @param   string  $url                    The URL the client will be redirected to.
909     * @param   mixed   $carry_args             Additional url arguments to carry in the query,
910     *                                          or FALSE to prevent carrying queries. Can be any of the following formats:
911     *                                          -array('key1', key2', key3')  <-- to save these keys if in the form data.
912     *                                          -array('key1' => 'value', key2' => 'value')  <-- to set keys to default values if not present in form data.
913     *                                          -false  <-- To not carry any queries. If URL already has queries those will be retained.
914     * @param   bool    $always_include_sid     Force session id to be added to Location header.
915     */
916    function dieURL($url, $carry_args=null, $always_include_sid=false)
917    {
918        if (!$this->running) {
919            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
920            return false;
921        }
922
923        if ('' == $url) {
924            // If URL is not specified, use the redirect_home_url.
925            $url = $this->getParam('redirect_home_url');
926        }
927
928        if (preg_match('!^/!', $url)) {
929            // If relative URL is given, prepend correct local hostname.
930            $scheme = 'on' == getenv('HTTPS') ? 'https' : 'http';
931            $host = getenv('HTTP_HOST');
932            $url = sprintf('%s://%s%s', $scheme, $host, $url);
933        }
934
935        $url = $this->url($url, $carry_args, $always_include_sid);
936
937        // Should we send a "303 See Other" header here instead of relying on the 302 sent automatically by PHP?
938        header(sprintf('Location: %s', $url));
939        $this->logMsg(sprintf('dieURL: %s', $url), LOG_DEBUG, __FILE__, __LINE__);
940
941        // End application.
942        // Recommended, although I'm not sure it's necessary: http://cn2.php.net/session_write_close
943        $this->stop();
944        die;
945    }
946
947    /*
948    * Redirects a user by calling $app->dieURL(). It will use:
949    * 1. the stored boomerang URL, it it exists
950    * 2. a specified $default_url, it it exists
951    * 3. the referring URL, it it exists.
952    * 4. redirect_home_url configuration variable.
953    *
954    * @access   public
955    * @param    string  $id             Identifier for this script.
956    * @param    mixed   $carry_args     Additional arguments to carry in the URL automatically (see $app->oHREF()).
957    * @param    string  $default_url    A default URL if there is not a valid specified boomerang URL.
958    * @param    bool    $queryless_referrer_comparison   Exclude the URL query from the refererIsMe() comparison.
959    * @return   bool                    False if the session is not running. No return otherwise.
960    * @author   Quinn Comendant <quinn@strangecode.com>
961    * @since    31 Mar 2006 19:17:00
962    */
963    function dieBoomerangURL($id=null, $carry_args=null, $default_url=null, $queryless_referrer_comparison=false)
964    {
965        if (!$this->running) {
966            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
967            return false;
968        }
969
970        // Get URL from stored boomerang. Allow non specific URL if ID not valid.
971        if ($this->validBoomerangURL($id, true)) {
972            if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
973                $url = $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id];
974                $this->logMsg(sprintf('dieBoomerangURL(%s) found: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
975            } else {
976                $url = end($_SESSION['_app'][$this->_ns]['boomerang']['url']);
977                $this->logMsg(sprintf('dieBoomerangURL(%s) using: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
978            }
979            // Delete stored boomerang.
980            $this->deleteBoomerangURL($id);
981        } else if (isset($default_url)) {
982            $url = $default_url;
983        } else if (!refererIsMe(true === $queryless_referrer_comparison)) {
984            // Ensure that the redirecting page is not also the referrer.
985            $url = getenv('HTTP_REFERER');
986            $this->logMsg(sprintf('dieBoomerangURL(%s) using referrer: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
987        } else {
988            // If URL is not specified, use the redirect_home_url.
989            $url = $this->getParam('redirect_home_url');
990            $this->logMsg(sprintf('dieBoomerangURL(%s) using redirect_home_url: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
991        }
992
993        // A redirection will never happen immediately twice.
994        // Set the time so ensure this doesn't happen.
995        $_SESSION['_app'][$this->_ns]['boomerang']['time'] = time();
996        $this->dieURL($url, $carry_args);
997    }
998
999    /**
1000     * Set the URL to return to when $app->dieBoomerangURL() is called.
1001     *
1002     * @param string  $url  A fully validated URL.
1003     * @param bool  $id     An identification tag for this url.
1004     * FIXME: url garbage collection?
1005     */
1006    function setBoomerangURL($url=null, $id=null)
1007    {
1008        if (!$this->running) {
1009            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1010            return false;
1011        }
1012        // A redirection will never happen immediately after setting the boomerangURL.
1013        // Set the time so ensure this doesn't happen. See $app->validBoomerangURL for more.
1014
1015        if ('' != $url && is_string($url)) {
1016            // Delete any boomerang request keys in the query string (along with any trailing delimiters after the deletion).
1017            $url = preg_replace(array('/([&?])boomerang=\w+[&?]?/', '/[&?]$/'), array('$1', ''), $url);
1018
1019            if (isset($_SESSION['_app'][$this->_ns]['boomerang']['url']) && is_array($_SESSION['_app'][$this->_ns]['boomerang']['url']) && !empty($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1020                // If the URL currently exists in the boomerang array, delete.
1021                while ($existing_key = array_search($url, $_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1022                    unset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$existing_key]);
1023                }
1024            }
1025
1026            if (isset($id)) {
1027                $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id] = $url;
1028            } else {
1029                $_SESSION['_app'][$this->_ns]['boomerang']['url'][] = $url;
1030            }
1031            $this->logMsg(sprintf('setBoomerangURL(%s): %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1032            return true;
1033        } else {
1034            $this->logMsg(sprintf('setBoomerangURL(%s) is empty!', $id, $url), LOG_NOTICE, __FILE__, __LINE__);
1035            return false;
1036        }
1037    }
1038
1039    /**
1040     * Return the URL set for the specified $id, or an empty string if one isn't set.
1041     *
1042     * @param string  $id     An identification tag for this url.
1043     */
1044    function getBoomerangURL($id=null)
1045    {
1046        if (!$this->running) {
1047            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1048            return false;
1049        }
1050
1051        if (isset($id)) {
1052            if (isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1053                return $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id];
1054            } else {
1055                return '';
1056            }
1057        } else if (is_array($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1058            return end($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1059        } else {
1060            return false;
1061        }
1062    }
1063
1064    /**
1065     * Delete the URL set for the specified $id.
1066     *
1067     * @param string  $id     An identification tag for this url.
1068     */
1069    function deleteBoomerangURL($id=null)
1070    {
1071        if (!$this->running) {
1072            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1073            return false;
1074        }
1075
1076        $this->logMsg(sprintf('deleteBoomerangURL(%s): %s', $id, $this->getBoomerangURL($id)), LOG_DEBUG, __FILE__, __LINE__);
1077
1078        if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1079            unset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id]);
1080        } else if (is_array($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1081            array_pop($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1082        }
1083    }
1084
1085    /**
1086     * Check if a valid boomerang URL value has been set. A boomerang URL is considered
1087     * valid if: 1) it is not empty, 2) it is not the current URL, and 3) has not been accessed within n seconds.
1088     *
1089     * @return bool  True if it is set and valid, false otherwise.
1090     */
1091    function validBoomerangURL($id=null, $use_nonspecificboomerang=false)
1092    {
1093        if (!$this->running) {
1094            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1095            return false;
1096        }
1097
1098        if (!isset($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1099            $this->logMsg(sprintf('validBoomerangURL(%s) no boomerang URL set.', $id), LOG_DEBUG, __FILE__, __LINE__);
1100            return false;
1101        }
1102
1103        // Time is the time stamp of a boomerangURL redirection, or setting of a boomerangURL.
1104        // a boomerang redirection will always occur at least several seconds after the last boomerang redirect
1105        // or a boomerang being set.
1106        $boomerang_time = isset($_SESSION['_app'][$this->_ns]['boomerang']['time']) ? $_SESSION['_app'][$this->_ns]['boomerang']['time'] : 0;
1107
1108        $url = '';
1109        if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1110            $url = $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id];
1111        } else if (!isset($id) || $use_nonspecificboomerang) {
1112            // Use non specific boomerang if available.
1113            $url = end($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1114        }
1115
1116        $this->logMsg(sprintf('validBoomerangURL(%s) testing: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1117
1118        if ('' == $url) {
1119            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, empty!', $id), LOG_DEBUG, __FILE__, __LINE__);
1120            return false;
1121        }
1122        if ($url == absoluteMe()) {
1123            // The URL we are directing to is the current page.
1124            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, same as absoluteMe: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1125            return false;
1126        }
1127        if ($boomerang_time >= (time() - 2)) {
1128            // Last boomerang direction was less than 2 seconds ago.
1129            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, boomerang_time too short: %s seconds', $id, time() - $boomerang_time), LOG_DEBUG, __FILE__, __LINE__);
1130            return false;
1131        }
1132
1133        $this->logMsg(sprintf('validBoomerangURL(%s) is valid: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1134        return true;
1135    }
1136
1137    /**
1138     * Force the user to connect via https (port 443) by redirecting them to
1139     * the same page but with https.
1140     */
1141    function sslOn()
1142    {
1143        if (function_exists('apache_get_modules')) {
1144            $modules = apache_get_modules();
1145        } else {
1146            // It's safe to assume we have mod_ssl if we can't determine otherwise.
1147            $modules = array('mod_ssl');
1148        }
1149
1150        if ('' == getenv('HTTPS') && $this->getParam('ssl_enabled') && in_array('mod_ssl', $modules)) {
1151            $this->raiseMsg(sprintf(_("Secure SSL connection made to %s"), $this->getParam('ssl_domain')), MSG_NOTICE, __FILE__, __LINE__);
1152            // Always append session because some browsers do not send cookie when crossing to SSL URL.
1153            $this->dieURL('https://' . $this->getParam('ssl_domain') . getenv('REQUEST_URI'), null, true);
1154        }
1155    }
1156
1157
1158    /**
1159     * to enforce the user to connect via http (port 80) by redirecting them to
1160     * a http version of the current url.
1161     */
1162    function sslOff()
1163    {
1164        if ('' != getenv('HTTPS')) {
1165            $this->dieURL('http://' . getenv('HTTP_HOST') . getenv('REQUEST_URI'), null, true);
1166        }
1167    }
1168
1169
1170} // End.
1171
1172?>
Note: See TracBrowser for help on using the repository browser.