5 ini_set(
'display_errors',
false);
6 error_reporting(E_ALL);
18 class Fred
extends Thing{
20 const EVENT_HALT =
'fred:halt';
21 const EVENT_EXTERNAL_ERROR =
'fred:externalError';
22 const EVENT_SHUTDOWN =
'fred:shutdown';
24 public $debug =
false;
25 public $errorMapping = [
26 E_STRICT => Fred\Log::WARNING,
27 E_DEPRECATED => Fred\Log::WARNING
29 public $rootPath =
null;
30 public $autoloadCacheKey =
'fred:autoloadCache';
31 public $autoloadMissingKey =
'fred:autoloadMissing';
32 public $defaultComponentNamespace = __CLASS__;
35 public $templatePath = __DIR__ .
'/../../template/';
36 public $version =
null;
37 public $ignoreErrors =
'/^SOAP-ERROR/';
38 public $stripObjectsMemoryLimit =
null;
39 public $stripObjectsMaxDepth = 10;
41 protected $_startTime =
null;
42 protected $_initialized =
false;
43 protected $_config = [];
44 protected $_internalError =
false;
45 protected $_errorHash =
null;
46 protected $_errorBacktrace =
null;
47 protected $_timeLimit =
null;
49 protected $_autoloadNamespaces = [];
50 protected $_autoloadClasses = [];
51 protected $_autoloadFiles = [];
52 protected $_autoloadCache = [];
53 protected $_autoloadCacheLimit = 250;
54 protected $_autoloadMissing = [];
55 protected $_sharedCacheFile =
null;
56 protected $_sharedCacheTime =
null;
57 protected $_sharedCacheTtl = 600;
58 protected $_sharedCacheTtlSpread = 10;
59 protected $_components = [];
61 protected $_releaseNotesFile = __DIR__ .
'/../../doc/pages/notes.php';
62 protected $_releaseNotesKey =
'fred:releaseNotesTime';
64 protected $_maintenanceTime =
null;
65 protected $_maintenanceMessage = [];
69 protected $_maintenanceKey =
'fred:maintenanceMessage';
75 public function __construct($config){
77 $this->_startTime = $_SERVER[
'REQUEST_TIME_FLOAT'] ?? microtime(
true);
78 $this->_config = is_array($config) ? $config : require($config);
79 spl_autoload_register([$this,
'autoload'],
true,
true);
83 $this->exceptionHandler($e);
89 protected function init(){
90 $this->configure($this->_config);
91 ini_set(
'display_errors',$this->debug);
92 ini_set(
'exception_ignore_args',!$this->debug);
93 set_error_handler([$this,
'errorHandler']);
94 set_exception_handler([$this,
'exceptionHandler']);
95 register_shutdown_function([$this,
'shutdownFunction']);
96 if(!$this->rootPath) $this->rootPath = dirname(__DIR__,5);
97 $this->publish([
'startTime' => self::READABLE,
'autoloadNamespaces' => self::READABLE,
'autoloadClasses' => self::READABLE]);
99 if(session_status() == PHP_SESSION_NONE) session_start();
100 if($this->_sharedCacheFile)
try{
101 include($this->_sharedCacheFile);
102 if((($delta = $this->_startTime - $this->_sharedCacheTime - $this->_sharedCacheTtl) > 0) && ($delta > rand(0,1000) / 1000 * $this->_sharedCacheTtlSpread)){
103 $this->_autoloadCache = $this->_autoloadMissing = [];
104 $this->_sharedCacheTime =
null;
107 catch(\Throwable $e){
108 \Rsi\File::unlink($this->_sharedCacheFile);
109 $this->log->info($e);
111 if(array_key_exists($this->autoloadCacheKey,$_SESSION))
112 $this->_autoloadCache = array_merge($this->_autoloadCache,$_SESSION[$this->autoloadCacheKey]);
113 if(array_key_exists($this->autoloadMissingKey,$_SESSION))
114 $this->_autoloadMissing = array_merge($this->_autoloadMissing,$_SESSION[$this->autoloadMissingKey]);
116 catch(\Exception $e){
117 if($this->debug)
throw $e;
120 $this->log->debug(
'FRED™ framework initialized',__FILE__,__LINE__);
121 $this->releaseNotes();
123 else $this->maintenance();
124 $this->_initialized =
true;
127 protected function saveSharedCache(){
128 if($this->_initialized && $this->_sharedCacheFile)
try{
129 file_put_contents($temp = $this->_sharedCacheFile . uniqid(
'-',
true) .
'.tmp',
"<?php\n" .
130 '$this->_sharedCacheTime = ' . ($this->_sharedCacheTime ?: $this->_startTime) .
'; //modified ' . date(
'Y-m-d H:i:s') .
"\n" .
131 '$this->_autoloadCache = ' . var_export($this->_autoloadCache,
true) .
";\n" .
132 '$this->_autoloadMissing = ' . var_export(count($this->_autoloadMissing) > $this->_autoloadCacheLimit ? [] : $this->_autoloadMissing,
true) .
';'
134 if(!rename($temp,$this->_sharedCacheFile))
throw new \Exception(
"Could not rename('$temp','{$this->_sharedCacheFile}')");
135 chmod($this->_sharedCacheFile,0666);
137 catch(\Exception $e){
138 $this->log->info($e);
142 catch(\Exception $e){}
145 $_SESSION[$this->autoloadCacheKey] = array_slice($this->_autoloadCache,0,$this->_autoloadCacheLimit,
true);
146 $_SESSION[$this->autoloadMissingKey] = array_slice($this->_autoloadMissing,0,$this->_autoloadCacheLimit);
158 public function autoload($class_name){
159 if($this->debug && $this->_initialized) $this->log->debug(__CLASS__ .
"::autoload('$class_name')",__FILE__,__LINE__,[
'className' => $class_name]);
160 if(array_key_exists($class_name,$this->_autoloadCache))
return require(str_replace(
'~',$this->rootPath,$this->_autoloadCache[$class_name]));
161 if(array_key_exists($class_name,$this->_autoloadClasses))
return require($this->_autoloadClasses[$class_name]);
162 if(!in_array($class_name,$this->_autoloadMissing)){
163 $prefix_match =
false;
164 foreach($this->_autoloadNamespaces as $prefix => $paths)
if(substr($class_name,0,strlen($prefix)) == $prefix){
165 $prefix_match =
true;
166 foreach($paths as $path)
if(is_file($filename = $path . str_replace([
'_',
'\\'],
'/',substr($class_name,strlen(rtrim($prefix,
'\\')))) .
'.php')){
167 $this->_autoloadCache[$class_name] = str_replace($this->rootPath,
'~',$filename);
168 $this->saveSharedCache();
169 return require($filename);
172 if(!$prefix_match && ($files = $this->_autoloadFiles)){
173 $this->_autoloadFiles =
false;
174 foreach($files as $filename) require($filename);
177 $this->_autoloadMissing[] = $class_name;
178 $this->saveSharedCache();
186 public function halt($status =
null){
187 if($this->event->trigger(self::EVENT_HALT,$this,$status) !==
false){
188 $this->_initialized =
false;
198 public function stripObjects(&$item,&$objects =
false,$level = 0){
200 if(is_object($item)){
201 if($item instanceof \Closure){
202 $reflect = new \ReflectionFunction($item);
203 $filename = $reflect->getFileName();
204 $line_no = $reflect->getStartLine();
206 "<closure file='$filename' line='$line_no'>\n" .
207 implode(array_slice(file($filename),$line_no - 1,$reflect->getEndLine() - $line_no + 1)) .
210 elseif($objects !==
false){
211 if(!$this->stripObjectsMemoryLimit) $this->stripObjectsMemoryLimit = \Rsi::memoryLimit() / 5;
212 if(memory_get_usage() > $this->stripObjectsMemoryLimit) $item =
'out of memory';
213 $objects[$key =
'@@object_' . md5(print_r($item,
true)) .
'@@'] = $item;
216 else $item =
'** object **';
218 elseif(is_array($item)){
219 if($level >= $this->stripObjectsMaxDepth) $item =
'** max depth **';
220 else foreach($item as &$sub) $this->stripObjects($sub,$objects,$level + 1);
224 catch(\Exception $e){
225 $item =
'@@' . $e->getMessage() .
'@@';
233 public function stripTraceObjects(&$trace,&$objects){
234 foreach($trace as &$step){
235 if(array_key_exists(
'object',$step)) $this->stripObjects($step[
'object'],$objects);
236 if(array_key_exists(
'args',$step) && $step[
'args']) $this->stripObjects($step[
'args'],$objects);
237 else unset($step[
'args']);
244 public function externalError($message,$context =
null){
246 print($message .
"\n\n");
247 if(!\Rsi::commandLine()){
248 $trace = debug_backtrace();
250 $this->stripTraceObjects($trace,$objects);
255 $this->halt(
'External error');
257 $this->log->notice(
'External error: ' . $message,$context);
258 if($this->event->trigger(self::EVENT_EXTERNAL_ERROR,$this,$message) !==
false){
259 http_response_code(400);
265 protected function printError($message,$filename,$line_no,$trace){
266 print($message . ($filename ?
" ($filename" . ($line_no ?
'@' . $line_no :
null) .
")" :
null) .
"\n\n");
267 foreach($trace as $step) print(
268 ($step[
'file'] ??
null) . (($line_no = $step[
'line'] ?? 0) ?
'@' . $line_no :
null) .
': ' .
269 (($class_name = $step[
'object'] ?? $step[
'class'] ??
null) ? $class_name . ($step[
'type'] ??
'::') :
null) . ($step[
'function'] ??
null) .
"()\n"
279 public function internalError($message,$filename =
null,$line_no =
null,$trace =
null){
281 if(ob_get_length()) ob_clean();
283 if($this->debug && (strpos($message,
'Undefined variable: ') === 0)){
284 $this->_internalError =
true;
288 if(!$trace) $trace = debug_backtrace();
289 if($this->debug) $this->log->emergency($message,$filename,$line_no);
290 $this->stripTraceObjects($trace,$objects);
292 if($this->_internalError){
294 if(\Rsi::commandLine()) $this->printError($message,$filename,$line_no,$trace);
296 print(
"<code>$message ($filename:$line_no)<code><br><br>");
297 $dump = $this->dump();
298 print($dump->head() . $dump->source($filename,$line_no));
299 if($trace) print($dump->var(
'trace',$trace));
300 if($objects) print($dump->var(
'objects',$objects));
303 exit(
'Internal error');
305 $this->_internalError =
true;
307 if(!$this->debug)
try{
308 $this->log->emergency($message,$filename,$line_no,array_filter([
310 'objects' => $objects,
311 'headers' => function_exists(
'getallheaders') ? getallheaders() :
null,
312 'args' => $_SERVER[
'argv'] ??
null,
315 'COOKIE' => $_COOKIE,
316 'SESSION' => isset($_SESSION) ? $_SESSION :
null
318 usleep(rand(0,10000000));
319 http_response_code(500);
320 if(\Rsi::commandLine()) print(
"Error in '$filename' on line $line_no at " . date(
'Y-m-d H:i:s'));
321 elseif(is_file($template = $this->templatePath .
'error.php')) require($template);
323 catch(\Exception $e){
324 print(
'An unexpected error has occurred');
326 elseif(!\Rsi::commandLine() && is_file($template = $this->templatePath .
'debug.php')) require($template);
327 else $this->printError($message,$filename,$line_no,$trace);
330 catch(\Exception $e){
331 $this->internalError($e->getMessage(),$e->getFile(),$e->getLine(),$e->getTrace());
335 protected function errorHash($message,$filename,$line_no){
336 return md5(
"errorHash($message,$filename,$line_no)");
342 public function errorHandler($error_no,$message,$filename,$line_no){
343 if((error_reporting() & $error_no) && !array_key_exists($error_no,$this->errorMapping)){
344 $this->_errorHash = $this->errorHash($message,$filename,$line_no);
345 $this->_errorBacktrace = debug_backtrace();
346 throw new \ErrorException($message,$error_no,0,$filename,$line_no);
348 elseif($this->_initialized) $this->log->add($this->errorMapping[$error_no] ?? Fred\Log::INFO,$message,$filename,$line_no,[
'trace' => array_map(
function($step){
349 unset($step[
'args']);
351 },debug_backtrace(
false))]);
357 public function exceptionHandler($exception){
358 $message = $exception->getMessage();
359 $filename = $exception->getFile();
360 $line_no = $exception->getLine();
361 if(!($exception instanceof \ErrorException) || ($this->errorHash($message,$filename,$line_no) != $this->_errorHash)) $this->_errorBacktrace =
null;
362 $this->internalError($message,$filename,$line_no,$this->_errorBacktrace ?: $exception->getTrace());
370 public function shutdownFunction(){
372 $this->_initialized && !$this->_internalError &&
373 ($error = error_get_last()) &&
374 !array_key_exists($error[
'type'],$this->errorMapping) &&
375 !preg_match($this->ignoreErrors,$error[
'message']) &&
376 ($this->event->trigger(self::EVENT_SHUTDOWN,$this,$error) !==
false)
377 ) $this->internalError($error[
'message'],$error[
'file'],$error[
'line']);
384 public function version(&$hash =
null){
385 list($version,$hash) = explode(
'-',$this->version .
'-',2);
391 protected function releaseNotes(){
393 $this->_releaseNotesFile &&
394 ($time = File::mtime($this->_releaseNotesFile)) &&
395 ($time != Record::get($_SESSION,$this->_releaseNotesKey)) &&
396 preg_match_all(
'/\\n- ([\\d\\.]+): (.*)/',file_get_contents($this->_releaseNotesFile),$matches,PREG_SET_ORDER)
399 foreach($matches as $match){
400 $version = $match[1] .
'-' . str_replace(
' ',
'-',strtolower(\
Rsi\Str::codeName(crc32($match[2]))));
401 if($this->version == $version) $messages = [];
402 else $messages[] =
"FRED™ version $version: " . str_replace(
'\\\\',
'\\',$match[2]);
405 foreach($messages as $message){
406 $this->message->warning($message);
407 $this->log->warning($message,__FILE__,__LINE__);
409 $this->log->notice(
"Upgraded to FRED™ version $version.",__FILE__,__LINE__);
411 else $_SESSION[$this->_releaseNotesKey] = $time;
417 protected function maintenance(){
418 if($this->_maintenanceTime){
419 if(($delta = (strtotime($this->_maintenanceTime)) - $this->_startTime) <= 0){
420 if(is_file($template = $this->templatePath .
'maintenance.php')) require($template);
421 http_response_code(503);
425 else foreach($this->_maintenanceMessage as $limit => $message){
426 list($limit,$type) = explode(
':',$limit .
':warning');
427 if($delta <= $limit){
428 if($limit != \
Rsi\Record::get($_SESSION,$this->_maintenanceKey)){
429 $this->message->add($type,$message,[
'time' => $this->_maintenanceTime,
'delta' => round($delta / 60)] + compact(
'limit',
'type'));
430 $_SESSION[$this->_maintenanceKey] = $limit;
443 public function replaceVars($config){
444 if(!is_array($config))
return $config;
446 foreach($config as $key => $value)
if(substr($key,0,1) ==
'@')
447 $result[substr($key,1)] = (substr($value,0,1) ==
'{') && (substr($value,-1) ==
'}')
448 ? json_decode($this->vars->value(substr($value,1,-1)),
true)
449 : $this->vars->value($value);
450 else $result[$key] = $this->replaceVars($value);
457 public function dump(){
458 $class_name = \Rsi\Record::get($config = $this->config(
'dump',[]),
'className',
'Rsi\\Dump');
459 return new $class_name($config + [
'rootPath' => $this->rootPath]);
467 public function config($key,$default =
null){
468 return $this->replaceVars(Record::get($this->_config,$key,$default));
471 protected function defaultComponentClassName($name){
472 return $this->defaultComponentNamespace .
'\\' . ucfirst($name);
479 public function component($name){
480 if(!$this->has($name)){
481 $config = $this->config($name);
482 if($config && !is_array($config)){
483 if(is_callable($config)) $config = call_user_func($config,$name);
484 if(is_string($config)) $config = require($config);
486 if(!class_exists($class_name = Record::get($config,
'className') ?: $this->defaultComponentClassName($name)))
487 throw new \Exception(
"Unknown component '$name' ($class_name)");
488 $this->_components[$name] =
new $class_name($this,array_merge([
'name' => $name],$config ?: []));
490 return $this->_components[$name];
497 public function may($name){
498 return array_key_exists($name,$this->_components) || array_key_exists($name,$this->_config) || class_exists($this->defaultComponentClassName($name))
499 ? $this->component($name)
507 public function has($name){
508 return $this->_components[$name] ??
false;
514 public function clientConfig(){
516 if($this->debug) $config[
'debug'] =
true;
517 foreach($this->_components as $name => $component) $config[$name] = $component->clientConfig();
518 return array_filter($config);
521 protected function setAutoloadNamespaces($namespaces){
522 $this->_autoloadNamespaces = array_merge($this->_autoloadNamespaces,$namespaces);
525 protected function setAutoloadClasses($classes){
526 $this->_autoloadClasses = array_merge($this->_autoloadClasses,$classes);
529 protected function setAutoloadFiles($classes){
530 $this->_autoloadFiles = array_merge($this->_autoloadFiles,$classes);
533 protected function setTimeLimit($value){
534 set_time_limit($value);
535 $this->_timeLimit = microtime(
true) + $value;
538 protected function getTimeLimit(){
539 if($this->_timeLimit ===
null) $this->_timeLimit = $this->_startTime + ini_get(
'max_execution_time');
540 return max(0,$this->_timeLimit - microtime(
true));
543 protected function _get($key){
544 return $this->component($key);
547 public function __call($func_name,$params){
548 return call_user_func_array([$this->entity,
'item'],array_merge([$func_name],$params));