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

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

Remove cruft from timezone experiment

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