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

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

Update admins.php. Fix $safe_http_host

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

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