CWIS Developer Documentation
ApplicationFramework.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: ApplicationFramework.php
4 #
5 # Part of the ScoutLib application support library
6 # Copyright 2009-2016 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu
8 #
9 
15 {
16 
17  # ---- PUBLIC INTERFACE --------------------------------------------------
18  /*@(*/
20 
25  public function __construct()
26  {
27  # set up a class alias for convenience
28  class_alias("ApplicationFramework", "AF");
29 
30  # adjust environment in case we are being run via CGI
31  $this->AdjustEnvironmentForCgi();
32 
33  # make sure default time zone is set
34  # (using CST if nothing set because we have to use something
35  # and Scout is based in Madison, WI, which is in CST)
36  if ((ini_get("date.timezone") === NULL)
37  || !strlen(ini_get("date.timezone")))
38  {
39  ini_set("date.timezone", "America/Chicago");
40  }
41 
42  # save execution start time
43  $this->ExecutionStartTime = microtime(TRUE);
44 
45  # set up default object file search locations
46  self::AddObjectDirectory("local/interface/%ACTIVEUI%/objects");
47  self::AddObjectDirectory("interface/%ACTIVEUI%/objects");
48  self::AddObjectDirectory("local/interface/%DEFAULTUI%/objects");
49  self::AddObjectDirectory("interface/%DEFAULTUI%/objects");
50  self::AddObjectDirectory("local/objects");
51  self::AddObjectDirectory("objects");
52 
53  # set up object file autoloader
54  spl_autoload_register(array($this, "AutoloadObjects"));
55 
56  # set up function to output any buffered text in case of crash
57  register_shutdown_function(array($this, "OnCrash"));
58 
59  # if we were not invoked via command line interface
60  # and session initialization has not been explicitly suppressed
61  if ((php_sapi_name() !== "cli") && (!self::$SuppressSessionInitialization))
62  {
63  # build cookie domain string
64  $SessionDomain = isset($_SERVER["SERVER_NAME"]) ? $_SERVER["SERVER_NAME"]
65  : isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"]
66  : php_uname("n");
67 
68  # include a leading period so that older browsers implementing
69  # rfc2109 do not reject our cookie
70  $SessionDomain = ".".$SessionDomain;
71 
72  # if it appears our session storage area is writable
73  if (is_writable(session_save_path()))
74  {
75  # store our session files in a subdirectory to avoid
76  # accidentally sharing sessions with other installations
77  # on the same domain
78  $SessionStorage = session_save_path()
79  ."/".self::$AppName."_".md5($SessionDomain.dirname(__FILE__));
80 
81  # create session storage subdirectory if not found
82  if (!is_dir($SessionStorage)) { mkdir($SessionStorage, 0700 ); }
83 
84  # if session storage subdirectory is writable
85  if (is_writable($SessionStorage))
86  {
87  # save parameters of our session storage as instance variables
88  # for later use
89  $this->SessionGcProbability =
90  ini_get("session.gc_probability") / ini_get("session.gc_divisor");
91  # require a gc probability of at least MIN_GC_PROBABILITY
92  if ($this->SessionGcProbability < self::MIN_GC_PROBABILITY)
93  {
94  $this->SessionGcProbability = self::MIN_GC_PROBABILITY;
95  }
96 
97  $this->SessionStorage = $SessionStorage;
98 
99  # set the new session storage location
100  session_save_path($SessionStorage);
101 
102  # disable PHP's garbage collection, as it does not handle
103  # subdirectories (instead, we'll do the cleanup as we run
104  # background tasks)
105  ini_set("session.gc_probability", 0);
106  }
107  }
108 
109  # set garbage collection max period to our session lifetime
110  ini_set("session.gc_maxlifetime", self::$SessionLifetime);
111 
112  # limit cookie to secure connection if we are running over same
113  $SecureCookie = isset($_SERVER["HTTPS"]) ? TRUE : FALSE;
114 
115  # Cookies lacking embedded dots are... fun.
116  # rfc2109 sec 4.3.2 says to reject them
117  # rfc2965 sec 3.3.2 says to reject them
118  # rfc6265 sec 4.1.2.3 says only that "public suffixes"
119  # should be rejected. They reference Mozilla's
120  # publicsuffix.org, which does not contain 'localhost'.
121  # However, empirically in early 2017 Firefox still rejects
122  # 'localhost'.
123  # Therefore, don't set a cookie domain if we're running on
124  # localhost to avoid this problem.
125  if (preg_match('/^\.localhost(:[0-9]+)?$/', $SessionDomain))
126  {
127  $SessionDomain = "";
128  }
129  session_set_cookie_params(self::$SessionLifetime, "/",
130  $SessionDomain, $SecureCookie, TRUE);
131 
132  # attempt to start session
133  $SessionStarted = @session_start();
134 
135  # if session start failed
136  if (!$SessionStarted)
137  {
138  # regenerate session ID and attempt to start session again
139  session_regenerate_id(TRUE);
140  session_start();
141  }
142 
143  # bump up our cookie expiry time, so that it'll die
144  # $SessionLifetime from now, rather than $SessionLifetime
145  # from whenever we created it
146  setcookie(
147  session_name(), session_id(),
148  time() + self::$SessionLifetime, "/",
149  $SessionDomain, $SecureCookie, TRUE);
150  }
151 
152  # set up our internal environment
153  $this->DB = new Database();
154 
155  # set up our exception handler
156  set_exception_handler(array($this, "GlobalExceptionHandler"));
157 
158  # load our settings from database
159  $this->LoadSettings();
160 
161  # set PHP maximum execution time
162  ini_set("max_execution_time", $this->Settings["MaxExecTime"]);
163  set_time_limit($this->Settings["MaxExecTime"]);
164 
165  # set database slow query threshold for foreground execution
167  self::MIN_DB_SLOW_QUERY_THRESHOLD,
168  self::DatabaseSlowQueryThresholdForForeground()));
169 
170  # register events we handle internally
171  $this->RegisterEvent($this->PeriodicEvents);
172  $this->RegisterEvent($this->UIEvents);
173 
174  # attempt to create SCSS cache directory if needed and it does not exist
175  if ($this->ScssSupportEnabled() && !is_dir(self::$ScssCacheDir))
176  { @mkdir(self::$ScssCacheDir, 0777, TRUE); }
177 
178  # attempt to create minimized JS cache directory if needed and it does not exist
179  if ($this->UseMinimizedJavascript()
181  && !is_dir(self::$JSMinCacheDir))
182  {
183  @mkdir(self::$JSMinCacheDir, 0777, TRUE);
184  }
185 
186  # initialize task manager
187  $this->TaskMgr = new AFTaskManager();
188  }
195  public function __destruct()
196  {
197  # if template location cache is flagged to be saved
198  if ($this->SaveTemplateLocationCache)
199  {
200  # write template location cache out and update cache expiration
201  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
202  ." SET TemplateLocationCache = '"
203  .addslashes(serialize(
204  $this->TemplateLocationCache))."',"
205  ." TemplateLocationCacheExpiration = '"
206  .date("Y-m-d H:i:s",
207  $this->TemplateLocationCacheExpiration)."'");
208  }
209 
210  # if object location cache is flagged to be saved
211  if (self::$SaveObjectLocationCache)
212  {
213  # write object location cache out and update cache expiration
214  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
215  ." SET ObjectLocationCache = '"
216  .addslashes(serialize(
217  self::$ObjectLocationCache))."',"
218  ." ObjectLocationCacheExpiration = '"
219  .date("Y-m-d H:i:s",
220  self::$ObjectLocationCacheExpiration)."'");
221  }
222  }
230  public function GlobalExceptionHandler($Exception)
231  {
232  # display exception info
233  $Message = $Exception->getMessage();
234  $Location = str_replace(getcwd()."/", "",
235  $Exception->getFile()."[".$Exception->getLine()."]");
236  $Trace = preg_replace(":(#[0-9]+) ".getcwd()."/".":", "$1 ",
237  $Exception->getTraceAsString());
238  if (php_sapi_name() == "cli")
239  {
240  print "Uncaught Exception\n"
241  ."Message: ".$Message."\n"
242  ."Location: ".$Location."\n"
243  ."Trace: \n"
244  .$Trace."\n";
245  }
246  else
247  {
248  ?><table width="100%" cellpadding="5"
249  style="border: 2px solid #666666; background: #CCCCCC;
250  font-family: Courier New, Courier, monospace;
251  margin-top: 10px;"><tr><td>
252  <div style="color: #666666;">
253  <span style="font-size: 150%;">
254  <b>Uncaught Exception</b></span><br />
255  <b>Message:</b> <i><?= $Message ?></i><br />
256  <b>Location:</b> <i><?= $Location ?></i><br />
257  <b>Trace:</b> <blockquote><pre><?= $Trace ?></pre></blockquote>
258  </div>
259  </td></tr></table><?PHP
260  }
261 
262  # log exception if not running from command line
263  if (php_sapi_name() !== "cli")
264  {
265  $TraceString = $Exception->getTraceAsString();
266  $TraceString = str_replace("\n", ", ", $TraceString);
267  $TraceString = preg_replace(":(#[0-9]+) ".getcwd()."/".":",
268  "$1 ", $TraceString);
269  $LogMsg = "Uncaught exception (".$Exception->getMessage().")"
270  ." at ".$Location."."
271  ." TRACE: ".$TraceString
272  ." URL: ".$this->FullUrl();
273  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
274  }
275  }
296  public static function AddObjectDirectory(
297  $Dir, $NamespacePrefixes = array(), $Callback = NULL)
298  {
299  # check to make sure any supplied callback looks valid
300  if (!is_null($Callback) && !is_callable($Callback))
301  {
302  throw new InvalidArgumentException("Supplied callback (\""
303  .$Callback."\") is invalid.");
304  }
305 
306  # make sure directory has trailing slash
307  $Dir = $Dir.((substr($Dir, -1) != "/") ? "/" : "");
308 
309  # make sure namespaces is an array
310  if (!is_array($NamespacePrefixes))
311  {
312  if (is_string($NamespacePrefixes))
313  {
314  $NamespacePrefixes = [ $NamespacePrefixes ];
315  }
316  else
317  {
318  throw new InvalidArgumentException("Supplied namespace (\""
319  .$NamespacePrefixes."\") is invalid.");
320  }
321  }
322 
323  # make sure namespace prefixes are in decreasing order of length
324  usort($NamespacePrefixes, function($A, $B)
325  {
326  return strlen($B) - strlen($A);
327  });
328 
329  # add directory to directory list
330  self::$ObjectDirectories[$Dir] = array(
331  "NamespacePrefixes" => $NamespacePrefixes,
332  "Callback" => $Callback,
333  );
334  }
335 
356  public function AddImageDirectories(
357  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
358  {
359  # add directories to existing image directory list
360  $this->ImageDirList = $this->AddToDirList(
361  $this->ImageDirList, $Dir, $SearchLast, $SkipSlashCheck);
362  }
363 
385  public function AddIncludeDirectories(
386  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
387  {
388  # add directories to existing image directory list
389  $this->IncludeDirList = $this->AddToDirList(
390  $this->IncludeDirList, $Dir, $SearchLast, $SkipSlashCheck);
391  }
392 
413  public function AddInterfaceDirectories(
414  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
415  {
416  # add directories to existing image directory list
417  $this->InterfaceDirList = $this->AddToDirList(
418  $this->InterfaceDirList, $Dir, $SearchLast, $SkipSlashCheck);
419 
420  # cleared cached lists for user interfaces
421  self::$UserInterfaceListCache = array();
422  self::$UserInterfacePathsCache = array();
423  }
424 
445  public function AddFunctionDirectories(
446  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
447  {
448  # add directories to existing image directory list
449  $this->FunctionDirList = $this->AddToDirList(
450  $this->FunctionDirList, $Dir, $SearchLast, $SkipSlashCheck);
451  }
452 
458  public function SetBrowserDetectionFunc($DetectionFunc)
459  {
460  $this->BrowserDetectFunc = $DetectionFunc;
461  }
462 
469  public function AddUnbufferedCallback($Callback, $Parameters = array())
470  {
471  if (is_callable($Callback))
472  {
473  $this->UnbufferedCallbacks[] = array($Callback, $Parameters);
474  }
475  }
476 
487  $NewInterval = DB_NOVALUE, $Persistent = FALSE)
488  {
489  return $this->UpdateSetting(
490  "TemplateLocationCacheInterval", $NewInterval, $Persistent);
491  }
492 
496  public function ClearTemplateLocationCache()
497  {
498  $this->TemplateLocationCache = array();
499  $this->SaveTemplateLocationCache = TRUE;
500  }
501 
512  $NewValue = DB_NOVALUE, $Persistent = FALSE)
513  {
514  return $this->UpdateSetting(
515  "ObjectLocationCacheInterval", $NewValue, $Persistent);
516  }
517 
521  public function ClearObjectLocationCache()
522  {
523  self::$ObjectLocationCache = array();
524  self::$SaveObjectLocationCache = TRUE;
525  }
526 
536  public function UrlFingerprintingEnabled(
537  $NewValue = DB_NOVALUE, $Persistent = FALSE)
538  {
539  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
540  }
541 
552  public function ScssSupportEnabled(
553  $NewValue = DB_NOVALUE, $Persistent = FALSE)
554  {
555  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
556  }
557 
569  public function GenerateCompactCss(
570  $NewValue = DB_NOVALUE, $Persistent = FALSE)
571  {
572  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
573  }
574 
586  public function UseMinimizedJavascript(
587  $NewValue = DB_NOVALUE, $Persistent = FALSE)
588  {
589  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
590  }
591 
604  $NewValue = DB_NOVALUE, $Persistent = FALSE)
605  {
606  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
607  }
608 
622  public function RecordContextInCaseOfCrash(
623  $BacktraceOptions = 0, $BacktraceLimit = 0)
624  {
625  if (version_compare(PHP_VERSION, "5.4.0", ">="))
626  {
627  $this->SavedContext = debug_backtrace(
628  $BacktraceOptions, $BacktraceLimit);
629  }
630  else
631  {
632  $this->SavedContext = debug_backtrace($BacktraceOptions);
633  }
634  array_shift($this->SavedContext);
635  }
636 
641  public function LoadPage($PageName)
642  {
643  # perform any clean URL rewriting
644  $PageName = $this->RewriteCleanUrls($PageName);
645 
646  # sanitize incoming page name and save local copy
647  $PageName = preg_replace("/[^a-zA-Z0-9_.-]/", "", $PageName);
648  $this->PageName = $PageName;
649 
650  # if page caching is turned on
651  if ($this->PageCacheEnabled())
652  {
653  # if we have a cached page
654  $CachedPage = $this->CheckForCachedPage($PageName);
655  if ($CachedPage !== NULL)
656  {
657  # set header to indicate cache hit was found
658  header("X-ScoutAF-Cache: HIT");
659 
660  # display cached page and exit
661  print $CachedPage;
662  return;
663  }
664  else
665  {
666  # set header to indicate no cache hit was found
667  header("X-ScoutAF-Cache: MISS");
668  }
669  }
670 
671  # buffer any output from includes or PHP file
672  ob_start();
673 
674  # include any files needed to set up execution environment
675  $IncludeFileContext = array();
676  foreach ($this->EnvIncludes as $IncludeFile)
677  {
678  $IncludeFileContext = $this->FilterContext(self::CONTEXT_ENV,
679  self::IncludeFile($IncludeFile, $IncludeFileContext));
680  }
681 
682  # signal page load
683  $this->SignalEvent("EVENT_PAGE_LOAD", array("PageName" => $PageName));
684 
685  # signal PHP file load
686  $SignalResult = $this->SignalEvent("EVENT_PHP_FILE_LOAD", array(
687  "PageName" => $PageName));
688 
689  # if signal handler returned new page name value
690  $NewPageName = $PageName;
691  if (($SignalResult["PageName"] != $PageName)
692  && strlen($SignalResult["PageName"]))
693  {
694  # if new page name value is page file
695  if (file_exists($SignalResult["PageName"]))
696  {
697  # use new value for PHP file name
698  $PageFile = $SignalResult["PageName"];
699  }
700  else
701  {
702  # use new value for page name
703  $NewPageName = $SignalResult["PageName"];
704  }
705 
706  # update local copy of page name
707  $this->PageName = $NewPageName;
708  }
709 
710  # if we do not already have a PHP file
711  if (!isset($PageFile))
712  {
713  # look for PHP file for page
714  $OurPageFile = "pages/".$NewPageName.".php";
715  $LocalPageFile = "local/pages/".$NewPageName.".php";
716  $PageFile = file_exists($LocalPageFile) ? $LocalPageFile
717  : (file_exists($OurPageFile) ? $OurPageFile
718  : "pages/".$this->DefaultPage.".php");
719  }
720 
721  # load PHP file
722  $IncludeFileContext = $this->FilterContext(self::CONTEXT_PAGE,
723  self::IncludeFile($PageFile, $IncludeFileContext));
724 
725  # save buffered output to be displayed later after HTML file loads
726  $PageOutput = ob_get_contents();
727  ob_end_clean();
728 
729  # signal PHP file load is complete
730  ob_start();
731  $Context["Variables"] = $IncludeFileContext;
732  $this->SignalEvent("EVENT_PHP_FILE_LOAD_COMPLETE",
733  array("PageName" => $PageName, "Context" => $Context));
734  $PageCompleteOutput = ob_get_contents();
735  ob_end_clean();
736 
737  # set up for possible TSR (Terminate and Stay Resident :))
738  $ShouldTSR = $this->PrepForTSR();
739 
740  # if PHP file indicated we should autorefresh to somewhere else
741  if (($this->JumpToPage) && ($this->JumpToPageDelay == 0))
742  {
743  # if no page output
744  if (!strlen(trim($PageOutput)))
745  {
746  # if client supports HTTP/1.1, use a 303 as it is most accurate
747  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.1")
748  {
749  header("HTTP/1.1 303 See Other");
750  header("Location: ".$this->JumpToPage);
751  }
752  else
753  {
754  # if the request was an HTTP/1.0 GET or HEAD, then
755  # use a 302 response code.
756 
757  # NB: both RFC 2616 (HTTP/1.1) and RFC1945 (HTTP/1.0)
758  # explicitly prohibit automatic redirection via a 302
759  # if the request was not GET or HEAD.
760  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.0" &&
761  ($_SERVER["REQUEST_METHOD"] == "GET" ||
762  $_SERVER["REQUEST_METHOD"] == "HEAD") )
763  {
764  header("HTTP/1.0 302 Found");
765  header("Location: ".$this->JumpToPage);
766  }
767 
768  # otherwise, fall back to a meta refresh
769  else
770  {
771  print '<html><head><meta http-equiv="refresh" '
772  .'content="0; URL='.$this->JumpToPage.'">'
773  .'</head><body></body></html>';
774  }
775  }
776  }
777  }
778  # else if HTML loading is not suppressed
779  elseif (!$this->SuppressHTML)
780  {
781  # set content-type to avoid diacritic errors
782  header("Content-Type: text/html; charset="
783  .$this->HtmlCharset, TRUE);
784 
785  # load common HTML file (defines common functions) if available
786  $CommonHtmlFile = $this->FindFile($this->IncludeDirList,
787  "Common", array("tpl", "html"));
788  if ($CommonHtmlFile)
789  {
790  $IncludeFileContext = $this->FilterContext(self::CONTEXT_COMMON,
791  self::IncludeFile($CommonHtmlFile, $IncludeFileContext));
792  }
793 
794  # load UI functions
795  $this->LoadUIFunctions();
796 
797  # begin buffering content
798  ob_start();
799 
800  # signal HTML file load
801  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD", array(
802  "PageName" => $PageName));
803 
804  # if signal handler returned new page name value
805  $NewPageName = $PageName;
806  $PageContentFile = NULL;
807  if (($SignalResult["PageName"] != $PageName)
808  && strlen($SignalResult["PageName"]))
809  {
810  # if new page name value is HTML file
811  if (file_exists($SignalResult["PageName"]))
812  {
813  # use new value for HTML file name
814  $PageContentFile = $SignalResult["PageName"];
815  }
816  else
817  {
818  # use new value for page name
819  $NewPageName = $SignalResult["PageName"];
820  }
821  }
822 
823  # load page content HTML file if available
824  if ($PageContentFile === NULL)
825  {
826  $PageContentFile = $this->FindFile(
827  $this->InterfaceDirList, $NewPageName,
828  array("tpl", "html"));
829  }
830  if ($PageContentFile)
831  {
832  $IncludeFileContext = $this->FilterContext(self::CONTEXT_INTERFACE,
833  self::IncludeFile($PageContentFile, $IncludeFileContext));
834  }
835  else
836  {
837  print "<h2>ERROR: No HTML/TPL template found"
838  ." for this page (".$NewPageName.").</h2>";
839  $this->DoNotCacheCurrentPage();
840  }
841 
842  # signal HTML file load complete
843  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD_COMPLETE");
844 
845  # stop buffering and save output
846  $PageContentOutput = ob_get_contents();
847  ob_end_clean();
848 
849  # if standard page start/end have not been suppressed
850  $PageStartOutput = "";
851  $PageEndOutput = "";
852  if (!$this->SuppressStdPageStartAndEnd)
853  {
854  # load page start HTML file if available
855  $PageStartFile = $this->FindFile($this->IncludeDirList, "Start",
856  array("tpl", "html"), array("StdPage", "StandardPage"));
857  if ($PageStartFile)
858  {
859  ob_start();
860  $IncludeFileContext = self::IncludeFile(
861  $PageStartFile, $IncludeFileContext);
862  $PageStartOutput = ob_get_contents();
863  ob_end_clean();
864  }
865  $IncludeFileContext = $this->FilterContext(
866  self::CONTEXT_START, $IncludeFileContext);
867 
868  # load page end HTML file if available
869  $PageEndFile = $this->FindFile($this->IncludeDirList, "End",
870  array("tpl", "html"), array("StdPage", "StandardPage"));
871  if ($PageEndFile)
872  {
873  ob_start();
874  self::IncludeFile($PageEndFile, $IncludeFileContext);
875  $PageEndOutput = ob_get_contents();
876  ob_end_clean();
877  }
878  }
879 
880  # clear include file context because it may be large and is no longer needed
881  unset($IncludeFileContext);
882 
883  # if page auto-refresh requested
884  if ($this->JumpToPage)
885  {
886  # add auto-refresh tag to page
887  $this->AddMetaTag([
888  "http-equiv" => "refresh",
889  "content" => $this->JumpToPageDelay,
890  "url" => $this->JumpToPage,
891  ]);
892  }
893 
894  # assemble full page
895  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
896 
897  # get list of any required files not loaded
898  $RequiredFiles = $this->GetRequiredFilesNotYetLoaded($PageContentFile);
899 
900  # add file loading tags to page
901  $FullPageOutput = $this->AddFileTagsToPageOutput(
902  $FullPageOutput, $RequiredFiles);
903 
904  # add any requested meta tags to page
905  $FullPageOutput = $this->AddMetaTagsToPageOutput($FullPageOutput);
906 
907  # perform any regular expression replacements in output
908  $NewFullPageOutput = preg_replace($this->OutputModificationPatterns,
909  $this->OutputModificationReplacements, $FullPageOutput);
910 
911  # check to make sure replacements didn't fail
912  $FullPageOutput = $this->CheckOutputModification(
913  $FullPageOutput, $NewFullPageOutput,
914  "regular expression replacements");
915 
916  # for each registered output modification callback
917  foreach ($this->OutputModificationCallbacks as $Info)
918  {
919  # set up data for callback
920  $this->OutputModificationCallbackInfo = $Info;
921 
922  # perform output modification
923  $NewFullPageOutput = preg_replace_callback($Info["SearchPattern"],
924  array($this, "OutputModificationCallbackShell"),
925  $FullPageOutput);
926 
927  # check to make sure modification didn't fail
928  $ErrorInfo = "callback info: ".print_r($Info, TRUE);
929  $FullPageOutput = $this->CheckOutputModification(
930  $FullPageOutput, $NewFullPageOutput, $ErrorInfo);
931  }
932 
933  # provide the opportunity to modify full page output
934  $SignalResult = $this->SignalEvent("EVENT_PAGE_OUTPUT_FILTER", array(
935  "PageOutput" => $FullPageOutput));
936  if (isset($SignalResult["PageOutput"])
937  && strlen(trim($SignalResult["PageOutput"])))
938  {
939  $FullPageOutput = $SignalResult["PageOutput"];
940  }
941 
942  # if relative paths may not work because we were invoked via clean URL
943  if ($this->CleanUrlRewritePerformed || self::WasUrlRewritten())
944  {
945  # if using the <base> tag is okay
946  $BaseUrl = $this->BaseUrl();
947  if ($this->UseBaseTag)
948  {
949  # add <base> tag to header
950  $PageStartOutput = str_replace("<head>",
951  "<head><base href=\"".$BaseUrl."\" />",
952  $PageStartOutput);
953 
954  # re-assemble full page with new header
955  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
956 
957  # the absolute URL to the current page
958  $FullUrl = $BaseUrl . $this->GetPageLocation();
959 
960  # make HREF attribute values with just a fragment ID
961  # absolute since they don't work with the <base> tag because
962  # they are relative to the current page/URL, not the site
963  # root
964  $NewFullPageOutput = preg_replace(
965  array("%href=\"(#[^:\" ]+)\"%i", "%href='(#[^:' ]+)'%i"),
966  array("href=\"".$FullUrl."$1\"", "href='".$FullUrl."$1'"),
967  $FullPageOutput);
968 
969  # check to make sure HREF cleanup didn't fail
970  $FullPageOutput = $this->CheckOutputModification(
971  $FullPageOutput, $NewFullPageOutput,
972  "HREF cleanup");
973  }
974  else
975  {
976  # try to fix any relative paths throughout code
977  $SrcFileExtensions = "(js|css|gif|png|jpg|svg|ico)";
978  $RelativePathPatterns = array(
979  "%src=\"/?([^?*:;{}\\\\\" ]+)\.".$SrcFileExtensions."\"%i",
980  "%src='/?([^?*:;{}\\\\' ]+)\.".$SrcFileExtensions."'%i",
981  # don't rewrite HREF attributes that are just
982  # fragment IDs because they are relative to the
983  # current page/URL, not the site root
984  "%href=\"/?([^#][^:\" ]*)\"%i",
985  "%href='/?([^#][^:' ]*)'%i",
986  "%action=\"/?([^#][^:\" ]*)\"%i",
987  "%action='/?([^#][^:' ]*)'%i",
988  "%@import\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
989  "%@import\s+url\('/?([^:\" ]+)'\s*\)%i",
990  "%src:\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
991  "%src:\s+url\('/?([^:\" ]+)'\s*\)%i",
992  "%@import\s+\"/?([^:\" ]+)\"\s*%i",
993  "%@import\s+'/?([^:\" ]+)'\s*%i",
994  );
995  $RelativePathReplacements = array(
996  "src=\"".$BaseUrl."$1.$2\"",
997  "src=\"".$BaseUrl."$1.$2\"",
998  "href=\"".$BaseUrl."$1\"",
999  "href=\"".$BaseUrl."$1\"",
1000  "action=\"".$BaseUrl."$1\"",
1001  "action=\"".$BaseUrl."$1\"",
1002  "@import url(\"".$BaseUrl."$1\")",
1003  "@import url('".$BaseUrl."$1')",
1004  "src: url(\"".$BaseUrl."$1\")",
1005  "src: url('".$BaseUrl."$1')",
1006  "@import \"".$BaseUrl."$1\"",
1007  "@import '".$BaseUrl."$1'",
1008  );
1009  $NewFullPageOutput = preg_replace($RelativePathPatterns,
1010  $RelativePathReplacements, $FullPageOutput);
1011 
1012  # check to make sure relative path fixes didn't fail
1013  $FullPageOutput = $this->CheckOutputModification(
1014  $FullPageOutput, $NewFullPageOutput,
1015  "relative path fixes");
1016  }
1017  }
1018 
1019  # handle any necessary alternate domain rewriting
1020  $FullPageOutput = $this->RewriteAlternateDomainUrls($FullPageOutput);
1021 
1022  # update page cache for this page
1023  $this->UpdatePageCache($PageName, $FullPageOutput);
1024 
1025  # write out full page
1026  print $FullPageOutput;
1027  }
1028 
1029  # run any post-processing routines
1030  foreach ($this->PostProcessingFuncs as $Func)
1031  {
1032  call_user_func_array($Func["FunctionName"], $Func["Arguments"]);
1033  }
1034 
1035  # write out any output buffered from page code execution
1036  if (strlen($PageOutput))
1037  {
1038  if (!$this->SuppressHTML)
1039  {
1040  ?><table width="100%" cellpadding="5"
1041  style="border: 2px solid #666666; background: #CCCCCC;
1042  font-family: Courier New, Courier, monospace;
1043  margin-top: 10px;"><tr><td><?PHP
1044  }
1045  if ($this->JumpToPage)
1046  {
1047  ?><div style="color: #666666;"><span style="font-size: 150%;">
1048  <b>Page Jump Aborted</b></span>
1049  (because of error or other unexpected output)<br />
1050  <b>Jump Target:</b>
1051  <i><?PHP print($this->JumpToPage); ?></i></div><?PHP
1052  }
1053  print $PageOutput;
1054  if (!$this->SuppressHTML)
1055  {
1056  ?></td></tr></table><?PHP
1057  }
1058  }
1059 
1060  # write out any output buffered from the page code execution complete signal
1061  if (!$this->JumpToPage && !$this->SuppressHTML && strlen($PageCompleteOutput))
1062  {
1063  print $PageCompleteOutput;
1064  }
1065 
1066  # log slow page loads
1067  if ($this->LogSlowPageLoads()
1068  && !$this->DoNotLogSlowPageLoad
1069  && ($this->GetElapsedExecutionTime()
1070  >= ($this->SlowPageLoadThreshold())))
1071  {
1072  $RemoteHost = gethostbyaddr($_SERVER["REMOTE_ADDR"]);
1073  if ($RemoteHost === FALSE)
1074  {
1075  $RemoteHost = $_SERVER["REMOTE_ADDR"];
1076  }
1077  elseif ($RemoteHost != $_SERVER["REMOTE_ADDR"])
1078  {
1079  $RemoteHost .= " (".$_SERVER["REMOTE_ADDR"].")";
1080  }
1081  $SlowPageLoadMsg = "Slow page load ("
1082  .intval($this->GetElapsedExecutionTime())."s) for "
1083  .$this->FullUrl()." from ".$RemoteHost;
1084  $this->LogMessage(self::LOGLVL_INFO, $SlowPageLoadMsg);
1085  }
1086 
1087  # execute callbacks that should not have their output buffered
1088  foreach ($this->UnbufferedCallbacks as $Callback)
1089  {
1090  call_user_func_array($Callback[0], $Callback[1]);
1091  }
1092 
1093  # log high memory usage
1094  if (function_exists("memory_get_peak_usage"))
1095  {
1096  $MemoryThreshold = ($this->HighMemoryUsageThreshold()
1097  * $this->GetPhpMemoryLimit()) / 100;
1098  if ($this->LogHighMemoryUsage()
1099  && (memory_get_peak_usage(TRUE) >= $MemoryThreshold))
1100  {
1101  $HighMemUsageMsg = "High peak memory usage ("
1102  .number_format(memory_get_peak_usage(TRUE)).") for "
1103  .$this->FullUrl()." from "
1104  .$_SERVER["REMOTE_ADDR"];
1105  $this->LogMessage(self::LOGLVL_INFO, $HighMemUsageMsg);
1106  }
1107  }
1108 
1109  $this->UpdateLastUsedTimeForActiveSessions();
1110 
1111  # terminate and stay resident (TSR!) if indicated and HTML has been output
1112  # (only TSR if HTML has been output because otherwise browsers will misbehave)
1113  if ($ShouldTSR) { $this->LaunchTSR(); }
1114  }
1115 
1120  public function IsRunningInBackground()
1121  {
1122  return $this->RunningInBackground;
1123  }
1124 
1130  public function GetPageName()
1131  {
1132  return $this->PageName;
1133  }
1134 
1140  public function GetPageLocation()
1141  {
1142  # retrieve current URL
1143  $Url = self::GetScriptUrl();
1144 
1145  # remove the base path if present
1146  $BasePath = $this->Settings["BasePath"];
1147  if (stripos($Url, $BasePath) === 0)
1148  {
1149  $Url = substr($Url, strlen($BasePath));
1150  }
1151 
1152  # if we're being accessed via an alternate domain,
1153  # add the appropriate prefix in
1154  if ($this->HtaccessSupport() &&
1155  self::$RootUrlOverride !== NULL)
1156  {
1157  $VHost = $_SERVER["SERVER_NAME"];
1158  if (isset($this->AlternateDomainPrefixes[$VHost]))
1159  {
1160  $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
1161  $Url = $ThisPrefix."/".$Url;
1162  }
1163  }
1164 
1165  return $Url;
1166  }
1167 
1173  public function GetPageUrl()
1174  {
1175  return self::BaseUrl().$this->GetPageLocation();
1176  }
1177 
1190  public function SetJumpToPage($Page, $Delay = 0, $IsLiteral = FALSE)
1191  {
1192  if (!is_null($Page)
1193  && (!$IsLiteral)
1194  && (strpos($Page, "?") === FALSE)
1195  && ((strpos($Page, "=") !== FALSE)
1196  || ((stripos($Page, ".php") === FALSE)
1197  && (stripos($Page, ".htm") === FALSE)
1198  && (strpos($Page, "/") === FALSE)))
1199  && (stripos($Page, "http://") !== 0)
1200  && (stripos($Page, "https://") !== 0))
1201  {
1202  $this->JumpToPage = self::BaseUrl() . "index.php?P=".$Page;
1203  }
1204  else
1205  {
1206  $this->JumpToPage = $Page;
1207  }
1208  $this->JumpToPageDelay = $Delay;
1209  }
1210 
1215  public function JumpToPageIsSet()
1216  {
1217  return ($this->JumpToPage === NULL) ? FALSE : TRUE;
1218  }
1219 
1229  public function HtmlCharset($NewSetting = NULL)
1230  {
1231  if ($NewSetting !== NULL) { $this->HtmlCharset = $NewSetting; }
1232  return $this->HtmlCharset;
1233  }
1234 
1244  public function DoNotMinimizeFile($File)
1245  {
1246  if (!is_array($File)) { $File = array($File); }
1247  $this->DoNotMinimizeList = array_merge($this->DoNotMinimizeList, $File);
1248  }
1249 
1260  public function UseBaseTag($NewValue = NULL)
1261  {
1262  if ($NewValue !== NULL) { $this->UseBaseTag = $NewValue ? TRUE : FALSE; }
1263  return $this->UseBaseTag;
1264  }
1265 
1273  public function SuppressHTMLOutput($NewSetting = TRUE)
1274  {
1275  $this->SuppressHTML = $NewSetting;
1276  }
1277 
1285  public function SuppressStandardPageStartAndEnd($NewSetting = TRUE)
1286  {
1287  $this->SuppressStdPageStartAndEnd = $NewSetting;
1288  }
1289 
1295  public static function SuppressSessionInitialization($NewValue = NULL)
1296  {
1297  if ($NewValue !== NULL)
1298  {
1299  self::$SuppressSessionInitialization = $NewValue;
1300  }
1301  return self::$SuppressSessionInitialization;
1302  }
1303 
1309  public static function DefaultUserInterface($UIName = NULL)
1310  {
1311  if ($UIName !== NULL)
1312  {
1313  self::$DefaultUI = $UIName;
1314  }
1315  return self::$DefaultUI;
1316  }
1317 
1324  public static function ActiveUserInterface($UIName = NULL)
1325  {
1326  if ($UIName !== NULL)
1327  {
1328  self::$ActiveUI = preg_replace("/^SPTUI--/", "", $UIName);
1329  }
1330  return self::$ActiveUI;
1331  }
1332 
1343  public function GetUserInterfaces($FilterExp = NULL)
1344  {
1345  if (!isset(self::$UserInterfaceListCache[$FilterExp]))
1346  {
1347  # retrieve paths to user interface directories
1348  $Paths = $this->GetUserInterfacePaths($FilterExp);
1349 
1350  # start out with an empty list
1351  self::$UserInterfaceListCache[$FilterExp] = array();
1352 
1353  # for each possible UI directory
1354  foreach ($Paths as $CanonicalName => $Path)
1355  {
1356  # if name file available
1357  $LabelFile = $Path."/NAME";
1358  if (is_readable($LabelFile))
1359  {
1360  # read the UI name
1361  $Label = file_get_contents($LabelFile);
1362 
1363  # if the UI name looks reasonable
1364  if (strlen(trim($Label)))
1365  {
1366  # use read name
1367  self::$UserInterfaceListCache[$FilterExp][$CanonicalName] =
1368  $Label;
1369  }
1370  }
1371 
1372  # if we do not have a name yet
1373  if (!isset(self::$UserInterfaceListCache[$FilterExp][$CanonicalName]))
1374  {
1375  # use base directory for name
1376  self::$UserInterfaceListCache[$FilterExp][$CanonicalName] =
1377  basename($Path);
1378  }
1379  }
1380  }
1381 
1382  # return list to caller
1383  return self::$UserInterfaceListCache[$FilterExp];
1384  }
1385 
1394  public function GetUserInterfacePaths($FilterExp = NULL)
1395  {
1396  if (!isset(self::$UserInterfacePathsCache[$FilterExp]))
1397  {
1398  # extract possible UI directories from interface directory list
1399  $InterfaceDirs = array();
1400  foreach ($this->ExpandDirectoryList($this->InterfaceDirList) as $Dir)
1401  {
1402  $Matches = array();
1403  if (preg_match("#([a-zA-Z0-9/]*interface)/[a-zA-Z0-9%/]*#",
1404  $Dir, $Matches))
1405  {
1406  $Dir = $Matches[1];
1407  if (!in_array($Dir, $InterfaceDirs))
1408  {
1409  $InterfaceDirs[] = $Dir;
1410  }
1411  }
1412  }
1413 
1414  # reverse order of interface directories so that the directory
1415  # returned is the base directory for the interface
1416  $InterfaceDirs = array_reverse($InterfaceDirs);
1417 
1418  # start out with an empty list
1419  self::$UserInterfacePathsCache[$FilterExp] = array();
1420  $InterfacesFound = array();
1421 
1422  # for each possible UI directory
1423  foreach ($InterfaceDirs as $InterfaceDir)
1424  {
1425  # check if the dir exists
1426  if (!is_dir($InterfaceDir))
1427  {
1428  continue;
1429  }
1430 
1431  $Dir = dir($InterfaceDir);
1432 
1433  # for each file in current directory
1434  while (($DirEntry = $Dir->read()) !== FALSE)
1435  {
1436  $InterfacePath = $InterfaceDir."/".$DirEntry;
1437 
1438  # skip anything we have already found
1439  # or that doesn't have a name in the required format
1440  # or that isn't a directory
1441  # or that doesn't match the filter regex (if supplied)
1442  if (in_array($DirEntry, $InterfacesFound)
1443  || !preg_match('/^[a-zA-Z0-9]+$/', $DirEntry)
1444  || !is_dir($InterfacePath)
1445  || (($FilterExp !== NULL)
1446  && !preg_match($FilterExp, $InterfacePath)))
1447  {
1448  continue;
1449  }
1450 
1451  # add interface to list
1452  self::$UserInterfacePathsCache[$FilterExp][$DirEntry] =
1453  $InterfacePath;
1454  $InterfacesFound[] = $DirEntry;
1455  }
1456 
1457  $Dir->close();
1458  }
1459  }
1460 
1461  # return list to caller
1462  return self::$UserInterfacePathsCache[$FilterExp];
1463  }
1464 
1489  public function AddPostProcessingCall($FunctionName,
1490  &$Arg1 = self::NOVALUE, &$Arg2 = self::NOVALUE, &$Arg3 = self::NOVALUE,
1491  &$Arg4 = self::NOVALUE, &$Arg5 = self::NOVALUE, &$Arg6 = self::NOVALUE,
1492  &$Arg7 = self::NOVALUE, &$Arg8 = self::NOVALUE, &$Arg9 = self::NOVALUE)
1493  {
1494  $FuncIndex = count($this->PostProcessingFuncs);
1495  $this->PostProcessingFuncs[$FuncIndex]["FunctionName"] = $FunctionName;
1496  $this->PostProcessingFuncs[$FuncIndex]["Arguments"] = array();
1497  $Index = 1;
1498  while (isset(${"Arg".$Index}) && (${"Arg".$Index} !== self::NOVALUE))
1499  {
1500  $this->PostProcessingFuncs[$FuncIndex]["Arguments"][$Index]
1501  =& ${"Arg".$Index};
1502  $Index++;
1503  }
1504  }
1505 
1511  public function AddEnvInclude($FileName)
1512  {
1513  $this->EnvIncludes[] = $FileName;
1514  }
1515 
1532  public function SetContextFilter($Context, $NewSetting)
1533  {
1534  if (($NewSetting === TRUE)
1535  || ($NewSetting === FALSE)
1536  || is_array($NewSetting))
1537  {
1538  $this->ContextFilters[$Context] = $NewSetting;
1539  }
1540  elseif (is_string($NewSetting))
1541  {
1542  $this->ContextFilters[$Context] = array($NewSetting);
1543  }
1544  else
1545  {
1546  throw new InvalidArgumentException(
1547  "Invalid setting (".$NewSetting.").");
1548  }
1549  }
1551  const CONTEXT_ENV = 1;
1553  const CONTEXT_PAGE = 2;
1555  const CONTEXT_COMMON = 3;
1559  const CONTEXT_START = 5;
1561  const CONTEXT_END = 6;
1562 
1569  public function GUIFile($FileName)
1570  {
1571  # determine which location to search based on file suffix
1572  $FileType = $this->GetFileType($FileName);
1573  $DirList = ($FileType == self::FT_IMAGE)
1574  ? $this->ImageDirList : $this->IncludeDirList;
1575 
1576  # if directed to use minimized JavaScript file
1577  if (($FileType == self::FT_JAVASCRIPT) && $this->UseMinimizedJavascript())
1578  {
1579  # look for minimized version of file
1580  $MinimizedFileName = substr_replace($FileName, ".min", -3, 0);
1581  $FoundFileName = $this->FindFile($DirList, $MinimizedFileName);
1582 
1583  # if minimized file was not found
1584  if (is_null($FoundFileName))
1585  {
1586  # look for unminimized file
1587  $FoundFileName = $this->FindFile($DirList, $FileName);
1588 
1589  # if unminimized file found
1590  if (!is_null($FoundFileName))
1591  {
1592  # if minimization enabled and supported
1593  if ($this->JavascriptMinimizationEnabled()
1594  && self::JsMinRewriteSupport())
1595  {
1596  # attempt to create minimized file
1597  $MinFileName = $this->MinimizeJavascriptFile(
1598  $FoundFileName);
1599 
1600  # if minimization succeeded
1601  if ($MinFileName !== NULL)
1602  {
1603  # use minimized version
1604  $FoundFileName = $MinFileName;
1605 
1606  # save file modification time if needed for fingerprinting
1607  if ($this->UrlFingerprintingEnabled())
1608  {
1609  $FileMTime = filemtime($FoundFileName);
1610  }
1611 
1612  # strip off the cache location, allowing .htaccess
1613  # to handle that for us
1614  $FoundFileName = str_replace(
1615  self::$JSMinCacheDir."/", "", $FoundFileName);
1616  }
1617  }
1618  }
1619  }
1620  }
1621  # else if directed to use SCSS files
1622  elseif (($FileType == self::FT_CSS) && $this->ScssSupportEnabled())
1623  {
1624  # look for SCSS version of file
1625  $SourceFileName = preg_replace("/.css$/", ".scss", $FileName);
1626  $FoundSourceFileName = $this->FindFile($DirList, $SourceFileName);
1627 
1628  # if SCSS file not found
1629  if ($FoundSourceFileName === NULL)
1630  {
1631  # look for CSS file
1632  $FoundFileName = $this->FindFile($DirList, $FileName);
1633  }
1634  else
1635  {
1636  # compile SCSS file (if updated) and return resulting CSS file
1637  $FoundFileName = $this->CompileScssFile($FoundSourceFileName);
1638 
1639  # save file modification time if needed for fingerprinting
1640  if ($this->UrlFingerprintingEnabled())
1641  {
1642  $FileMTime = filemtime($FoundFileName);
1643  }
1644 
1645  # strip off the cache location, allowing .htaccess to handle that for us
1646  if (self::ScssRewriteSupport())
1647  {
1648  $FoundFileName = str_replace(
1649  self::$ScssCacheDir."/", "", $FoundFileName);
1650  }
1651  }
1652  }
1653  # otherwise just search for the file
1654  else
1655  {
1656  $FoundFileName = $this->FindFile($DirList, $FileName);
1657  }
1658 
1659  # add non-image files to list of found files (used for required files loading)
1660  if ($FileType != self::FT_IMAGE)
1661  { $this->FoundUIFiles[] = basename($FoundFileName); }
1662 
1663  # if UI file fingerprinting is enabled and supported
1664  if ($this->UrlFingerprintingEnabled()
1665  && self::UrlFingerprintingRewriteSupport()
1666  && (isset($FileMTime) || file_exists($FoundFileName)))
1667  {
1668  # if file does not appear to be a server-side inclusion
1669  if (!preg_match('/\.(html|php)$/i', $FoundFileName))
1670  {
1671  # for each URL fingerprinting blacklist entry
1672  $OnBlacklist = FALSE;
1673  foreach ($this->UrlFingerprintBlacklist as $BlacklistEntry)
1674  {
1675  # if entry looks like a regular expression pattern
1676  if ($BlacklistEntry[0] == substr($BlacklistEntry, -1))
1677  {
1678  # check file name against regular expression
1679  if (preg_match($BlacklistEntry, $FoundFileName))
1680  {
1681  $OnBlacklist = TRUE;
1682  break;
1683  }
1684  }
1685  else
1686  {
1687  # check file name directly against entry
1688  if (basename($FoundFileName) == $BlacklistEntry)
1689  {
1690  $OnBlacklist = TRUE;
1691  break;
1692  }
1693  }
1694  }
1695 
1696  # if file was not on blacklist
1697  if (!$OnBlacklist)
1698  {
1699  # get file modification time if not already retrieved
1700  if (!isset($FileMTime))
1701  {
1702  $FileMTime = filemtime($FoundFileName);
1703  }
1704 
1705  # add timestamp fingerprint to file name
1706  $Fingerprint = sprintf("%06X",
1707  ($FileMTime % 0xFFFFFF));
1708  $FoundFileName = preg_replace("/^(.+)\.([a-z]+)$/",
1709  "$1.".$Fingerprint.".$2",
1710  $FoundFileName);
1711  }
1712  }
1713  }
1714 
1715  # return file name to caller
1716  return $FoundFileName;
1717  }
1718 
1727  public function PUIFile($FileName)
1728  {
1729  $FullFileName = $this->GUIFile($FileName);
1730  if ($FullFileName) { print($FullFileName); }
1731  }
1732 
1747  public function IncludeUIFile($FileNames, $AdditionalAttributes = NULL)
1748  {
1749  # convert file name to array if necessary
1750  if (!is_array($FileNames)) { $FileNames = [ $FileNames ]; }
1751 
1752  # pad additional attributes if supplied
1753  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
1754 
1755  # for each file
1756  foreach ($FileNames as $BaseFileName)
1757  {
1758  # retrieve full file name
1759  $FileName = $this->GUIFile($BaseFileName);
1760 
1761  # if file was found
1762  if ($FileName)
1763  {
1764  # print appropriate tag
1765  print $this->GetUIFileLoadingTag($FileName, $AdditionalAttributes);
1766  }
1767 
1768  # if we are not already loading an override file
1769  if (!preg_match("/-Override.(css|scss|js)$/", $BaseFileName))
1770  {
1771  # attempt to load override file if available
1772  $FileType = $this->GetFileType($BaseFileName);
1773  switch ($FileType)
1774  {
1775  case self::FT_CSS:
1776  $OverrideFileName = preg_replace(
1777  "/\.(css|scss)$/", "-Override.$1",
1778  $BaseFileName);
1779  $this->IncludeUIFile($OverrideFileName,
1780  $AdditionalAttributes);
1781  break;
1782 
1783  case self::FT_JAVASCRIPT:
1784  $OverrideFileName = preg_replace(
1785  "/\.js$/", "-Override.js",
1786  $BaseFileName);
1787  $this->IncludeUIFile($OverrideFileName,
1788  $AdditionalAttributes);
1789  break;
1790  }
1791  }
1792  }
1793  }
1794 
1801  public function DoNotUrlFingerprint($Pattern)
1802  {
1803  $this->UrlFingerprintBlacklist[] = $Pattern;
1804  }
1805 
1817  public function RequireUIFile($FileNames, $Order = self::ORDER_MIDDLE)
1818  {
1819  # convert file names to array if necessary
1820  if (!is_array($FileNames)) { $FileNames = [ $FileNames ]; }
1821 
1822  # add file names to list of required files
1823  foreach ($FileNames as $FileName)
1824  {
1825  $this->AdditionalRequiredUIFiles[$FileName] = $Order;
1826  }
1827  }
1828 
1834  public static function GetFileType($FileName)
1835  {
1836  static $FileTypeCache;
1837  if (isset($FileTypeCache[$FileName]))
1838  {
1839  return $FileTypeCache[$FileName];
1840  }
1841 
1842  $FileSuffix = strtolower(substr($FileName, -3));
1843  if ($FileSuffix == "css")
1844  {
1845  $FileTypeCache[$FileName] = self::FT_CSS;
1846  }
1847  elseif ($FileSuffix == ".js")
1848  {
1849  $FileTypeCache[$FileName] = self::FT_JAVASCRIPT;
1850  }
1851  elseif (($FileSuffix == "gif")
1852  || ($FileSuffix == "jpg")
1853  || ($FileSuffix == "png")
1854  || ($FileSuffix == "svg")
1855  || ($FileSuffix == "ico"))
1856  {
1857  $FileTypeCache[$FileName] = self::FT_IMAGE;
1858  }
1859  else
1860  {
1861  $FileTypeCache[$FileName] = self::FT_OTHER;
1862  }
1863 
1864  return $FileTypeCache[$FileName];
1865  }
1867  const FT_OTHER = 0;
1869  const FT_CSS = 1;
1871  const FT_IMAGE = 2;
1873  const FT_JAVASCRIPT = 3;
1874 
1883  public function LoadFunction($Callback)
1884  {
1885  # if specified function is not currently available
1886  if (!is_callable($Callback))
1887  {
1888  # if function info looks legal
1889  if (is_string($Callback) && strlen($Callback))
1890  {
1891  # start with function directory list
1892  $Locations = $this->FunctionDirList;
1893 
1894  # add object directories to list
1895  $Locations = array_merge(
1896  $Locations, array_keys(self::$ObjectDirectories));
1897 
1898  # look for function file
1899  $FunctionFileName = $this->FindFile($Locations, "F-".$Callback,
1900  array("php", "html"));
1901 
1902  # if function file was found
1903  if ($FunctionFileName)
1904  {
1905  # load function file
1906  include_once($FunctionFileName);
1907  }
1908  else
1909  {
1910  # log error indicating function load failed
1911  $this->LogError(self::LOGLVL_ERROR, "Unable to load function"
1912  ." for callback \"".$Callback."\".");
1913  }
1914  }
1915  else
1916  {
1917  # log error indicating specified function info was bad
1918  $this->LogError(self::LOGLVL_ERROR, "Unloadable callback value"
1919  ." (".$Callback.")"
1920  ." passed to AF::LoadFunction() by "
1921  .StdLib::GetMyCaller().".");
1922  }
1923  }
1924 
1925  # report to caller whether function load succeeded
1926  return is_callable($Callback);
1927  }
1928 
1933  public function GetElapsedExecutionTime()
1934  {
1935  return microtime(TRUE) - $this->ExecutionStartTime;
1936  }
1937 
1942  public function GetSecondsBeforeTimeout()
1943  {
1944  return $this->MaxExecutionTime() - $this->GetElapsedExecutionTime();
1945  }
1946 
1951  public function AddMetaTag($Attribs)
1952  {
1953  # add new meta tag to list
1954  $this->MetaTags[] = $Attribs;
1955  }
1956 
1965  public function AddMetaTagOnce($Attribs, $UniqueAttribs = NULL)
1966  {
1967  # add new meta tag to list
1968  $this->UniqueMetaTags[] = [
1969  "Attribs" => $Attribs,
1970  "UniqueAttribs" => $UniqueAttribs,
1971  ];
1972  }
1973 
1974  /*@)*/ /* Application Framework */
1975 
1976 
1977  # ---- Page Caching ------------------------------------------------------
1978  /*@(*/
1980 
1990  public function PageCacheEnabled(
1991  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1992  {
1993  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
1994  }
1995 
2005  public function PageCacheExpirationPeriod(
2006  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2007  {
2008  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
2009  }
2010 
2015  public function DoNotCacheCurrentPage()
2016  {
2017  $this->CacheCurrentPage = FALSE;
2018  }
2019 
2026  public function AddPageCacheTag($Tag, $Pages = NULL)
2027  {
2028  # normalize tag
2029  $Tag = strtolower($Tag);
2030 
2031  # if pages were supplied
2032  if ($Pages !== NULL)
2033  {
2034  # add pages to list for this tag
2035  if (isset($this->PageCacheTags[$Tag]))
2036  {
2037  $this->PageCacheTags[$Tag] = array_merge(
2038  $this->PageCacheTags[$Tag], $Pages);
2039  }
2040  else
2041  {
2042  $this->PageCacheTags[$Tag] = $Pages;
2043  }
2044  }
2045  else
2046  {
2047  # add current page to list for this tag
2048  $this->PageCacheTags[$Tag][] = "CURRENT";
2049  }
2050  }
2051 
2057  public function ClearPageCacheForTag($Tag)
2058  {
2059  # get tag ID
2060  $TagId = $this->GetPageCacheTagId($Tag);
2061 
2062  # delete pages and tag/page connections for specified tag
2063  $this->DB->Query("DELETE CP, CPTI"
2064  ." FROM AF_CachedPages CP, AF_CachedPageTagInts CPTI"
2065  ." WHERE CPTI.TagId = ".intval($TagId)
2066  ." AND CP.CacheId = CPTI.CacheId");
2067  }
2068 
2072  public function ClearPageCache()
2073  {
2074  # clear all page cache tables
2075  $this->DB->Query("TRUNCATE TABLE AF_CachedPages");
2076  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTags");
2077  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTagInts");
2078  }
2079 
2086  public function GetPageCacheInfo()
2087  {
2088  $Length = $this->DB->Query("SELECT COUNT(*) AS CacheLen"
2089  ." FROM AF_CachedPages", "CacheLen");
2090  $Oldest = $this->DB->Query("SELECT CachedAt FROM AF_CachedPages"
2091  ." ORDER BY CachedAt ASC LIMIT 1", "CachedAt");
2092  return array(
2093  "NumberOfEntries" => $Length,
2094  "OldestTimestamp" => strtotime($Oldest),
2095  );
2096  }
2097 
2104  public function GetPageCachePageList()
2105  {
2106  $this->DB->Query("SELECT SUBSTRING_INDEX(Fingerprint, '-', 1) AS PageName,"
2107  ." COUNT(*) AS Number FROM AF_CachedPages"
2108  ." GROUP BY PageName ORDER BY Number DESC");
2109  return $this->DB->FetchColumn("Number", "PageName");
2110  }
2111 
2112  /*@)*/ /* Page Caching */
2113 
2114 
2115  # ---- Logging -----------------------------------------------------------
2116  /*@(*/
2118 
2132  public function LogSlowPageLoads(
2133  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2134  {
2135  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
2136  }
2137 
2148  public function SlowPageLoadThreshold(
2149  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2150  {
2151  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
2152  }
2153 
2167  public function LogHighMemoryUsage(
2168  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2169  {
2170  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
2171  }
2172 
2184  public function HighMemoryUsageThreshold(
2185  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2186  {
2187  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
2188  }
2189 
2203  public function LogError($Level, $Msg)
2204  {
2205  # if error level is at or below current logging level
2206  if ($this->Settings["LoggingLevel"] >= $Level)
2207  {
2208  # attempt to log error message
2209  $Result = $this->LogMessage($Level, $Msg);
2210 
2211  # if logging attempt failed and level indicated significant error
2212  if (($Result === FALSE) && ($Level <= self::LOGLVL_ERROR))
2213  {
2214  # throw exception about inability to log error
2215  static $AlreadyThrewException = FALSE;
2216  if (!$AlreadyThrewException)
2217  {
2218  $AlreadyThrewException = TRUE;
2219  throw new Exception("Unable to log error (".$Level.": ".$Msg
2220  .") to ".$this->LogFileName);
2221  }
2222  }
2223 
2224  # report to caller whether message was logged
2225  return $Result;
2226  }
2227  else
2228  {
2229  # report to caller that message was not logged
2230  return FALSE;
2231  }
2232  }
2233 
2245  public function LogMessage($Level, $Msg)
2246  {
2247  # if message level is at or below current logging level
2248  if ($this->Settings["LoggingLevel"] >= $Level)
2249  {
2250  # attempt to open log file
2251  $FHndl = @fopen($this->LogFileName, "a");
2252 
2253  # if log file could not be open
2254  if ($FHndl === FALSE)
2255  {
2256  # report to caller that message was not logged
2257  return FALSE;
2258  }
2259  else
2260  {
2261  # format log entry
2262  $ErrorAbbrevs = array(
2263  self::LOGLVL_FATAL => "FTL",
2264  self::LOGLVL_ERROR => "ERR",
2265  self::LOGLVL_WARNING => "WRN",
2266  self::LOGLVL_INFO => "INF",
2267  self::LOGLVL_DEBUG => "DBG",
2268  self::LOGLVL_TRACE => "TRC",
2269  );
2270  $Msg = str_replace(array("\n", "\t", "\r"), " ", $Msg);
2271  $Msg = substr(trim($Msg), 0, self::LOGFILE_MAX_LINE_LENGTH);
2272  $LogEntry = date("Y-m-d H:i:s")
2273  ." ".($this->IsRunningInBackground() ? "B" : "F")
2274  ." ".$ErrorAbbrevs[$Level]
2275  ." ".$Msg;
2276 
2277  # write entry to log
2278  $Success = fwrite($FHndl, $LogEntry."\n");
2279 
2280  # close log file
2281  fclose($FHndl);
2282 
2283  # report to caller whether message was logged
2284  return ($Success === FALSE) ? FALSE : TRUE;
2285  }
2286  }
2287  else
2288  {
2289  # report to caller that message was not logged
2290  return FALSE;
2291  }
2292  }
2293 
2318  public function LoggingLevel($NewValue = DB_NOVALUE, $Persistent = FALSE)
2319  {
2320  # constrain new level (if supplied) to within legal bounds
2321  if ($NewValue !== DB_NOVALUE)
2322  {
2323  $NewValue = max(min($NewValue, 6), 1);
2324  }
2325 
2326  # set new logging level (if supplied) and return current level to caller
2327  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
2328  }
2329 
2336  public function LogFile($NewValue = NULL)
2337  {
2338  if ($NewValue !== NULL) { $this->LogFileName = $NewValue; }
2339  return $this->LogFileName;
2340  }
2341 
2351  public function GetLogEntries($Limit = 0)
2352  {
2353  # return no entries if there isn't a log file
2354  # or we can't read it or it's empty
2355  $LogFile = $this->LogFile();
2356  if (!is_readable($LogFile) || !filesize($LogFile))
2357  {
2358  return array();
2359  }
2360 
2361  # if max number of entries specified
2362  if ($Limit > 0)
2363  {
2364  # load lines from file
2365  $FHandle = fopen($LogFile, "r");
2366  $FileSize = filesize($LogFile);
2367  $SeekPosition = max(0,
2368  ($FileSize - (self::LOGFILE_MAX_LINE_LENGTH
2369  * ($Limit + 1))));
2370  fseek($FHandle, $SeekPosition);
2371  $Block = fread($FHandle, ($FileSize - $SeekPosition));
2372  fclose($FHandle);
2373  $Lines = explode(PHP_EOL, $Block);
2374  array_pop($Lines);
2375 
2376  # prune array back to requested number of entries
2377  $Lines = array_slice($Lines, (0 - $Limit));
2378  }
2379  else
2380  {
2381  # load all lines from log file
2382  $Lines = file($LogFile, FILE_IGNORE_NEW_LINES);
2383  if ($Lines === FALSE)
2384  {
2385  return array();
2386  }
2387  }
2388 
2389  # reverse line order
2390  $Lines = array_reverse($Lines);
2391 
2392  # for each log file line
2393  $Entries = array();
2394  foreach ($Lines as $Line)
2395  {
2396  # attempt to parse line into component parts
2397  $Pieces = explode(" ", $Line, 5);
2398  $Date = isset($Pieces[0]) ? $Pieces[0] : "";
2399  $Time = isset($Pieces[1]) ? $Pieces[1] : "";
2400  $Back = isset($Pieces[2]) ? $Pieces[2] : "";
2401  $Level = isset($Pieces[3]) ? $Pieces[3] : "";
2402  $Msg = isset($Pieces[4]) ? $Pieces[4] : "";
2403 
2404  # skip line if it looks invalid
2405  $ErrorAbbrevs = array(
2406  "FTL" => self::LOGLVL_FATAL,
2407  "ERR" => self::LOGLVL_ERROR,
2408  "WRN" => self::LOGLVL_WARNING,
2409  "INF" => self::LOGLVL_INFO,
2410  "DBG" => self::LOGLVL_DEBUG,
2411  "TRC" => self::LOGLVL_TRACE,
2412  );
2413  if ((($Back != "F") && ($Back != "B"))
2414  || !array_key_exists($Level, $ErrorAbbrevs)
2415  || !strlen($Msg))
2416  {
2417  continue;
2418  }
2419 
2420  # convert parts into appropriate values and add to entries
2421  $Entries[] = array(
2422  "Time" => strtotime($Date." ".$Time),
2423  "Background" => ($Back == "B") ? TRUE : FALSE,
2424  "Level" => $ErrorAbbrevs[$Level],
2425  "Message" => $Msg,
2426  );
2427  }
2428 
2429  # return entries to caller
2430  return $Entries;
2431  }
2432 
2437  const LOGLVL_TRACE = 6;
2442  const LOGLVL_DEBUG = 5;
2448  const LOGLVL_INFO = 4;
2453  const LOGLVL_WARNING = 3;
2459  const LOGLVL_ERROR = 2;
2464  const LOGLVL_FATAL = 1;
2465 
2470 
2471  /*@)*/ /* Logging */
2472 
2473 
2474  # ---- Event Handling ----------------------------------------------------
2475  /*@(*/
2477 
2487  const EVENTTYPE_CHAIN = 2;
2493  const EVENTTYPE_FIRST = 3;
2501  const EVENTTYPE_NAMED = 4;
2502 
2504  const ORDER_FIRST = 1;
2506  const ORDER_MIDDLE = 2;
2508  const ORDER_LAST = 3;
2509 
2518  public function RegisterEvent($EventsOrEventName, $EventType = NULL)
2519  {
2520  # convert parameters to array if not already in that form
2521  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2522  : array($EventsOrEventName => $EventType);
2523 
2524  # for each event
2525  foreach ($Events as $Name => $Type)
2526  {
2527  # store event information
2528  $this->RegisteredEvents[$Name]["Type"] = $Type;
2529  $this->RegisteredEvents[$Name]["Hooks"] = array();
2530  }
2531  }
2532 
2539  public function IsRegisteredEvent($EventName)
2540  {
2541  return array_key_exists($EventName, $this->RegisteredEvents)
2542  ? TRUE : FALSE;
2543  }
2544 
2551  public function IsHookedEvent($EventName)
2552  {
2553  # the event isn't hooked to if it isn't even registered
2554  if (!$this->IsRegisteredEvent($EventName))
2555  {
2556  return FALSE;
2557  }
2558 
2559  # return TRUE if there is at least one callback hooked to the event
2560  return count($this->RegisteredEvents[$EventName]["Hooks"]) > 0;
2561  }
2562 
2576  public function HookEvent(
2577  $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2578  {
2579  # convert parameters to array if not already in that form
2580  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2581  : array($EventsOrEventName => $Callback);
2582 
2583  # for each event
2584  $Success = TRUE;
2585  foreach ($Events as $EventName => $EventCallback)
2586  {
2587  # if callback is valid
2588  if (is_callable($EventCallback))
2589  {
2590  # if this is a periodic event we process internally
2591  if (isset($this->PeriodicEvents[$EventName]))
2592  {
2593  # process event now
2594  $this->ProcessPeriodicEvent($EventName, $EventCallback);
2595  }
2596  # if specified event has been registered
2597  elseif (isset($this->RegisteredEvents[$EventName]))
2598  {
2599  # add callback for event
2600  $this->RegisteredEvents[$EventName]["Hooks"][]
2601  = array("Callback" => $EventCallback, "Order" => $Order);
2602 
2603  # sort callbacks by order
2604  if (count($this->RegisteredEvents[$EventName]["Hooks"]) > 1)
2605  {
2606  usort($this->RegisteredEvents[$EventName]["Hooks"],
2607  function ($A, $B) {
2608  return StdLib::SortCompare(
2609  $A["Order"], $B["Order"]);
2610  });
2611  }
2612  }
2613  else
2614  {
2615  $Success = FALSE;
2616  }
2617  }
2618  else
2619  {
2620  $Success = FALSE;
2621  }
2622  }
2623 
2624  # report to caller whether all callbacks were hooked
2625  return $Success;
2626  }
2627 
2641  public function UnhookEvent(
2642  $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2643  {
2644  # convert parameters to array if not already in that form
2645  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2646  : array($EventsOrEventName => $Callback);
2647 
2648  # for each event
2649  $UnhookCount = 0;
2650  foreach ($Events as $EventName => $EventCallback)
2651  {
2652  # if this event has been registered and hooked
2653  if (isset($this->RegisteredEvents[$EventName])
2654  && count($this->RegisteredEvents[$EventName]))
2655  {
2656  # if this callback has been hooked for this event
2657  $CallbackData = array("Callback" => $EventCallback, "Order" => $Order);
2658  if (in_array($CallbackData,
2659  $this->RegisteredEvents[$EventName]["Hooks"]))
2660  {
2661  # unhook callback
2662  $HookIndex = array_search($CallbackData,
2663  $this->RegisteredEvents[$EventName]["Hooks"]);
2664  unset($this->RegisteredEvents[$EventName]["Hooks"][$HookIndex]);
2665  $UnhookCount++;
2666  }
2667  }
2668  }
2669 
2670  # report number of callbacks unhooked to caller
2671  return $UnhookCount;
2672  }
2673 
2684  public function SignalEvent($EventName, $Parameters = NULL)
2685  {
2686  $ReturnValue = NULL;
2687 
2688  # if event has been registered
2689  if (isset($this->RegisteredEvents[$EventName]))
2690  {
2691  # set up default return value (if not NULL)
2692  switch ($this->RegisteredEvents[$EventName]["Type"])
2693  {
2694  case self::EVENTTYPE_CHAIN:
2695  $ReturnValue = $Parameters;
2696  break;
2697 
2698  case self::EVENTTYPE_NAMED:
2699  $ReturnValue = array();
2700  break;
2701  }
2702 
2703  # for each callback for this event
2704  foreach ($this->RegisteredEvents[$EventName]["Hooks"] as $Hook)
2705  {
2706  # invoke callback
2707  $Callback = $Hook["Callback"];
2708  $Result = ($Parameters !== NULL)
2709  ? call_user_func_array($Callback, $Parameters)
2710  : call_user_func($Callback);
2711 
2712  # process return value based on event type
2713  switch ($this->RegisteredEvents[$EventName]["Type"])
2714  {
2715  case self::EVENTTYPE_CHAIN:
2716  if ($Result !== NULL)
2717  {
2718  foreach ($Parameters as $Index => $Value)
2719  {
2720  if (array_key_exists($Index, $Result))
2721  {
2722  $Parameters[$Index] = $Result[$Index];
2723  }
2724  }
2725  $ReturnValue = $Parameters;
2726  }
2727  break;
2728 
2729  case self::EVENTTYPE_FIRST:
2730  if ($Result !== NULL)
2731  {
2732  $ReturnValue = $Result;
2733  break 2;
2734  }
2735  break;
2736 
2737  case self::EVENTTYPE_NAMED:
2738  $CallbackName = is_array($Callback)
2739  ? (is_object($Callback[0])
2740  ? get_class($Callback[0])
2741  : $Callback[0])."::".$Callback[1]
2742  : $Callback;
2743  $ReturnValue[$CallbackName] = $Result;
2744  break;
2745 
2746  default:
2747  break;
2748  }
2749  }
2750  }
2751  else
2752  {
2753  $this->LogError(self::LOGLVL_WARNING,
2754  "Unregistered event (".$EventName.") signaled by "
2755  .StdLib::GetMyCaller().".");
2756  }
2757 
2758  # return value if any to caller
2759  return $ReturnValue;
2760  }
2761 
2767  public function IsStaticOnlyEvent($EventName)
2768  {
2769  return isset($this->PeriodicEvents[$EventName]) ? TRUE : FALSE;
2770  }
2771 
2782  public function EventWillNextRunAt($EventName, $Callback)
2783  {
2784  # if event is not a periodic event report failure to caller
2785  if (!array_key_exists($EventName, $this->EventPeriods)) { return FALSE; }
2786 
2787  # retrieve last execution time for event if available
2788  $Signature = self::GetCallbackSignature($Callback);
2789  $LastRunTime = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
2790  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
2791 
2792  # if event was not found report failure to caller
2793  if ($LastRunTime === NULL) { return FALSE; }
2794 
2795  # calculate next run time based on event period
2796  $NextRunTime = strtotime($LastRunTime) + $this->EventPeriods[$EventName];
2797 
2798  # report next run time to caller
2799  return $NextRunTime;
2800  }
2801 
2817  public function GetKnownPeriodicEvents()
2818  {
2819  # retrieve last execution times
2820  $this->DB->Query("SELECT * FROM PeriodicEvents");
2821  $LastRunTimes = $this->DB->FetchColumn("LastRunAt", "Signature");
2822 
2823  # for each known event
2824  $Events = array();
2825  foreach ($this->KnownPeriodicEvents as $Signature => $Info)
2826  {
2827  # if last run time for event is available
2828  if (array_key_exists($Signature, $LastRunTimes))
2829  {
2830  # calculate next run time for event
2831  $LastRun = strtotime($LastRunTimes[$Signature]);
2832  $NextRun = $LastRun + $this->EventPeriods[$Info["Period"]];
2833  if ($Info["Period"] == "EVENT_PERIODIC") { $LastRun = FALSE; }
2834  }
2835  else
2836  {
2837  # set info to indicate run times are not known
2838  $LastRun = FALSE;
2839  $NextRun = FALSE;
2840  }
2841 
2842  # add event info to list
2843  $Events[$Signature] = $Info;
2844  $Events[$Signature]["LastRun"] = $LastRun;
2845  $Events[$Signature]["NextRun"] = $NextRun;
2846  $Events[$Signature]["Parameters"] = NULL;
2847  }
2848 
2849  # return list of known events to caller
2850  return $Events;
2851  }
2852 
2859  public static function RunPeriodicEvent(
2860  $EventName, $Callback, $Parameters)
2861  {
2862  static $DB;
2863  if (!isset($DB)) { $DB = new Database(); }
2864 
2865  # run event
2866  $ReturnVal = call_user_func_array($Callback, $Parameters);
2867 
2868  # if event is already in database
2869  $Signature = self::GetCallbackSignature($Callback);
2870  if ($DB->Query("SELECT COUNT(*) AS EventCount FROM PeriodicEvents"
2871  ." WHERE Signature = '".addslashes($Signature)."'", "EventCount"))
2872  {
2873  # update last run time for event
2874  $DB->Query("UPDATE PeriodicEvents SET LastRunAt = "
2875  .(($EventName == "EVENT_PERIODIC")
2876  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
2877  : "NOW()")
2878  ." WHERE Signature = '".addslashes($Signature)."'");
2879  }
2880  else
2881  {
2882  # add last run time for event to database
2883  $DB->Query("INSERT INTO PeriodicEvents (Signature, LastRunAt) VALUES "
2884  ."('".addslashes($Signature)."', "
2885  .(($EventName == "EVENT_PERIODIC")
2886  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
2887  : "NOW()").")");
2888  }
2889  }
2890 
2891  /*@)*/ /* Event Handling */
2892 
2893 
2894  # ---- Task Management ---------------------------------------------------
2895  /*@(*/
2897 
2899  const PRIORITY_HIGH = 1;
2901  const PRIORITY_MEDIUM = 2;
2903  const PRIORITY_LOW = 3;
2906 
2911  public function &TaskMgr()
2912  {
2913  return $this->TaskMgr;
2914  }
2915 
2929  public function QueueTask($Callback, $Parameters = NULL,
2930  $Priority = self::PRIORITY_LOW, $Description = "")
2931  {
2932  call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
2933  }
2934 
2953  public function QueueUniqueTask($Callback, $Parameters = NULL,
2954  $Priority = self::PRIORITY_LOW, $Description = "")
2955  {
2956  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
2957  }
2958 
2968  public function TaskIsInQueue($Callback, $Parameters = NULL)
2969  {
2970  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
2971  }
2972 
2978  public function GetTaskQueueSize($Priority = NULL)
2979  {
2980  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
2981  }
2982 
2990  public function GetQueuedTaskList($Count = 100, $Offset = 0)
2991  {
2992  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
2993  }
2994 
3008  public function GetQueuedTaskCount($Callback = NULL,
3009  $Parameters = NULL, $Priority = NULL, $Description = NULL)
3010  {
3011  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3012  }
3013 
3021  public function GetRunningTaskList($Count = 100, $Offset = 0)
3022  {
3023  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3024  }
3025 
3033  public function GetOrphanedTaskList($Count = 100, $Offset = 0)
3034  {
3035  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3036  }
3037 
3042  public function GetOrphanedTaskCount()
3043  {
3044  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3045  }
3046 
3052  public function ReQueueOrphanedTask($TaskId, $NewPriority = NULL)
3053  {
3054  call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3055  }
3056 
3063  public function RequeueCurrentTask($NewValue = TRUE)
3064  {
3065  call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3066  }
3067 
3073  public function DeleteTask($TaskId)
3074  {
3075  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3076  }
3077 
3085  public function GetTask($TaskId)
3086  {
3087  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3088  }
3089 
3100  public function TaskExecutionEnabled(
3101  $NewValue = DB_NOVALUE, $Persistent = FALSE)
3102  {
3103  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3104  }
3105 
3114  public function MaxTasks($NewValue = DB_NOVALUE, $Persistent = FALSE)
3115  {
3116  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3117  }
3118 
3126  public static function GetTaskCallbackSynopsis($TaskInfo)
3127  {
3128  return call_user_func_array(["AFTaskManager", __FUNCTION__], func_get_args());
3129  }
3130 
3137  {
3138  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3139  }
3140 
3150  public function GetNextHigherBackgroundPriority($Priority = NULL)
3151  {
3152  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3153  }
3154 
3164  public function GetNextLowerBackgroundPriority($Priority = NULL)
3165  {
3166  return call_user_func_array([$this->TaskMgr, __FUNCTION__], func_get_args());
3167  }
3168 
3169  /*@)*/ /* Task Management */
3170 
3171 
3172  # ---- Clean URL Support -------------------------------------------------
3173  /*@(*/
3175 
3202  public function AddCleanUrl($Pattern, $Page,
3203  $GetVars = NULL, $Template = NULL)
3204  {
3205  # save clean URL mapping parameters
3206  $this->CleanUrlMappings[] = array(
3207  "Pattern" => $Pattern,
3208  "Page" => $Page,
3209  "GetVars" => $GetVars,
3210  "AddedBy" => StdLib::GetCallerInfo(),
3211  );
3212 
3213  # if replacement template specified
3214  if ($Template !== NULL)
3215  {
3216  # if GET parameters specified
3217  if (count($GetVars))
3218  {
3219  # retrieve all possible permutations of GET parameters
3220  $GetPerms = StdLib::ArrayPermutations(array_keys($GetVars));
3221 
3222  # for each permutation of GET parameters
3223  foreach ($GetPerms as $VarPermutation)
3224  {
3225  # construct search pattern for permutation
3226  $SearchPattern = "/href=([\"'])index\\.php\\?P=".$Page;
3227  $GetVarSegment = "";
3228  foreach ($VarPermutation as $GetVar)
3229  {
3230  if (preg_match("%\\\$[0-9]+%", $GetVars[$GetVar]))
3231  {
3232  $GetVarSegment .= "&amp;".$GetVar."=((?:(?!\\1)[^&])+)";
3233  }
3234  else
3235  {
3236  $GetVarSegment .= "&amp;".$GetVar."=".$GetVars[$GetVar];
3237  }
3238  }
3239  $SearchPattern .= $GetVarSegment."\\1/i";
3240 
3241  # if template is actually a callback
3242  if (is_callable($Template))
3243  {
3244  # add pattern to HTML output mod callbacks list
3245  $this->OutputModificationCallbacks[] = array(
3246  "Pattern" => $Pattern,
3247  "Page" => $Page,
3248  "SearchPattern" => $SearchPattern,
3249  "Callback" => $Template,
3250  );
3251  }
3252  else
3253  {
3254  # construct replacement string for permutation
3255  $Replacement = $Template;
3256  $Index = 2;
3257  foreach ($VarPermutation as $GetVar)
3258  {
3259  $Replacement = str_replace(
3260  "\$".$GetVar, "\$".$Index, $Replacement);
3261  $Index++;
3262  }
3263  $Replacement = "href=\"".$Replacement."\"";
3264 
3265  # add pattern to HTML output modifications list
3266  $this->OutputModificationPatterns[] = $SearchPattern;
3267  $this->OutputModificationReplacements[] = $Replacement;
3268  }
3269  }
3270  }
3271  else
3272  {
3273  # construct search pattern
3274  $SearchPattern = "/href=\"index\\.php\\?P=".$Page."\"/i";
3275 
3276  # if template is actually a callback
3277  if (is_callable($Template))
3278  {
3279  # add pattern to HTML output mod callbacks list
3280  $this->OutputModificationCallbacks[] = array(
3281  "Pattern" => $Pattern,
3282  "Page" => $Page,
3283  "SearchPattern" => $SearchPattern,
3284  "Callback" => $Template,
3285  );
3286  }
3287  else
3288  {
3289  # add simple pattern to HTML output modifications list
3290  $this->OutputModificationPatterns[] = $SearchPattern;
3291  $this->OutputModificationReplacements[] = "href=\"".$Template."\"";
3292  }
3293  }
3294  }
3295  }
3296 
3302  public function CleanUrlIsMapped($Path)
3303  {
3304  foreach ($this->CleanUrlMappings as $Info)
3305  {
3306  if (preg_match($Info["Pattern"], $Path))
3307  {
3308  return TRUE;
3309  }
3310  }
3311  return FALSE;
3312  }
3313 
3323  public function GetCleanUrlForPath($Path)
3324  {
3325  # the search patterns and callbacks require a specific format
3326  $Format = "href=\"".str_replace("&", "&amp;", $Path)."\"";
3327  $Search = $Format;
3328 
3329  # perform any regular expression replacements on the search string
3330  $Search = preg_replace($this->OutputModificationPatterns,
3331  $this->OutputModificationReplacements, $Search);
3332 
3333  # only run the callbacks if a replacement hasn't already been performed
3334  if ($Search == $Format)
3335  {
3336  # perform any callback replacements on the search string
3337  foreach ($this->OutputModificationCallbacks as $Info)
3338  {
3339  # make the information available to the callback
3340  $this->OutputModificationCallbackInfo = $Info;
3341 
3342  # execute the callback
3343  $Search = preg_replace_callback($Info["SearchPattern"],
3344  array($this, "OutputModificationCallbackShell"),
3345  $Search);
3346  }
3347  }
3348 
3349  # return the path untouched if no replacements were performed
3350  if ($Search == $Format)
3351  {
3352  return $Path;
3353  }
3354 
3355  # remove the bits added to the search string to get it recognized by
3356  # the replacement expressions and callbacks
3357  $Result = substr($Search, 6, -1);
3358 
3359  return $Result;
3360  }
3361 
3368  public function GetUncleanUrlForPath($Path)
3369  {
3370  # for each clean URL mapping
3371  foreach ($this->CleanUrlMappings as $Info)
3372  {
3373  # if current path matches the clean URL pattern
3374  if (preg_match($Info["Pattern"], $Path, $Matches))
3375  {
3376  # the GET parameters for the URL, starting with the page name
3377  $GetVars = array("P" => $Info["Page"]);
3378 
3379  # if additional $_GET variables specified for clean URL
3380  if ($Info["GetVars"] !== NULL)
3381  {
3382  # for each $_GET variable specified for clean URL
3383  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
3384  {
3385  # start with template for variable value
3386  $Value = $VarTemplate;
3387 
3388  # for each subpattern matched in current URL
3389  foreach ($Matches as $Index => $Match)
3390  {
3391  # if not first (whole) match
3392  if ($Index > 0)
3393  {
3394  # make any substitutions in template
3395  $Value = str_replace("$".$Index, $Match, $Value);
3396  }
3397  }
3398 
3399  # add the GET variable
3400  $GetVars[$VarName] = $Value;
3401  }
3402  }
3403 
3404  # return the unclean URL
3405  return "index.php?" . http_build_query($GetVars);
3406  }
3407  }
3408 
3409  # return the path unchanged
3410  return $Path;
3411  }
3412 
3418  public function GetCleanUrl()
3419  {
3420  return $this->GetCleanUrlForPath($this->GetUncleanUrl());
3421  }
3422 
3427  public function GetUncleanUrl()
3428  {
3429  $GetVars = array("P" => $this->GetPageName()) + $_GET;
3430  return "index.php?" . http_build_query($GetVars);
3431  }
3432 
3440  public function GetCleanUrlList()
3441  {
3442  return $this->CleanUrlMappings;
3443  }
3444 
3457  public function AddPrefixForAlternateDomain($Domain, $Prefix)
3458  {
3459  $this->AlternateDomainPrefixes[$Domain] = $Prefix;
3460  }
3461 
3462 
3467  public function GetAlternateDomains()
3468  {
3469  return array_keys($this->AlternateDomainPrefixes);
3470  }
3471 
3478  public function GetPrefixForAlternateDomain($Domain)
3479  {
3480  return isset($this->AlternateDomainPrefixes[$Domain]) ?
3481  $this->AlternateDomainPrefixes[$Domain] : NULL;
3482  }
3483 
3484  /*@)*/ /* Clean URL Support */
3485 
3486  # ---- Server Environment ------------------------------------------------
3487  /*@(*/
3489 
3495  public static function SessionLifetime($NewValue = NULL)
3496  {
3497  if ($NewValue !== NULL)
3498  {
3499  self::$SessionLifetime = $NewValue;
3500  }
3501  return self::$SessionLifetime;
3502  }
3503 
3509  public static function HtaccessSupport()
3510  {
3511  return isset($_SERVER["HTACCESS_SUPPORT"])
3512  || isset($_SERVER["REDIRECT_HTACCESS_SUPPORT"]);
3513  }
3514 
3521  public static function UrlFingerprintingRewriteSupport()
3522  {
3523  return isset($_SERVER["URL_FINGERPRINTING_SUPPORT"])
3524  || isset($_SERVER["REDIRECT_URL_FINGERPRINTING_SUPPORT"]);
3525  }
3526 
3533  public static function ScssRewriteSupport()
3534  {
3535  return isset($_SERVER["SCSS_REWRITE_SUPPORT"])
3536  || isset($_SERVER["REDIRECT_SCSS_REWRITE_SUPPORT"]);
3537  }
3538 
3545  public static function JsMinRewriteSupport()
3546  {
3547  return isset($_SERVER["JSMIN_REWRITE_SUPPORT"])
3548  || isset($_SERVER["REDIRECT_JSMIN_REWRITE_SUPPORT"]);
3549  }
3550 
3558  public static function RootUrl()
3559  {
3560  # return override value if one is set
3561  if (self::$RootUrlOverride !== NULL)
3562  {
3563  return self::$RootUrlOverride;
3564  }
3565 
3566  # determine scheme name
3567  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
3568 
3569  # if HTTP_HOST is preferred or SERVER_NAME points to localhost
3570  # and HTTP_HOST is set
3571  if ((self::$PreferHttpHost || ($_SERVER["SERVER_NAME"] == "127.0.0.1"))
3572  && isset($_SERVER["HTTP_HOST"]))
3573  {
3574  # use HTTP_HOST for domain name
3575  $DomainName = $_SERVER["HTTP_HOST"];
3576  }
3577  else
3578  {
3579  # use SERVER_NAME for domain name
3580  $DomainName = $_SERVER["SERVER_NAME"];
3581  }
3582 
3583  # build URL root and return to caller
3584  return $Protocol."://".$DomainName;
3585  }
3586 
3601  public static function RootUrlOverride($NewValue = self::NOVALUE)
3602  {
3603  if ($NewValue !== self::NOVALUE)
3604  {
3605  self::$RootUrlOverride = strlen(trim($NewValue)) ? $NewValue : NULL;
3606  }
3607  return self::$RootUrlOverride;
3608  }
3609 
3619  public static function BaseUrl()
3620  {
3621  $BaseUrl = self::RootUrl().dirname($_SERVER["SCRIPT_NAME"]);
3622  if (substr($BaseUrl, -1) != "/") { $BaseUrl .= "/"; }
3623  return $BaseUrl;
3624  }
3625 
3633  public static function FullUrl()
3634  {
3635  return self::RootUrl().$_SERVER["REQUEST_URI"];
3636  }
3637 
3648  public static function PreferHttpHost($NewValue = NULL)
3649  {
3650  if ($NewValue !== NULL)
3651  {
3652  self::$PreferHttpHost = ($NewValue ? TRUE : FALSE);
3653  }
3654  return self::$PreferHttpHost;
3655  }
3656 
3661  public static function BasePath()
3662  {
3663  $BasePath = dirname($_SERVER["SCRIPT_NAME"]);
3664 
3665  if (substr($BasePath, -1) != "/")
3666  {
3667  $BasePath .= "/";
3668  }
3669 
3670  return $BasePath;
3671  }
3672 
3678  public static function GetScriptUrl()
3679  {
3680  if (array_key_exists("SCRIPT_URL", $_SERVER))
3681  {
3682  return $_SERVER["SCRIPT_URL"];
3683  }
3684  elseif (array_key_exists("REQUEST_URI", $_SERVER))
3685  {
3686  $Pieces = parse_url($_SERVER["REQUEST_URI"]);
3687  return isset($Pieces["path"]) ? $Pieces["path"] : NULL;
3688  }
3689  elseif (array_key_exists("REDIRECT_URL", $_SERVER))
3690  {
3691  return $_SERVER["REDIRECT_URL"];
3692  }
3693  else
3694  {
3695  return NULL;
3696  }
3697  }
3698 
3707  public static function WasUrlRewritten($ScriptName="index.php")
3708  {
3709  # needed to get the path of the URL minus the query and fragment pieces
3710  $Components = parse_url(self::GetScriptUrl());
3711 
3712  # if parsing was successful and a path is set
3713  if (is_array($Components) && isset($Components["path"]))
3714  {
3715  $BasePath = self::BasePath();
3716  $Path = $Components["path"];
3717 
3718  # the URL was rewritten if the path isn't the base path, i.e., the
3719  # home page, and the file in the URL isn't the script generating the
3720  # page
3721  if ($BasePath != $Path && basename($Path) != $ScriptName)
3722  {
3723  return TRUE;
3724  }
3725  }
3726 
3727  # the URL wasn't rewritten
3728  return FALSE;
3729  }
3730 
3740  public static function ReachedViaAjax($NewSetting = NULL)
3741  {
3742  if ($NewSetting !== NULL)
3743  {
3744  self::$IsAjaxPageLoad = $NewSetting;
3745  }
3746 
3747  if (isset(self::$IsAjaxPageLoad))
3748  {
3749  return self::$IsAjaxPageLoad;
3750  }
3751  elseif (isset($_SERVER["HTTP_X_REQUESTED_WITH"])
3752  && (strtolower($_SERVER["HTTP_X_REQUESTED_WITH"])
3753  == "xmlhttprequest"))
3754  {
3755  return TRUE;
3756  }
3757  else
3758  {
3759  return FALSE;
3760  }
3761  }
3762 
3768  public static function GetFreeMemory()
3769  {
3770  return self::GetPhpMemoryLimit() - memory_get_usage(TRUE);
3771  }
3772 
3778  public static function GetPercentFreeMemory()
3779  {
3780  return (self::GetFreeMemory() / self::GetPhpMemoryLimit()) * 100;
3781  }
3782 
3788  public static function GetPhpMemoryLimit()
3789  {
3790  return self::ConvertPhpIniSizeToBytes(
3791  ini_get("memory_limit"));
3792  }
3793 
3800  public static function GetPhpMaxUploadSize()
3801  {
3802  return min(
3803  self::ConvertPhpIniSizeToBytes(
3804  ini_get("post_max_size")),
3805  self::ConvertPhpIniSizeToBytes(
3806  ini_get("upload_max_filesize")));
3807  }
3808 
3821  public function MaxExecutionTime($NewValue = NULL, $Persistent = FALSE)
3822  {
3823  if ($NewValue !== NULL)
3824  {
3825  $NewValue = max($NewValue, 5);
3826  ini_set("max_execution_time", $NewValue);
3827  set_time_limit($NewValue - $this->GetElapsedExecutionTime());
3828  $this->UpdateSetting("MaxExecTime", $NewValue, $Persistent);
3829  }
3830  return ini_get("max_execution_time");
3831  }
3832 
3843  $NewValue = DB_NOVALUE, $Persistent = FALSE)
3844  {
3845  if (($NewValue !== DB_NOVALUE) && !$this->IsRunningInBackground())
3846  {
3847  Database::SlowQueryThreshold($NewValue);
3848  }
3849  return $this->UpdateSetting("DbSlowQueryThresholdForeground", $NewValue);
3850  }
3851 
3862  $NewValue = DB_NOVALUE, $Persistent = FALSE)
3863  {
3864  if (($NewValue !== DB_NOVALUE) && !$this->IsRunningInBackground())
3865  {
3866  Database::SlowQueryThreshold($NewValue);
3867  }
3868  return $this->UpdateSetting("DbSlowQueryThresholdBackground", $NewValue);
3869  }
3870 
3873 
3874  /*@)*/ /* Server Environment */
3875 
3876 
3877  # ---- Utility -----------------------------------------------------------
3878  /*@(*/
3880 
3892  public function DownloadFile($FilePath, $FileName = NULL,
3893  $MimeType = NULL)
3894  {
3895  # check that file is readable
3896  if (!is_readable($FilePath))
3897  {
3898  return FALSE;
3899  }
3900 
3901  # if file name was not supplied
3902  if ($FileName === NULL)
3903  {
3904  # extract file name from path
3905  $FileName = basename($FilePath);
3906  }
3907 
3908  # if MIME type was not supplied
3909  if ($MimeType === NULL)
3910  {
3911  # attempt to determine MIME type
3912  $FInfoHandle = finfo_open(FILEINFO_MIME);
3913  if ($FInfoHandle)
3914  {
3915  $FInfoMime = finfo_file($FInfoHandle, $FilePath);
3916  finfo_close($FInfoHandle);
3917  if ($FInfoMime)
3918  {
3919  $MimeType = $FInfoMime;
3920  }
3921  }
3922 
3923  # use default if unable to determine MIME type
3924  if ($MimeType === NULL)
3925  {
3926  $MimeType = "application/octet-stream";
3927  }
3928  }
3929 
3930  # list of mime types where we allow the browser to decide on
3931  # how to display the item by omitting the Content-Disposition
3932  # header
3933  $InlineTypes = [
3934  "image/gif",
3935  "image/jpeg",
3936  "image/png",
3937  "application/pdf",
3938  ];
3939 
3940  # set headers to download file
3941  header("Content-Type: ".$MimeType);
3942  header("Content-Length: ".filesize($FilePath));
3943  if ($this->CleanUrlRewritePerformed &&
3944  !in_array($MimeType, $InlineTypes))
3945  {
3946  header('Content-Disposition: attachment; filename="'.$FileName.'"');
3947  }
3948 
3949  # make sure that apache does not attempt to compress file
3950  apache_setenv('no-gzip', '1');
3951 
3952  # send file to user, but unbuffered to avoid memory issues
3953  $this->AddUnbufferedCallback(function ($File)
3954  {
3955  $BlockSize = 512000;
3956 
3957  $Handle = @fopen($File, "rb");
3958  if ($Handle === FALSE)
3959  {
3960  return;
3961  }
3962 
3963  # (close out session, making it read-only, so that session file
3964  # lock is released and others are not potentially hanging
3965  # waiting for it while the download completes)
3966  session_write_close();
3967 
3968  while (!feof($Handle))
3969  {
3970  print fread($Handle, $BlockSize);
3971  flush();
3972  }
3973 
3974  fclose($Handle);
3975  }, array($FilePath));
3976 
3977  # prevent HTML output that might interfere with download
3978  $this->SuppressHTMLOutput();
3979 
3980  # set flag to indicate not to log a slow page load in case client
3981  # connection delays PHP execution because of header
3982  $this->DoNotLogSlowPageLoad = TRUE;
3983 
3984  # report no errors found to caller
3985  return TRUE;
3986  }
3987 
4000  public function GetLock($LockName, $Wait = TRUE)
4001  {
4002  # assume we will not get a lock
4003  $GotLock = FALSE;
4004 
4005  # clear out any stale locks
4006  static $CleanupHasBeenDone = FALSE;
4007  if (!$CleanupHasBeenDone)
4008  {
4009  # (margin for clearing stale locks is twice the known
4010  # maximum PHP execution time, because the max time
4011  # techinically does not include external operations
4012  # like database queries)
4013  $ClearLocksObtainedBefore = date(StdLib::SQL_DATE_FORMAT,
4014  (time() - ($this->MaxExecutionTime() * 2)));
4015  $this->DB->Query("DELETE FROM AF_Locks WHERE"
4016  ." ObtainedAt < '".$ClearLocksObtainedBefore."' AND"
4017  ." LockName = '".addslashes($LockName)."'");
4018  }
4019 
4020  do
4021  {
4022  # lock database table so nobody else can try to get a lock
4023  $this->DB->Query("LOCK TABLES AF_Locks WRITE");
4024 
4025  # look for lock with specified name
4026  $FoundCount = $this->DB->Query("SELECT COUNT(*) AS FoundCount"
4027  ." FROM AF_Locks WHERE LockName = '"
4028  .addslashes($LockName)."'", "FoundCount");
4029  $LockFound = ($FoundCount > 0) ? TRUE : FALSE;
4030 
4031  # if lock found
4032  if ($LockFound)
4033  {
4034  # unlock database tables
4035  $this->DB->Query("UNLOCK TABLES");
4036 
4037  # if blocking was requested
4038  if ($Wait)
4039  {
4040  # wait to give someone else a chance to release lock
4041  sleep(2);
4042  }
4043  }
4044  // @codingStandardsIgnoreStart
4045  // (because phpcs does not correctly handle do-while loops)
4046  # while lock was found and blocking was requested
4047  } while ($LockFound && $Wait);
4048  // @codingStandardsIgnoreEnd
4049 
4050  # if lock was not found
4051  if (!$LockFound)
4052  {
4053  # get our lock
4054  $this->DB->Query("INSERT INTO AF_Locks (LockName) VALUES ('"
4055  .addslashes($LockName)."')");
4056  $GotLock = TRUE;
4057 
4058  # unlock database tables
4059  $this->DB->Query("UNLOCK TABLES");
4060  }
4061 
4062  # report to caller whether lock was obtained
4063  return $GotLock;
4064  }
4065 
4073  public function ReleaseLock($LockName)
4074  {
4075  # release any existing locks
4076  $this->DB->Query("DELETE FROM AF_Locks WHERE LockName = '"
4077  .addslashes($LockName)."'");
4078 
4079  # report to caller whether existing lock was released
4080  return $this->DB->NumRowsAffected() ? TRUE : FALSE;
4081  }
4082 
4101  public function BeginAjaxResponse(
4102  $ResponseType = "JSON",
4103  $CloseSession = TRUE)
4104  {
4105  switch ($ResponseType)
4106  {
4107  case "JSON":
4108  $this->SuppressHTMLOutput();
4109  header("Content-Type: application/json; charset="
4110  .$GLOBALS["G_SysConfig"]->DefaultCharacterSet(), TRUE);
4111  break;
4112  case "XML":
4113  $this->SuppressHTMLOutput();
4114  header("Content-Type: application/xml; charset="
4115  .$GLOBALS["G_SysConfig"]->DefaultCharacterSet(), TRUE);
4116  break;
4117  case "HTML":
4118  break;
4119  default:
4120  throw new Exception(
4121  "Unsupported response type: ".$ResponseType);
4122  }
4123 
4125  self::$DefaultBrowserCacheExpiration);
4126 
4127  if ($CloseSession)
4128  {
4129  session_write_close();
4130  }
4131 
4132  }
4133 
4139  public function SetBrowserCacheExpirationTime($MaxAge)
4140  {
4141  # set headers to control caching
4142  header("Expires: ".gmdate("D, d M Y H:i:s \G\M\T", time()+$MaxAge));
4143  header("Cache-Control: private, max-age=".$MaxAge);
4144  header("Pragma:");
4145  }
4146 
4153  public static function ConvertPhpIniSizeToBytes($Size)
4154  {
4155  $Str = strtoupper($Size);
4156 
4157  # trim off 'B' suffix for KB/MB/GB
4158  if (substr($Str, -1) == "B")
4159  {
4160  $Str = substr($Str, 0, strlen($Str) - 1);
4161  }
4162 
4163  # pull out the numeric part of our setting
4164  $Val = intval($Str);
4165 
4166  # adjust it based on the units
4167  switch (substr($Str, -1))
4168  {
4169  case "G":
4170  $Val *= 1073741824;
4171  break;
4172 
4173  case "M":
4174  $Val *= 1048576;
4175  break;
4176 
4177  case "K":
4178  $Val *= 1024;
4179  break;
4180 
4181  default:
4182  break;
4183  }
4184 
4185  return $Val;
4186  }
4187 
4194  public function SessionInUse($InUse = NULL)
4195  {
4196  if ($InUse !== NULL)
4197  {
4198  $this->SessionInUse = $InUse;
4199  }
4200 
4201  return $this->SessionInUse;
4202  }
4203 
4204  /*@)*/ /* Utility */
4205 
4206 
4207  # ---- Backward Compatibility --------------------------------------------
4208  /*@(*/
4210 
4217  public function FindCommonTemplate($BaseName)
4218  {
4219  return $this->FindFile(
4220  $this->IncludeDirList, $BaseName, array("tpl", "html"));
4221  }
4222 
4223  /*@)*/ /* Backward Compatibility */
4224 
4225 
4226  # ---- PRIVATE INTERFACE -------------------------------------------------
4227 
4228  private $AdditionalRequiredUIFiles = array();
4229  private $AlternateDomainPrefixes = array();
4230  private $BrowserDetectFunc;
4231  private $CacheCurrentPage = TRUE;
4232  private $CleanUrlMappings = array();
4233  private $CleanUrlRewritePerformed = FALSE;
4234  private $ContextFilters = array(
4235  self::CONTEXT_START => TRUE,
4236  self::CONTEXT_PAGE => array("H_"),
4237  self::CONTEXT_COMMON => array("H_"),
4238  );
4239  private $CssUrlFingerprintPath;
4240  private $DB;
4241  private $DefaultPage = "Home";
4242  private $DoNotMinimizeList = array();
4243  private $DoNotLogSlowPageLoad = FALSE;
4244  private $EnvIncludes = array();
4245  private $ExecutionStartTime;
4246  private $FoundUIFiles = array();
4247  private $HtmlCharset = "UTF-8";
4248  private $InterfaceSettings = array();
4249  private $JSMinimizerJavaScriptPackerAvailable = FALSE;
4250  private $JSMinimizerJShrinkAvailable = TRUE;
4251  private $JumpToPage = NULL;
4252  private $JumpToPageDelay = 0;
4253  private $LogFileName = "local/logs/site.log";
4254  private $MetaTags = array();
4255  private $OutputModificationCallbackInfo;
4256  private $OutputModificationCallbacks = array();
4257  private $OutputModificationPatterns = array();
4258  private $OutputModificationReplacements = array();
4259  private $PageCacheTags = array();
4260  private $PageName;
4261  private $PostProcessingFuncs = array();
4262  private $RunningInBackground = FALSE;
4263  private $SavedContext;
4264  private $SaveTemplateLocationCache = FALSE;
4265  private $SessionStorage;
4266  private $SessionGcProbability;
4267  private $Settings;
4268  private $SuppressHTML = FALSE;
4269  private $SuppressStdPageStartAndEnd = FALSE;
4270  private $TaskMgr;
4271  private $TemplateLocationCache;
4272  private $TemplateLocationCacheInterval = 60; # in minutes
4273  private $TemplateLocationCacheExpiration;
4274  private $UnbufferedCallbacks = array();
4275  private $UniqueMetaTags = array();
4276  private $UrlFingerprintBlacklist = array();
4277  private $UseBaseTag = FALSE;
4278  private $SessionInUse = FALSE;
4279 
4280  private static $ActiveUI = "default";
4281  private static $AppName = "ScoutAF";
4282  private static $DefaultBrowserCacheExpiration = 30; # in seconds
4283  private static $DefaultUI = "default";
4284  private static $IsAjaxPageLoad;
4285  private static $JSMinCacheDir = "local/data/caches/JSMin";
4286  private static $NamespaceDirectories = array();
4287  private static $ObjectDirectories = array();
4288  private static $ObjectLocationCache;
4289  private static $ObjectLocationCacheInterval = 60;
4290  private static $ObjectLocationCacheExpiration;
4291  private static $PreferHttpHost = FALSE;
4292  private static $RootUrlOverride = NULL;
4293  private static $SaveObjectLocationCache = FALSE;
4294  private static $ScssCacheDir = "local/data/caches/SCSS";
4295  private static $SessionLifetime = 1440; # in seconds
4296  private static $SuppressSessionInitialization = FALSE;
4297  private static $UserInterfaceListCache = array();
4298  private static $UserInterfacePathsCache = array();
4299 
4300  # offset used to generate page cache tag IDs from numeric tags
4301  const PAGECACHETAGIDOFFSET = 100000;
4302 
4303  # minimum expired session garbage collection probability
4304  const MIN_GC_PROBABILITY = 0.01;
4305 
4310  private $NoTSR = FALSE;
4311 
4312  private $RegisteredEvents = array();
4313  private $KnownPeriodicEvents = array();
4314  private $PeriodicEvents = array(
4315  "EVENT_HOURLY" => self::EVENTTYPE_DEFAULT,
4316  "EVENT_DAILY" => self::EVENTTYPE_DEFAULT,
4317  "EVENT_WEEKLY" => self::EVENTTYPE_DEFAULT,
4318  "EVENT_MONTHLY" => self::EVENTTYPE_DEFAULT,
4319  "EVENT_PERIODIC" => self::EVENTTYPE_NAMED,
4320  );
4321  private $EventPeriods = array(
4322  "EVENT_HOURLY" => 3600,
4323  "EVENT_DAILY" => 86400,
4324  "EVENT_WEEKLY" => 604800,
4325  "EVENT_MONTHLY" => 2592000,
4326  "EVENT_PERIODIC" => 0,
4327  );
4328  private $UIEvents = array(
4329  "EVENT_PAGE_LOAD" => self::EVENTTYPE_DEFAULT,
4330  "EVENT_PHP_FILE_LOAD" => self::EVENTTYPE_CHAIN,
4331  "EVENT_PHP_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
4332  "EVENT_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
4333  "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
4334  "EVENT_PAGE_OUTPUT_FILTER" => self::EVENTTYPE_CHAIN,
4335  );
4336 
4341  private function LoadSettings()
4342  {
4343  # read settings in from database
4344  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
4345  $this->Settings = $this->DB->FetchRow();
4346 
4347  # if settings were not previously initialized
4348  if ($this->Settings === FALSE)
4349  {
4350  # initialize settings in database
4351  $this->DB->Query("INSERT INTO ApplicationFrameworkSettings"
4352  ." (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");
4353 
4354  # read new settings in from database
4355  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
4356  $this->Settings = $this->DB->FetchRow();
4357 
4358  # bail out if reloading new settings failed
4359  if ($this->Settings === FALSE)
4360  {
4361  throw new Exception(
4362  "Unable to load application framework settings.");
4363  }
4364  }
4365 
4366  # if base path was not previously set or we appear to have moved
4367  if (!array_key_exists("BasePath", $this->Settings)
4368  || (!strlen($this->Settings["BasePath"]))
4369  || (!array_key_exists("BasePathCheck", $this->Settings))
4370  || (__FILE__ != $this->Settings["BasePathCheck"]))
4371  {
4372  # attempt to extract base path from Apache .htaccess file
4373  if (is_readable(".htaccess"))
4374  {
4375  $Lines = file(".htaccess");
4376  foreach ($Lines as $Line)
4377  {
4378  if (preg_match("/\\s*RewriteBase\\s+/", $Line))
4379  {
4380  $Pieces = preg_split(
4381  "/\\s+/", $Line, NULL, PREG_SPLIT_NO_EMPTY);
4382  $BasePath = $Pieces[1];
4383  }
4384  }
4385  }
4386 
4387  # if base path was found
4388  if (isset($BasePath))
4389  {
4390  # save base path locally
4391  $this->Settings["BasePath"] = $BasePath;
4392 
4393  # save base path to database
4394  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
4395  ." SET BasePath = '".addslashes($BasePath)."'"
4396  .", BasePathCheck = '".addslashes(__FILE__)."'");
4397  }
4398  }
4399 
4400  # retrieve template location cache
4401  $this->TemplateLocationCache = unserialize(
4402  $this->Settings["TemplateLocationCache"]);
4403  $this->TemplateLocationCacheInterval =
4404  $this->Settings["TemplateLocationCacheInterval"];
4405  $this->TemplateLocationCacheExpiration =
4406  strtotime($this->Settings["TemplateLocationCacheExpiration"]);
4407 
4408  # if template location cache looks invalid or has expired
4409  $CurrentTime = time();
4410  if (!is_array($this->TemplateLocationCache)
4411  || !count($this->TemplateLocationCache)
4412  || ($CurrentTime >= $this->TemplateLocationCacheExpiration))
4413  {
4414  # clear cache and reset cache expiration
4415  $this->TemplateLocationCache = array();
4416  $this->TemplateLocationCacheExpiration =
4417  $CurrentTime + ($this->TemplateLocationCacheInterval * 60);
4418  $this->SaveTemplateLocationCache = TRUE;
4419  }
4420 
4421  # retrieve object location cache
4422  self::$ObjectLocationCache =
4423  unserialize($this->Settings["ObjectLocationCache"]);
4424  self::$ObjectLocationCacheInterval =
4425  $this->Settings["ObjectLocationCacheInterval"];
4426  self::$ObjectLocationCacheExpiration =
4427  strtotime($this->Settings["ObjectLocationCacheExpiration"]);
4428 
4429  # if object location cache looks invalid or has expired
4430  if (!is_array(self::$ObjectLocationCache)
4431  || !count(self::$ObjectLocationCache)
4432  || ($CurrentTime >= self::$ObjectLocationCacheExpiration))
4433  {
4434  # clear cache and reset cache expiration
4435  self::$ObjectLocationCache = array();
4436  self::$ObjectLocationCacheExpiration =
4437  $CurrentTime + (self::$ObjectLocationCacheInterval * 60);
4438  self::$SaveObjectLocationCache = TRUE;
4439  }
4440  }
4441 
4448  private function RewriteCleanUrls($PageName)
4449  {
4450  # if URL rewriting is supported by the server
4451  if ($this->HtaccessSupport())
4452  {
4453  # retrieve current URL and remove base path if present
4454  $Url = $this->GetPageLocation();
4455 
4456  # for each clean URL mapping
4457  foreach ($this->CleanUrlMappings as $Info)
4458  {
4459  # if current URL matches clean URL pattern
4460  if (preg_match($Info["Pattern"], $Url, $Matches))
4461  {
4462  # set new page
4463  $PageName = $Info["Page"];
4464 
4465  # if $_GET variables specified for clean URL
4466  if ($Info["GetVars"] !== NULL)
4467  {
4468  # for each $_GET variable specified for clean URL
4469  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
4470  {
4471  # start with template for variable value
4472  $Value = $VarTemplate;
4473 
4474  # for each subpattern matched in current URL
4475  foreach ($Matches as $Index => $Match)
4476  {
4477  # if not first (whole) match
4478  if ($Index > 0)
4479  {
4480  # make any substitutions in template
4481  $Value = str_replace("$".$Index, $Match, $Value);
4482  }
4483  }
4484 
4485  # set $_GET variable
4486  $_GET[$VarName] = $Value;
4487  }
4488  }
4489 
4490  # set flag indicating clean URL mapped
4491  $this->CleanUrlRewritePerformed = TRUE;
4492 
4493  # stop looking for a mapping
4494  break;
4495  }
4496  }
4497  }
4498 
4499  # return (possibly) updated page name to caller
4500  return $PageName;
4501  }
4502 
4515  private function RewriteAlternateDomainUrls($Html)
4516  {
4517  # if we were loaded via an alternate domain, and we have a
4518  # RootUrlOverride configured to tell us which domain is the
4519  # primary, and if rewriting support is enabled, then we can
4520  # handle URL Rewriting
4521  if ($this->LoadedViaAlternateDomain() &&
4522  self::$RootUrlOverride !== NULL &&
4523  $this->HtaccessSupport())
4524  {
4525  # pull out the configured prefix for this domain
4526  $VHost = $_SERVER["SERVER_NAME"];
4527  $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
4528 
4529  # get the URL for the primary domain, including the base path
4530  # (usually the part between the host name and the PHP file name)
4531  $RootUrl = $this->RootUrl().self::BasePath();
4532 
4533  # and figure out what protcol we were using
4534  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
4535 
4536  # NB: preg_replace iterates through the configured
4537  # search/replacement pairs, such that the second one
4538  # runs after the first and so on
4539 
4540  # the first n-1 patterns below convert any relative
4541  # links in the generated HTML to absolute links using
4542  # our primary domain (e.g., for stylesheets, javascript,
4543  # images, etc)
4544 
4545  # the nth pattern looks for links that live within the
4546  # path subtree specified by our configured prefix on
4547  # our primary domain, then replaces them with equivalent
4548  # links on our secondary domain
4549 
4550  # for example, if our primary domain is
4551  # example.com/MySite and our secondary domain is
4552  # things.example.org/MySite with 'things' as the
4553  # configured prefix, then this last pattern will look
4554  # for example.com/MySite/things and replace it with
4555  # things.example.org/MySite
4556  $RelativePathPatterns = array(
4557  "%src=\"(?!http://|https://)%i",
4558  "%src='(?!http://|https://)%i",
4559  "%href=\"(?!http://|https://)%i",
4560  "%href='(?!http://|https://)%i",
4561  "%action=\"(?!http://|https://)%i",
4562  "%action='(?!http://|https://)%i",
4563  "%@import\s+url\(\"(?!http://|https://)%i",
4564  "%@import\s+url\('(?!http://|https://)%i",
4565  "%src:\s+url\(\"(?!http://|https://)%i",
4566  "%src:\s+url\('(?!http://|https://)%i",
4567  "%@import\s+\"(?!http://|https://)%i",
4568  "%@import\s+'(?!http://|https://)%i",
4569  "%".preg_quote($RootUrl.$ThisPrefix."/", "%")."%",
4570  );
4571  $RelativePathReplacements = array(
4572  "src=\"".$RootUrl,
4573  "src='".$RootUrl,
4574  "href=\"".$RootUrl,
4575  "href='".$RootUrl,
4576  "action=\"".$RootUrl,
4577  "action='".$RootUrl,
4578  "@import url(\"".$RootUrl,
4579  "@import url('".$RootUrl,
4580  "src: url(\"".$RootUrl,
4581  "src: url('".$RootUrl,
4582  "@import \"".$RootUrl,
4583  "@import '".$RootUrl,
4584  $Protocol."://".$VHost.self::BasePath(),
4585  );
4586 
4587  $NewHtml = preg_replace(
4588  $RelativePathPatterns,
4589  $RelativePathReplacements,
4590  $Html);
4591 
4592  # check to make sure relative path fixes didn't fail
4593  $Html = $this->CheckOutputModification(
4594  $Html, $NewHtml,
4595  "alternate domain substitutions");
4596  }
4597 
4598  return $Html;
4599  }
4600 
4605  private function LoadedViaAlternateDomain()
4606  {
4607  return (isset($_SERVER["SERVER_NAME"]) &&
4608  isset($this->AlternateDomainPrefixes[$_SERVER["SERVER_NAME"]])) ?
4609  TRUE : FALSE ;
4610  }
4611 
4630  private function FindFile($DirectoryList, $BaseName,
4631  $PossibleSuffixes = NULL, $PossiblePrefixes = NULL)
4632  {
4633  # generate template cache index for this page
4634  $CacheIndex = md5(serialize($DirectoryList))
4635  .self::$DefaultUI.self::$ActiveUI.$BaseName;
4636 
4637  # if caching is enabled and we have cached location
4638  if (($this->TemplateLocationCacheInterval > 0)
4639  && array_key_exists($CacheIndex,
4640  $this->TemplateLocationCache))
4641  {
4642  # use template location from cache
4643  $FoundFileName = $this->TemplateLocationCache[$CacheIndex];
4644  }
4645  else
4646  {
4647  # if suffixes specified and base name does not include suffix
4648  if ($PossibleSuffixes !== NULL
4649  && count($PossibleSuffixes)
4650  && !preg_match("/\.[a-zA-Z0-9]+$/", $BaseName))
4651  {
4652  # add versions of file names with suffixes to file name list
4653  $FileNames = array();
4654  foreach ($PossibleSuffixes as $Suffix)
4655  {
4656  $FileNames[] = $BaseName.".".$Suffix;
4657  }
4658  }
4659  else
4660  {
4661  # use base name as file name
4662  $FileNames = array($BaseName);
4663  }
4664 
4665  # if prefixes specified
4666  if ($PossiblePrefixes !== NULL && count($PossiblePrefixes))
4667  {
4668  # add versions of file names with prefixes to file name list
4669  $NewFileNames = array();
4670  foreach ($FileNames as $FileName)
4671  {
4672  foreach ($PossiblePrefixes as $Prefix)
4673  {
4674  $NewFileNames[] = $Prefix.$FileName;
4675  }
4676  }
4677  $FileNames = $NewFileNames;
4678  }
4679 
4680  # expand directory list to include variants
4681  $DirectoryList = $this->ExpandDirectoryList($DirectoryList);
4682 
4683  # for each possible location
4684  $FoundFileName = NULL;
4685  foreach ($DirectoryList as $Dir)
4686  {
4687  # for each possible file name
4688  foreach ($FileNames as $File)
4689  {
4690  # if template is found at location
4691  if (file_exists($Dir.$File))
4692  {
4693  # save full template file name and stop looking
4694  $FoundFileName = $Dir.$File;
4695  break 2;
4696  }
4697  }
4698  }
4699 
4700  # save location in cache
4701  $this->TemplateLocationCache[$CacheIndex]
4702  = $FoundFileName;
4703 
4704  # set flag indicating that cache should be saved
4705  $this->SaveTemplateLocationCache = TRUE;
4706  }
4707 
4708  # return full template file name to caller
4709  return $FoundFileName;
4710  }
4711 
4718  private function ExpandDirectoryList($DirList)
4719  {
4720  # generate lookup for supplied list
4721  $ExpandedListKey = md5(serialize($DirList)
4722  .self::$DefaultUI.self::$ActiveUI);
4723 
4724  # if we already have expanded version of supplied list
4725  if (isset($this->ExpandedDirectoryLists[$ExpandedListKey]))
4726  {
4727  # return expanded version to caller
4728  return $this->ExpandedDirectoryLists[$ExpandedListKey];
4729  }
4730 
4731  # for each directory in list
4732  $ExpDirList = array();
4733  foreach ($DirList as $Dir)
4734  {
4735  # if directory includes substitution keyword
4736  if ((strpos($Dir, "%DEFAULTUI%") !== FALSE)
4737  || (strpos($Dir, "%ACTIVEUI%") !== FALSE))
4738  {
4739  # start with empty new list segment
4740  $ExpDirListSegment = array();
4741 
4742  # use default values for initial parent
4743  $ParentInterface = array(self::$ActiveUI, self::$DefaultUI);
4744 
4745  do
4746  {
4747  # substitute in for keyword on parent
4748  $CurrDir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4749  $ParentInterface, $Dir);
4750 
4751  # add local version of parent directory to new list segment
4752  $ExpDirListSegment[] = "local/".$CurrDir;
4753 
4754  # add parent directory to new list segment
4755  $ExpDirListSegment[] = $CurrDir;
4756 
4757  # look for new parent interface
4758  $ParentInterface = $this->GetInterfaceSetting(
4759  $CurrDir, "ParentInterface");
4760 
4761  # repeat if parent is available
4762  } while (strlen($ParentInterface));
4763 
4764  # add new list segment to expanded list
4765  $ExpDirList = array_merge($ExpDirList, $ExpDirListSegment);
4766  }
4767  else
4768  {
4769  # add local version of directory to expanded list
4770  $ExpDirList[] = "local/".$Dir;
4771 
4772  # add directory to expanded list
4773  $ExpDirList[] = $Dir;
4774  }
4775  }
4776 
4777  # return expanded version to caller
4778  $this->ExpandedDirectoryLists[$ExpandedListKey] = $ExpDirList;
4779  return $this->ExpandedDirectoryLists[$ExpandedListKey];
4780  }
4781 
4790  private function GetInterfaceSetting(
4791  $InterfaceDir, $SettingName = NULL)
4792  {
4793  # extract canonical interface name and base interface directory
4794  preg_match("%(.*interface/)([^/]+)%", $InterfaceDir, $Matches);
4795  $InterfaceDir = (count($Matches) > 2)
4796  ? $Matches[1].$Matches[2] : $InterfaceDir;
4797  $InterfaceName = (count($Matches) > 2)
4798  ? $Matches[2] : "UNKNOWN";
4799 
4800  # if we do not have settings for interface
4801  if (!isset($this->InterfaceSettings[$InterfaceName]))
4802  {
4803  # load default values for settings
4804  $this->InterfaceSettings[$InterfaceName] = array(
4805  "Source" => "",
4806  );
4807  }
4808 
4809  # if directory takes precedence over existing settings source
4810  # ("takes precendence" == is more local == longer directory length)
4811  if (strlen($InterfaceDir)
4812  > strlen($this->InterfaceSettings[$InterfaceName]["Source"]))
4813  {
4814  # if settings file exists in directory
4815  $SettingsFile = $InterfaceDir."/interface.ini";
4816  if (is_readable($SettingsFile))
4817  {
4818  # read in values from file
4819  $NewSettings = parse_ini_file($SettingsFile);
4820 
4821  # merge in values with existing settings
4822  $this->InterfaceSettings[$InterfaceName] = array_merge(
4823  $this->InterfaceSettings[$InterfaceName], $NewSettings);
4824 
4825  # save new source of settings
4826  $this->InterfaceSettings[$InterfaceName]["Source"] = $InterfaceDir;
4827  }
4828  }
4829 
4830  # return interface settings to caller
4831  return $SettingName
4832  ? (isset($this->InterfaceSettings[$InterfaceName][$SettingName])
4833  ? $this->InterfaceSettings[$InterfaceName][$SettingName]
4834  : NULL)
4835  : $this->InterfaceSettings[$InterfaceName];
4836  }
4837 
4846  private function CompileScssFile($SrcFile)
4847  {
4848  # build path to CSS file
4849  $DstFile = self::$ScssCacheDir."/".dirname($SrcFile)
4850  ."/".basename($SrcFile);
4851  $DstFile = substr_replace($DstFile, "css", -4);
4852 
4853  # if SCSS file is newer than CSS file
4854  if (!file_exists($DstFile)
4855  || (filemtime($SrcFile) > filemtime($DstFile)))
4856  {
4857  # attempt to create CSS cache subdirectory if not present
4858  if (!is_dir(dirname($DstFile)))
4859  {
4860  @mkdir(dirname($DstFile), 0777, TRUE);
4861  }
4862 
4863  # if CSS cache directory and CSS file path appear writable
4864  static $CacheDirIsWritable;
4865  if (!isset($CacheDirIsWritable))
4866  { $CacheDirIsWritable = is_writable(self::$ScssCacheDir); }
4867  if (is_writable($DstFile)
4868  || (!file_exists($DstFile) && $CacheDirIsWritable))
4869  {
4870  # load SCSS and compile to CSS
4871  $ScssCode = file_get_contents($SrcFile);
4872  $ScssCompiler = new scssc();
4873  $ScssCompiler->setFormatter($this->GenerateCompactCss()
4874  ? "scss_formatter_compressed" : "scss_formatter");
4875  try
4876  {
4877  $CssCode = $ScssCompiler->compile($ScssCode);
4878 
4879  # add fingerprinting for URLs in CSS
4880  $this->CssUrlFingerprintPath = dirname($SrcFile);
4881  $CssCode = preg_replace_callback(
4882  "/url\((['\"]*)(.+)\.([a-z]+)(['\"]*)\)/",
4883  array($this, "CssUrlFingerprintInsertion"),
4884  $CssCode);
4885 
4886  # strip out comments from CSS (if requested)
4887  if ($this->GenerateCompactCss())
4888  {
4889  $CssCode = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!',
4890  '', $CssCode);
4891  }
4892 
4893  # write out CSS file
4894  file_put_contents($DstFile, $CssCode);
4895  }
4896  catch (Exception $Ex)
4897  {
4898  $this->LogError(self::LOGLVL_ERROR,
4899  "Error compiling SCSS file ".$SrcFile.": "
4900  .$Ex->getMessage());
4901  $DstFile = NULL;
4902  }
4903  }
4904  else
4905  {
4906  # log error and set CSS file path to indicate failure
4907  $this->LogError(self::LOGLVL_ERROR,
4908  "Unable to write out CSS file (compiled from SCSS) to "
4909  .$DstFile);
4910  $DstFile = NULL;
4911  }
4912  }
4913 
4914  # return CSS file path to caller
4915  return $DstFile;
4916  }
4917 
4925  private function MinimizeJavascriptFile($SrcFile)
4926  {
4927  # bail out if file is on exclusion list
4928  foreach ($this->DoNotMinimizeList as $DNMFile)
4929  {
4930  if (($SrcFile == $DNMFile) || (basename($SrcFile) == $DNMFile))
4931  {
4932  return NULL;
4933  }
4934  }
4935 
4936  # build path to minimized file
4937  $DstFile = self::$JSMinCacheDir."/".dirname($SrcFile)
4938  ."/".basename($SrcFile);
4939  $DstFile = substr_replace($DstFile, ".min", -3, 0);
4940 
4941  # if original file is newer than minimized file
4942  if (!file_exists($DstFile)
4943  || (filemtime($SrcFile) > filemtime($DstFile)))
4944  {
4945  # attempt to create cache subdirectory if not present
4946  if (!is_dir(dirname($DstFile)))
4947  {
4948  @mkdir(dirname($DstFile), 0777, TRUE);
4949  }
4950 
4951  # if cache directory and minimized file path appear writable
4952  static $CacheDirIsWritable;
4953  if (!isset($CacheDirIsWritable))
4954  { $CacheDirIsWritable = is_writable(self::$JSMinCacheDir); }
4955  if (is_writable($DstFile)
4956  || (!file_exists($DstFile) && $CacheDirIsWritable))
4957  {
4958  # load JavaScript code
4959  $Code = file_get_contents($SrcFile);
4960 
4961  # decide which minimizer to use
4962  if ($this->JSMinimizerJavaScriptPackerAvailable
4963  && $this->JSMinimizerJShrinkAvailable)
4964  {
4965  $Minimizer = (strlen($Code) < 5000)
4966  ? "JShrink" : "JavaScriptPacker";
4967  }
4968  elseif ($this->JSMinimizerJShrinkAvailable)
4969  {
4970  $Minimizer = "JShrink";
4971  }
4972  else
4973  {
4974  $Minimizer = "NONE";
4975  }
4976 
4977  # minimize code
4978  switch ($Minimizer)
4979  {
4980  case "JavaScriptMinimizer":
4981  $Packer = new JavaScriptPacker($Code, "Normal");
4982  $MinimizedCode = $Packer->pack();
4983  break;
4984 
4985  case "JShrink":
4986  try
4987  {
4988  $MinimizedCode = \JShrink\Minifier::minify($Code);
4989  }
4990  catch (Exception $Exception)
4991  {
4992  unset($MinimizedCode);
4993  $MinimizeError = $Exception->getMessage();
4994  }
4995  break;
4996  }
4997 
4998  # if minimization succeeded
4999  if (isset($MinimizedCode))
5000  {
5001  # write out minimized file
5002  file_put_contents($DstFile, $MinimizedCode);
5003  }
5004  else
5005  {
5006  # log error and set destination file path to indicate failure
5007  $ErrMsg = "Unable to minimize JavaScript file ".$SrcFile;
5008  if (isset($MinimizeError))
5009  {
5010  $ErrMsg .= " (".$MinimizeError.")";
5011  }
5012  $this->LogError(self::LOGLVL_ERROR, $ErrMsg);
5013  $DstFile = NULL;
5014  }
5015  }
5016  else
5017  {
5018  # log error and set destination file path to indicate failure
5019  $this->LogError(self::LOGLVL_ERROR,
5020  "Unable to write out minimized JavaScript to file ".$DstFile);
5021  $DstFile = NULL;
5022  }
5023  }
5024 
5025  # return CSS file path to caller
5026  return $DstFile;
5027  }
5028 
5036  private function CssUrlFingerprintInsertion($Matches)
5037  {
5038  # generate fingerprint string from CSS file modification time
5039  $FileName = realpath($this->CssUrlFingerprintPath."/".
5040  $Matches[2].".".$Matches[3]);
5041  $MTime = filemtime($FileName);
5042  $Fingerprint = sprintf("%06X", ($MTime % 0xFFFFFF));
5043 
5044  # build URL string with fingerprint and return it to caller
5045  return "url(".$Matches[1].$Matches[2].".".$Fingerprint
5046  .".".$Matches[3].$Matches[4].")";
5047  }
5048 
5056  private function GetRequiredFilesNotYetLoaded($PageContentFile)
5057  {
5058  # start out assuming no files required
5059  $RequiredFiles = array();
5060 
5061  # if page content file supplied
5062  if ($PageContentFile)
5063  {
5064  # if file containing list of required files is available
5065  $Path = dirname($PageContentFile);
5066  $RequireListFile = $Path."/REQUIRES";
5067  if (file_exists($RequireListFile))
5068  {
5069  # read in list of required files
5070  $RequestedFiles = file($RequireListFile);
5071 
5072  # for each line in required file list
5073  foreach ($RequestedFiles as $Line)
5074  {
5075  # if line is not a comment
5076  $Line = trim($Line);
5077  if (!preg_match("/^#/", $Line))
5078  {
5079  # if file has not already been loaded
5080  if (!in_array($Line, $this->FoundUIFiles))
5081  {
5082  # add to list of required files
5083  $RequiredFiles[$Line] = self::ORDER_MIDDLE;
5084  }
5085  }
5086  }
5087  }
5088  }
5089 
5090  # add in additional required files if any
5091  if (count($this->AdditionalRequiredUIFiles))
5092  {
5093  # make sure there are no duplicates
5094  $AdditionalRequiredUIFiles = array_unique(
5095  $this->AdditionalRequiredUIFiles);
5096 
5097  $RequiredFiles = array_merge(
5098  $RequiredFiles, $this->AdditionalRequiredUIFiles);
5099  }
5100 
5101  # return list of required files to caller
5102  return $RequiredFiles;
5103  }
5104 
5113  private function SubBrowserIntoFileNames($FileNames)
5114  {
5115  # if a browser detection function has been made available
5116  $UpdatedFileNames = array();
5117  if (is_callable($this->BrowserDetectFunc))
5118  {
5119  # call function to get browser list
5120  $Browsers = call_user_func($this->BrowserDetectFunc);
5121 
5122  # for each required file
5123  foreach ($FileNames as $FileName => $Value)
5124  {
5125  # if file name includes browser keyword
5126  if (preg_match("/%BROWSER%/", $FileName))
5127  {
5128  # for each browser
5129  foreach ($Browsers as $Browser)
5130  {
5131  # substitute in browser name and add to new file list
5132  $NewFileName = preg_replace(
5133  "/%BROWSER%/", $Browser, $FileName);
5134  $UpdatedFileNames[$NewFileName] = $Value;
5135  }
5136  }
5137  else
5138  {
5139  # add to new file list
5140  $UpdatedFileNames[$FileName] = $Value;
5141  }
5142  }
5143  }
5144  else
5145  {
5146  # filter out any files with browser keyword in their name
5147  foreach ($FileNames as $FileName => $Value)
5148  {
5149  if (!preg_match("/%BROWSER%/", $FileName))
5150  {
5151  $UpdatedFileNames[$FileName] = $Value;
5152  }
5153  }
5154  }
5155 
5156  return $UpdatedFileNames;
5157  }
5158 
5164  private function AddMetaTagsToPageOutput($PageOutput)
5165  {
5166  # start with unconditional (non-unique) tags
5167  $TagsToAdd = $this->MetaTags;
5168 
5169  # for each unique tag
5170  foreach ($this->UniqueMetaTags as $UniqueMetaTag)
5171  {
5172  $Attribs = $UniqueMetaTag["Attribs"];
5173  $UniqueAttribs = $UniqueMetaTag["UniqueAttribs"];
5174 
5175  # if no unique attributes specified
5176  if ($UniqueAttribs === NULL)
5177  {
5178  # use first attribute as unique attribute
5179  $UniqueAttribs = array_slice($Attribs, 0, 1);
5180  }
5181 
5182  # for each already-queued tag
5183  # (look for meta tags that match all attributes in
5184  # the current unique tag)
5185  foreach ($TagsToAdd as $TagAttribs)
5186  {
5187  # for each attribute in current unique tag
5188  # (look for attributes in the current unique tag that do
5189  # not match attributes in the this queued tag)
5190  foreach ($UniqueAttribs as $UniqueName => $UniqueValue)
5191  {
5192  # if unique attribute is not found in queued tag
5193  # or queued tag attribute has a different value
5194  if (!isset($TagAttribs[$UniqueName])
5195  || ($TagAttribs[$UniqueName] != $UniqueValue))
5196  {
5197  # skip to next queued tag
5198  # (some attribute in the current unique tag
5199  # was not found in the queued tag)
5200  continue 2;
5201  }
5202  }
5203 
5204  # skip to next unique tag
5205  # (all attributes in the current unique tag were found
5206  # in the queued tag, so do not queue this unique tag)
5207  continue 2;
5208  }
5209 
5210  # generate potential combinations of unique attributes
5211  $UniqueAttribNameCombos = StdLib::ArrayPermutations(
5212  array_keys($UniqueAttribs));
5213 
5214  # for each combination of unique attributes
5215  foreach ($UniqueAttribNameCombos as $UniqueNameCombo)
5216  {
5217  # for each attribute in combination
5218  $AttribStrings = array();
5219  foreach ($UniqueNameCombo as $UniqueName)
5220  {
5221  # add attrib/value string to list
5222  $AttribStrings[] = $UniqueName."=\""
5223  .htmlspecialchars($UniqueAttribs[$UniqueName])."\"";
5224  }
5225 
5226  # build search string from list of attribute pairs
5227  $SearchString = "<meta ".implode(" ", $AttribStrings);
5228 
5229  # if search string appears in page output
5230  if (strpos($PageOutput, $SearchString) !== FALSE)
5231  {
5232  # skip to next unique tag
5233  continue 2;
5234  }
5235 
5236  # repeat search with single quotes instead of double quotes
5237  $SearchString = strtr($SearchString, '"', "'");
5238  if (strpos($PageOutput, $SearchString) !== FALSE)
5239  {
5240  # skip to next unique tag
5241  continue 2;
5242  }
5243  }
5244 
5245  # unique tag was not found in page output, so add it to inserted tags
5246  $TagsToAdd[] = $Attribs;
5247  }
5248 
5249  # if there are meta tags to be added
5250  if (count($TagsToAdd))
5251  {
5252  # start with an empty segment
5253  $Section = "";
5254 
5255  # for each meta tag
5256  foreach ($TagsToAdd as $Attribs)
5257  {
5258  # assemble tag and add it to the segment
5259  $Section .= "<meta";
5260  foreach ($Attribs as $AttribName => $AttribValue)
5261  {
5262  $Section .= " ".$AttribName."=\""
5263  .htmlspecialchars(trim($AttribValue))."\"";
5264  }
5265  $Section .= " />\n";
5266  }
5267 
5268  # if standard page start and end have been disabled
5269  if ($this->SuppressStdPageStartAndEnd)
5270  {
5271  # add segment to beginning of page output
5272  $PageOutput = $Section.$PageOutput;
5273  }
5274  else
5275  {
5276  # insert segment at beginning of HTML head section in page output
5277  $PageOutput = preg_replace("#<head>#i",
5278  "<head>\n".$Section, $PageOutput, 1);
5279  }
5280  }
5281 
5282  # return (potentially modified) page output to caller
5283  return $PageOutput;
5284  }
5285 
5293  private function AddFileTagsToPageOutput($PageOutput, $Files)
5294  {
5295  # substitute browser name into names of required files as appropriate
5296  $Files = $this->SubBrowserIntoFileNames($Files);
5297 
5298  # initialize content sections
5299  $HeadContent = [
5300  self::ORDER_FIRST => "",
5301  self::ORDER_MIDDLE => "",
5302  self::ORDER_LAST => "",
5303  ];
5304  $BodyContent = [
5305  self::ORDER_FIRST => "",
5306  self::ORDER_MIDDLE => "",
5307  self::ORDER_LAST => "",
5308  ];
5309 
5310  # for each required file
5311  foreach ($Files as $File => $Order)
5312  {
5313  # locate specific file to use
5314  $FilePath = $this->GUIFile($File);
5315 
5316  # if file was found
5317  if ($FilePath)
5318  {
5319  # generate tag for file
5320  $Tag = $this->GetUIFileLoadingTag($FilePath);
5321 
5322  # add file to HTML output based on file type
5323  $FileType = $this->GetFileType($FilePath);
5324  switch ($FileType)
5325  {
5326  case self::FT_CSS:
5327  $HeadContent[$Order] .= $Tag."\n";
5328  break;
5329 
5330  case self::FT_JAVASCRIPT:
5331  $BodyContent[$Order] .= $Tag."\n";
5332  break;
5333  }
5334  }
5335  }
5336 
5337  # add content to head
5338  $Replacement = $HeadContent[self::ORDER_MIDDLE]
5339  .$HeadContent[self::ORDER_LAST];
5340  $UpdatedPageOutput = str_ireplace("</head>",
5341  $Replacement."</head>",
5342  $PageOutput, $ReplacementCount);
5343  # (if no </head> tag was found, just prepend tags to page content)
5344  if ($ReplacementCount == 0)
5345  {
5346  $PageOutput = $Replacement.$PageOutput;
5347  }
5348  # (else if multiple </head> tags found, only prepend tags to the first)
5349  elseif ($ReplacementCount > 1)
5350  {
5351  $PageOutput = preg_replace("#</head>#i",
5352  $Replacement."</head>",
5353  $PageOutput, 1);
5354  }
5355  else
5356  {
5357  $PageOutput = $UpdatedPageOutput;
5358  }
5359  $Replacement = $HeadContent[self::ORDER_FIRST];
5360  $UpdatedPageOutput = str_ireplace("<head>",
5361  "<head>\n".$Replacement,
5362  $PageOutput, $ReplacementCount);
5363  # (if no <head> tag was found, just prepend tags to page content)
5364  if ($ReplacementCount == 0)
5365  {
5366  $PageOutput = $Replacement.$PageOutput;
5367  }
5368  # (else if multiple <head> tags found, only append tags to the first)
5369  elseif ($ReplacementCount > 1)
5370  {
5371  $PageOutput = preg_replace("#<head>#i",
5372  "<head>\n".$Replacement,
5373  $PageOutput, 1);
5374  }
5375  else
5376  {
5377  $PageOutput = $UpdatedPageOutput;
5378  }
5379 
5380  # add content to body
5381  $Replacement = $BodyContent[self::ORDER_FIRST];
5382  $PageOutput = preg_replace("#<body([^>]*)>#i",
5383  "<body\\1>\n".$Replacement,
5384  $PageOutput, 1, $ReplacementCount);
5385  # (if no <body> tag was found, just append tags to page content)
5386  if ($ReplacementCount == 0)
5387  {
5388  $PageOutput = $PageOutput.$Replacement;
5389  }
5390  $Replacement = $BodyContent[self::ORDER_MIDDLE]
5391  .$BodyContent[self::ORDER_LAST];
5392  $UpdatedPageOutput = str_ireplace("</body>",
5393  $Replacement."\n</body>",
5394  $PageOutput, $ReplacementCount);
5395  # (if no </body> tag was found, just append tags to page content)
5396  if ($ReplacementCount == 0)
5397  {
5398  $PageOutput = $PageOutput.$Replacement;
5399  }
5400  # (else if multiple </body> tags found, only prepend tag to the first)
5401  elseif ($ReplacementCount > 1)
5402  {
5403  $PageOutput = preg_replace("#</body>#i",
5404  $Replacement."\n</body>",
5405  $PageOutput, 1);
5406  }
5407  else
5408  {
5409  $PageOutput = $UpdatedPageOutput;
5410  }
5411 
5412  return $PageOutput;
5413  }
5414 
5425  private function GetUIFileLoadingTag(
5426  $FileName, $AdditionalAttributes = NULL)
5427  {
5428  # pad additional attributes if supplied
5429  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
5430 
5431  # retrieve type of UI file
5432  $FileType = $this->GetFileType($FileName);
5433 
5434  # construct tag based on file type
5435  switch ($FileType)
5436  {
5437  case self::FT_CSS:
5438  $Tag = " <link rel=\"stylesheet\" type=\"text/css\""
5439  ." media=\"all\" href=\"".$FileName."\""
5440  .$AddAttribs." />\n";
5441  break;
5442 
5443  case self::FT_JAVASCRIPT:
5444  $Tag = " <script type=\"text/javascript\""
5445  ." src=\"".$FileName."\""
5446  .$AddAttribs."></script>\n";
5447  break;
5448 
5449  case self::FT_IMAGE:
5450  $Tag = "<img src=\"".$FileName."\"".$AddAttribs.">";
5451  break;
5452 
5453  default:
5454  $Tag = "";
5455  break;
5456  }
5457 
5458  # return constructed tag to caller
5459  return $Tag;
5460  }
5461 
5466  private function AutoloadObjects($ClassName)
5467  {
5468  # if caching is not turned off
5469  # and we have a cached location for class
5470  # and file at cached location is readable
5471  if ((self::$ObjectLocationCacheInterval > 0)
5472  && array_key_exists($ClassName,
5473  self::$ObjectLocationCache)
5474  && is_readable(self::$ObjectLocationCache[$ClassName]))
5475  {
5476  # use object location from cache
5477  require_once(self::$ObjectLocationCache[$ClassName]);
5478  }
5479  else
5480  {
5481  # for each possible object file directory
5482  static $FileLists;
5483  foreach (self::$ObjectDirectories as $Location => $Info)
5484  {
5485  # make any needed replacements in directory path
5486  $Location = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
5487  array(self::$ActiveUI, self::$DefaultUI), $Location);
5488 
5489  # if directory looks valid
5490  if (is_dir($Location))
5491  {
5492  # pass class name through callback (if supplied)
5493  $ClassFileName = $ClassName;
5494  if (is_callable($Info["Callback"]))
5495  {
5496  $ClassFileName = $Info["Callback"]($ClassFileName);
5497  }
5498 
5499  # strip off any namespace prefix
5500  foreach ($Info["NamespacePrefixes"] as $Prefix)
5501  {
5502  if (strpos($ClassFileName, $Prefix) === 0)
5503  {
5504  $ClassFileName = substr($ClassFileName, strlen($Prefix));
5505  break;
5506  }
5507  }
5508 
5509  # strip off any leading namespace separator
5510  if (strpos($ClassFileName, "\\") === 0)
5511  {
5512  $ClassFileName = substr($ClassFileName, 1);
5513  }
5514 
5515  # convert any namespace separators to directory separators
5516  $ClassFileName = str_replace("\\", "/", $ClassFileName);
5517 
5518  # finish building class file name
5519  $ClassFileName = $ClassFileName.".php";
5520 
5521  # read in directory contents if not already retrieved
5522  if (!isset($FileLists[$Location]))
5523  {
5524  $FileLists[$Location] = self::ReadDirectoryTree(
5525  $Location, '/^.+\.php$/i');
5526  }
5527 
5528  # for each file in target directory
5529  foreach ($FileLists[$Location] as $FileName)
5530  {
5531  # if file matches our target object file name
5532  if ($FileName == $ClassFileName)
5533  {
5534  # include object file
5535  require_once($Location.$FileName);
5536 
5537  # save location to cache
5538  self::$ObjectLocationCache[$ClassName]
5539  = $Location.$FileName;
5540 
5541  # set flag indicating that cache should be saved
5542  self::$SaveObjectLocationCache = TRUE;
5543 
5544  # stop looking
5545  break 2;
5546  }
5547  }
5548  }
5549  }
5550  }
5551  }
5552 
5560  private static function ReadDirectoryTree($Directory, $Pattern)
5561  {
5562  $CurrentDir = getcwd();
5563  chdir($Directory);
5564  $DirIter = new RecursiveDirectoryIterator(".");
5565  $IterIter = new RecursiveIteratorIterator($DirIter);
5566  $RegexResults = new RegexIterator($IterIter, $Pattern,
5567  RecursiveRegexIterator::GET_MATCH);
5568  $FileList = array();
5569  foreach ($RegexResults as $Result)
5570  {
5571  $FileList[] = substr($Result[0], 2);
5572  }
5573  chdir($CurrentDir);
5574  return $FileList;
5575  }
5576 
5581  private function LoadUIFunctions()
5582  {
5583  $Dirs = array(
5584  "local/interface/%ACTIVEUI%/include",
5585  "interface/%ACTIVEUI%/include",
5586  "local/interface/%DEFAULTUI%/include",
5587  "interface/%DEFAULTUI%/include",
5588  );
5589  foreach ($Dirs as $Dir)
5590  {
5591  $Dir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
5592  array(self::$ActiveUI, self::$DefaultUI), $Dir);
5593  if (is_dir($Dir))
5594  {
5595  $FileNames = scandir($Dir);
5596  foreach ($FileNames as $FileName)
5597  {
5598  if (preg_match("/^F-([A-Za-z0-9_]+)\.php/",
5599  $FileName, $Matches)
5600  || preg_match("/^F-([A-Za-z0-9_]+)\.html/",
5601  $FileName, $Matches))
5602  {
5603  if (!function_exists($Matches[1]))
5604  {
5605  include_once($Dir."/".$FileName);
5606  }
5607  }
5608  }
5609  }
5610  }
5611  }
5612 
5618  private function ProcessPeriodicEvent($EventName, $Callback)
5619  {
5620  # retrieve last execution time for event if available
5621  $Signature = self::GetCallbackSignature($Callback);
5622  $LastRun = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
5623  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
5624 
5625  # determine whether enough time has passed for event to execute
5626  $ShouldExecute = (($LastRun === NULL)
5627  || (time() > (strtotime($LastRun) + $this->EventPeriods[$EventName])))
5628  ? TRUE : FALSE;
5629 
5630  # if event should run
5631  if ($ShouldExecute)
5632  {
5633  # add event to task queue
5634  $WrapperCallback = array("ApplicationFramework", "RunPeriodicEvent");
5635  $WrapperParameters = array(
5636  $EventName, $Callback, array("LastRunAt" => $LastRun));
5637  $this->QueueUniqueTask($WrapperCallback, $WrapperParameters);
5638  }
5639 
5640  # add event to list of periodic events
5641  $this->KnownPeriodicEvents[$Signature] = array(
5642  "Period" => $EventName,
5643  "Callback" => $Callback,
5644  "Queued" => $ShouldExecute);
5645  }
5646 
5652  private static function GetCallbackSignature($Callback)
5653  {
5654  return !is_array($Callback) ? $Callback
5655  : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
5656  ."::".$Callback[1];
5657  }
5658 
5663  private function PrepForTSR()
5664  {
5665  # if HTML has been output and it's time to launch another task
5666  # (only TSR if HTML has been output because otherwise browsers
5667  # may misbehave after connection is closed)
5668  if ((PHP_SAPI != "cli")
5669  && ($this->JumpToPage || !$this->SuppressHTML)
5670  && !$this->LoadedViaAlternateDomain()
5671  && $this->TaskMgr->TaskExecutionEnabled()
5672  && $this->TaskMgr->GetTaskQueueSize())
5673  {
5674  # begin buffering output for TSR
5675  ob_start();
5676 
5677  # let caller know it is time to launch another task
5678  return TRUE;
5679  }
5680  else
5681  {
5682  # let caller know it is not time to launch another task
5683  return FALSE;
5684  }
5685  }
5686 
5691  private function LaunchTSR()
5692  {
5693  # set headers to close out connection to browser
5694  if (!$this->NoTSR)
5695  {
5696  ignore_user_abort(TRUE);
5697  header("Connection: close");
5698  header("Content-Length: ".ob_get_length());
5699  }
5700 
5701  # output buffered content
5702  while (ob_get_level()) { ob_end_flush(); }
5703  flush();
5704 
5705  # write out any outstanding data and end HTTP session
5706  session_write_close();
5707 
5708  # set flag indicating that we are now running in background
5709  $this->RunningInBackground = TRUE;
5710 
5711  # set database slow query threshold for background execution
5713  self::MIN_DB_SLOW_QUERY_THRESHOLD,
5714  self::DatabaseSlowQueryThresholdForBackground()));
5715 
5716  # handle garbage collection for session data
5717  if (isset($this->SessionStorage) &&
5718  (rand()/getrandmax()) <= $this->SessionGcProbability)
5719  {
5720  # determine when sessions will expire
5721  $ExpiredTime = strtotime("-". self::$SessionLifetime." seconds");
5722 
5723  # iterate over files in the session directory with a DirectoryIterator
5724  # NB: we cannot use scandir() here because it reads the
5725  # entire list of files into memory and may exceed the memory
5726  # limit for directories with very many files
5727  $DI = new DirectoryIterator($this->SessionStorage);
5728  while ($DI->valid())
5729  {
5730  if ((strpos($DI->getFilename(), "sess_") === 0) &&
5731  $DI->isFile() &&
5732  $DI->getCTime() < $ExpiredTime)
5733  {
5734  unlink($DI->getPathname());
5735  }
5736  $DI->next();
5737  }
5738  unset($DI);
5739  }
5740 
5741  # run qny queued tasks
5742  $this->TaskMgr->RunQueuedTasks();
5743  }
5744 
5750  public function OnCrash()
5751  {
5752  # attempt to remove any memory limits
5753  $FreeMemory = $this->GetFreeMemory();
5754  ini_set("memory_limit", -1);
5755 
5756  # if there is a background task currently running
5757  if (isset($this->RunningTask))
5758  {
5759  # add info about current page load
5760  $CrashInfo["ElapsedTime"] = $this->GetElapsedExecutionTime();
5761  $CrashInfo["FreeMemory"] = $FreeMemory;
5762  $CrashInfo["REMOTE_ADDR"] = $_SERVER["REMOTE_ADDR"];
5763  $CrashInfo["REQUEST_URI"] = $_SERVER["REQUEST_URI"];
5764  if (isset($_SERVER["REQUEST_TIME"]))
5765  {
5766  $CrashInfo["REQUEST_TIME"] = $_SERVER["REQUEST_TIME"];
5767  }
5768  if (isset($_SERVER["REMOTE_HOST"]))
5769  {
5770  $CrashInfo["REMOTE_HOST"] = $_SERVER["REMOTE_HOST"];
5771  }
5772 
5773  # add info about error that caused crash (if available)
5774  if (function_exists("error_get_last"))
5775  {
5776  $CrashInfo["LastError"] = error_get_last();
5777  }
5778 
5779  # add info about current output buffer contents (if available)
5780  if (ob_get_length() !== FALSE)
5781  {
5782  $CrashInfo["OutputBuffer"] = ob_get_contents();
5783  }
5784 
5785  # if backtrace info is available for the crash
5786  $Backtrace = debug_backtrace();
5787  if (count($Backtrace) > 1)
5788  {
5789  # discard the current context from the backtrace
5790  array_shift($Backtrace);
5791 
5792  # add the backtrace to the crash info
5793  $CrashInfo["Backtrace"] = $Backtrace;
5794  }
5795  # else if saved backtrace info is available
5796  elseif (isset($this->SavedContext))
5797  {
5798  # add the saved backtrace to the crash info
5799  $CrashInfo["Backtrace"] = $this->SavedContext;
5800  }
5801 
5802  # save crash info for currently running task
5803  $DB = new Database();
5804  $DB->Query("UPDATE RunningTasks SET CrashInfo = '"
5805  .addslashes(serialize($CrashInfo))
5806  ."' WHERE TaskId = ".intval($this->RunningTask["TaskId"]));
5807  }
5808 
5809  print("\n");
5810  return;
5811  }
5812 
5830  private function AddToDirList(
5831  $DirList, $Dir, $SearchLast, $SkipSlashCheck)
5832  {
5833  # convert incoming directory to array of directories (if needed)
5834  $Dirs = is_array($Dir) ? $Dir : array($Dir);
5835 
5836  # reverse array so directories are searched in specified order
5837  $Dirs = array_reverse($Dirs);
5838 
5839  # for each directory
5840  foreach ($Dirs as $Location)
5841  {
5842  # make sure directory includes trailing slash
5843  if (!$SkipSlashCheck)
5844  {
5845  $Location = $Location
5846  .((substr($Location, -1) != "/") ? "/" : "");
5847  }
5848 
5849  # remove directory from list if already present
5850  if (in_array($Location, $DirList))
5851  {
5852  $DirList = array_diff(
5853  $DirList, array($Location));
5854  }
5855 
5856  # add directory to list of directories
5857  if ($SearchLast)
5858  {
5859  array_push($DirList, $Location);
5860  }
5861  else
5862  {
5863  array_unshift($DirList, $Location);
5864  }
5865  }
5866 
5867  # return updated directory list to caller
5868  return $DirList;
5869  }
5870 
5877  private function OutputModificationCallbackShell($Matches)
5878  {
5879  # call previously-stored external function
5880  return call_user_func($this->OutputModificationCallbackInfo["Callback"],
5881  $Matches,
5882  $this->OutputModificationCallbackInfo["Pattern"],
5883  $this->OutputModificationCallbackInfo["Page"],
5884  $this->OutputModificationCallbackInfo["SearchPattern"]);
5885  }
5886 
5895  private function CheckOutputModification(
5896  $Original, $Modified, $ErrorInfo)
5897  {
5898  # if error was reported by regex engine
5899  if (preg_last_error() !== PREG_NO_ERROR)
5900  {
5901  # log error
5902  $this->LogError(self::LOGLVL_ERROR,
5903  "Error reported by regex engine when modifying output."
5904  ." (".$ErrorInfo.")");
5905 
5906  # use unmodified version of output
5907  $OutputToUse = $Original;
5908  }
5909  # else if modification reduced output by more than threshold
5910  elseif ((strlen(trim($Modified)) / strlen(trim($Original)))
5911  < self::OUTPUT_MODIFICATION_THRESHOLD)
5912  {
5913  # log error
5914  $this->LogError(self::LOGLVL_WARNING,
5915  "Content reduced below acceptable threshold while modifying output."
5916  ." (".$ErrorInfo.")");
5917 
5918  # use unmodified version of output
5919  $OutputToUse = $Original;
5920  }
5921  else
5922  {
5923  # use modified version of output
5924  $OutputToUse = $Modified;
5925  }
5926 
5927  # return output to use to caller
5928  return $OutputToUse;
5929  }
5930 
5933 
5943  private function UpdateSetting(
5944  $FieldName, $NewValue = DB_NOVALUE, $Persistent = TRUE)
5945  {
5946  static $LocalSettings;
5947  if ($NewValue !== DB_NOVALUE)
5948  {
5949  if ($Persistent)
5950  {
5951  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5952  "ApplicationFrameworkSettings",
5953  $FieldName, $NewValue, NULL, $this->Settings);
5954  }
5955  else
5956  {
5957  $LocalSettings[$FieldName] = $NewValue;
5958  }
5959  }
5960  elseif (!isset($LocalSettings[$FieldName]))
5961  {
5962  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5963  "ApplicationFrameworkSettings",
5964  $FieldName, $NewValue, NULL, $this->Settings);
5965  }
5966  return $LocalSettings[$FieldName];
5967  }
5968 
5978  private static function IncludeFile(
5979  $_AF_File, $_AF_ContextVars = array())
5980  {
5981  # set up context
5982  foreach ($_AF_ContextVars as $_AF_VarName => $_AF_VarValue)
5983  {
5984  $$_AF_VarName = $_AF_VarValue;
5985  }
5986  unset($_AF_VarName);
5987  unset($_AF_VarValue);
5988  unset($_AF_ContextVars);
5989 
5990  # add variables to context that we assume are always available
5991  $AF = $GLOBALS["AF"];
5992 
5993  # load file
5994  include($_AF_File);
5995 
5996  # return updated context
5997  $ContextVars = get_defined_vars();
5998  unset($ContextVars["_AF_File"]);
5999  return $ContextVars;
6000  }
6001 
6008  private function FilterContext($Context, $ContextVars)
6009  {
6010  # clear all variables if no setting for context is available
6011  # or setting is FALSE
6012  if (!isset($this->ContextFilters[$Context])
6013  || ($this->ContextFilters[$Context] == FALSE))
6014  {
6015  return array();
6016  }
6017  # keep all variables if setting for context is TRUE
6018  elseif ($this->ContextFilters[$Context] == TRUE)
6019  {
6020  return $ContextVars;
6021  }
6022  else
6023  {
6024  $Prefixes = $this->ContextFilters[$Context];
6025  $FilterFunc = function($VarName) use ($Prefixes) {
6026  foreach ($Prefixes as $Prefix)
6027  {
6028  if (substr($VarName, $Prefix) === 0)
6029  {
6030  return TRUE;
6031  }
6032  }
6033  return FALSE;
6034  };
6035  return array_filter(
6036  $ContextVars, $FilterFunc, ARRAY_FILTER_USE_KEY);
6037  }
6038  }
6039 
6041  private $InterfaceDirList = array(
6042  "interface/%ACTIVEUI%/",
6043  "interface/%DEFAULTUI%/",
6044  );
6049  private $IncludeDirList = array(
6050  "interface/%ACTIVEUI%/include/",
6051  "interface/%ACTIVEUI%/objects/",
6052  "interface/%DEFAULTUI%/include/",
6053  "interface/%DEFAULTUI%/objects/",
6054  );
6056  private $ImageDirList = array(
6057  "interface/%ACTIVEUI%/images/",
6058  "interface/%DEFAULTUI%/images/",
6059  );
6061  private $FunctionDirList = array(
6062  "interface/%ACTIVEUI%/include/",
6063  "interface/%DEFAULTUI%/include/",
6064  "include/",
6065  );
6066 
6067  private $ExpandedDirectoryLists = array();
6068 
6069  const NOVALUE = ".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";
6070 
6071 
6072  # ---- Page Caching (Internal Methods) -----------------------------------
6073 
6079  private function CheckForCachedPage($PageName)
6080  {
6081  # assume no cached page will be found
6082  $CachedPage = NULL;
6083 
6084  # if returning a cached page is allowed
6085  if ($this->CacheCurrentPage)
6086  {
6087  # get fingerprint for requested page
6088  $PageFingerprint = $this->GetPageFingerprint($PageName);
6089 
6090  # look for matching page in cache in database
6091  $this->DB->Query("SELECT * FROM AF_CachedPages"
6092  ." WHERE Fingerprint = '".addslashes($PageFingerprint)."'");
6093 
6094  # if matching page found
6095  if ($this->DB->NumRowsSelected())
6096  {
6097  # if cached page has expired
6098  $Row = $this->DB->FetchRow();
6099  $ExpirationTime = strtotime(
6100  "-".$this->PageCacheExpirationPeriod()." seconds");
6101  if (strtotime($Row["CachedAt"]) < $ExpirationTime)
6102  {
6103  # clear expired pages from cache
6104  $ExpirationTimestamp = date("Y-m-d H:i:s", $ExpirationTime);
6105  $this->DB->Query("DELETE CP, CPTI FROM AF_CachedPages CP,"
6106  ." AF_CachedPageTagInts CPTI"
6107  ." WHERE CP.CachedAt < '".$ExpirationTimestamp."'"
6108  ." AND CPTI.CacheId = CP.CacheId");
6109  $this->DB->Query("DELETE FROM AF_CachedPages "
6110  ." WHERE CachedAt < '".$ExpirationTimestamp."'");
6111  }
6112  else
6113  {
6114  # display cached page and exit
6115  $CachedPage = $Row["PageContent"];
6116  }
6117  }
6118  }
6119 
6120  # return any cached page found to caller
6121  return $CachedPage;
6122  }
6123 
6129  private function UpdatePageCache($PageName, $PageContent)
6130  {
6131  # if page caching is enabled and current page should be cached
6132  if ($this->PageCacheEnabled()
6133  && $this->CacheCurrentPage)
6134  {
6135  # if page content looks invalid
6136  if (strlen(trim(strip_tags($PageContent))) == 0)
6137  {
6138  # log error
6139  $LogMsg = "Page not cached because content was empty."
6140  ." (PAGE: ".$PageName.", URL: ".$this->FullUrl().")";
6141  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
6142  }
6143  else
6144  {
6145  # save page to cache
6146  $PageFingerprint = $this->GetPageFingerprint($PageName);
6147  $this->DB->Query("INSERT INTO AF_CachedPages"
6148  ." (Fingerprint, PageContent) VALUES"
6149  ." ('".$this->DB->EscapeString($PageFingerprint)."', '"
6150  .$this->DB->EscapeString($PageContent)."')");
6151  $CacheId = $this->DB->LastInsertId();
6152 
6153  # for each page cache tag that was added
6154  foreach ($this->PageCacheTags as $Tag => $Pages)
6155  {
6156  # if current page is in list for tag
6157  if (in_array("CURRENT", $Pages) || in_array($PageName, $Pages))
6158  {
6159  # look up tag ID
6160  $TagId = $this->GetPageCacheTagId($Tag);
6161 
6162  # mark current page as associated with tag
6163  $this->DB->Query("INSERT INTO AF_CachedPageTagInts"
6164  ." (TagId, CacheId) VALUES "
6165  ." (".intval($TagId).", ".intval($CacheId).")");
6166  }
6167  }
6168  }
6169  }
6170  }
6171 
6177  private function GetPageCacheTagId($Tag)
6178  {
6179  # if tag is a non-negative integer
6180  if (is_numeric($Tag) && ($Tag > 0) && (intval($Tag) == $Tag))
6181  {
6182  # generate ID
6183  $Id = self::PAGECACHETAGIDOFFSET + $Tag;
6184  }
6185  else
6186  {
6187  # look up ID in database
6188  $Id = $this->DB->Query("SELECT TagId FROM AF_CachedPageTags"
6189  ." WHERE Tag = '".addslashes($Tag)."'", "TagId");
6190 
6191  # if ID was not found
6192  if ($Id === NULL)
6193  {
6194  # add tag to database
6195  $this->DB->Query("INSERT INTO AF_CachedPageTags"
6196  ." SET Tag = '".addslashes($Tag)."'");
6197  $Id = $this->DB->LastInsertId();
6198  }
6199  }
6200 
6201  # return tag ID to caller
6202  return $Id;
6203  }
6204 
6210  private function GetPageFingerprint($PageName)
6211  {
6212  # only get the environmental fingerprint once so that it is consistent
6213  # between page construction start and end
6214  static $EnvFingerprint;
6215  if (!isset($EnvFingerprint))
6216  {
6217  $GetVars = $_GET;
6218  ksort($GetVars);
6219  $PostVars = $_POST;
6220  ksort($PostVars);
6221  $EnvData = json_encode($GetVars)
6222  .json_encode($PostVars)
6223  .$_SERVER["SERVER_NAME"];
6224  $EnvFingerprint = md5($EnvData);
6225  }
6226 
6227  # build page fingerprint and return it to caller
6228  return $PageName."-".$EnvFingerprint;
6229  }
6230 
6237  private function UpdateLastUsedTimeForActiveSessions()
6238  {
6239  if ($this->SessionInUse)
6240  {
6241  $_SESSION["AF_SessionLastUsed"] = date("Y-m-d H:i:s");
6242  }
6243  elseif (isset($_SESSION["AF_SessionLastUsed"]))
6244  {
6245  unset($_SESSION["AF_SessionLastUsed"]);
6246  }
6247  }
6248 
6256  private function AdjustEnvironmentForCgi()
6257  {
6258  # if it appears we are running via a CGI interpreter
6259  if (isset($_SERVER["ORIG_SCRIPT_NAME"]))
6260  {
6261  # for each server environment variable
6262  foreach ($_SERVER as $Key => $Value)
6263  {
6264  # if variable appears the result of using CGI
6265  if (strpos($Key, "REDIRECT_") === 0)
6266  {
6267  # if unmodified version of variable is not set
6268  $KeyWithoutPrefix = substr($Key, 9);
6269  if (!isset($_SERVER[$KeyWithoutPrefix]))
6270  {
6271  # set unmodified version of variable
6272  $_SERVER[$KeyWithoutPrefix] = $_SERVER[$Key];
6273  }
6274  }
6275  }
6276  }
6277  }
6278 }
const LOGLVL_ERROR
ERROR error logging level.
PageCacheEnabled($NewValue=DB_NOVALUE, $Persistent=FALSE)
Enable/disable page caching.
UnhookEvent($EventsOrEventName, $Callback=NULL, $Order=self::ORDER_MIDDLE)
Unhook one or more functions that were previously hooked to be called when the specified event is sig...
GetOrphanedTaskList($Count=100, $Offset=0)
Retrieve list of tasks currently orphaned.
SuppressHTMLOutput($NewSetting=TRUE)
Suppress loading of HTML files.
AddInterfaceDirectories($Dir, $SearchLast=FALSE, $SkipSlashCheck=FALSE)
Add additional directory(s) to be searched for user interface (HTML/TPL) files.
AddIncludeDirectories($Dir, $SearchLast=FALSE, $SkipSlashCheck=FALSE)
Add additional directory(s) to be searched for user interface include (CSS, JavaScript, common PHP, common HTML, etc) files.
static GetScriptUrl()
Retrieve SCRIPT_URL server value, pulling it from elsewhere if that variable isn&#39;t set...
const LOGLVL_INFO
INFO error logging level.
AddUnbufferedCallback($Callback, $Parameters=array())
Add a callback that will be executed after buffered content has been output and that won&#39;t have its o...
DoNotUrlFingerprint($Pattern)
Specify file or file name pattern to exclude from URL fingerprinting.
QueueUniqueTask($Callback, $Parameters=NULL, $Priority=self::PRIORITY_LOW, $Description="")
Add task to queue if not already in queue or currently running.
AddPrefixForAlternateDomain($Domain, $Prefix)
Add an alternate domain for the site which should map to a path tree under the main site URL...
DatabaseSlowQueryThresholdForBackground($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set threshold for when database queries are considered "slow" when running in the background...
static ConvertPhpIniSizeToBytes($Size)
Convert an abbreviated size from php.ini (e.g., 2g) to a number of bytes.
RequeueCurrentTask($NewValue=TRUE)
Set whether to requeue the currently-running background task when it completes.
const LOGLVL_FATAL
FATAL error logging level.
AddMetaTag($Attribs)
Add meta tag to page output.
MaxExecutionTime($NewValue=NULL, $Persistent=FALSE)
Get/set maximum PHP execution time.
AddPostProcessingCall($FunctionName, &$Arg1=self::NOVALUE, &$Arg2=self::NOVALUE, &$Arg3=self::NOVALUE, &$Arg4=self::NOVALUE, &$Arg5=self::NOVALUE, &$Arg6=self::NOVALUE, &$Arg7=self::NOVALUE, &$Arg8=self::NOVALUE, &$Arg9=self::NOVALUE)
Add function to be called after HTML has been loaded.
GetCleanUrlForPath($Path)
Get the clean URL mapped for a path.
const PRIORITY_LOW
Lower priority.
IsRunningInBackground()
Determine whether currently running inside a background task.
Abstraction for forum messages and resource comments.
Definition: Message.php:14
GetQueuedTaskList($Count=100, $Offset=0)
Retrieve list of tasks currently in queue.
LogFile($NewValue=NULL)
Get/set log file name.
GetCleanUrl()
Get the clean URL for the current page if one is available.
GetPageCachePageList()
Get list of cached pages.
const CONTEXT_END
File loading context: page end file.
static PreferHttpHost($NewValue=NULL)
Get/set whether to prefer $_SERVER["HTTP_HOST"] (if available) over $_SERVER["SERVER_NAME"] when dete...
ClearObjectLocationCache()
Clear object (class) file location cache.
GetLock($LockName, $Wait=TRUE)
Get an exclusive ("write") lock on the specified name.
static FullUrl()
Get current full URL, before any clean URL remapping and with any query string (e.g.
Top-level framework for web applications.
static BaseUrl()
Get current base URL (the part before index.php) (e.g.
GetOrphanedTaskCount()
Retrieve current number of orphaned tasks.
SQL database abstraction object with smart query caching.
Definition: Database.php:22
static ActiveUserInterface($UIName=NULL)
Get/set name of current active user interface.
SessionInUse($InUse=NULL)
Get/Set value of SessionInUse, which indicates if the current session is currently in use...
GetTaskQueueSize($Priority=NULL)
Retrieve current number of tasks in queue.
static SortCompare($A, $B)
Perform compare and return value appropriate for sort function callbacks.
Definition: StdLib.php:575
static RootUrlOverride($NewValue=self::NOVALUE)
Get/set root URL override.
GetNextLowerBackgroundPriority($Priority=NULL)
Get next lower possible background task priority.
static UrlFingerprintingRewriteSupport()
Determine if rewrite support for URL fingerprinting is available.
GetQueuedTaskCount($Callback=NULL, $Parameters=NULL, $Priority=NULL, $Description=NULL)
Get number of queued tasks that match supplied values.
DeleteTask($TaskId)
Remove task from task queues.
const LOGLVL_DEBUG
DEBUG error logging level.
static ReachedViaAjax($NewSetting=NULL)
Determine if we were reached via an AJAX-based (or other automated) page load.
const EVENTTYPE_NAMED
Named result event type.
const EVENTTYPE_FIRST
First response event type.
static WasUrlRewritten($ScriptName="index.php")
Determine if the URL was rewritten, i.e., the script is being accessed through a URL that isn&#39;t direc...
const EVENTTYPE_DEFAULT
Default event type.
GetPageUrl()
Get the full URL to the page.
static minify($js, $options=array())
Takes a string containing javascript and removes unneeded characters in order to shrink the code with...
Definition: Minifier.php:103
const CONTEXT_ENV
File loading context: environmental include files.
TemplateLocationCacheExpirationInterval($NewInterval=DB_NOVALUE, $Persistent=FALSE)
Get/set UI template location cache expiration period in minutes.
RequireUIFile($FileNames, $Order=self::ORDER_MIDDLE)
Add file to list of required UI files.
SetBrowserCacheExpirationTime($MaxAge)
Set headers to control client-side caching of data served to the browser in this page load (usually J...
IsStaticOnlyEvent($EventName)
Report whether specified event only allows static callbacks.
const CONTEXT_PAGE
File loading context: PHP page file (from "pages").
PageCacheExpirationPeriod($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set page cache expiration period in seconds.
IsRegisteredEvent($EventName)
Check if event has been registered (is available to be signaled).
ReleaseLock($LockName)
Release lock with specified name.
SignalEvent($EventName, $Parameters=NULL)
Signal that an event has occured.
static BasePath()
Get current base path (usually the part after the host name).
const LOGLVL_TRACE
TRACE error logging level.
const LOGLVL_WARNING
WARNING error logging level.
const PRIORITY_MEDIUM
Medium (default) priority.
LogError($Level, $Msg)
Write error message to log.
DatabaseSlowQueryThresholdForForeground($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set threshold for when database queries are considered "slow" when running in the foreground...
LogHighMemoryUsage($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set whether logging of high memory usage is enabled.
static SlowQueryThreshold($NewValue=NULL)
Get/set current threshold for what is considered a "slow" SQL query.
Definition: Database.php:1248
static ScssRewriteSupport()
Determine if SCSS rewrite support is available.
OnCrash()
Called automatically at program termination to ensure output is written out.
GetKnownPeriodicEvents()
Get list of known periodic events.
JavascriptMinimizationEnabled($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set whether the application framework will attempt to generate minimized JavaScript.
UseMinimizedJavascript($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set whether minimized JavaScript will be searched for and used if found.
const EVENTTYPE_CHAIN
Result chaining event type.
SlowPageLoadThreshold($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set how long a page load can take before it should be considered "slow" and may be logged...
AddMetaTagOnce($Attribs, $UniqueAttribs=NULL)
Add meta tag to page output if not already present.
ClearPageCacheForTag($Tag)
Clear all cached pages associated with specified tag.
DoNotCacheCurrentPage()
Prevent the current page from being cached.
SuppressStandardPageStartAndEnd($NewSetting=TRUE)
Suppress loading of standard page start and end files.
SCSS compiler written in PHP.
Definition: scssc.php:45
GetSecondsBeforeTimeout()
Get remaining available (PHP) execution time.
TaskIsInQueue($Callback, $Parameters=NULL)
Check if task is already in queue or currently running.
GetCurrentBackgroundPriority()
Determine current priority if running in background.
AddFunctionDirectories($Dir, $SearchLast=FALSE, $SkipSlashCheck=FALSE)
Add additional directory(s) to be searched for function ("F-") files.
GetAlternateDomains()
Get the list of configured alternate domains.
static ArrayPermutations($Items, $Perms=array())
Return all possible permutations of a given array.
Definition: StdLib.php:724
AddPageCacheTag($Tag, $Pages=NULL)
Add caching tag for current page or specified pages.
GetPageCacheInfo()
Get page cache information.
const ORDER_MIDDLE
Handle item after ORDER_FIRST and before ORDER_LAST items.
static GetPercentFreeMemory()
Get current percentage of free memory.
const MIN_DB_SLOW_QUERY_THRESHOLD
minimum threshold for what is considered a slow database query
const OUTPUT_MODIFICATION_THRESHOLD
Threshold below which page output modifications are considered to have failed.
RegisterEvent($EventsOrEventName, $EventType=NULL)
Register one or more events that may be signaled.
UrlFingerprintingEnabled($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set whether URL fingerprinting is enabled.
static JsMinRewriteSupport()
Determine if rewrite support for JavaScript minification is available.
DownloadFile($FilePath, $FileName=NULL, $MimeType=NULL)
Send specified file for download by user.
GetPageName()
Get name of page being loaded.
LogSlowPageLoads($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set whether logging of long page load times is enabled.
const DB_NOVALUE
Definition: Database.php:1784
CleanUrlIsMapped($Path)
Report whether clean URL has already been mapped.
HighMemoryUsageThreshold($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set what percentage of max memory (set via the memory_limit PHP configuration directive) a page l...
SetJumpToPage($Page, $Delay=0, $IsLiteral=FALSE)
Set URL of page to autoload after PHP page file is executed.
const CONTEXT_START
File loading context: page start file.
const CONTEXT_COMMON
File loading context: common HTML files.
const PRIORITY_HIGH
Highest priority.
static SuppressSessionInitialization($NewValue=NULL)
Get/set whether session initialization is intentionally suppressed.
LoadFunction($Callback)
Attempt to load code for function or method if not currently available.
GetNextHigherBackgroundPriority($Priority=NULL)
Get next higher possible background task priority.
GetRunningTaskList($Count=100, $Offset=0)
Retrieve list of tasks currently running.
FindCommonTemplate($BaseName)
Preserved for backward compatibility for use with code written prior to October 2012.
EventWillNextRunAt($EventName, $Callback)
Get date/time a periodic event will next run.
static SessionLifetime($NewValue=NULL)
Get/set session timeout in seconds.
static DefaultUserInterface($UIName=NULL)
Get/set name of current default user interface.
HookEvent($EventsOrEventName, $Callback=NULL, $Order=self::ORDER_MIDDLE)
Hook one or more functions to be called when the specified event is signaled.
ClearPageCache()
Clear all pages from page cache.
IsHookedEvent($EventName)
Check if an event is registered and is hooked to.
static GetFreeMemory()
Get current amount of free memory.
ObjectLocationCacheExpirationInterval($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set object file location cache expiration period in minutes.
const FT_JAVASCRIPT
JavaScript file type.
const CONTEXT_INTERFACE
File loading context: HTML interface file.
AddImageDirectories($Dir, $SearchLast=FALSE, $SkipSlashCheck=FALSE)
Add additional directory(s) to be searched for image files.
const FT_CSS
CSS file type.
HtmlCharset($NewSetting=NULL)
Get/set HTTP character encoding value.
const ORDER_FIRST
Handle item first (i.e.
const SQL_DATE_FORMAT
Format to feed to date() to get SQL-compatible date/time string.
Definition: StdLib.php:913
static GetPhpMaxUploadSize()
Get the maximum size for a file upload in bytes.
Task manager component of top-level framework for web applications.
IncludeUIFile($FileNames, $AdditionalAttributes=NULL)
Search UI directories for specified JavaScript or CSS file and print HTML tag to load file...
GetUncleanUrlForPath($Path)
Get the unclean URL for mapped for a path.
const FT_IMAGE
Image (GIF/JPG/PNG) file type.
ClearTemplateLocationCache()
Clear template location cache.
GetPrefixForAlternateDomain($Domain)
Get configured prefix for an alternate domain, if one exists.
static GetFileType($FileName)
Determine type of specified file based on the file name.
GetUncleanUrl()
Get the unclean URL for the current page.
DoNotMinimizeFile($File)
Specify file(s) to not attempt to minimize.
RecordContextInCaseOfCrash($BacktraceOptions=0, $BacktraceLimit=0)
Record the current execution context in case of crash.
JumpToPageIsSet()
Report whether a page to autoload has been set.
const ORDER_LAST
Handle item last (i.e.
LoggingLevel($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set logging level.
UseBaseTag($NewValue=NULL)
Get/set whether or not to use the "base" tag to ensure relative URL paths are correct.
static HtaccessSupport()
Determine if .htaccess files are enabled.
GetTask($TaskId)
Retrieve task info from queue (either running or queued tasks).
static GetPhpMemoryLimit()
Get PHP memory limit in bytes.
SetContextFilter($Context, $NewSetting)
Configure filtering of variables left in the execution environment for the next loaded file after a P...
LoadPage($PageName)
Load page PHP and HTML/TPL files.
GetUserInterfacePaths($FilterExp=NULL)
Get list of available user interfaces and the relative paths to the base directory for each interface...
AddEnvInclude($FileName)
Add file to be included to set up environment.
GetCleanUrlList()
Get list of all clean URLs currently added.
GetUserInterfaces($FilterExp=NULL)
Get list of available user interfaces and their labels.
ReQueueOrphanedTask($TaskId, $NewPriority=NULL)
Move orphaned task back into queue.
GetLogEntries($Limit=0)
Get log entries, in reverse chronological order.
GUIFile($FileName)
Search UI directories for specified image or CSS file and return name of correct file.
QueueTask($Callback, $Parameters=NULL, $Priority=self::PRIORITY_LOW, $Description="")
Add task to queue.
TaskExecutionEnabled($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set whether automatic task execution is enabled.
static GetTaskCallbackSynopsis($TaskInfo)
Get printable synopsis for task callback.
PUIFile($FileName)
Search UI directories for specified interface (image, CSS, JavaScript etc) file and print name of cor...
GenerateCompactCss($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set whether generating compact CSS (when compiling SCSS) is enabled.
const LOGFILE_MAX_LINE_LENGTH
Maximum length for a line in the log file.
& TaskMgr()
Access the task manager.
GetPageLocation()
Get the URL path to the page without the base path, if present.
ScssSupportEnabled($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set whether SCSS compilation support is enabled.
BeginAjaxResponse($ResponseType="JSON", $CloseSession=TRUE)
Begin an AJAX response, setting the necessary HTTP headers and optionally closing the PHP session...
MaxTasks($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set maximum number of tasks to have running simultaneously.
GetElapsedExecutionTime()
Get time elapsed since constructor was called.
static RunPeriodicEvent($EventName, $Callback, $Parameters)
Run periodic event and then save info needed to know when to run it again.
SetBrowserDetectionFunc($DetectionFunc)
Specify function to use to detect the web browser type.
static RootUrl()
Get portion of current URL through host name, with no trailing slash (e.g.
AddCleanUrl($Pattern, $Page, $GetVars=NULL, $Template=NULL)
Add clean URL mapping.
static GetCallerInfo($Element=NULL)
Get info about call to current function.
Definition: StdLib.php:26
const PRIORITY_BACKGROUND
Lowest priority.
LogMessage($Level, $Msg)
Write status message to log.
const FT_OTHER
File type other than CSS, image, or JavaScript.
static AddObjectDirectory($Dir, $NamespacePrefixes=array(), $Callback=NULL)
Add directory to be searched for object files when autoloading.