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

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

Add CLI coloring method. Allow longer email logging in dev.

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

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