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

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

misc

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

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