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

Last change on this file since 500 was 500, checked in by anonymous, 10 years ago

Many auth and crypto changes; various other bugfixes while working on pulso.

File size: 65.7 KB
Line 
1<?php
2/**
3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
5 * Copyright 2001-2012 Strangecode, LLC
6 *
7 * This file is part of The Strangecode Codebase.
8 *
9 * The Strangecode Codebase is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
13 *
14 * The Strangecode Codebase is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23/**
24 * App.inc.php
25 *
26 * Primary application framework class.
27 *
28 * @author  Quinn Comendant <quinn@strangecode.com>
29 * @version 2.1
30 */
31
32// Message Types.
33define('MSG_ERR', 1);
34define('MSG_ERROR', MSG_ERR);
35define('MSG_WARNING', 2);
36define('MSG_NOTICE', 4);
37define('MSG_SUCCESS', 8);
38define('MSG_ALL', MSG_SUCCESS | MSG_NOTICE | MSG_WARNING | MSG_ERROR);
39
40require_once dirname(__FILE__) . '/Utilities.inc.php';
41
42class App {
43
44    // Minimum version of PHP required for this version of the Codebase.
45    const CODEBASE_MIN_PHP_VERSION = '5.3.0';
46
47    // A place to keep an object instance for the singleton pattern.
48    protected static $instance = null;
49
50    // Namespace of this application instance.
51    protected $_ns;
52
53    // If $app->start has run successfully.
54    public $running = false;
55
56    // Instance of database object.
57    public $db;
58
59    // Array of query arguments will be carried persistently between requests.
60    protected $_carry_queries = array();
61
62    // Dictionary of global application parameters.
63    protected $_params = array();
64
65    // Default parameters.
66    protected $_param_defaults = array(
67
68        // Public name and email address for this application.
69        'site_name' => null,
70        'site_email' => '', // Set to no-reply@HTTP_HOST if not set here.
71        'site_url' => '', // URL automatically determined by _SERVER['HTTP_HOST'] if not set here.
72        'images_path' => '', // Location for codebase-generated interface widgets (ex: "/admin/i").
73        'site_version_file' => 'docs/version.txt', // File containing version number of this app, relative to the include path.
74
75        // The location the user will go if the system doesn't know where else to send them.
76        'redirect_home_url' => '/',
77
78        // SSL URL used when redirecting with $app->sslOn().
79        'ssl_domain' => null,
80        'ssl_enabled' => false,
81
82        // Use CSRF tokens.
83        'csrf_token_enabled' => true,
84        'csrf_token_name' => 'csrf_token',
85        'csrf_token_timeout' => 86400, // In seconds. This causes form tokens to be unusable after this duration. This might only cause problems when opening forms in multiple tabs left open beyond the timeout duration. But usually their session will timeout first, and they'll receive new tokens when they load the form again..
86
87        // HMAC signing method
88        'signing_method' => 'sha512+base64',
89
90        // Character set for page output. Used in the Content-Type header and the HTML <meta content-type> tag.
91        'character_set' => 'utf-8',
92
93        // Human-readable format used to display dates.
94        'date_format' => 'd M Y',
95        'time_format' => 'h:i:s A',
96        'sql_date_format' => '%e %b %Y',
97        'sql_time_format' => '%k:%i',
98
99        // Use php sessions?
100        'enable_session' => false,
101        'session_name' => '_session',
102        'session_use_cookies' => true,
103
104        // Pass the session-id through URLs if cookies are not enabled?
105        // Disable this to prevent session ID theft.
106        'session_use_trans_sid' => false,
107
108        // Use database?
109        'enable_db' => false,
110
111        // Use db-based sessions?
112        'enable_db_session_handler' => false,
113
114        // DB passwords should be set as apache environment variables in httpd.conf, readable only by root.
115        'db_server' => 'localhost',
116        'db_name' => null,
117        'db_user' => null,
118        'db_pass' => null,
119
120        // Database debugging.
121        'db_always_debug' => false, // TRUE = display all SQL queries.
122        'db_debug' => false, // TRUE = display db errors.
123        'db_die_on_failure' => false, // TRUE = script stops on db error.
124
125        // For classes that require db tables, do we check that a table exists and create if missing?
126        'db_create_tables' => true,
127
128        // The level of error reporting. Don't change this to suppress messages, instead use display_errors to control display.
129        'error_reporting' => E_ALL,
130
131        // Don't display errors by default; it is preferable to log them to a file.
132        'display_errors' => false,
133
134        // Directory in which to store log files.
135        'log_directory' => '',
136
137        // PHP error log.
138        'php_error_log' => 'php_error_log',
139
140        // General application log.
141        'log_filename' => 'app_log',
142
143        // Don't email or SMS duplicate messages that happen more often than this value (in seconds).
144        'log_multiple_timeout' => 3600, // Hourly
145
146        // Logging priority can be any of the following, or false to deactivate:
147        // LOG_EMERG     system is unusable
148        // LOG_ALERT     action must be taken immediately
149        // LOG_CRIT      critical conditions
150        // LOG_ERR       error conditions
151        // LOG_WARNING   warning conditions
152        // LOG_NOTICE    normal, but significant, condition
153        // LOG_INFO      informational message
154        // LOG_DEBUG     debug-level message
155        'log_file_priority' => LOG_INFO,
156        'log_email_priority' => false,
157        'log_sms_priority' => false,
158        'log_screen_priority' => false,
159
160        // Email address to receive log event emails. Use multiple addresses by separating them with commas.
161        'log_to_email_address' => null,
162
163        // SMS Email address to receive log event SMS messages. Use multiple addresses by separating them with commas.
164        'log_to_sms_address' => null,
165
166        // Should we avoid logging repeated logMsg() events? You might want to set this false if you need to see more accurate logging, particularly for long-running scripts.
167        'log_ignore_repeated_events' => true,
168
169        // Temporary files directory.
170        'tmp_dir' => '/tmp',
171
172        // A key for calculating simple cryptographic signatures. Set using as an environment variables in the httpd.conf with 'SetEnv SIGNING_KEY <key>'.
173        // Existing password hashes rely on the same key/salt being used to compare encryptions.
174        // Don't change this unless you know existing hashes or signatures will not be affected!
175        'signing_key' => 'aae6abd6209d82a691a9f96384a7634a',
176    );
177
178    /**
179     * Constructor.
180     */
181    public function __construct($namespace='')
182    {
183        // Set namespace of application instance.
184        $this->_ns = $namespace;
185
186        // Initialize default parameters.
187        $this->_params = array_merge($this->_params, $this->_param_defaults);
188
189        // Begin timing script.
190        require_once dirname(__FILE__) . '/ScriptTimer.inc.php';
191        $this->timer = new ScriptTimer();
192        $this->timer->start('_app');
193    }
194
195    /**
196     * This method enforces the singleton pattern for this class. Only one application is running at a time.
197     *
198     * $param   string  $namespace  Name of this application.
199     * @return  object  Reference to the global Cache object.
200     * @access  public
201     * @static
202     */
203    public static function &getInstance($namespace='')
204    {
205        if (self::$instance === null) {
206            // TODO: Yep, having a namespace with one singletone instance is not very useful.
207            self::$instance = new self($namespace);
208        }
209
210        return self::$instance;
211    }
212
213    /**
214     * Set (or overwrite existing) parameters by passing an array of new parameters.
215     *
216     * @access public
217     * @param  array    $param     Array of parameters (key => val pairs).
218     */
219    public function setParam($param=null)
220    {
221        if (isset($param) && is_array($param)) {
222            // Merge new parameters with old overriding only those passed.
223            $this->_params = array_merge($this->_params, $param);
224        }
225    }
226
227    /**
228     * Return the value of a parameter.
229     *
230     * @access  public
231     * @param   string  $param      The key of the parameter to return.
232     * @return  mixed               Parameter value, or null if not existing.
233     */
234    public function getParam($param=null)
235    {
236        if ($param === null) {
237            return $this->_params;
238        } else if (array_key_exists($param, $this->_params)) {
239            return $this->_params[$param];
240        } else {
241            return null;
242        }
243    }
244
245    /**
246     * Begin running this application.
247     *
248     * @access  public
249     * @author  Quinn Comendant <quinn@strangecode.com>
250     * @since   15 Jul 2005 00:32:21
251     */
252    public function start()
253    {
254        if ($this->running) {
255            return false;
256        }
257
258        // Error reporting.
259        ini_set('error_reporting', $this->getParam('error_reporting'));
260        ini_set('display_errors', $this->getParam('display_errors'));
261        ini_set('log_errors', true);
262        if (is_dir($this->getParam('log_directory')) && is_writable($this->getParam('log_directory'))) {
263            ini_set('error_log', $this->getParam('log_directory') . '/' . $this->getParam('php_error_log'));
264        }
265
266        // Set character set to use for multi-byte string functions.
267        mb_internal_encoding($this->getParam('character_set'));
268        switch (mb_strtolower($this->getParam('character_set'))) {
269        case 'utf-8' :
270            mb_language('uni');
271            break;
272
273        case 'iso-2022-jp' :
274            mb_language('ja');
275            break;
276
277        case 'iso-8859-1' :
278        default :
279            mb_language('en');
280            break;
281        }
282
283        /**
284         * 1. Start Database.
285         */
286
287        if (true === $this->getParam('enable_db')) {
288
289            // DB connection parameters taken from environment variables in the httpd.conf file, readable only by root.
290            if (!empty($_SERVER['DB_SERVER']) && !$this->getParam('db_server')) {
291                $this->setParam(array('db_server' => $_SERVER['DB_SERVER']));
292            }
293            if (!empty($_SERVER['DB_NAME']) && !$this->getParam('db_name')) {
294                $this->setParam(array('db_name' => $_SERVER['DB_NAME']));
295            }
296            if (!empty($_SERVER['DB_USER']) && !$this->getParam('db_user')) {
297                $this->setParam(array('db_user' => $_SERVER['DB_USER']));
298            }
299            if (!empty($_SERVER['DB_PASS']) && !$this->getParam('db_pass')) {
300                $this->setParam(array('db_pass' => $_SERVER['DB_PASS']));
301            }
302
303            // There will ever only be one instance of the DB object, and here is where it is instantiated.
304            require_once dirname(__FILE__) . '/DB.inc.php';
305            $this->db =& DB::getInstance();
306            $this->db->setParam(array(
307                'db_server' => $this->getParam('db_server'),
308                'db_name' => $this->getParam('db_name'),
309                'db_user' => $this->getParam('db_user'),
310                'db_pass' => $this->getParam('db_pass'),
311                'db_always_debug' => $this->getParam('db_always_debug'),
312                'db_debug' => $this->getParam('db_debug'),
313                'db_die_on_failure' => $this->getParam('db_die_on_failure'),
314            ));
315
316            // Connect to database.
317            $this->db->connect();
318        }
319
320
321        /**
322         * 2. Start PHP session.
323         */
324
325        // Skip sessions if disabled or automatically skip if run in a CLI script.
326        if (true === $this->getParam('enable_session') && !defined('_CLI')) {
327
328            // Session parameters.
329            ini_set('session.gc_probability', 1);
330            ini_set('session.gc_divisor', 1000);
331            ini_set('session.gc_maxlifetime', 43200); // 12 hours
332            ini_set('session.use_cookies', $this->getParam('session_use_cookies'));
333            ini_set('session.use_trans_sid', false);
334            ini_set('session.entropy_file', '/dev/urandom');
335            ini_set('session.entropy_length', '512');
336            ini_set('session.cookie_httponly', true);
337            session_name($this->getParam('session_name'));
338
339            if (true === $this->getParam('enable_db_session_handler') && true === $this->getParam('enable_db')) {
340                // Database session handling.
341                require_once dirname(__FILE__) . '/DBSessionHandler.inc.php';
342                $db_save_handler = new DBSessionHandler($this->db, array(
343                    'db_table' => 'session_tbl',
344                    'create_table' => $this->getParam('db_create_tables'),
345                ));
346            }
347
348            // Start the session.
349            session_start();
350
351            if (!isset($_SESSION['_app'][$this->_ns])) {
352                // Access session data using: $_SESSION['...'].
353                // Initialize here _after_ session has started.
354                $_SESSION['_app'][$this->_ns] = array(
355                    'messages' => array(),
356                    'boomerang' => array('url'),
357                );
358            }
359        }
360
361
362        /**
363         * 3. Misc setup.
364         */
365
366        // Script URI will be something like http://host.name.tld (no ending slash)
367        // and is used whenever a URL need be used to the current site.
368        // Not available on cli scripts obviously.
369        if (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST'] && '' == $this->getParam('site_url')) {
370            $this->setParam(array('site_url' => sprintf('%s://%s', ('on' == getenv('HTTPS') ? 'https' : 'http'), getenv('HTTP_HOST'))));
371        }
372
373        // In case site_email isn't set, use something halfway presentable.
374        if (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST'] && '' == $this->getParam('site_email')) {
375            $this->setParam(array('site_email' => sprintf('no-reply@%s', getenv('HTTP_HOST'))));
376        }
377
378        // A key for calculating simple cryptographic signatures.
379        if (isset($_SERVER['SIGNING_KEY'])) {
380            $this->setParam(array('signing_key' => $_SERVER['SIGNING_KEY']));
381        }
382
383        // Character set. This should also be printed in the html header template.
384        if (!defined('_CLI')) {
385            header('Content-type: text/html; charset=' . $this->getParam('character_set'));
386        }
387
388        // Set the version of the codebase we're using.
389        $codebase_version_file = dirname(__FILE__) . '/../docs/version.txt';
390        $codebase_version = '';
391        if (is_readable($codebase_version_file) && !is_dir($codebase_version_file)) {
392            $codebase_version = trim(file_get_contents($codebase_version_file));
393            $this->setParam(array('codebase_version' => $codebase_version));
394            if (!defined('_CLI')) {
395                header('X-Codebase-Version: ' . $codebase_version);
396            }
397        }
398
399        if (version_compare(PHP_VERSION, self::CODEBASE_MIN_PHP_VERSION, '<')) {
400            $this->logMsg(sprintf('PHP %s required for Codebase %s, using %s; some things will break.', self::CODEBASE_MIN_PHP_VERSION, $codebase_version, PHP_VERSION), LOG_NOTICE, __FILE__, __LINE__);
401        }
402
403        // Set the application version if defined.
404        if (false !== stream_resolve_include_path($this->getParam('site_version_file'))) {
405            $site_version = trim(file_get_contents($this->getParam('site_version_file'), true));
406            $this->setParam(array('site_version' => $site_version));
407            if (!defined('_CLI')) {
408                header('X-Site-Version: ' . $site_version);
409            }
410        }
411
412        $this->running = true;
413    }
414
415    /**
416     * Stop running this application.
417     *
418     * @access  public
419     * @author  Quinn Comendant <quinn@strangecode.com>
420     * @since   17 Jul 2005 17:20:18
421     */
422    public function stop()
423    {
424        session_write_close();
425        $this->running = false;
426        $num_queries = 0;
427        if (true === $this->getParam('enable_db')) {
428            $num_queries = $this->db->numQueries();
429            $this->db->close();
430        }
431        $mem_current = memory_get_usage();
432        $mem_peak = memory_get_peak_usage();
433        $this->timer->stop('_app');
434        $this->logMsg(sprintf('Script ended gracefully. Execution time: %s. Number of db queries: %s. Memory usage: %s. Peak memory: %s.', $this->timer->getTime('_app'), $num_queries, $mem_current, $mem_peak), LOG_DEBUG, __FILE__, __LINE__);
435    }
436
437
438    /**
439     * Add a message to the session, which is printed in the header.
440     * Just a simple way to print messages to the user.
441     *
442     * @access public
443     *
444     * @param string $message The text description of the message.
445     * @param int    $type    The type of message: MSG_NOTICE,
446     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
447     * @param string $file    __FILE__.
448     * @param string $line    __LINE__.
449     */
450    public function raiseMsg($message, $type=MSG_NOTICE, $file=null, $line=null)
451    {
452        $message = trim($message);
453
454        if (!$this->running) {
455            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
456            return false;
457        }
458
459        if ('' == trim($message)) {
460            $this->logMsg(sprintf('Raised message is an empty string.', null), LOG_NOTICE, __FILE__, __LINE__);
461            return false;
462        }
463
464        // Avoid duplicate full-stops..
465        $message = trim(preg_replace('/\.{2}$/', '.', $message));
466
467        // Save message in session under unique key to avoid duplicate messages.
468        $msg_id = md5($type . $message);
469        if (!isset($_SESSION['_app'][$this->_ns]['messages'][$msg_id])) {
470            $_SESSION['_app'][$this->_ns]['messages'][$msg_id] = array(
471                'type'    => $type,
472                'message' => $message,
473                'file'    => $file,
474                'line'    => $line,
475                'count'   => (isset($_SESSION['_app'][$this->_ns]['messages'][$msg_id]['count']) ? (1 + $_SESSION['_app'][$this->_ns]['messages'][$msg_id]['count']) : 1)
476            );
477        }
478
479        if (!in_array($type, array(MSG_NOTICE, MSG_SUCCESS, MSG_WARNING, MSG_ERR))) {
480            $this->logMsg(sprintf('Invalid MSG_* type: %s', $type), LOG_NOTICE, __FILE__, __LINE__);
481        }
482    }
483
484    /**
485     * Returns an array of the raised messages.
486     *
487     * @access  public
488     * @return  array   List of messages in FIFO order.
489     * @author  Quinn Comendant <quinn@strangecode.com>
490     * @since   21 Dec 2005 13:09:20
491     */
492    public function getRaisedMessages()
493    {
494        if (!$this->running) {
495            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
496            return false;
497        }
498        return isset($_SESSION['_app'][$this->_ns]['messages']) ? $_SESSION['_app'][$this->_ns]['messages'] : array();
499    }
500
501    /**
502     * Resets the message list.
503     *
504     * @access  public
505     * @author  Quinn Comendant <quinn@strangecode.com>
506     * @since   21 Dec 2005 13:21:54
507     */
508    public function clearRaisedMessages()
509    {
510        if (!$this->running) {
511            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
512            return false;
513        }
514
515        $_SESSION['_app'][$this->_ns]['messages'] = array();
516    }
517
518    /**
519     * Prints the HTML for displaying raised messages.
520     *
521     * @param   string  $above    Additional message to print above error messages (e.g. "Oops!").
522     * @param   string  $below    Additional message to print below error messages (e.g. "Please fix and resubmit").
523     * @param   string  $print_gotohash_js  Print a line of javascript that scrolls the browser window down to view any error messages.
524     * @param   string  $hash     The #hashtag to scroll to.
525     * @access  public
526     * @author  Quinn Comendant <quinn@strangecode.com>
527     * @since   15 Jul 2005 01:39:14
528     */
529    public function printRaisedMessages($above='', $below='', $print_gotohash_js=false, $hash='sc-msg')
530    {
531
532        if (!$this->running) {
533            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
534            return false;
535        }
536
537        $messages = $this->getRaisedMessages();
538        if (!empty($messages)) {
539            ?><div id="sc-msg" class="sc-msg"><?php
540            if ('' != $above) {
541                ?><div class="sc-above"><?php echo oTxt($above); ?></div><?php
542            }
543            foreach ($messages as $m) {
544                if (error_reporting() > 0 && $this->getParam('display_errors') && isset($m['file']) && isset($m['line'])) {
545                    echo "\n<!-- [" . $m['file'] . ' : ' . $m['line'] . '] -->';
546                }
547                switch ($m['type']) {
548                case MSG_ERR:
549                    echo '<div data-alert class="sc-msg-error alert-box alert">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
550                    break;
551
552                case MSG_WARNING:
553                    echo '<div data-alert class="sc-msg-warning alert-box warning">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
554                    break;
555
556                case MSG_SUCCESS:
557                    echo '<div data-alert class="sc-msg-success alert-box success">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
558                    break;
559
560                case MSG_NOTICE:
561                default:
562                    echo '<div data-alert class="sc-msg-notice alert-box info">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
563                    break;
564                }
565            }
566            if ('' != $below) {
567                ?><div class="sc-below"><?php echo oTxt($below); ?></div><?php
568            }
569            ?></div><?php
570            if ($print_gotohash_js) {
571                ?>
572                <script type="text/javascript">
573                /* <![CDATA[ */
574                window.location.hash = '#<?php echo urlencode($hash); ?>';
575                /* ]]> */
576                </script>
577                <?php
578            }
579        }
580        $this->clearRaisedMessages();
581    }
582
583    /**
584     * Logs messages to defined channels: file, email, sms, and screen. Repeated messages are
585     * not repeated but printed once with count. Log events that match a sendable channel (email or SMS)
586     * are sent once per 'log_multiple_timeout' setting (to avoid a flood of error emails).
587     *
588     * @access public
589     * @param string $message   The text description of the message.
590     * @param int    $priority  The type of message priority (in descending order):
591     *                          LOG_EMERG     0 system is unusable
592     *                          LOG_ALERT     1 action must be taken immediately
593     *                          LOG_CRIT      2 critical conditions
594     *                          LOG_ERR       3 error conditions
595     *                          LOG_WARNING   4 warning conditions
596     *                          LOG_NOTICE    5 normal, but significant, condition
597     *                          LOG_INFO      6 informational message
598     *                          LOG_DEBUG     7 debug-level message
599     * @param string $file      The file where the log event occurs.
600     * @param string $line      The line of the file where the log event occurs.
601     */
602    public function logMsg($message, $priority=LOG_INFO, $file=null, $line=null)
603    {
604        static $previous_events = array();
605
606        // If priority is not specified, assume the worst.
607        if (!$this->logPriorityToString($priority)) {
608            $this->logMsg(sprintf('Log priority %s not defined. (Message: %s)', $priority, $message), LOG_EMERG, $file, $line);
609            $priority = LOG_EMERG;
610        }
611
612        // If log file is not specified, don't log to a file.
613        if (!$this->getParam('log_directory') || !$this->getParam('log_filename') || !is_dir($this->getParam('log_directory')) || !is_writable($this->getParam('log_directory'))) {
614            $this->setParam(array('log_file_priority' => false));
615            // We must use trigger_error to report this problem rather than calling $app->logMsg, which might lead to an infinite loop.
616            trigger_error(sprintf('Codebase error: log directory (%s) not found or writable.', $this->getParam('log_directory')), E_USER_NOTICE);
617        }
618
619        // Before we get any further, let's see if ANY log events are configured to be reported.
620        if ((false === $this->getParam('log_file_priority') || $priority > $this->getParam('log_file_priority'))
621        && (false === $this->getParam('log_email_priority') || $priority > $this->getParam('log_email_priority'))
622        && (false === $this->getParam('log_sms_priority') || $priority > $this->getParam('log_sms_priority'))
623        && (false === $this->getParam('log_screen_priority') || $priority > $this->getParam('log_screen_priority'))) {
624            // This event would not be recorded, skip it entirely.
625            return false;
626        }
627
628        // Make sure to log in the system's locale.
629        $locale = setlocale(LC_TIME, 0);
630        setlocale(LC_TIME, 'C');
631
632        // Strip HTML tags except any with more than 7 characters because that's probably not a HTML tag, e.g. <email@address.com>.
633        preg_match_all('/(<[^>\s]{7,})[^>]*>/', $message, $strip_tags_allow);
634        $message = strip_tags(preg_replace('/\s+/', ' ', $message), (!empty($strip_tags_allow[1]) ? join('> ', $strip_tags_allow[1]) . '>' : null));
635
636        // Serialize multi-line messages.
637        $message = preg_replace('/\s+/m', ' ', $message);
638
639        // Store this event under a unique key, counting each time it occurs so that it only gets reported a limited number of times.
640        $msg_id = md5($message . $priority . $file . $line);
641        if ($this->getParam('log_ignore_repeated_events') && isset($previous_events[$msg_id])) {
642            $previous_events[$msg_id]++;
643            if ($previous_events[$msg_id] == 2) {
644                $this->logMsg(sprintf('%s (Event repeated %s or more times)', $message, $previous_events[$msg_id]), $priority, $file, $line);
645            }
646            return false;
647        } else {
648            $previous_events[$msg_id] = 1;
649        }
650
651        // For email and SMS notification types use "lock" files to prevent sending email and SMS notices ad infinitum.
652        if ((false !== $this->getParam('log_email_priority') && $priority <= $this->getParam('log_email_priority'))
653        || (false !== $this->getParam('log_sms_priority') && $priority <= $this->getParam('log_sms_priority'))) {
654            // This event will generate a "send" notification. Prepare lock file.
655            $site_hash = md5(empty($_SERVER['SERVER_NAME']) ? $_SERVER['SCRIPT_FILENAME'] : $_SERVER['SERVER_NAME']);
656            $lock_dir = $this->getParam('tmp_dir') . "/codebase_msgs_$site_hash/";
657            // Just use the file and line for the msg_id to limit the number of possible messages
658            // (the message string itself shan't be used as it may contain innumerable combinations).
659            $lock_file = $lock_dir . md5($file . ':' . $line);
660            if (!is_dir($lock_dir)) {
661                mkdir($lock_dir);
662            }
663            $send_notifications = true;
664            if (is_file($lock_file)) {
665                $msg_last_sent = filectime($lock_file);
666                // Has this message been sent more recently than the timeout?
667                if ((time() - $msg_last_sent) <= $this->getParam('log_multiple_timeout')) {
668                    // This message was already sent recently.
669                    $send_notifications = false;
670                } else {
671                    // Timeout has expired; send notifications again and reset timeout.
672                    touch($lock_file);
673                }
674            } else {
675                touch($lock_file);
676            }
677        }
678
679        // Data to be stored for a log event.
680        $event = array(
681            'date'      => date('Y-m-d H:i:s'),
682            'remote ip' => getRemoteAddr(),
683            'pid'       => getmypid(),
684            'type'      => $this->logPriorityToString($priority),
685            'file:line' => "$file : $line",
686            'url'       => mb_substr(isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '', 0, 1024),
687            'message'   => $message
688        );
689        // Here's a shortened version of event data.
690        $event_short = $event;
691        $event_short['url'] = truncate($event_short['url'], 120);
692
693
694        // FILE ACTION
695        if (false !== $this->getParam('log_file_priority') && $priority <= $this->getParam('log_file_priority')) {
696            $event_str = '[' . join('] [', $event_short) . ']';
697            error_log(mb_substr($event_str, 0, 1024) . "\n", 3, $this->getParam('log_directory') . '/' . $this->getParam('log_filename'));
698        }
699
700        // EMAIL ACTION
701        if (false !== $this->getParam('log_email_priority') && $priority <= $this->getParam('log_email_priority') && $send_notifications) {
702            $hostname = (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : php_uname('n');
703            $subject = sprintf('[%s %s] %s', $hostname, $event['type'], mb_substr($message, 0, 64));
704            $email_msg = sprintf("A %s log event occurred on %s\n\n", $event['type'], $hostname);
705            $headers = 'From: ' . $this->getParam('site_email');
706            foreach ($event as $k=>$v) {
707                $email_msg .= sprintf("%-11s%s\n", $k, $v);
708            }
709            mb_send_mail($this->getParam('log_to_email_address'), $subject, $email_msg, $headers);
710        }
711
712        // SMS ACTION
713        if (false !== $this->getParam('log_sms_priority') && $priority <= $this->getParam('log_sms_priority') && $send_notifications) {
714            $hostname = (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : php_uname('n');
715            $subject = sprintf('[%s %s]', $hostname, $priority);
716            $sms_msg = sprintf('%s [%s:%s]', mb_substr($event_short['message'], 0, 64), basename($file), $line);
717            $headers = 'From: ' . $this->getParam('site_email');
718            mb_send_mail($this->getParam('log_to_sms_address'), $subject, $sms_msg, $headers);
719        }
720
721        // SCREEN ACTION
722        if (false !== $this->getParam('log_screen_priority') && $priority <= $this->getParam('log_screen_priority')) {
723            file_put_contents('php://stderr', "[{$event['type']}] [{$event['message']}]\n", FILE_APPEND);
724        }
725
726        // Restore original locale.
727        setlocale(LC_TIME, $locale);
728
729        unset($event, $event_short);
730
731        return true;
732    }
733
734    /**
735     * Returns the string representation of a LOG_* integer constant.
736     *
737     * @param int  $priority  The LOG_* integer constant.
738     *
739     * @return                The string representation of $priority.
740     */
741    public function logPriorityToString($priority) {
742        $priorities = array(
743            LOG_EMERG   => 'emergency',
744            LOG_ALERT   => 'alert',
745            LOG_CRIT    => 'critical',
746            LOG_ERR     => 'error',
747            LOG_WARNING => 'warning',
748            LOG_NOTICE  => 'notice',
749            LOG_INFO    => 'info',
750            LOG_DEBUG   => 'debug'
751        );
752        if (isset($priorities[$priority])) {
753            return $priorities[$priority];
754        } else {
755            return false;
756        }
757    }
758
759    /**
760     * Forcefully set a query argument even if one currently exists in the request.
761     * Values in the _carry_queries array will be copied to URLs (via $app->url()) and
762     * to hidden input values (via printHiddenSession()).
763     *
764     * @access  public
765     * @param   mixed   $query_key  The key (or keys, as an array) of the query argument to save.
766     * @param   mixed   $val        The new value of the argument key.
767     * @author  Quinn Comendant <quinn@strangecode.com>
768     * @since   13 Oct 2007 11:34:51
769     */
770    public function setQuery($query_key, $val)
771    {
772        if (!is_array($query_key)) {
773            $query_key = array($query_key);
774        }
775        foreach ($query_key as $k) {
776            // Set the value of the specified query argument into the _carry_queries array.
777            $this->_carry_queries[$k] = $val;
778        }
779    }
780
781    /**
782     * Specify which query arguments will be carried persistently between requests.
783     * Values in the _carry_queries array will be copied to URLs (via $app->url()) and
784     * to hidden input values (via printHiddenSession()).
785     *
786     * @access  public
787     * @param   mixed   $query_key   The key (or keys, as an array) of the query argument to save.
788     * @param   mixed   $default    If the key is not available, set to this default value.
789     * @author  Quinn Comendant <quinn@strangecode.com>
790     * @since   14 Nov 2005 19:24:52
791     */
792    public function carryQuery($query_key, $default=false)
793    {
794        if (!is_array($query_key)) {
795            $query_key = array($query_key);
796        }
797        foreach ($query_key as $k) {
798            // If not already set, and there is a non-empty value provided in the request...
799            if (!isset($this->_carry_queries[$k]) && false !== getFormData($k, $default)) {
800                // Copy the value of the specified query argument into the _carry_queries array.
801                $this->_carry_queries[$k] = getFormData($k, $default);
802                $this->logMsg(sprintf('Carrying query: %s => %s', $k, truncate(getDump($this->_carry_queries[$k], true), 128, 'end')), LOG_DEBUG, __FILE__, __LINE__);
803            }
804        }
805    }
806
807    /**
808     * dropQuery() is the opposite of carryQuery(). The specified value will not appear in
809     * url()/ohref()/printHiddenSession() modified URLs unless explicitly written in.
810     *
811     * @access  public
812     * @param   mixed   $query_key  The key (or keys, as an array) of the query argument to remove.
813     * @param   bool    $unset      Remove any values set in the request matching the given $query_key.
814     * @author  Quinn Comendant <quinn@strangecode.com>
815     * @since   18 Jun 2007 20:57:29
816     */
817    public function dropQuery($query_key, $unset=false)
818    {
819        if (!is_array($query_key)) {
820            $query_key = array($query_key);
821        }
822        foreach ($query_key as $k) {
823            if (array_key_exists($k, $this->_carry_queries)) {
824                // Remove the value of the specified query argument from the _carry_queries array.
825                $this->logMsg(sprintf('Dropping carried query: %s => %s', $k, $this->_carry_queries[$k]), LOG_DEBUG, __FILE__, __LINE__);
826                unset($this->_carry_queries[$k]);
827            }
828            if ($unset && (isset($_REQUEST) && array_key_exists($k, $_REQUEST))) {
829                unset($_REQUEST[$k], $_GET[$k], $_POST[$k], $_COOKIE[$k]);
830            }
831        }
832    }
833
834    /**
835     * Outputs a fully qualified URL with a query of all the used (ie: not empty)
836     * keys and values, including optional queries. This allows mindless retention
837     * of query arguments across page requests. If cookies are not
838     * used, the session id will be propagated in the URL.
839     *
840     * @param  string $url              The initial url
841     * @param  mixed  $carry_args       Additional url arguments to carry in the query,
842     *                                  or FALSE to prevent carrying queries. Can be any of the following formats:
843     *                                      array('key1', key2', key3')  <-- to save these keys if in the form data.
844     *                                      array('key1'=>'value', key2'='value')  <-- to set keys to default values if not present in form data.
845     *                                      false  <-- To not carry any queries. If URL already has queries those will be retained.
846     *
847     * @param  mixed  $always_include_sid  Always add the session id, even if using_trans_sid = true. This is required when
848     *                                     URL starts with http, since PHP using_trans_sid doesn't do those and also for
849     *                                     header('Location...') redirections.
850     *
851     * @return string url with attached queries and, if not using cookies, the session id
852     */
853    public function url($url, $carry_args=null, $always_include_sid=false)
854    {
855        if (!$this->running) {
856            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
857            return false;
858        }
859
860        // Get any provided query arguments to include in the final URL.
861        // If FALSE is a provided here, DO NOT carry the queries.
862        $do_carry_queries = true;
863        $one_time_carry_queries = array();
864        if (!is_null($carry_args)) {
865            if (is_array($carry_args) && !empty($carry_args)) {
866                foreach ($carry_args as $key=>$arg) {
867                    // Get query from appropriate source.
868                    if (false === $arg) {
869                        $do_carry_queries = false;
870                    } else if (false !== getFormData($arg, false)) {
871                        $one_time_carry_queries[$arg] = getFormData($arg); // Set arg to form data if available.
872                    } else if (!is_numeric($key) && '' != $arg) {
873                        $one_time_carry_queries[$key] = getFormData($key, $arg); // Set to arg to default if specified (overwritten by form data).
874                    }
875                }
876            } else if (false !== getFormData($carry_args, false)) {
877                $one_time_carry_queries[$carry_args] = getFormData($carry_args);
878            } else if (false === $carry_args) {
879                $do_carry_queries = false;
880            }
881        }
882
883        // Get the first delimiter that is needed in the url.
884        $delim = mb_strpos($url, '?') !== false ? ini_get('arg_separator.output') : '?';
885
886        $q = '';
887        if ($do_carry_queries) {
888            // Join the global _carry_queries and local one_time_carry_queries.
889            $query_args = urlEncodeArray(array_merge($this->_carry_queries, $one_time_carry_queries));
890            foreach ($query_args as $key=>$val) {
891                // Check value is set and value does not already exist in the url.
892                if (!preg_match('/[?&]' . preg_quote($key) . '=/', $url)) {
893                    $q .= $delim . $key . '=' . $val;
894                    $delim = ini_get('arg_separator.output');
895                }
896            }
897        }
898
899        // Include the necessary SID if the following is true:
900        // - no cookie in http request OR cookies disabled in App
901        // - sessions are enabled
902        // - the link stays on our site
903        // - transparent SID propagation with session.use_trans_sid is not being used OR url begins with protocol (using_trans_sid has no effect here)
904        // OR
905        // - 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)
906        // AND
907        // - the SID is not already in the query.
908        if (
909            (
910                (
911                    (
912                        !isset($_COOKIE[session_name()])
913                        || !$this->getParam('session_use_cookies')
914                    )
915                    && $this->getParam('session_use_trans_sid')
916                    && $this->getParam('enable_session')
917                    && isMyDomain($url)
918                    && (
919                        !ini_get('session.use_trans_sid')
920                        || preg_match('!^(http|https)://!i', $url)
921                    )
922                )
923                || $always_include_sid
924            )
925            && !preg_match('/[?&]' . preg_quote(session_name()) . '=/', $url)
926        ) {
927            $url .= $q . $delim . session_name() . '=' . session_id();
928        } else {
929            $url .= $q;
930        }
931
932        return $url;
933    }
934
935    /**
936     * Returns a HTML-friendly URL processed with $app->url and & replaced with &amp;
937     *
938     * @access  public
939     * @param   string  $url    Input URL to parse.
940     * @return  string          URL passed through $app->url() and then & turned to $amp;.
941     * @author  Quinn Comendant <quinn@strangecode.com>
942     * @since   09 Dec 2005 17:58:45
943     */
944    public function oHREF($url, $carry_args=null, $always_include_sid=false)
945    {
946        $url = $this->url($url, $carry_args, $always_include_sid);
947
948        // Replace any & not followed by an html or unicode entity with it's &amp; equivalent.
949        $url = preg_replace('/&(?![\w\d#]{1,10};)/', '&amp;', $url);
950
951        return $url;
952    }
953
954    /**
955     * Prints a hidden form element with the PHPSESSID when cookies are not used, as well
956     * as hidden form elements for GET_VARS that might be in use.
957     *
958     * @param  mixed  $carry_args        Additional url arguments to carry in the query,
959     *                                   or FALSE to prevent carrying queries. Can be any of the following formats:
960     *                                      array('key1', key2', key3')  <-- to save these keys if in the form data.
961     *                                      array('key1'=>'value', key2'='value')  <-- to set keys to default values if not present in form data.
962     *                                      false  <-- To not carry any queries. If URL already has queries those will be retained.
963     * @param   bool    $include_csrf_token     Set to true to include the csrf_token in the form. Only use this for forms with action="post" to prevent the token from being revealed in the URL.
964     */
965    public function printHiddenSession($carry_args=null, $include_csrf_token=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 any provided query arguments to include in the final hidden form data.
973        // If FALSE is a provided here, DO NOT carry the queries.
974        $do_carry_queries = true;
975        $one_time_carry_queries = array();
976        if (!is_null($carry_args)) {
977            if (is_array($carry_args) && !empty($carry_args)) {
978                foreach ($carry_args as $key=>$arg) {
979                    // Get query from appropriate source.
980                    if (false === $arg) {
981                        $do_carry_queries = false;
982                    } else if (false !== getFormData($arg, false)) {
983                        $one_time_carry_queries[$arg] = getFormData($arg); // Set arg to form data if available.
984                    } else if (!is_numeric($key) && '' != $arg) {
985                        $one_time_carry_queries[$key] = getFormData($key, $arg); // Set to arg to default if specified (overwritten by form data).
986                    }
987                }
988            } else if (false !== getFormData($carry_args, false)) {
989                $one_time_carry_queries[$carry_args] = getFormData($carry_args);
990            } else if (false === $carry_args) {
991                $do_carry_queries = false;
992            }
993        }
994
995        // For each existing request value, we create a hidden input to carry it through a form.
996        if ($do_carry_queries) {
997            // Join the global _carry_queries and local one_time_carry_queries.
998            // urlencode is not used here, not for form data!
999            $query_args = array_merge($this->_carry_queries, $one_time_carry_queries);
1000            foreach ($query_args as $key => $val) {
1001                if (is_array($val)) {
1002                    foreach ($val as $subval) {
1003                        printf('<input type="hidden" name="%s[]" value="%s" />', $key, $subval);
1004                    }
1005                } else {
1006                    printf('<input type="hidden" name="%s" value="%s" />', $key, $val);
1007                }
1008            }
1009            unset($query_args, $key, $val, $subval);
1010        }
1011
1012        // Include the SID if cookies are disabled.
1013        if (!isset($_COOKIE[session_name()]) && !ini_get('session.use_trans_sid')) {
1014            printf('<input type="hidden" name="%s" value="%s" />', session_name(), session_id());
1015        }
1016
1017        // Include the csrf_token in the form.
1018        // This token can be validated upon form submission with $app->verifyCSRFToken() or $app->requireValidCSRFToken()
1019        if ($this->getParam('csrf_token_enabled') && $include_csrf_token) {
1020            printf('<input type="hidden" name="%s" value="%s" />', $this->getParam('csrf_token_name'), $this->recycleCSRFToken());
1021        }
1022    }
1023
1024    /*
1025    * Return a URL with a version number attached. This is useful for overriding network caches ("cache buster") for sourced media, e.g., /style.css?812763482
1026    *
1027    * @access   public
1028    * @param    string  $url    URL to media (e.g., /foo.js)
1029    * @return   string          URL with cache-busting version appended (/foo.js?v=1234567890)
1030    * @author   Quinn Comendant <quinn@strangecode.com>
1031    * @version  1.0
1032    * @since    03 Sep 2014 22:40:24
1033    */
1034    public function cacheBustURL($url)
1035    {
1036        // Get the first delimiter that is needed in the url.
1037        $delim = mb_strpos($url, '?') !== false ? ini_get('arg_separator.output') : '?';
1038        $v = crc32($this->getParam('codebase_version') . '|' . $this->getParam('site_version'));
1039        return sprintf('%s%sv=%s', $url, $delim, $v);
1040    }
1041
1042    /*
1043    * Generate a csrf_token, saving it to the session and returning its value.
1044    * https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#General_Recommendation:_Synchronizer_Token_Pattern
1045    * @access   public
1046    * @return   string  The new csrf_token.
1047    * @author   Quinn Comendant <quinn@strangecode.com>
1048    * @version  1.0
1049    * @since    15 Nov 2014 17:53:51
1050    */
1051    public function generateCSRFToken()
1052    {
1053        return $_SESSION['_app'][$this->_ns]['csrf_tokens'][] = addSignature(time(), null, 64);
1054    }
1055
1056    /*
1057    * Update the csrf_token to a new value if it hasn't been set yet or has expired.
1058    * Save the previous csrf_token in the session to ensure continuity of currently open sessions.
1059    *
1060    * @access   public
1061    * @return   string The current csrf_token
1062    * @author   Quinn Comendant <quinn@strangecode.com>
1063    * @version  1.0
1064    * @since    15 Nov 2014 17:57:17
1065    */
1066    public function recycleCSRFToken()
1067    {
1068        if (!isset($_SESSION['_app'][$this->_ns]['csrf_tokens'])) {
1069            // No token yet; generate one and return it.
1070            $_SESSION['_app'][$this->_ns]['csrf_tokens'] = array();
1071            $return = $this->generateCSRFToken();
1072        }
1073        if (removeSignature(end($_SESSION['_app'][$this->_ns]['csrf_tokens'])) + $this->getParam('csrf_token_timeout') < time()) {
1074            // Newest token is expired; prune array of tokens and generate new token.
1075            // We'll save the 10-most-recent tokens. This allows the user to submit up to 5 forms saved in previously opened tabs with expired tokens (loading a form will prune one token, and submitting a form will prune one token, thus 10 = 5).
1076            $_SESSION['_app'][$this->_ns]['csrf_tokens'] = array_slice($_SESSION['_app'][$this->_ns]['csrf_tokens'], -10, 10);
1077            $return = $this->generateCSRFToken();
1078        }
1079        // Current token is not expired; return it.
1080        $return = end($_SESSION['_app'][$this->_ns]['csrf_tokens']);
1081        $app =& App::getInstance();
1082        return $return;
1083    }
1084
1085    /*
1086    * Compares the given csrf_token with the current or previous one saved in the session.
1087    *
1088    * @access   public
1089    * @param    string  $csrf_token     The token to compare with the session token.
1090    * @return   bool    True if the tokens match, false otherwise.
1091    * @author   Quinn Comendant <quinn@strangecode.com>
1092    * @version  1.0
1093    * @since    15 Nov 2014 18:06:55
1094    */
1095    public function verifyCSRFToken($csrf_token)
1096    {
1097        $app =& App::getInstance();
1098
1099        if (!$this->getParam('csrf_token_enabled')) {
1100            $app->logMsg(sprintf('%s method called, but csrf_token_enabled=false', __FUNCTION__), LOG_ERR, __FILE__, __LINE__);
1101            return false;
1102        }
1103        if ('' == trim($csrf_token)) {
1104            $app->logMsg(sprintf('Empty string failed CSRF verification.', null), LOG_NOTICE, __FILE__, __LINE__);
1105            return false;
1106        }
1107        if (!verifySignature($csrf_token, null, 64)) {
1108            $app->logMsg(sprintf('Input failed CSRF verification (invalid signature in %s).', $csrf_token), LOG_WARNING, __FILE__, __LINE__);
1109            return false;
1110        }
1111        $this->recycleCSRFToken();
1112        if (!in_array($csrf_token, $_SESSION['_app'][$this->_ns]['csrf_tokens'])) {
1113            $app->logMsg(sprintf('Input failed CSRF verification (%s not in %s).', $csrf_token, getDump($_SESSION['_app'][$this->_ns]['csrf_tokens'])), LOG_WARNING, __FILE__, __LINE__);
1114            return false;
1115        }
1116        // $app->logMsg(sprintf('Verified token %s is in %s', $csrf_token, getDump($_SESSION['_app'][$this->_ns]['csrf_tokens'])), LOG_DEBUG, __FILE__, __LINE__);
1117        return true;
1118    }
1119
1120    /*
1121    * Bounce user if they submit a token that doesn't match the one saved in the session.
1122    * Because this function calls dieURL() it must be called before any other HTTP header output.
1123    *
1124    * @access   public
1125    * @param    string  $csrf_token The token to compare with the session token.
1126    * @param    string  $message    Optional message to display to the user (otherwise default message will display). Set to an empty string to display no message.
1127    * @param    int    $type    The type of message: MSG_NOTICE,
1128    *                           MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
1129    * @param    string $file    __FILE__.
1130    * @param    string $line    __LINE__.
1131    * @return   void
1132    * @author   Quinn Comendant <quinn@strangecode.com>
1133    * @version  1.0
1134    * @since    15 Nov 2014 18:10:17
1135    */
1136    public function requireValidCSRFToken($csrf_token, $message=null, $type=MSG_NOTICE, $file=null, $line=null)
1137    {
1138        $app =& App::getInstance();
1139
1140        if (!$this->verifyCSRFToken($csrf_token)) {
1141            $message = isset($message) ? $message : _("Something went wrong; please try again.");
1142            $app->raiseMsg($message, $type, $file, $line);
1143            $app->dieBoomerangURL();
1144        }
1145    }
1146
1147    /**
1148     * Uses an http header to redirect the client to the given $url. If sessions are not used
1149     * and the session is not already defined in the given $url, the SID is appended as a URI query.
1150     * As with all header generating functions, make sure this is called before any other output.
1151     *
1152     * @param   string  $url                    The URL the client will be redirected to.
1153     * @param   mixed   $carry_args             Additional url arguments to carry in the query,
1154     *                                          or FALSE to prevent carrying queries. Can be any of the following formats:
1155     *                                          -array('key1', key2', key3')  <-- to save these keys if in the form data.
1156     *                                          -array('key1' => 'value', key2' => 'value')  <-- to set keys to default values if not present in form data.
1157     *                                          -false  <-- To not carry any queries. If URL already has queries those will be retained.
1158     * @param   bool    $always_include_sid     Force session id to be added to Location header.
1159     */
1160    public function dieURL($url, $carry_args=null, $always_include_sid=false)
1161    {
1162        if (!$this->running) {
1163            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1164            return false;
1165        }
1166
1167        if ('' == $url) {
1168            // If URL is not specified, use the redirect_home_url.
1169            $url = $this->getParam('redirect_home_url');
1170        }
1171
1172        if (preg_match('!^/!', $url)) {
1173            // If relative URL is given, prepend correct local hostname.
1174            $scheme = 'on' == getenv('HTTPS') ? 'https' : 'http';
1175            $host = getenv('HTTP_HOST');
1176            $url = sprintf('%s://%s%s', $scheme, $host, $url);
1177        }
1178
1179        $url = $this->url($url, $carry_args, $always_include_sid);
1180
1181        // Should we send a "303 See Other" header here instead of relying on the 302 sent automatically by PHP?
1182        header(sprintf('Location: %s', $url));
1183        $this->logMsg(sprintf('dieURL: %s', $url), LOG_DEBUG, __FILE__, __LINE__);
1184
1185        // End application.
1186        // Recommended, although I'm not sure it's necessary: http://cn2.php.net/session_write_close
1187        $this->stop();
1188        die;
1189    }
1190
1191    /*
1192    * Redirects a user by calling $app->dieURL(). It will use:
1193    * 1. the stored boomerang URL, it it exists
1194    * 2. a specified $default_url, it it exists
1195    * 3. the referring URL, it it exists.
1196    * 4. redirect_home_url configuration variable.
1197    *
1198    * @access   public
1199    * @param    string  $id             Identifier for this script.
1200    * @param    mixed   $carry_args     Additional arguments to carry in the URL automatically (see $app->oHREF()).
1201    * @param    string  $default_url    A default URL if there is not a valid specified boomerang URL.
1202    * @param    bool    $queryless_referrer_comparison   Exclude the URL query from the refererIsMe() comparison.
1203    * @return   bool                    False if the session is not running. No return otherwise.
1204    * @author   Quinn Comendant <quinn@strangecode.com>
1205    * @since    31 Mar 2006 19:17:00
1206    */
1207    public function dieBoomerangURL($id=null, $carry_args=null, $default_url=null, $queryless_referrer_comparison=false)
1208    {
1209        if (!$this->running) {
1210            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1211            return false;
1212        }
1213
1214        // Get URL from stored boomerang. Allow non specific URL if ID not valid.
1215        if ($this->validBoomerangURL($id, true)) {
1216            if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1217                $url = $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id];
1218                $this->logMsg(sprintf('dieBoomerangURL(%s) found: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1219            } else {
1220                $url = end($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1221                $this->logMsg(sprintf('dieBoomerangURL(%s) using: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1222            }
1223            // Delete stored boomerang.
1224            $this->deleteBoomerangURL($id);
1225        } else if (isset($default_url)) {
1226            $url = $default_url;
1227        } else if (!refererIsMe(true === $queryless_referrer_comparison)) {
1228            // Ensure that the redirecting page is not also the referrer.
1229            $url = getenv('HTTP_REFERER');
1230            $this->logMsg(sprintf('dieBoomerangURL(%s) using referrer: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1231        } else {
1232            // If URL is not specified, use the redirect_home_url.
1233            $url = $this->getParam('redirect_home_url');
1234            $this->logMsg(sprintf('dieBoomerangURL(%s) using redirect_home_url: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1235        }
1236
1237        // A redirection will never happen immediately twice.
1238        // Set the time so ensure this doesn't happen.
1239        $_SESSION['_app'][$this->_ns]['boomerang']['time'] = time();
1240        $this->dieURL($url, $carry_args);
1241    }
1242
1243    /**
1244     * Set the URL to return to when $app->dieBoomerangURL() is called.
1245     *
1246     * @param string  $url  A fully validated URL.
1247     * @param bool  $id     An identification tag for this url.
1248     * FIXME: url garbage collection?
1249     */
1250    public function setBoomerangURL($url=null, $id=null)
1251    {
1252        if (!$this->running) {
1253            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1254            return false;
1255        }
1256        // A redirection will never happen immediately after setting the boomerangURL.
1257        // Set the time so ensure this doesn't happen. See $app->validBoomerangURL for more.
1258
1259        if ('' != $url && is_string($url)) {
1260            // Delete any boomerang request keys in the query string (along with any trailing delimiters after the deletion).
1261            $url = preg_replace(array('/([&?])boomerang=\w+[&?]?/', '/[&?]$/'), array('$1', ''), $url);
1262
1263            if (isset($_SESSION['_app'][$this->_ns]['boomerang']['url']) && is_array($_SESSION['_app'][$this->_ns]['boomerang']['url']) && !empty($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1264                // If the URL currently exists in the boomerang array, delete.
1265                while ($existing_key = array_search($url, $_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1266                    unset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$existing_key]);
1267                }
1268            }
1269
1270            if (isset($id)) {
1271                $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id] = $url;
1272            } else {
1273                $_SESSION['_app'][$this->_ns]['boomerang']['url'][] = $url;
1274            }
1275            $this->logMsg(sprintf('setBoomerangURL(%s): %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1276            return true;
1277        } else {
1278            $this->logMsg(sprintf('setBoomerangURL(%s) is empty!', $id, $url), LOG_NOTICE, __FILE__, __LINE__);
1279            return false;
1280        }
1281    }
1282
1283    /**
1284     * Return the URL set for the specified $id, or an empty string if one isn't set.
1285     *
1286     * @param string  $id     An identification tag for this url.
1287     */
1288    public function getBoomerangURL($id=null)
1289    {
1290        if (!$this->running) {
1291            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1292            return false;
1293        }
1294
1295        if (isset($id)) {
1296            if (isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1297                return $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id];
1298            } else {
1299                return '';
1300            }
1301        } else if (is_array($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1302            return end($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1303        } else {
1304            return false;
1305        }
1306    }
1307
1308    /**
1309     * Delete the URL set for the specified $id.
1310     *
1311     * @param string  $id     An identification tag for this url.
1312     */
1313    public function deleteBoomerangURL($id=null)
1314    {
1315        if (!$this->running) {
1316            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1317            return false;
1318        }
1319
1320        $this->logMsg(sprintf('deleteBoomerangURL(%s): %s', $id, $this->getBoomerangURL($id)), LOG_DEBUG, __FILE__, __LINE__);
1321
1322        if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1323            unset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id]);
1324        } else if (is_array($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1325            array_pop($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1326        }
1327    }
1328
1329    /**
1330     * Check if a valid boomerang URL value has been set. A boomerang URL is considered
1331     * valid if: 1) it is not empty, 2) it is not the current URL, and 3) has not been accessed within n seconds.
1332     *
1333     * @return bool  True if it is set and valid, false otherwise.
1334     */
1335    public function validBoomerangURL($id=null, $use_nonspecificboomerang=false)
1336    {
1337        if (!$this->running) {
1338            $this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
1339            return false;
1340        }
1341
1342        if (!isset($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1343            $this->logMsg(sprintf('validBoomerangURL(%s) no boomerang URL set.', $id), LOG_DEBUG, __FILE__, __LINE__);
1344            return false;
1345        }
1346
1347        // Time is the time stamp of a boomerangURL redirection, or setting of a boomerangURL.
1348        // a boomerang redirection will always occur at least several seconds after the last boomerang redirect
1349        // or a boomerang being set.
1350        $boomerang_time = isset($_SESSION['_app'][$this->_ns]['boomerang']['time']) ? $_SESSION['_app'][$this->_ns]['boomerang']['time'] : 0;
1351
1352        $url = '';
1353        if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1354            $url = $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id];
1355        } else if (!isset($id) || $use_nonspecificboomerang) {
1356            // Use non specific boomerang if available.
1357            $url = end($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1358        }
1359
1360        $this->logMsg(sprintf('validBoomerangURL(%s) testing: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1361
1362        if ('' == $url) {
1363            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, empty!', $id), LOG_DEBUG, __FILE__, __LINE__);
1364            return false;
1365        }
1366        if ($url == absoluteMe()) {
1367            // The URL we are directing to is the current page.
1368            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, same as absoluteMe: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1369            return false;
1370        }
1371        if ($boomerang_time >= (time() - 2)) {
1372            // Last boomerang direction was less than 2 seconds ago.
1373            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, boomerang_time too short: %s seconds', $id, time() - $boomerang_time), LOG_DEBUG, __FILE__, __LINE__);
1374            return false;
1375        }
1376
1377        $this->logMsg(sprintf('validBoomerangURL(%s) is valid: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1378        return true;
1379    }
1380
1381    /**
1382     * Force the user to connect via https (port 443) by redirecting them to
1383     * the same page but with https.
1384     */
1385    public function sslOn()
1386    {
1387        if (function_exists('apache_get_modules')) {
1388            $modules = apache_get_modules();
1389        } else {
1390            // It's safe to assume we have mod_ssl if we can't determine otherwise.
1391            $modules = array('mod_ssl');
1392        }
1393
1394        if ('' == getenv('HTTPS') && $this->getParam('ssl_enabled') && in_array('mod_ssl', $modules)) {
1395            $this->raiseMsg(sprintf(_("Secure SSL connection made to %s"), $this->getParam('ssl_domain')), MSG_NOTICE, __FILE__, __LINE__);
1396            // Always append session because some browsers do not send cookie when crossing to SSL URL.
1397            $this->dieURL('https://' . $this->getParam('ssl_domain') . getenv('REQUEST_URI'), null, true);
1398        }
1399    }
1400
1401    /**
1402     * to enforce the user to connect via http (port 80) by redirecting them to
1403     * a http version of the current url.
1404     */
1405    public function sslOff()
1406    {
1407        if ('' != getenv('HTTPS')) {
1408            $this->dieURL('http://' . getenv('HTTP_HOST') . getenv('REQUEST_URI'), null, true);
1409        }
1410    }
1411
1412    /*
1413    * Sets a cookie, with error checking and some sane defaults.
1414    *
1415    * @access   public
1416    * @param    string  $name       The name of the cookie.
1417    * @param    string  $value      The value of the cookie.
1418    * @param    string  $expire     The time the cookie expires, as a unix timestamp or string value passed to strtotime.
1419    * @param    string  $path       The path on the server in which the cookie will be available on.
1420    * @param    string  $domain     The domain that the cookie is available to.
1421    * @param    bool    $secure     Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client.
1422    * @param    bool    $httponly   When TRUE the cookie will be made accessible only through the HTTP protocol (makes cookies unreadable to javascript).
1423    * @return   bool                True on success, false on error.
1424    * @author   Quinn Comendant <quinn@strangecode.com>
1425    * @version  1.0
1426    * @since    02 May 2014 16:36:34
1427    */
1428    public function setCookie($name, $value, $expire='+10 years', $path='/', $domain=null, $secure=null, $httponly=null)
1429    {
1430        if (!is_scalar($name)) {
1431            $this->logMsg(sprintf('Cookie name must be scalar, is not: %s', getDump($name)), LOG_NOTICE, __FILE__, __LINE__);
1432            return false;
1433        }
1434        if (!is_scalar($value)) {
1435            $this->logMsg(sprintf('Cookie "%s" value must be scalar, is not: %s', $name, getDump($value)), LOG_NOTICE, __FILE__, __LINE__);
1436            return false;
1437        }
1438
1439        // Defaults.
1440        $expire = (is_numeric($expire) ? $expire : (is_string($expire) ? strtotime($expire) : $expire));
1441        $secure = $secure ?: ('' != getenv('HTTPS') && $this->getParam('ssl_enabled'));
1442        $httponly = $httponly ?: true;
1443
1444        // Make sure the expiration date is a valid 32bit integer.
1445        if (is_int($expire) && $expire > 2147483647) {
1446            $this->logMsg(sprintf('Cookie "%s" expire time exceeds a 32bit integer (%s)', $key, date('r', $expire)), LOG_NOTICE, __FILE__, __LINE__);
1447        }
1448
1449        // Measure total cookie length and warn if larger than max recommended size of 4093.
1450        // https://stackoverflow.com/questions/640938/what-is-the-maximum-size-of-a-web-browsers-cookies-key
1451        // The date the header name include 51 bytes: Set-Cookie: ; expires=Fri, 03-May-2024 00:04:47 GMT
1452        $cookielen = strlen($name . $value . $path . $domain . ($secure ? '; secure' : '') . ($httponly ? '; httponly' : '')) + 51;
1453        if ($cookielen > 4093) {
1454            $this->logMsg(sprintf('Cookie "%s" has a size greater than 4093 bytes (is %s bytes)', $key, $cookielen), LOG_NOTICE, __FILE__, __LINE__);
1455        }
1456
1457        // Ensure PHP version allow use of httponly.
1458        if (version_compare(PHP_VERSION, '5.2.0', '>=')) {
1459            $ret = setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
1460        } else {
1461            $ret = setcookie($name, $value, $expire, $path, $domain, $secure);
1462        }
1463
1464        if (false === $ret) {
1465            $this->logMsg(sprintf('Failed to set cookie (%s=%s) probably due to output before headers.', $name, $value), LOG_NOTICE, __FILE__, __LINE__);
1466        }
1467        return $ret;
1468    }
1469} // End.
Note: See TracBrowser for help on using the repository browser.