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 # limit cookie to secure connection if we are running over same 106 $SecureCookie = isset($_SERVER[
"HTTPS"]) ? TRUE : FALSE;
108 # Cookies lacking embedded dots are... fun. 109 # rfc2109 sec 4.3.2 says to reject them 110 # rfc2965 sec 3.3.2 says to reject them 111 # rfc6265 sec 4.1.2.3 says only that "public suffixes" 112 # should be rejected. They reference Mozilla's 113 # publicsuffix.org, which does not contain 'localhost'. 114 # However, empirically in early 2017 Firefox still rejects 116 # Therefore, don't set a cookie domain if we're running on 117 # localhost to avoid this problem. 118 if (!preg_match(
'/^\.localhost(:[0-9]+)?$/', $SessionDomain))
122 session_set_cookie_params(self::$SessionLifetime,
"/",
123 $SessionDomain, $SecureCookie, TRUE);
125 # attempt to start session 126 $SessionStarted = @session_start();
128 # if session start failed 129 if (!$SessionStarted)
131 # regenerate session ID and attempt to start session again 132 session_regenerate_id(TRUE);
137 # set up our internal environment 140 # set up our exception handler 141 set_exception_handler(array($this,
"GlobalExceptionHandler"));
143 # load our settings from database 144 $this->LoadSettings();
146 # set PHP maximum execution time 147 ini_set(
"max_execution_time", $this->Settings[
"MaxExecTime"]);
148 set_time_limit($this->Settings[
"MaxExecTime"]);
150 # register events we handle internally 151 $this->RegisterEvent($this->PeriodicEvents);
152 $this->RegisterEvent($this->UIEvents);
154 # attempt to create SCSS cache directory if needed and it does not exist 155 if ($this->ScssSupportEnabled() && !is_dir(self::$ScssCacheDir))
156 { @mkdir(self::$ScssCacheDir, 0777, TRUE); }
158 # attempt to create minimized JS cache directory if needed and it does not exist 159 if ($this->UseMinimizedJavascript()
160 && $this->JavascriptMinimizationEnabled()
161 && !is_dir(self::$JSMinCacheDir))
163 @mkdir(self::$JSMinCacheDir, 0777, TRUE);
172 public function __destruct()
174 # if template location cache is flagged to be saved 175 if ($this->SaveTemplateLocationCache)
177 # write template location cache out and update cache expiration 178 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings" 179 .
" SET TemplateLocationCache = '" 180 .addslashes(serialize(
181 $this->TemplateLocationCache)).
"'," 182 .
" TemplateLocationCacheExpiration = '" 184 $this->TemplateLocationCacheExpiration).
"'");
187 # if object location cache is flagged to be saved 188 if (self::$SaveObjectLocationCache)
190 # write object location cache out and update cache expiration 191 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings" 192 .
" SET ObjectLocationCache = '" 193 .addslashes(serialize(
194 self::$ObjectLocationCache)).
"'," 195 .
" ObjectLocationCacheExpiration = '" 197 self::$ObjectLocationCacheExpiration).
"'");
207 public function GlobalExceptionHandler($Exception)
209 # display exception info 210 $Message = $Exception->getMessage();
211 $Location = str_replace(getcwd().
"/",
"",
212 $Exception->getFile().
"[".$Exception->getLine().
"]");
213 $Trace = preg_replace(
":(#[0-9]+) ".getcwd().
"/".
":",
"$1 ",
214 $Exception->getTraceAsString());
215 if (php_sapi_name() ==
"cli")
217 print
"Uncaught Exception\n" 218 .
"Message: ".$Message.
"\n" 219 .
"Location: ".$Location.
"\n" 225 ?><table width=
"100%" cellpadding=
"5" 226 style=
"border: 2px solid #666666; background: #CCCCCC; 227 font-family: Courier New, Courier, monospace; 228 margin-top: 10px;"><tr><td>
229 <div style=
"color: #666666;">
230 <span style=
"font-size: 150%;">
231 <b>Uncaught Exception</b></span><br />
232 <b>
Message:</b> <i><?= $Message ?></i><br />
233 <b>Location:</b> <i><?= $Location ?></i><br />
234 <b>Trace:</b> <blockquote><pre><?= $Trace ?></pre></blockquote>
236 </td></tr></table><?
PHP 239 # log exception if not running from command line 240 if (php_sapi_name() !==
"cli")
242 $TraceString = $Exception->getTraceAsString();
243 $TraceString = str_replace(
"\n",
", ", $TraceString);
244 $TraceString = preg_replace(
":(#[0-9]+) ".getcwd().
"/".
":",
245 "$1 ", $TraceString);
246 $LogMsg =
"Uncaught exception (".$Exception->getMessage().
")" 247 .
" at ".$Location.
"." 248 .
" TRACE: ".$TraceString
249 .
" URL: ".$this->FullUrl();
250 $this->LogError(self::LOGLVL_ERROR, $LogMsg);
269 public static function AddObjectDirectory(
270 $Dir, $Prefix =
"", $ClassPattern = NULL, $ClassReplacement = NULL)
272 # make sure directory has trailing slash 273 $Dir = $Dir.((substr($Dir, -1) !=
"/") ?
"/" :
"");
275 # add directory to directory list 276 self::$ObjectDirectories[$Dir] = array(
278 "ClassPattern" => $ClassPattern,
279 "ClassReplacement" => $ClassReplacement,
302 public function AddImageDirectories(
303 $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
305 # add directories to existing image directory list 306 $this->ImageDirList = $this->AddToDirList(
307 $this->ImageDirList, $Dir, $SearchLast, $SkipSlashCheck);
330 public function AddIncludeDirectories(
331 $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
333 # add directories to existing image directory list 334 $this->IncludeDirList = $this->AddToDirList(
335 $this->IncludeDirList, $Dir, $SearchLast, $SkipSlashCheck);
357 public function AddInterfaceDirectories(
358 $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
360 # add directories to existing image directory list 361 $this->InterfaceDirList = $this->AddToDirList(
362 $this->InterfaceDirList, $Dir, $SearchLast, $SkipSlashCheck);
384 public function AddFunctionDirectories(
385 $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
387 # add directories to existing image directory list 388 $this->FunctionDirList = $this->AddToDirList(
389 $this->FunctionDirList, $Dir, $SearchLast, $SkipSlashCheck);
397 public function SetBrowserDetectionFunc($DetectionFunc)
399 $this->BrowserDetectFunc = $DetectionFunc;
408 public function AddUnbufferedCallback($Callback, $Parameters=array())
410 if (is_callable($Callback))
412 $this->UnbufferedCallbacks[] = array($Callback, $Parameters);
422 public function TemplateLocationCacheExpirationInterval($NewInterval =
DB_NOVALUE)
424 return $this->UpdateSetting(
"TemplateLocationCacheInterval", $NewInterval);
430 public function ClearTemplateLocationCache()
432 $this->TemplateLocationCache = array();
433 $this->SaveTemplateLocationCache = TRUE;
442 public function ObjectLocationCacheExpirationInterval($NewInterval =
DB_NOVALUE)
444 return $this->UpdateSetting(
"ObjectLocationCacheInterval", $NewInterval);
450 public function ClearObjectLocationCache()
452 self::$ObjectLocationCache = array();
453 self::$SaveObjectLocationCache = TRUE;
462 public function UrlFingerprintingEnabled($NewValue =
DB_NOVALUE)
464 return $this->UpdateSetting(
"UrlFingerprintingEnabled", $NewValue);
474 public function ScssSupportEnabled($NewValue =
DB_NOVALUE)
476 return $this->UpdateSetting(
"ScssSupportEnabled", $NewValue);
487 public function GenerateCompactCss($NewValue =
DB_NOVALUE)
489 return $this->UpdateSetting(
"GenerateCompactCss", $NewValue);
500 public function UseMinimizedJavascript($NewValue =
DB_NOVALUE)
502 return $this->UpdateSetting(
"UseMinimizedJavascript", $NewValue);
513 public function JavascriptMinimizationEnabled($NewValue =
DB_NOVALUE)
515 return $this->UpdateSetting(
"JavascriptMinimizationEnabled", $NewValue);
531 public function RecordContextInCaseOfCrash(
532 $BacktraceOptions = 0, $BacktraceLimit = 0)
534 if (version_compare(PHP_VERSION,
"5.4.0",
">="))
536 $this->SavedContext = debug_backtrace(
537 $BacktraceOptions, $BacktraceLimit);
541 $this->SavedContext = debug_backtrace($BacktraceOptions);
543 array_shift($this->SavedContext);
550 public function LoadPage($PageName)
552 # perform any clean URL rewriting 553 $PageName = $this->RewriteCleanUrls($PageName);
555 # sanitize incoming page name and save local copy 556 $PageName = preg_replace(
"/[^a-zA-Z0-9_.-]/",
"", $PageName);
557 $this->PageName = $PageName;
559 # if page caching is turned on 560 if ($this->PageCacheEnabled())
562 # if we have a cached page 563 $CachedPage = $this->CheckForCachedPage($PageName);
564 if ($CachedPage !== NULL)
566 # set header to indicate cache hit was found 567 header(
"X-ScoutAF-Cache: HIT");
569 # display cached page and exit 575 # set header to indicate no cache hit was found 576 header(
"X-ScoutAF-Cache: MISS");
580 # buffer any output from includes or PHP file 583 # include any files needed to set up execution environment 584 $IncludeFileContext = array();
585 foreach ($this->EnvIncludes as $IncludeFile)
587 $IncludeFileContext = $this->FilterContext(self::CONTEXT_ENV,
588 self::IncludeFile($IncludeFile, $IncludeFileContext));
592 $this->SignalEvent(
"EVENT_PAGE_LOAD", array(
"PageName" => $PageName));
594 # signal PHP file load 595 $SignalResult = $this->SignalEvent(
"EVENT_PHP_FILE_LOAD", array(
596 "PageName" => $PageName));
598 # if signal handler returned new page name value 599 $NewPageName = $PageName;
600 if (($SignalResult[
"PageName"] != $PageName)
601 && strlen($SignalResult[
"PageName"]))
603 # if new page name value is page file 604 if (file_exists($SignalResult[
"PageName"]))
606 # use new value for PHP file name 607 $PageFile = $SignalResult[
"PageName"];
611 # use new value for page name 612 $NewPageName = $SignalResult[
"PageName"];
615 # update local copy of page name 616 $this->PageName = $NewPageName;
619 # if we do not already have a PHP file 620 if (!isset($PageFile))
622 # look for PHP file for page 623 $OurPageFile =
"pages/".$NewPageName.
".php";
624 $LocalPageFile =
"local/pages/".$NewPageName.
".php";
625 $PageFile = file_exists($LocalPageFile) ? $LocalPageFile
626 : (file_exists($OurPageFile) ? $OurPageFile
627 :
"pages/".$this->DefaultPage.
".php");
631 $IncludeFileContext = $this->FilterContext(self::CONTEXT_PAGE,
632 self::IncludeFile($PageFile, $IncludeFileContext));
634 # save buffered output to be displayed later after HTML file loads 635 $PageOutput = ob_get_contents();
638 # signal PHP file load is complete 640 $Context[
"Variables"] = $IncludeFileContext;
641 $this->SignalEvent(
"EVENT_PHP_FILE_LOAD_COMPLETE",
642 array(
"PageName" => $PageName,
"Context" => $Context));
643 $PageCompleteOutput = ob_get_contents();
646 # set up for possible TSR (Terminate and Stay Resident :)) 647 $ShouldTSR = $this->PrepForTSR();
649 # if PHP file indicated we should autorefresh to somewhere else 650 if (($this->JumpToPage) && ($this->JumpToPageDelay == 0))
652 if (!strlen(trim($PageOutput)))
654 # if client supports HTTP/1.1, use a 303 as it is most accurate 655 if ($_SERVER[
"SERVER_PROTOCOL"] ==
"HTTP/1.1")
657 header(
"HTTP/1.1 303 See Other");
658 header(
"Location: ".$this->JumpToPage);
662 # if the request was an HTTP/1.0 GET or HEAD, then 663 # use a 302 response code. 665 # NB: both RFC 2616 (HTTP/1.1) and RFC1945 (HTTP/1.0) 666 # explicitly prohibit automatic redirection via a 302 667 # if the request was not GET or HEAD. 668 if ($_SERVER[
"SERVER_PROTOCOL"] ==
"HTTP/1.0" &&
669 ($_SERVER[
"REQUEST_METHOD"] ==
"GET" ||
670 $_SERVER[
"REQUEST_METHOD"] ==
"HEAD") )
672 header(
"HTTP/1.0 302 Found");
673 header(
"Location: ".$this->JumpToPage);
676 # otherwise, fall back to a meta refresh 679 print
'<html><head><meta http-equiv="refresh" ' 680 .
'content="0; URL='.$this->JumpToPage.
'">' 681 .
'</head><body></body></html>';
686 # else if HTML loading is not suppressed 687 elseif (!$this->SuppressHTML)
689 # set content-type to get rid of diacritic errors 690 header(
"Content-Type: text/html; charset=" 691 .$this->HtmlCharset, TRUE);
693 # load common HTML file (defines common functions) if available 694 $CommonHtmlFile = $this->FindFile($this->IncludeDirList,
695 "Common", array(
"tpl",
"html"));
698 $IncludeFileContext = $this->FilterContext(self::CONTEXT_COMMON,
699 self::IncludeFile($CommonHtmlFile, $IncludeFileContext));
703 $this->LoadUIFunctions();
705 # begin buffering content 708 # signal HTML file load 709 $SignalResult = $this->SignalEvent(
"EVENT_HTML_FILE_LOAD", array(
710 "PageName" => $PageName));
712 # if signal handler returned new page name value 713 $NewPageName = $PageName;
714 $PageContentFile = NULL;
715 if (($SignalResult[
"PageName"] != $PageName)
716 && strlen($SignalResult[
"PageName"]))
718 # if new page name value is HTML file 719 if (file_exists($SignalResult[
"PageName"]))
721 # use new value for HTML file name 722 $PageContentFile = $SignalResult[
"PageName"];
726 # use new value for page name 727 $NewPageName = $SignalResult[
"PageName"];
731 # load page content HTML file if available 732 if ($PageContentFile === NULL)
734 $PageContentFile = $this->FindFile(
735 $this->InterfaceDirList, $NewPageName,
736 array(
"tpl",
"html"));
738 if ($PageContentFile)
740 $IncludeFileContext = $this->FilterContext(self::CONTEXT_INTERFACE,
741 self::IncludeFile($PageContentFile, $IncludeFileContext));
745 print
"<h2>ERROR: No HTML/TPL template found" 746 .
" for this page (".$NewPageName.
").</h2>";
749 # signal HTML file load complete 750 $SignalResult = $this->SignalEvent(
"EVENT_HTML_FILE_LOAD_COMPLETE");
752 # stop buffering and save output 753 $PageContentOutput = ob_get_contents();
756 # if standard page start/end have not been suppressed 757 $PageStartOutput =
"";
759 if (!$this->SuppressStdPageStartAndEnd)
761 # load page start HTML file if available 762 $PageStartFile = $this->FindFile($this->IncludeDirList,
"Start",
763 array(
"tpl",
"html"), array(
"StdPage",
"StandardPage"));
767 $IncludeFileContext = self::IncludeFile(
768 $PageStartFile, $IncludeFileContext);
769 $PageStartOutput = ob_get_contents();
772 $IncludeFileContext = $this->FilterContext(
773 self::CONTEXT_START, $IncludeFileContext);
775 # load page end HTML file if available 776 $PageEndFile = $this->FindFile($this->IncludeDirList,
"End",
777 array(
"tpl",
"html"), array(
"StdPage",
"StandardPage"));
781 self::IncludeFile($PageEndFile, $IncludeFileContext);
782 $PageEndOutput = ob_get_contents();
787 # clear include file context because it may be large and is no longer needed 788 unset($IncludeFileContext);
790 # if page auto-refresh requested 791 if ($this->JumpToPage)
793 # add auto-refresh tag to page 795 "http-equiv" =>
"refresh",
796 "content" => $this->JumpToPageDelay,
797 "url" => $this->JumpToPage,
802 $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
804 # get list of any required files not loaded 805 $RequiredFiles = $this->GetRequiredFilesNotYetLoaded($PageContentFile);
807 # add file loading tags to page 808 $FullPageOutput = $this->AddFileTagsToPageOutput(
809 $FullPageOutput, $RequiredFiles);
811 # add any requested meta tags to page 812 $FullPageOutput = $this->AddMetaTagsToPageOutput($FullPageOutput);
814 # perform any regular expression replacements in output 815 $NewFullPageOutput = preg_replace($this->OutputModificationPatterns,
816 $this->OutputModificationReplacements, $FullPageOutput);
818 # check to make sure replacements didn't fail 819 $FullPageOutput = $this->CheckOutputModification(
820 $FullPageOutput, $NewFullPageOutput,
821 "regular expression replacements");
823 # for each registered output modification callback 824 foreach ($this->OutputModificationCallbacks as $Info)
826 # set up data for callback 827 $this->OutputModificationCallbackInfo = $Info;
829 # perform output modification 830 $NewFullPageOutput = preg_replace_callback($Info[
"SearchPattern"],
831 array($this,
"OutputModificationCallbackShell"),
834 # check to make sure modification didn't fail 835 $ErrorInfo =
"callback info: ".print_r($Info, TRUE);
836 $FullPageOutput = $this->CheckOutputModification(
837 $FullPageOutput, $NewFullPageOutput, $ErrorInfo);
840 # provide the opportunity to modify full page output 841 $SignalResult = $this->SignalEvent(
"EVENT_PAGE_OUTPUT_FILTER", array(
842 "PageOutput" => $FullPageOutput));
843 if (isset($SignalResult[
"PageOutput"])
844 && strlen(trim($SignalResult[
"PageOutput"])))
846 $FullPageOutput = $SignalResult[
"PageOutput"];
849 # if relative paths may not work because we were invoked via clean URL 850 if ($this->CleanUrlRewritePerformed || self::WasUrlRewritten())
852 # if using the <base> tag is okay 853 $BaseUrl = $this->BaseUrl();
854 if ($this->UseBaseTag)
856 # add <base> tag to header 857 $PageStartOutput = str_replace(
"<head>",
858 "<head><base href=\"".$BaseUrl.
"\" />",
861 # re-assemble full page with new header 862 $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
864 # the absolute URL to the current page 865 $FullUrl = $BaseUrl . $this->GetPageLocation();
867 # make HREF attribute values with just a fragment ID 868 # absolute since they don't work with the <base> tag because 869 # they are relative to the current page/URL, not the site 871 $NewFullPageOutput = preg_replace(
872 array(
"%href=\"(#[^:\" ]+)\"%i",
"%href='(#[^:' ]+)'%i"),
873 array(
"href=\"".$FullUrl.
"$1\"",
"href='".$FullUrl.
"$1'"),
876 # check to make sure HREF cleanup didn't fail 877 $FullPageOutput = $this->CheckOutputModification(
878 $FullPageOutput, $NewFullPageOutput,
883 # try to fix any relative paths throughout code 884 $SrcFileExtensions =
"(js|css|gif|png|jpg|svg|ico)";
885 $RelativePathPatterns = array(
886 "%src=\"/?([^?*:;{}\\\\\" ]+)\.".$SrcFileExtensions.
"\"%i",
887 "%src='/?([^?*:;{}\\\\' ]+)\.".$SrcFileExtensions.
"'%i",
888 # don
't rewrite HREF attributes that are just 889 # fragment IDs because they are relative to the 890 # current page/URL, not the site root 891 "%href=\"/?([^#][^:\" ]*)\"%i", 892 "%href='/?([^#][^:
' ]*)'%i
", 893 "%action=\
"/?([^#][^:\" ]*)\"%i",
894 "%action='/?([^#][^:' ]*)'%i",
895 "%@import\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
896 "%@import\s+url\('/?([^:\" ]+)'\s*\)%i",
897 "%src:\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
898 "%src:\s+url\('/?([^:\" ]+)'\s*\)%i",
899 "%@import\s+\"/?([^:\" ]+)\"\s*%i",
900 "%@import\s+'/?([^:\" ]+)'\s*%i",
902 $RelativePathReplacements = array(
903 "src=\"".$BaseUrl.
"$1.$2\"",
904 "src=\"".$BaseUrl.
"$1.$2\"",
905 "href=\"".$BaseUrl.
"$1\"",
906 "href=\"".$BaseUrl.
"$1\"",
907 "action=\"".$BaseUrl.
"$1\"",
908 "action=\"".$BaseUrl.
"$1\"",
909 "@import url(\"".$BaseUrl.
"$1\")",
910 "@import url('".$BaseUrl.
"$1')",
911 "src: url(\"".$BaseUrl.
"$1\")",
912 "src: url('".$BaseUrl.
"$1')",
913 "@import \"".$BaseUrl.
"$1\"",
914 "@import '".$BaseUrl.
"$1'",
916 $NewFullPageOutput = preg_replace($RelativePathPatterns,
917 $RelativePathReplacements, $FullPageOutput);
919 # check to make sure relative path fixes didn't fail 920 $FullPageOutput = $this->CheckOutputModification(
921 $FullPageOutput, $NewFullPageOutput,
922 "relative path fixes");
926 # handle any necessary alternate domain rewriting 927 $FullPageOutput = $this->RewriteAlternateDomainUrls($FullPageOutput);
929 # update page cache for this page 930 $this->UpdatePageCache($PageName, $FullPageOutput);
932 # write out full page 933 print $FullPageOutput;
936 # run any post-processing routines 937 foreach ($this->PostProcessingFuncs as $Func)
939 call_user_func_array($Func[
"FunctionName"], $Func[
"Arguments"]);
942 # write out any output buffered from page code execution 943 if (strlen($PageOutput))
945 if (!$this->SuppressHTML)
947 ?><table width=
"100%" cellpadding=
"5" 948 style=
"border: 2px solid #666666; background: #CCCCCC; 949 font-family: Courier New, Courier, monospace; 950 margin-top: 10px;"><tr><td><?
PHP 952 if ($this->JumpToPage)
954 ?><div style=
"color: #666666;"><span style=
"font-size: 150%;">
955 <b>Page Jump Aborted</b></span>
956 (because of error or other unexpected output)<br />
958 <i><?
PHP print($this->JumpToPage); ?></i></div><?
PHP 961 if (!$this->SuppressHTML)
963 ?></td></tr></table><?
PHP 967 # write out any output buffered from the page code execution complete signal 968 if (!$this->JumpToPage && !$this->SuppressHTML && strlen($PageCompleteOutput))
970 print $PageCompleteOutput;
973 # log slow page loads 974 if ($this->LogSlowPageLoads()
975 && !$this->DoNotLogSlowPageLoad
976 && ($this->GetElapsedExecutionTime()
977 >= ($this->SlowPageLoadThreshold())))
979 $RemoteHost = gethostbyaddr($_SERVER[
"REMOTE_ADDR"]);
980 if ($RemoteHost === FALSE)
982 $RemoteHost = $_SERVER[
"REMOTE_ADDR"];
984 elseif ($RemoteHost != $_SERVER[
"REMOTE_ADDR"])
986 $RemoteHost .=
" (".$_SERVER[
"REMOTE_ADDR"].
")";
988 $SlowPageLoadMsg =
"Slow page load (" 989 .intval($this->GetElapsedExecutionTime()).
"s) for " 990 .$this->FullUrl().
" from ".$RemoteHost;
991 $this->LogMessage(self::LOGLVL_INFO, $SlowPageLoadMsg);
994 # execute callbacks that should not have their output buffered 995 foreach ($this->UnbufferedCallbacks as $Callback)
997 call_user_func_array($Callback[0], $Callback[1]);
1000 # log high memory usage 1001 if (function_exists(
"memory_get_peak_usage"))
1003 $MemoryThreshold = ($this->HighMemoryUsageThreshold()
1004 * $this->GetPhpMemoryLimit()) / 100;
1005 if ($this->LogHighMemoryUsage()
1006 && (memory_get_peak_usage(TRUE) >= $MemoryThreshold))
1008 $HighMemUsageMsg =
"High peak memory usage (" 1009 .number_format(memory_get_peak_usage(TRUE)).
") for " 1010 .$this->FullUrl().
" from " 1011 .$_SERVER[
"REMOTE_ADDR"];
1012 $this->LogMessage(self::LOGLVL_INFO, $HighMemUsageMsg);
1016 # terminate and stay resident (TSR!) if indicated and HTML has been output 1017 # (only TSR if HTML has been output because otherwise browsers will misbehave) 1018 if ($ShouldTSR) { $this->LaunchTSR(); }
1026 public function GetPageName()
1028 return $this->PageName;
1036 public function GetPageLocation()
1038 # retrieve current URL 1039 $Url = self::GetScriptUrl();
1041 # remove the base path if present 1042 $BasePath = $this->Settings[
"BasePath"];
1043 if (stripos($Url, $BasePath) === 0)
1045 $Url = substr($Url, strlen($BasePath));
1048 # if we're being accessed via an alternate domain, 1049 # add the appropriate prefix in 1050 if ($this->HtaccessSupport() &&
1051 self::$RootUrlOverride !== NULL)
1053 $VHost = $_SERVER[
"SERVER_NAME"];
1054 if (isset($this->AlternateDomainPrefixes[$VHost]))
1056 $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
1057 $Url = $ThisPrefix.
"/".$Url;
1069 public function GetPageUrl()
1071 return self::BaseUrl() . $this->GetPageLocation();
1085 public function SetJumpToPage($Page, $Delay = 0, $IsLiteral = FALSE)
1089 && (strpos($Page,
"?") === FALSE)
1090 && ((strpos($Page,
"=") !== FALSE)
1091 || ((stripos($Page,
".php") === FALSE)
1092 && (stripos($Page,
".htm") === FALSE)
1093 && (strpos($Page,
"/") === FALSE)))
1094 && (stripos($Page,
"http://") !== 0)
1095 && (stripos($Page,
"https://") !== 0))
1097 $this->JumpToPage = self::BaseUrl() .
"index.php?P=".$Page;
1101 $this->JumpToPage = $Page;
1103 $this->JumpToPageDelay = $Delay;
1110 public function JumpToPageIsSet()
1112 return ($this->JumpToPage === NULL) ? FALSE : TRUE;
1124 public function HtmlCharset($NewSetting = NULL)
1126 if ($NewSetting !== NULL) { $this->HtmlCharset = $NewSetting; }
1127 return $this->HtmlCharset;
1139 public function DoNotMinimizeFile($File)
1141 if (!is_array($File)) { $File = array($File); }
1142 $this->DoNotMinimizeList = array_merge($this->DoNotMinimizeList, $File);
1155 public function UseBaseTag($NewValue = NULL)
1157 if ($NewValue !== NULL) { $this->UseBaseTag = $NewValue ? TRUE : FALSE; }
1158 return $this->UseBaseTag;
1168 public function SuppressHTMLOutput($NewSetting = TRUE)
1170 $this->SuppressHTML = $NewSetting;
1180 public function SuppressStandardPageStartAndEnd($NewSetting = TRUE)
1182 $this->SuppressStdPageStartAndEnd = $NewSetting;
1190 public static function DefaultUserInterface($UIName = NULL)
1192 if ($UIName !== NULL)
1194 self::$DefaultUI = $UIName;
1196 return self::$DefaultUI;
1205 public static function ActiveUserInterface($UIName = NULL)
1207 if ($UIName !== NULL)
1209 self::$ActiveUI = preg_replace(
"/^SPTUI--/",
"", $UIName);
1211 return self::$ActiveUI;
1224 public function GetUserInterfaces($FilterExp = NULL)
1228 if (!isset($Interfaces[$FilterExp]))
1230 # retrieve paths to user interface directories 1231 $Paths = $this->GetUserInterfacePaths($FilterExp);
1233 # start out with an empty list 1234 $Interfaces[$FilterExp] = array();
1236 # for each possible UI directory 1237 foreach ($Paths as $CanonicalName => $Path)
1239 # if name file available 1240 $LabelFile = $Path.
"/NAME";
1241 if (is_readable($LabelFile))
1244 $Label = file_get_contents($LabelFile);
1246 # if the UI name looks reasonable 1247 if (strlen(trim($Label)))
1250 $Interfaces[$FilterExp][$CanonicalName] = $Label;
1254 # if we do not have a name yet 1255 if (!isset($Interfaces[$FilterExp][$CanonicalName]))
1257 # use base directory for name 1258 $Interfaces[$FilterExp][$CanonicalName] = basename($Path);
1263 # return list to caller 1264 return $Interfaces[$FilterExp];
1275 public function GetUserInterfacePaths($FilterExp = NULL)
1277 static $InterfacePaths;
1279 if (!isset($InterfacePaths[$FilterExp]))
1281 # extract possible UI directories from interface directory list 1282 $InterfaceDirs = array();
1283 foreach ($this->ExpandDirectoryList($this->InterfaceDirList) as $Dir)
1286 if (preg_match(
"#([a-zA-Z0-9/]*interface)/[a-zA-Z0-9%/]*#",
1290 if (!in_array($Dir, $InterfaceDirs))
1292 $InterfaceDirs[] = $Dir;
1297 # reverse order of interface directories so that the directory 1298 # returned is the base directory for the interface 1299 $InterfaceDirs = array_reverse($InterfaceDirs);
1301 # start out with an empty list 1302 $InterfacePaths[$FilterExp] = array();
1303 $InterfacesFound = array();
1305 # for each possible UI directory 1306 foreach ($InterfaceDirs as $InterfaceDir)
1308 # check if the dir exists 1309 if (!is_dir($InterfaceDir))
1314 $Dir = dir($InterfaceDir);
1316 # for each file in current directory 1317 while (($DirEntry = $Dir->read()) !== FALSE)
1319 $InterfacePath = $InterfaceDir.
"/".$DirEntry;
1321 # skip anything we have already found 1322 # or that doesn't have a name in the required format 1323 # or that isn't a directory 1324 # or that doesn't match the filter regex (if supplied) 1325 if (in_array($DirEntry, $InterfacesFound)
1326 || !preg_match(
'/^[a-zA-Z0-9]+$/', $DirEntry)
1327 || !is_dir($InterfacePath)
1328 || (($FilterExp !== NULL)
1329 && !preg_match($FilterExp, $InterfacePath)))
1334 # add interface to list 1335 $InterfacePaths[$FilterExp][$DirEntry] = $InterfacePath;
1336 $InterfacesFound[] = $DirEntry;
1343 # return list to caller 1344 return $InterfacePaths[$FilterExp];
1371 public function AddPostProcessingCall($FunctionName,
1372 &$Arg1 = self::NOVALUE, &$Arg2 = self::NOVALUE, &$Arg3 = self::NOVALUE,
1373 &$Arg4 = self::NOVALUE, &$Arg5 = self::NOVALUE, &$Arg6 = self::NOVALUE,
1374 &$Arg7 = self::NOVALUE, &$Arg8 = self::NOVALUE, &$Arg9 = self::NOVALUE)
1376 $FuncIndex = count($this->PostProcessingFuncs);
1377 $this->PostProcessingFuncs[$FuncIndex][
"FunctionName"] = $FunctionName;
1378 $this->PostProcessingFuncs[$FuncIndex][
"Arguments"] = array();
1380 while (isset(${
"Arg".$Index}) && (${
"Arg".$Index} !== self::NOVALUE))
1382 $this->PostProcessingFuncs[$FuncIndex][
"Arguments"][$Index]
1393 public function AddEnvInclude($FileName)
1395 $this->EnvIncludes[] = $FileName;
1414 public function SetContextFilter($Context, $NewSetting)
1416 if (($NewSetting === TRUE)
1417 || ($NewSetting === FALSE)
1418 || is_array($NewSetting))
1420 $this->ContextFilters[$Context] = $NewSetting;
1422 elseif (is_string($NewSetting))
1424 $this->ContextFilters[$Context] = array($NewSetting);
1428 throw new InvalidArgumentException(
1429 "Invalid setting (".$NewSetting.
").");
1433 const CONTEXT_ENV = 1;
1435 const CONTEXT_PAGE = 2;
1437 const CONTEXT_COMMON = 3;
1439 const CONTEXT_INTERFACE = 4;
1441 const CONTEXT_START = 5;
1443 const CONTEXT_END = 6;
1451 public function GUIFile($FileName)
1453 # determine which location to search based on file suffix 1454 $FileType = $this->GetFileType($FileName);
1455 $DirList = ($FileType == self::FT_IMAGE)
1456 ? $this->ImageDirList : $this->IncludeDirList;
1458 # if directed to use minimized JavaScript file 1459 if (($FileType == self::FT_JAVASCRIPT) && $this->UseMinimizedJavascript())
1461 # look for minimized version of file 1462 $MinimizedFileName = substr_replace($FileName,
".min", -3, 0);
1463 $FoundFileName = $this->FindFile($DirList, $MinimizedFileName);
1465 # if minimized file was not found 1466 if (is_null($FoundFileName))
1468 # look for unminimized file 1469 $FoundFileName = $this->FindFile($DirList, $FileName);
1471 # if unminimized file found 1472 if (!is_null($FoundFileName))
1474 # if minimization enabled and supported 1475 if ($this->JavascriptMinimizationEnabled()
1476 && self::JsMinRewriteSupport())
1478 # attempt to create minimized file 1479 $MinFileName = $this->MinimizeJavascriptFile(
1482 # if minimization succeeded 1483 if ($MinFileName !== NULL)
1485 # use minimized version 1486 $FoundFileName = $MinFileName;
1488 # save file modification time if needed for fingerprinting 1489 if ($this->UrlFingerprintingEnabled())
1491 $FileMTime = filemtime($FoundFileName);
1494 # strip off the cache location, allowing .htaccess 1495 # to handle that for us 1496 $FoundFileName = str_replace(
1497 self::$JSMinCacheDir.
"/",
"", $FoundFileName);
1503 # else if directed to use SCSS files 1504 elseif (($FileType == self::FT_CSS) && $this->ScssSupportEnabled())
1506 # look for SCSS version of file 1507 $SourceFileName = preg_replace(
"/.css$/",
".scss", $FileName);
1508 $FoundSourceFileName = $this->FindFile($DirList, $SourceFileName);
1510 # if SCSS file not found 1511 if ($FoundSourceFileName === NULL)
1514 $FoundFileName = $this->FindFile($DirList, $FileName);
1518 # compile SCSS file (if updated) and return resulting CSS file 1519 $FoundFileName = $this->CompileScssFile($FoundSourceFileName);
1521 # save file modification time if needed for fingerprinting 1522 if ($this->UrlFingerprintingEnabled())
1524 $FileMTime = filemtime($FoundFileName);
1527 # strip off the cache location, allowing .htaccess to handle that for us 1528 if (self::ScssRewriteSupport())
1530 $FoundFileName = str_replace(
1531 self::$ScssCacheDir.
"/",
"", $FoundFileName);
1535 # otherwise just search for the file 1538 $FoundFileName = $this->FindFile($DirList, $FileName);
1541 # add non-image files to list of found files (used for required files loading) 1542 if ($FileType != self::FT_IMAGE)
1543 { $this->FoundUIFiles[] = basename($FoundFileName); }
1545 # if UI file fingerprinting is enabled and supported 1546 if ($this->UrlFingerprintingEnabled()
1547 && self::UrlFingerprintingRewriteSupport()
1548 && (isset($FileMTime) || file_exists($FoundFileName)))
1550 # if file does not appear to be a server-side inclusion 1551 if (!preg_match(
'/\.(html|php)$/i', $FoundFileName))
1553 # for each URL fingerprinting blacklist entry 1554 $OnBlacklist = FALSE;
1555 foreach ($this->UrlFingerprintBlacklist as $BlacklistEntry)
1557 # if entry looks like a regular expression pattern 1558 if ($BlacklistEntry[0] == substr($BlacklistEntry, -1))
1560 # check file name against regular expression 1561 if (preg_match($BlacklistEntry, $FoundFileName))
1563 $OnBlacklist = TRUE;
1569 # check file name directly against entry 1570 if (basename($FoundFileName) == $BlacklistEntry)
1572 $OnBlacklist = TRUE;
1578 # if file was not on blacklist 1581 # get file modification time if not already retrieved 1582 if (!isset($FileMTime))
1584 $FileMTime = filemtime($FoundFileName);
1587 # add timestamp fingerprint to file name 1588 $Fingerprint = sprintf(
"%06X",
1589 ($FileMTime % 0xFFFFFF));
1590 $FoundFileName = preg_replace(
"/^(.+)\.([a-z]+)$/",
1591 "$1.".$Fingerprint.
".$2",
1597 # return file name to caller 1598 return $FoundFileName;
1609 public function PUIFile($FileName)
1611 $FullFileName = $this->GUIFile($FileName);
1612 if ($FullFileName) { print($FullFileName); }
1629 public function IncludeUIFile($FileNames, $AdditionalAttributes = NULL)
1631 # convert file name to array if necessary 1632 if (!is_array($FileNames)) { $FileNames = array($FileNames); }
1634 # pad additional attributes if supplied 1635 $AddAttribs = $AdditionalAttributes ?
" ".$AdditionalAttributes :
"";
1638 foreach ($FileNames as $BaseFileName)
1640 # retrieve full file name 1641 $FileName = $this->GUIFile($BaseFileName);
1646 # print appropriate tag 1647 print $this->GetUIFileLoadingTag($FileName, $AdditionalAttributes);
1650 # if we are not already loading an override file 1651 if (!preg_match(
"/-Override.(css|scss|js)$/", $BaseFileName))
1653 # attempt to load override file if available 1654 $FileType = $this->GetFileType($BaseFileName);
1658 $OverrideFileName = preg_replace(
1659 "/\.(css|scss)$/",
"-Override.$1",
1661 $this->IncludeUIFile($OverrideFileName,
1662 $AdditionalAttributes);
1665 case self::FT_JAVASCRIPT:
1666 $OverrideFileName = preg_replace(
1667 "/\.js$/",
"-Override.js",
1669 $this->IncludeUIFile($OverrideFileName,
1670 $AdditionalAttributes);
1683 public function DoNotUrlFingerprint($Pattern)
1685 $this->UrlFingerprintBlacklist[] = $Pattern;
1698 public function RequireUIFile($FileName, $Order = self::ORDER_MIDDLE)
1700 $this->AdditionalRequiredUIFiles[$FileName] = $Order;
1708 public static function GetFileType($FileName)
1710 static $FileTypeCache;
1711 if (isset($FileTypeCache[$FileName]))
1713 return $FileTypeCache[$FileName];
1716 $FileSuffix = strtolower(substr($FileName, -3));
1717 if ($FileSuffix ==
"css")
1719 $FileTypeCache[$FileName] = self::FT_CSS;
1721 elseif ($FileSuffix ==
".js")
1723 $FileTypeCache[$FileName] = self::FT_JAVASCRIPT;
1725 elseif (($FileSuffix ==
"gif")
1726 || ($FileSuffix ==
"jpg")
1727 || ($FileSuffix ==
"png")
1728 || ($FileSuffix ==
"svg")
1729 || ($FileSuffix ==
"ico"))
1731 $FileTypeCache[$FileName] = self::FT_IMAGE;
1735 $FileTypeCache[$FileName] = self::FT_OTHER;
1738 return $FileTypeCache[$FileName];
1747 const FT_JAVASCRIPT = 3;
1757 public function LoadFunction($Callback)
1759 # if specified function is not currently available 1760 if (!is_callable($Callback))
1762 # if function info looks legal 1763 if (is_string($Callback) && strlen($Callback))
1765 # start with function directory list 1766 $Locations = $this->FunctionDirList;
1768 # add object directories to list 1769 $Locations = array_merge(
1770 $Locations, array_keys(self::$ObjectDirectories));
1772 # look for function file 1773 $FunctionFileName = $this->FindFile($Locations,
"F-".$Callback,
1774 array(
"php",
"html"));
1776 # if function file was found 1777 if ($FunctionFileName)
1779 # load function file 1780 include_once($FunctionFileName);
1784 # log error indicating function load failed 1785 $this->LogError(self::LOGLVL_ERROR,
"Unable to load function" 1786 .
" for callback \"".$Callback.
"\".");
1791 # log error indicating specified function info was bad 1792 $this->LogError(self::LOGLVL_ERROR,
"Unloadable callback value" 1794 .
" passed to AF::LoadFunction() by " 1795 .StdLib::GetMyCaller().
".");
1799 # report to caller whether function load succeeded 1800 return is_callable($Callback);
1807 public function GetElapsedExecutionTime()
1809 return microtime(TRUE) - $this->ExecutionStartTime;
1816 public function GetSecondsBeforeTimeout()
1818 return $this->MaxExecutionTime() - $this->GetElapsedExecutionTime();
1825 public function AddMetaTag($Attribs)
1827 # add new meta tag to list 1828 $this->MetaTags[] = $Attribs;
1834 # ---- Page Caching ------------------------------------------------------ 1844 public function PageCacheEnabled($NewValue =
DB_NOVALUE)
1846 return $this->UpdateSetting(
"PageCacheEnabled", $NewValue);
1855 public function PageCacheExpirationPeriod($NewValue =
DB_NOVALUE)
1857 return $this->UpdateSetting(
"PageCacheExpirationPeriod", $NewValue);
1864 public function DoNotCacheCurrentPage()
1866 $this->CacheCurrentPage = FALSE;
1875 public function AddPageCacheTag($Tag, $Pages = NULL)
1878 $Tag = strtolower($Tag);
1880 # if pages were supplied 1881 if ($Pages !== NULL)
1883 # add pages to list for this tag 1884 if (isset($this->PageCacheTags[$Tag]))
1886 $this->PageCacheTags[$Tag] = array_merge(
1887 $this->PageCacheTags[$Tag], $Pages);
1891 $this->PageCacheTags[$Tag] = $Pages;
1896 # add current page to list for this tag 1897 $this->PageCacheTags[$Tag][] =
"CURRENT";
1906 public function ClearPageCacheForTag($Tag)
1909 $TagId = $this->GetPageCacheTagId($Tag);
1911 # delete pages and tag/page connections for specified tag 1912 $this->DB->Query(
"DELETE CP, CPTI" 1913 .
" FROM AF_CachedPages CP, AF_CachedPageTagInts CPTI" 1914 .
" WHERE CPTI.TagId = ".intval($TagId)
1915 .
" AND CP.CacheId = CPTI.CacheId");
1921 public function ClearPageCache()
1923 # clear all page cache tables 1924 $this->DB->Query(
"TRUNCATE TABLE AF_CachedPages");
1925 $this->DB->Query(
"TRUNCATE TABLE AF_CachedPageTags");
1926 $this->DB->Query(
"TRUNCATE TABLE AF_CachedPageTagInts");
1935 public function GetPageCacheInfo()
1937 $Length = $this->DB->Query(
"SELECT COUNT(*) AS CacheLen" 1938 .
" FROM AF_CachedPages",
"CacheLen");
1939 $Oldest = $this->DB->Query(
"SELECT CachedAt FROM AF_CachedPages" 1940 .
" ORDER BY CachedAt ASC LIMIT 1",
"CachedAt");
1942 "NumberOfEntries" => $Length,
1943 "OldestTimestamp" => strtotime($Oldest),
1950 # ---- Logging ----------------------------------------------------------- 1967 public function LogSlowPageLoads(
1970 return $this->UpdateSetting(
1971 "LogSlowPageLoads", $NewValue, $Persistent);
1984 public function SlowPageLoadThreshold(
1987 return $this->UpdateSetting(
1988 "SlowPageLoadThreshold", $NewValue, $Persistent);
2004 public function LogHighMemoryUsage(
2007 return $this->UpdateSetting(
2008 "LogHighMemoryUsage", $NewValue, $Persistent);
2022 public function HighMemoryUsageThreshold(
2025 return $this->UpdateSetting(
2026 "HighMemoryUsageThreshold", $NewValue, $Persistent);
2042 public function LogError($Level, $Msg)
2044 # if error level is at or below current logging level 2045 if ($this->Settings[
"LoggingLevel"] >= $Level)
2047 # attempt to log error message 2048 $Result = $this->LogMessage($Level, $Msg);
2050 # if logging attempt failed and level indicated significant error 2051 if (($Result === FALSE) && ($Level <= self::LOGLVL_ERROR))
2053 # throw exception about inability to log error 2054 static $AlreadyThrewException = FALSE;
2055 if (!$AlreadyThrewException)
2057 $AlreadyThrewException = TRUE;
2058 throw new Exception(
"Unable to log error (".$Level.
": ".$Msg
2059 .
") to ".$this->LogFileName);
2063 # report to caller whether message was logged 2068 # report to caller that message was not logged 2084 public function LogMessage($Level, $Msg)
2086 # if message level is at or below current logging level 2087 if ($this->Settings[
"LoggingLevel"] >= $Level)
2089 # attempt to open log file 2090 $FHndl = @fopen($this->LogFileName,
"a");
2092 # if log file could not be open 2093 if ($FHndl === FALSE)
2095 # report to caller that message was not logged 2101 $ErrorAbbrevs = array(
2102 self::LOGLVL_FATAL =>
"FTL",
2103 self::LOGLVL_ERROR =>
"ERR",
2104 self::LOGLVL_WARNING =>
"WRN",
2105 self::LOGLVL_INFO =>
"INF",
2106 self::LOGLVL_DEBUG =>
"DBG",
2107 self::LOGLVL_TRACE =>
"TRC",
2109 $Msg = str_replace(array(
"\n",
"\t",
"\r"),
" ", $Msg);
2110 $Msg = substr(trim($Msg), 0, self::LOGFILE_MAX_LINE_LENGTH);
2111 $LogEntry = date(
"Y-m-d H:i:s")
2112 .
" ".($this->RunningInBackground ?
"B" :
"F")
2113 .
" ".$ErrorAbbrevs[$Level]
2116 # write entry to log 2117 $Success = fwrite($FHndl, $LogEntry.
"\n");
2122 # report to caller whether message was logged 2123 return ($Success === FALSE) ? FALSE : TRUE;
2128 # report to caller that message was not logged 2154 public function LoggingLevel($NewValue =
DB_NOVALUE)
2156 # constrain new level (if supplied) to within legal bounds 2159 $NewValue = max(min($NewValue, 6), 1);
2162 # set new logging level (if supplied) and return current level to caller 2163 return $this->UpdateSetting(
"LoggingLevel", $NewValue);
2172 public function LogFile($NewValue = NULL)
2174 if ($NewValue !== NULL) { $this->LogFileName = $NewValue; }
2175 return $this->LogFileName;
2187 public function GetLogEntries($Limit = 0)
2189 # return no entries if there isn't a log file 2190 # or we can't read it or it's empty 2191 $LogFile = $this->LogFile();
2192 if (!is_readable($LogFile) || !filesize($LogFile))
2197 # if max number of entries specified 2200 # load lines from file 2201 $FHandle = fopen($LogFile,
"r");
2202 $FileSize = filesize($LogFile);
2203 $SeekPosition = max(0,
2204 ($FileSize - (self::LOGFILE_MAX_LINE_LENGTH
2206 fseek($FHandle, $SeekPosition);
2207 $Block = fread($FHandle, ($FileSize - $SeekPosition));
2209 $Lines = explode(PHP_EOL, $Block);
2212 # prune array back to requested number of entries 2213 $Lines = array_slice($Lines, (0 - $Limit));
2217 # load all lines from log file 2218 $Lines = file($LogFile, FILE_IGNORE_NEW_LINES);
2219 if ($Lines === FALSE)
2225 # reverse line order 2226 $Lines = array_reverse($Lines);
2228 # for each log file line 2230 foreach ($Lines as $Line)
2232 # attempt to parse line into component parts 2233 $Pieces = explode(
" ", $Line, 5);
2234 $Date = isset($Pieces[0]) ? $Pieces[0] :
"";
2235 $Time = isset($Pieces[1]) ? $Pieces[1] :
"";
2236 $Back = isset($Pieces[2]) ? $Pieces[2] :
"";
2237 $Level = isset($Pieces[3]) ? $Pieces[3] :
"";
2238 $Msg = isset($Pieces[4]) ? $Pieces[4] :
"";
2240 # skip line if it looks invalid 2241 $ErrorAbbrevs = array(
2242 "FTL" => self::LOGLVL_FATAL,
2243 "ERR" => self::LOGLVL_ERROR,
2244 "WRN" => self::LOGLVL_WARNING,
2245 "INF" => self::LOGLVL_INFO,
2246 "DBG" => self::LOGLVL_DEBUG,
2247 "TRC" => self::LOGLVL_TRACE,
2249 if ((($Back !=
"F") && ($Back !=
"B"))
2250 || !array_key_exists($Level, $ErrorAbbrevs)
2256 # convert parts into appropriate values and add to entries 2258 "Time" => strtotime($Date.
" ".$Time),
2259 "Background" => ($Back ==
"B") ? TRUE : FALSE,
2260 "Level" => $ErrorAbbrevs[$Level],
2265 # return entries to caller 2273 const LOGLVL_TRACE = 6;
2278 const LOGLVL_DEBUG = 5;
2284 const LOGLVL_INFO = 4;
2289 const LOGLVL_WARNING = 3;
2295 const LOGLVL_ERROR = 2;
2300 const LOGLVL_FATAL = 1;
2305 const LOGFILE_MAX_LINE_LENGTH = 2048;
2310 # ---- Event Handling ---------------------------------------------------- 2317 const EVENTTYPE_DEFAULT = 1;
2323 const EVENTTYPE_CHAIN = 2;
2329 const EVENTTYPE_FIRST = 3;
2337 const EVENTTYPE_NAMED = 4;
2340 const ORDER_FIRST = 1;
2342 const ORDER_MIDDLE = 2;
2344 const ORDER_LAST = 3;
2354 public function RegisterEvent($EventsOrEventName, $EventType = NULL)
2356 # convert parameters to array if not already in that form 2357 $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2358 : array($EventsOrEventName => $EventType);
2361 foreach ($Events as $Name => $Type)
2363 # store event information 2364 $this->RegisteredEvents[$Name][
"Type"] = $Type;
2365 $this->RegisteredEvents[$Name][
"Hooks"] = array();
2375 public function IsRegisteredEvent($EventName)
2377 return array_key_exists($EventName, $this->RegisteredEvents)
2387 public function IsHookedEvent($EventName)
2389 # the event isn't hooked to if it isn't even registered 2390 if (!$this->IsRegisteredEvent($EventName))
2395 # return TRUE if there is at least one callback hooked to the event 2396 return count($this->RegisteredEvents[$EventName][
"Hooks"]) > 0;
2412 public function HookEvent(
2413 $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2415 # convert parameters to array if not already in that form 2416 $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2417 : array($EventsOrEventName => $Callback);
2421 foreach ($Events as $EventName => $EventCallback)
2423 # if callback is valid 2424 if (is_callable($EventCallback))
2426 # if this is a periodic event we process internally 2427 if (isset($this->PeriodicEvents[$EventName]))
2430 $this->ProcessPeriodicEvent($EventName, $EventCallback);
2432 # if specified event has been registered 2433 elseif (isset($this->RegisteredEvents[$EventName]))
2435 # add callback for event 2436 $this->RegisteredEvents[$EventName][
"Hooks"][]
2437 = array(
"Callback" => $EventCallback,
"Order" => $Order);
2439 # sort callbacks by order 2440 if (count($this->RegisteredEvents[$EventName][
"Hooks"]) > 1)
2442 usort($this->RegisteredEvents[$EventName][
"Hooks"],
2445 $A[
"Order"], $B[
"Order"]);
2460 # report to caller whether all callbacks were hooked 2477 public function UnhookEvent(
2478 $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2480 # convert parameters to array if not already in that form 2481 $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2482 : array($EventsOrEventName => $Callback);
2486 foreach ($Events as $EventName => $EventCallback)
2488 # if this event has been registered and hooked 2489 if (isset($this->RegisteredEvents[$EventName])
2490 && count($this->RegisteredEvents[$EventName]))
2492 # if this callback has been hooked for this event 2493 $CallbackData = array(
"Callback" => $EventCallback,
"Order" => $Order);
2494 if (in_array($CallbackData,
2495 $this->RegisteredEvents[$EventName][
"Hooks"]))
2498 $HookIndex = array_search($CallbackData,
2499 $this->RegisteredEvents[$EventName][
"Hooks"]);
2500 unset($this->RegisteredEvents[$EventName][
"Hooks"][$HookIndex]);
2506 # report number of callbacks unhooked to caller 2507 return $UnhookCount;
2520 public function SignalEvent($EventName, $Parameters = NULL)
2522 $ReturnValue = NULL;
2524 # if event has been registered 2525 if (isset($this->RegisteredEvents[$EventName]))
2527 # set up default return value (if not NULL) 2528 switch ($this->RegisteredEvents[$EventName][
"Type"])
2530 case self::EVENTTYPE_CHAIN:
2531 $ReturnValue = $Parameters;
2534 case self::EVENTTYPE_NAMED:
2535 $ReturnValue = array();
2539 # for each callback for this event 2540 foreach ($this->RegisteredEvents[$EventName][
"Hooks"] as $Hook)
2543 $Callback = $Hook[
"Callback"];
2544 $Result = ($Parameters !== NULL)
2545 ? call_user_func_array($Callback, $Parameters)
2546 : call_user_func($Callback);
2548 # process return value based on event type 2549 switch ($this->RegisteredEvents[$EventName][
"Type"])
2551 case self::EVENTTYPE_CHAIN:
2552 if ($Result !== NULL)
2554 foreach ($Parameters as $Index => $Value)
2556 if (array_key_exists($Index, $Result))
2558 $Parameters[$Index] = $Result[$Index];
2561 $ReturnValue = $Parameters;
2565 case self::EVENTTYPE_FIRST:
2566 if ($Result !== NULL)
2568 $ReturnValue = $Result;
2573 case self::EVENTTYPE_NAMED:
2574 $CallbackName = is_array($Callback)
2575 ? (is_object($Callback[0])
2576 ? get_class($Callback[0])
2577 : $Callback[0]).
"::".$Callback[1]
2579 $ReturnValue[$CallbackName] = $Result;
2589 $this->LogError(self::LOGLVL_WARNING,
2590 "Unregistered event (".$EventName.
") signaled by " 2591 .StdLib::GetMyCaller().
".");
2594 # return value if any to caller 2595 return $ReturnValue;
2603 public function IsStaticOnlyEvent($EventName)
2605 return isset($this->PeriodicEvents[$EventName]) ? TRUE : FALSE;
2618 public function EventWillNextRunAt($EventName, $Callback)
2620 # if event is not a periodic event report failure to caller 2621 if (!array_key_exists($EventName, $this->EventPeriods)) {
return FALSE; }
2623 # retrieve last execution time for event if available 2624 $Signature = self::GetCallbackSignature($Callback);
2625 $LastRunTime = $this->DB->Query(
"SELECT LastRunAt FROM PeriodicEvents" 2626 .
" WHERE Signature = '".addslashes($Signature).
"'",
"LastRunAt");
2628 # if event was not found report failure to caller 2629 if ($LastRunTime === NULL) {
return FALSE; }
2631 # calculate next run time based on event period 2632 $NextRunTime = strtotime($LastRunTime) + $this->EventPeriods[$EventName];
2634 # report next run time to caller 2635 return $NextRunTime;
2653 public function GetKnownPeriodicEvents()
2655 # retrieve last execution times 2656 $this->DB->Query(
"SELECT * FROM PeriodicEvents");
2657 $LastRunTimes = $this->DB->FetchColumn(
"LastRunAt",
"Signature");
2659 # for each known event 2661 foreach ($this->KnownPeriodicEvents as $Signature => $Info)
2663 # if last run time for event is available 2664 if (array_key_exists($Signature, $LastRunTimes))
2666 # calculate next run time for event 2667 $LastRun = strtotime($LastRunTimes[$Signature]);
2668 $NextRun = $LastRun + $this->EventPeriods[$Info[
"Period"]];
2669 if ($Info[
"Period"] ==
"EVENT_PERIODIC") { $LastRun = FALSE; }
2673 # set info to indicate run times are not known 2678 # add event info to list 2679 $Events[$Signature] = $Info;
2680 $Events[$Signature][
"LastRun"] = $LastRun;
2681 $Events[$Signature][
"NextRun"] = $NextRun;
2682 $Events[$Signature][
"Parameters"] = NULL;
2685 # return list of known events to caller 2695 public static function RunPeriodicEvent($EventName, $Callback, $Parameters)
2698 if (!isset($DB)) { $DB =
new Database(); }
2701 $ReturnVal = call_user_func_array($Callback, $Parameters);
2703 # if event is already in database 2704 $Signature = self::GetCallbackSignature($Callback);
2705 if ($DB->Query(
"SELECT COUNT(*) AS EventCount FROM PeriodicEvents" 2706 .
" WHERE Signature = '".addslashes($Signature).
"'",
"EventCount"))
2708 # update last run time for event 2709 $DB->Query(
"UPDATE PeriodicEvents SET LastRunAt = " 2710 .(($EventName ==
"EVENT_PERIODIC")
2711 ?
"'".date(
"Y-m-d H:i:s", time() + ($ReturnVal * 60)).
"'" 2713 .
" WHERE Signature = '".addslashes($Signature).
"'");
2717 # add last run time for event to database 2718 $DB->Query(
"INSERT INTO PeriodicEvents (Signature, LastRunAt) VALUES " 2719 .
"('".addslashes($Signature).
"', " 2720 .(($EventName ==
"EVENT_PERIODIC")
2721 ?
"'".date(
"Y-m-d H:i:s", time() + ($ReturnVal * 60)).
"'" 2729 # ---- Task Management --------------------------------------------------- 2734 const PRIORITY_HIGH = 1;
2736 const PRIORITY_MEDIUM = 2;
2738 const PRIORITY_LOW = 3;
2740 const PRIORITY_BACKGROUND = 4;
2754 public function QueueTask($Callback, $Parameters = NULL,
2755 $Priority = self::PRIORITY_LOW, $Description =
"")
2757 # pack task info and write to database 2758 if ($Parameters === NULL) { $Parameters = array(); }
2759 $this->DB->Query(
"INSERT INTO TaskQueue" 2760 .
" (Callback, Parameters, Priority, Description)" 2761 .
" VALUES ('".addslashes(serialize($Callback)).
"', '" 2762 .addslashes(serialize($Parameters)).
"', ".intval($Priority).
", '" 2763 .addslashes($Description).
"')");
2783 public function QueueUniqueTask($Callback, $Parameters = NULL,
2784 $Priority = self::PRIORITY_LOW, $Description =
"")
2786 if ($this->TaskIsInQueue($Callback, $Parameters))
2788 $QueryResult = $this->DB->Query(
"SELECT TaskId,Priority FROM TaskQueue" 2789 .
" WHERE Callback = '".addslashes(serialize($Callback)).
"'" 2790 .($Parameters ?
" AND Parameters = '" 2791 .addslashes(serialize($Parameters)).
"'" :
""));
2792 if ($QueryResult !== FALSE)
2794 $Record = $this->DB->FetchRow();
2795 if ($Record[
"Priority"] > $Priority)
2797 $this->DB->Query(
"UPDATE TaskQueue" 2798 .
" SET Priority = ".intval($Priority)
2799 .
" WHERE TaskId = ".intval($Record[
"TaskId"]));
2806 $this->QueueTask($Callback, $Parameters, $Priority, $Description);
2820 public function TaskIsInQueue($Callback, $Parameters = NULL)
2822 $QueuedCount = $this->DB->Query(
2823 "SELECT COUNT(*) AS FoundCount FROM TaskQueue" 2824 .
" WHERE Callback = '".addslashes(serialize($Callback)).
"'" 2825 .($Parameters ?
" AND Parameters = '" 2826 .addslashes(serialize($Parameters)).
"'" :
""),
2828 $RunningCount = $this->DB->Query(
2829 "SELECT COUNT(*) AS FoundCount FROM RunningTasks" 2830 .
" WHERE Callback = '".addslashes(serialize($Callback)).
"'" 2831 .($Parameters ?
" AND Parameters = '" 2832 .addslashes(serialize($Parameters)).
"'" :
""),
2834 $FoundCount = $QueuedCount + $RunningCount;
2835 return ($FoundCount ? TRUE : FALSE);
2843 public function GetTaskQueueSize($Priority = NULL)
2845 return $this->GetQueuedTaskCount(NULL, NULL, $Priority);
2855 public function GetQueuedTaskList($Count = 100, $Offset = 0)
2857 return $this->GetTaskList(
"SELECT * FROM TaskQueue" 2858 .
" ORDER BY Priority, TaskId ", $Count, $Offset);
2874 public function GetQueuedTaskCount($Callback = NULL,
2875 $Parameters = NULL, $Priority = NULL, $Description = NULL)
2877 $Query =
"SELECT COUNT(*) AS TaskCount FROM TaskQueue";
2879 if ($Callback !== NULL)
2881 $Query .= $Sep.
" Callback = '".addslashes(serialize($Callback)).
"'";
2884 if ($Parameters !== NULL)
2886 $Query .= $Sep.
" Parameters = '".addslashes(serialize($Parameters)).
"'";
2889 if ($Priority !== NULL)
2891 $Query .= $Sep.
" Priority = ".intval($Priority);
2894 if ($Description !== NULL)
2896 $Query .= $Sep.
" Description = '".addslashes($Description).
"'";
2898 return $this->DB->Query($Query,
"TaskCount");
2908 public function GetRunningTaskList($Count = 100, $Offset = 0)
2910 return $this->GetTaskList(
"SELECT * FROM RunningTasks" 2911 .
" WHERE StartedAt >= '".date(
"Y-m-d H:i:s",
2912 (time() - $this->MaxExecutionTime())).
"'" 2913 .
" ORDER BY StartedAt", $Count, $Offset);
2923 public function GetOrphanedTaskList($Count = 100, $Offset = 0)
2925 return $this->GetTaskList(
"SELECT * FROM RunningTasks" 2926 .
" WHERE StartedAt < '".date(
"Y-m-d H:i:s",
2927 (time() - $this->MaxExecutionTime())).
"'" 2928 .
" ORDER BY StartedAt", $Count, $Offset);
2935 public function GetOrphanedTaskCount()
2937 return $this->DB->Query(
"SELECT COUNT(*) AS Count FROM RunningTasks" 2938 .
" WHERE StartedAt < '".date(
"Y-m-d H:i:s",
2939 (time() - $this->MaxExecutionTime())).
"'",
2948 public function ReQueueOrphanedTask($TaskId, $NewPriority = NULL)
2950 $this->DB->Query(
"LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
2951 $this->DB->Query(
"INSERT INTO TaskQueue" 2952 .
" (Callback,Parameters,Priority,Description) " 2953 .
"SELECT Callback, Parameters, Priority, Description" 2954 .
" FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2955 if ($NewPriority !== NULL)
2957 $NewTaskId = $this->DB->LastInsertId();
2958 $this->DB->Query(
"UPDATE TaskQueue SET Priority = " 2959 .intval($NewPriority)
2960 .
" WHERE TaskId = ".intval($NewTaskId));
2962 $this->DB->Query(
"DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2963 $this->DB->Query(
"UNLOCK TABLES");
2972 public function RequeueCurrentTask($NewValue = TRUE)
2974 $this->RequeueCurrentTask = $NewValue;
2982 public function DeleteTask($TaskId)
2984 $this->DB->Query(
"DELETE FROM TaskQueue WHERE TaskId = ".intval($TaskId));
2985 $TasksRemoved = $this->DB->NumRowsAffected();
2986 $this->DB->Query(
"DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2987 $TasksRemoved += $this->DB->NumRowsAffected();
2988 return $TasksRemoved;
2998 public function GetTask($TaskId)
3000 # assume task will not be found 3003 # look for task in task queue 3004 $this->DB->Query(
"SELECT * FROM TaskQueue WHERE TaskId = ".intval($TaskId));
3006 # if task was not found in queue 3007 if (!$this->DB->NumRowsSelected())
3009 # look for task in running task list 3010 $this->DB->Query(
"SELECT * FROM RunningTasks WHERE TaskId = " 3015 if ($this->DB->NumRowsSelected())
3017 # if task was periodic 3018 $Row = $this->DB->FetchRow();
3019 if ($Row[
"Callback"] ==
3020 serialize(array(
"ApplicationFramework",
"RunPeriodicEvent")))
3022 # unpack periodic task callback 3023 $WrappedCallback = unserialize($Row[
"Parameters"]);
3024 $Task[
"Callback"] = $WrappedCallback[1];
3025 $Task[
"Parameters"] = $WrappedCallback[2];
3029 # unpack task callback and parameters 3030 $Task[
"Callback"] = unserialize($Row[
"Callback"]);
3031 $Task[
"Parameters"] = unserialize($Row[
"Parameters"]);
3035 # return task to caller 3046 public function TaskExecutionEnabled($NewValue =
DB_NOVALUE)
3048 return $this->UpdateSetting(
"TaskExecutionEnabled", $NewValue);
3056 public function MaxTasks($NewValue =
DB_NOVALUE)
3058 return $this->UpdateSetting(
"MaxTasksRunning", $NewValue);
3068 public static function GetTaskCallbackSynopsis($TaskInfo)
3070 # if task callback is function use function name 3071 $Callback = $TaskInfo[
"Callback"];
3073 if (!is_array($Callback))
3079 # if task callback is object 3080 if (is_object($Callback[0]))
3082 # if task callback is encapsulated ask encapsulation for name 3083 if (method_exists($Callback[0],
"GetCallbackAsText"))
3085 $Name = $Callback[0]->GetCallbackAsText();
3087 # else assemble name from object 3090 $Name = get_class($Callback[0]) .
"::" . $Callback[1];
3093 # else assemble name from supplied info 3096 $Name= $Callback[0] .
"::" . $Callback[1];
3100 # if parameter array was supplied 3101 $Parameters = $TaskInfo[
"Parameters"];
3102 $ParameterString =
"";
3103 if (is_array($Parameters))
3105 # assemble parameter string 3107 foreach ($Parameters as $Parameter)
3109 $ParameterString .= $Separator;
3110 if (is_int($Parameter) || is_float($Parameter))
3112 $ParameterString .= $Parameter;
3114 else if (is_string($Parameter))
3116 $ParameterString .=
"\"".htmlspecialchars($Parameter).
"\"";
3118 else if (is_array($Parameter))
3120 $ParameterString .=
"ARRAY";
3122 else if (is_object($Parameter))
3124 $ParameterString .=
"OBJECT";
3126 else if (is_null($Parameter))
3128 $ParameterString .=
"NULL";
3130 else if (is_bool($Parameter))
3132 $ParameterString .= $Parameter ?
"TRUE" :
"FALSE";
3134 else if (is_resource($Parameter))
3136 $ParameterString .= get_resource_type($Parameter);
3140 $ParameterString .=
"????";
3146 # assemble name and parameters and return result to caller 3147 return $Name.
"(".$ParameterString.
")";
3154 public function IsRunningInBackground()
3156 return $this->RunningInBackground;
3164 public function GetCurrentBackgroundPriority()
3166 return isset($this->RunningTask)
3167 ? $this->RunningTask[
"Priority"] : NULL;
3178 public function GetNextHigherBackgroundPriority($Priority = NULL)
3180 if ($Priority === NULL)
3182 $Priority = $this->GetCurrentBackgroundPriority();
3183 if ($Priority === NULL)
3188 return ($Priority > self::PRIORITY_HIGH)
3189 ? ($Priority - 1) : self::PRIORITY_HIGH;
3200 public function GetNextLowerBackgroundPriority($Priority = NULL)
3202 if ($Priority === NULL)
3204 $Priority = $this->GetCurrentBackgroundPriority();
3205 if ($Priority === NULL)
3210 return ($Priority < self::PRIORITY_BACKGROUND)
3211 ? ($Priority + 1) : self::PRIORITY_BACKGROUND;
3217 # ---- Clean URL Support ------------------------------------------------- 3247 public function AddCleanUrl($Pattern, $Page, $GetVars = NULL, $Template = NULL)
3249 # save clean URL mapping parameters 3250 $this->CleanUrlMappings[] = array(
3251 "Pattern" => $Pattern,
3253 "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);
3484 public function GetCleanUrlList()
3486 return $this->CleanUrlMappings;
3501 public function AddPrefixForAlternateDomain($Domain, $Prefix)
3503 $this->AlternateDomainPrefixes[$Domain] = $Prefix;
3511 public function GetAlternateDomains()
3513 return array_keys($this->AlternateDomainPrefixes);
3522 public function GetPrefixForAlternateDomain($Domain)
3524 return isset($this->AlternateDomainPrefixes[$Domain]) ?
3525 $this->AlternateDomainPrefixes[$Domain] : NULL;
3530 # ---- Server Environment ------------------------------------------------ 3539 public static function SessionLifetime($NewValue = NULL)
3541 if ($NewValue !== NULL)
3543 self::$SessionLifetime = $NewValue;
3545 return self::$SessionLifetime;
3553 public static function HtaccessSupport()
3555 return isset($_SERVER[
"HTACCESS_SUPPORT"])
3556 || isset($_SERVER[
"REDIRECT_HTACCESS_SUPPORT"]);
3565 public static function UrlFingerprintingRewriteSupport()
3567 return isset($_SERVER[
"URL_FINGERPRINTING_SUPPORT"])
3568 || isset($_SERVER[
"REDIRECT_URL_FINGERPRINTING_SUPPORT"]);
3577 public static function ScssRewriteSupport()
3579 return isset($_SERVER[
"SCSS_REWRITE_SUPPORT"])
3580 || isset($_SERVER[
"REDIRECT_SCSS_REWRITE_SUPPORT"]);
3589 public static function JsMinRewriteSupport()
3591 return isset($_SERVER[
"JSMIN_REWRITE_SUPPORT"])
3592 || isset($_SERVER[
"REDIRECT_JSMIN_REWRITE_SUPPORT"]);
3602 public static function RootUrl()
3604 # return override value if one is set 3605 if (self::$RootUrlOverride !== NULL)
3607 return self::$RootUrlOverride;
3610 # determine scheme name 3611 $Protocol = (isset($_SERVER[
"HTTPS"]) ?
"https" :
"http");
3613 # if HTTP_HOST is preferred or SERVER_NAME points to localhost 3614 # and HTTP_HOST is set 3615 if ((self::$PreferHttpHost || ($_SERVER[
"SERVER_NAME"] ==
"127.0.0.1"))
3616 && isset($_SERVER[
"HTTP_HOST"]))
3618 # use HTTP_HOST for domain name 3619 $DomainName = $_SERVER[
"HTTP_HOST"];
3623 # use SERVER_NAME for domain name 3624 $DomainName = $_SERVER[
"SERVER_NAME"];
3627 # build URL root and return to caller 3628 return $Protocol.
"://".$DomainName;
3645 public static function RootUrlOverride($NewValue = self::NOVALUE)
3647 if ($NewValue !== self::NOVALUE)
3649 self::$RootUrlOverride = strlen(trim($NewValue)) ? $NewValue : NULL;
3651 return self::$RootUrlOverride;
3663 public static function BaseUrl()
3665 $BaseUrl = self::RootUrl().dirname($_SERVER[
"SCRIPT_NAME"]);
3666 if (substr($BaseUrl, -1) !=
"/") { $BaseUrl .=
"/"; }
3677 public static function FullUrl()
3679 return self::RootUrl().$_SERVER[
"REQUEST_URI"];
3692 public static function PreferHttpHost($NewValue = NULL)
3694 if ($NewValue !== NULL)
3696 self::$PreferHttpHost = ($NewValue ? TRUE : FALSE);
3698 return self::$PreferHttpHost;
3705 public static function BasePath()
3707 $BasePath = dirname($_SERVER[
"SCRIPT_NAME"]);
3709 if (substr($BasePath, -1) !=
"/")
3722 public static function GetScriptUrl()
3724 if (array_key_exists(
"SCRIPT_URL", $_SERVER))
3726 return $_SERVER[
"SCRIPT_URL"];
3728 elseif (array_key_exists(
"REQUEST_URI", $_SERVER))
3730 $Pieces = parse_url($_SERVER[
"REQUEST_URI"]);
3731 return isset($Pieces[
"path"]) ? $Pieces[
"path"] : NULL;
3733 elseif (array_key_exists(
"REDIRECT_URL", $_SERVER))
3735 return $_SERVER[
"REDIRECT_URL"];
3751 public static function WasUrlRewritten($ScriptName=
"index.php")
3753 # needed to get the path of the URL minus the query and fragment pieces 3754 $Components = parse_url(self::GetScriptUrl());
3756 # if parsing was successful and a path is set 3757 if (is_array($Components) && isset($Components[
"path"]))
3759 $BasePath = self::BasePath();
3760 $Path = $Components[
"path"];
3762 # the URL was rewritten if the path isn't the base path, i.e., the 3763 # home page, and the file in the URL isn't the script generating the 3765 if ($BasePath != $Path && basename($Path) != $ScriptName)
3771 # the URL wasn't rewritten 3784 public static function ReachedViaAjax($NewSetting = NULL)
3786 if ($NewSetting !== NULL)
3788 self::$IsAjaxPageLoad = $NewSetting;
3791 if (isset(self::$IsAjaxPageLoad))
3793 return self::$IsAjaxPageLoad;
3795 elseif (isset($_SERVER[
"HTTP_X_REQUESTED_WITH"])
3796 && (strtolower($_SERVER[
"HTTP_X_REQUESTED_WITH"])
3797 ==
"xmlhttprequest"))
3812 public static function GetFreeMemory()
3814 return self::GetPhpMemoryLimit() - memory_get_usage(TRUE);
3822 public static function GetPhpMemoryLimit()
3824 $Str = strtoupper(ini_get(
"memory_limit"));
3825 if (substr($Str, -1) ==
"B") { $Str = substr($Str, 0, strlen($Str) - 1); }
3826 switch (substr($Str, -1))
3829 $MemoryLimit = (int)$Str * 1024;
3833 $MemoryLimit = (int)$Str * 1048576;
3837 $MemoryLimit = (int)$Str * 1073741824;
3841 $MemoryLimit = (int)$Str;
3844 return $MemoryLimit;
3859 public function MaxExecutionTime($NewValue =
DB_NOVALUE, $Persistent = FALSE)
3863 $NewValue = max($NewValue, 5);
3864 ini_set(
"max_execution_time", $NewValue);
3865 set_time_limit($NewValue - $this->GetElapsedExecutionTime());
3866 $this->UpdateSetting(
"MaxExecTime", $NewValue, $Persistent);
3868 return ini_get(
"max_execution_time");
3874 # ---- Utility ----------------------------------------------------------- 3889 public function DownloadFile($FilePath, $FileName = NULL, $MimeType = NULL)
3891 # check that file is readable 3892 if (!is_readable($FilePath))
3897 # if file name was not supplied 3898 if ($FileName === NULL)
3900 # extract file name from path 3901 $FileName = basename($FilePath);
3904 # if MIME type was not supplied 3905 if ($MimeType === NULL)
3907 # attempt to determine MIME type 3908 $FInfoHandle = finfo_open(FILEINFO_MIME);
3911 $FInfoMime = finfo_file($FInfoHandle, $FilePath);
3912 finfo_close($FInfoHandle);
3915 $MimeType = $FInfoMime;
3919 # use default if unable to determine MIME type 3920 if ($MimeType === NULL)
3922 $MimeType =
"application/octet-stream";
3926 # set headers to download file 3927 header(
"Content-Type: ".$MimeType);
3928 header(
"Content-Length: ".filesize($FilePath));
3929 if ($this->CleanUrlRewritePerformed)
3931 header(
'Content-Disposition: attachment; filename="'.$FileName.
'"');
3934 # make sure that apache does not attempt to compress file 3935 apache_setenv(
'no-gzip',
'1');
3937 # send file to user, but unbuffered to avoid memory issues 3938 $this->AddUnbufferedCallback(
function ($File)
3940 $BlockSize = 512000;
3942 $Handle = @fopen($File,
"rb");
3943 if ($Handle === FALSE)
3948 # (close out session, making it read-only, so that session file 3949 # lock is released and others are not potentially hanging 3950 # waiting for it while the download completes) 3951 session_write_close();
3953 while (!feof($Handle))
3955 print fread($Handle, $BlockSize);
3960 }, array($FilePath));
3962 # prevent HTML output that might interfere with download 3963 $this->SuppressHTMLOutput();
3965 # set flag to indicate not to log a slow page load in case client 3966 # connection delays PHP execution because of header 3967 $this->DoNotLogSlowPageLoad = TRUE;
3969 # report no errors found to caller 3985 public function GetLock($LockName, $Wait = TRUE)
3987 # assume we will not get a lock 3990 # clear out any stale locks 3991 static $CleanupHasBeenDone = FALSE;
3992 if (!$CleanupHasBeenDone)
3994 # (margin for clearing stale locks is twice the known 3995 # maximum PHP execution time, because the max time 3996 # techinically does not include external operations 3997 # like database queries) 3999 (time() - ($this->MaxExecutionTime() * 2)));
4000 $this->DB->Query(
"DELETE FROM AF_Locks WHERE" 4001 .
" ObtainedAt < '".$ClearLocksObtainedBefore.
"' AND" 4002 .
" LockName = '".addslashes($LockName).
"'");
4007 # lock database table so nobody else can try to get a lock 4008 $this->DB->Query(
"LOCK TABLES AF_Locks WRITE");
4010 # look for lock with specified name 4011 $FoundCount = $this->DB->Query(
"SELECT COUNT(*) AS FoundCount" 4012 .
" FROM AF_Locks WHERE LockName = '" 4013 .addslashes($LockName).
"'",
"FoundCount");
4014 $LockFound = ($FoundCount > 0) ? TRUE : FALSE;
4019 # unlock database tables 4020 $this->DB->Query(
"UNLOCK TABLES");
4022 # if blocking was requested 4025 # wait to give someone else a chance to release lock 4031 # while lock was found and blocking was requested 4032 }
while ($LockFound && $Wait);
4035 # if lock was not found 4039 $this->DB->Query(
"INSERT INTO AF_Locks (LockName) VALUES ('" 4040 .addslashes($LockName).
"')");
4043 # unlock database tables 4044 $this->DB->Query(
"UNLOCK TABLES");
4047 # report to caller whether lock was obtained 4058 public function ReleaseLock($LockName)
4060 # release any existing locks 4061 $this->DB->Query(
"DELETE FROM AF_Locks WHERE LockName = '" 4062 .addslashes($LockName).
"'");
4064 # report to caller whether existing lock was released 4065 return $this->DB->NumRowsAffected() ? TRUE : FALSE;
4071 # ---- Backward Compatibility -------------------------------------------- 4081 public function FindCommonTemplate($BaseName)
4083 return $this->FindFile(
4084 $this->IncludeDirList, $BaseName, array(
"tpl",
"html"));
4090 # ---- PRIVATE INTERFACE ------------------------------------------------- 4092 private $AdditionalRequiredUIFiles = array();
4093 private $AlternateDomainPrefixes = array();
4094 private $BackgroundTaskMemLeakLogThreshold = 10; # percentage of max mem
4095 private $BackgroundTaskMinFreeMemPercent = 25;
4096 private $BrowserDetectFunc;
4097 private $CacheCurrentPage = TRUE;
4098 private $CleanUrlMappings = array();
4099 private $CleanUrlRewritePerformed = FALSE;
4100 private $ContextFilters = array(
4101 self::CONTEXT_START => TRUE,
4102 self::CONTEXT_PAGE => array(
"H_"),
4103 self::CONTEXT_COMMON => array(
"H_"),
4105 private $CssUrlFingerprintPath;
4107 private $DefaultPage =
"Home";
4108 private $DoNotMinimizeList = array();
4109 private $DoNotLogSlowPageLoad = FALSE;
4110 private $EnvIncludes = array();
4111 private $ExecutionStartTime;
4112 private $FoundUIFiles = array();
4113 private $HtmlCharset =
"UTF-8";
4114 private $InterfaceSettings = array();
4115 private $JSMinimizerJavaScriptPackerAvailable = FALSE;
4116 private $JSMinimizerJShrinkAvailable = TRUE;
4117 private $JumpToPage = NULL;
4118 private $JumpToPageDelay = 0;
4119 private $LogFileName =
"local/logs/site.log";
4120 private $MaxRunningTasksToTrack = 250;
4122 private $OutputModificationCallbackInfo;
4123 private $OutputModificationCallbacks = array();
4124 private $OutputModificationPatterns = array();
4125 private $OutputModificationReplacements = array();
4126 private $PageCacheTags = array();
4128 private $PostProcessingFuncs = array();
4129 private $RequeueCurrentTask;
4130 private $RunningInBackground = FALSE;
4131 private $RunningTask;
4132 private $SavedContext;
4133 private $SaveTemplateLocationCache = FALSE;
4134 private $SessionStorage;
4135 private $SessionGcProbability;
4137 private $SuppressHTML = FALSE;
4138 private $SuppressStdPageStartAndEnd = FALSE;
4139 private $TemplateLocationCache;
4140 private $TemplateLocationCacheInterval = 60; # in minutes
4141 private $TemplateLocationCacheExpiration;
4142 private $UnbufferedCallbacks = array();
4143 private $UrlFingerprintBlacklist = array();
4144 private $UseBaseTag = FALSE;
4146 private static $ActiveUI =
"default";
4147 private static $AppName =
"ScoutAF";
4148 private static $DefaultUI =
"default";
4149 private static $IsAjaxPageLoad;
4150 private static $JSMinCacheDir =
"local/data/caches/JSMin";
4151 private static $ObjectDirectories = array();
4152 private static $ObjectLocationCache;
4153 private static $ObjectLocationCacheInterval = 60;
4154 private static $ObjectLocationCacheExpiration;
4155 private static $PreferHttpHost = FALSE;
4156 private static $RootUrlOverride = NULL;
4157 private static $SaveObjectLocationCache = FALSE;
4158 private static $ScssCacheDir =
"local/data/caches/SCSS";
4159 private static $SessionLifetime = 1440; # in seconds
4161 # offset used to generate page cache tag IDs from numeric tags 4162 const PAGECACHETAGIDOFFSET = 100000;
4164 # minimum expired session garbage collection probability 4165 const MIN_GC_PROBABILITY = 0.01;
4171 private $NoTSR = FALSE;
4173 private $KnownPeriodicEvents = array();
4174 private $PeriodicEvents = array(
4175 "EVENT_HOURLY" => self::EVENTTYPE_DEFAULT,
4176 "EVENT_DAILY" => self::EVENTTYPE_DEFAULT,
4177 "EVENT_WEEKLY" => self::EVENTTYPE_DEFAULT,
4178 "EVENT_MONTHLY" => self::EVENTTYPE_DEFAULT,
4179 "EVENT_PERIODIC" => self::EVENTTYPE_NAMED,
4181 private $EventPeriods = array(
4182 "EVENT_HOURLY" => 3600,
4183 "EVENT_DAILY" => 86400,
4184 "EVENT_WEEKLY" => 604800,
4185 "EVENT_MONTHLY" => 2592000,
4186 "EVENT_PERIODIC" => 0,
4188 private $UIEvents = array(
4189 "EVENT_PAGE_LOAD" => self::EVENTTYPE_DEFAULT,
4190 "EVENT_PHP_FILE_LOAD" => self::EVENTTYPE_CHAIN,
4191 "EVENT_PHP_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
4192 "EVENT_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
4193 "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
4194 "EVENT_PAGE_OUTPUT_FILTER" => self::EVENTTYPE_CHAIN,
4201 private function LoadSettings()
4203 # read settings in from database 4204 $this->DB->Query(
"SELECT * FROM ApplicationFrameworkSettings");
4205 $this->Settings = $this->DB->FetchRow();
4207 # if settings were not previously initialized 4208 if ($this->Settings === FALSE)
4210 # initialize settings in database 4211 $this->DB->Query(
"INSERT INTO ApplicationFrameworkSettings" 4212 .
" (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");
4214 # read new settings in from database 4215 $this->DB->Query(
"SELECT * FROM ApplicationFrameworkSettings");
4216 $this->Settings = $this->DB->FetchRow();
4218 # bail out if reloading new settings failed 4219 if ($this->Settings === FALSE)
4221 throw new Exception(
4222 "Unable to load application framework settings.");
4226 # if base path was not previously set or we appear to have moved 4227 if (!array_key_exists(
"BasePath", $this->Settings)
4228 || (!strlen($this->Settings[
"BasePath"]))
4229 || (!array_key_exists(
"BasePathCheck", $this->Settings))
4230 || (__FILE__ != $this->Settings[
"BasePathCheck"]))
4232 # attempt to extract base path from Apache .htaccess file 4233 if (is_readable(
".htaccess"))
4235 $Lines = file(
".htaccess");
4236 foreach ($Lines as $Line)
4238 if (preg_match(
"/\\s*RewriteBase\\s+/", $Line))
4240 $Pieces = preg_split(
4241 "/\\s+/", $Line, NULL, PREG_SPLIT_NO_EMPTY);
4242 $BasePath = $Pieces[1];
4247 # if base path was found 4248 if (isset($BasePath))
4250 # save base path locally 4251 $this->Settings[
"BasePath"] = $BasePath;
4253 # save base path to database 4254 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings" 4255 .
" SET BasePath = '".addslashes($BasePath).
"'" 4256 .
", BasePathCheck = '".addslashes(__FILE__).
"'");
4260 # retrieve template location cache 4261 $this->TemplateLocationCache = unserialize(
4262 $this->Settings[
"TemplateLocationCache"]);
4263 $this->TemplateLocationCacheInterval =
4264 $this->Settings[
"TemplateLocationCacheInterval"];
4265 $this->TemplateLocationCacheExpiration =
4266 strtotime($this->Settings[
"TemplateLocationCacheExpiration"]);
4268 # if template location cache looks invalid or has expired 4269 $CurrentTime = time();
4270 if (!count($this->TemplateLocationCache)
4271 || ($CurrentTime >= $this->TemplateLocationCacheExpiration))
4273 # clear cache and reset cache expiration 4274 $this->TemplateLocationCache = array();
4275 $this->TemplateLocationCacheExpiration =
4276 $CurrentTime + ($this->TemplateLocationCacheInterval * 60);
4277 $this->SaveTemplateLocationCache = TRUE;
4280 # retrieve object location cache 4281 self::$ObjectLocationCache =
4282 unserialize($this->Settings[
"ObjectLocationCache"]);
4283 self::$ObjectLocationCacheInterval =
4284 $this->Settings[
"ObjectLocationCacheInterval"];
4285 self::$ObjectLocationCacheExpiration =
4286 strtotime($this->Settings[
"ObjectLocationCacheExpiration"]);
4288 # if object location cache looks invalid or has expired 4289 if (!count(self::$ObjectLocationCache)
4290 || ($CurrentTime >= self::$ObjectLocationCacheExpiration))
4292 # clear cache and reset cache expiration 4293 self::$ObjectLocationCache = array();
4294 self::$ObjectLocationCacheExpiration =
4295 $CurrentTime + (self::$ObjectLocationCacheInterval * 60);
4296 self::$SaveObjectLocationCache = TRUE;
4306 private function RewriteCleanUrls($PageName)
4308 # if URL rewriting is supported by the server 4309 if ($this->HtaccessSupport())
4311 # retrieve current URL and remove base path if present 4312 $Url = $this->GetPageLocation();
4314 # for each clean URL mapping 4315 foreach ($this->CleanUrlMappings as $Info)
4317 # if current URL matches clean URL pattern 4318 if (preg_match($Info[
"Pattern"], $Url, $Matches))
4321 $PageName = $Info[
"Page"];
4323 # if $_GET variables specified for clean URL 4324 if ($Info[
"GetVars"] !== NULL)
4326 # for each $_GET variable specified for clean URL 4327 foreach ($Info[
"GetVars"] as $VarName => $VarTemplate)
4329 # start with template for variable value 4330 $Value = $VarTemplate;
4332 # for each subpattern matched in current URL 4333 foreach ($Matches as $Index => $Match)
4335 # if not first (whole) match 4338 # make any substitutions in template 4339 $Value = str_replace(
"$".$Index, $Match, $Value);
4343 # set $_GET variable 4344 $_GET[$VarName] = $Value;
4348 # set flag indicating clean URL mapped 4349 $this->CleanUrlRewritePerformed = TRUE;
4351 # stop looking for a mapping 4357 # return (possibly) updated page name to caller 4373 private function RewriteAlternateDomainUrls($Html)
4375 # if we were loaded via an alternate domain, and we have a 4376 # RootUrlOverride configured to tell us which domain is the 4377 # primary, and if rewriting support is enabled, then we can 4378 # handle URL Rewriting 4379 if ($this->LoadedViaAlternateDomain() &&
4380 self::$RootUrlOverride !== NULL &&
4381 $this->HtaccessSupport())
4383 # pull out the configured prefix for this domain 4384 $VHost = $_SERVER[
"SERVER_NAME"];
4385 $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
4387 # get the URL for the primary domain, including the base path 4388 # (usually the part between the host name and the PHP file name) 4389 $RootUrl = $this->RootUrl().self::BasePath();
4391 # and figure out what protcol we were using 4392 $Protocol = (isset($_SERVER[
"HTTPS"]) ?
"https" :
"http");
4394 # NB: preg_replace iterates through the configured 4395 # search/replacement pairs, such that the second one 4396 # runs after the first and so on 4398 # the first n-1 patterns below convert any relative 4399 # links in the generated HTML to absolute links using 4400 # our primary domain (e.g., for stylesheets, javascript, 4403 # the nth pattern looks for links that live within the 4404 # path subtree specified by our configured prefix on 4405 # our primary domain, then replaces them with equivalent 4406 # links on our secondary domain 4408 # for example, if our primary domain is 4409 # example.com/MySite and our secondary domain is 4410 # things.example.org/MySite with 'things' as the 4411 # configured prefix, then this last pattern will look 4412 # for example.com/MySite/things and replace it with 4413 # things.example.org/MySite 4414 $RelativePathPatterns = array(
4415 "%src=\"(?!http://|https://)%i",
4416 "%src='(?!http://|https://)%i",
4417 "%href=\"(?!http://|https://)%i",
4418 "%href='(?!http://|https://)%i",
4419 "%action=\"(?!http://|https://)%i",
4420 "%action='(?!http://|https://)%i",
4421 "%@import\s+url\(\"(?!http://|https://)%i",
4422 "%@import\s+url\('(?!http://|https://)%i",
4423 "%src:\s+url\(\"(?!http://|https://)%i",
4424 "%src:\s+url\('(?!http://|https://)%i",
4425 "%@import\s+\"(?!http://|https://)%i",
4426 "%@import\s+'(?!http://|https://)%i",
4427 "%".preg_quote($RootUrl.$ThisPrefix.
"/",
"%").
"%",
4429 $RelativePathReplacements = array(
4434 "action=\"".$RootUrl,
4435 "action='".$RootUrl,
4436 "@import url(\"".$RootUrl,
4437 "@import url('".$RootUrl,
4438 "src: url(\"".$RootUrl,
4439 "src: url('".$RootUrl,
4440 "@import \"".$RootUrl,
4441 "@import '".$RootUrl,
4442 $Protocol.
"://".$VHost.self::BasePath(),
4445 $NewHtml = preg_replace(
4446 $RelativePathPatterns,
4447 $RelativePathReplacements,
4450 # check to make sure relative path fixes didn't fail 4451 $Html = $this->CheckOutputModification(
4453 "alternate domain substitutions");
4463 private function LoadedViaAlternateDomain()
4465 return (isset($_SERVER[
"SERVER_NAME"]) &&
4466 isset($this->AlternateDomainPrefixes[$_SERVER[
"SERVER_NAME"]])) ?
4488 private function FindFile($DirectoryList, $BaseName,
4489 $PossibleSuffixes = NULL, $PossiblePrefixes = NULL)
4491 # generate template cache index for this page 4492 $CacheIndex = md5(serialize($DirectoryList))
4493 .self::$DefaultUI.self::$ActiveUI.$BaseName;
4495 # if caching is enabled and we have cached location 4496 if (($this->TemplateLocationCacheInterval > 0)
4497 && array_key_exists($CacheIndex,
4498 $this->TemplateLocationCache))
4500 # use template location from cache 4501 $FoundFileName = $this->TemplateLocationCache[$CacheIndex];
4505 # if suffixes specified and base name does not include suffix 4506 if (count($PossibleSuffixes)
4507 && !preg_match(
"/\.[a-zA-Z0-9]+$/", $BaseName))
4509 # add versions of file names with suffixes to file name list 4510 $FileNames = array();
4511 foreach ($PossibleSuffixes as $Suffix)
4513 $FileNames[] = $BaseName.
".".$Suffix;
4518 # use base name as file name 4519 $FileNames = array($BaseName);
4522 # if prefixes specified 4523 if (count($PossiblePrefixes))
4525 # add versions of file names with prefixes to file name list 4526 $NewFileNames = array();
4527 foreach ($FileNames as $FileName)
4529 foreach ($PossiblePrefixes as $Prefix)
4531 $NewFileNames[] = $Prefix.$FileName;
4534 $FileNames = $NewFileNames;
4537 # expand directory list to include variants 4538 $DirectoryList = $this->ExpandDirectoryList($DirectoryList);
4540 # for each possible location 4541 $FoundFileName = NULL;
4542 foreach ($DirectoryList as $Dir)
4544 # for each possible file name 4545 foreach ($FileNames as $File)
4547 # if template is found at location 4548 if (file_exists($Dir.$File))
4550 # save full template file name and stop looking 4551 $FoundFileName = $Dir.$File;
4557 # save location in cache 4558 $this->TemplateLocationCache[$CacheIndex]
4561 # set flag indicating that cache should be saved 4562 $this->SaveTemplateLocationCache = TRUE;
4565 # return full template file name to caller 4566 return $FoundFileName;
4575 private function ExpandDirectoryList($DirList)
4577 # generate lookup for supplied list 4578 $ExpandedListKey = md5(serialize($DirList)
4579 .self::$DefaultUI.self::$ActiveUI);
4581 # if we already have expanded version of supplied list 4582 if (isset($this->ExpandedDirectoryLists[$ExpandedListKey]))
4584 # return expanded version to caller 4585 return $this->ExpandedDirectoryLists[$ExpandedListKey];
4588 # for each directory in list 4589 $ExpDirList = array();
4590 foreach ($DirList as $Dir)
4592 # if directory includes substitution keyword 4593 if ((strpos($Dir,
"%DEFAULTUI%") !== FALSE)
4594 || (strpos($Dir,
"%ACTIVEUI%") !== FALSE))
4596 # start with empty new list segment 4597 $ExpDirListSegment = array();
4599 # use default values for initial parent 4600 $ParentInterface = array(self::$ActiveUI, self::$DefaultUI);
4604 # substitute in for keyword on parent 4605 $CurrDir = str_replace(array(
"%ACTIVEUI%",
"%DEFAULTUI%"),
4606 $ParentInterface, $Dir);
4608 # add local version of parent directory to new list segment 4609 $ExpDirListSegment[] =
"local/".$CurrDir;
4611 # add parent directory to new list segment 4612 $ExpDirListSegment[] = $CurrDir;
4614 # look for new parent interface 4615 $ParentInterface = $this->GetInterfaceSetting(
4616 $CurrDir,
"ParentInterface");
4618 # repeat if parent is available 4619 }
while (strlen($ParentInterface));
4621 # add new list segment to expanded list 4622 $ExpDirList = array_merge($ExpDirList, $ExpDirListSegment);
4626 # add local version of directory to expanded list 4627 $ExpDirList[] =
"local/".$Dir;
4629 # add directory to expanded list 4630 $ExpDirList[] = $Dir;
4634 # return expanded version to caller 4635 $this->ExpandedDirectoryLists[$ExpandedListKey] = $ExpDirList;
4636 return $this->ExpandedDirectoryLists[$ExpandedListKey];
4647 private function GetInterfaceSetting($InterfaceDir, $SettingName = NULL)
4649 # extract canonical interface name and base interface directory 4650 preg_match(
"%(.*interface/)([^/]+)%", $InterfaceDir, $Matches);
4651 $InterfaceDir = (count($Matches) > 2)
4652 ? $Matches[1].$Matches[2] : $InterfaceDir;
4653 $InterfaceName = (count($Matches) > 2)
4654 ? $Matches[2] :
"UNKNOWN";
4656 # if we do not have settings for interface 4657 if (!isset($this->InterfaceSettings[$InterfaceName]))
4659 # load default values for settings 4660 $this->InterfaceSettings[$InterfaceName] = array(
4665 # if directory takes precedence over existing settings source 4666 # ("takes precendence" == is more local == longer directory length) 4667 if (strlen($InterfaceDir)
4668 > strlen($this->InterfaceSettings[$InterfaceName][
"Source"]))
4670 # if settings file exists in directory 4671 $SettingsFile = $InterfaceDir.
"/interface.ini";
4672 if (is_readable($SettingsFile))
4674 # read in values from file 4675 $NewSettings = parse_ini_file($SettingsFile);
4677 # merge in values with existing settings 4678 $this->InterfaceSettings[$InterfaceName] = array_merge(
4679 $this->InterfaceSettings[$InterfaceName], $NewSettings);
4681 # save new source of settings 4682 $this->InterfaceSettings[$InterfaceName][
"Source"] = $InterfaceDir;
4686 # return interface settings to caller 4688 ? (isset($this->InterfaceSettings[$InterfaceName][$SettingName])
4689 ? $this->InterfaceSettings[$InterfaceName][$SettingName]
4691 : $this->InterfaceSettings[$InterfaceName];
4702 private function CompileScssFile($SrcFile)
4704 # build path to CSS file 4705 $DstFile = self::$ScssCacheDir.
"/".dirname($SrcFile)
4706 .
"/".basename($SrcFile);
4707 $DstFile = substr_replace($DstFile,
"css", -4);
4709 # if SCSS file is newer than CSS file 4710 if (!file_exists($DstFile)
4711 || (filemtime($SrcFile) > filemtime($DstFile)))
4713 # attempt to create CSS cache subdirectory if not present 4714 if (!is_dir(dirname($DstFile)))
4716 @mkdir(dirname($DstFile), 0777, TRUE);
4719 # if CSS cache directory and CSS file path appear writable 4720 static $CacheDirIsWritable;
4721 if (!isset($CacheDirIsWritable))
4722 { $CacheDirIsWritable = is_writable(self::$ScssCacheDir); }
4723 if (is_writable($DstFile)
4724 || (!file_exists($DstFile) && $CacheDirIsWritable))
4726 # load SCSS and compile to CSS 4727 $ScssCode = file_get_contents($SrcFile);
4728 $ScssCompiler =
new scssc();
4729 $ScssCompiler->setFormatter($this->GenerateCompactCss()
4730 ?
"scss_formatter_compressed" :
"scss_formatter");
4733 $CssCode = $ScssCompiler->compile($ScssCode);
4735 # add fingerprinting for URLs in CSS 4736 $this->CssUrlFingerprintPath = dirname($SrcFile);
4737 $CssCode = preg_replace_callback(
4738 "/url\((['\"]*)(.+)\.([a-z]+)(['\"]*)\)/",
4739 array($this,
"CssUrlFingerprintInsertion"),
4742 # strip out comments from CSS (if requested) 4743 if ($this->GenerateCompactCss())
4745 $CssCode = preg_replace(
'!/\*[^*]*\*+([^/][^*]*\*+)*/!',
4749 # write out CSS file 4750 file_put_contents($DstFile, $CssCode);
4752 catch (Exception $Ex)
4754 $this->LogError(self::LOGLVL_ERROR,
4755 "Error compiling SCSS file ".$SrcFile.
": " 4756 .$Ex->getMessage());
4762 # log error and set CSS file path to indicate failure 4763 $this->LogError(self::LOGLVL_ERROR,
4764 "Unable to write out CSS file (compiled from SCSS) to " 4770 # return CSS file path to caller 4781 private function MinimizeJavascriptFile($SrcFile)
4783 # bail out if file is on exclusion list 4784 foreach ($this->DoNotMinimizeList as $DNMFile)
4786 if (($SrcFile == $DNMFile) || (basename($SrcFile) == $DNMFile))
4792 # build path to minimized file 4793 $DstFile = self::$JSMinCacheDir.
"/".dirname($SrcFile)
4794 .
"/".basename($SrcFile);
4795 $DstFile = substr_replace($DstFile,
".min", -3, 0);
4797 # if original file is newer than minimized file 4798 if (!file_exists($DstFile)
4799 || (filemtime($SrcFile) > filemtime($DstFile)))
4801 # attempt to create cache subdirectory if not present 4802 if (!is_dir(dirname($DstFile)))
4804 @mkdir(dirname($DstFile), 0777, TRUE);
4807 # if cache directory and minimized file path appear writable 4808 static $CacheDirIsWritable;
4809 if (!isset($CacheDirIsWritable))
4810 { $CacheDirIsWritable = is_writable(self::$JSMinCacheDir); }
4811 if (is_writable($DstFile)
4812 || (!file_exists($DstFile) && $CacheDirIsWritable))
4814 # load JavaScript code 4815 $Code = file_get_contents($SrcFile);
4817 # decide which minimizer to use 4818 if ($this->JSMinimizerJavaScriptPackerAvailable
4819 && $this->JSMinimizerJShrinkAvailable)
4821 $Minimizer = (strlen($Code) < 5000)
4822 ?
"JShrink" :
"JavaScriptPacker";
4824 elseif ($this->JSMinimizerJShrinkAvailable)
4826 $Minimizer =
"JShrink";
4830 $Minimizer =
"NONE";
4836 case "JavaScriptMinimizer":
4838 $MinimizedCode = $Packer->pack();
4846 catch (Exception $Exception)
4848 unset($MinimizedCode);
4849 $MinimizeError = $Exception->getMessage();
4854 # if minimization succeeded 4855 if (isset($MinimizedCode))
4857 # write out minimized file 4858 file_put_contents($DstFile, $MinimizedCode);
4862 # log error and set destination file path to indicate failure 4863 $ErrMsg =
"Unable to minimize JavaScript file ".$SrcFile;
4864 if (isset($MinimizeError))
4866 $ErrMsg .=
" (".$MinimizeError.
")";
4868 $this->LogError(self::LOGLVL_ERROR, $ErrMsg);
4874 # log error and set destination file path to indicate failure 4875 $this->LogError(self::LOGLVL_ERROR,
4876 "Unable to write out minimized JavaScript to file ".$DstFile);
4881 # return CSS file path to caller 4892 private function CssUrlFingerprintInsertion($Matches)
4894 # generate fingerprint string from CSS file modification time 4895 $FileName = realpath($this->CssUrlFingerprintPath.
"/".
4896 $Matches[2].
".".$Matches[3]);
4897 $MTime = filemtime($FileName);
4898 $Fingerprint = sprintf(
"%06X", ($MTime % 0xFFFFFF));
4900 # build URL string with fingerprint and return it to caller 4901 return "url(".$Matches[1].$Matches[2].
".".$Fingerprint
4902 .
".".$Matches[3].$Matches[4].
")";
4912 private function GetRequiredFilesNotYetLoaded($PageContentFile)
4914 # start out assuming no files required 4915 $RequiredFiles = array();
4917 # if page content file supplied 4918 if ($PageContentFile)
4920 # if file containing list of required files is available 4921 $Path = dirname($PageContentFile);
4922 $RequireListFile = $Path.
"/REQUIRES";
4923 if (file_exists($RequireListFile))
4925 # read in list of required files 4926 $RequestedFiles = file($RequireListFile);
4928 # for each line in required file list 4929 foreach ($RequestedFiles as $Line)
4931 # if line is not a comment 4932 $Line = trim($Line);
4933 if (!preg_match(
"/^#/", $Line))
4935 # if file has not already been loaded 4936 if (!in_array($Line, $this->FoundUIFiles))
4938 # add to list of required files 4939 $RequiredFiles[$Line] = self::ORDER_MIDDLE;
4946 # add in additional required files if any 4947 if (count($this->AdditionalRequiredUIFiles))
4949 # make sure there are no duplicates 4950 $AdditionalRequiredUIFiles = array_unique(
4951 $this->AdditionalRequiredUIFiles);
4953 $RequiredFiles = array_merge(
4954 $RequiredFiles, $this->AdditionalRequiredUIFiles);
4957 # return list of required files to caller 4958 return $RequiredFiles;
4969 private function SubBrowserIntoFileNames($FileNames)
4971 # if a browser detection function has been made available 4972 $UpdatedFileNames = array();
4973 if (is_callable($this->BrowserDetectFunc))
4975 # call function to get browser list 4976 $Browsers = call_user_func($this->BrowserDetectFunc);
4978 # for each required file 4979 foreach ($FileNames as $FileName => $Value)
4981 # if file name includes browser keyword 4982 if (preg_match(
"/%BROWSER%/", $FileName))
4985 foreach ($Browsers as $Browser)
4987 # substitute in browser name and add to new file list 4988 $NewFileName = preg_replace(
4989 "/%BROWSER%/", $Browser, $FileName);
4990 $UpdatedFileNames[$NewFileName] = $Value;
4995 # add to new file list 4996 $UpdatedFileNames[$FileName] = $Value;
5002 # filter out any files with browser keyword in their name 5003 foreach ($FileNames as $FileName => $Value)
5005 if (!preg_match(
"/%BROWSER%/", $FileName))
5007 $UpdatedFileNames[$FileName] = $Value;
5012 return $UpdatedFileNames;
5020 private function AddMetaTagsToPageOutput($PageOutput)
5022 if (isset($this->MetaTags))
5024 $MetaTagSection =
"";
5025 foreach ($this->MetaTags as $MetaTagAttribs)
5027 $MetaTagSection .=
"<meta";
5028 foreach ($MetaTagAttribs as
5029 $MetaTagAttribName => $MetaTagAttribValue)
5031 $MetaTagSection .=
" ".$MetaTagAttribName.
"=\"" 5032 .htmlspecialchars(trim($MetaTagAttribValue)).
"\"";
5034 $MetaTagSection .=
" />\n";
5037 if ($this->SuppressStdPageStartAndEnd)
5039 $PageOutput = $MetaTagSection.$PageOutput;
5043 $PageOutput = preg_replace(
"#<head>#i",
5044 "<head>\n".$MetaTagSection, $PageOutput, 1);
5058 private function AddFileTagsToPageOutput($PageOutput, $Files)
5060 # substitute browser name into names of required files as appropriate 5061 $Files = $this->SubBrowserIntoFileNames($Files);
5063 # initialize content sections 5065 self::ORDER_FIRST =>
"",
5066 self::ORDER_MIDDLE =>
"",
5067 self::ORDER_LAST =>
"",
5070 self::ORDER_FIRST =>
"",
5071 self::ORDER_MIDDLE =>
"",
5072 self::ORDER_LAST =>
"",
5075 # for each required file 5076 foreach ($Files as $File => $Order)
5078 # locate specific file to use 5079 $FilePath = $this->GUIFile($File);
5084 # generate tag for file 5085 $Tag = $this->GetUIFileLoadingTag($FilePath);
5087 # add file to HTML output based on file type 5088 $FileType = $this->GetFileType($FilePath);
5092 $HeadContent[$Order] .= $Tag.
"\n";
5095 case self::FT_JAVASCRIPT:
5096 $BodyContent[$Order] .= $Tag.
"\n";
5102 # add content to head 5103 $Replacement = $HeadContent[self::ORDER_MIDDLE]
5104 .$HeadContent[self::ORDER_LAST];
5105 $UpdatedPageOutput = str_ireplace(
"</head>",
5106 $Replacement.
"</head>",
5107 $PageOutput, $ReplacementCount);
5108 # (if no </head> tag was found, just prepend tags to page content) 5109 if ($ReplacementCount == 0)
5111 $PageOutput = $Replacement.$PageOutput;
5113 # (else if multiple </head> tags found, only prepend tags to the first) 5114 elseif ($ReplacementCount > 1)
5116 $PageOutput = preg_replace(
"#</head>#i",
5117 $Replacement.
"</head>",
5122 $PageOutput = $UpdatedPageOutput;
5124 $Replacement = $HeadContent[self::ORDER_FIRST];
5125 $UpdatedPageOutput = str_ireplace(
"<head>",
5126 "<head>\n".$Replacement,
5127 $PageOutput, $ReplacementCount);
5128 # (if no <head> tag was found, just prepend tags to page content) 5129 if ($ReplacementCount == 0)
5131 $PageOutput = $Replacement.$PageOutput;
5133 # (else if multiple <head> tags found, only append tags to the first) 5134 elseif ($ReplacementCount > 1)
5136 $PageOutput = preg_replace(
"#<head>#i",
5137 "<head>\n".$Replacement,
5142 $PageOutput = $UpdatedPageOutput;
5145 # add content to body 5146 $Replacement = $BodyContent[self::ORDER_FIRST];
5147 $PageOutput = preg_replace(
"#<body([^>]*)>#i",
5148 "<body\\1>\n".$Replacement,
5149 $PageOutput, 1, $ReplacementCount);
5150 # (if no <body> tag was found, just append tags to page content) 5151 if ($ReplacementCount == 0)
5153 $PageOutput = $PageOutput.$Replacement;
5155 $Replacement = $BodyContent[self::ORDER_MIDDLE]
5156 .$BodyContent[self::ORDER_LAST];
5157 $UpdatedPageOutput = str_ireplace(
"</body>",
5158 $Replacement.
"\n</body>",
5159 $PageOutput, $ReplacementCount);
5160 # (if no </body> tag was found, just append tags to page content) 5161 if ($ReplacementCount == 0)
5163 $PageOutput = $PageOutput.$Replacement;
5165 # (else if multiple </body> tags found, only prepend tag to the first) 5166 elseif ($ReplacementCount > 1)
5168 $PageOutput = preg_replace(
"#</body>#i",
5169 $Replacement.
"\n</body>",
5174 $PageOutput = $UpdatedPageOutput;
5190 private function GetUIFileLoadingTag($FileName, $AdditionalAttributes = NULL)
5192 # pad additional attributes if supplied 5193 $AddAttribs = $AdditionalAttributes ?
" ".$AdditionalAttributes :
"";
5195 # retrieve type of UI file 5196 $FileType = $this->GetFileType($FileName);
5198 # construct tag based on file type 5202 $Tag =
" <link rel=\"stylesheet\" type=\"text/css\"" 5203 .
" media=\"all\" href=\"".$FileName.
"\"" 5204 .$AddAttribs.
" />\n";
5207 case self::FT_JAVASCRIPT:
5208 $Tag =
" <script type=\"text/javascript\"" 5209 .
" src=\"".$FileName.
"\"" 5210 .$AddAttribs.
"></script>\n";
5213 case self::FT_IMAGE:
5214 $Tag =
"<img src=\"".$FileName.
"\"".$AddAttribs.
">";
5222 # return constructed tag to caller 5230 private function AutoloadObjects($ClassName)
5232 # if caching is not turned off 5233 # and we have a cached location for class 5234 # and file at cached location is readable 5235 if ((self::$ObjectLocationCacheInterval > 0)
5236 && array_key_exists($ClassName,
5237 self::$ObjectLocationCache)
5238 && is_readable(self::$ObjectLocationCache[$ClassName]))
5240 # use object location from cache 5241 require_once(self::$ObjectLocationCache[$ClassName]);
5245 # convert any namespace separators in class name 5246 $ClassName = str_replace(
"\\",
"-", $ClassName);
5248 # for each possible object file directory 5250 foreach (self::$ObjectDirectories as $Location => $Info)
5252 # make any needed replacements in directory path 5253 $Location = str_replace(array(
"%ACTIVEUI%",
"%DEFAULTUI%"),
5254 array(self::$ActiveUI, self::$DefaultUI), $Location);
5256 # if directory looks valid 5257 if (is_dir($Location))
5259 # build class file name 5260 $NewClassName = ($Info[
"ClassPattern"] && $Info[
"ClassReplacement"])
5261 ? preg_replace($Info[
"ClassPattern"],
5262 $Info[
"ClassReplacement"], $ClassName)
5265 # read in directory contents if not already retrieved 5266 if (!isset($FileLists[$Location]))
5268 $FileLists[$Location] = self::ReadDirectoryTree(
5269 $Location,
'/^.+\.php$/i');
5272 # for each file in target directory 5273 $FileNames = $FileLists[$Location];
5274 $TargetName = strtolower($Info[
"Prefix"].$NewClassName.
".php");
5275 foreach ($FileNames as $FileName)
5277 # if file matches our target object file name 5278 if (strtolower($FileName) == $TargetName)
5280 # include object file 5281 require_once($Location.$FileName);
5283 # save location to cache 5284 self::$ObjectLocationCache[$ClassName]
5285 = $Location.$FileName;
5287 # set flag indicating that cache should be saved 5288 self::$SaveObjectLocationCache = TRUE;
5306 private static function ReadDirectoryTree($Directory, $Pattern)
5308 $CurrentDir = getcwd();
5310 $DirIter =
new RecursiveDirectoryIterator(
".");
5311 $IterIter =
new RecursiveIteratorIterator($DirIter);
5312 $RegexResults =
new RegexIterator($IterIter, $Pattern,
5313 RecursiveRegexIterator::GET_MATCH);
5314 $FileList = array();
5315 foreach ($RegexResults as $Result)
5317 $FileList[] = substr($Result[0], 2);
5327 private function LoadUIFunctions()
5330 "local/interface/%ACTIVEUI%/include",
5331 "interface/%ACTIVEUI%/include",
5332 "local/interface/%DEFAULTUI%/include",
5333 "interface/%DEFAULTUI%/include",
5335 foreach ($Dirs as $Dir)
5337 $Dir = str_replace(array(
"%ACTIVEUI%",
"%DEFAULTUI%"),
5338 array(self::$ActiveUI, self::$DefaultUI), $Dir);
5341 $FileNames = scandir($Dir);
5342 foreach ($FileNames as $FileName)
5344 if (preg_match(
"/^F-([A-Za-z0-9_]+)\.php/",
5345 $FileName, $Matches)
5346 || preg_match(
"/^F-([A-Za-z0-9_]+)\.html/",
5347 $FileName, $Matches))
5349 if (!function_exists($Matches[1]))
5351 include_once($Dir.
"/".$FileName);
5364 private function ProcessPeriodicEvent($EventName, $Callback)
5366 # retrieve last execution time for event if available 5367 $Signature = self::GetCallbackSignature($Callback);
5368 $LastRun = $this->DB->Query(
"SELECT LastRunAt FROM PeriodicEvents" 5369 .
" WHERE Signature = '".addslashes($Signature).
"'",
"LastRunAt");
5371 # determine whether enough time has passed for event to execute 5372 $ShouldExecute = (($LastRun === NULL)
5373 || (time() > (strtotime($LastRun) + $this->EventPeriods[$EventName])))
5376 # if event should run 5379 # add event to task queue 5380 $WrapperCallback = array(
"ApplicationFramework",
"RunPeriodicEvent");
5381 $WrapperParameters = array(
5382 $EventName, $Callback, array(
"LastRunAt" => $LastRun));
5383 $this->QueueUniqueTask($WrapperCallback, $WrapperParameters);
5386 # add event to list of periodic events 5387 $this->KnownPeriodicEvents[$Signature] = array(
5388 "Period" => $EventName,
5389 "Callback" => $Callback,
5390 "Queued" => $ShouldExecute);
5398 private static function GetCallbackSignature($Callback)
5400 return !is_array($Callback) ? $Callback
5401 : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
5409 private function PrepForTSR()
5411 # if HTML has been output and it's time to launch another task 5412 # (only TSR if HTML has been output because otherwise browsers 5413 # may misbehave after connection is closed) 5414 if ((PHP_SAPI !=
"cli")
5415 && ($this->JumpToPage || !$this->SuppressHTML)
5416 && !$this->LoadedViaAlternateDomain()
5417 && (time() > (strtotime($this->Settings[
"LastTaskRunAt"])
5418 + ($this->MaxExecutionTime()
5419 / $this->Settings[
"MaxTasksRunning"]) + 5))
5420 && $this->GetTaskQueueSize()
5421 && $this->Settings[
"TaskExecutionEnabled"])
5423 # begin buffering output for TSR 5426 # let caller know it is time to launch another task 5431 # let caller know it is not time to launch another task 5440 private function LaunchTSR()
5442 # set headers to close out connection to browser 5445 ignore_user_abort(TRUE);
5446 header(
"Connection: close");
5447 header(
"Content-Length: ".ob_get_length());
5450 # output buffered content 5451 while (ob_get_level()) { ob_end_flush(); }
5454 # write out any outstanding data and end HTTP session 5455 session_write_close();
5457 # set flag indicating that we are now running in background 5458 $this->RunningInBackground = TRUE;
5460 # handle garbage collection for session data 5461 if (isset($this->SessionStorage) &&
5462 (rand()/getrandmax()) <= $this->SessionGcProbability)
5464 # determine when sessions will expire 5465 $ExpiredTime = strtotime(
"-". self::$SessionLifetime.
" seconds");
5467 # iterate over files in the session directory with a DirectoryIterator 5468 # NB: we cannot use scandir() here because it reads the 5469 # entire list of files into memory and may exceed the memory 5470 # limit for directories with very many files 5471 $DI =
new DirectoryIterator($this->SessionStorage);
5472 while ($DI->valid())
5474 if ((strpos($DI->getFilename(),
"sess_") === 0) &&
5476 $DI->getCTime() < $ExpiredTime)
5478 unlink($DI->getPathname());
5485 # if there is still a task in the queue 5486 if ($this->GetTaskQueueSize())
5488 # garbage collect to give as much memory as possible for tasks 5489 if (function_exists(
"gc_collect_cycles")) { gc_collect_cycles(); }
5491 # turn on output buffering to (hopefully) record any crash output 5494 # lock tables and grab last task run time to double check 5495 $this->DB->Query(
"LOCK TABLES ApplicationFrameworkSettings WRITE");
5496 $this->LoadSettings();
5498 # if still time to launch another task 5499 if (time() > (strtotime($this->Settings[
"LastTaskRunAt"])
5500 + ($this->MaxExecutionTime()
5501 / $this->Settings[
"MaxTasksRunning"]) + 5))
5503 # update the "last run" time and release tables 5504 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings" 5505 .
" SET LastTaskRunAt = '".date(
"Y-m-d H:i:s").
"'");
5506 $this->DB->Query(
"UNLOCK TABLES");
5508 # run tasks while there is a task in the queue 5509 # and enough time and memory left 5513 $this->RunNextTask();
5515 # calculate percentage of memory still available 5516 $PercentFreeMem = (self::GetFreeMemory()
5517 / self::GetPhpMemoryLimit()) * 100;
5519 while ($this->GetTaskQueueSize()
5520 && ($this->GetSecondsBeforeTimeout() > 65)
5521 && ($PercentFreeMem > $this->BackgroundTaskMinFreeMemPercent));
5526 $this->DB->Query(
"UNLOCK TABLES");
5540 private function GetTaskList($DBQuery, $Count, $Offset)
5542 $this->DB->Query($DBQuery.
" LIMIT ".intval($Offset).
",".intval($Count));
5544 while ($Row = $this->DB->FetchRow())
5546 $Tasks[$Row[
"TaskId"]] = $Row;
5547 if ($Row[
"Callback"] ==
5548 serialize(array(
"ApplicationFramework",
"RunPeriodicEvent")))
5550 $WrappedCallback = unserialize($Row[
"Parameters"]);
5551 $Tasks[$Row[
"TaskId"]][
"Callback"] = $WrappedCallback[1];
5552 $Tasks[$Row[
"TaskId"]][
"Parameters"] = NULL;
5556 $Tasks[$Row[
"TaskId"]][
"Callback"] = unserialize($Row[
"Callback"]);
5557 $Tasks[$Row[
"TaskId"]][
"Parameters"] = unserialize($Row[
"Parameters"]);
5566 private function RunNextTask()
5568 # lock tables to prevent same task from being run by multiple sessions 5569 $this->DB->Query(
"LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
5571 # look for task at head of queue 5572 $this->DB->Query(
"SELECT * FROM TaskQueue ORDER BY Priority, TaskId LIMIT 1");
5573 $Task = $this->DB->FetchRow();
5575 # if there was a task available 5578 # move task from queue to running tasks list 5579 $this->DB->Query(
"INSERT INTO RunningTasks " 5580 .
"(TaskId,Callback,Parameters,Priority,Description) " 5581 .
"SELECT * FROM TaskQueue WHERE TaskId = " 5582 .intval($Task[
"TaskId"]));
5583 $this->DB->Query(
"DELETE FROM TaskQueue WHERE TaskId = " 5584 .intval($Task[
"TaskId"]));
5586 # release table locks to again allow other sessions to run tasks 5587 $this->DB->Query(
"UNLOCK TABLES");
5589 # unpack stored task info 5590 $Callback = unserialize($Task[
"Callback"]);
5591 $Parameters = unserialize($Task[
"Parameters"]);
5593 # attempt to load task callback if not already available 5594 $this->LoadFunction($Callback);
5596 # clear task requeue flag 5597 $this->RequeueCurrentTask = FALSE;
5599 # save amount of free memory for later comparison 5600 $BeforeFreeMem = self::GetFreeMemory();
5603 $this->RunningTask = $Task;
5606 call_user_func_array($Callback, $Parameters);
5610 call_user_func($Callback);
5612 unset($this->RunningTask);
5614 # log if task leaked significant memory 5615 if (function_exists(
"gc_collect_cycles")) { gc_collect_cycles(); }
5616 $AfterFreeMem = self::GetFreeMemory();
5617 $LeakThreshold = self::GetPhpMemoryLimit()
5618 * ($this->BackgroundTaskMemLeakLogThreshold / 100);
5619 if (($BeforeFreeMem - $AfterFreeMem) > $LeakThreshold)
5621 $this->LogError(self::LOGLVL_DEBUG,
"Task " 5622 .self::GetTaskCallbackSynopsis(
5623 $this->GetTask($Task[
"TaskId"])).
" leaked " 5624 .number_format($BeforeFreeMem - $AfterFreeMem).
" bytes.");
5627 # if task requeue requested 5628 if ($this->RequeueCurrentTask)
5630 # move task from running tasks list to queue 5631 $this->DB->Query(
"LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
5632 $this->DB->Query(
"INSERT INTO TaskQueue" 5633 .
" (Callback,Parameters,Priority,Description)" 5634 .
" SELECT Callback,Parameters,Priority,Description" 5635 .
" FROM RunningTasks WHERE TaskId = " 5636 .intval($Task[
"TaskId"]));
5637 $this->DB->Query(
"DELETE FROM RunningTasks WHERE TaskId = " 5638 .intval($Task[
"TaskId"]));
5639 $this->DB->Query(
"UNLOCK TABLES");
5643 # remove task from running tasks list 5644 $this->DB->Query(
"DELETE FROM RunningTasks" 5645 .
" WHERE TaskId = ".intval($Task[
"TaskId"]));
5648 # prune running tasks list if necessary 5649 $RunningTasksCount = $this->DB->Query(
5650 "SELECT COUNT(*) AS TaskCount FROM RunningTasks",
"TaskCount");
5651 if ($RunningTasksCount > $this->MaxRunningTasksToTrack)
5653 $this->DB->Query(
"DELETE FROM RunningTasks ORDER BY StartedAt" 5654 .
" LIMIT ".($RunningTasksCount - $this->MaxRunningTasksToTrack));
5659 # release table locks to again allow other sessions to run tasks 5660 $this->DB->Query(
"UNLOCK TABLES");
5669 public function OnCrash()
5671 # attempt to remove any memory limits 5672 $FreeMemory = $this->GetFreeMemory();
5673 ini_set(
"memory_limit", -1);
5675 # if there is a background task currently running 5676 if (isset($this->RunningTask))
5678 # add info about current page load 5679 $CrashInfo[
"ElapsedTime"] = $this->GetElapsedExecutionTime();
5680 $CrashInfo[
"FreeMemory"] = $FreeMemory;
5681 $CrashInfo[
"REMOTE_ADDR"] = $_SERVER[
"REMOTE_ADDR"];
5682 $CrashInfo[
"REQUEST_URI"] = $_SERVER[
"REQUEST_URI"];
5683 if (isset($_SERVER[
"REQUEST_TIME"]))
5685 $CrashInfo[
"REQUEST_TIME"] = $_SERVER[
"REQUEST_TIME"];
5687 if (isset($_SERVER[
"REMOTE_HOST"]))
5689 $CrashInfo[
"REMOTE_HOST"] = $_SERVER[
"REMOTE_HOST"];
5692 # add info about error that caused crash (if available) 5693 if (function_exists(
"error_get_last"))
5695 $CrashInfo[
"LastError"] = error_get_last();
5698 # add info about current output buffer contents (if available) 5699 if (ob_get_length() !== FALSE)
5701 $CrashInfo[
"OutputBuffer"] = ob_get_contents();
5704 # if backtrace info is available for the crash 5705 $Backtrace = debug_backtrace();
5706 if (count($Backtrace) > 1)
5708 # discard the current context from the backtrace 5709 array_shift($Backtrace);
5711 # add the backtrace to the crash info 5712 $CrashInfo[
"Backtrace"] = $Backtrace;
5714 # else if saved backtrace info is available 5715 elseif (isset($this->SavedContext))
5717 # add the saved backtrace to the crash info 5718 $CrashInfo[
"Backtrace"] = $this->SavedContext;
5721 # save crash info for currently running task 5723 $DB->Query(
"UPDATE RunningTasks SET CrashInfo = '" 5724 .addslashes(serialize($CrashInfo))
5725 .
"' WHERE TaskId = ".intval($this->RunningTask[
"TaskId"]));
5748 private function AddToDirList($DirList, $Dir, $SearchLast, $SkipSlashCheck)
5750 # convert incoming directory to array of directories (if needed) 5751 $Dirs = is_array($Dir) ? $Dir : array($Dir);
5753 # reverse array so directories are searched in specified order 5754 $Dirs = array_reverse($Dirs);
5756 # for each directory 5757 foreach ($Dirs as $Location)
5759 # make sure directory includes trailing slash 5760 if (!$SkipSlashCheck)
5762 $Location = $Location
5763 .((substr($Location, -1) !=
"/") ?
"/" :
"");
5766 # remove directory from list if already present 5767 if (in_array($Location, $DirList))
5769 $DirList = array_diff(
5770 $DirList, array($Location));
5773 # add directory to list of directories 5776 array_push($DirList, $Location);
5780 array_unshift($DirList, $Location);
5784 # return updated directory list to caller 5794 private function OutputModificationCallbackShell($Matches)
5796 # call previously-stored external function 5797 return call_user_func($this->OutputModificationCallbackInfo[
"Callback"],
5799 $this->OutputModificationCallbackInfo[
"Pattern"],
5800 $this->OutputModificationCallbackInfo[
"Page"],
5801 $this->OutputModificationCallbackInfo[
"SearchPattern"]);
5812 private function CheckOutputModification($Original, $Modified, $ErrorInfo)
5814 # if error was reported by regex engine 5815 if (preg_last_error() !== PREG_NO_ERROR)
5818 $this->LogError(self::LOGLVL_ERROR,
5819 "Error reported by regex engine when modifying output." 5820 .
" (".$ErrorInfo.
")");
5822 # use unmodified version of output 5823 $OutputToUse = $Original;
5825 # else if modification reduced output by more than threshold 5826 elseif ((strlen(trim($Modified)) / strlen(trim($Original)))
5827 < self::OUTPUT_MODIFICATION_THRESHOLD)
5830 $this->LogError(self::LOGLVL_WARNING,
5831 "Content reduced below acceptable threshold while modifying output." 5832 .
" (".$ErrorInfo.
")");
5834 # use unmodified version of output 5835 $OutputToUse = $Original;
5839 # use modified version of output 5840 $OutputToUse = $Modified;
5843 # return output to use to caller 5844 return $OutputToUse;
5848 const OUTPUT_MODIFICATION_THRESHOLD = 0.10;
5859 private function UpdateSetting(
5860 $FieldName, $NewValue =
DB_NOVALUE, $Persistent = TRUE)
5862 static $LocalSettings;
5867 $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5868 "ApplicationFrameworkSettings",
5869 $FieldName, $NewValue, NULL, $this->Settings);
5873 $LocalSettings[$FieldName] = $NewValue;
5876 elseif (!isset($LocalSettings[$FieldName]))
5878 $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5879 "ApplicationFrameworkSettings",
5880 $FieldName, $NewValue, NULL, $this->Settings);
5882 return $LocalSettings[$FieldName];
5894 private static function IncludeFile($_AF_File, $_AF_ContextVars = array())
5897 foreach ($_AF_ContextVars as $_AF_VarName => $_AF_VarValue)
5899 $$_AF_VarName = $_AF_VarValue;
5901 unset($_AF_VarName);
5902 unset($_AF_VarValue);
5903 unset($_AF_ContextVars);
5905 # add variables to context that we assume are always available 5906 $AF = $GLOBALS[
"AF"];
5911 # return updated context 5912 $ContextVars = get_defined_vars();
5913 unset($ContextVars[
"_AF_File"]);
5914 return $ContextVars;
5923 private function FilterContext($Context, $ContextVars)
5925 # clear all variables if no setting for context is available 5926 # or setting is FALSE 5927 if (!isset($this->ContextFilters[$Context])
5928 || ($this->ContextFilters[$Context] == FALSE))
5932 # keep all variables if setting for context is TRUE 5933 elseif ($this->ContextFilters[$Context] == TRUE)
5935 return $ContextVars;
5939 $Prefixes = $this->ContextFilters[$Context];
5940 $FilterFunc =
function($VarName) use ($Prefixes) {
5941 foreach ($Prefixes as $Prefix)
5943 if (substr($VarName, $Prefix) === 0)
5950 return array_filter(
5951 $ContextVars, $FilterFunc, ARRAY_FILTER_USE_KEY);
5956 private $InterfaceDirList = array(
5957 "interface/%ACTIVEUI%/",
5958 "interface/%DEFAULTUI%/",
5964 private $IncludeDirList = array(
5965 "interface/%ACTIVEUI%/include/",
5966 "interface/%ACTIVEUI%/objects/",
5967 "interface/%DEFAULTUI%/include/",
5968 "interface/%DEFAULTUI%/objects/",
5971 private $ImageDirList = array(
5972 "interface/%ACTIVEUI%/images/",
5973 "interface/%DEFAULTUI%/images/",
5976 private $FunctionDirList = array(
5977 "interface/%ACTIVEUI%/include/",
5978 "interface/%DEFAULTUI%/include/",
5982 const NOVALUE =
".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";
5985 # ---- Page Caching (Internal Methods) ----------------------------------- 5992 private function CheckForCachedPage($PageName)
5994 # assume no cached page will be found 5997 # if returning a cached page is allowed 5998 if ($this->CacheCurrentPage)
6000 # get fingerprint for requested page 6001 $PageFingerprint = $this->GetPageFingerprint($PageName);
6003 # look for matching page in cache in database 6004 $this->DB->Query(
"SELECT * FROM AF_CachedPages" 6005 .
" WHERE Fingerprint = '".addslashes($PageFingerprint).
"'");
6007 # if matching page found 6008 if ($this->DB->NumRowsSelected())
6010 # if cached page has expired 6011 $Row = $this->DB->FetchRow();
6012 $ExpirationTime = strtotime(
6013 "-".$this->PageCacheExpirationPeriod().
" seconds");
6014 if (strtotime($Row[
"CachedAt"]) < $ExpirationTime)
6016 # clear expired pages from cache 6017 $ExpirationTimestamp = date(
"Y-m-d H:i:s", $ExpirationTime);
6018 $this->DB->Query(
"DELETE CP, CPTI FROM AF_CachedPages CP," 6019 .
" AF_CachedPageTagInts CPTI" 6020 .
" WHERE CP.CachedAt < '".$ExpirationTimestamp.
"'" 6021 .
" AND CPTI.CacheId = CP.CacheId");
6022 $this->DB->Query(
"DELETE FROM AF_CachedPages " 6023 .
" WHERE CachedAt < '".$ExpirationTimestamp.
"'");
6027 # display cached page and exit 6028 $CachedPage = $Row[
"PageContent"];
6033 # return any cached page found to caller 6042 private function UpdatePageCache($PageName, $PageContent)
6044 # if page caching is enabled and current page should be cached 6045 if ($this->PageCacheEnabled()
6046 && $this->CacheCurrentPage
6047 && ($PageName !=
"404"))
6049 # if page content looks invalid 6050 if (strlen(trim(strip_tags($PageContent))) == 0)
6053 $LogMsg =
"Page not cached because content was empty." 6054 .
" (PAGE: ".$PageName.
", URL: ".$this->FullUrl().
")";
6055 $this->LogError(self::LOGLVL_ERROR, $LogMsg);
6059 # save page to cache 6060 $PageFingerprint = $this->GetPageFingerprint($PageName);
6061 $this->DB->Query(
"INSERT INTO AF_CachedPages" 6062 .
" (Fingerprint, PageContent) VALUES" 6063 .
" ('".$this->DB->EscapeString($PageFingerprint).
"', '" 6064 .$this->DB->EscapeString($PageContent).
"')");
6065 $CacheId = $this->DB->LastInsertId();
6067 # for each page cache tag that was added 6068 foreach ($this->PageCacheTags as $Tag => $Pages)
6070 # if current page is in list for tag 6071 if (in_array(
"CURRENT", $Pages) || in_array($PageName, $Pages))
6074 $TagId = $this->GetPageCacheTagId($Tag);
6076 # mark current page as associated with tag 6077 $this->DB->Query(
"INSERT INTO AF_CachedPageTagInts" 6078 .
" (TagId, CacheId) VALUES " 6079 .
" (".intval($TagId).
", ".intval($CacheId).
")");
6091 private function GetPageCacheTagId($Tag)
6093 # if tag is a non-negative integer 6094 if (is_numeric($Tag) && ($Tag > 0) && (intval($Tag) == $Tag))
6097 $Id = self::PAGECACHETAGIDOFFSET + $Tag;
6101 # look up ID in database 6102 $Id = $this->DB->Query(
"SELECT TagId FROM AF_CachedPageTags" 6103 .
" WHERE Tag = '".addslashes($Tag).
"'",
"TagId");
6105 # if ID was not found 6108 # add tag to database 6109 $this->DB->Query(
"INSERT INTO AF_CachedPageTags" 6110 .
" SET Tag = '".addslashes($Tag).
"'");
6111 $Id = $this->DB->LastInsertId();
6115 # return tag ID to caller 6124 private function GetPageFingerprint($PageName)
6126 # only get the environmental fingerprint once so that it is consistent 6127 # between page construction start and end 6128 static $EnvFingerprint;
6129 if (!isset($EnvFingerprint))
6131 $EnvData = json_encode($_GET).json_encode($_POST);
6133 # if alternate domain support is enabled 6134 if ($this->HtaccessSupport() && self::$RootUrlOverride !== NULL)
6136 # and if we were accessed via an alternate domain 6137 $VHost = $_SERVER[
"SERVER_NAME"];
6138 if (isset($this->AlternateDomainPrefixes[$VHost]))
6140 # then add the alternate domain that was used to our 6146 $EnvFingerprint = md5($EnvData);
6150 # build page fingerprint and return it to caller 6151 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.
static GetCallerInfo($Element=NULL)
Get info about call to current function.