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

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

Added common config for codebase cli scripts. Changed behavior of db_auth.json loading. validSignature() now fails on empty string.

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

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