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

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

Fix minor bugs. Detect http port and add to site_port, site_url, and page_url params of App.

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

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