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

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

Enabled App to load version from json file. Lock URL fixed to permit additional query arguments. Return array instead of object for Prefs cookie json.

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

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