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

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

Various minor changes:

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

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