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

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

Moved CLI flag to ->cli which can be forced off for tests

File size: 72.5 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    // We're running as CLI. Public becuase we must force this as false when testing sessions via CLI.
67    public $cli = false;
68
69    // Dictionary of global application parameters.
70    protected $_params = array();
71
72    // Default parameters.
73    protected $_param_defaults = array(
74
75        // Public name and email address for this application.
76        'site_name' => null,
77        'site_email' => '', // Set to no-reply@HTTP_HOST if not set here.
78        'site_url' => '', // URL to the root of the site (created during App->start()).
79        'page_url' => '', // URL to the current page (created during App->start()).
80        'images_path' => '', // Location for codebase-generated interface widgets (ex: "/admin/i").
81        'site_version' => '', // Version of this application (set automatically during start() if site_version_file is used).
82        'site_version_file' => 'docs/version.txt', // File containing version number of this app, relative to the include path.
83
84        // The location the user will go if the system doesn't know where else to send them.
85        'redirect_home_url' => '/',
86
87        // SSL URL used when redirecting with $app->sslOn().
88        'ssl_domain' => null,
89        'ssl_enabled' => false,
90
91        // Use CSRF tokens. See notes in the getCSRFToken() method.
92        'csrf_token_enabled' => true,
93        // Form tokens will expire after this duration, in seconds.
94        'csrf_token_timeout' => 259200, // 259200 seconds = 3 days.
95        'csrf_token_name' => 'csrf_token',
96
97        // HMAC signing method
98        'signing_method' => 'sha512+base64',
99
100        // Character set for page output. Used in the Content-Type header and the HTML <meta content-type> tag.
101        'character_set' => 'utf-8',
102
103        // Human-readable format used to display dates.
104        'date_format' => 'd M Y',
105        'time_format' => 'h:i:s A',
106        'sql_date_format' => '%e %b %Y',
107        'sql_time_format' => '%k:%i',
108
109        // Use php sessions?
110        'enable_session' => false,
111        'session_name' => '_session',
112        'session_use_cookies' => true,
113
114        // Pass the session-id through URLs if cookies are not enabled?
115        // Disable this to prevent session ID theft.
116        'session_use_trans_sid' => false,
117
118        // Use database?
119        'enable_db' => false,
120
121        // Use db-based sessions?
122        'enable_db_session_handler' => false,
123
124        // DB credentials should be set as apache environment variables in httpd.conf, readable only by root.
125        'db_server' => 'localhost',
126        'db_name' => null,
127        'db_user' => null,
128        'db_pass' => null,
129
130        // And for CLI scripts, which should include a JSON file at this specified location in the include path.
131        'db_auth_file' => 'db_auth.json',
132
133        // Database debugging.
134        'db_always_debug' => false, // TRUE = display all SQL queries.
135        'db_debug' => false, // TRUE = display db errors.
136        'db_die_on_failure' => false, // TRUE = script stops on db error.
137
138        // For classes that require db tables, do we check that a table exists and create if missing?
139        'db_create_tables' => true,
140
141        // The level of error reporting. Don't change this to suppress messages, instead use display_errors to control display.
142        'error_reporting' => E_ALL,
143
144        // Don't display errors by default; it is preferable to log them to a file. For CLI scripts, set this to the string 'stderr'.
145        'display_errors' => false,
146
147        // Directory in which to store log files.
148        'log_directory' => '',
149
150        // PHP error log.
151        'php_error_log' => 'php_error_log',
152
153        // General application log.
154        'log_filename' => 'app_log',
155
156        // Don't email or SMS duplicate messages that happen more often than this value (in seconds).
157        'log_multiple_timeout' => 3600, // Hourly
158
159        // Logging priority can be any of the following, or false to deactivate:
160        // LOG_EMERG     system is unusable
161        // LOG_ALERT     action must be taken immediately
162        // LOG_CRIT      critical conditions
163        // LOG_ERR       error conditions
164        // LOG_WARNING   warning conditions
165        // LOG_NOTICE    normal, but significant, condition
166        // LOG_INFO      informational message
167        // LOG_DEBUG     debug-level message
168        'log_file_priority' => LOG_INFO,
169        'log_email_priority' => false,
170        'log_sms_priority' => false,
171        'log_screen_priority' => false,
172
173        // Email address to receive log event emails. Use multiple addresses by separating them with commas.
174        'log_to_email_address' => null,
175
176        // SMS Email address to receive log event SMS messages. Use multiple addresses by separating them with commas.
177        'log_to_sms_address' => null,
178
179        // 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.
180        'log_ignore_repeated_events' => true,
181
182        // Temporary files directory.
183        'tmp_dir' => '/tmp',
184
185        // A key for calculating simple cryptographic signatures. Set using as an environment variables in the httpd.conf with 'SetEnv SIGNING_KEY <key>'.
186        // Existing password hashes rely on the same key/salt being used to compare encryptions.
187        // Don't change this unless you know existing hashes or signatures will not be affected!
188        'signing_key' => 'aae6abd6209d82a691a9f96384a7634a',
189
190        // Force getFormData, getPost, and getGet to always run dispelMagicQuotes() with stripslashes().
191        // This should be set to 'true' when using the codebase with Wordpress because
192        // WP forcefully adds slashes to all input despite the setting of magic_quotes_gpc.
193        'always_dispel_magicquotes' => false,
194    );
195
196    /**
197     * Constructor.
198     */
199    public function __construct($namespace='')
200    {
201        // Set namespace of application instance.
202        $this->_ns = $namespace;
203
204        // Initialize default parameters.
205        $this->_params = array_merge($this->_params, $this->_param_defaults);
206
207        // Begin timing script.
208        require_once dirname(__FILE__) . '/ScriptTimer.inc.php';
209        $this->timer = new ScriptTimer();
210        $this->timer->start('_app');
211
212        // Are we running as a CLI?
213        $this->cli = ('cli' === php_sapi_name() || defined('_CLI'));
214    }
215
216    /**
217     * This method enforces the singleton pattern for this class. Only one application is running at a time.
218     *
219     * $param   string  $namespace  Name of this application.
220     * @return  object  Reference to the global Cache object.
221     * @access  public
222     * @static
223     */
224    public static function &getInstance($namespace='')
225    {
226        if (self::$instance === null) {
227            // TODO: Yep, having a namespace with one singletone instance is not very useful.
228            self::$instance = new self($namespace);
229        }
230
231        return self::$instance;
232    }
233
234    /**
235     * Set (or overwrite existing) parameters by passing an array of new parameters.
236     *
237     * @access public
238     * @param  array    $param     Array of parameters (key => val pairs).
239     */
240    public function setParam($param=null)
241    {
242        if (isset($param) && is_array($param)) {
243            // Merge new parameters with old overriding old ones that are passed.
244            $this->_params = array_merge($this->_params, $param);
245
246            if ($this->running) {
247                // Params that require additional processing if set during runtime.
248                foreach ($param as $key => $val) {
249                    switch ($key) {
250                    case 'session_name':
251                        session_name($val);
252                        break;
253
254                    case 'session_use_cookies':
255                        ini_set('session.use_cookies', $val);
256                        break;
257
258                    case 'error_reporting':
259                        ini_set('error_reporting', $val);
260                        break;
261
262                    case 'display_errors':
263                        ini_set('display_errors', $val);
264                        break;
265
266                    case 'log_errors':
267                        ini_set('log_errors', true);
268                        break;
269
270                    case 'log_directory':
271                        if (is_dir($val) && is_writable($val)) {
272                            ini_set('error_log', $val . '/' . $this->getParam('php_error_log'));
273                        }
274                        break;
275                    }
276                }
277            }
278        }
279    }
280
281    /**
282     * Return the value of a parameter.
283     *
284     * @access  public
285     * @param   string  $param      The key of the parameter to return.
286     * @return  mixed               Parameter value, or null if not existing.
287     */
288    public function getParam($param=null)
289    {
290        if ($param === null) {
291            return $this->_params;
292        } else if (array_key_exists($param, $this->_params)) {
293            return $this->_params[$param];
294        } else {
295            return null;
296        }
297    }
298
299    /**
300     * Begin running this application.
301     *
302     * @access  public
303     * @author  Quinn Comendant <quinn@strangecode.com>
304     * @since   15 Jul 2005 00:32:21
305     */
306    public function start()
307    {
308        if ($this->running) {
309            return false;
310        }
311
312        // Error reporting.
313        ini_set('error_reporting', $this->getParam('error_reporting'));
314        ini_set('display_errors', $this->getParam('display_errors'));
315        ini_set('log_errors', true);
316        if (is_dir($this->getParam('log_directory')) && is_writable($this->getParam('log_directory'))) {
317            ini_set('error_log', $this->getParam('log_directory') . '/' . $this->getParam('php_error_log'));
318        }
319
320        // Set character set to use for multi-byte string functions.
321        mb_internal_encoding($this->getParam('character_set'));
322        switch (mb_strtolower($this->getParam('character_set'))) {
323        case 'utf-8' :
324            mb_language('uni');
325            break;
326
327        case 'iso-2022-jp' :
328            mb_language('ja');
329            break;
330
331        case 'iso-8859-1' :
332        default :
333            mb_language('en');
334            break;
335        }
336
337        /**
338         * 1. Start Database.
339         */
340
341        if (true === $this->getParam('enable_db')) {
342
343            // DB connection parameters taken from environment variables in the server httpd.conf file (readable only by root)

344            if (!empty($_SERVER['DB_SERVER']) && !$this->getParam('db_server')) {
345                $this->setParam(array('db_server' => $_SERVER['DB_SERVER']));
346            }
347            if (!empty($_SERVER['DB_NAME']) && !$this->getParam('db_name')) {
348                $this->setParam(array('db_name' => $_SERVER['DB_NAME']));
349            }
350            if (!empty($_SERVER['DB_USER']) && !$this->getParam('db_user')) {
351                $this->setParam(array('db_user' => $_SERVER['DB_USER']));
352            }
353            if (!empty($_SERVER['DB_PASS']) && !$this->getParam('db_pass')) {
354                $this->setParam(array('db_pass' => $_SERVER['DB_PASS']));
355            }
356
357            // DB credentials for CLI scripts stored in a JSON file with read rights given only to the user who will be executing the scripts: -r--------
358            // But not if all DB credentials have been defined already by other means.
359            if ($this->cli && (!$this->getParam('db_server') || !$this->getParam('db_name') || !$this->getParam('db_user') || !$this->getParam('db_pass'))) {
360                if (false !== $db_auth_file = stream_resolve_include_path($this->getParam('db_auth_file'))) {
361                    if (is_readable($db_auth_file)) {
362                        $this->setParam(json_decode(file_get_contents($db_auth_file), true));
363                    } else {
364                        $this->logMsg(sprintf('Unable to read DB auth file: %s', $db_auth_file), LOG_ALERT, __FILE__, __LINE__);
365                    }
366                } else {
367                    $this->logMsg(sprintf('DB auth file not found: %s', $this->getParam('db_auth_file')), LOG_ALERT, __FILE__, __LINE__);
368                }
369            }
370
371            // There will ever only be one instance of the DB object, and here is where it is instantiated.
372            require_once dirname(__FILE__) . '/DB.inc.php';
373            $this->db =& DB::getInstance();
374            $this->db->setParam(array(
375                'db_server' => $this->getParam('db_server'),
376                'db_name' => $this->getParam('db_name'),
377                'db_user' => $this->getParam('db_user'),
378                'db_pass' => $this->getParam('db_pass'),
379                'db_always_debug' => $this->getParam('db_always_debug'),
380                'db_debug' => $this->getParam('db_debug'),
381                'db_die_on_failure' => $this->getParam('db_die_on_failure'),
382            ));
383
384            // Connect to database.
385            $this->db->connect();
386        }
387
388
389        /**
390         * 2. Start PHP session.
391         */
392
393        // Use sessions if enabled and not a CLI script.
394        if (true === $this->getParam('enable_session') && !$this->cli) {
395
396            // Session parameters.
397            ini_set('session.gc_probability', 1);
398            ini_set('session.gc_divisor', 1000);
399            ini_set('session.gc_maxlifetime', 43200); // 12 hours
400            ini_set('session.use_cookies', $this->getParam('session_use_cookies'));
401            ini_set('session.use_trans_sid', false);
402            ini_set('session.entropy_file', '/dev/urandom');
403            ini_set('session.entropy_length', '512');
404            ini_set('session.cookie_httponly', true);
405            session_name($this->getParam('session_name'));
406
407            if (true === $this->getParam('enable_db_session_handler') && true === $this->getParam('enable_db')) {
408                // Database session handling.
409                require_once dirname(__FILE__) . '/DBSessionHandler.inc.php';
410                $db_save_handler = new DBSessionHandler($this->db, array(
411                    'db_table' => 'session_tbl',
412                    'create_table' => $this->getParam('db_create_tables'),
413                ));
414            }
415
416            // Start the session.
417            session_start();
418
419            if (!isset($_SESSION['_app'][$this->_ns])) {
420                // Access session data using: $_SESSION['...'].
421                // Initialize here _after_ session has started.
422                $_SESSION['_app'][$this->_ns] = array(
423                    'messages' => array(),
424                    'boomerang' => array('url' => array()),
425                );
426            }
427        }
428
429
430        /**
431         * 3. Misc setup.
432         */
433
434        // Site URL will become something like http://host.name.tld (no ending slash)
435        // and is used whenever a URL need be used to the current site.
436        // Not available on CLI scripts obviously.
437        if (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST'] && '' == $this->getParam('site_url')) {
438            $this->setParam(array('site_url' => sprintf('%s://%s', ('on' == getenv('HTTPS') ? 'https' : 'http'), getenv('HTTP_HOST'))));
439        }
440
441        // Page URL will become a permalink to the current page.
442        // Also not available on CLI scripts obviously.
443        if (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST']) {
444            $this->setParam(array('page_url' => sprintf('%s://%s%s', ('on' == getenv('HTTPS') ? 'https' : 'http'), getenv('HTTP_HOST'), getenv('REQUEST_URI'))));
445        }
446
447        // In case site_email isn't set, use something halfway presentable.
448        if (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST'] && '' == $this->getParam('site_email')) {
449            $this->setParam(array('site_email' => sprintf('no-reply@%s', getenv('HTTP_HOST'))));
450        }
451
452        // A key for calculating simple cryptographic signatures.
453        if (isset($_SERVER['SIGNING_KEY'])) {
454            $this->setParam(array('signing_key' => $_SERVER['SIGNING_KEY']));
455        }
456
457        // Character set. This should also be printed in the html header template.
458        if (!$this->cli) {
459            if (!headers_sent($h_file, $h_line)) {
460                header('Content-type: text/html; charset=' . $this->getParam('character_set'));
461            } else {
462                $this->logMsg(sprintf('Unable to set Content-type; headers already sent (output started in %s : %s)', $h_file, $h_line), LOG_DEBUG, __FILE__, __LINE__);
463            }
464        }
465
466        // Set the version of the codebase we're using.
467        $codebase_version_file = dirname(__FILE__) . '/../docs/version.txt';
468        $codebase_version = '';
469        if (is_readable($codebase_version_file) && !is_dir($codebase_version_file)) {
470            $codebase_version = trim(file_get_contents($codebase_version_file));
471            $this->setParam(array('codebase_version' => $codebase_version));
472            if (!$this->cli) {
473                if (!headers_sent($h_file, $h_line)) {
474                    header('X-Codebase-Version: ' . $codebase_version);
475                } else {
476                    $this->logMsg(sprintf('Unable to set X-Codebase-Version; headers already sent (output started in %s : %s)', $h_file, $h_line), LOG_DEBUG, __FILE__, __LINE__);
477                }
478            }
479        }
480
481        if (version_compare(PHP_VERSION, self::CODEBASE_MIN_PHP_VERSION, '<')) {
482            $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__);
483        }
484
485        // Set the application version if defined.
486        if (false !== $site_version_file = stream_resolve_include_path($this->getParam('site_version_file'))) {
487            if (mb_strpos($site_version_file, '.json') !== false) {
488                $version_json = json_decode(trim(file_get_contents($site_version_file)), true);
489                $site_version = $version_json['version'];
490            } else {
491                $site_version = trim(file_get_contents($site_version_file));
492            }
493            $this->setParam(array('site_version' => $site_version));
494        }
495        if (!$this->cli && $this->getParam('site_version')) {
496            if (!headers_sent($h_file, $h_line)) {
497                header('X-Site-Version: ' . $site_version);
498            } else {
499                $this->logMsg(sprintf('Unable to set X-Site-Version; headers already sent (output started in %s : %s)', $h_file, $h_line), LOG_DEBUG, __FILE__, __LINE__);
500            }
501        }
502
503        $this->running = true;
504        return true;
505    }
506
507    /**
508     * Stop running this application.
509     *
510     * @access  public
511     * @author  Quinn Comendant <quinn@strangecode.com>
512     * @since   17 Jul 2005 17:20:18
513     */
514    public function stop()
515    {
516        session_write_close();
517        $this->running = false;
518        $num_queries = 0;
519        if (true === $this->getParam('enable_db')) {
520            $num_queries = $this->db->numQueries();
521            $this->db->close();
522        }
523        $mem_current = memory_get_usage();
524        $mem_peak = memory_get_peak_usage();
525        $this->timer->stop('_app');
526        $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__);
527    }
528
529
530    /**
531     * Add a message to the session, which is printed in the header.
532     * Just a simple way to print messages to the user.
533     *
534     * @access public
535     *
536     * @param string $message The text description of the message.
537     * @param int    $type    The type of message: MSG_NOTICE,
538     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
539     * @param string $file    __FILE__.
540     * @param string $line    __LINE__.
541     */
542    public function raiseMsg($message, $type=MSG_NOTICE, $file=null, $line=null)
543    {
544        $message = trim($message);
545
546        if (!$this->running) {
547            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
548            return false;
549        }
550
551        if (!$this->getParam('enable_session')) {
552            $this->logMsg(sprintf('Canceled %s, session not enabled.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
553            return false;
554        }
555
556        if ('' == trim($message)) {
557            $this->logMsg(sprintf('Raised message is an empty string.', null), LOG_NOTICE, __FILE__, __LINE__);
558            return false;
559        }
560
561        // Avoid duplicate full-stops..
562        $message = trim(preg_replace('/\.{2}$/', '.', $message));
563
564        // Save message in session under unique key to avoid duplicate messages.
565        $msg_id = md5($type . $message);
566        if (!isset($_SESSION['_app'][$this->_ns]['messages'][$msg_id])) {
567            $_SESSION['_app'][$this->_ns]['messages'][$msg_id] = array(
568                'type'    => $type,
569                'message' => $message,
570                'file'    => $file,
571                'line'    => $line,
572                'count'   => (isset($_SESSION['_app'][$this->_ns]['messages'][$msg_id]['count']) ? (1 + $_SESSION['_app'][$this->_ns]['messages'][$msg_id]['count']) : 1)
573            );
574        }
575
576        if (!in_array($type, array(MSG_NOTICE, MSG_SUCCESS, MSG_WARNING, MSG_ERR))) {
577            $this->logMsg(sprintf('Invalid MSG_* type: %s', $type), LOG_NOTICE, __FILE__, __LINE__);
578        }
579
580        // Increment the counter for this message type.
581        $this->_raised_msg_counter[$type] += 1;
582    }
583
584    /*
585    * 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)
586    *
587    * @access   public
588    * @param
589    * @return
590    * @author   Quinn Comendant <quinn@strangecode.com>
591    * @version  1.0
592    * @since    30 Apr 2015 17:13:03
593    */
594    public function getRaisedMessageCount($type='all')
595    {
596        if ('all' == $type) {
597            return array_sum($this->_raised_msg_counter);
598        } else if (isset($this->_raised_msg_counter[$type])) {
599            return $this->_raised_msg_counter[$type];
600        } else {
601            $this->logMsg(sprintf('Cannot return count of unknown raised message type: %s', $type), LOG_WARNING, __FILE__, __LINE__);
602            return false;
603        }
604    }
605
606    /**
607     * Returns an array of the raised messages.
608     *
609     * @access  public
610     * @return  array   List of messages in FIFO order.
611     * @author  Quinn Comendant <quinn@strangecode.com>
612     * @since   21 Dec 2005 13:09:20
613     */
614    public function getRaisedMessages()
615    {
616        if (!$this->running) {
617            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
618            return false;
619        }
620        return isset($_SESSION['_app'][$this->_ns]['messages']) ? $_SESSION['_app'][$this->_ns]['messages'] : array();
621    }
622
623    /**
624     * Resets the message list.
625     *
626     * @access  public
627     * @author  Quinn Comendant <quinn@strangecode.com>
628     * @since   21 Dec 2005 13:21:54
629     */
630    public function clearRaisedMessages()
631    {
632        if (!$this->running) {
633            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
634            return false;
635        }
636
637        $_SESSION['_app'][$this->_ns]['messages'] = array();
638    }
639
640    /**
641     * Prints the HTML for displaying raised messages.
642     *
643     * @param   string  $above    Additional message to print above error messages (e.g. "Oops!").
644     * @param   string  $below    Additional message to print below error messages (e.g. "Please fix and resubmit").
645     * @param   string  $print_gotohash_js  Print a line of javascript that scrolls the browser window down to view any error messages.
646     * @param   string  $hash     The #hashtag to scroll to.
647     * @access  public
648     * @author  Quinn Comendant <quinn@strangecode.com>
649     * @since   15 Jul 2005 01:39:14
650     */
651    public function printRaisedMessages($above='', $below='', $print_gotohash_js=false, $hash='sc-msg')
652    {
653
654        if (!$this->running) {
655            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
656            return false;
657        }
658
659        $messages = $this->getRaisedMessages();
660        if (!empty($messages)) {
661            ?><div id="sc-msg" class="sc-msg"><?php
662            if ('' != $above) {
663                ?><div class="sc-above"><?php echo oTxt($above); ?></div><?php
664            }
665            foreach ($messages as $m) {
666                if (error_reporting() > 0 && $this->getParam('display_errors') && isset($m['file']) && isset($m['line'])) {
667                    echo "\n<!-- [" . $m['file'] . ' : ' . $m['line'] . '] -->';
668                }
669                switch ($m['type']) {
670                case MSG_ERR:
671                    echo '<div data-alert class="sc-msg-error alert-box alert">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
672                    break;
673
674                case MSG_WARNING:
675                    echo '<div data-alert class="sc-msg-warning alert-box warning">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
676                    break;
677
678                case MSG_SUCCESS:
679                    echo '<div data-alert class="sc-msg-success alert-box success">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
680                    break;
681
682                case MSG_NOTICE:
683                default:
684                    echo '<div data-alert class="sc-msg-notice alert-box info">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
685                    break;
686                }
687            }
688            if ('' != $below) {
689                ?><div class="sc-below"><?php echo oTxt($below); ?></div><?php
690            }
691            ?></div><?php
692            if ($print_gotohash_js) {
693                ?>
694                <script type="text/javascript">
695                /* <![CDATA[ */
696                window.location.hash = '#<?php echo urlencode($hash); ?>';
697                /* ]]> */
698                </script>
699                <?php
700            }
701        }
702        $this->clearRaisedMessages();
703    }
704
705    /**
706     * Logs messages to defined channels: file, email, sms, and screen. Repeated messages are
707     * not repeated but printed once with count. Log events that match a sendable channel (email or SMS)
708     * are sent once per 'log_multiple_timeout' setting (to avoid a flood of error emails).
709     *
710     * @access public
711     * @param string $message   The text description of the message.
712     * @param int    $priority  The type of message priority (in descending order):
713     *                          LOG_EMERG     0 system is unusable
714     *                          LOG_ALERT     1 action must be taken immediately
715     *                          LOG_CRIT      2 critical conditions
716     *                          LOG_ERR       3 error conditions
717     *                          LOG_WARNING   4 warning conditions
718     *                          LOG_NOTICE    5 normal, but significant, condition
719     *                          LOG_INFO      6 informational message
720     *                          LOG_DEBUG     7 debug-level message
721     * @param string $file      The file where the log event occurs.
722     * @param string $line      The line of the file where the log event occurs.
723     */
724    public function logMsg($message, $priority=LOG_INFO, $file=null, $line=null)
725    {
726        static $previous_events = array();
727
728        // If priority is not specified, assume the worst.
729        if (!$this->logPriorityToString($priority)) {
730            $this->logMsg(sprintf('Log priority %s not defined. (Message: %s)', $priority, $message), LOG_EMERG, $file, $line);
731            $priority = LOG_EMERG;
732        }
733
734        // If log file is not specified, don't log to a file.
735        if (!$this->getParam('log_directory') || !$this->getParam('log_filename') || !is_dir($this->getParam('log_directory')) || !is_writable($this->getParam('log_directory'))) {
736            $this->setParam(array('log_file_priority' => false));
737            // We must use trigger_error to report this problem rather than calling $app->logMsg, which might lead to an infinite loop.
738            trigger_error(sprintf('Codebase error: log directory (%s) not found or writable.', $this->getParam('log_directory')), E_USER_NOTICE);
739        }
740
741        // Before we get any further, let's see if ANY log events are configured to be reported.
742        if ((false === $this->getParam('log_file_priority') || $priority > $this->getParam('log_file_priority'))
743        && (false === $this->getParam('log_email_priority') || $priority > $this->getParam('log_email_priority'))
744        && (false === $this->getParam('log_sms_priority') || $priority > $this->getParam('log_sms_priority'))
745        && (false === $this->getParam('log_screen_priority') || $priority > $this->getParam('log_screen_priority'))) {
746            // This event would not be recorded, skip it entirely.
747            return false;
748        }
749
750        // Strip HTML tags except any with more than 7 characters because that's probably not a HTML tag, e.g. <email@address.com>.
751        preg_match_all('/(<[^>\s]{7,})[^>]*>/', $message, $strip_tags_allow);
752        $message = strip_tags(preg_replace('/\s+/', ' ', $message), (!empty($strip_tags_allow[1]) ? join('> ', $strip_tags_allow[1]) . '>' : null));
753
754        // Serialize multi-line messages.
755        $message = preg_replace('/\s+/m', ' ', $message);
756
757        // Store this event under a unique key, counting each time it occurs so that it only gets reported a limited number of times.
758        $msg_id = md5($message . $priority . $file . $line);
759        if ($this->getParam('log_ignore_repeated_events') && isset($previous_events[$msg_id])) {
760            $previous_events[$msg_id]++;
761            if ($previous_events[$msg_id] == 2) {
762                $this->logMsg(sprintf('%s (Event repeated %s or more times)', $message, $previous_events[$msg_id]), $priority, $file, $line);
763            }
764            return false;
765        } else {
766            $previous_events[$msg_id] = 1;
767        }
768
769        // For email and SMS notification types use "lock" files to prevent sending email and SMS notices ad infinitum.
770        if ((false !== $this->getParam('log_email_priority') && $priority <= $this->getParam('log_email_priority'))
771        || (false !== $this->getParam('log_sms_priority') && $priority <= $this->getParam('log_sms_priority'))) {
772            // This event will generate a "send" notification. Prepare lock file.
773            $site_hash = md5(empty($_SERVER['SERVER_NAME']) ? $_SERVER['SCRIPT_FILENAME'] : $_SERVER['SERVER_NAME']);
774            $lock_dir = $this->getParam('tmp_dir') . "/codebase_msgs_$site_hash/";
775            // Just use the file and line for the msg_id to limit the number of possible messages
776            // (the message string itself shan't be used as it may contain innumerable combinations).
777            $lock_file = $lock_dir . md5($file . ':' . $line);
778            if (!is_dir($lock_dir)) {
779                mkdir($lock_dir);
780            }
781            $send_notifications = true;
782            if (is_file($lock_file)) {
783                $msg_last_sent = filectime($lock_file);
784                // Has this message been sent more recently than the timeout?
785                if ((time() - $msg_last_sent) <= $this->getParam('log_multiple_timeout')) {
786                    // This message was already sent recently.
787                    $send_notifications = false;
788                } else {
789                    // Timeout has expired; send notifications again and reset timeout.
790                    touch($lock_file);
791                }
792            } else {
793                touch($lock_file);
794            }
795        }
796
797        // Make sure to log in the system's locale.
798        $locale = setlocale(LC_TIME, 0);
799        setlocale(LC_TIME, 'C');
800
801        // Data to be stored for a log event.
802        $event = array(
803            'date'      => date('Y-m-d H:i:s'),
804            'remote ip' => getRemoteAddr(),
805            'pid'       => getmypid(),
806            'type'      => $this->logPriorityToString($priority),
807            'file:line' => "$file : $line",
808            'url'       => mb_substr(isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '', 0, 1024),
809            'message'   => $message
810        );
811        // Here's a shortened version of event data.
812        $event_short = $event;
813        $event_short['url'] = truncate($event_short['url'], 120);
814
815        // Restore original locale.
816        setlocale(LC_TIME, $locale);
817
818        // FILE ACTION
819        if (false !== $this->getParam('log_file_priority') && $priority <= $this->getParam('log_file_priority')) {
820            $event_str = '[' . join('] [', $event_short) . ']';
821            error_log(mb_substr($event_str, 0, 1024) . "\n", 3, $this->getParam('log_directory') . '/' . $this->getParam('log_filename'));
822        }
823
824        // EMAIL ACTION
825        if (false !== $this->getParam('log_email_priority') && $priority <= $this->getParam('log_email_priority') && $send_notifications) {
826            $hostname = (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : php_uname('n');
827            $subject = sprintf('[%s %s] %s', $hostname, $event['type'], mb_substr($event['message'], 0, 64));
828            $email_msg = sprintf("A %s log event occurred on %s\n\n", $event['type'], $hostname);
829            $headers = 'From: ' . $this->getParam('site_email');
830            foreach ($event as $k=>$v) {
831                $email_msg .= sprintf("%-11s%s\n", $k, $v);
832            }
833            mb_send_mail($this->getParam('log_to_email_address'), $subject, $email_msg, $headers);
834        }
835
836        // SMS ACTION
837        if (false !== $this->getParam('log_sms_priority') && $priority <= $this->getParam('log_sms_priority') && $send_notifications) {
838            $hostname = (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : php_uname('n');
839            $subject = sprintf('[%s %s]', $hostname, $priority);
840            $sms_msg = sprintf('%s [%s:%s]', mb_substr($event_short['message'], 0, 64), basename($file), $line);
841            $headers = 'From: ' . $this->getParam('site_email');
842            mb_send_mail($this->getParam('log_to_sms_address'), $subject, $sms_msg, $headers);
843        }
844
845        // SCREEN ACTION
846        if (false !== $this->getParam('log_screen_priority') && $priority <= $this->getParam('log_screen_priority')) {
847            file_put_contents('php://stderr', "[{$event['type']}] [{$event['message']}]\n", FILE_APPEND);
848        }
849
850        return true;
851    }
852
853    /**
854     * Returns the string representation of a LOG_* integer constant.
855     *
856     * @param int  $priority  The LOG_* integer constant.
857     *
858     * @return                The string representation of $priority.
859     */
860    public function logPriorityToString($priority) {
861        $priorities = array(
862            LOG_EMERG   => 'emergency',
863            LOG_ALERT   => 'alert',
864            LOG_CRIT    => 'critical',
865            LOG_ERR     => 'error',
866            LOG_WARNING => 'warning',
867            LOG_NOTICE  => 'notice',
868            LOG_INFO    => 'info',
869            LOG_DEBUG   => 'debug'
870        );
871        if (isset($priorities[$priority])) {
872            return $priorities[$priority];
873        } else {
874            return false;
875        }
876    }
877
878    /**
879     * Forcefully set a query argument even if one currently exists in the request.
880     * Values in the _carry_queries array will be copied to URLs (via $app->url()) and
881     * to hidden input values (via printHiddenSession()).
882     *
883     * @access  public
884     * @param   mixed   $query_key  The key (or keys, as an array) of the query argument to save.
885     * @param   mixed   $val        The new value of the argument key.
886     * @author  Quinn Comendant <quinn@strangecode.com>
887     * @since   13 Oct 2007 11:34:51
888     */
889    public function setQuery($query_key, $val)
890    {
891        if (!is_array($query_key)) {
892            $query_key = array($query_key);
893        }
894        foreach ($query_key as $k) {
895            // Set the value of the specified query argument into the _carry_queries array.
896            $this->_carry_queries[$k] = $val;
897        }
898    }
899
900    /**
901     * Specify which query arguments will be carried persistently between requests.
902     * Values in the _carry_queries array will be copied to URLs (via $app->url()) and
903     * to hidden input values (via printHiddenSession()).
904     *
905     * @access  public
906     * @param   mixed   $query_key   The key (or keys, as an array) of the query argument to save.
907     * @param   mixed   $default    If the key is not available, set to this default value.
908     * @author  Quinn Comendant <quinn@strangecode.com>
909     * @since   14 Nov 2005 19:24:52
910     */
911    public function carryQuery($query_key, $default=false)
912    {
913        if (!is_array($query_key)) {
914            $query_key = array($query_key);
915        }
916        foreach ($query_key as $k) {
917            // If not already set, and there is a non-empty value provided in the request...
918            if (!isset($this->_carry_queries[$k]) && false !== getFormData($k, $default)) {
919                // Copy the value of the specified query argument into the _carry_queries array.
920                $this->_carry_queries[$k] = getFormData($k, $default);
921                $this->logMsg(sprintf('Carrying query: %s => %s', $k, truncate(getDump($this->_carry_queries[$k], true), 128, 'end')), LOG_DEBUG, __FILE__, __LINE__);
922            }
923        }
924    }
925
926    /**
927     * dropQuery() is the opposite of carryQuery(). The specified value will not appear in
928     * url()/ohref()/printHiddenSession() modified URLs unless explicitly written in.
929     *
930     * @access  public
931     * @param   mixed   $query_key  The key (or keys, as an array) of the query argument to remove.
932     * @param   bool    $unset      Remove any values set in the request matching the given $query_key.
933     * @author  Quinn Comendant <quinn@strangecode.com>
934     * @since   18 Jun 2007 20:57:29
935     */
936    public function dropQuery($query_key, $unset=false)
937    {
938        if (!is_array($query_key)) {
939            $query_key = array($query_key);
940        }
941        foreach ($query_key as $k) {
942            if (array_key_exists($k, $this->_carry_queries)) {
943                // Remove the value of the specified query argument from the _carry_queries array.
944                $this->logMsg(sprintf('Dropping carried query: %s => %s', $k, $this->_carry_queries[$k]), LOG_DEBUG, __FILE__, __LINE__);
945                unset($this->_carry_queries[$k]);
946            }
947            if ($unset && (isset($_REQUEST) && array_key_exists($k, $_REQUEST))) {
948                unset($_REQUEST[$k], $_GET[$k], $_POST[$k], $_COOKIE[$k]);
949            }
950        }
951    }
952
953    /**
954     * Outputs a fully qualified URL with a query of all the used (ie: not empty)
955     * keys and values, including optional queries. This allows mindless retention
956     * of query arguments across page requests. If cookies are not
957     * used and session_use_trans_sid=true the session id will be propagated in the URL.
958     *
959     * @param  string $url              The initial url
960     * @param  mixed  $carry_args       Additional url arguments to carry in the query,
961     *                                  or FALSE to prevent carrying queries. Can be any of the following formats:
962     *                                      array('key1', key2', key3')  <-- to save these keys if in the form data.
963     *                                      array('key1'=>'value', key2'='value')  <-- to set keys to default values if not present in form data.
964     *                                      false  <-- To not carry any queries. If URL already has queries those will be retained.
965     *
966     * @param  mixed  $always_include_sid  Always add the session id, even if using_trans_sid = true. This is required when
967     *                                     URL starts with http, since PHP using_trans_sid doesn't do those and also for
968     *                                     header('Location...') redirections.
969     *
970     * @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.
971     * @return string url with attached queries and, if not using cookies, the session id
972     */
973    public function url($url, $carry_args=null, $always_include_sid=false, $include_csrf_token=false)
974    {
975        if (!$this->running) {
976            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
977            return false;
978        }
979
980        if ($this->getParam('csrf_token_enabled') && $include_csrf_token) {
981            // Include the csrf_token as a carried query argument.
982            // This token can be validated upon form submission with $app->verifyCSRFToken() or $app->requireValidCSRFToken()
983            $carry_args = is_array($carry_args) ? $carry_args : array();
984            $carry_args = array_merge($carry_args, array($this->getParam('csrf_token_name') => $this->getCSRFToken()));
985        }
986
987        // Get any provided query arguments to include in the final URL.
988        // If FALSE is a provided here, DO NOT carry the queries.
989        $do_carry_queries = true;
990        $one_time_carry_queries = array();
991        if (!is_null($carry_args)) {
992            if (is_array($carry_args) && !empty($carry_args)) {
993                foreach ($carry_args as $key=>$arg) {
994                    // Get query from appropriate source.
995                    if (false === $arg) {
996                        $do_carry_queries = false;
997                    } else if (false !== getFormData($arg, false)) {
998                        $one_time_carry_queries[$arg] = getFormData($arg); // Set arg to form data if available.
999                    } else if (!is_numeric($key) && '' != $arg) {
1000                        $one_time_carry_queries[$key] = getFormData($key, $arg); // Set to arg to default if specified (overwritten by form data).
1001                    }
1002                }
1003            } else if (false !== getFormData($carry_args, false)) {
1004                $one_time_carry_queries[$carry_args] = getFormData($carry_args);
1005            } else if (false === $carry_args) {
1006                $do_carry_queries = false;
1007            }
1008        }
1009
1010        // Get the first delimiter that is needed in the url.
1011        $delim = mb_strpos($url, '?') !== false ? ini_get('arg_separator.output') : '?';
1012
1013        $q = '';
1014        if ($do_carry_queries) {
1015            // Join the global _carry_queries and local one_time_carry_queries.
1016            $query_args = urlEncodeArray(array_merge($this->_carry_queries, $one_time_carry_queries));
1017            foreach ($query_args as $key=>$val) {
1018                // Check value is set and value does not already exist in the url.
1019                if (!preg_match('/[?&]' . preg_quote($key) . '=/', $url)) {
1020                    $q .= $delim . $key . '=' . $val;
1021                    $delim = ini_get('arg_separator.output');
1022                }
1023            }
1024        }
1025
1026        // Pop off any named anchors to push them back on after appending additional query args.
1027        $parts = explode('#', $url, 2);
1028        $url = $parts[0];
1029        $anchor = isset($parts[1]) ? $parts[1] : '';
1030
1031        // $anchor =
1032
1033        // Include the necessary SID if the following is true:
1034        // - no cookie in http request OR cookies disabled in App
1035        // - sessions are enabled
1036        // - the link stays on our site
1037        // - transparent SID propagation with session.use_trans_sid is not being used OR url begins with protocol (using_trans_sid has no effect here)
1038        // OR
1039        // - 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)
1040        // AND
1041        // - the SID is not already in the query.
1042        if (
1043            (
1044                (
1045                    (
1046                        !isset($_COOKIE[session_name()])
1047                        || !$this->getParam('session_use_cookies')
1048                    )
1049                    && $this->getParam('session_use_trans_sid')
1050                    && $this->getParam('enable_session')
1051                    && isMyDomain($url)
1052                    && (
1053                        !ini_get('session.use_trans_sid')
1054                        || preg_match('!^(http|https)://!i', $url)
1055                    )
1056                )
1057                || $always_include_sid
1058            )
1059            && !preg_match('/[?&]' . preg_quote(session_name()) . '=/', $url)
1060        ) {
1061            $url = sprintf('%s%s%s%s=%s%s', $url, $q, $delim, session_name(), session_id(), ('' == $anchor ? '' : "#$anchor"));
1062        } else {
1063            $url = sprintf('%s%s%s', $url, $q, ('' == $anchor ? '' : "#$anchor"));
1064        }
1065
1066        return $url;
1067    }
1068
1069    /**
1070     * Returns a HTML-friendly URL processed with $app->url and & replaced with &amp;
1071     *
1072     * @access  public
1073     * @param   (see param reference for url() method)
1074     * @return  string          URL passed through $app->url() with ampersands transformed to $amp;
1075     * @author  Quinn Comendant <quinn@strangecode.com>
1076     * @since   09 Dec 2005 17:58:45
1077     */
1078    public function oHREF($url, $carry_args=null, $always_include_sid=false, $include_csrf_token=false)
1079    {
1080        // Process the URL.
1081        $url = $this->url($url, $carry_args, $always_include_sid, $include_csrf_token);
1082
1083        // Replace any & not followed by an html or unicode entity with its &amp; equivalent.
1084        $url = preg_replace('/&(?![\w\d#]{1,10};)/', '&amp;', $url);
1085
1086        return $url;
1087    }
1088
1089    /**
1090     * Prints a hidden form element with the PHPSESSID when cookies are not used, as well
1091     * as hidden form elements for GET_VARS that might be in use.
1092     *
1093     * @param  mixed  $carry_args        Additional url arguments to carry in the query,
1094     *                                   or FALSE to prevent carrying queries. Can be any of the following formats:
1095     *                                      array('key1', key2', key3')  <-- to save these keys if in the form data.
1096     *                                      array('key1'=>'value', key2'='value')  <-- to set keys to default values if not present in form data.
1097     *                                      false  <-- To not carry any queries. If URL already has queries those will be retained.
1098     * @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.
1099     */
1100    public function printHiddenSession($carry_args=null, $include_csrf_token=false)
1101    {
1102        if (!$this->running) {
1103            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
1104            return false;
1105        }
1106
1107        // Get any provided query arguments to include in the final hidden form data.
1108        // If FALSE is a provided here, DO NOT carry the queries.
1109        $do_carry_queries = true;
1110        $one_time_carry_queries = array();
1111        if (!is_null($carry_args)) {
1112            if (is_array($carry_args) && !empty($carry_args)) {
1113                foreach ($carry_args as $key=>$arg) {
1114                    // Get query from appropriate source.
1115                    if (false === $arg) {
1116                        $do_carry_queries = false;
1117                    } else if (false !== getFormData($arg, false)) {
1118                        $one_time_carry_queries[$arg] = getFormData($arg); // Set arg to form data if available.
1119                    } else if (!is_numeric($key) && '' != $arg) {
1120                        $one_time_carry_queries[$key] = getFormData($key, $arg); // Set to arg to default if specified (overwritten by form data).
1121                    }
1122                }
1123            } else if (false !== getFormData($carry_args, false)) {
1124                $one_time_carry_queries[$carry_args] = getFormData($carry_args);
1125            } else if (false === $carry_args) {
1126                $do_carry_queries = false;
1127            }
1128        }
1129
1130        // For each existing request value, we create a hidden input to carry it through a form.
1131        if ($do_carry_queries) {
1132            // Join the global _carry_queries and local one_time_carry_queries.
1133            // urlencode is not used here, not for form data!
1134            $query_args = array_merge($this->_carry_queries, $one_time_carry_queries);
1135            foreach ($query_args as $key => $val) {
1136                if (is_array($val)) {
1137                    foreach ($val as $subval) {
1138                        printf('<input type="hidden" name="%s[]" value="%s" />', $key, $subval);
1139                    }
1140                } else {
1141                    printf('<input type="hidden" name="%s" value="%s" />', $key, $val);
1142                }
1143            }
1144            unset($query_args, $key, $val, $subval);
1145        }
1146
1147        // Include the SID if:
1148        // * cookies are disabled
1149        // * the system isn't automatically adding trans_sid
1150        // * the session is enabled
1151        // * and we're configured to use trans_sid
1152        if (!isset($_COOKIE[session_name()])
1153        && !ini_get('session.use_trans_sid')
1154        && $this->getParam('enable_session')
1155        && $this->getParam('session_use_trans_sid')
1156        ) {
1157            printf('<input type="hidden" name="%s" value="%s" />', session_name(), session_id());
1158        }
1159
1160        // Include the csrf_token in the form.
1161        // This token can be validated upon form submission with $app->verifyCSRFToken() or $app->requireValidCSRFToken()
1162        if ($this->getParam('csrf_token_enabled') && $include_csrf_token) {
1163            printf('<input type="hidden" name="%s" value="%s" />', $this->getParam('csrf_token_name'), $this->getCSRFToken());
1164        }
1165    }
1166
1167    /*
1168    * 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
1169    *
1170    * @access   public
1171    * @param    string  $url    URL to media (e.g., /foo.js)
1172    * @return   string          URL with cache-busting version appended (/foo.js?v=1234567890)
1173    * @author   Quinn Comendant <quinn@strangecode.com>
1174    * @version  1.0
1175    * @since    03 Sep 2014 22:40:24
1176    */
1177    public function cacheBustURL($url)
1178    {
1179        // Get the first delimiter that is needed in the url.
1180        $delim = mb_strpos($url, '?') !== false ? ini_get('arg_separator.output') : '?';
1181        $v = crc32($this->getParam('codebase_version') . '|' . $this->getParam('site_version'));
1182        return sprintf('%s%sv=%s', $url, $delim, $v);
1183    }
1184
1185    /*
1186    * Generate a csrf_token if it doesn't exist or is expired, save it to the session and return its value.
1187    * Otherwise just return the current token.
1188    * Details on the synchronizer token pattern:
1189    * https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#General_Recommendation:_Synchronizer_Token_Pattern
1190    *
1191    * @access   public
1192    * @return   string The new or current csrf_token
1193    * @author   Quinn Comendant <quinn@strangecode.com>
1194    * @version  1.0
1195    * @since    15 Nov 2014 17:57:17
1196    */
1197    public function getCSRFToken()
1198    {
1199        if (!isset($_SESSION['_app'][$this->_ns]['csrf_token']) || (removeSignature($_SESSION['_app'][$this->_ns]['csrf_token']) + $this->getParam('csrf_token_timeout') < time())) {
1200            // No token, or token is expired; generate one and return it.
1201            return $_SESSION['_app'][$this->_ns]['csrf_token'] = addSignature(time(), null, 64);
1202        }
1203        // Current token is not expired; return it.
1204        return $_SESSION['_app'][$this->_ns]['csrf_token'];
1205    }
1206
1207    /*
1208    * Compares the given csrf_token with the current or previous one saved in the session.
1209    *
1210    * @access   public
1211    * @param    string  $user_submitted_csrf_token The user-submitted token to compare with the session token.
1212    * @param    string  $csrf_token     The token to compare with the session token.
1213    * @return   bool    True if the tokens match, false otherwise.
1214    * @author   Quinn Comendant <quinn@strangecode.com>
1215    * @version  1.0
1216    * @since    15 Nov 2014 18:06:55
1217    */
1218    public function verifyCSRFToken($user_submitted_csrf_token)
1219    {
1220
1221        if (!$this->getParam('csrf_token_enabled')) {
1222            $this->logMsg(sprintf('%s called, but csrf_token_enabled=false', __METHOD__), LOG_ERR, __FILE__, __LINE__);
1223            return true;
1224        }
1225        if ('' == trim($user_submitted_csrf_token)) {
1226            $this->logMsg(sprintf('Empty string failed CSRF verification.', null), LOG_NOTICE, __FILE__, __LINE__);
1227            return false;
1228        }
1229        if (!verifySignature($user_submitted_csrf_token, null, 64)) {
1230            $this->logMsg(sprintf('Input failed CSRF verification (invalid signature in %s).', $user_submitted_csrf_token), LOG_WARNING, __FILE__, __LINE__);
1231            return false;
1232        }
1233        $csrf_token = $this->getCSRFToken();
1234        if ($user_submitted_csrf_token != $csrf_token) {
1235            $this->logMsg(sprintf('Input failed CSRF verification (%s not in %s).', $user_submitted_csrf_token, $csrf_token), LOG_WARNING, __FILE__, __LINE__);
1236            return false;
1237        }
1238        $this->logMsg(sprintf('Verified CSRF token %s', $user_submitted_csrf_token), LOG_DEBUG, __FILE__, __LINE__);
1239        return true;
1240    }
1241
1242    /*
1243    * Bounce user if they submit a token that doesn't match the one saved in the session.
1244    * Because this function calls dieURL() it must be called before any other HTTP header output.
1245    *
1246    * @access   public
1247    * @param    string  $message    Optional message to display to the user (otherwise default message will display). Set to an empty string to display no message.
1248    * @param    int    $type    The type of message: MSG_NOTICE,
1249    *                           MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
1250    * @param    string $file    __FILE__.
1251    * @param    string $line    __LINE__.
1252    * @return   void
1253    * @author   Quinn Comendant <quinn@strangecode.com>
1254    * @version  1.0
1255    * @since    15 Nov 2014 18:10:17
1256    */
1257    public function requireValidCSRFToken($message=null, $type=MSG_NOTICE, $file=null, $line=null)
1258    {
1259        if (!$this->verifyCSRFToken(getFormData($this->getParam('csrf_token_name')))) {
1260            $message = isset($message) ? $message : _("Sorry, the form token expired. Please try again.");
1261            $this->raiseMsg($message, $type, $file, $line);
1262            $this->dieBoomerangURL();
1263        }
1264    }
1265
1266    /**
1267     * Uses an http header to redirect the client to the given $url. If sessions are not used
1268     * and the session is not already defined in the given $url, the SID is appended as a URI query.
1269     * As with all header generating functions, make sure this is called before any other output.
1270     *
1271     * @param   string  $url                    The URL the client will be redirected to.
1272     * @param   mixed   $carry_args             Additional url arguments to carry in the query,
1273     *                                          or FALSE to prevent carrying queries. Can be any of the following formats:
1274     *                                          -array('key1', key2', key3')  <-- to save these keys if in the form data.
1275     *                                          -array('key1' => 'value', key2' => 'value')  <-- to set keys to default values if not present in form data.
1276     *                                          -false  <-- To not carry any queries. If URL already has queries those will be retained.
1277     * @param   bool    $always_include_sid     Force session id to be added to Location header.
1278     */
1279    public function dieURL($url, $carry_args=null, $always_include_sid=false)
1280    {
1281        if (!$this->running) {
1282            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
1283            return false;
1284        }
1285
1286        if (!$url) {
1287            // If URL is not specified, use the redirect_home_url.
1288            $url = $this->getParam('redirect_home_url');
1289        }
1290
1291        if (preg_match('!^/!', $url)) {
1292            // If relative URL is given, prepend correct local hostname.
1293            $scheme = 'on' == getenv('HTTPS') ? 'https' : 'http';
1294            $host = getenv('HTTP_HOST');
1295            $url = sprintf('%s://%s%s', $scheme, $host, $url);
1296        }
1297
1298        $url = $this->url($url, $carry_args, $always_include_sid);
1299
1300        // Should we send a "303 See Other" header here instead of relying on the 302 sent automatically by PHP?
1301        if (!headers_sent($h_file, $h_line)) {
1302            header(sprintf('Location: %s', $url));
1303            $this->logMsg(sprintf('dieURL: %s', $url), LOG_DEBUG, __FILE__, __LINE__);
1304        } else {
1305            // Fallback: die using meta refresh instead.
1306            printf('<meta http-equiv="refresh" content="0;url=%s" />', $url);
1307            $this->logMsg(sprintf('dieURL (refresh): %s; headers already sent (output started in %s : %s)', $url, $h_file, $h_line), LOG_NOTICE, __FILE__, __LINE__);
1308        }
1309
1310        // End application.
1311        // Recommended, although I'm not sure it's necessary: http://cn2.php.net/session_write_close
1312        $this->stop();
1313        die;
1314    }
1315
1316    /*
1317    * Redirects a user by calling $app->dieURL(). It will use:
1318    * 1. the stored boomerang URL, it it exists
1319    * 2. a specified $default_url, it it exists
1320    * 3. the referring URL, it it exists.
1321    * 4. redirect_home_url configuration variable.
1322    *
1323    * @access   public
1324    * @param    string  $id             Identifier for this script.
1325    * @param    mixed   $carry_args     Additional arguments to carry in the URL automatically (see $app->oHREF()).
1326    * @param    string  $default_url    A default URL if there is not a valid specified boomerang URL.
1327    * @param    bool    $queryless_referrer_comparison   Exclude the URL query from the refererIsMe() comparison.
1328    * @return   bool                    False if the session is not running. No return otherwise.
1329    * @author   Quinn Comendant <quinn@strangecode.com>
1330    * @since    31 Mar 2006 19:17:00
1331    */
1332    public function dieBoomerangURL($id=null, $carry_args=null, $default_url=null, $queryless_referrer_comparison=false)
1333    {
1334        if (!$this->running) {
1335            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
1336            return false;
1337        }
1338
1339        // Get URL from stored boomerang. Allow non specific URL if ID not valid.
1340        if ($this->validBoomerangURL($id, true)) {
1341            if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1342                $url = $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id];
1343                $this->logMsg(sprintf('dieBoomerangURL(%s) found: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1344            } else {
1345                $url = end($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1346                $this->logMsg(sprintf('dieBoomerangURL(%s) using: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1347            }
1348            // Delete stored boomerang.
1349            $this->deleteBoomerangURL($id);
1350        } else if (isset($default_url)) {
1351            $url = $default_url;
1352        } else if (!refererIsMe(true === $queryless_referrer_comparison) && '' != ($url = getenv('HTTP_REFERER'))) {
1353            // Ensure that the redirecting page is not also the referrer.
1354            $this->logMsg(sprintf('dieBoomerangURL(%s) using referrer: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1355        } else {
1356            // If URL is not specified, use the redirect_home_url.
1357            $url = $this->getParam('redirect_home_url');
1358            $this->logMsg(sprintf('dieBoomerangURL(%s) using redirect_home_url: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1359        }
1360
1361        // A redirection will never happen immediately twice.
1362        // Set the time so ensure this doesn't happen.
1363        $_SESSION['_app'][$this->_ns]['boomerang']['time'] = time();
1364        $this->dieURL($url, $carry_args);
1365    }
1366
1367    /**
1368     * Set the URL to return to when $app->dieBoomerangURL() is called.
1369     *
1370     * @param string  $url  A fully validated URL.
1371     * @param bool  $id     An identification tag for this url.
1372     * FIXME: url garbage collection?
1373     */
1374    public function setBoomerangURL($url=null, $id=null)
1375    {
1376        if (!$this->running) {
1377            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
1378            return false;
1379        }
1380        // A redirection will never happen immediately after setting the boomerangURL.
1381        // Set the time so ensure this doesn't happen. See $app->validBoomerangURL for more.
1382
1383        if ('' != $url && is_string($url)) {
1384            // Delete any boomerang request keys in the query string (along with any trailing delimiters after the deletion).
1385            $url = preg_replace(array('/([&?])boomerang=[^&?]+[&?]?/', '/[&?]$/'), array('$1', ''), $url);
1386
1387            if (isset($_SESSION['_app'][$this->_ns]['boomerang']['url']) && is_array($_SESSION['_app'][$this->_ns]['boomerang']['url']) && !empty($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1388                // If the ID=>URL pair currently exists in the boomerang array, delete.
1389                foreach (array_keys($_SESSION['_app'][$this->_ns]['boomerang']['url'], $url) as $existing_key) {
1390                    if ($existing_key == $id) {
1391                        $this->logMsg(sprintf('Found and deleting existing ID=>URL pair: %s=>%s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1392                        unset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$existing_key]);
1393                    }
1394                }
1395            }
1396
1397            if (isset($id)) {
1398                $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id] = $url;
1399            } else {
1400                $_SESSION['_app'][$this->_ns]['boomerang']['url'][] = $url;
1401            }
1402            $this->logMsg(sprintf('setBoomerangURL(%s): %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1403            return true;
1404        } else {
1405            $this->logMsg(sprintf('setBoomerangURL(%s) is empty!', $id, $url), LOG_NOTICE, __FILE__, __LINE__);
1406            return false;
1407        }
1408    }
1409
1410    /**
1411     * Return the URL set for the specified $id, or an empty string if one isn't set.
1412     *
1413     * @param string  $id     An identification tag for this url.
1414     */
1415    public function getBoomerangURL($id=null)
1416    {
1417        if (!$this->running) {
1418            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
1419            return false;
1420        }
1421
1422        if (isset($id)) {
1423            if (isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1424                return $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id];
1425            } else {
1426                return '';
1427            }
1428        } else if (is_array($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1429            return end($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1430        } else {
1431            return false;
1432        }
1433    }
1434
1435    /**
1436     * Delete the URL set for the specified $id.
1437     *
1438     * @param string  $id     An identification tag for this url.
1439     */
1440    public function deleteBoomerangURL($id=null)
1441    {
1442        if (!$this->running) {
1443            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
1444            return false;
1445        }
1446
1447        if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1448            $url = $this->getBoomerangURL($id);
1449            unset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id]);
1450        } else if (is_array($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1451            $url = array_pop($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1452        }
1453        $this->logMsg(sprintf('deleteBoomerangURL(%s): %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1454    }
1455
1456    /**
1457     * Check if a valid boomerang URL value has been set. A boomerang URL is considered
1458     * valid if: 1) it is not empty, 2) it is not the current URL, and 3) has not been accessed within n seconds.
1459     *
1460     * @return bool  True if it is set and valid, false otherwise.
1461     */
1462    public function validBoomerangURL($id=null, $use_nonspecificboomerang=false)
1463    {
1464        if (!$this->running) {
1465            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
1466            return false;
1467        }
1468
1469        if (!isset($_SESSION['_app'][$this->_ns]['boomerang']['url'])) {
1470            $this->logMsg(sprintf('validBoomerangURL(%s) no boomerang URL set.', $id), LOG_DEBUG, __FILE__, __LINE__);
1471            return false;
1472        }
1473
1474        // Time is the time stamp of a boomerangURL redirection, or setting of a boomerangURL.
1475        // a boomerang redirection will always occur at least several seconds after the last boomerang redirect
1476        // or a boomerang being set.
1477        $boomerang_time = isset($_SESSION['_app'][$this->_ns]['boomerang']['time']) ? $_SESSION['_app'][$this->_ns]['boomerang']['time'] : 0;
1478
1479        $url = '';
1480        if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang']['url'][$id])) {
1481            $url = $_SESSION['_app'][$this->_ns]['boomerang']['url'][$id];
1482        } else if (!isset($id) || $use_nonspecificboomerang) {
1483            // Use non specific boomerang if available.
1484            $url = end($_SESSION['_app'][$this->_ns]['boomerang']['url']);
1485        }
1486
1487        $this->logMsg(sprintf('validBoomerangURL(%s) testing: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1488
1489        if ('' == $url) {
1490            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, empty!', $id), LOG_DEBUG, __FILE__, __LINE__);
1491            return false;
1492        }
1493        if ($url == absoluteMe()) {
1494            // The URL we are directing to is the current page.
1495            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, same as absoluteMe: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1496            return false;
1497        }
1498        if ($boomerang_time >= (time() - 2)) {
1499            // Last boomerang direction was less than 2 seconds ago.
1500            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, boomerang_time too short: %s seconds', $id, time() - $boomerang_time), LOG_DEBUG, __FILE__, __LINE__);
1501            return false;
1502        }
1503        if ($boomerang_time < (time() - 72000)) {
1504            // Last boomerang direction was more than 20 minutes ago.
1505            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, boomerang_time too long: %s seconds', $id, time() - $boomerang_time), LOG_DEBUG, __FILE__, __LINE__);
1506            return false;
1507        }
1508
1509        $this->logMsg(sprintf('validBoomerangURL(%s) is valid: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
1510        return true;
1511    }
1512
1513    /**
1514     * Force the user to connect via https (port 443) by redirecting them to
1515     * the same page but with https.
1516     */
1517    public function sslOn()
1518    {
1519        if (function_exists('apache_get_modules')) {
1520            $modules = apache_get_modules();
1521        } else {
1522            // It's safe to assume we have mod_ssl if we can't determine otherwise.
1523            $modules = array('mod_ssl');
1524        }
1525
1526        if ('' == getenv('HTTPS') && $this->getParam('ssl_enabled') && in_array('mod_ssl', $modules)) {
1527            $this->raiseMsg(sprintf(_("Secure SSL connection made to %s"), $this->getParam('ssl_domain')), MSG_NOTICE, __FILE__, __LINE__);
1528            // Always append session because some browsers do not send cookie when crossing to SSL URL.
1529            $this->dieURL('https://' . $this->getParam('ssl_domain') . getenv('REQUEST_URI'), null, true);
1530        }
1531    }
1532
1533    /**
1534     * to enforce the user to connect via http (port 80) by redirecting them to
1535     * a http version of the current url.
1536     */
1537    public function sslOff()
1538    {
1539        if ('' != getenv('HTTPS')) {
1540            $this->dieURL('http://' . getenv('HTTP_HOST') . getenv('REQUEST_URI'), null, true);
1541        }
1542    }
1543
1544    /*
1545    * Sets a cookie, with error checking and some sane defaults.
1546    *
1547    * @access   public
1548    * @param    string  $name       The name of the cookie.
1549    * @param    string  $value      The value of the cookie.
1550    * @param    string  $expire     The time the cookie expires, as a unix timestamp or string value passed to strtotime.
1551    * @param    string  $path       The path on the server in which the cookie will be available on.
1552    * @param    string  $domain     The domain that the cookie is available to.
1553    * @param    bool    $secure     Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client.
1554    * @param    bool    $httponly   When TRUE the cookie will be made accessible only through the HTTP protocol (makes cookies unreadable to javascript).
1555    * @return   bool                True on success, false on error.
1556    * @author   Quinn Comendant <quinn@strangecode.com>
1557    * @version  1.0
1558    * @since    02 May 2014 16:36:34
1559    */
1560    public function setCookie($name, $value, $expire='+10 years', $path='/', $domain=null, $secure=null, $httponly=null)
1561    {
1562        if (!is_scalar($name)) {
1563            $this->logMsg(sprintf('Cookie name must be scalar, is not: %s', getDump($name)), LOG_NOTICE, __FILE__, __LINE__);
1564            return false;
1565        }
1566        if (!is_scalar($value)) {
1567            $this->logMsg(sprintf('Cookie "%s" value must be scalar, is not: %s', $name, getDump($value)), LOG_NOTICE, __FILE__, __LINE__);
1568            return false;
1569        }
1570
1571        // Defaults.
1572        $expire = (is_numeric($expire) ? $expire : (is_string($expire) ? strtotime($expire) : $expire));
1573        $secure = $secure ?: ('' != getenv('HTTPS') && $this->getParam('ssl_enabled'));
1574        $httponly = $httponly ?: true;
1575
1576        // Make sure the expiration date is a valid 32bit integer.
1577        if (is_int($expire) && $expire > 2147483647) {
1578            $this->logMsg(sprintf('Cookie "%s" expire time exceeds a 32bit integer (%s)', $key, date('r', $expire)), LOG_NOTICE, __FILE__, __LINE__);
1579        }
1580
1581        // Measure total cookie length and warn if larger than max recommended size of 4093.
1582        // https://stackoverflow.com/questions/640938/what-is-the-maximum-size-of-a-web-browsers-cookies-key
1583        // The date the header name include 51 bytes: Set-Cookie: ; expires=Fri, 03-May-2024 00:04:47 GMT
1584        $cookielen = strlen($name . $value . $path . $domain . ($secure ? '; secure' : '') . ($httponly ? '; httponly' : '')) + 51;
1585        if ($cookielen > 4093) {
1586            $this->logMsg(sprintf('Cookie "%s" has a size greater than 4093 bytes (is %s bytes)', $key, $cookielen), LOG_NOTICE, __FILE__, __LINE__);
1587        }
1588
1589        // Ensure PHP version allow use of httponly.
1590        if (version_compare(PHP_VERSION, '5.2.0', '>=')) {
1591            $ret = setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
1592        } else {
1593            $ret = setcookie($name, $value, $expire, $path, $domain, $secure);
1594        }
1595
1596        if (false === $ret) {
1597            $this->logMsg(sprintf('Failed to set cookie (%s=%s) probably due to output before headers.', $name, $value), LOG_NOTICE, __FILE__, __LINE__);
1598        }
1599        return $ret;
1600    }
1601} // End.
Note: See TracBrowser for help on using the repository browser.