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

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

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