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

Last change on this file since 658 was 657, checked in by anonymous, 5 years ago

Update timezone test regex to support edge cases

File size: 79.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    // Instance of database session handler object.
60    public $db_session;
61
62    // Array of query arguments will be carried persistently between requests.
63    protected $_carry_queries = array();
64
65    // Array of raised message counters.
66    protected $_raised_msg_counter = array(MSG_NOTICE => 0, MSG_SUCCESS => 0, MSG_WARNING => 0, MSG_ERR => 0);
67
68    // We're running as CLI. Public because we must force this as false when testing sessions via CLI.
69    public $cli = false;
70
71    // Dictionary of global application parameters.
72    protected $_params = array();
73
74    // Default parameters.
75    protected $_param_defaults = array(
76
77        // Public name and email address for this application.
78        'site_name' => null,
79        'site_email' => '', // Set to no-reply@HTTP_HOST if not set here.
80        'site_url' => '', // URL to the root of the site (created during App->start()).
81        'page_url' => '', // URL to the current page (created during App->start()).
82        'images_path' => '', // Location for codebase-generated interface widgets (ex: "/admin/i").
83        'site_version' => '', // Version of this application (set automatically during start() if site_version_file is used).
84        'site_version_file' => 'docs/version.txt', // File containing version number of this app, relative to the include path.
85
86        // The location the user will go if the system doesn't know where else to send them.
87        'redirect_home_url' => '/',
88
89        // SSL URL used when redirecting with $app->sslOn().
90        'ssl_domain' => null,
91        'ssl_enabled' => false,
92
93        // Use CSRF tokens. See notes in the getCSRFToken() method.
94        'csrf_token_enabled' => true,
95        // Form tokens will expire after this duration, in seconds.
96        'csrf_token_timeout' => 259200, // 259200 seconds = 3 days.
97        'csrf_token_name' => 'csrf_token',
98
99        // HMAC signing method
100        'signing_method' => 'sha512+base64',
101
102        // Content type of output sent in the Content-type: http header.
103        'content_type' => 'text/html',
104
105        // Allow HTTP caching with max-age setting. Possible values:
106        //  >= 1    Allow HTTP caching with this value set as the max-age (in seconds, i.e., 3600 = 1 hour).
107        //  0       Disallow HTTP caching.
108        //  false   Don't send any cache-related HTTP headers (if you want to control this via server config or custom headers)
109        // This should be '0' for websites that use authentication or have frequently changing dynamic content.
110        'http_cache_headers' => 0,
111
112        // Character set for page output. Used in the Content-Type header and the HTML <meta content-type> tag.
113        'character_set' => 'utf-8',
114
115        // Human-readable format used to display dates.
116        'date_format' => 'd M Y',
117        'time_format' => 'h:i:s A',
118        'sql_date_format' => '%e %b %Y',
119        'sql_time_format' => '%k:%i',
120
121        // Use php sessions?
122        'enable_session' => false,
123        'session_name' => '_session',
124        'session_use_cookies' => true,
125
126        // Pass the session-id through URLs if cookies are not enabled?
127        // Disable this to prevent session ID theft.
128        'session_use_trans_sid' => false,
129
130        // Use database?
131        'enable_db' => false,
132
133        // Use db-based sessions?
134        'enable_db_session_handler' => false,
135
136        // DB credentials should be set as apache environment variables in httpd.conf, readable only by root.
137        'db_server' => null,
138        'db_name' => null,
139        'db_user' => null,
140        'db_pass' => null,
141
142        // And for CLI scripts, which should include a JSON file at this specified location in the include path.
143        'db_auth_file' => 'db_auth.json',
144
145        // Database debugging.
146        'db_always_debug' => false, // TRUE = display all SQL queries.
147        'db_debug' => false, // TRUE = display db errors.
148        'db_die_on_failure' => false, // TRUE = script stops on db error.
149
150        // For classes that require db tables, do we check that a table exists and create if missing?
151        'db_create_tables' => true,
152
153        // The level of error reporting. Don't change this to suppress messages, instead use display_errors to control display.
154        'error_reporting' => E_ALL,
155
156        // Don't display errors by default; it is preferable to log them to a file. For CLI scripts, set this to the string 'stderr'.
157        'display_errors' => false,
158
159        // Directory in which to store log files.
160        'log_directory' => '',
161
162        // PHP error log.
163        'php_error_log' => 'php_error_log',
164
165        // General application log.
166        'log_filename' => 'app_log',
167
168        // Don't email or SMS duplicate messages that happen more often than this value (in seconds).
169        'log_multiple_timeout' => 3600, // Hourly
170
171        // Logging priority can be any of the following, or false to deactivate:
172        // LOG_EMERG     system is unusable
173        // LOG_ALERT     action must be taken immediately
174        // LOG_CRIT      critical conditions
175        // LOG_ERR       error conditions
176        // LOG_WARNING   warning conditions
177        // LOG_NOTICE    normal, but significant, condition
178        // LOG_INFO      informational message
179        // LOG_DEBUG     debug-level message
180        'log_file_priority' => LOG_INFO,
181        'log_email_priority' => false,
182        'log_sms_priority' => false,
183        'log_screen_priority' => false,
184
185        // Email address to receive log event emails. Use multiple addresses by separating them with commas.
186        'log_to_email_address' => null,
187
188        // SMS Email address to receive log event SMS messages. Use multiple addresses by separating them with commas.
189        'log_to_sms_address' => null,
190
191        // 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.
192        'log_ignore_repeated_events' => true,
193
194        // Maximum length of log messages, in bytes.
195        'log_message_max_length' => 1024,
196
197        // Strip line-endings from log messages.
198        'log_serialize' => true,
199
200        // Temporary files directory.
201        'tmp_dir' => '/tmp',
202
203        // A key for calculating simple cryptographic signatures. Set using as an environment variables in the httpd.conf with 'SetEnv SIGNING_KEY <key>'.
204        // Existing password hashes rely on the same key/salt being used to compare encryptions.
205        // Don't change this unless you know existing hashes or signatures will not be affected!
206        'signing_key' => 'aae6abd6209d82a691a9f96384a7634a',
207
208        // Force getFormData, getPost, and getGet to always run dispelMagicQuotes() with stripslashes().
209        // This should be set to 'true' when using the codebase with Wordpress because
210        // WP forcefully adds slashes to all input despite the setting of magic_quotes_gpc.
211        'always_dispel_magicquotes' => false,
212    );
213
214    /**
215     * Constructor.
216     */
217    public function __construct($namespace='')
218    {
219        // Initialize default parameters.
220        $this->_params = array_merge($this->_params, array('namespace' => $namespace), $this->_param_defaults);
221
222        // Begin timing script.
223        require_once dirname(__FILE__) . '/ScriptTimer.inc.php';
224        $this->timer = new ScriptTimer();
225        $this->timer->start('_app');
226
227        // Are we running as a CLI?
228        $this->cli = ('cli' === php_sapi_name() || defined('_CLI'));
229    }
230
231    /**
232     * This method enforces the singleton pattern for this class. Only one application is running at a time.
233     *
234     * $param   string  $namespace  Name of this application.
235     * @return  object  Reference to the global Cache object.
236     * @access  public
237     * @static
238     */
239    public static function &getInstance($namespace='')
240    {
241        if (self::$instance === null) {
242            // FIXME: Yep, having a namespace with one singleton instance is not very useful. This is a design flaw with the Codebase.
243            // We're currently getting instances of App throughout the codebase using `$app =& App::getInstance();`
244            // with no way to determine what the namespace of the containing application is (e.g., `public` vs. `admin`).
245            // Option 1: provide the project namespace to all classes that use App, and then instantiate with `$app =& App::getInstance($this->_ns);`.
246            // In this case the namespace of the App and the namespace of the auxiliary class must match.
247            // Option 2: may be to clone a specific instance to the "default" instance, so, e.g., `$app =& App::getInstance();`
248            // refers to the same namespace as `$app =& App::getInstance('admin');`
249            // Option 3: is to check if there is only one instance, and return it if an unspecified namespace is requested.
250            // However, in the case when multiple namespaces are in play at the same time, this will fail; unspecified namespaces
251            // would cause the creation of an additional instance, since there would not be an obvious named instance to return.
252            self::$instance = new self($namespace);
253        }
254
255        if ('' != $namespace) {
256            // We may not be able to request a specific instance, but we can specify a specific namespace.
257            // We're ignoring all instance requests with a blank namespace, so we use the last given one.
258            self::$instance->_ns = $namespace;
259        }
260
261        return self::$instance;
262    }
263
264    /**
265     * Set (or overwrite existing) parameters by passing an array of new parameters.
266     *
267     * @access public
268     * @param  array    $param     Array of parameters (key => val pairs).
269     */
270    public function setParam($param=null)
271    {
272        if (isset($param) && is_array($param)) {
273            // Merge new parameters with old overriding old ones that are passed.
274            $this->_params = array_merge($this->_params, $param);
275
276            if ($this->running) {
277                // Params that require additional processing if set during runtime.
278                foreach ($param as $key => $val) {
279                    switch ($key) {
280                    case 'namespace':
281                        $this->logMsg(sprintf('Setting namespace not allowed', null), LOG_WARNING, __FILE__, __LINE__);
282                        return false;
283
284                    case 'session_name':
285                        session_name($val);
286                        return true;
287
288                    case 'session_use_cookies':
289                        ini_set('session.use_cookies', $val);
290                        return true;
291
292                    case 'error_reporting':
293                        ini_set('error_reporting', $val);
294                        return true;
295
296                    case 'display_errors':
297                        ini_set('display_errors', $val);
298                        return true;
299
300                    case 'log_errors':
301                        ini_set('log_errors', true);
302                        return true;
303
304                    case 'log_directory':
305                        if (is_dir($val) && is_writable($val)) {
306                            ini_set('error_log', $val . '/' . $this->getParam('php_error_log'));
307                        }
308                        return true;
309                    }
310                }
311            }
312        }
313    }
314
315    /**
316     * Return the value of a parameter.
317     *
318     * @access  public
319     * @param   string  $param      The key of the parameter to return.
320     * @return  mixed               Parameter value, or null if not existing.
321     */
322    public function getParam($param=null)
323    {
324        if ($param === null) {
325            return $this->_params;
326        } else if (array_key_exists($param, $this->_params)) {
327            return $this->_params[$param];
328        } else {
329            return null;
330        }
331    }
332
333    /**
334     * Begin running this application.
335     *
336     * @access  public
337     * @author  Quinn Comendant <quinn@strangecode.com>
338     * @since   15 Jul 2005 00:32:21
339     */
340    public function start()
341    {
342        if ($this->running) {
343            return false;
344        }
345
346        // Error reporting.
347        ini_set('error_reporting', $this->getParam('error_reporting'));
348        ini_set('display_errors', $this->getParam('display_errors'));
349        ini_set('log_errors', true);
350        if (is_dir($this->getParam('log_directory')) && is_writable($this->getParam('log_directory'))) {
351            ini_set('error_log', $this->getParam('log_directory') . '/' . $this->getParam('php_error_log'));
352        }
353
354        // Set character set to use for multi-byte string functions.
355        mb_internal_encoding($this->getParam('character_set'));
356        ini_set('default_charset', $this->getParam('character_set'));
357        switch (mb_strtolower($this->getParam('character_set'))) {
358        case 'utf-8' :
359            mb_language('uni');
360            break;
361
362        case 'iso-2022-jp' :
363            mb_language('ja');
364            break;
365
366        case 'iso-8859-1' :
367        default :
368            mb_language('en');
369            break;
370        }
371
372        // If the 'timezone' parameter is not set, check for a tz cookie.
373        if (!$this->getParam('timezone') && isset($_COOKIE['tz']) && '' != $_COOKIE['tz']) {
374            if (preg_match('!^[A-Z_/-]{3,}$!/i', $_COOKIE['tz'])) {
375                // Valid: tz cookie has a timezone name, like "UTC" or "America/Mexico_City".
376                $this->setParam(array('timezone' => $_COOKIE['tz']));
377            } else if (is_numeric($_COOKIE['tz']) && $_COOKIE['tz'] > -12 && $_COOKIE['tz'] < 14) { // https://en.wikipedia.org/wiki/List_of_UTC_time_offsets#UTC+14:00,_M%E2%80%A0
378                // tz cookie has a timezone offset, like "-6" (assume UTC).
379                $tz = timezone_name_from_abbr('', $_COOKIE['tz'] * 3600, 0);
380                if ($tz && preg_match('!^[A-Z_/-]{3,}$!/i', $tz)) {
381                    // Valid.
382                    $this->setParam(array('timezone' => $tz));
383                } else {
384                    $this->logMsg(sprintf('Failed to convert UTC offset to timezone: %s', $_COOKIE['tz']), LOG_NOTICE, __FILE__, __LINE__);
385                }
386            } else {
387                $this->logMsg(sprintf('Invalid timezone cookie value: %s', $_COOKIE['tz']), LOG_NOTICE, __FILE__, __LINE__);
388            }
389        }
390        if ($this->getParam('timezone')) {
391            // Set timezone of the user.
392            if (date_default_timezone_set($this->getParam('timezone'))) {
393                $this->logMsg(sprintf('Using timezone: %s', $this->getParam('timezone')), LOG_DEBUG, __FILE__, __LINE__);
394            } else {
395                // Failed: unset the timezone parameter so it isn't used to set the database timezone.
396                $this->setParam(array('timezone' => null));
397                $this->logMsg(sprintf('Failed to set timezone: %s', $this->getParam('timezone')), LOG_NOTICE, __FILE__, __LINE__);
398            }
399        } else {
400            $this->logMsg(sprintf('Using server timezone: %s', date_default_timezone_get()), LOG_DEBUG, __FILE__, __LINE__);
401        }
402
403        /**
404         * 1. Start Database.
405         */
406
407        if (true === $this->getParam('enable_db')) {
408
409            // DB connection parameters taken from environment variables in the server httpd.conf file (readable only by root)

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