1<?php
2/*******************************************************************
3    class.osticket.php
4
5    osTicket (sys) -> Config.
6
7    Core osTicket object: loads congfig and provides loggging facility.
8
9    Use osTicket::start(configId)
10
11    Peter Rotich <peter@osticket.com>
12    Copyright (c)  2006-2013 osTicket
13    http://www.osticket.com
14
15    Released under the GNU General Public License WITHOUT ANY WARRANTY.
16    See LICENSE.TXT for details.
17
18    vim: expandtab sw=4 ts=4 sts=4:
19**********************************************************************/
20
21require_once(INCLUDE_DIR.'class.csrf.php'); //CSRF token class.
22require_once(INCLUDE_DIR.'class.migrater.php');
23require_once(INCLUDE_DIR.'class.plugin.php');
24require_once INCLUDE_DIR . 'class.message.php';
25
26define('LOG_WARN',LOG_WARNING);
27
28class osTicket {
29
30    var $loglevel=array(1=>'Error','Warning','Debug');
31
32    //Page errors.
33    var $errors;
34
35    //System
36    var $system;
37
38
39
40
41    var $warning;
42    var $message;
43
44    var $title; //Custom title. html > head > title.
45    var $headers;
46    var $pjax_extra;
47
48    var $config;
49    var $session;
50    var $csrf;
51    var $company;
52    var $plugins;
53
54    function __construct() {
55
56        require_once(INCLUDE_DIR.'class.config.php'); //Config helper
57        require_once(INCLUDE_DIR.'class.company.php');
58
59        if (!defined('DISABLE_SESSION') || !DISABLE_SESSION)
60            $this->session = osTicketSession::start(SESSION_TTL); // start DB based session
61
62        $this->config = new OsticketConfig();
63
64        $this->csrf = new CSRF('__CSRFToken__');
65
66        $this->company = new Company();
67
68        $this->plugins = new PluginManager();
69    }
70
71    function isSystemOnline() {
72        return ($this->getConfig() && $this->getConfig()->isHelpDeskOnline() && !$this->isUpgradePending());
73    }
74
75    function isUpgradePending() {
76		foreach (DatabaseMigrater::getUpgradeStreams(UPGRADE_DIR.'streams/') as $stream=>$hash)
77			if (strcasecmp($hash,
78					$this->getConfig()->getSchemaSignature($stream)))
79				return true;
80		return false;
81    }
82
83    function getSession() {
84        return $this->session;
85    }
86
87    function getConfig() {
88        return $this->config;
89    }
90
91    function getDBSignature($namespace='core') {
92        return $this->getConfig()->getSchemaSignature($namespace);
93    }
94
95    function getVersion() {
96        return THIS_VERSION;
97    }
98
99    function getCSRF(){
100        return $this->csrf;
101    }
102
103    function getCSRFToken() {
104        return $this->getCSRF()->getToken();
105    }
106
107    function getCSRFFormInput() {
108        return $this->getCSRF()->getFormInput();
109    }
110
111    function validateCSRFToken($token) {
112        return ($token && $this->getCSRF()->validateToken($token));
113    }
114
115    function checkCSRFToken($name=false, $rotate=false) {
116        $name = $name ?: $this->getCSRF()->getTokenName();
117        $token = $_POST[$name] ?: $_SERVER['HTTP_X_CSRFTOKEN'];
118        if ($token && $this->validateCSRFToken($token)) {
119            if ($rotate) $this->getCSRF()->rotate();
120            return true;
121        }
122
123        $msg=sprintf(__('Invalid CSRF token [%1$s] on %2$s'),
124                Format::htmlchars($token), THISPAGE);
125        $this->logWarning(__('Invalid CSRF Token').' '.$name, $msg, false);
126
127        return false;
128    }
129
130    function getLinkToken() {
131        return md5($this->getCSRFToken().SECRET_SALT.session_id());
132    }
133
134    function validateLinkToken($token) {
135            return ($token && !strcasecmp($token, $this->getLinkToken()));
136    }
137
138    /* Replace Template Variables */
139    function replaceTemplateVariables($input, $vars=array()) {
140
141        $replacer = new VariableReplacer();
142        $replacer->assign(array_merge($vars,
143            array('url' => $this->getConfig()->getBaseUrl(),
144                'company' => $this->company)
145                    ));
146
147        return $replacer->replaceVars($input);
148    }
149
150    static function getVarScope() {
151        return array(
152            'url' => __("osTicket's base url (FQDN)"),
153            'company' => array('class' => 'Company', 'desc' => __('Company Information')),
154        );
155    }
156
157    function addExtraHeader($header, $pjax_script=false) {
158        $this->headers[md5($header)] = $header;
159        $this->pjax_extra[md5($header)] = $pjax_script;
160    }
161
162    function getExtraHeaders() {
163        return $this->headers;
164    }
165    function getExtraPjax() {
166        return $this->pjax_extra;
167    }
168
169    function setPageTitle($title) {
170        $this->title = $title;
171    }
172
173    function getPageTitle() {
174        return $this->title;
175    }
176
177    function getErrors() {
178        return $this->errors;
179    }
180
181    function setErrors($errors) {
182        $this->errors = $errors;
183    }
184
185    function getError() {
186        return $this->system['err'];
187    }
188
189    function setError($error) {
190        $this->system['error'] = $error;
191    }
192
193    function clearError() {
194        $this->setError('');
195    }
196
197    function getWarning() {
198        return $this->system['warning'];
199    }
200
201    function setWarning($warning) {
202        $this->system['warning'] = $warning;
203    }
204
205    function clearWarning() {
206        $this->setWarning('');
207    }
208
209
210    function getNotice() {
211        return $this->system['notice'];
212    }
213
214    function setNotice($notice) {
215        $this->system['notice'] = $notice;
216    }
217
218    function clearNotice() {
219        $this->setNotice('');
220    }
221
222
223    function alertAdmin($subject, $message, $log=false) {
224
225        //Set admin's email address
226        if (!($to = $this->getConfig()->getAdminEmail()))
227            $to = ADMIN_EMAIL;
228
229        //append URL to the message
230        $message.="\n\n".$this->getConfig()->getBaseUrl();
231
232        //Try getting the alert email.
233        $email=null;
234        if(!($email=$this->getConfig()->getAlertEmail()))
235            $email=$this->getConfig()->getDefaultEmail(); //will take the default email.
236
237        if($email) {
238            $email->sendAlert($to, $subject, $message, null, array('text'=>true, 'reply-tag'=>false));
239        } else {//no luck - try the system mail.
240            Mailer::sendmail($to, $subject, $message, '"'.__('osTicket Alerts').sprintf('" <%s>',$to));
241        }
242
243        //log the alert? Watch out for loops here.
244        if($log)
245            $this->log(LOG_CRIT, $subject, $message, false); //Log the entry...and make sure no alerts are resent.
246
247    }
248
249    function logDebug($title, $message, $force=false) {
250        return $this->log(LOG_DEBUG, $title, $message, false, $force);
251    }
252
253    function logInfo($title, $message, $alert=false) {
254        return $this->log(LOG_INFO, $title, $message, $alert);
255    }
256
257    function logWarning($title, $message, $alert=true) {
258        return $this->log(LOG_WARN, $title, $message, $alert);
259    }
260
261    function logError($title, $error, $alert=true) {
262        return $this->log(LOG_ERR, $title, $error, $alert);
263    }
264
265    function logDBError($title, $error, $alert=true) {
266
267        if($alert && !$this->getConfig()->alertONSQLError())
268            $alert =false;
269
270        $e = new Exception();
271        $bt = str_replace(ROOT_DIR, _S(/* `root` is a root folder */ '(root)').'/',
272            $e->getTraceAsString());
273        $error .= nl2br("\n\n---- "._S('Backtrace')." ----\n".$bt);
274
275        // Prevent recursive loops through this code path
276        if (substr_count($bt, __FUNCTION__) > 1)
277            return;
278
279        return $this->log(LOG_ERR, $title, $error, $alert);
280    }
281
282    function log($priority, $title, $message, $alert=false, $force=false) {
283
284        //We are providing only 3 levels of logs. Windows style.
285        switch($priority) {
286            case LOG_EMERG:
287            case LOG_ALERT:
288            case LOG_CRIT:
289            case LOG_ERR:
290                $level=1; //Error
291                break;
292            case LOG_WARN:
293            case LOG_WARNING:
294                $level=2; //Warning
295                break;
296            case LOG_NOTICE:
297            case LOG_INFO:
298            case LOG_DEBUG:
299            default:
300                $level=3; //Debug
301        }
302
303        $loglevel=array(1=>'Error','Warning','Debug');
304
305        $info = array(
306            'title' => &$title,
307            'level' => $loglevel[$level],
308            'level_id' => $level,
309            'body' => &$message,
310        );
311        Signal::send('syslog', null, $info);
312
313        //Logging everything during upgrade.
314        if($this->getConfig()->getLogLevel()<$level && !$force)
315            return false;
316
317        //Alert admin if enabled...
318        $alert = $alert && !$this->isUpgradePending();
319        if ($alert && $this->getConfig()->getLogLevel() >= $level)
320            $this->alertAdmin($title, $message);
321
322        //Save log based on system log level settings.
323        $sql='INSERT INTO '.SYSLOG_TABLE.' SET created=NOW(), updated=NOW() '
324            .',title='.db_input(Format::sanitize($title, true))
325            .',log_type='.db_input($loglevel[$level])
326            .',log='.db_input(Format::sanitize($message, false))
327            .',ip_address='.db_input($_SERVER['REMOTE_ADDR']);
328
329        db_query($sql, false);
330
331        return true;
332    }
333
334    function purgeLogs() {
335
336        if(!($gp=$this->getConfig()->getLogGracePeriod()) || !is_numeric($gp))
337            return false;
338
339        //System logs
340        $sql='DELETE  FROM '.SYSLOG_TABLE.' WHERE DATE_ADD(created, INTERVAL '.$gp.' MONTH)<=NOW()';
341        db_query($sql);
342
343        //TODO: Activity logs
344
345        return true;
346    }
347    /*
348     * Util functions
349     *
350     */
351
352    function get_var($index, $vars, $default='', $type=null) {
353
354        if(is_array($vars)
355                && array_key_exists($index, $vars)
356                && (!$type || gettype($vars[$index])==$type))
357            return $vars[$index];
358
359        return $default;
360    }
361
362    function get_db_input($index, $vars, $quote=true) {
363        return db_input($this->get_var($index, $vars), $quote);
364    }
365
366    function get_path_info() {
367        if(isset($_SERVER['PATH_INFO']))
368            return $_SERVER['PATH_INFO'];
369
370        if(isset($_SERVER['ORIG_PATH_INFO']))
371            return $_SERVER['ORIG_PATH_INFO'];
372
373        //TODO: conruct possible path info.
374
375        return null;
376    }
377
378    /**
379     * Fetch the current version(s) of osTicket softwares via DNS. The
380     * constants of MAJOR_VERSION, THIS_VERSION, and GIT_VERSION will be
381     * consulted to arrive at the most relevant version code for the latest
382     * release.
383     *
384     * Parameters:
385     * $product - (string|default:'core') the product to fetch versions for
386     * $major - (string|optional) optional major version to compare. This is
387     *      useful if more than one version is available. Only versions
388     *      specifying this major version ('m') are considered as version
389     *      candidates.
390     *
391     * Dns:
392     * The DNS zone will have TXT records for the product will be published
393     * in this format:
394     *
395     * "v=1; m=1.9; V=1.9.11; c=deadbeef"
396     *
397     * Where the string is a semicolon-separated string of key/value pairs
398     * with the following meanings:
399     *
400     * --+--------------------------
401     * v | DNS record format version
402     *
403     * For v=1, this is the meaning of the other keys
404     * --+-------------------------------------------
405     * m | (optional) major product version
406     * V | Full product version (usually a git tag)
407     * c | Git commit id of the release tag
408     * s | Schema signature of the version, which might help detect
409     *   | required migration
410     *
411     * Returns:
412     * (string|bool|null)
413     *  - 'v1.9.11' or 'deadbeef' if release tag or git commit id seems to
414     *      be most appropriate based on the value of GIT_VERSION
415     *  - null if the $major version is no longer supported
416     *  - false if no information is available in DNS
417     */
418     function getLatestVersion($product='core', $major=null) {
419        $records = dns_get_record($product.'.updates.osticket.com', DNS_TXT);
420        if (!$records)
421            return false;
422
423        $versions = array();
424        foreach ($records as $r) {
425            $txt = $r['txt'];
426            $info = array();
427            foreach (explode(';', $r['txt']) as $kv) {
428                list($k, $v) = explode('=', $kv);
429                if (!($k = trim($k)))
430                    continue;
431                $info[$k] = trim($v);
432            }
433            $versions[] = $info;
434        }
435        foreach ($versions as $info) {
436            switch ($info['v']) {
437            case '1':
438                if ($major && $info['m'] && $info['m'] != $major)
439                    continue 2;
440                if ($product == 'core' && GIT_VERSION == '$git')
441                    return $info['c'];
442                return $info['V'];
443            }
444        }
445    }
446
447   /*
448    * getTrustedProxies
449    *
450    * Get defined trusted proxies
451    */
452
453    static function getTrustedProxies() {
454        static $proxies = null;
455        // Parse trusted proxies from config file
456        if (!isset($proxies) && defined('TRUSTED_PROXIES'))
457            $proxies = array_filter(
458                    array_map('trim', explode(',', TRUSTED_PROXIES)));
459
460        return $proxies ?: array();
461    }
462
463    /*
464     * getLocalNetworkAddresses
465     *
466     * Get defined local network addresses
467     */
468    static function getLocalNetworkAddresses() {
469        static $ips = null;
470        // Parse local addreses from config file
471        if (!isset($ips) && defined('LOCAL_NETWORKS'))
472            $ips = array_filter(
473                    array_map('trim', explode(',', LOCAL_NETWORKS)));
474
475        return $ips ?: array();
476    }
477
478    static function get_root_path($dir) {
479
480        /* If run from the commandline, DOCUMENT_ROOT will not be set. It is
481         * also likely that the ROOT_PATH will not be necessary, so don't
482         * bother attempting to figure it out.
483         *
484         * Secondly, if the directory of main.inc.php is the same as the
485         * document root, the the ROOT path truly is '/'
486         */
487        if(!isset($_SERVER['DOCUMENT_ROOT'])
488                || !strcasecmp($_SERVER['DOCUMENT_ROOT'], $dir))
489            return '/';
490
491        /* The main idea is to try and use full-path filename of PHP_SELF and
492         * SCRIPT_NAME. The SCRIPT_NAME should be the path of that script
493         * inside the DOCUMENT_ROOT. This is most likely useful if osTicket
494         * is run using something like Apache UserDir setting where the
495         * DOCUMENT_ROOT of Apache and the installation path of osTicket
496         * have nothing in comon.
497         *
498         * +---------------------------+-------------------+----------------+
499         * | PHP Script                | SCRIPT_NAME       | ROOT_PATH      |
500         * +---------------------------+-------------------+----------------+
501         * | /home/u1/www/osticket/... | /~u1/osticket/... | /~u1/osticket/ |
502         * +---------------------------+-------------------+----------------+
503         *
504         * The algorithm will remove the directory of main.inc.php from
505         * as seen. What's left should be the script executed inside
506         * the osTicket installation. That is removed from SCRIPT_NAME.
507         * What's left is the ROOT_PATH.
508         */
509        $bt = debug_backtrace(false);
510        $frame = array_pop($bt);
511        $file = str_replace('\\','/', $frame['file']);
512        $path = substr($file, strlen(ROOT_DIR));
513        if($path && ($pos=strpos($_SERVER['SCRIPT_NAME'], $path))!==false)
514            return ($pos) ? substr($_SERVER['SCRIPT_NAME'], 0, $pos) : '/';
515
516        if (self::is_cli())
517            return '/';
518
519        return null;
520    }
521
522    /*
523     * get_client_ip
524     *
525     * Get client IP address from "Http_X-Forwarded-For" header by following a
526     * chain of trusted proxies.
527     *
528     * "Http_X-Forwarded-For" header value is a comma+space separated list of IP
529     * addresses, the left-most being the original client, and each successive
530     * proxy that passed the request all the way to the originating IP address.
531     *
532     */
533    static function get_client_ip($header='HTTP_X_FORWARDED_FOR') {
534
535        // Request IP
536        $ip = $_SERVER['REMOTE_ADDR'];
537        // Trusted proxies.
538        $proxies = self::getTrustedProxies();
539        // Return current IP address if header is not set and
540        // request is not from a trusted proxy.
541        if (!isset($_SERVER[$header])
542                || !$proxies
543                || !self::is_trusted_proxy($ip, $proxies))
544            return $ip;
545
546        // Get chain of proxied ip addresses
547        $ips = array_map('trim', explode(',', $_SERVER[$header]));
548        // Add request IP to the chain
549        $ips[] = $ip;
550        // Walk the chain in reverse - remove invalid IPs
551        $ips = array_reverse($ips);
552        foreach ($ips as $k => $ip) {
553            // Make sure the IP is valid and not a trusted proxy
554            if ($k && !Validator::is_ip($ip))
555                unset($ips[$k]);
556            elseif ($k && !self::is_trusted_proxy($ip, $proxies))
557                return $ip;
558        }
559
560        // We trust the 400 lb hacker... return left most valid IP
561        return array_pop($ips);
562    }
563
564    /*
565     * Checks if the IP is that of a trusted proxy
566     *
567     */
568    static function is_trusted_proxy($ip, $proxies=array()) {
569        $proxies = $proxies ?: self::getTrustedProxies();
570        // We don't have any proxies set.
571        if (!$proxies)
572            return false;
573        // Wildcard set - trust all proxies
574        else if ($proxies == '*')
575            return true;
576
577        return ($proxies && Validator::check_ip($ip, $proxies));
578    }
579
580    /**
581     * is_local_ip
582     *
583     * Check if a given IP is part of defined local address blocks
584     *
585     */
586    static function is_local_ip($ip, $ips=array()) {
587        $ips = $ips
588            ?: self::getLocalNetworkAddresses()
589            ?: array();
590
591        foreach ($ips as $addr) {
592            if (Validator::check_ip($ip, $addr))
593                return true;
594        }
595
596        return false;
597    }
598
599    /**
600     * Returns TRUE if the request was made via HTTPS and false otherwise
601     */
602    function is_https() {
603
604        // Local server flags
605        if (isset($_SERVER['HTTPS'])
606                && strtolower($_SERVER['HTTPS']) == 'on')
607            return true;
608
609        // Check if SSL was terminated by a loadbalancer
610        return (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
611                && !strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https'));
612    }
613
614    /**
615     * Returns TRUE if the current browser is IE and FALSE otherwise
616     */
617    function is_ie() {
618        if (preg_match('/MSIE|Internet Explorer|Trident\/[\d]{1}\.[\d]{1,2}/',
619                $_SERVER['HTTP_USER_AGENT']))
620            return true;
621
622        return false;
623    }
624
625    /* returns true if script is being executed via commandline */
626    static function is_cli() {
627        return (!strcasecmp(substr(php_sapi_name(), 0, 3), 'cli')
628                || (!isset($_SERVER['REQUEST_METHOD']) &&
629                    !isset($_SERVER['HTTP_HOST']))
630                    //Fallback when php-cgi binary is used via cli
631                );
632    }
633
634    /**** static functions ****/
635    function start() {
636        // Prep basic translation support
637        Internationalization::bootstrap();
638
639        if(!($ost = new osTicket()))
640            return null;
641
642        // Bootstrap installed plugins
643        $ost->plugins->bootstrap();
644
645        // Mirror content updates to the search backend
646        $ost->searcher = new SearchInterface();
647
648        return $ost;
649    }
650}
651
652?>
653