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