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

Last change on this file since 518 was 518, checked in by anonymous, 9 years ago

Non-breaking function changes to App and Utilities.

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