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

Last change on this file since 588 was 588, checked in by anonymous, 7 years ago

Added as fifth parameter to logMsg(). Change logPriorityToString() also map the reverse. Update App->url() to never allow indexed array query params for carried queries, use arr[]&arr[] instead.

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

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