3 # FILE: ApplicationFramework.php 5 # Part of the ScoutLib application support library 6 # Copyright 2009-2016 Edward Almasy and Internet Scout Research Group 7 # http://scout.wisc.edu 14 class ApplicationFramework
17 # ---- PUBLIC INTERFACE -------------------------------------------------- 25 public function __construct()
27 # make sure default time zone is set 28 # (using CST if nothing set because we have to use something 29 # and Scout is based in Madison, WI, which is in CST) 30 if ((ini_get(
"date.timezone") === NULL)
31 || !strlen(ini_get(
"date.timezone")))
33 ini_set(
"date.timezone",
"America/Chicago");
36 # save execution start time 37 $this->ExecutionStartTime = microtime(TRUE);
39 # set up default object file search locations 40 self::AddObjectDirectory(
"local/interface/%ACTIVEUI%/objects");
41 self::AddObjectDirectory(
"interface/%ACTIVEUI%/objects");
42 self::AddObjectDirectory(
"local/interface/%DEFAULTUI%/objects");
43 self::AddObjectDirectory(
"interface/%DEFAULTUI%/objects");
44 self::AddObjectDirectory(
"local/objects");
45 self::AddObjectDirectory(
"objects");
47 # set up object file autoloader 48 spl_autoload_register(array($this,
"AutoloadObjects"));
50 # set up function to output any buffered text in case of crash 51 register_shutdown_function(array($this,
"OnCrash"));
53 # if we were not invoked via command line interface 54 if (php_sapi_name() !==
"cli")
56 # build cookie domain string 57 $SessionDomain = isset($_SERVER[
"SERVER_NAME"]) ? $_SERVER[
"SERVER_NAME"]
58 : isset($_SERVER[
"HTTP_HOST"]) ? $_SERVER[
"HTTP_HOST"]
61 # include a leading period so that older browsers implementing 62 # rfc2109 do not reject our cookie 63 $SessionDomain =
".".$SessionDomain;
65 # if it appears our session storage area is writable 66 if (is_writable(session_save_path()))
68 # store our session files in a subdirectory to avoid 69 # accidentally sharing sessions with other installations 71 $SessionStorage = session_save_path()
72 .
"/".self::$AppName.
"_".md5($SessionDomain.dirname(__FILE__));
74 # create session storage subdirectory if not found 75 if (!is_dir($SessionStorage)) { mkdir($SessionStorage, 0700 ); }
77 # if session storage subdirectory is writable 78 if (is_writable($SessionStorage))
80 # save parameters of our session storage as instance variables 82 $this->SessionGcProbability =
83 ini_get(
"session.gc_probability") / ini_get(
"session.gc_divisor");
84 # require a gc probability of at least MIN_GC_PROBABILITY 85 if ($this->SessionGcProbability < self::MIN_GC_PROBABILITY)
87 $this->SessionGcProbability = self::MIN_GC_PROBABILITY;
90 $this->SessionStorage = $SessionStorage;
92 # set the new session storage location 93 session_save_path($SessionStorage);
95 # disable PHP's garbage collection, as it does not handle 96 # subdirectories (instead, we'll do the cleanup as we run 98 ini_set(
"session.gc_probability", 0);
102 # set garbage collection max period to our session lifetime 103 ini_set(
"session.gc_maxlifetime", self::$SessionLifetime);
105 # Cookies lacking embedded dots are... fun. 106 # rfc2109 sec 4.3.2 says to reject them 107 # rfc2965 sec 3.3.2 says to reject them 108 # rfc6265 sec 4.1.2.3 says only that "public suffixes" 109 # should be rejected. They reference Mozilla's 110 # publicsuffix.org, which does not contain 'localhost'. 111 # However, empirically in early 2017 Firefox still rejects 113 # Therefore, don't set a cookie domain if we're running on 114 # localhost to avoid this problem. 115 if (preg_match(
'/^\.localhost(:[0-9]+)?$/', $SessionDomain))
117 session_set_cookie_params(
118 self::$SessionLifetime,
"/",
"");
122 session_set_cookie_params(
123 self::$SessionLifetime,
"/", $SessionDomain);
126 # attempt to start session 127 $SessionStarted = @session_start();
129 # if session start failed 130 if (!$SessionStarted)
132 # regenerate session ID and attempt to start session again 133 session_regenerate_id(TRUE);
138 # set up our internal environment 141 # set up our exception handler 142 set_exception_handler(array($this,
"GlobalExceptionHandler"));
144 # perform any work needed to undo PHP magic quotes 145 $this->UndoMagicQuotes();
147 # load our settings from database 148 $this->LoadSettings();
150 # set PHP maximum execution time 151 ini_set(
"max_execution_time", $this->Settings[
"MaxExecTime"]);
152 set_time_limit($this->Settings[
"MaxExecTime"]);
154 # register events we handle internally 155 $this->RegisterEvent($this->PeriodicEvents);
156 $this->RegisterEvent($this->UIEvents);
158 # attempt to create SCSS cache directory if needed and it does not exist 159 if ($this->ScssSupportEnabled() && !is_dir(self::$ScssCacheDir))
160 { @mkdir(self::$ScssCacheDir, 0777, TRUE); }
162 # attempt to create minimized JS cache directory if needed and it does not exist 163 if ($this->UseMinimizedJavascript()
164 && $this->JavascriptMinimizationEnabled()
165 && !is_dir(self::$JSMinCacheDir))
167 @mkdir(self::$JSMinCacheDir, 0777, TRUE);
176 public function __destruct()
178 # if template location cache is flagged to be saved 179 if ($this->SaveTemplateLocationCache)
181 # write template location cache out and update cache expiration 182 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings" 183 .
" SET TemplateLocationCache = '" 184 .addslashes(serialize(
185 $this->TemplateLocationCache)).
"'," 186 .
" TemplateLocationCacheExpiration = '" 188 $this->TemplateLocationCacheExpiration).
"'");
191 # if object location cache is flagged to be saved 192 if (self::$SaveObjectLocationCache)
194 # write object location cache out and update cache expiration 195 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings" 196 .
" SET ObjectLocationCache = '" 197 .addslashes(serialize(
198 self::$ObjectLocationCache)).
"'," 199 .
" ObjectLocationCacheExpiration = '" 201 self::$ObjectLocationCacheExpiration).
"'");
211 public function GlobalExceptionHandler($Exception)
213 # display exception info 214 $Message = $Exception->getMessage();
215 $Location = str_replace(getcwd().
"/",
"",
216 $Exception->getFile().
"[".$Exception->getLine().
"]");
217 $Trace = preg_replace(
":(#[0-9]+) ".getcwd().
"/".
":",
"$1 ",
218 $Exception->getTraceAsString());
219 if (php_sapi_name() ==
"cli")
221 print
"Uncaught Exception\n" 222 .
"Message: ".$Message.
"\n" 223 .
"Location: ".$Location.
"\n" 229 ?><table width=
"100%" cellpadding=
"5" 230 style=
"border: 2px solid #666666; background: #CCCCCC; 231 font-family: Courier New, Courier, monospace; 232 margin-top: 10px;"><tr><td>
233 <div style=
"color: #666666;">
234 <span style=
"font-size: 150%;">
235 <b>Uncaught Exception</b></span><br />
236 <b>
Message:</b> <i><?= $Message ?></i><br />
237 <b>Location:</b> <i><?= $Location ?></i><br />
238 <b>Trace:</b> <blockquote><pre><?= $Trace ?></pre></blockquote>
240 </td></tr></table><?
PHP 243 # log exception if not running from command line 244 if (php_sapi_name() !==
"cli")
246 $TraceString = $Exception->getTraceAsString();
247 $TraceString = str_replace(
"\n",
", ", $TraceString);
248 $TraceString = preg_replace(
":(#[0-9]+) ".getcwd().
"/".
":",
249 "$1 ", $TraceString);
250 $LogMsg =
"Uncaught exception (".$Exception->getMessage().
")" 251 .
" at ".$Location.
"." 252 .
" TRACE: ".$TraceString
253 .
" URL: ".$this->FullUrl();
254 $this->LogError(self::LOGLVL_ERROR, $LogMsg);
273 public static function AddObjectDirectory(
274 $Dir, $Prefix =
"", $ClassPattern = NULL, $ClassReplacement = NULL)
276 # make sure directory has trailing slash 277 $Dir = $Dir.((substr($Dir, -1) !=
"/") ?
"/" :
"");
279 # add directory to directory list 280 self::$ObjectDirectories[$Dir] = array(
282 "ClassPattern" => $ClassPattern,
283 "ClassReplacement" => $ClassReplacement,
306 public function AddImageDirectories(
307 $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
309 # add directories to existing image directory list 310 $this->ImageDirList = $this->AddToDirList(
311 $this->ImageDirList, $Dir, $SearchLast, $SkipSlashCheck);
334 public function AddIncludeDirectories(
335 $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
337 # add directories to existing image directory list 338 $this->IncludeDirList = $this->AddToDirList(
339 $this->IncludeDirList, $Dir, $SearchLast, $SkipSlashCheck);
361 public function AddInterfaceDirectories(
362 $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
364 # add directories to existing image directory list 365 $this->InterfaceDirList = $this->AddToDirList(
366 $this->InterfaceDirList, $Dir, $SearchLast, $SkipSlashCheck);
388 public function AddFunctionDirectories(
389 $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
391 # add directories to existing image directory list 392 $this->FunctionDirList = $this->AddToDirList(
393 $this->FunctionDirList, $Dir, $SearchLast, $SkipSlashCheck);
401 public function SetBrowserDetectionFunc($DetectionFunc)
403 $this->BrowserDetectFunc = $DetectionFunc;
412 public function AddUnbufferedCallback($Callback, $Parameters=array())
414 if (is_callable($Callback))
416 $this->UnbufferedCallbacks[] = array($Callback, $Parameters);
426 public function TemplateLocationCacheExpirationInterval($NewInterval =
DB_NOVALUE)
428 return $this->UpdateSetting(
"TemplateLocationCacheInterval", $NewInterval);
434 public function ClearTemplateLocationCache()
436 $this->TemplateLocationCache = array();
437 $this->SaveTemplateLocationCache = TRUE;
446 public function ObjectLocationCacheExpirationInterval($NewInterval =
DB_NOVALUE)
448 return $this->UpdateSetting(
"ObjectLocationCacheInterval", $NewInterval);
454 public function ClearObjectLocationCache()
456 self::$ObjectLocationCache = array();
457 self::$SaveObjectLocationCache = TRUE;
466 public function UrlFingerprintingEnabled($NewValue =
DB_NOVALUE)
468 return $this->UpdateSetting(
"UrlFingerprintingEnabled", $NewValue);
478 public function ScssSupportEnabled($NewValue =
DB_NOVALUE)
480 return $this->UpdateSetting(
"ScssSupportEnabled", $NewValue);
491 public function GenerateCompactCss($NewValue =
DB_NOVALUE)
493 return $this->UpdateSetting(
"GenerateCompactCss", $NewValue);
504 public function UseMinimizedJavascript($NewValue =
DB_NOVALUE)
506 return $this->UpdateSetting(
"UseMinimizedJavascript", $NewValue);
517 public function JavascriptMinimizationEnabled($NewValue =
DB_NOVALUE)
519 return $this->UpdateSetting(
"JavascriptMinimizationEnabled", $NewValue);
535 public function RecordContextInCaseOfCrash(
536 $BacktraceOptions = 0, $BacktraceLimit = 0)
538 if (version_compare(PHP_VERSION,
"5.4.0",
">="))
540 $this->SavedContext = debug_backtrace(
541 $BacktraceOptions, $BacktraceLimit);
545 $this->SavedContext = debug_backtrace($BacktraceOptions);
547 array_shift($this->SavedContext);
554 public function LoadPage($PageName)
556 # perform any clean URL rewriting 557 $PageName = $this->RewriteCleanUrls($PageName);
559 # sanitize incoming page name and save local copy 560 $PageName = preg_replace(
"/[^a-zA-Z0-9_.-]/",
"", $PageName);
561 $this->PageName = $PageName;
563 # if page caching is turned on 564 if ($this->PageCacheEnabled())
566 # if we have a cached page 567 $CachedPage = $this->CheckForCachedPage($PageName);
568 if ($CachedPage !== NULL)
570 # set header to indicate cache hit was found 571 header(
"X-ScoutAF-Cache: HIT");
573 # display cached page and exit 579 # set header to indicate no cache hit was found 580 header(
"X-ScoutAF-Cache: MISS");
584 # buffer any output from includes or PHP file 587 # include any files needed to set up execution environment 588 $IncludeFileContext = array();
589 foreach ($this->EnvIncludes as $IncludeFile)
591 $IncludeFileContext = $this->FilterContext(self::CONTEXT_ENV,
592 self::IncludeFile($IncludeFile, $IncludeFileContext));
596 $this->SignalEvent(
"EVENT_PAGE_LOAD", array(
"PageName" => $PageName));
598 # signal PHP file load 599 $SignalResult = $this->SignalEvent(
"EVENT_PHP_FILE_LOAD", array(
600 "PageName" => $PageName));
602 # if signal handler returned new page name value 603 $NewPageName = $PageName;
604 if (($SignalResult[
"PageName"] != $PageName)
605 && strlen($SignalResult[
"PageName"]))
607 # if new page name value is page file 608 if (file_exists($SignalResult[
"PageName"]))
610 # use new value for PHP file name 611 $PageFile = $SignalResult[
"PageName"];
615 # use new value for page name 616 $NewPageName = $SignalResult[
"PageName"];
619 # update local copy of page name 620 $this->PageName = $NewPageName;
623 # if we do not already have a PHP file 624 if (!isset($PageFile))
626 # look for PHP file for page 627 $OurPageFile =
"pages/".$NewPageName.
".php";
628 $LocalPageFile =
"local/pages/".$NewPageName.
".php";
629 $PageFile = file_exists($LocalPageFile) ? $LocalPageFile
630 : (file_exists($OurPageFile) ? $OurPageFile
631 :
"pages/".$this->DefaultPage.
".php");
635 $IncludeFileContext = $this->FilterContext(self::CONTEXT_PAGE,
636 self::IncludeFile($PageFile, $IncludeFileContext));
638 # save buffered output to be displayed later after HTML file loads 639 $PageOutput = ob_get_contents();
642 # signal PHP file load is complete 644 $Context[
"Variables"] = $IncludeFileContext;
645 $this->SignalEvent(
"EVENT_PHP_FILE_LOAD_COMPLETE",
646 array(
"PageName" => $PageName,
"Context" => $Context));
647 $PageCompleteOutput = ob_get_contents();
650 # set up for possible TSR (Terminate and Stay Resident :)) 651 $ShouldTSR = $this->PrepForTSR();
653 # if PHP file indicated we should autorefresh to somewhere else 654 if (($this->JumpToPage) && ($this->JumpToPageDelay == 0))
656 if (!strlen(trim($PageOutput)))
658 # if client supports HTTP/1.1, use a 303 as it is most accurate 659 if ($_SERVER[
"SERVER_PROTOCOL"] ==
"HTTP/1.1")
661 header(
"HTTP/1.1 303 See Other");
662 header(
"Location: ".$this->JumpToPage);
666 # if the request was an HTTP/1.0 GET or HEAD, then 667 # use a 302 response code. 669 # NB: both RFC 2616 (HTTP/1.1) and RFC1945 (HTTP/1.0) 670 # explicitly prohibit automatic redirection via a 302 671 # if the request was not GET or HEAD. 672 if ($_SERVER[
"SERVER_PROTOCOL"] ==
"HTTP/1.0" &&
673 ($_SERVER[
"REQUEST_METHOD"] ==
"GET" ||
674 $_SERVER[
"REQUEST_METHOD"] ==
"HEAD") )
676 header(
"HTTP/1.0 302 Found");
677 header(
"Location: ".$this->JumpToPage);
680 # otherwise, fall back to a meta refresh 683 print
'<html><head><meta http-equiv="refresh" ' 684 .
'content="0; URL='.$this->JumpToPage.
'">' 685 .
'</head><body></body></html>';
690 # else if HTML loading is not suppressed 691 elseif (!$this->SuppressHTML)
693 # set content-type to get rid of diacritic errors 694 header(
"Content-Type: text/html; charset=" 695 .$this->HtmlCharset, TRUE);
697 # load common HTML file (defines common functions) if available 698 $CommonHtmlFile = $this->FindFile($this->IncludeDirList,
699 "Common", array(
"tpl",
"html"));
702 $IncludeFileContext = $this->FilterContext(self::CONTEXT_COMMON,
703 self::IncludeFile($CommonHtmlFile, $IncludeFileContext));
707 $this->LoadUIFunctions();
709 # begin buffering content 712 # signal HTML file load 713 $SignalResult = $this->SignalEvent(
"EVENT_HTML_FILE_LOAD", array(
714 "PageName" => $PageName));
716 # if signal handler returned new page name value 717 $NewPageName = $PageName;
718 $PageContentFile = NULL;
719 if (($SignalResult[
"PageName"] != $PageName)
720 && strlen($SignalResult[
"PageName"]))
722 # if new page name value is HTML file 723 if (file_exists($SignalResult[
"PageName"]))
725 # use new value for HTML file name 726 $PageContentFile = $SignalResult[
"PageName"];
730 # use new value for page name 731 $NewPageName = $SignalResult[
"PageName"];
735 # load page content HTML file if available 736 if ($PageContentFile === NULL)
738 $PageContentFile = $this->FindFile(
739 $this->InterfaceDirList, $NewPageName,
740 array(
"tpl",
"html"));
742 if ($PageContentFile)
744 $IncludeFileContext = $this->FilterContext(self::CONTEXT_INTERFACE,
745 self::IncludeFile($PageContentFile, $IncludeFileContext));
749 print
"<h2>ERROR: No HTML/TPL template found" 750 .
" for this page (".$NewPageName.
").</h2>";
753 # signal HTML file load complete 754 $SignalResult = $this->SignalEvent(
"EVENT_HTML_FILE_LOAD_COMPLETE");
756 # stop buffering and save output 757 $PageContentOutput = ob_get_contents();
760 # if standard page start/end have not been suppressed 761 $PageStartOutput =
"";
763 if (!$this->SuppressStdPageStartAndEnd)
765 # load page start HTML file if available 766 $PageStartFile = $this->FindFile($this->IncludeDirList,
"Start",
767 array(
"tpl",
"html"), array(
"StdPage",
"StandardPage"));
771 $IncludeFileContext = self::IncludeFile(
772 $PageStartFile, $IncludeFileContext);
773 $PageStartOutput = ob_get_contents();
776 $IncludeFileContext = $this->FilterContext(
777 self::CONTEXT_START, $IncludeFileContext);
779 # load page end HTML file if available 780 $PageEndFile = $this->FindFile($this->IncludeDirList,
"End",
781 array(
"tpl",
"html"), array(
"StdPage",
"StandardPage"));
785 self::IncludeFile($PageEndFile, $IncludeFileContext);
786 $PageEndOutput = ob_get_contents();
791 # clear include file context because it may be large and is no longer needed 792 unset($IncludeFileContext);
794 # if page auto-refresh requested 795 if ($this->JumpToPage)
797 # add auto-refresh tag to page 799 "http-equiv" =>
"refresh",
800 "content" => $this->JumpToPageDelay,
801 "url" => $this->JumpToPage,
806 $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
808 # get list of any required files not loaded 809 $RequiredFiles = $this->GetRequiredFilesNotYetLoaded($PageContentFile);
811 # add file loading tags to page 812 $FullPageOutput = $this->AddFileTagsToPageOutput(
813 $FullPageOutput, $RequiredFiles);
815 # add any requested meta tags to page 816 $FullPageOutput = $this->AddMetaTagsToPageOutput($FullPageOutput);
818 # perform any regular expression replacements in output 819 $NewFullPageOutput = preg_replace($this->OutputModificationPatterns,
820 $this->OutputModificationReplacements, $FullPageOutput);
822 # check to make sure replacements didn't fail 823 $FullPageOutput = $this->CheckOutputModification(
824 $FullPageOutput, $NewFullPageOutput,
825 "regular expression replacements");
827 # for each registered output modification callback 828 foreach ($this->OutputModificationCallbacks as $Info)
830 # set up data for callback 831 $this->OutputModificationCallbackInfo = $Info;
833 # perform output modification 834 $NewFullPageOutput = preg_replace_callback($Info[
"SearchPattern"],
835 array($this,
"OutputModificationCallbackShell"),
838 # check to make sure modification didn't fail 839 $ErrorInfo =
"callback info: ".print_r($Info, TRUE);
840 $FullPageOutput = $this->CheckOutputModification(
841 $FullPageOutput, $NewFullPageOutput, $ErrorInfo);
844 # provide the opportunity to modify full page output 845 $SignalResult = $this->SignalEvent(
"EVENT_PAGE_OUTPUT_FILTER", array(
846 "PageOutput" => $FullPageOutput));
847 if (isset($SignalResult[
"PageOutput"])
848 && strlen(trim($SignalResult[
"PageOutput"])))
850 $FullPageOutput = $SignalResult[
"PageOutput"];
853 # if relative paths may not work because we were invoked via clean URL 854 if ($this->CleanUrlRewritePerformed || self::WasUrlRewritten())
856 # if using the <base> tag is okay 857 $BaseUrl = $this->BaseUrl();
858 if ($this->UseBaseTag)
860 # add <base> tag to header 861 $PageStartOutput = str_replace(
"<head>",
862 "<head><base href=\"".$BaseUrl.
"\" />",
865 # re-assemble full page with new header 866 $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
868 # the absolute URL to the current page 869 $FullUrl = $BaseUrl . $this->GetPageLocation();
871 # make HREF attribute values with just a fragment ID 872 # absolute since they don't work with the <base> tag because 873 # they are relative to the current page/URL, not the site 875 $NewFullPageOutput = preg_replace(
876 array(
"%href=\"(#[^:\" ]+)\"%i",
"%href='(#[^:' ]+)'%i"),
877 array(
"href=\"".$FullUrl.
"$1\"",
"href='".$FullUrl.
"$1'"),
880 # check to make sure HREF cleanup didn't fail 881 $FullPageOutput = $this->CheckOutputModification(
882 $FullPageOutput, $NewFullPageOutput,
887 # try to fix any relative paths throughout code 888 $RelativePathPatterns = array(
889 "%src=\"/?([^?*:;{}\\\\\" ]+)\.(js|css|gif|png|jpg)\"%i",
890 "%src='/?([^?*:;{}\\\\' ]+)\.(js|css|gif|png|jpg)'%i",
891 # don
't rewrite HREF attributes that are just 892 # fragment IDs because they are relative to the 893 # current page/URL, not the site root 894 "%href=\"/?([^#][^:\" ]*)\"%i", 895 "%href='/?([^#][^:
' ]*)'%i
", 896 "%action=\
"/?([^#][^:\" ]*)\"%i",
897 "%action='/?([^#][^:' ]*)'%i",
898 "%@import\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
899 "%@import\s+url\('/?([^:\" ]+)'\s*\)%i",
900 "%src:\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
901 "%src:\s+url\('/?([^:\" ]+)'\s*\)%i",
902 "%@import\s+\"/?([^:\" ]+)\"\s*%i",
903 "%@import\s+'/?([^:\" ]+)'\s*%i",
905 $RelativePathReplacements = array(
906 "src=\"".$BaseUrl.
"$1.$2\"",
907 "src=\"".$BaseUrl.
"$1.$2\"",
908 "href=\"".$BaseUrl.
"$1\"",
909 "href=\"".$BaseUrl.
"$1\"",
910 "action=\"".$BaseUrl.
"$1\"",
911 "action=\"".$BaseUrl.
"$1\"",
912 "@import url(\"".$BaseUrl.
"$1\")",
913 "@import url('".$BaseUrl.
"$1')",
914 "src: url(\"".$BaseUrl.
"$1\")",
915 "src: url('".$BaseUrl.
"$1')",
916 "@import \"".$BaseUrl.
"$1\"",
917 "@import '".$BaseUrl.
"$1'",
919 $NewFullPageOutput = preg_replace($RelativePathPatterns,
920 $RelativePathReplacements, $FullPageOutput);
922 # check to make sure relative path fixes didn't fail 923 $FullPageOutput = $this->CheckOutputModification(
924 $FullPageOutput, $NewFullPageOutput,
925 "relative path fixes");
929 # handle any necessary alternate domain rewriting 930 $FullPageOutput = $this->RewriteAlternateDomainUrls($FullPageOutput);
932 # update page cache for this page 933 $this->UpdatePageCache($PageName, $FullPageOutput);
935 # write out full page 936 print $FullPageOutput;
939 # run any post-processing routines 940 foreach ($this->PostProcessingFuncs as $Func)
942 call_user_func_array($Func[
"FunctionName"], $Func[
"Arguments"]);
945 # write out any output buffered from page code execution 946 if (strlen($PageOutput))
948 if (!$this->SuppressHTML)
950 ?><table width=
"100%" cellpadding=
"5" 951 style=
"border: 2px solid #666666; background: #CCCCCC; 952 font-family: Courier New, Courier, monospace; 953 margin-top: 10px;"><tr><td><?
PHP 955 if ($this->JumpToPage)
957 ?><div style=
"color: #666666;"><span style=
"font-size: 150%;">
958 <b>Page Jump Aborted</b></span>
959 (because of error or other unexpected output)<br />
961 <i><?
PHP print($this->JumpToPage); ?></i></div><?
PHP 964 if (!$this->SuppressHTML)
966 ?></td></tr></table><?
PHP 970 # write out any output buffered from the page code execution complete signal 971 if (!$this->JumpToPage && !$this->SuppressHTML && strlen($PageCompleteOutput))
973 print $PageCompleteOutput;
976 # log slow page loads 977 if ($this->LogSlowPageLoads()
978 && !$this->DoNotLogSlowPageLoad
979 && ($this->GetElapsedExecutionTime()
980 >= ($this->SlowPageLoadThreshold())))
982 $RemoteHost = gethostbyaddr($_SERVER[
"REMOTE_ADDR"]);
983 if ($RemoteHost === FALSE)
985 $RemoteHost = $_SERVER[
"REMOTE_ADDR"];
987 elseif ($RemoteHost != $_SERVER[
"REMOTE_ADDR"])
989 $RemoteHost .=
" (".$_SERVER[
"REMOTE_ADDR"].
")";
991 $SlowPageLoadMsg =
"Slow page load (" 992 .intval($this->GetElapsedExecutionTime()).
"s) for " 993 .$this->FullUrl().
" from ".$RemoteHost;
994 $this->LogMessage(self::LOGLVL_INFO, $SlowPageLoadMsg);
997 # execute callbacks that should not have their output buffered 998 foreach ($this->UnbufferedCallbacks as $Callback)
1000 call_user_func_array($Callback[0], $Callback[1]);
1003 # log high memory usage 1004 if (function_exists(
"memory_get_peak_usage"))
1006 $MemoryThreshold = ($this->HighMemoryUsageThreshold()
1007 * $this->GetPhpMemoryLimit()) / 100;
1008 if ($this->LogHighMemoryUsage()
1009 && (memory_get_peak_usage(TRUE) >= $MemoryThreshold))
1011 $HighMemUsageMsg =
"High peak memory usage (" 1012 .number_format(memory_get_peak_usage(TRUE)).
") for " 1013 .$this->FullUrl().
" from " 1014 .$_SERVER[
"REMOTE_ADDR"];
1015 $this->LogMessage(self::LOGLVL_INFO, $HighMemUsageMsg);
1019 # terminate and stay resident (TSR!) if indicated and HTML has been output 1020 # (only TSR if HTML has been output because otherwise browsers will misbehave) 1021 if ($ShouldTSR) { $this->LaunchTSR(); }
1029 public function GetPageName()
1031 return $this->PageName;
1039 public function GetPageLocation()
1041 # retrieve current URL 1042 $Url = self::GetScriptUrl();
1044 # remove the base path if present 1045 $BasePath = $this->Settings[
"BasePath"];
1046 if (stripos($Url, $BasePath) === 0)
1048 $Url = substr($Url, strlen($BasePath));
1051 # if we're being accessed via an alternate domain, 1052 # add the appropriate prefix in 1053 if ($this->HtaccessSupport() &&
1054 self::$RootUrlOverride !== NULL)
1056 $VHost = $_SERVER[
"SERVER_NAME"];
1057 if (isset($this->AlternateDomainPrefixes[$VHost]))
1059 $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
1060 $Url = $ThisPrefix.
"/".$Url;
1072 public function GetPageUrl()
1074 return self::BaseUrl() . $this->GetPageLocation();
1088 public function SetJumpToPage($Page, $Delay = 0, $IsLiteral = FALSE)
1092 && (strpos($Page,
"?") === FALSE)
1093 && ((strpos($Page,
"=") !== FALSE)
1094 || ((stripos($Page,
".php") === FALSE)
1095 && (stripos($Page,
".htm") === FALSE)
1096 && (strpos($Page,
"/") === FALSE)))
1097 && (stripos($Page,
"http://") !== 0)
1098 && (stripos($Page,
"https://") !== 0))
1100 $this->JumpToPage = self::BaseUrl() .
"index.php?P=".$Page;
1104 $this->JumpToPage = $Page;
1106 $this->JumpToPageDelay = $Delay;
1113 public function JumpToPageIsSet()
1115 return ($this->JumpToPage === NULL) ? FALSE : TRUE;
1127 public function HtmlCharset($NewSetting = NULL)
1129 if ($NewSetting !== NULL) { $this->HtmlCharset = $NewSetting; }
1130 return $this->HtmlCharset;
1142 public function DoNotMinimizeFile($File)
1144 if (!is_array($File)) { $File = array($File); }
1145 $this->DoNotMinimizeList = array_merge($this->DoNotMinimizeList, $File);
1158 public function UseBaseTag($NewValue = NULL)
1160 if ($NewValue !== NULL) { $this->UseBaseTag = $NewValue ? TRUE : FALSE; }
1161 return $this->UseBaseTag;
1171 public function SuppressHTMLOutput($NewSetting = TRUE)
1173 $this->SuppressHTML = $NewSetting;
1183 public function SuppressStandardPageStartAndEnd($NewSetting = TRUE)
1185 $this->SuppressStdPageStartAndEnd = $NewSetting;
1193 public static function DefaultUserInterface($UIName = NULL)
1195 if ($UIName !== NULL)
1197 self::$DefaultUI = $UIName;
1199 return self::$DefaultUI;
1208 public static function ActiveUserInterface($UIName = NULL)
1210 if ($UIName !== NULL)
1212 self::$ActiveUI = preg_replace(
"/^SPTUI--/",
"", $UIName);
1214 return self::$ActiveUI;
1227 public function GetUserInterfaces($FilterExp = NULL)
1231 if (!isset($Interfaces[$FilterExp]))
1233 # retrieve paths to user interface directories 1234 $Paths = $this->GetUserInterfacePaths($FilterExp);
1236 # start out with an empty list 1237 $Interfaces[$FilterExp] = array();
1239 # for each possible UI directory 1240 foreach ($Paths as $CanonicalName => $Path)
1242 # if name file available 1243 $LabelFile = $Path.
"/NAME";
1244 if (is_readable($LabelFile))
1247 $Label = file_get_contents($LabelFile);
1249 # if the UI name looks reasonable 1250 if (strlen(trim($Label)))
1253 $Interfaces[$FilterExp][$CanonicalName] = $Label;
1257 # if we do not have a name yet 1258 if (!isset($Interfaces[$FilterExp][$CanonicalName]))
1260 # use base directory for name 1261 $Interfaces[$FilterExp][$CanonicalName] = basename($Path);
1266 # return list to caller 1267 return $Interfaces[$FilterExp];
1278 public function GetUserInterfacePaths($FilterExp = NULL)
1280 static $InterfacePaths;
1282 if (!isset($InterfacePaths[$FilterExp]))
1284 # extract possible UI directories from interface directory list 1285 $InterfaceDirs = array();
1286 foreach ($this->ExpandDirectoryList($this->InterfaceDirList) as $Dir)
1289 if (preg_match(
"#([a-zA-Z0-9/]*interface)/[a-zA-Z0-9%/]*#",
1293 if (!in_array($Dir, $InterfaceDirs))
1295 $InterfaceDirs[] = $Dir;
1300 # reverse order of interface directories so that the directory 1301 # returned is the base directory for the interface 1302 $InterfaceDirs = array_reverse($InterfaceDirs);
1304 # start out with an empty list 1305 $InterfacePaths[$FilterExp] = array();
1306 $InterfacesFound = array();
1308 # for each possible UI directory 1309 foreach ($InterfaceDirs as $InterfaceDir)
1311 # check if the dir exists 1312 if (!is_dir($InterfaceDir))
1317 $Dir = dir($InterfaceDir);
1319 # for each file in current directory 1320 while (($DirEntry = $Dir->read()) !== FALSE)
1322 $InterfacePath = $InterfaceDir.
"/".$DirEntry;
1324 # skip anything we have already found 1325 # or that doesn't have a name in the required format 1326 # or that isn't a directory 1327 # or that doesn't match the filter regex (if supplied) 1328 if (in_array($DirEntry, $InterfacesFound)
1329 || !preg_match(
'/^[a-zA-Z0-9]+$/', $DirEntry)
1330 || !is_dir($InterfacePath)
1331 || (($FilterExp !== NULL)
1332 && !preg_match($FilterExp, $InterfacePath)))
1337 # add interface to list 1338 $InterfacePaths[$FilterExp][$DirEntry] = $InterfacePath;
1339 $InterfacesFound[] = $DirEntry;
1346 # return list to caller 1347 return $InterfacePaths[$FilterExp];
1374 public function AddPostProcessingCall($FunctionName,
1375 &$Arg1 = self::NOVALUE, &$Arg2 = self::NOVALUE, &$Arg3 = self::NOVALUE,
1376 &$Arg4 = self::NOVALUE, &$Arg5 = self::NOVALUE, &$Arg6 = self::NOVALUE,
1377 &$Arg7 = self::NOVALUE, &$Arg8 = self::NOVALUE, &$Arg9 = self::NOVALUE)
1379 $FuncIndex = count($this->PostProcessingFuncs);
1380 $this->PostProcessingFuncs[$FuncIndex][
"FunctionName"] = $FunctionName;
1381 $this->PostProcessingFuncs[$FuncIndex][
"Arguments"] = array();
1383 while (isset(${
"Arg".$Index}) && (${
"Arg".$Index} !== self::NOVALUE))
1385 $this->PostProcessingFuncs[$FuncIndex][
"Arguments"][$Index]
1396 public function AddEnvInclude($FileName)
1398 $this->EnvIncludes[] = $FileName;
1417 public function SetContextFilter($Context, $NewSetting)
1419 if (($NewSetting === TRUE)
1420 || ($NewSetting === FALSE)
1421 || is_array($NewSetting))
1423 $this->ContextFilters[$Context] = $NewSetting;
1425 elseif (is_string($NewSetting))
1427 $this->ContextFilters[$Context] = array($NewSetting);
1431 throw new InvalidArgumentException(
1432 "Invalid setting (".$NewSetting.
").");
1436 const CONTEXT_ENV = 1;
1438 const CONTEXT_PAGE = 2;
1440 const CONTEXT_COMMON = 3;
1442 const CONTEXT_INTERFACE = 4;
1444 const CONTEXT_START = 5;
1446 const CONTEXT_END = 6;
1454 public function GUIFile($FileName)
1456 # determine which location to search based on file suffix 1457 $FileType = $this->GetFileType($FileName);
1458 $DirList = ($FileType == self::FT_IMAGE)
1459 ? $this->ImageDirList : $this->IncludeDirList;
1461 # if directed to use minimized JavaScript file 1462 if (($FileType == self::FT_JAVASCRIPT) && $this->UseMinimizedJavascript())
1464 # look for minimized version of file 1465 $MinimizedFileName = substr_replace($FileName,
".min", -3, 0);
1466 $FoundFileName = $this->FindFile($DirList, $MinimizedFileName);
1468 # if minimized file was not found 1469 if (is_null($FoundFileName))
1471 # look for unminimized file 1472 $FoundFileName = $this->FindFile($DirList, $FileName);
1474 # if unminimized file found 1475 if (!is_null($FoundFileName))
1477 # if minimization enabled and supported 1478 if ($this->JavascriptMinimizationEnabled()
1479 && self::JsMinRewriteSupport())
1481 # attempt to create minimized file 1482 $MinFileName = $this->MinimizeJavascriptFile(
1485 # if minimization succeeded 1486 if ($MinFileName !== NULL)
1488 # use minimized version 1489 $FoundFileName = $MinFileName;
1491 # save file modification time if needed for fingerprinting 1492 if ($this->UrlFingerprintingEnabled())
1494 $FileMTime = filemtime($FoundFileName);
1497 # strip off the cache location, allowing .htaccess 1498 # to handle that for us 1499 $FoundFileName = str_replace(
1500 self::$JSMinCacheDir.
"/",
"", $FoundFileName);
1506 # else if directed to use SCSS files 1507 elseif (($FileType == self::FT_CSS) && $this->ScssSupportEnabled())
1509 # look for SCSS version of file 1510 $SourceFileName = preg_replace(
"/.css$/",
".scss", $FileName);
1511 $FoundSourceFileName = $this->FindFile($DirList, $SourceFileName);
1513 # if SCSS file not found 1514 if ($FoundSourceFileName === NULL)
1517 $FoundFileName = $this->FindFile($DirList, $FileName);
1521 # compile SCSS file (if updated) and return resulting CSS file 1522 $FoundFileName = $this->CompileScssFile($FoundSourceFileName);
1524 # save file modification time if needed for fingerprinting 1525 if ($this->UrlFingerprintingEnabled())
1527 $FileMTime = filemtime($FoundFileName);
1530 # strip off the cache location, allowing .htaccess to handle that for us 1531 if (self::ScssRewriteSupport())
1533 $FoundFileName = str_replace(
1534 self::$ScssCacheDir.
"/",
"", $FoundFileName);
1538 # otherwise just search for the file 1541 $FoundFileName = $this->FindFile($DirList, $FileName);
1544 # add non-image files to list of found files (used for required files loading) 1545 if ($FileType != self::FT_IMAGE)
1546 { $this->FoundUIFiles[] = basename($FoundFileName); }
1548 # if UI file fingerprinting is enabled and supported 1549 if ($this->UrlFingerprintingEnabled()
1550 && self::UrlFingerprintingRewriteSupport()
1551 && (isset($FileMTime) || file_exists($FoundFileName)))
1553 # if file does not appear to be a server-side inclusion 1554 if (!preg_match(
'/\.(html|php)$/i', $FoundFileName))
1556 # for each URL fingerprinting blacklist entry 1557 $OnBlacklist = FALSE;
1558 foreach ($this->UrlFingerprintBlacklist as $BlacklistEntry)
1560 # if entry looks like a regular expression pattern 1561 if ($BlacklistEntry[0] == substr($BlacklistEntry, -1))
1563 # check file name against regular expression 1564 if (preg_match($BlacklistEntry, $FoundFileName))
1566 $OnBlacklist = TRUE;
1572 # check file name directly against entry 1573 if (basename($FoundFileName) == $BlacklistEntry)
1575 $OnBlacklist = TRUE;
1581 # if file was not on blacklist 1584 # get file modification time if not already retrieved 1585 if (!isset($FileMTime))
1587 $FileMTime = filemtime($FoundFileName);
1590 # add timestamp fingerprint to file name 1591 $Fingerprint = sprintf(
"%06X",
1592 ($FileMTime % 0xFFFFFF));
1593 $FoundFileName = preg_replace(
"/^(.+)\.([a-z]+)$/",
1594 "$1.".$Fingerprint.
".$2",
1600 # return file name to caller 1601 return $FoundFileName;
1612 public function PUIFile($FileName)
1614 $FullFileName = $this->GUIFile($FileName);
1615 if ($FullFileName) { print($FullFileName); }
1632 public function IncludeUIFile($FileNames, $AdditionalAttributes = NULL)
1634 # convert file name to array if necessary 1635 if (!is_array($FileNames)) { $FileNames = array($FileNames); }
1637 # pad additional attributes if supplied 1638 $AddAttribs = $AdditionalAttributes ?
" ".$AdditionalAttributes :
"";
1641 foreach ($FileNames as $BaseFileName)
1643 # retrieve full file name 1644 $FileName = $this->GUIFile($BaseFileName);
1649 # print appropriate tag 1650 print $this->GetUIFileLoadingTag($FileName, $AdditionalAttributes);
1653 # if we are not already loading an override file 1654 if (!preg_match(
"/-Override.(css|scss|js)$/", $BaseFileName))
1656 # attempt to load override file if available 1657 $FileType = $this->GetFileType($BaseFileName);
1661 $OverrideFileName = preg_replace(
1662 "/\.(css|scss)$/",
"-Override.$1",
1664 $this->IncludeUIFile($OverrideFileName,
1665 $AdditionalAttributes);
1668 case self::FT_JAVASCRIPT:
1669 $OverrideFileName = preg_replace(
1670 "/\.js$/",
"-Override.js",
1672 $this->IncludeUIFile($OverrideFileName,
1673 $AdditionalAttributes);
1686 public function DoNotUrlFingerprint($Pattern)
1688 $this->UrlFingerprintBlacklist[] = $Pattern;
1701 public function RequireUIFile($FileName, $Order = self::ORDER_MIDDLE)
1703 $this->AdditionalRequiredUIFiles[$FileName] = $Order;
1711 public static function GetFileType($FileName)
1713 static $FileTypeCache;
1714 if (isset($FileTypeCache[$FileName]))
1716 return $FileTypeCache[$FileName];
1719 $FileSuffix = strtolower(substr($FileName, -3));
1720 if ($FileSuffix ==
"css")
1722 $FileTypeCache[$FileName] = self::FT_CSS;
1724 elseif ($FileSuffix ==
".js")
1726 $FileTypeCache[$FileName] = self::FT_JAVASCRIPT;
1728 elseif (($FileSuffix ==
"gif")
1729 || ($FileSuffix ==
"jpg")
1730 || ($FileSuffix ==
"png"))
1732 $FileTypeCache[$FileName] = self::FT_IMAGE;
1736 $FileTypeCache[$FileName] = self::FT_OTHER;
1739 return $FileTypeCache[$FileName];
1748 const FT_JAVASCRIPT = 3;
1758 public function LoadFunction($Callback)
1760 # if specified function is not currently available 1761 if (!is_callable($Callback))
1763 # if function info looks legal 1764 if (is_string($Callback) && strlen($Callback))
1766 # start with function directory list 1767 $Locations = $this->FunctionDirList;
1769 # add object directories to list 1770 $Locations = array_merge(
1771 $Locations, array_keys(self::$ObjectDirectories));
1773 # look for function file 1774 $FunctionFileName = $this->FindFile($Locations,
"F-".$Callback,
1775 array(
"php",
"html"));
1777 # if function file was found 1778 if ($FunctionFileName)
1780 # load function file 1781 include_once($FunctionFileName);
1785 # log error indicating function load failed 1786 $this->LogError(self::LOGLVL_ERROR,
"Unable to load function" 1787 .
" for callback \"".$Callback.
"\".");
1792 # log error indicating specified function info was bad 1793 $this->LogError(self::LOGLVL_ERROR,
"Unloadable callback value" 1795 .
" passed to AF::LoadFunction() by " 1796 .StdLib::GetMyCaller().
".");
1800 # report to caller whether function load succeeded 1801 return is_callable($Callback);
1808 public function GetElapsedExecutionTime()
1810 return microtime(TRUE) - $this->ExecutionStartTime;
1817 public function GetSecondsBeforeTimeout()
1819 return $this->MaxExecutionTime() - $this->GetElapsedExecutionTime();
1826 public function AddMetaTag($Attribs)
1828 # add new meta tag to list 1829 $this->MetaTags[] = $Attribs;
1835 # ---- Page Caching ------------------------------------------------------ 1845 public function PageCacheEnabled($NewValue =
DB_NOVALUE)
1847 return $this->UpdateSetting(
"PageCacheEnabled", $NewValue);
1856 public function PageCacheExpirationPeriod($NewValue =
DB_NOVALUE)
1858 return $this->UpdateSetting(
"PageCacheExpirationPeriod", $NewValue);
1865 public function DoNotCacheCurrentPage()
1867 $this->CacheCurrentPage = FALSE;
1876 public function AddPageCacheTag($Tag, $Pages = NULL)
1879 $Tag = strtolower($Tag);
1881 # if pages were supplied 1882 if ($Pages !== NULL)
1884 # add pages to list for this tag 1885 if (isset($this->PageCacheTags[$Tag]))
1887 $this->PageCacheTags[$Tag] = array_merge(
1888 $this->PageCacheTags[$Tag], $Pages);
1892 $this->PageCacheTags[$Tag] = $Pages;
1897 # add current page to list for this tag 1898 $this->PageCacheTags[$Tag][] =
"CURRENT";
1907 public function ClearPageCacheForTag($Tag)
1910 $TagId = $this->GetPageCacheTagId($Tag);
1912 # delete pages and tag/page connections for specified tag 1913 $this->DB->Query(
"DELETE CP, CPTI" 1914 .
" FROM AF_CachedPages CP, AF_CachedPageTagInts CPTI" 1915 .
" WHERE CPTI.TagId = ".intval($TagId)
1916 .
" AND CP.CacheId = CPTI.CacheId");
1922 public function ClearPageCache()
1924 # clear all page cache tables 1925 $this->DB->Query(
"TRUNCATE TABLE AF_CachedPages");
1926 $this->DB->Query(
"TRUNCATE TABLE AF_CachedPageTags");
1927 $this->DB->Query(
"TRUNCATE TABLE AF_CachedPageTagInts");
1936 public function GetPageCacheInfo()
1938 $Length = $this->DB->Query(
"SELECT COUNT(*) AS CacheLen" 1939 .
" FROM AF_CachedPages",
"CacheLen");
1940 $Oldest = $this->DB->Query(
"SELECT CachedAt FROM AF_CachedPages" 1941 .
" ORDER BY CachedAt ASC LIMIT 1",
"CachedAt");
1943 "NumberOfEntries" => $Length,
1944 "OldestTimestamp" => strtotime($Oldest),
1951 # ---- Logging ----------------------------------------------------------- 1968 public function LogSlowPageLoads(
1971 return $this->UpdateSetting(
1972 "LogSlowPageLoads", $NewValue, $Persistent);
1985 public function SlowPageLoadThreshold(
1988 return $this->UpdateSetting(
1989 "SlowPageLoadThreshold", $NewValue, $Persistent);
2005 public function LogHighMemoryUsage(
2008 return $this->UpdateSetting(
2009 "LogHighMemoryUsage", $NewValue, $Persistent);
2023 public function HighMemoryUsageThreshold(
2026 return $this->UpdateSetting(
2027 "HighMemoryUsageThreshold", $NewValue, $Persistent);
2043 public function LogError($Level, $Msg)
2045 # if error level is at or below current logging level 2046 if ($this->Settings[
"LoggingLevel"] >= $Level)
2048 # attempt to log error message 2049 $Result = $this->LogMessage($Level, $Msg);
2051 # if logging attempt failed and level indicated significant error 2052 if (($Result === FALSE) && ($Level <= self::LOGLVL_ERROR))
2054 # throw exception about inability to log error 2055 static $AlreadyThrewException = FALSE;
2056 if (!$AlreadyThrewException)
2058 $AlreadyThrewException = TRUE;
2059 throw new Exception(
"Unable to log error (".$Level.
": ".$Msg
2060 .
") to ".$this->LogFileName);
2064 # report to caller whether message was logged 2069 # report to caller that message was not logged 2085 public function LogMessage($Level, $Msg)
2087 # if message level is at or below current logging level 2088 if ($this->Settings[
"LoggingLevel"] >= $Level)
2090 # attempt to open log file 2091 $FHndl = @fopen($this->LogFileName,
"a");
2093 # if log file could not be open 2094 if ($FHndl === FALSE)
2096 # report to caller that message was not logged 2102 $ErrorAbbrevs = array(
2103 self::LOGLVL_FATAL =>
"FTL",
2104 self::LOGLVL_ERROR =>
"ERR",
2105 self::LOGLVL_WARNING =>
"WRN",
2106 self::LOGLVL_INFO =>
"INF",
2107 self::LOGLVL_DEBUG =>
"DBG",
2108 self::LOGLVL_TRACE =>
"TRC",
2110 $Msg = str_replace(array(
"\n",
"\t",
"\r"),
" ", $Msg);
2111 $Msg = substr(trim($Msg), 0, self::LOGFILE_MAX_LINE_LENGTH);
2112 $LogEntry = date(
"Y-m-d H:i:s")
2113 .
" ".($this->RunningInBackground ?
"B" :
"F")
2114 .
" ".$ErrorAbbrevs[$Level]
2117 # write entry to log 2118 $Success = fwrite($FHndl, $LogEntry.
"\n");
2123 # report to caller whether message was logged 2124 return ($Success === FALSE) ? FALSE : TRUE;
2129 # report to caller that message was not logged 2155 public function LoggingLevel($NewValue =
DB_NOVALUE)
2157 # constrain new level (if supplied) to within legal bounds 2160 $NewValue = max(min($NewValue, 6), 1);
2163 # set new logging level (if supplied) and return current level to caller 2164 return $this->UpdateSetting(
"LoggingLevel", $NewValue);
2173 public function LogFile($NewValue = NULL)
2175 if ($NewValue !== NULL) { $this->LogFileName = $NewValue; }
2176 return $this->LogFileName;
2188 public function GetLogEntries($Limit = 0)
2190 # return no entries if there isn't a log file 2191 # or we can't read it or it's empty 2192 $LogFile = $this->LogFile();
2193 if (!is_readable($LogFile) || !filesize($LogFile))
2198 # if max number of entries specified 2201 # load lines from file 2202 $FHandle = fopen($LogFile,
"r");
2203 $FileSize = filesize($LogFile);
2204 $SeekPosition = max(0,
2205 ($FileSize - (self::LOGFILE_MAX_LINE_LENGTH
2207 fseek($FHandle, $SeekPosition);
2208 $Block = fread($FHandle, ($FileSize - $SeekPosition));
2210 $Lines = explode(PHP_EOL, $Block);
2213 # prune array back to requested number of entries 2214 $Lines = array_slice($Lines, (0 - $Limit));
2218 # load all lines from log file 2219 $Lines = file($LogFile, FILE_IGNORE_NEW_LINES);
2220 if ($Lines === FALSE)
2226 # reverse line order 2227 $Lines = array_reverse($Lines);
2229 # for each log file line 2231 foreach ($Lines as $Line)
2233 # attempt to parse line into component parts 2234 $Pieces = explode(
" ", $Line, 5);
2235 $Date = isset($Pieces[0]) ? $Pieces[0] :
"";
2236 $Time = isset($Pieces[1]) ? $Pieces[1] :
"";
2237 $Back = isset($Pieces[2]) ? $Pieces[2] :
"";
2238 $Level = isset($Pieces[3]) ? $Pieces[3] :
"";
2239 $Msg = isset($Pieces[4]) ? $Pieces[4] :
"";
2241 # skip line if it looks invalid 2242 $ErrorAbbrevs = array(
2243 "FTL" => self::LOGLVL_FATAL,
2244 "ERR" => self::LOGLVL_ERROR,
2245 "WRN" => self::LOGLVL_WARNING,
2246 "INF" => self::LOGLVL_INFO,
2247 "DBG" => self::LOGLVL_DEBUG,
2248 "TRC" => self::LOGLVL_TRACE,
2250 if ((($Back !=
"F") && ($Back !=
"B"))
2251 || !array_key_exists($Level, $ErrorAbbrevs)
2257 # convert parts into appropriate values and add to entries 2259 "Time" => strtotime($Date.
" ".$Time),
2260 "Background" => ($Back ==
"B") ? TRUE : FALSE,
2261 "Level" => $ErrorAbbrevs[$Level],
2266 # return entries to caller 2274 const LOGLVL_TRACE = 6;
2279 const LOGLVL_DEBUG = 5;
2285 const LOGLVL_INFO = 4;
2290 const LOGLVL_WARNING = 3;
2296 const LOGLVL_ERROR = 2;
2301 const LOGLVL_FATAL = 1;
2306 const LOGFILE_MAX_LINE_LENGTH = 2048;
2311 # ---- Event Handling ---------------------------------------------------- 2318 const EVENTTYPE_DEFAULT = 1;
2324 const EVENTTYPE_CHAIN = 2;
2330 const EVENTTYPE_FIRST = 3;
2338 const EVENTTYPE_NAMED = 4;
2341 const ORDER_FIRST = 1;
2343 const ORDER_MIDDLE = 2;
2345 const ORDER_LAST = 3;
2355 public function RegisterEvent($EventsOrEventName, $EventType = NULL)
2357 # convert parameters to array if not already in that form 2358 $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2359 : array($EventsOrEventName => $EventType);
2362 foreach ($Events as $Name => $Type)
2364 # store event information 2365 $this->RegisteredEvents[$Name][
"Type"] = $Type;
2366 $this->RegisteredEvents[$Name][
"Hooks"] = array();
2376 public function IsRegisteredEvent($EventName)
2378 return array_key_exists($EventName, $this->RegisteredEvents)
2388 public function IsHookedEvent($EventName)
2390 # the event isn't hooked to if it isn't even registered 2391 if (!$this->IsRegisteredEvent($EventName))
2396 # return TRUE if there is at least one callback hooked to the event 2397 return count($this->RegisteredEvents[$EventName][
"Hooks"]) > 0;
2413 public function HookEvent(
2414 $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2416 # convert parameters to array if not already in that form 2417 $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2418 : array($EventsOrEventName => $Callback);
2422 foreach ($Events as $EventName => $EventCallback)
2424 # if callback is valid 2425 if (is_callable($EventCallback))
2427 # if this is a periodic event we process internally 2428 if (isset($this->PeriodicEvents[$EventName]))
2431 $this->ProcessPeriodicEvent($EventName, $EventCallback);
2433 # if specified event has been registered 2434 elseif (isset($this->RegisteredEvents[$EventName]))
2436 # add callback for event 2437 $this->RegisteredEvents[$EventName][
"Hooks"][]
2438 = array(
"Callback" => $EventCallback,
"Order" => $Order);
2440 # sort callbacks by order 2441 if (count($this->RegisteredEvents[$EventName][
"Hooks"]) > 1)
2443 usort($this->RegisteredEvents[$EventName][
"Hooks"],
2446 $A[
"Order"], $B[
"Order"]);
2461 # report to caller whether all callbacks were hooked 2478 public function UnhookEvent(
2479 $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2481 # convert parameters to array if not already in that form 2482 $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2483 : array($EventsOrEventName => $Callback);
2487 foreach ($Events as $EventName => $EventCallback)
2489 # if this event has been registered and hooked 2490 if (isset($this->RegisteredEvents[$EventName])
2491 && count($this->RegisteredEvents[$EventName]))
2493 # if this callback has been hooked for this event 2494 $CallbackData = array(
"Callback" => $EventCallback,
"Order" => $Order);
2495 if (in_array($CallbackData,
2496 $this->RegisteredEvents[$EventName][
"Hooks"]))
2499 $HookIndex = array_search($CallbackData,
2500 $this->RegisteredEvents[$EventName][
"Hooks"]);
2501 unset($this->RegisteredEvents[$EventName][
"Hooks"][$HookIndex]);
2507 # report number of callbacks unhooked to caller 2508 return $UnhookCount;
2521 public function SignalEvent($EventName, $Parameters = NULL)
2523 $ReturnValue = NULL;
2525 # if event has been registered 2526 if (isset($this->RegisteredEvents[$EventName]))
2528 # set up default return value (if not NULL) 2529 switch ($this->RegisteredEvents[$EventName][
"Type"])
2531 case self::EVENTTYPE_CHAIN:
2532 $ReturnValue = $Parameters;
2535 case self::EVENTTYPE_NAMED:
2536 $ReturnValue = array();
2540 # for each callback for this event 2541 foreach ($this->RegisteredEvents[$EventName][
"Hooks"] as $Hook)
2544 $Callback = $Hook[
"Callback"];
2545 $Result = ($Parameters !== NULL)
2546 ? call_user_func_array($Callback, $Parameters)
2547 : call_user_func($Callback);
2549 # process return value based on event type 2550 switch ($this->RegisteredEvents[$EventName][
"Type"])
2552 case self::EVENTTYPE_CHAIN:
2553 if ($Result !== NULL)
2555 foreach ($Parameters as $Index => $Value)
2557 if (array_key_exists($Index, $Result))
2559 $Parameters[$Index] = $Result[$Index];
2562 $ReturnValue = $Parameters;
2566 case self::EVENTTYPE_FIRST:
2567 if ($Result !== NULL)
2569 $ReturnValue = $Result;
2574 case self::EVENTTYPE_NAMED:
2575 $CallbackName = is_array($Callback)
2576 ? (is_object($Callback[0])
2577 ? get_class($Callback[0])
2578 : $Callback[0]).
"::".$Callback[1]
2580 $ReturnValue[$CallbackName] = $Result;
2590 $this->LogError(self::LOGLVL_WARNING,
2591 "Unregistered event (".$EventName.
") signaled by " 2592 .StdLib::GetMyCaller().
".");
2595 # return value if any to caller 2596 return $ReturnValue;
2604 public function IsStaticOnlyEvent($EventName)
2606 return isset($this->PeriodicEvents[$EventName]) ? TRUE : FALSE;
2619 public function EventWillNextRunAt($EventName, $Callback)
2621 # if event is not a periodic event report failure to caller 2622 if (!array_key_exists($EventName, $this->EventPeriods)) {
return FALSE; }
2624 # retrieve last execution time for event if available 2625 $Signature = self::GetCallbackSignature($Callback);
2626 $LastRunTime = $this->DB->Query(
"SELECT LastRunAt FROM PeriodicEvents" 2627 .
" WHERE Signature = '".addslashes($Signature).
"'",
"LastRunAt");
2629 # if event was not found report failure to caller 2630 if ($LastRunTime === NULL) {
return FALSE; }
2632 # calculate next run time based on event period 2633 $NextRunTime = strtotime($LastRunTime) + $this->EventPeriods[$EventName];
2635 # report next run time to caller 2636 return $NextRunTime;
2654 public function GetKnownPeriodicEvents()
2656 # retrieve last execution times 2657 $this->DB->Query(
"SELECT * FROM PeriodicEvents");
2658 $LastRunTimes = $this->DB->FetchColumn(
"LastRunAt",
"Signature");
2660 # for each known event 2662 foreach ($this->KnownPeriodicEvents as $Signature => $Info)
2664 # if last run time for event is available 2665 if (array_key_exists($Signature, $LastRunTimes))
2667 # calculate next run time for event 2668 $LastRun = strtotime($LastRunTimes[$Signature]);
2669 $NextRun = $LastRun + $this->EventPeriods[$Info[
"Period"]];
2670 if ($Info[
"Period"] ==
"EVENT_PERIODIC") { $LastRun = FALSE; }
2674 # set info to indicate run times are not known 2679 # add event info to list 2680 $Events[$Signature] = $Info;
2681 $Events[$Signature][
"LastRun"] = $LastRun;
2682 $Events[$Signature][
"NextRun"] = $NextRun;
2683 $Events[$Signature][
"Parameters"] = NULL;
2686 # return list of known events to caller 2696 public static function RunPeriodicEvent($EventName, $Callback, $Parameters)
2699 if (!isset($DB)) { $DB =
new Database(); }
2702 $ReturnVal = call_user_func_array($Callback, $Parameters);
2704 # if event is already in database 2705 $Signature = self::GetCallbackSignature($Callback);
2706 if ($DB->Query(
"SELECT COUNT(*) AS EventCount FROM PeriodicEvents" 2707 .
" WHERE Signature = '".addslashes($Signature).
"'",
"EventCount"))
2709 # update last run time for event 2710 $DB->Query(
"UPDATE PeriodicEvents SET LastRunAt = " 2711 .(($EventName ==
"EVENT_PERIODIC")
2712 ?
"'".date(
"Y-m-d H:i:s", time() + ($ReturnVal * 60)).
"'" 2714 .
" WHERE Signature = '".addslashes($Signature).
"'");
2718 # add last run time for event to database 2719 $DB->Query(
"INSERT INTO PeriodicEvents (Signature, LastRunAt) VALUES " 2720 .
"('".addslashes($Signature).
"', " 2721 .(($EventName ==
"EVENT_PERIODIC")
2722 ?
"'".date(
"Y-m-d H:i:s", time() + ($ReturnVal * 60)).
"'" 2730 # ---- Task Management --------------------------------------------------- 2735 const PRIORITY_HIGH = 1;
2737 const PRIORITY_MEDIUM = 2;
2739 const PRIORITY_LOW = 3;
2741 const PRIORITY_BACKGROUND = 4;
2755 public function QueueTask($Callback, $Parameters = NULL,
2756 $Priority = self::PRIORITY_LOW, $Description =
"")
2758 # pack task info and write to database 2759 if ($Parameters === NULL) { $Parameters = array(); }
2760 $this->DB->Query(
"INSERT INTO TaskQueue" 2761 .
" (Callback, Parameters, Priority, Description)" 2762 .
" VALUES ('".addslashes(serialize($Callback)).
"', '" 2763 .addslashes(serialize($Parameters)).
"', ".intval($Priority).
", '" 2764 .addslashes($Description).
"')");
2784 public function QueueUniqueTask($Callback, $Parameters = NULL,
2785 $Priority = self::PRIORITY_LOW, $Description =
"")
2787 if ($this->TaskIsInQueue($Callback, $Parameters))
2789 $QueryResult = $this->DB->Query(
"SELECT TaskId,Priority FROM TaskQueue" 2790 .
" WHERE Callback = '".addslashes(serialize($Callback)).
"'" 2791 .($Parameters ?
" AND Parameters = '" 2792 .addslashes(serialize($Parameters)).
"'" :
""));
2793 if ($QueryResult !== FALSE)
2795 $Record = $this->DB->FetchRow();
2796 if ($Record[
"Priority"] > $Priority)
2798 $this->DB->Query(
"UPDATE TaskQueue" 2799 .
" SET Priority = ".intval($Priority)
2800 .
" WHERE TaskId = ".intval($Record[
"TaskId"]));
2807 $this->QueueTask($Callback, $Parameters, $Priority, $Description);
2821 public function TaskIsInQueue($Callback, $Parameters = NULL)
2823 $QueuedCount = $this->DB->Query(
2824 "SELECT COUNT(*) AS FoundCount FROM TaskQueue" 2825 .
" WHERE Callback = '".addslashes(serialize($Callback)).
"'" 2826 .($Parameters ?
" AND Parameters = '" 2827 .addslashes(serialize($Parameters)).
"'" :
""),
2829 $RunningCount = $this->DB->Query(
2830 "SELECT COUNT(*) AS FoundCount FROM RunningTasks" 2831 .
" WHERE Callback = '".addslashes(serialize($Callback)).
"'" 2832 .($Parameters ?
" AND Parameters = '" 2833 .addslashes(serialize($Parameters)).
"'" :
""),
2835 $FoundCount = $QueuedCount + $RunningCount;
2836 return ($FoundCount ? TRUE : FALSE);
2844 public function GetTaskQueueSize($Priority = NULL)
2846 return $this->GetQueuedTaskCount(NULL, NULL, $Priority);
2856 public function GetQueuedTaskList($Count = 100, $Offset = 0)
2858 return $this->GetTaskList(
"SELECT * FROM TaskQueue" 2859 .
" ORDER BY Priority, TaskId ", $Count, $Offset);
2875 public function GetQueuedTaskCount($Callback = NULL,
2876 $Parameters = NULL, $Priority = NULL, $Description = NULL)
2878 $Query =
"SELECT COUNT(*) AS TaskCount FROM TaskQueue";
2880 if ($Callback !== NULL)
2882 $Query .= $Sep.
" Callback = '".addslashes(serialize($Callback)).
"'";
2885 if ($Parameters !== NULL)
2887 $Query .= $Sep.
" Parameters = '".addslashes(serialize($Parameters)).
"'";
2890 if ($Priority !== NULL)
2892 $Query .= $Sep.
" Priority = ".intval($Priority);
2895 if ($Description !== NULL)
2897 $Query .= $Sep.
" Description = '".addslashes($Description).
"'";
2899 return $this->DB->Query($Query,
"TaskCount");
2909 public function GetRunningTaskList($Count = 100, $Offset = 0)
2911 return $this->GetTaskList(
"SELECT * FROM RunningTasks" 2912 .
" WHERE StartedAt >= '".date(
"Y-m-d H:i:s",
2913 (time() - $this->MaxExecutionTime())).
"'" 2914 .
" ORDER BY StartedAt", $Count, $Offset);
2924 public function GetOrphanedTaskList($Count = 100, $Offset = 0)
2926 return $this->GetTaskList(
"SELECT * FROM RunningTasks" 2927 .
" WHERE StartedAt < '".date(
"Y-m-d H:i:s",
2928 (time() - $this->MaxExecutionTime())).
"'" 2929 .
" ORDER BY StartedAt", $Count, $Offset);
2936 public function GetOrphanedTaskCount()
2938 return $this->DB->Query(
"SELECT COUNT(*) AS Count FROM RunningTasks" 2939 .
" WHERE StartedAt < '".date(
"Y-m-d H:i:s",
2940 (time() - $this->MaxExecutionTime())).
"'",
2949 public function ReQueueOrphanedTask($TaskId, $NewPriority = NULL)
2951 $this->DB->Query(
"LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
2952 $this->DB->Query(
"INSERT INTO TaskQueue" 2953 .
" (Callback,Parameters,Priority,Description) " 2954 .
"SELECT Callback, Parameters, Priority, Description" 2955 .
" FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2956 if ($NewPriority !== NULL)
2958 $NewTaskId = $this->DB->LastInsertId();
2959 $this->DB->Query(
"UPDATE TaskQueue SET Priority = " 2960 .intval($NewPriority)
2961 .
" WHERE TaskId = ".intval($NewTaskId));
2963 $this->DB->Query(
"DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2964 $this->DB->Query(
"UNLOCK TABLES");
2973 public function RequeueCurrentTask($NewValue = TRUE)
2975 $this->RequeueCurrentTask = $NewValue;
2983 public function DeleteTask($TaskId)
2985 $this->DB->Query(
"DELETE FROM TaskQueue WHERE TaskId = ".intval($TaskId));
2986 $TasksRemoved = $this->DB->NumRowsAffected();
2987 $this->DB->Query(
"DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2988 $TasksRemoved += $this->DB->NumRowsAffected();
2989 return $TasksRemoved;
2999 public function GetTask($TaskId)
3001 # assume task will not be found 3004 # look for task in task queue 3005 $this->DB->Query(
"SELECT * FROM TaskQueue WHERE TaskId = ".intval($TaskId));
3007 # if task was not found in queue 3008 if (!$this->DB->NumRowsSelected())
3010 # look for task in running task list 3011 $this->DB->Query(
"SELECT * FROM RunningTasks WHERE TaskId = " 3016 if ($this->DB->NumRowsSelected())
3018 # if task was periodic 3019 $Row = $this->DB->FetchRow();
3020 if ($Row[
"Callback"] ==
3021 serialize(array(
"ApplicationFramework",
"RunPeriodicEvent")))
3023 # unpack periodic task callback 3024 $WrappedCallback = unserialize($Row[
"Parameters"]);
3025 $Task[
"Callback"] = $WrappedCallback[1];
3026 $Task[
"Parameters"] = $WrappedCallback[2];
3030 # unpack task callback and parameters 3031 $Task[
"Callback"] = unserialize($Row[
"Callback"]);
3032 $Task[
"Parameters"] = unserialize($Row[
"Parameters"]);
3036 # return task to caller 3047 public function TaskExecutionEnabled($NewValue =
DB_NOVALUE)
3049 return $this->UpdateSetting(
"TaskExecutionEnabled", $NewValue);
3057 public function MaxTasks($NewValue =
DB_NOVALUE)
3059 return $this->UpdateSetting(
"MaxTasksRunning", $NewValue);
3069 public static function GetTaskCallbackSynopsis($TaskInfo)
3071 # if task callback is function use function name 3072 $Callback = $TaskInfo[
"Callback"];
3074 if (!is_array($Callback))
3080 # if task callback is object 3081 if (is_object($Callback[0]))
3083 # if task callback is encapsulated ask encapsulation for name 3084 if (method_exists($Callback[0],
"GetCallbackAsText"))
3086 $Name = $Callback[0]->GetCallbackAsText();
3088 # else assemble name from object 3091 $Name = get_class($Callback[0]) .
"::" . $Callback[1];
3094 # else assemble name from supplied info 3097 $Name= $Callback[0] .
"::" . $Callback[1];
3101 # if parameter array was supplied 3102 $Parameters = $TaskInfo[
"Parameters"];
3103 $ParameterString =
"";
3104 if (is_array($Parameters))
3106 # assemble parameter string 3108 foreach ($Parameters as $Parameter)
3110 $ParameterString .= $Separator;
3111 if (is_int($Parameter) || is_float($Parameter))
3113 $ParameterString .= $Parameter;
3115 else if (is_string($Parameter))
3117 $ParameterString .=
"\"".htmlspecialchars($Parameter).
"\"";
3119 else if (is_array($Parameter))
3121 $ParameterString .=
"ARRAY";
3123 else if (is_object($Parameter))
3125 $ParameterString .=
"OBJECT";
3127 else if (is_null($Parameter))
3129 $ParameterString .=
"NULL";
3131 else if (is_bool($Parameter))
3133 $ParameterString .= $Parameter ?
"TRUE" :
"FALSE";
3135 else if (is_resource($Parameter))
3137 $ParameterString .= get_resource_type($Parameter);
3141 $ParameterString .=
"????";
3147 # assemble name and parameters and return result to caller 3148 return $Name.
"(".$ParameterString.
")";
3155 public function IsRunningInBackground()
3157 return $this->RunningInBackground;
3165 public function GetCurrentBackgroundPriority()
3167 return isset($this->RunningTask)
3168 ? $this->RunningTask[
"Priority"] : NULL;
3179 public function GetNextHigherBackgroundPriority($Priority = NULL)
3181 if ($Priority === NULL)
3183 $Priority = $this->GetCurrentBackgroundPriority();
3184 if ($Priority === NULL)
3189 return ($Priority > self::PRIORITY_HIGH)
3190 ? ($Priority - 1) : self::PRIORITY_HIGH;
3201 public function GetNextLowerBackgroundPriority($Priority = NULL)
3203 if ($Priority === NULL)
3205 $Priority = $this->GetCurrentBackgroundPriority();
3206 if ($Priority === NULL)
3211 return ($Priority < self::PRIORITY_BACKGROUND)
3212 ? ($Priority + 1) : self::PRIORITY_BACKGROUND;
3218 # ---- Clean URL Support ------------------------------------------------- 3248 public function AddCleanUrl($Pattern, $Page, $GetVars = NULL, $Template = NULL)
3250 # save clean URL mapping parameters 3251 $this->CleanUrlMappings[] = array(
3252 "Pattern" => $Pattern,
3254 "GetVars" => $GetVars,
3257 # if replacement template specified 3258 if ($Template !== NULL)
3260 # if GET parameters specified 3261 if (count($GetVars))
3263 # retrieve all possible permutations of GET parameters 3266 # for each permutation of GET parameters 3267 foreach ($GetPerms as $VarPermutation)
3269 # construct search pattern for permutation 3270 $SearchPattern =
"/href=([\"'])index\\.php\\?P=".$Page;
3271 $GetVarSegment =
"";
3272 foreach ($VarPermutation as $GetVar)
3274 if (preg_match(
"%\\\$[0-9]+%", $GetVars[$GetVar]))
3276 $GetVarSegment .=
"&".$GetVar.
"=((?:(?!\\1)[^&])+)";
3280 $GetVarSegment .=
"&".$GetVar.
"=".$GetVars[$GetVar];
3283 $SearchPattern .= $GetVarSegment.
"\\1/i";
3285 # if template is actually a callback 3286 if (is_callable($Template))
3288 # add pattern to HTML output mod callbacks list 3289 $this->OutputModificationCallbacks[] = array(
3290 "Pattern" => $Pattern,
3292 "SearchPattern" => $SearchPattern,
3293 "Callback" => $Template,
3298 # construct replacement string for permutation 3299 $Replacement = $Template;
3301 foreach ($VarPermutation as $GetVar)
3303 $Replacement = str_replace(
3304 "\$".$GetVar,
"\$".$Index, $Replacement);
3307 $Replacement =
"href=\"".$Replacement.
"\"";
3309 # add pattern to HTML output modifications list 3310 $this->OutputModificationPatterns[] = $SearchPattern;
3311 $this->OutputModificationReplacements[] = $Replacement;
3317 # construct search pattern 3318 $SearchPattern =
"/href=\"index\\.php\\?P=".$Page.
"\"/i";
3320 # if template is actually a callback 3321 if (is_callable($Template))
3323 # add pattern to HTML output mod callbacks list 3324 $this->OutputModificationCallbacks[] = array(
3325 "Pattern" => $Pattern,
3327 "SearchPattern" => $SearchPattern,
3328 "Callback" => $Template,
3333 # add simple pattern to HTML output modifications list 3334 $this->OutputModificationPatterns[] = $SearchPattern;
3335 $this->OutputModificationReplacements[] =
"href=\"".$Template.
"\"";
3346 public function CleanUrlIsMapped($Path)
3348 foreach ($this->CleanUrlMappings as $Info)
3350 if (preg_match($Info[
"Pattern"], $Path))
3367 public function GetCleanUrlForPath($Path)
3369 # the search patterns and callbacks require a specific format 3370 $Format =
"href=\"".str_replace(
"&",
"&", $Path).
"\"";
3373 # perform any regular expression replacements on the search string 3374 $Search = preg_replace($this->OutputModificationPatterns,
3375 $this->OutputModificationReplacements, $Search);
3377 # only run the callbacks if a replacement hasn't already been performed 3378 if ($Search == $Format)
3380 # perform any callback replacements on the search string 3381 foreach ($this->OutputModificationCallbacks as $Info)
3383 # make the information available to the callback 3384 $this->OutputModificationCallbackInfo = $Info;
3386 # execute the callback 3387 $Search = preg_replace_callback($Info[
"SearchPattern"],
3388 array($this,
"OutputModificationCallbackShell"),
3393 # return the path untouched if no replacements were performed 3394 if ($Search == $Format)
3399 # remove the bits added to the search string to get it recognized by 3400 # the replacement expressions and callbacks 3401 $Result = substr($Search, 6, -1);
3412 public function GetUncleanUrlForPath($Path)
3414 # for each clean URL mapping 3415 foreach ($this->CleanUrlMappings as $Info)
3417 # if current path matches the clean URL pattern 3418 if (preg_match($Info[
"Pattern"], $Path, $Matches))
3420 # the GET parameters for the URL, starting with the page name 3421 $GetVars = array(
"P" => $Info[
"Page"]);
3423 # if additional $_GET variables specified for clean URL 3424 if ($Info[
"GetVars"] !== NULL)
3426 # for each $_GET variable specified for clean URL 3427 foreach ($Info[
"GetVars"] as $VarName => $VarTemplate)
3429 # start with template for variable value 3430 $Value = $VarTemplate;
3432 # for each subpattern matched in current URL 3433 foreach ($Matches as $Index => $Match)
3435 # if not first (whole) match 3438 # make any substitutions in template 3439 $Value = str_replace(
"$".$Index, $Match, $Value);
3443 # add the GET variable 3444 $GetVars[$VarName] = $Value;
3448 # return the unclean URL 3449 return "index.php?" . http_build_query($GetVars);
3453 # return the path unchanged 3462 public function GetCleanUrl()
3464 return $this->GetCleanUrlForPath($this->GetUncleanUrl());
3471 public function GetUncleanUrl()
3473 $GetVars = array(
"P" => $this->GetPageName()) + $_GET;
3474 return "index.php?" . http_build_query($GetVars);
3488 public function AddPrefixForAlternateDomain($Domain, $Prefix)
3490 $this->AlternateDomainPrefixes[$Domain] = $Prefix;
3495 # ---- Server Environment ------------------------------------------------ 3504 public static function SessionLifetime($NewValue = NULL)
3506 if ($NewValue !== NULL)
3508 self::$SessionLifetime = $NewValue;
3510 return self::$SessionLifetime;
3518 public static function HtaccessSupport()
3520 return isset($_SERVER[
"HTACCESS_SUPPORT"])
3521 || isset($_SERVER[
"REDIRECT_HTACCESS_SUPPORT"]);
3530 public static function UrlFingerprintingRewriteSupport()
3532 return isset($_SERVER[
"URL_FINGERPRINTING_SUPPORT"])
3533 || isset($_SERVER[
"REDIRECT_URL_FINGERPRINTING_SUPPORT"]);
3542 public static function ScssRewriteSupport()
3544 return isset($_SERVER[
"SCSS_REWRITE_SUPPORT"])
3545 || isset($_SERVER[
"REDIRECT_SCSS_REWRITE_SUPPORT"]);
3554 public static function JsMinRewriteSupport()
3556 return isset($_SERVER[
"JSMIN_REWRITE_SUPPORT"])
3557 || isset($_SERVER[
"REDIRECT_JSMIN_REWRITE_SUPPORT"]);
3567 public static function RootUrl()
3569 # return override value if one is set 3570 if (self::$RootUrlOverride !== NULL)
3572 return self::$RootUrlOverride;
3575 # determine scheme name 3576 $Protocol = (isset($_SERVER[
"HTTPS"]) ?
"https" :
"http");
3578 # if HTTP_HOST is preferred or SERVER_NAME points to localhost 3579 # and HTTP_HOST is set 3580 if ((self::$PreferHttpHost || ($_SERVER[
"SERVER_NAME"] ==
"127.0.0.1"))
3581 && isset($_SERVER[
"HTTP_HOST"]))
3583 # use HTTP_HOST for domain name 3584 $DomainName = $_SERVER[
"HTTP_HOST"];
3588 # use SERVER_NAME for domain name 3589 $DomainName = $_SERVER[
"SERVER_NAME"];
3592 # build URL root and return to caller 3593 return $Protocol.
"://".$DomainName;
3610 public static function RootUrlOverride($NewValue = self::NOVALUE)
3612 if ($NewValue !== self::NOVALUE)
3614 self::$RootUrlOverride = strlen(trim($NewValue)) ? $NewValue : NULL;
3616 return self::$RootUrlOverride;
3628 public static function BaseUrl()
3630 $BaseUrl = self::RootUrl().dirname($_SERVER[
"SCRIPT_NAME"]);
3631 if (substr($BaseUrl, -1) !=
"/") { $BaseUrl .=
"/"; }
3642 public static function FullUrl()
3644 return self::RootUrl().$_SERVER[
"REQUEST_URI"];
3657 public static function PreferHttpHost($NewValue = NULL)
3659 if ($NewValue !== NULL)
3661 self::$PreferHttpHost = ($NewValue ? TRUE : FALSE);
3663 return self::$PreferHttpHost;
3670 public static function BasePath()
3672 $BasePath = dirname($_SERVER[
"SCRIPT_NAME"]);
3674 if (substr($BasePath, -1) !=
"/")
3687 public static function GetScriptUrl()
3689 if (array_key_exists(
"SCRIPT_URL", $_SERVER))
3691 return $_SERVER[
"SCRIPT_URL"];
3693 elseif (array_key_exists(
"REQUEST_URI", $_SERVER))
3695 $Pieces = parse_url($_SERVER[
"REQUEST_URI"]);
3696 return isset($Pieces[
"path"]) ? $Pieces[
"path"] : NULL;
3698 elseif (array_key_exists(
"REDIRECT_URL", $_SERVER))
3700 return $_SERVER[
"REDIRECT_URL"];
3716 public static function WasUrlRewritten($ScriptName=
"index.php")
3718 # needed to get the path of the URL minus the query and fragment pieces 3719 $Components = parse_url(self::GetScriptUrl());
3721 # if parsing was successful and a path is set 3722 if (is_array($Components) && isset($Components[
"path"]))
3724 $BasePath = self::BasePath();
3725 $Path = $Components[
"path"];
3727 # the URL was rewritten if the path isn't the base path, i.e., the 3728 # home page, and the file in the URL isn't the script generating the 3730 if ($BasePath != $Path && basename($Path) != $ScriptName)
3736 # the URL wasn't rewritten 3746 public static function ReachedViaAjax()
3748 return (isset($_SERVER[
"HTTP_X_REQUESTED_WITH"])
3749 && (strtolower($_SERVER[
"HTTP_X_REQUESTED_WITH"])
3750 ==
"xmlhttprequest"))
3759 public static function GetFreeMemory()
3761 return self::GetPhpMemoryLimit() - memory_get_usage(TRUE);
3769 public static function GetPhpMemoryLimit()
3771 $Str = strtoupper(ini_get(
"memory_limit"));
3772 if (substr($Str, -1) ==
"B") { $Str = substr($Str, 0, strlen($Str) - 1); }
3773 switch (substr($Str, -1))
3776 $MemoryLimit = (int)$Str * 1024;
3780 $MemoryLimit = (int)$Str * 1048576;
3784 $MemoryLimit = (int)$Str * 1073741824;
3788 $MemoryLimit = (int)$Str;
3791 return $MemoryLimit;
3806 public function MaxExecutionTime($NewValue =
DB_NOVALUE, $Persistent = FALSE)
3810 $NewValue = max($NewValue, 5);
3811 ini_set(
"max_execution_time", $NewValue);
3812 set_time_limit($NewValue - $this->GetElapsedExecutionTime());
3813 $this->UpdateSetting(
"MaxExecTime", $NewValue, $Persistent);
3815 return ini_get(
"max_execution_time");
3821 # ---- Utility ----------------------------------------------------------- 3836 public function DownloadFile($FilePath, $FileName = NULL, $MimeType = NULL)
3838 # check that file is readable 3839 if (!is_readable($FilePath))
3844 # if file name was not supplied 3845 if ($FileName === NULL)
3847 # extract file name from path 3848 $FileName = basename($FilePath);
3851 # if MIME type was not supplied 3852 if ($MimeType === NULL)
3854 # attempt to determine MIME type 3855 $FInfoHandle = finfo_open(FILEINFO_MIME);
3858 $FInfoMime = finfo_file($FInfoHandle, $FilePath);
3859 finfo_close($FInfoHandle);
3862 $MimeType = $FInfoMime;
3866 # use default if unable to determine MIME type 3867 if ($MimeType === NULL)
3869 $MimeType =
"application/octet-stream";
3873 # set headers to download file 3874 header(
"Content-Type: ".$MimeType);
3875 header(
"Content-Length: ".filesize($FilePath));
3876 if ($this->CleanUrlRewritePerformed)
3878 header(
'Content-Disposition: attachment; filename="'.$FileName.
'"');
3881 # make sure that apache does not attempt to compress file 3882 apache_setenv(
'no-gzip',
'1');
3884 # send file to user, but unbuffered to avoid memory issues 3885 $this->AddUnbufferedCallback(
function ($File)
3887 $BlockSize = 512000;
3889 $Handle = @fopen($File,
"rb");
3890 if ($Handle === FALSE)
3895 # (close out session, making it read-only, so that session file 3896 # lock is released and others are not potentially hanging 3897 # waiting for it while the download completes) 3898 session_write_close();
3900 while (!feof($Handle))
3902 print fread($Handle, $BlockSize);
3907 }, array($FilePath));
3909 # prevent HTML output that might interfere with download 3910 $this->SuppressHTMLOutput();
3912 # set flag to indicate not to log a slow page load in case client 3913 # connection delays PHP execution because of header 3914 $this->DoNotLogSlowPageLoad = TRUE;
3916 # report no errors found to caller 3932 public function GetLock($LockName, $Wait = TRUE)
3934 # assume we will not get a lock 3937 # clear out any stale locks 3938 static $CleanupHasBeenDone = FALSE;
3939 if (!$CleanupHasBeenDone)
3941 # (margin for clearing stale locks is twice the known 3942 # maximum PHP execution time, because the max time 3943 # techinically does not include external operations 3944 # like database queries) 3946 (time() - ($this->MaxExecutionTime() * 2)));
3947 $this->DB->Query(
"DELETE FROM AF_Locks WHERE" 3948 .
" ObtainedAt < '".$ClearLocksObtainedBefore.
"' AND" 3949 .
" LockName = '".addslashes($LockName).
"'");
3954 # lock database table so nobody else can try to get a lock 3955 $this->DB->Query(
"LOCK TABLES AF_Locks WRITE");
3957 # look for lock with specified name 3958 $FoundCount = $this->DB->Query(
"SELECT COUNT(*) AS FoundCount" 3959 .
" FROM AF_Locks WHERE LockName = '" 3960 .addslashes($LockName).
"'",
"FoundCount");
3961 $LockFound = ($FoundCount > 0) ? TRUE : FALSE;
3966 # unlock database tables 3967 $this->DB->Query(
"UNLOCK TABLES");
3969 # if blocking was requested 3972 # wait to give someone else a chance to release lock 3978 # while lock was found and blocking was requested 3979 }
while ($LockFound && $Wait);
3982 # if lock was not found 3986 $this->DB->Query(
"INSERT INTO AF_Locks (LockName) VALUES ('" 3987 .addslashes($LockName).
"')");
3990 # unlock database tables 3991 $this->DB->Query(
"UNLOCK TABLES");
3994 # report to caller whether lock was obtained 4005 public function ReleaseLock($LockName)
4007 # release any existing locks 4008 $this->DB->Query(
"DELETE FROM AF_Locks WHERE LockName = '" 4009 .addslashes($LockName).
"'");
4011 # report to caller whether existing lock was released 4012 return $this->DB->NumRowsAffected() ? TRUE : FALSE;
4018 # ---- Backward Compatibility -------------------------------------------- 4028 public function FindCommonTemplate($BaseName)
4030 return $this->FindFile(
4031 $this->IncludeDirList, $BaseName, array(
"tpl",
"html"));
4037 # ---- PRIVATE INTERFACE ------------------------------------------------- 4039 private $AdditionalRequiredUIFiles = array();
4040 private $BackgroundTaskMemLeakLogThreshold = 10; # percentage of max mem
4041 private $BackgroundTaskMinFreeMemPercent = 25;
4042 private $BrowserDetectFunc;
4043 private $CacheCurrentPage = TRUE;
4044 private $AlternateDomainPrefixes = array();
4045 private $CleanUrlMappings = array();
4046 private $CleanUrlRewritePerformed = FALSE;
4047 private $ContextFilters = array(
4048 self::CONTEXT_START => TRUE,
4049 self::CONTEXT_PAGE => array(
"H_"),
4050 self::CONTEXT_COMMON => array(
"H_"),
4052 private $CssUrlFingerprintPath;
4054 private $DefaultPage =
"Home";
4055 private $DoNotMinimizeList = array();
4056 private $DoNotLogSlowPageLoad = FALSE;
4057 private $EnvIncludes = array();
4058 private $ExecutionStartTime;
4059 private $FoundUIFiles = array();
4060 private $HtmlCharset =
"UTF-8";
4061 private $InterfaceSettings = array();
4062 private $JSMinimizerJavaScriptPackerAvailable = FALSE;
4063 private $JSMinimizerJShrinkAvailable = TRUE;
4064 private $JumpToPage = NULL;
4065 private $JumpToPageDelay = 0;
4066 private $LogFileName =
"local/logs/site.log";
4067 private $MaxRunningTasksToTrack = 250;
4069 private $OutputModificationCallbackInfo;
4070 private $OutputModificationCallbacks = array();
4071 private $OutputModificationPatterns = array();
4072 private $OutputModificationReplacements = array();
4073 private $PageCacheTags = array();
4075 private $PostProcessingFuncs = array();
4076 private $RequeueCurrentTask;
4077 private $RunningInBackground = FALSE;
4078 private $RunningTask;
4079 private $SavedContext;
4080 private $SaveTemplateLocationCache = FALSE;
4081 private $SessionStorage;
4082 private $SessionGcProbability;
4084 private $SuppressHTML = FALSE;
4085 private $SuppressStdPageStartAndEnd = FALSE;
4086 private $TemplateLocationCache;
4087 private $TemplateLocationCacheInterval = 60; # in minutes
4088 private $TemplateLocationCacheExpiration;
4089 private $UnbufferedCallbacks = array();
4090 private $UrlFingerprintBlacklist = array();
4091 private $UseBaseTag = FALSE;
4093 private static $ActiveUI =
"default";
4094 private static $AppName =
"ScoutAF";
4095 private static $DefaultUI =
"default";
4096 private static $JSMinCacheDir =
"local/data/caches/JSMin";
4097 private static $ObjectDirectories = array();
4098 private static $ObjectLocationCache;
4099 private static $ObjectLocationCacheInterval = 60;
4100 private static $ObjectLocationCacheExpiration;
4101 private static $PreferHttpHost = FALSE;
4102 private static $RootUrlOverride = NULL;
4103 private static $SaveObjectLocationCache = FALSE;
4104 private static $ScssCacheDir =
"local/data/caches/SCSS";
4105 private static $SessionLifetime = 1440; # in seconds
4107 # offset used to generate page cache tag IDs from numeric tags 4108 const PAGECACHETAGIDOFFSET = 100000;
4110 # minimum expired session garbage collection probability 4111 const MIN_GC_PROBABILITY = 0.01;
4117 private $NoTSR = FALSE;
4119 private $KnownPeriodicEvents = array();
4120 private $PeriodicEvents = array(
4121 "EVENT_HOURLY" => self::EVENTTYPE_DEFAULT,
4122 "EVENT_DAILY" => self::EVENTTYPE_DEFAULT,
4123 "EVENT_WEEKLY" => self::EVENTTYPE_DEFAULT,
4124 "EVENT_MONTHLY" => self::EVENTTYPE_DEFAULT,
4125 "EVENT_PERIODIC" => self::EVENTTYPE_NAMED,
4127 private $EventPeriods = array(
4128 "EVENT_HOURLY" => 3600,
4129 "EVENT_DAILY" => 86400,
4130 "EVENT_WEEKLY" => 604800,
4131 "EVENT_MONTHLY" => 2592000,
4132 "EVENT_PERIODIC" => 0,
4134 private $UIEvents = array(
4135 "EVENT_PAGE_LOAD" => self::EVENTTYPE_DEFAULT,
4136 "EVENT_PHP_FILE_LOAD" => self::EVENTTYPE_CHAIN,
4137 "EVENT_PHP_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
4138 "EVENT_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
4139 "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
4140 "EVENT_PAGE_OUTPUT_FILTER" => self::EVENTTYPE_CHAIN,
4147 private function LoadSettings()
4149 # read settings in from database 4150 $this->DB->Query(
"SELECT * FROM ApplicationFrameworkSettings");
4151 $this->Settings = $this->DB->FetchRow();
4153 # if settings were not previously initialized 4154 if ($this->Settings === FALSE)
4156 # initialize settings in database 4157 $this->DB->Query(
"INSERT INTO ApplicationFrameworkSettings" 4158 .
" (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");
4160 # read new settings in from database 4161 $this->DB->Query(
"SELECT * FROM ApplicationFrameworkSettings");
4162 $this->Settings = $this->DB->FetchRow();
4164 # bail out if reloading new settings failed 4165 if ($this->Settings === FALSE)
4167 throw new Exception(
4168 "Unable to load application framework settings.");
4172 # if base path was not previously set or we appear to have moved 4173 if (!array_key_exists(
"BasePath", $this->Settings)
4174 || (!strlen($this->Settings[
"BasePath"]))
4175 || (!array_key_exists(
"BasePathCheck", $this->Settings))
4176 || (__FILE__ != $this->Settings[
"BasePathCheck"]))
4178 # attempt to extract base path from Apache .htaccess file 4179 if (is_readable(
".htaccess"))
4181 $Lines = file(
".htaccess");
4182 foreach ($Lines as $Line)
4184 if (preg_match(
"/\\s*RewriteBase\\s+/", $Line))
4186 $Pieces = preg_split(
4187 "/\\s+/", $Line, NULL, PREG_SPLIT_NO_EMPTY);
4188 $BasePath = $Pieces[1];
4193 # if base path was found 4194 if (isset($BasePath))
4196 # save base path locally 4197 $this->Settings[
"BasePath"] = $BasePath;
4199 # save base path to database 4200 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings" 4201 .
" SET BasePath = '".addslashes($BasePath).
"'" 4202 .
", BasePathCheck = '".addslashes(__FILE__).
"'");
4206 # retrieve template location cache 4207 $this->TemplateLocationCache = unserialize(
4208 $this->Settings[
"TemplateLocationCache"]);
4209 $this->TemplateLocationCacheInterval =
4210 $this->Settings[
"TemplateLocationCacheInterval"];
4211 $this->TemplateLocationCacheExpiration =
4212 strtotime($this->Settings[
"TemplateLocationCacheExpiration"]);
4214 # if template location cache looks invalid or has expired 4215 $CurrentTime = time();
4216 if (!count($this->TemplateLocationCache)
4217 || ($CurrentTime >= $this->TemplateLocationCacheExpiration))
4219 # clear cache and reset cache expiration 4220 $this->TemplateLocationCache = array();
4221 $this->TemplateLocationCacheExpiration =
4222 $CurrentTime + ($this->TemplateLocationCacheInterval * 60);
4223 $this->SaveTemplateLocationCache = TRUE;
4226 # retrieve object location cache 4227 self::$ObjectLocationCache =
4228 unserialize($this->Settings[
"ObjectLocationCache"]);
4229 self::$ObjectLocationCacheInterval =
4230 $this->Settings[
"ObjectLocationCacheInterval"];
4231 self::$ObjectLocationCacheExpiration =
4232 strtotime($this->Settings[
"ObjectLocationCacheExpiration"]);
4234 # if object location cache looks invalid or has expired 4235 if (!count(self::$ObjectLocationCache)
4236 || ($CurrentTime >= self::$ObjectLocationCacheExpiration))
4238 # clear cache and reset cache expiration 4239 self::$ObjectLocationCache = array();
4240 self::$ObjectLocationCacheExpiration =
4241 $CurrentTime + (self::$ObjectLocationCacheInterval * 60);
4242 self::$SaveObjectLocationCache = TRUE;
4252 private function RewriteCleanUrls($PageName)
4254 # if URL rewriting is supported by the server 4255 if ($this->HtaccessSupport())
4257 # retrieve current URL and remove base path if present 4258 $Url = $this->GetPageLocation();
4260 # for each clean URL mapping 4261 foreach ($this->CleanUrlMappings as $Info)
4263 # if current URL matches clean URL pattern 4264 if (preg_match($Info[
"Pattern"], $Url, $Matches))
4267 $PageName = $Info[
"Page"];
4269 # if $_GET variables specified for clean URL 4270 if ($Info[
"GetVars"] !== NULL)
4272 # for each $_GET variable specified for clean URL 4273 foreach ($Info[
"GetVars"] as $VarName => $VarTemplate)
4275 # start with template for variable value 4276 $Value = $VarTemplate;
4278 # for each subpattern matched in current URL 4279 foreach ($Matches as $Index => $Match)
4281 # if not first (whole) match 4284 # make any substitutions in template 4285 $Value = str_replace(
"$".$Index, $Match, $Value);
4289 # set $_GET variable 4290 $_GET[$VarName] = $Value;
4294 # set flag indicating clean URL mapped 4295 $this->CleanUrlRewritePerformed = TRUE;
4297 # stop looking for a mapping 4303 # return (possibly) updated page name to caller 4319 private function RewriteAlternateDomainUrls($Html)
4321 # if we have htaccess support, and if we've been told via a 4322 # RootUrlOverride which of our domains is the primary one 4323 if ($this->HtaccessSupport() &&
4324 self::$RootUrlOverride !== NULL)
4326 # see what domain this request was accessed under 4327 $VHost = $_SERVER[
"SERVER_NAME"];
4329 # if it was a domain for which we have a prefix 4330 # configured, we'll need to do some rewriting 4331 if (isset($this->AlternateDomainPrefixes[$VHost]))
4333 # pull out the configured prefix for this domain 4334 $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
4336 # get the URL for the primary domain, including the base path 4337 # (usually the part between the host name and the PHP file name) 4338 $RootUrl = $this->RootUrl().self::BasePath();
4340 # and figure out what protcol we were using 4341 $Protocol = (isset($_SERVER[
"HTTPS"]) ?
"https" :
"http");
4343 # NB: preg_replace iterates through the configured 4344 # search/replacement pairs, such that the second one 4345 # runs after the first and so on 4347 # the first n-1 patterns below convert any relative 4348 # links in the generated HTML to absolute links using 4349 # our primary domain (e.g., for stylesheets, javascript, 4352 # the nth pattern looks for links that live within the 4353 # path subtree specified by our configured prefix on 4354 # our primary domain, then replaces them with equivalent 4355 # links on our secondary domain 4357 # for example, if our primary domain is 4358 # example.com/MySite and our secondary domain is 4359 # things.example.org/MySite with 'things' as the 4360 # configured prefix, then this last pattern will look 4361 # for example.com/MySite/things and replace it with 4362 # things.example.org/MySite 4363 $RelativePathPatterns = array(
4364 "%src=\"(?!http://|https://)%i",
4365 "%src='(?!http://|https://)%i",
4366 "%href=\"(?!http://|https://)%i",
4367 "%href='(?!http://|https://)%i",
4368 "%action=\"(?!http://|https://)%i",
4369 "%action='(?!http://|https://)%i",
4370 "%@import\s+url\(\"(?!http://|https://)%i",
4371 "%@import\s+url\('(?!http://|https://)%i",
4372 "%src:\s+url\(\"(?!http://|https://)%i",
4373 "%src:\s+url\('(?!http://|https://)%i",
4374 "%@import\s+\"(?!http://|https://)%i",
4375 "%@import\s+'(?!http://|https://)%i",
4376 "%".preg_quote($RootUrl.$ThisPrefix.
"/",
"%").
"%",
4378 $RelativePathReplacements = array(
4383 "action=\"".$RootUrl,
4384 "action='".$RootUrl,
4385 "@import url(\"".$RootUrl,
4386 "@import url('".$RootUrl,
4387 "src: url(\"".$RootUrl,
4388 "src: url('".$RootUrl,
4389 "@import \"".$RootUrl,
4390 "@import '".$RootUrl,
4391 $Protocol.
"://".$VHost.self::BasePath(),
4394 $NewHtml = preg_replace(
4395 $RelativePathPatterns,
4396 $RelativePathReplacements,
4399 # check to make sure relative path fixes didn't fail 4400 $Html = $this->CheckOutputModification(
4402 "alternate domain substitutions");
4427 private function FindFile($DirectoryList, $BaseName,
4428 $PossibleSuffixes = NULL, $PossiblePrefixes = NULL)
4430 # generate template cache index for this page 4431 $CacheIndex = md5(serialize($DirectoryList))
4432 .self::$DefaultUI.self::$ActiveUI.$BaseName;
4434 # if caching is enabled and we have cached location 4435 if (($this->TemplateLocationCacheInterval > 0)
4436 && array_key_exists($CacheIndex,
4437 $this->TemplateLocationCache))
4439 # use template location from cache 4440 $FoundFileName = $this->TemplateLocationCache[$CacheIndex];
4444 # if suffixes specified and base name does not include suffix 4445 if (count($PossibleSuffixes)
4446 && !preg_match(
"/\.[a-zA-Z0-9]+$/", $BaseName))
4448 # add versions of file names with suffixes to file name list 4449 $FileNames = array();
4450 foreach ($PossibleSuffixes as $Suffix)
4452 $FileNames[] = $BaseName.
".".$Suffix;
4457 # use base name as file name 4458 $FileNames = array($BaseName);
4461 # if prefixes specified 4462 if (count($PossiblePrefixes))
4464 # add versions of file names with prefixes to file name list 4465 $NewFileNames = array();
4466 foreach ($FileNames as $FileName)
4468 foreach ($PossiblePrefixes as $Prefix)
4470 $NewFileNames[] = $Prefix.$FileName;
4473 $FileNames = $NewFileNames;
4476 # expand directory list to include variants 4477 $DirectoryList = $this->ExpandDirectoryList($DirectoryList);
4479 # for each possible location 4480 $FoundFileName = NULL;
4481 foreach ($DirectoryList as $Dir)
4483 # for each possible file name 4484 foreach ($FileNames as $File)
4486 # if template is found at location 4487 if (file_exists($Dir.$File))
4489 # save full template file name and stop looking 4490 $FoundFileName = $Dir.$File;
4496 # save location in cache 4497 $this->TemplateLocationCache[$CacheIndex]
4500 # set flag indicating that cache should be saved 4501 $this->SaveTemplateLocationCache = TRUE;
4504 # return full template file name to caller 4505 return $FoundFileName;
4514 private function ExpandDirectoryList($DirList)
4516 # generate lookup for supplied list 4517 $ExpandedListKey = md5(serialize($DirList)
4518 .self::$DefaultUI.self::$ActiveUI);
4520 # if we already have expanded version of supplied list 4521 if (isset($this->ExpandedDirectoryLists[$ExpandedListKey]))
4523 # return expanded version to caller 4524 return $this->ExpandedDirectoryLists[$ExpandedListKey];
4527 # for each directory in list 4528 $ExpDirList = array();
4529 foreach ($DirList as $Dir)
4531 # if directory includes substitution keyword 4532 if ((strpos($Dir,
"%DEFAULTUI%") !== FALSE)
4533 || (strpos($Dir,
"%ACTIVEUI%") !== FALSE))
4535 # start with empty new list segment 4536 $ExpDirListSegment = array();
4538 # use default values for initial parent 4539 $ParentInterface = array(self::$ActiveUI, self::$DefaultUI);
4543 # substitute in for keyword on parent 4544 $CurrDir = str_replace(array(
"%ACTIVEUI%",
"%DEFAULTUI%"),
4545 $ParentInterface, $Dir);
4547 # add local version of parent directory to new list segment 4548 $ExpDirListSegment[] =
"local/".$CurrDir;
4550 # add parent directory to new list segment 4551 $ExpDirListSegment[] = $CurrDir;
4553 # look for new parent interface 4554 $ParentInterface = $this->GetInterfaceSetting(
4555 $CurrDir,
"ParentInterface");
4557 # repeat if parent is available 4558 }
while (strlen($ParentInterface));
4560 # add new list segment to expanded list 4561 $ExpDirList = array_merge($ExpDirList, $ExpDirListSegment);
4565 # add local version of directory to expanded list 4566 $ExpDirList[] =
"local/".$Dir;
4568 # add directory to expanded list 4569 $ExpDirList[] = $Dir;
4573 # return expanded version to caller 4574 $this->ExpandedDirectoryLists[$ExpandedListKey] = $ExpDirList;
4575 return $this->ExpandedDirectoryLists[$ExpandedListKey];
4586 private function GetInterfaceSetting($InterfaceDir, $SettingName = NULL)
4588 # extract canonical interface name and base interface directory 4589 preg_match(
"%(.*interface/)([^/]+)%", $InterfaceDir, $Matches);
4590 $InterfaceDir = (count($Matches) > 2)
4591 ? $Matches[1].$Matches[2] : $InterfaceDir;
4592 $InterfaceName = (count($Matches) > 2)
4593 ? $Matches[2] :
"UNKNOWN";
4595 # if we do not have settings for interface 4596 if (!isset($this->InterfaceSettings[$InterfaceName]))
4598 # load default values for settings 4599 $this->InterfaceSettings[$InterfaceName] = array(
4604 # if directory takes precedence over existing settings source 4605 # ("takes precendence" == is more local == longer directory length) 4606 if (strlen($InterfaceDir)
4607 > strlen($this->InterfaceSettings[$InterfaceName][
"Source"]))
4609 # if settings file exists in directory 4610 $SettingsFile = $InterfaceDir.
"/interface.ini";
4611 if (is_readable($SettingsFile))
4613 # read in values from file 4614 $NewSettings = parse_ini_file($SettingsFile);
4616 # merge in values with existing settings 4617 $this->InterfaceSettings[$InterfaceName] = array_merge(
4618 $this->InterfaceSettings[$InterfaceName], $NewSettings);
4620 # save new source of settings 4621 $this->InterfaceSettings[$InterfaceName][
"Source"] = $InterfaceDir;
4625 # return interface settings to caller 4627 ? (isset($this->InterfaceSettings[$InterfaceName][$SettingName])
4628 ? $this->InterfaceSettings[$InterfaceName][$SettingName]
4630 : $this->InterfaceSettings[$InterfaceName];
4641 private function CompileScssFile($SrcFile)
4643 # build path to CSS file 4644 $DstFile = self::$ScssCacheDir.
"/".dirname($SrcFile)
4645 .
"/".basename($SrcFile);
4646 $DstFile = substr_replace($DstFile,
"css", -4);
4648 # if SCSS file is newer than CSS file 4649 if (!file_exists($DstFile)
4650 || (filemtime($SrcFile) > filemtime($DstFile)))
4652 # attempt to create CSS cache subdirectory if not present 4653 if (!is_dir(dirname($DstFile)))
4655 @mkdir(dirname($DstFile), 0777, TRUE);
4658 # if CSS cache directory and CSS file path appear writable 4659 static $CacheDirIsWritable;
4660 if (!isset($CacheDirIsWritable))
4661 { $CacheDirIsWritable = is_writable(self::$ScssCacheDir); }
4662 if (is_writable($DstFile)
4663 || (!file_exists($DstFile) && $CacheDirIsWritable))
4665 # load SCSS and compile to CSS 4666 $ScssCode = file_get_contents($SrcFile);
4667 $ScssCompiler =
new scssc();
4668 $ScssCompiler->setFormatter($this->GenerateCompactCss()
4669 ?
"scss_formatter_compressed" :
"scss_formatter");
4672 $CssCode = $ScssCompiler->compile($ScssCode);
4674 # add fingerprinting for URLs in CSS 4675 $this->CssUrlFingerprintPath = dirname($SrcFile);
4676 $CssCode = preg_replace_callback(
4677 "/url\((['\"]*)(.+)\.([a-z]+)(['\"]*)\)/",
4678 array($this,
"CssUrlFingerprintInsertion"),
4681 # strip out comments from CSS (if requested) 4682 if ($this->GenerateCompactCss())
4684 $CssCode = preg_replace(
'!/\*[^*]*\*+([^/][^*]*\*+)*/!',
4688 # write out CSS file 4689 file_put_contents($DstFile, $CssCode);
4691 catch (Exception $Ex)
4693 $this->LogError(self::LOGLVL_ERROR,
4694 "Error compiling SCSS file ".$SrcFile.
": " 4695 .$Ex->getMessage());
4701 # log error and set CSS file path to indicate failure 4702 $this->LogError(self::LOGLVL_ERROR,
4703 "Unable to write out CSS file (compiled from SCSS) to " 4709 # return CSS file path to caller 4720 private function MinimizeJavascriptFile($SrcFile)
4722 # bail out if file is on exclusion list 4723 foreach ($this->DoNotMinimizeList as $DNMFile)
4725 if (($SrcFile == $DNMFile) || (basename($SrcFile) == $DNMFile))
4731 # build path to minimized file 4732 $DstFile = self::$JSMinCacheDir.
"/".dirname($SrcFile)
4733 .
"/".basename($SrcFile);
4734 $DstFile = substr_replace($DstFile,
".min", -3, 0);
4736 # if original file is newer than minimized file 4737 if (!file_exists($DstFile)
4738 || (filemtime($SrcFile) > filemtime($DstFile)))
4740 # attempt to create cache subdirectory if not present 4741 if (!is_dir(dirname($DstFile)))
4743 @mkdir(dirname($DstFile), 0777, TRUE);
4746 # if cache directory and minimized file path appear writable 4747 static $CacheDirIsWritable;
4748 if (!isset($CacheDirIsWritable))
4749 { $CacheDirIsWritable = is_writable(self::$JSMinCacheDir); }
4750 if (is_writable($DstFile)
4751 || (!file_exists($DstFile) && $CacheDirIsWritable))
4753 # load JavaScript code 4754 $Code = file_get_contents($SrcFile);
4756 # decide which minimizer to use 4757 if ($this->JSMinimizerJavaScriptPackerAvailable
4758 && $this->JSMinimizerJShrinkAvailable)
4760 $Minimizer = (strlen($Code) < 5000)
4761 ?
"JShrink" :
"JavaScriptPacker";
4763 elseif ($this->JSMinimizerJShrinkAvailable)
4765 $Minimizer =
"JShrink";
4769 $Minimizer =
"NONE";
4775 case "JavaScriptMinimizer":
4777 $MinimizedCode = $Packer->pack();
4785 catch (Exception $Exception)
4787 unset($MinimizedCode);
4788 $MinimizeError = $Exception->getMessage();
4793 # if minimization succeeded 4794 if (isset($MinimizedCode))
4796 # write out minimized file 4797 file_put_contents($DstFile, $MinimizedCode);
4801 # log error and set destination file path to indicate failure 4802 $ErrMsg =
"Unable to minimize JavaScript file ".$SrcFile;
4803 if (isset($MinimizeError))
4805 $ErrMsg .=
" (".$MinimizeError.
")";
4807 $this->LogError(self::LOGLVL_ERROR, $ErrMsg);
4813 # log error and set destination file path to indicate failure 4814 $this->LogError(self::LOGLVL_ERROR,
4815 "Unable to write out minimized JavaScript to file ".$DstFile);
4820 # return CSS file path to caller 4831 private function CssUrlFingerprintInsertion($Matches)
4833 # generate fingerprint string from CSS file modification time 4834 $FileName = realpath($this->CssUrlFingerprintPath.
"/".
4835 $Matches[2].
".".$Matches[3]);
4836 $MTime = filemtime($FileName);
4837 $Fingerprint = sprintf(
"%06X", ($MTime % 0xFFFFFF));
4839 # build URL string with fingerprint and return it to caller 4840 return "url(".$Matches[1].$Matches[2].
".".$Fingerprint
4841 .
".".$Matches[3].$Matches[4].
")";
4851 private function GetRequiredFilesNotYetLoaded($PageContentFile)
4853 # start out assuming no files required 4854 $RequiredFiles = array();
4856 # if page content file supplied 4857 if ($PageContentFile)
4859 # if file containing list of required files is available 4860 $Path = dirname($PageContentFile);
4861 $RequireListFile = $Path.
"/REQUIRES";
4862 if (file_exists($RequireListFile))
4864 # read in list of required files 4865 $RequestedFiles = file($RequireListFile);
4867 # for each line in required file list 4868 foreach ($RequestedFiles as $Line)
4870 # if line is not a comment 4871 $Line = trim($Line);
4872 if (!preg_match(
"/^#/", $Line))
4874 # if file has not already been loaded 4875 if (!in_array($Line, $this->FoundUIFiles))
4877 # add to list of required files 4878 $RequiredFiles[$Line] = self::ORDER_MIDDLE;
4885 # add in additional required files if any 4886 if (count($this->AdditionalRequiredUIFiles))
4888 $RequiredFiles = array_merge(
4889 $RequiredFiles, $this->AdditionalRequiredUIFiles);
4892 # return list of required files to caller 4893 return $RequiredFiles;
4904 private function SubBrowserIntoFileNames($FileNames)
4906 # if a browser detection function has been made available 4907 $UpdatedFileNames = array();
4908 if (is_callable($this->BrowserDetectFunc))
4910 # call function to get browser list 4911 $Browsers = call_user_func($this->BrowserDetectFunc);
4913 # for each required file 4914 foreach ($FileNames as $FileName => $Value)
4916 # if file name includes browser keyword 4917 if (preg_match(
"/%BROWSER%/", $FileName))
4920 foreach ($Browsers as $Browser)
4922 # substitute in browser name and add to new file list 4923 $NewFileName = preg_replace(
4924 "/%BROWSER%/", $Browser, $FileName);
4925 $UpdatedFileNames[$NewFileName] = $Value;
4930 # add to new file list 4931 $UpdatedFileNames[$FileName] = $Value;
4937 # filter out any files with browser keyword in their name 4938 foreach ($FileNames as $FileName => $Value)
4940 if (!preg_match(
"/%BROWSER%/", $FileName))
4942 $UpdatedFileNames[$FileName] = $Value;
4947 return $UpdatedFileNames;
4955 private function AddMetaTagsToPageOutput($PageOutput)
4957 if (isset($this->MetaTags))
4959 $MetaTagSection =
"";
4960 foreach ($this->MetaTags as $MetaTagAttribs)
4962 $MetaTagSection .=
"<meta";
4963 foreach ($MetaTagAttribs as
4964 $MetaTagAttribName => $MetaTagAttribValue)
4966 $MetaTagSection .=
" ".$MetaTagAttribName.
"=\"" 4967 .htmlspecialchars(trim($MetaTagAttribValue)).
"\"";
4969 $MetaTagSection .=
" />\n";
4972 if ($this->SuppressStdPageStartAndEnd)
4974 $PageOutput = $MetaTagSection.$PageOutput;
4978 $PageOutput = preg_replace(
"#<head>#i",
4979 "<head>\n".$MetaTagSection, $PageOutput, 1);
4993 private function AddFileTagsToPageOutput($PageOutput, $Files)
4995 # substitute browser name into names of required files as appropriate 4996 $Files = $this->SubBrowserIntoFileNames($Files);
4998 # initialize content sections 5000 self::ORDER_FIRST =>
"",
5001 self::ORDER_MIDDLE =>
"",
5002 self::ORDER_LAST =>
"",
5005 self::ORDER_FIRST =>
"",
5006 self::ORDER_MIDDLE =>
"",
5007 self::ORDER_LAST =>
"",
5010 # for each required file 5011 foreach ($Files as $File => $Order)
5013 # locate specific file to use 5014 $FilePath = $this->GUIFile($File);
5019 # generate tag for file 5020 $Tag = $this->GetUIFileLoadingTag($FilePath);
5022 # add file to HTML output based on file type 5023 $FileType = $this->GetFileType($FilePath);
5027 $HeadContent[$Order] .= $Tag.
"\n";
5030 case self::FT_JAVASCRIPT:
5031 $BodyContent[$Order] .= $Tag.
"\n";
5037 # add content to head 5038 $Replacement = $HeadContent[self::ORDER_MIDDLE]
5039 .$HeadContent[self::ORDER_LAST];
5040 $UpdatedPageOutput = str_ireplace(
"</head>",
5041 $Replacement.
"</head>",
5042 $PageOutput, $ReplacementCount);
5043 # (if no </head> tag was found, just prepend tags to page content) 5044 if ($ReplacementCount == 0)
5046 $PageOutput = $Replacement.$PageOutput;
5048 # (else if multiple </head> tags found, only prepend tags to the first) 5049 elseif ($ReplacementCount > 1)
5051 $PageOutput = preg_replace(
"#</head>#i",
5052 $Replacement.
"</head>",
5057 $PageOutput = $UpdatedPageOutput;
5059 $Replacement = $HeadContent[self::ORDER_FIRST];
5060 $UpdatedPageOutput = str_ireplace(
"<head>",
5061 "<head>\n".$Replacement,
5062 $PageOutput, $ReplacementCount);
5063 # (if no <head> tag was found, just prepend tags to page content) 5064 if ($ReplacementCount == 0)
5066 $PageOutput = $Replacement.$PageOutput;
5068 # (else if multiple <head> tags found, only append tags to the first) 5069 elseif ($ReplacementCount > 1)
5071 $PageOutput = preg_replace(
"#<head>#i",
5072 "<head>\n".$Replacement,
5077 $PageOutput = $UpdatedPageOutput;
5080 # add content to body 5081 $Replacement = $BodyContent[self::ORDER_FIRST];
5082 $PageOutput = preg_replace(
"#<body([^>]*)>#i",
5083 "<body\\1>\n".$Replacement,
5084 $PageOutput, 1, $ReplacementCount);
5085 # (if no <body> tag was found, just append tags to page content) 5086 if ($ReplacementCount == 0)
5088 $PageOutput = $PageOutput.$Replacement;
5090 $Replacement = $BodyContent[self::ORDER_MIDDLE]
5091 .$BodyContent[self::ORDER_LAST];
5092 $UpdatedPageOutput = str_ireplace(
"</body>",
5093 $Replacement.
"\n</body>",
5094 $PageOutput, $ReplacementCount);
5095 # (if no </body> tag was found, just append tags to page content) 5096 if ($ReplacementCount == 0)
5098 $PageOutput = $PageOutput.$Replacement;
5100 # (else if multiple </body> tags found, only prepend tag to the first) 5101 elseif ($ReplacementCount > 1)
5103 $PageOutput = preg_replace(
"#</body>#i",
5104 $Replacement.
"\n</body>",
5109 $PageOutput = $UpdatedPageOutput;
5125 private function GetUIFileLoadingTag($FileName, $AdditionalAttributes = NULL)
5127 # pad additional attributes if supplied 5128 $AddAttribs = $AdditionalAttributes ?
" ".$AdditionalAttributes :
"";
5130 # retrieve type of UI file 5131 $FileType = $this->GetFileType($FileName);
5133 # construct tag based on file type 5137 $Tag =
" <link rel=\"stylesheet\" type=\"text/css\"" 5138 .
" media=\"all\" href=\"".$FileName.
"\"" 5139 .$AddAttribs.
" />\n";
5142 case self::FT_JAVASCRIPT:
5143 $Tag =
" <script type=\"text/javascript\"" 5144 .
" src=\"".$FileName.
"\"" 5145 .$AddAttribs.
"></script>\n";
5148 case self::FT_IMAGE:
5149 $Tag =
"<img src=\"".$FileName.
"\"".$AddAttribs.
">";
5157 # return constructed tag to caller 5165 private function AutoloadObjects($ClassName)
5167 # if caching is not turned off 5168 # and we have a cached location for class 5169 # and file at cached location is readable 5170 if ((self::$ObjectLocationCacheInterval > 0)
5171 && array_key_exists($ClassName,
5172 self::$ObjectLocationCache)
5173 && is_readable(self::$ObjectLocationCache[$ClassName]))
5175 # use object location from cache 5176 require_once(self::$ObjectLocationCache[$ClassName]);
5180 # convert any namespace separators in class name 5181 $ClassName = str_replace(
"\\",
"-", $ClassName);
5183 # for each possible object file directory 5185 foreach (self::$ObjectDirectories as $Location => $Info)
5187 # make any needed replacements in directory path 5188 $Location = str_replace(array(
"%ACTIVEUI%",
"%DEFAULTUI%"),
5189 array(self::$ActiveUI, self::$DefaultUI), $Location);
5191 # if directory looks valid 5192 if (is_dir($Location))
5194 # build class file name 5195 $NewClassName = ($Info[
"ClassPattern"] && $Info[
"ClassReplacement"])
5196 ? preg_replace($Info[
"ClassPattern"],
5197 $Info[
"ClassReplacement"], $ClassName)
5200 # read in directory contents if not already retrieved 5201 if (!isset($FileLists[$Location]))
5203 $FileLists[$Location] = self::ReadDirectoryTree(
5204 $Location,
'/^.+\.php$/i');
5207 # for each file in target directory 5208 $FileNames = $FileLists[$Location];
5209 $TargetName = strtolower($Info[
"Prefix"].$NewClassName.
".php");
5210 foreach ($FileNames as $FileName)
5212 # if file matches our target object file name 5213 if (strtolower($FileName) == $TargetName)
5215 # include object file 5216 require_once($Location.$FileName);
5218 # save location to cache 5219 self::$ObjectLocationCache[$ClassName]
5220 = $Location.$FileName;
5222 # set flag indicating that cache should be saved 5223 self::$SaveObjectLocationCache = TRUE;
5241 private static function ReadDirectoryTree($Directory, $Pattern)
5243 $CurrentDir = getcwd();
5245 $DirIter =
new RecursiveDirectoryIterator(
".");
5246 $IterIter =
new RecursiveIteratorIterator($DirIter);
5247 $RegexResults =
new RegexIterator($IterIter, $Pattern,
5248 RecursiveRegexIterator::GET_MATCH);
5249 $FileList = array();
5250 foreach ($RegexResults as $Result)
5252 $FileList[] = substr($Result[0], 2);
5261 private function UndoMagicQuotes()
5263 # if this PHP version has magic quotes support 5264 if (version_compare(PHP_VERSION,
"5.4.0",
"<"))
5266 # turn off runtime magic quotes if on 5267 if (get_magic_quotes_runtime())
5270 set_magic_quotes_runtime(FALSE);
5274 # if magic quotes GPC is on 5275 if (get_magic_quotes_gpc())
5277 # strip added slashes from incoming variables 5278 $GPC = array(&$_GET, &$_POST, &$_COOKIE, &$_REQUEST);
5279 array_walk_recursive($GPC,
5280 array($this,
"UndoMagicQuotes_StripCallback"));
5288 private function UndoMagicQuotes_StripCallback(&$Value)
5290 $Value = stripslashes($Value);
5297 private function LoadUIFunctions()
5300 "local/interface/%ACTIVEUI%/include",
5301 "interface/%ACTIVEUI%/include",
5302 "local/interface/%DEFAULTUI%/include",
5303 "interface/%DEFAULTUI%/include",
5305 foreach ($Dirs as $Dir)
5307 $Dir = str_replace(array(
"%ACTIVEUI%",
"%DEFAULTUI%"),
5308 array(self::$ActiveUI, self::$DefaultUI), $Dir);
5311 $FileNames = scandir($Dir);
5312 foreach ($FileNames as $FileName)
5314 if (preg_match(
"/^F-([A-Za-z0-9_]+)\.php/",
5315 $FileName, $Matches)
5316 || preg_match(
"/^F-([A-Za-z0-9_]+)\.html/",
5317 $FileName, $Matches))
5319 if (!function_exists($Matches[1]))
5321 include_once($Dir.
"/".$FileName);
5334 private function ProcessPeriodicEvent($EventName, $Callback)
5336 # retrieve last execution time for event if available 5337 $Signature = self::GetCallbackSignature($Callback);
5338 $LastRun = $this->DB->Query(
"SELECT LastRunAt FROM PeriodicEvents" 5339 .
" WHERE Signature = '".addslashes($Signature).
"'",
"LastRunAt");
5341 # determine whether enough time has passed for event to execute 5342 $ShouldExecute = (($LastRun === NULL)
5343 || (time() > (strtotime($LastRun) + $this->EventPeriods[$EventName])))
5346 # if event should run 5349 # add event to task queue 5350 $WrapperCallback = array(
"ApplicationFramework",
"RunPeriodicEvent");
5351 $WrapperParameters = array(
5352 $EventName, $Callback, array(
"LastRunAt" => $LastRun));
5353 $this->QueueUniqueTask($WrapperCallback, $WrapperParameters);
5356 # add event to list of periodic events 5357 $this->KnownPeriodicEvents[$Signature] = array(
5358 "Period" => $EventName,
5359 "Callback" => $Callback,
5360 "Queued" => $ShouldExecute);
5368 private static function GetCallbackSignature($Callback)
5370 return !is_array($Callback) ? $Callback
5371 : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
5379 private function PrepForTSR()
5381 # if HTML has been output and it's time to launch another task 5382 # (only TSR if HTML has been output because otherwise browsers 5383 # may misbehave after connection is closed) 5384 if ((PHP_SAPI !=
"cli")
5385 && ($this->JumpToPage || !$this->SuppressHTML)
5386 && (time() > (strtotime($this->Settings[
"LastTaskRunAt"])
5387 + ($this->MaxExecutionTime()
5388 / $this->Settings[
"MaxTasksRunning"]) + 5))
5389 && $this->GetTaskQueueSize()
5390 && $this->Settings[
"TaskExecutionEnabled"])
5392 # begin buffering output for TSR 5395 # let caller know it is time to launch another task 5400 # let caller know it is not time to launch another task 5409 private function LaunchTSR()
5411 # set headers to close out connection to browser 5414 ignore_user_abort(TRUE);
5415 header(
"Connection: close");
5416 header(
"Content-Length: ".ob_get_length());
5419 # output buffered content 5420 while (ob_get_level()) { ob_end_flush(); }
5423 # write out any outstanding data and end HTTP session 5424 session_write_close();
5426 # set flag indicating that we are now running in background 5427 $this->RunningInBackground = TRUE;
5429 # handle garbage collection for session data 5430 if (isset($this->SessionStorage) &&
5431 (rand()/getrandmax()) <= $this->SessionGcProbability)
5433 # determine when sessions will expire 5434 $ExpiredTime = strtotime(
"-". self::$SessionLifetime.
" seconds");
5436 # iterate over files in the session directory with a DirectoryIterator 5437 # NB: we cannot use scandir() here because it reads the 5438 # entire list of files into memory and may exceed the memory 5439 # limit for directories with very many files 5440 $DI =
new DirectoryIterator($this->SessionStorage);
5441 while ($DI->valid())
5443 if ((strpos($DI->getFilename(),
"sess_") === 0) &&
5445 $DI->getCTime() < $ExpiredTime)
5447 unlink($DI->getPathname());
5454 # if there is still a task in the queue 5455 if ($this->GetTaskQueueSize())
5457 # garbage collect to give as much memory as possible for tasks 5458 if (function_exists(
"gc_collect_cycles")) { gc_collect_cycles(); }
5460 # turn on output buffering to (hopefully) record any crash output 5463 # lock tables and grab last task run time to double check 5464 $this->DB->Query(
"LOCK TABLES ApplicationFrameworkSettings WRITE");
5465 $this->LoadSettings();
5467 # if still time to launch another task 5468 if (time() > (strtotime($this->Settings[
"LastTaskRunAt"])
5469 + ($this->MaxExecutionTime()
5470 / $this->Settings[
"MaxTasksRunning"]) + 5))
5472 # update the "last run" time and release tables 5473 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings" 5474 .
" SET LastTaskRunAt = '".date(
"Y-m-d H:i:s").
"'");
5475 $this->DB->Query(
"UNLOCK TABLES");
5477 # run tasks while there is a task in the queue 5478 # and enough time and memory left 5482 $this->RunNextTask();
5484 # calculate percentage of memory still available 5485 $PercentFreeMem = (self::GetFreeMemory()
5486 / self::GetPhpMemoryLimit()) * 100;
5488 while ($this->GetTaskQueueSize()
5489 && ($this->GetSecondsBeforeTimeout() > 65)
5490 && ($PercentFreeMem > $this->BackgroundTaskMinFreeMemPercent));
5495 $this->DB->Query(
"UNLOCK TABLES");
5509 private function GetTaskList($DBQuery, $Count, $Offset)
5511 $this->DB->Query($DBQuery.
" LIMIT ".intval($Offset).
",".intval($Count));
5513 while ($Row = $this->DB->FetchRow())
5515 $Tasks[$Row[
"TaskId"]] = $Row;
5516 if ($Row[
"Callback"] ==
5517 serialize(array(
"ApplicationFramework",
"RunPeriodicEvent")))
5519 $WrappedCallback = unserialize($Row[
"Parameters"]);
5520 $Tasks[$Row[
"TaskId"]][
"Callback"] = $WrappedCallback[1];
5521 $Tasks[$Row[
"TaskId"]][
"Parameters"] = NULL;
5525 $Tasks[$Row[
"TaskId"]][
"Callback"] = unserialize($Row[
"Callback"]);
5526 $Tasks[$Row[
"TaskId"]][
"Parameters"] = unserialize($Row[
"Parameters"]);
5535 private function RunNextTask()
5537 # lock tables to prevent same task from being run by multiple sessions 5538 $this->DB->Query(
"LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
5540 # look for task at head of queue 5541 $this->DB->Query(
"SELECT * FROM TaskQueue ORDER BY Priority, TaskId LIMIT 1");
5542 $Task = $this->DB->FetchRow();
5544 # if there was a task available 5547 # move task from queue to running tasks list 5548 $this->DB->Query(
"INSERT INTO RunningTasks " 5549 .
"(TaskId,Callback,Parameters,Priority,Description) " 5550 .
"SELECT * FROM TaskQueue WHERE TaskId = " 5551 .intval($Task[
"TaskId"]));
5552 $this->DB->Query(
"DELETE FROM TaskQueue WHERE TaskId = " 5553 .intval($Task[
"TaskId"]));
5555 # release table locks to again allow other sessions to run tasks 5556 $this->DB->Query(
"UNLOCK TABLES");
5558 # unpack stored task info 5559 $Callback = unserialize($Task[
"Callback"]);
5560 $Parameters = unserialize($Task[
"Parameters"]);
5562 # attempt to load task callback if not already available 5563 $this->LoadFunction($Callback);
5565 # clear task requeue flag 5566 $this->RequeueCurrentTask = FALSE;
5568 # save amount of free memory for later comparison 5569 $BeforeFreeMem = self::GetFreeMemory();
5572 $this->RunningTask = $Task;
5575 call_user_func_array($Callback, $Parameters);
5579 call_user_func($Callback);
5581 unset($this->RunningTask);
5583 # log if task leaked significant memory 5584 if (function_exists(
"gc_collect_cycles")) { gc_collect_cycles(); }
5585 $AfterFreeMem = self::GetFreeMemory();
5586 $LeakThreshold = self::GetPhpMemoryLimit()
5587 * ($this->BackgroundTaskMemLeakLogThreshold / 100);
5588 if (($BeforeFreeMem - $AfterFreeMem) > $LeakThreshold)
5590 $this->LogError(self::LOGLVL_DEBUG,
"Task " 5591 .self::GetTaskCallbackSynopsis(
5592 $this->GetTask($Task[
"TaskId"])).
" leaked " 5593 .number_format($BeforeFreeMem - $AfterFreeMem).
" bytes.");
5596 # if task requeue requested 5597 if ($this->RequeueCurrentTask)
5599 # move task from running tasks list to queue 5600 $this->DB->Query(
"LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
5601 $this->DB->Query(
"INSERT INTO TaskQueue" 5602 .
" (Callback,Parameters,Priority,Description)" 5603 .
" SELECT Callback,Parameters,Priority,Description" 5604 .
" FROM RunningTasks WHERE TaskId = " 5605 .intval($Task[
"TaskId"]));
5606 $this->DB->Query(
"DELETE FROM RunningTasks WHERE TaskId = " 5607 .intval($Task[
"TaskId"]));
5608 $this->DB->Query(
"UNLOCK TABLES");
5612 # remove task from running tasks list 5613 $this->DB->Query(
"DELETE FROM RunningTasks" 5614 .
" WHERE TaskId = ".intval($Task[
"TaskId"]));
5617 # prune running tasks list if necessary 5618 $RunningTasksCount = $this->DB->Query(
5619 "SELECT COUNT(*) AS TaskCount FROM RunningTasks",
"TaskCount");
5620 if ($RunningTasksCount > $this->MaxRunningTasksToTrack)
5622 $this->DB->Query(
"DELETE FROM RunningTasks ORDER BY StartedAt" 5623 .
" LIMIT ".($RunningTasksCount - $this->MaxRunningTasksToTrack));
5628 # release table locks to again allow other sessions to run tasks 5629 $this->DB->Query(
"UNLOCK TABLES");
5638 public function OnCrash()
5640 # attempt to remove any memory limits 5641 $FreeMemory = $this->GetFreeMemory();
5642 ini_set(
"memory_limit", -1);
5644 # if there is a background task currently running 5645 if (isset($this->RunningTask))
5647 # add info about current page load 5648 $CrashInfo[
"ElapsedTime"] = $this->GetElapsedExecutionTime();
5649 $CrashInfo[
"FreeMemory"] = $FreeMemory;
5650 $CrashInfo[
"REMOTE_ADDR"] = $_SERVER[
"REMOTE_ADDR"];
5651 $CrashInfo[
"REQUEST_URI"] = $_SERVER[
"REQUEST_URI"];
5652 if (isset($_SERVER[
"REQUEST_TIME"]))
5654 $CrashInfo[
"REQUEST_TIME"] = $_SERVER[
"REQUEST_TIME"];
5656 if (isset($_SERVER[
"REMOTE_HOST"]))
5658 $CrashInfo[
"REMOTE_HOST"] = $_SERVER[
"REMOTE_HOST"];
5661 # add info about error that caused crash (if available) 5662 if (function_exists(
"error_get_last"))
5664 $CrashInfo[
"LastError"] = error_get_last();
5667 # add info about current output buffer contents (if available) 5668 if (ob_get_length() !== FALSE)
5670 $CrashInfo[
"OutputBuffer"] = ob_get_contents();
5673 # if backtrace info is available for the crash 5674 $Backtrace = debug_backtrace();
5675 if (count($Backtrace) > 1)
5677 # discard the current context from the backtrace 5678 array_shift($Backtrace);
5680 # add the backtrace to the crash info 5681 $CrashInfo[
"Backtrace"] = $Backtrace;
5683 # else if saved backtrace info is available 5684 elseif (isset($this->SavedContext))
5686 # add the saved backtrace to the crash info 5687 $CrashInfo[
"Backtrace"] = $this->SavedContext;
5690 # save crash info for currently running task 5692 $DB->Query(
"UPDATE RunningTasks SET CrashInfo = '" 5693 .addslashes(serialize($CrashInfo))
5694 .
"' WHERE TaskId = ".intval($this->RunningTask[
"TaskId"]));
5717 private function AddToDirList($DirList, $Dir, $SearchLast, $SkipSlashCheck)
5719 # convert incoming directory to array of directories (if needed) 5720 $Dirs = is_array($Dir) ? $Dir : array($Dir);
5722 # reverse array so directories are searched in specified order 5723 $Dirs = array_reverse($Dirs);
5725 # for each directory 5726 foreach ($Dirs as $Location)
5728 # make sure directory includes trailing slash 5729 if (!$SkipSlashCheck)
5731 $Location = $Location
5732 .((substr($Location, -1) !=
"/") ?
"/" :
"");
5735 # remove directory from list if already present 5736 if (in_array($Location, $DirList))
5738 $DirList = array_diff(
5739 $DirList, array($Location));
5742 # add directory to list of directories 5745 array_push($DirList, $Location);
5749 array_unshift($DirList, $Location);
5753 # return updated directory list to caller 5763 private function OutputModificationCallbackShell($Matches)
5765 # call previously-stored external function 5766 return call_user_func($this->OutputModificationCallbackInfo[
"Callback"],
5768 $this->OutputModificationCallbackInfo[
"Pattern"],
5769 $this->OutputModificationCallbackInfo[
"Page"],
5770 $this->OutputModificationCallbackInfo[
"SearchPattern"]);
5781 private function CheckOutputModification($Original, $Modified, $ErrorInfo)
5783 # if error was reported by regex engine 5784 if (preg_last_error() !== PREG_NO_ERROR)
5787 $this->LogError(self::LOGLVL_ERROR,
5788 "Error reported by regex engine when modifying output." 5789 .
" (".$ErrorInfo.
")");
5791 # use unmodified version of output 5792 $OutputToUse = $Original;
5794 # else if modification reduced output by more than threshold 5795 elseif ((strlen(trim($Modified)) / strlen(trim($Original)))
5796 < self::OUTPUT_MODIFICATION_THRESHOLD)
5799 $this->LogError(self::LOGLVL_WARNING,
5800 "Content reduced below acceptable threshold while modifying output." 5801 .
" (".$ErrorInfo.
")");
5803 # use unmodified version of output 5804 $OutputToUse = $Original;
5808 # use modified version of output 5809 $OutputToUse = $Modified;
5812 # return output to use to caller 5813 return $OutputToUse;
5817 const OUTPUT_MODIFICATION_THRESHOLD = 0.10;
5828 private function UpdateSetting(
5829 $FieldName, $NewValue =
DB_NOVALUE, $Persistent = TRUE)
5831 static $LocalSettings;
5836 $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5837 "ApplicationFrameworkSettings",
5838 $FieldName, $NewValue, NULL, $this->Settings);
5842 $LocalSettings[$FieldName] = $NewValue;
5845 elseif (!isset($LocalSettings[$FieldName]))
5847 $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5848 "ApplicationFrameworkSettings",
5849 $FieldName, $NewValue, NULL, $this->Settings);
5851 return $LocalSettings[$FieldName];
5863 private static function IncludeFile($_AF_File, $_AF_ContextVars = array())
5866 foreach ($_AF_ContextVars as $_AF_VarName => $_AF_VarValue)
5868 $$_AF_VarName = $_AF_VarValue;
5870 unset($_AF_VarName);
5871 unset($_AF_VarValue);
5872 unset($_AF_ContextVars);
5874 # add variables to context that we assume are always available 5875 $AF = $GLOBALS[
"AF"];
5880 # return updated context 5881 $ContextVars = get_defined_vars();
5882 unset($ContextVars[
"_AF_File"]);
5883 return $ContextVars;
5892 private function FilterContext($Context, $ContextVars)
5894 # clear all variables if no setting for context is available 5895 # or setting is FALSE 5896 if (!isset($this->ContextFilters[$Context])
5897 || ($this->ContextFilters[$Context] == FALSE))
5901 # keep all variables if setting for context is TRUE 5902 elseif ($this->ContextFilters[$Context] == TRUE)
5904 return $ContextVars;
5908 $Prefixes = $this->ContextFilters[$Context];
5909 $FilterFunc =
function($VarName) use ($Prefixes) {
5910 foreach ($Prefixes as $Prefix)
5912 if (substr($VarName, $Prefix) === 0)
5919 return array_filter(
5920 $ContextVars, $FilterFunc, ARRAY_FILTER_USE_KEY);
5925 private $InterfaceDirList = array(
5926 "interface/%ACTIVEUI%/",
5927 "interface/%DEFAULTUI%/",
5933 private $IncludeDirList = array(
5934 "interface/%ACTIVEUI%/include/",
5935 "interface/%ACTIVEUI%/objects/",
5936 "interface/%DEFAULTUI%/include/",
5937 "interface/%DEFAULTUI%/objects/",
5940 private $ImageDirList = array(
5941 "interface/%ACTIVEUI%/images/",
5942 "interface/%DEFAULTUI%/images/",
5945 private $FunctionDirList = array(
5946 "interface/%ACTIVEUI%/include/",
5947 "interface/%DEFAULTUI%/include/",
5951 const NOVALUE =
".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";
5954 # ---- Page Caching (Internal Methods) ----------------------------------- 5961 private function CheckForCachedPage($PageName)
5963 # assume no cached page will be found 5966 # if returning a cached page is allowed 5967 if ($this->CacheCurrentPage)
5969 # get fingerprint for requested page 5970 $PageFingerprint = $this->GetPageFingerprint($PageName);
5972 # look for matching page in cache in database 5973 $this->DB->Query(
"SELECT * FROM AF_CachedPages" 5974 .
" WHERE Fingerprint = '".addslashes($PageFingerprint).
"'");
5976 # if matching page found 5977 if ($this->DB->NumRowsSelected())
5979 # if cached page has expired 5980 $Row = $this->DB->FetchRow();
5981 $ExpirationTime = strtotime(
5982 "-".$this->PageCacheExpirationPeriod().
" seconds");
5983 if (strtotime($Row[
"CachedAt"]) < $ExpirationTime)
5985 # clear expired pages from cache 5986 $ExpirationTimestamp = date(
"Y-m-d H:i:s", $ExpirationTime);
5987 $this->DB->Query(
"DELETE CP, CPTI FROM AF_CachedPages CP," 5988 .
" AF_CachedPageTagInts CPTI" 5989 .
" WHERE CP.CachedAt < '".$ExpirationTimestamp.
"'" 5990 .
" AND CPTI.CacheId = CP.CacheId");
5991 $this->DB->Query(
"DELETE FROM AF_CachedPages " 5992 .
" WHERE CachedAt < '".$ExpirationTimestamp.
"'");
5996 # display cached page and exit 5997 $CachedPage = $Row[
"PageContent"];
6002 # return any cached page found to caller 6011 private function UpdatePageCache($PageName, $PageContent)
6013 # if page caching is enabled and current page should be cached 6014 if ($this->PageCacheEnabled()
6015 && $this->CacheCurrentPage
6016 && ($PageName !=
"404"))
6018 # if page content looks invalid 6019 if (strlen(trim(strip_tags($PageContent))) == 0)
6022 $LogMsg =
"Page not cached because content was empty." 6023 .
" (PAGE: ".$PageName.
", URL: ".$this->FullUrl().
")";
6024 $this->LogError(self::LOGLVL_ERROR, $LogMsg);
6028 # save page to cache 6029 $PageFingerprint = $this->GetPageFingerprint($PageName);
6030 $this->DB->Query(
"INSERT INTO AF_CachedPages" 6031 .
" (Fingerprint, PageContent) VALUES" 6032 .
" ('".$this->DB->EscapeString($PageFingerprint).
"', '" 6033 .$this->DB->EscapeString($PageContent).
"')");
6034 $CacheId = $this->DB->LastInsertId();
6036 # for each page cache tag that was added 6037 foreach ($this->PageCacheTags as $Tag => $Pages)
6039 # if current page is in list for tag 6040 if (in_array(
"CURRENT", $Pages) || in_array($PageName, $Pages))
6043 $TagId = $this->GetPageCacheTagId($Tag);
6045 # mark current page as associated with tag 6046 $this->DB->Query(
"INSERT INTO AF_CachedPageTagInts" 6047 .
" (TagId, CacheId) VALUES " 6048 .
" (".intval($TagId).
", ".intval($CacheId).
")");
6060 private function GetPageCacheTagId($Tag)
6062 # if tag is a non-negative integer 6063 if (is_numeric($Tag) && ($Tag > 0) && (intval($Tag) == $Tag))
6066 $Id = self::PAGECACHETAGIDOFFSET + $Tag;
6070 # look up ID in database 6071 $Id = $this->DB->Query(
"SELECT TagId FROM AF_CachedPageTags" 6072 .
" WHERE Tag = '".addslashes($Tag).
"'",
"TagId");
6074 # if ID was not found 6077 # add tag to database 6078 $this->DB->Query(
"INSERT INTO AF_CachedPageTags" 6079 .
" SET Tag = '".addslashes($Tag).
"'");
6080 $Id = $this->DB->LastInsertId();
6084 # return tag ID to caller 6093 private function GetPageFingerprint($PageName)
6095 # only get the environmental fingerprint once so that it is consistent 6096 # between page construction start and end 6097 static $EnvFingerprint;
6098 if (!isset($EnvFingerprint))
6100 $EnvData = json_encode($_GET).json_encode($_POST);
6102 # if alternate domain support is enabled 6103 if ($this->HtaccessSupport() && self::$RootUrlOverride !== NULL)
6105 # and if we were accessed via an alternate domain 6106 $VHost = $_SERVER[
"SERVER_NAME"];
6107 if (isset($this->AlternateDomainPrefixes[$VHost]))
6109 # then add the alternate domain that was used to our 6115 $EnvFingerprint = md5($EnvData);
6119 # build page fingerprint and return it to caller 6120 return $PageName.
"-".$EnvFingerprint;
Abstraction for forum messages and resource comments.
SQL database abstraction object with smart query caching.
static SortCompare($A, $B)
Perform compare and return value appropriate for sort function callbacks.
static minify($js, $options=array())
Takes a string containing javascript and removes unneeded characters in order to shrink the code with...
SCSS compiler written in PHP.
static ArrayPermutations($Items, $Perms=array())
Return all possible permutations of a given array.
const SQL_DATE_FORMAT
Format to feed to date() to get SQL-compatible date/time string.