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

Last change on this file since 562 was 562, checked in by anonymous, 8 years ago

Added log_message_max_length param

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

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