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

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

Add support for PHP >=5.6 <=7.3. Add MySQL polyfill.

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

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