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  # make sure default time zone is set
31  # (using CST if nothing set because we have to use something
32  # and Scout is based in Madison, WI, which is in CST)
33  if ((ini_get("date.timezone") === NULL)
34  || !strlen(ini_get("date.timezone")))
35  {
36  ini_set("date.timezone", "America/Chicago");
37  }
38 
39  # save execution start time
40  $this->ExecutionStartTime = microtime(TRUE);
41 
42  # set up default object file search locations
43  self::AddObjectDirectory("local/interface/%ACTIVEUI%/objects");
44  self::AddObjectDirectory("interface/%ACTIVEUI%/objects");
45  self::AddObjectDirectory("local/interface/%DEFAULTUI%/objects");
46  self::AddObjectDirectory("interface/%DEFAULTUI%/objects");
47  self::AddObjectDirectory("local/objects");
48  self::AddObjectDirectory("objects");
49 
50  # set up object file autoloader
51  spl_autoload_register(array($this, "AutoloadObjects"));
52 
53  # set up function to output any buffered text in case of crash
54  register_shutdown_function(array($this, "OnCrash"));
55 
56  # if we were not invoked via command line interface
57  if (php_sapi_name() !== "cli")
58  {
59  # build cookie domain string
60  $SessionDomain = isset($_SERVER["SERVER_NAME"]) ? $_SERVER["SERVER_NAME"]
61  : isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"]
62  : php_uname("n");
63 
64  # include a leading period so that older browsers implementing
65  # rfc2109 do not reject our cookie
66  $SessionDomain = ".".$SessionDomain;
67 
68  # if it appears our session storage area is writable
69  if (is_writable(session_save_path()))
70  {
71  # store our session files in a subdirectory to avoid
72  # accidentally sharing sessions with other installations
73  # on the same domain
74  $SessionStorage = session_save_path()
75  ."/".self::$AppName."_".md5($SessionDomain.dirname(__FILE__));
76 
77  # create session storage subdirectory if not found
78  if (!is_dir($SessionStorage)) { mkdir($SessionStorage, 0700 ); }
79 
80  # if session storage subdirectory is writable
81  if (is_writable($SessionStorage))
82  {
83  # save parameters of our session storage as instance variables
84  # for later use
85  $this->SessionGcProbability =
86  ini_get("session.gc_probability") / ini_get("session.gc_divisor");
87  # require a gc probability of at least MIN_GC_PROBABILITY
88  if ($this->SessionGcProbability < self::MIN_GC_PROBABILITY)
89  {
90  $this->SessionGcProbability = self::MIN_GC_PROBABILITY;
91  }
92 
93  $this->SessionStorage = $SessionStorage;
94 
95  # set the new session storage location
96  session_save_path($SessionStorage);
97 
98  # disable PHP's garbage collection, as it does not handle
99  # subdirectories (instead, we'll do the cleanup as we run
100  # background tasks)
101  ini_set("session.gc_probability", 0);
102  }
103  }
104 
105  # set garbage collection max period to our session lifetime
106  ini_set("session.gc_maxlifetime", self::$SessionLifetime);
107 
108  # limit cookie to secure connection if we are running over same
109  $SecureCookie = isset($_SERVER["HTTPS"]) ? TRUE : FALSE;
110 
111  # Cookies lacking embedded dots are... fun.
112  # rfc2109 sec 4.3.2 says to reject them
113  # rfc2965 sec 3.3.2 says to reject them
114  # rfc6265 sec 4.1.2.3 says only that "public suffixes"
115  # should be rejected. They reference Mozilla's
116  # publicsuffix.org, which does not contain 'localhost'.
117  # However, empirically in early 2017 Firefox still rejects
118  # 'localhost'.
119  # Therefore, don't set a cookie domain if we're running on
120  # localhost to avoid this problem.
121  if (preg_match('/^\.localhost(:[0-9]+)?$/', $SessionDomain))
122  {
123  $SessionDomain = "";
124  }
125  session_set_cookie_params(self::$SessionLifetime, "/",
126  $SessionDomain, $SecureCookie, TRUE);
127 
128  # attempt to start session
129  $SessionStarted = @session_start();
130 
131  # if session start failed
132  if (!$SessionStarted)
133  {
134  # regenerate session ID and attempt to start session again
135  session_regenerate_id(TRUE);
136  session_start();
137  }
138 
139  # bump up our cookie expiry time, so that it'll die
140  # $SessionLifetime from now, rather than $SessionLifetime
141  # from whenever we created it
142  setcookie(
143  session_name(), session_id(),
144  time() + self::$SessionLifetime, "/",
145  $SessionDomain, $SecureCookie, TRUE);
146  }
147 
148  # set up our internal environment
149  $this->DB = new Database();
150 
151  # set up our exception handler
152  set_exception_handler(array($this, "GlobalExceptionHandler"));
153 
154  # load our settings from database
155  $this->LoadSettings();
156 
157  # set PHP maximum execution time
158  ini_set("max_execution_time", $this->Settings["MaxExecTime"]);
159  set_time_limit($this->Settings["MaxExecTime"]);
160 
161  # register events we handle internally
162  $this->RegisterEvent($this->PeriodicEvents);
163  $this->RegisterEvent($this->UIEvents);
164 
165  # attempt to create SCSS cache directory if needed and it does not exist
166  if ($this->ScssSupportEnabled() && !is_dir(self::$ScssCacheDir))
167  { @mkdir(self::$ScssCacheDir, 0777, TRUE); }
168 
169  # attempt to create minimized JS cache directory if needed and it does not exist
170  if ($this->UseMinimizedJavascript()
172  && !is_dir(self::$JSMinCacheDir))
173  {
174  @mkdir(self::$JSMinCacheDir, 0777, TRUE);
175  }
176  }
183  public function __destruct()
184  {
185  # if template location cache is flagged to be saved
186  if ($this->SaveTemplateLocationCache)
187  {
188  # write template location cache out and update cache expiration
189  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
190  ." SET TemplateLocationCache = '"
191  .addslashes(serialize(
192  $this->TemplateLocationCache))."',"
193  ." TemplateLocationCacheExpiration = '"
194  .date("Y-m-d H:i:s",
195  $this->TemplateLocationCacheExpiration)."'");
196  }
197 
198  # if object location cache is flagged to be saved
199  if (self::$SaveObjectLocationCache)
200  {
201  # write object location cache out and update cache expiration
202  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
203  ." SET ObjectLocationCache = '"
204  .addslashes(serialize(
205  self::$ObjectLocationCache))."',"
206  ." ObjectLocationCacheExpiration = '"
207  .date("Y-m-d H:i:s",
208  self::$ObjectLocationCacheExpiration)."'");
209  }
210  }
218  public function GlobalExceptionHandler($Exception)
219  {
220  # display exception info
221  $Message = $Exception->getMessage();
222  $Location = str_replace(getcwd()."/", "",
223  $Exception->getFile()."[".$Exception->getLine()."]");
224  $Trace = preg_replace(":(#[0-9]+) ".getcwd()."/".":", "$1 ",
225  $Exception->getTraceAsString());
226  if (php_sapi_name() == "cli")
227  {
228  print "Uncaught Exception\n"
229  ."Message: ".$Message."\n"
230  ."Location: ".$Location."\n"
231  ."Trace: \n"
232  .$Trace."\n";
233  }
234  else
235  {
236  ?><table width="100%" cellpadding="5"
237  style="border: 2px solid #666666; background: #CCCCCC;
238  font-family: Courier New, Courier, monospace;
239  margin-top: 10px;"><tr><td>
240  <div style="color: #666666;">
241  <span style="font-size: 150%;">
242  <b>Uncaught Exception</b></span><br />
243  <b>Message:</b> <i><?= $Message ?></i><br />
244  <b>Location:</b> <i><?= $Location ?></i><br />
245  <b>Trace:</b> <blockquote><pre><?= $Trace ?></pre></blockquote>
246  </div>
247  </td></tr></table><?PHP
248  }
249 
250  # log exception if not running from command line
251  if (php_sapi_name() !== "cli")
252  {
253  $TraceString = $Exception->getTraceAsString();
254  $TraceString = str_replace("\n", ", ", $TraceString);
255  $TraceString = preg_replace(":(#[0-9]+) ".getcwd()."/".":",
256  "$1 ", $TraceString);
257  $LogMsg = "Uncaught exception (".$Exception->getMessage().")"
258  ." at ".$Location."."
259  ." TRACE: ".$TraceString
260  ." URL: ".$this->FullUrl();
261  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
262  }
263  }
284  public static function AddObjectDirectory(
285  $Dir, $NamespacePrefixes = array(), $Callback = NULL)
286  {
287  # check to make sure any supplied callback looks valid
288  if (!is_null($Callback) && !is_callable($Callback))
289  {
290  throw new InvalidArgumentException("Supplied callback (\""
291  .$Callback."\") is invalid.");
292  }
293 
294  # make sure directory has trailing slash
295  $Dir = $Dir.((substr($Dir, -1) != "/") ? "/" : "");
296 
297  # make sure namespaces is an array
298  if (!is_array($NamespacePrefixes))
299  {
300  if (is_string($NamespacePrefixes))
301  {
302  $NamespacePrefixes = [ $NamespacePrefixes ];
303  }
304  else
305  {
306  throw new InvalidArgumentException("Supplied namespace (\""
307  .$NamespacePrefixes."\") is invalid.");
308  }
309  }
310 
311  # make sure namespace prefixes are in decreasing order of length
312  usort($NamespacePrefixes, function($A, $B)
313  {
314  return strlen($B) - strlen($A);
315  });
316 
317  # add directory to directory list
318  self::$ObjectDirectories[$Dir] = array(
319  "NamespacePrefixes" => $NamespacePrefixes,
320  "Callback" => $Callback,
321  );
322  }
323 
343  public function AddImageDirectories(
344  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
345  {
346  # add directories to existing image directory list
347  $this->ImageDirList = $this->AddToDirList(
348  $this->ImageDirList, $Dir, $SearchLast, $SkipSlashCheck);
349  }
350 
371  public function AddIncludeDirectories(
372  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
373  {
374  # add directories to existing image directory list
375  $this->IncludeDirList = $this->AddToDirList(
376  $this->IncludeDirList, $Dir, $SearchLast, $SkipSlashCheck);
377  }
378 
398  public function AddInterfaceDirectories(
399  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
400  {
401  # add directories to existing image directory list
402  $this->InterfaceDirList = $this->AddToDirList(
403  $this->InterfaceDirList, $Dir, $SearchLast, $SkipSlashCheck);
404 
405  # cleared cached lists for user interfaces
406  self::$UserInterfaceListCache = array();
407  self::$UserInterfacePathsCache = array();
408  }
409 
429  public function AddFunctionDirectories(
430  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
431  {
432  # add directories to existing image directory list
433  $this->FunctionDirList = $this->AddToDirList(
434  $this->FunctionDirList, $Dir, $SearchLast, $SkipSlashCheck);
435  }
436 
442  public function SetBrowserDetectionFunc($DetectionFunc)
443  {
444  $this->BrowserDetectFunc = $DetectionFunc;
445  }
446 
453  public function AddUnbufferedCallback($Callback, $Parameters=array())
454  {
455  if (is_callable($Callback))
456  {
457  $this->UnbufferedCallbacks[] = array($Callback, $Parameters);
458  }
459  }
460 
471  $NewInterval = DB_NOVALUE, $Persistent = FALSE)
472  {
473  return $this->UpdateSetting(
474  "TemplateLocationCacheInterval", $NewInterval, $Persistent);
475  }
476 
480  public function ClearTemplateLocationCache()
481  {
482  $this->TemplateLocationCache = array();
483  $this->SaveTemplateLocationCache = TRUE;
484  }
485 
496  $NewInterval = DB_NOVALUE, $Persistent = FALSE)
497  {
498  return $this->UpdateSetting(
499  "ObjectLocationCacheInterval", $NewInterval, $Persistent);
500  }
501 
505  public function ClearObjectLocationCache()
506  {
507  self::$ObjectLocationCache = array();
508  self::$SaveObjectLocationCache = TRUE;
509  }
510 
520  public function UrlFingerprintingEnabled($NewValue = DB_NOVALUE, $Persistent = FALSE)
521  {
522  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
523  }
524 
535  public function ScssSupportEnabled($NewValue = DB_NOVALUE, $Persistent = FALSE)
536  {
537  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
538  }
539 
551  public function GenerateCompactCss($NewValue = DB_NOVALUE, $Persistent = FALSE)
552  {
553  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
554  }
555 
567  public function UseMinimizedJavascript($NewValue = DB_NOVALUE, $Persistent = FALSE)
568  {
569  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
570  }
571 
584  $NewValue = DB_NOVALUE, $Persistent = FALSE)
585  {
586  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
587  }
588 
602  public function RecordContextInCaseOfCrash(
603  $BacktraceOptions = 0, $BacktraceLimit = 0)
604  {
605  if (version_compare(PHP_VERSION, "5.4.0", ">="))
606  {
607  $this->SavedContext = debug_backtrace(
608  $BacktraceOptions, $BacktraceLimit);
609  }
610  else
611  {
612  $this->SavedContext = debug_backtrace($BacktraceOptions);
613  }
614  array_shift($this->SavedContext);
615  }
616 
621  public function LoadPage($PageName)
622  {
623  # perform any clean URL rewriting
624  $PageName = $this->RewriteCleanUrls($PageName);
625 
626  # sanitize incoming page name and save local copy
627  $PageName = preg_replace("/[^a-zA-Z0-9_.-]/", "", $PageName);
628  $this->PageName = $PageName;
629 
630  # if page caching is turned on
631  if ($this->PageCacheEnabled())
632  {
633  # if we have a cached page
634  $CachedPage = $this->CheckForCachedPage($PageName);
635  if ($CachedPage !== NULL)
636  {
637  # set header to indicate cache hit was found
638  header("X-ScoutAF-Cache: HIT");
639 
640  # display cached page and exit
641  print $CachedPage;
642  return;
643  }
644  else
645  {
646  # set header to indicate no cache hit was found
647  header("X-ScoutAF-Cache: MISS");
648  }
649  }
650 
651  # buffer any output from includes or PHP file
652  ob_start();
653 
654  # include any files needed to set up execution environment
655  $IncludeFileContext = array();
656  foreach ($this->EnvIncludes as $IncludeFile)
657  {
658  $IncludeFileContext = $this->FilterContext(self::CONTEXT_ENV,
659  self::IncludeFile($IncludeFile, $IncludeFileContext));
660  }
661 
662  # signal page load
663  $this->SignalEvent("EVENT_PAGE_LOAD", array("PageName" => $PageName));
664 
665  # signal PHP file load
666  $SignalResult = $this->SignalEvent("EVENT_PHP_FILE_LOAD", array(
667  "PageName" => $PageName));
668 
669  # if signal handler returned new page name value
670  $NewPageName = $PageName;
671  if (($SignalResult["PageName"] != $PageName)
672  && strlen($SignalResult["PageName"]))
673  {
674  # if new page name value is page file
675  if (file_exists($SignalResult["PageName"]))
676  {
677  # use new value for PHP file name
678  $PageFile = $SignalResult["PageName"];
679  }
680  else
681  {
682  # use new value for page name
683  $NewPageName = $SignalResult["PageName"];
684  }
685 
686  # update local copy of page name
687  $this->PageName = $NewPageName;
688  }
689 
690  # if we do not already have a PHP file
691  if (!isset($PageFile))
692  {
693  # look for PHP file for page
694  $OurPageFile = "pages/".$NewPageName.".php";
695  $LocalPageFile = "local/pages/".$NewPageName.".php";
696  $PageFile = file_exists($LocalPageFile) ? $LocalPageFile
697  : (file_exists($OurPageFile) ? $OurPageFile
698  : "pages/".$this->DefaultPage.".php");
699  }
700 
701  # load PHP file
702  $IncludeFileContext = $this->FilterContext(self::CONTEXT_PAGE,
703  self::IncludeFile($PageFile, $IncludeFileContext));
704 
705  # save buffered output to be displayed later after HTML file loads
706  $PageOutput = ob_get_contents();
707  ob_end_clean();
708 
709  # signal PHP file load is complete
710  ob_start();
711  $Context["Variables"] = $IncludeFileContext;
712  $this->SignalEvent("EVENT_PHP_FILE_LOAD_COMPLETE",
713  array("PageName" => $PageName, "Context" => $Context));
714  $PageCompleteOutput = ob_get_contents();
715  ob_end_clean();
716 
717  # set up for possible TSR (Terminate and Stay Resident :))
718  $ShouldTSR = $this->PrepForTSR();
719 
720  # if PHP file indicated we should autorefresh to somewhere else
721  if (($this->JumpToPage) && ($this->JumpToPageDelay == 0))
722  {
723  if (!strlen(trim($PageOutput)))
724  {
725  # if client supports HTTP/1.1, use a 303 as it is most accurate
726  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.1")
727  {
728  header("HTTP/1.1 303 See Other");
729  header("Location: ".$this->JumpToPage);
730  }
731  else
732  {
733  # if the request was an HTTP/1.0 GET or HEAD, then
734  # use a 302 response code.
735 
736  # NB: both RFC 2616 (HTTP/1.1) and RFC1945 (HTTP/1.0)
737  # explicitly prohibit automatic redirection via a 302
738  # if the request was not GET or HEAD.
739  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.0" &&
740  ($_SERVER["REQUEST_METHOD"] == "GET" ||
741  $_SERVER["REQUEST_METHOD"] == "HEAD") )
742  {
743  header("HTTP/1.0 302 Found");
744  header("Location: ".$this->JumpToPage);
745  }
746 
747  # otherwise, fall back to a meta refresh
748  else
749  {
750  print '<html><head><meta http-equiv="refresh" '
751  .'content="0; URL='.$this->JumpToPage.'">'
752  .'</head><body></body></html>';
753  }
754  }
755  }
756  }
757  # else if HTML loading is not suppressed
758  elseif (!$this->SuppressHTML)
759  {
760  # set content-type to get rid of diacritic errors
761  header("Content-Type: text/html; charset="
762  .$this->HtmlCharset, TRUE);
763 
764  # load common HTML file (defines common functions) if available
765  $CommonHtmlFile = $this->FindFile($this->IncludeDirList,
766  "Common", array("tpl", "html"));
767  if ($CommonHtmlFile)
768  {
769  $IncludeFileContext = $this->FilterContext(self::CONTEXT_COMMON,
770  self::IncludeFile($CommonHtmlFile, $IncludeFileContext));
771  }
772 
773  # load UI functions
774  $this->LoadUIFunctions();
775 
776  # begin buffering content
777  ob_start();
778 
779  # signal HTML file load
780  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD", array(
781  "PageName" => $PageName));
782 
783  # if signal handler returned new page name value
784  $NewPageName = $PageName;
785  $PageContentFile = NULL;
786  if (($SignalResult["PageName"] != $PageName)
787  && strlen($SignalResult["PageName"]))
788  {
789  # if new page name value is HTML file
790  if (file_exists($SignalResult["PageName"]))
791  {
792  # use new value for HTML file name
793  $PageContentFile = $SignalResult["PageName"];
794  }
795  else
796  {
797  # use new value for page name
798  $NewPageName = $SignalResult["PageName"];
799  }
800  }
801 
802  # load page content HTML file if available
803  if ($PageContentFile === NULL)
804  {
805  $PageContentFile = $this->FindFile(
806  $this->InterfaceDirList, $NewPageName,
807  array("tpl", "html"));
808  }
809  if ($PageContentFile)
810  {
811  $IncludeFileContext = $this->FilterContext(self::CONTEXT_INTERFACE,
812  self::IncludeFile($PageContentFile, $IncludeFileContext));
813  }
814  else
815  {
816  print "<h2>ERROR: No HTML/TPL template found"
817  ." for this page (".$NewPageName.").</h2>";
818  }
819 
820  # signal HTML file load complete
821  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD_COMPLETE");
822 
823  # stop buffering and save output
824  $PageContentOutput = ob_get_contents();
825  ob_end_clean();
826 
827  # if standard page start/end have not been suppressed
828  $PageStartOutput = "";
829  $PageEndOutput = "";
830  if (!$this->SuppressStdPageStartAndEnd)
831  {
832  # load page start HTML file if available
833  $PageStartFile = $this->FindFile($this->IncludeDirList, "Start",
834  array("tpl", "html"), array("StdPage", "StandardPage"));
835  if ($PageStartFile)
836  {
837  ob_start();
838  $IncludeFileContext = self::IncludeFile(
839  $PageStartFile, $IncludeFileContext);
840  $PageStartOutput = ob_get_contents();
841  ob_end_clean();
842  }
843  $IncludeFileContext = $this->FilterContext(
844  self::CONTEXT_START, $IncludeFileContext);
845 
846  # load page end HTML file if available
847  $PageEndFile = $this->FindFile($this->IncludeDirList, "End",
848  array("tpl", "html"), array("StdPage", "StandardPage"));
849  if ($PageEndFile)
850  {
851  ob_start();
852  self::IncludeFile($PageEndFile, $IncludeFileContext);
853  $PageEndOutput = ob_get_contents();
854  ob_end_clean();
855  }
856  }
857 
858  # clear include file context because it may be large and is no longer needed
859  unset($IncludeFileContext);
860 
861  # if page auto-refresh requested
862  if ($this->JumpToPage)
863  {
864  # add auto-refresh tag to page
865  $this->AddMetaTag([
866  "http-equiv" => "refresh",
867  "content" => $this->JumpToPageDelay,
868  "url" => $this->JumpToPage,
869  ]);
870  }
871 
872  # assemble full page
873  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
874 
875  # get list of any required files not loaded
876  $RequiredFiles = $this->GetRequiredFilesNotYetLoaded($PageContentFile);
877 
878  # add file loading tags to page
879  $FullPageOutput = $this->AddFileTagsToPageOutput(
880  $FullPageOutput, $RequiredFiles);
881 
882  # add any requested meta tags to page
883  $FullPageOutput = $this->AddMetaTagsToPageOutput($FullPageOutput);
884 
885  # perform any regular expression replacements in output
886  $NewFullPageOutput = preg_replace($this->OutputModificationPatterns,
887  $this->OutputModificationReplacements, $FullPageOutput);
888 
889  # check to make sure replacements didn't fail
890  $FullPageOutput = $this->CheckOutputModification(
891  $FullPageOutput, $NewFullPageOutput,
892  "regular expression replacements");
893 
894  # for each registered output modification callback
895  foreach ($this->OutputModificationCallbacks as $Info)
896  {
897  # set up data for callback
898  $this->OutputModificationCallbackInfo = $Info;
899 
900  # perform output modification
901  $NewFullPageOutput = preg_replace_callback($Info["SearchPattern"],
902  array($this, "OutputModificationCallbackShell"),
903  $FullPageOutput);
904 
905  # check to make sure modification didn't fail
906  $ErrorInfo = "callback info: ".print_r($Info, TRUE);
907  $FullPageOutput = $this->CheckOutputModification(
908  $FullPageOutput, $NewFullPageOutput, $ErrorInfo);
909  }
910 
911  # provide the opportunity to modify full page output
912  $SignalResult = $this->SignalEvent("EVENT_PAGE_OUTPUT_FILTER", array(
913  "PageOutput" => $FullPageOutput));
914  if (isset($SignalResult["PageOutput"])
915  && strlen(trim($SignalResult["PageOutput"])))
916  {
917  $FullPageOutput = $SignalResult["PageOutput"];
918  }
919 
920  # if relative paths may not work because we were invoked via clean URL
921  if ($this->CleanUrlRewritePerformed || self::WasUrlRewritten())
922  {
923  # if using the <base> tag is okay
924  $BaseUrl = $this->BaseUrl();
925  if ($this->UseBaseTag)
926  {
927  # add <base> tag to header
928  $PageStartOutput = str_replace("<head>",
929  "<head><base href=\"".$BaseUrl."\" />",
930  $PageStartOutput);
931 
932  # re-assemble full page with new header
933  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
934 
935  # the absolute URL to the current page
936  $FullUrl = $BaseUrl . $this->GetPageLocation();
937 
938  # make HREF attribute values with just a fragment ID
939  # absolute since they don't work with the <base> tag because
940  # they are relative to the current page/URL, not the site
941  # root
942  $NewFullPageOutput = preg_replace(
943  array("%href=\"(#[^:\" ]+)\"%i", "%href='(#[^:' ]+)'%i"),
944  array("href=\"".$FullUrl."$1\"", "href='".$FullUrl."$1'"),
945  $FullPageOutput);
946 
947  # check to make sure HREF cleanup didn't fail
948  $FullPageOutput = $this->CheckOutputModification(
949  $FullPageOutput, $NewFullPageOutput,
950  "HREF cleanup");
951  }
952  else
953  {
954  # try to fix any relative paths throughout code
955  $SrcFileExtensions = "(js|css|gif|png|jpg|svg|ico)";
956  $RelativePathPatterns = array(
957  "%src=\"/?([^?*:;{}\\\\\" ]+)\.".$SrcFileExtensions."\"%i",
958  "%src='/?([^?*:;{}\\\\' ]+)\.".$SrcFileExtensions."'%i",
959  # don't rewrite HREF attributes that are just
960  # fragment IDs because they are relative to the
961  # current page/URL, not the site root
962  "%href=\"/?([^#][^:\" ]*)\"%i",
963  "%href='/?([^#][^:' ]*)'%i",
964  "%action=\"/?([^#][^:\" ]*)\"%i",
965  "%action='/?([^#][^:' ]*)'%i",
966  "%@import\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
967  "%@import\s+url\('/?([^:\" ]+)'\s*\)%i",
968  "%src:\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
969  "%src:\s+url\('/?([^:\" ]+)'\s*\)%i",
970  "%@import\s+\"/?([^:\" ]+)\"\s*%i",
971  "%@import\s+'/?([^:\" ]+)'\s*%i",
972  );
973  $RelativePathReplacements = array(
974  "src=\"".$BaseUrl."$1.$2\"",
975  "src=\"".$BaseUrl."$1.$2\"",
976  "href=\"".$BaseUrl."$1\"",
977  "href=\"".$BaseUrl."$1\"",
978  "action=\"".$BaseUrl."$1\"",
979  "action=\"".$BaseUrl."$1\"",
980  "@import url(\"".$BaseUrl."$1\")",
981  "@import url('".$BaseUrl."$1')",
982  "src: url(\"".$BaseUrl."$1\")",
983  "src: url('".$BaseUrl."$1')",
984  "@import \"".$BaseUrl."$1\"",
985  "@import '".$BaseUrl."$1'",
986  );
987  $NewFullPageOutput = preg_replace($RelativePathPatterns,
988  $RelativePathReplacements, $FullPageOutput);
989 
990  # check to make sure relative path fixes didn't fail
991  $FullPageOutput = $this->CheckOutputModification(
992  $FullPageOutput, $NewFullPageOutput,
993  "relative path fixes");
994  }
995  }
996 
997  # handle any necessary alternate domain rewriting
998  $FullPageOutput = $this->RewriteAlternateDomainUrls($FullPageOutput);
999 
1000  # update page cache for this page
1001  $this->UpdatePageCache($PageName, $FullPageOutput);
1002 
1003  # write out full page
1004  print $FullPageOutput;
1005  }
1006 
1007  # run any post-processing routines
1008  foreach ($this->PostProcessingFuncs as $Func)
1009  {
1010  call_user_func_array($Func["FunctionName"], $Func["Arguments"]);
1011  }
1012 
1013  # write out any output buffered from page code execution
1014  if (strlen($PageOutput))
1015  {
1016  if (!$this->SuppressHTML)
1017  {
1018  ?><table width="100%" cellpadding="5"
1019  style="border: 2px solid #666666; background: #CCCCCC;
1020  font-family: Courier New, Courier, monospace;
1021  margin-top: 10px;"><tr><td><?PHP
1022  }
1023  if ($this->JumpToPage)
1024  {
1025  ?><div style="color: #666666;"><span style="font-size: 150%;">
1026  <b>Page Jump Aborted</b></span>
1027  (because of error or other unexpected output)<br />
1028  <b>Jump Target:</b>
1029  <i><?PHP print($this->JumpToPage); ?></i></div><?PHP
1030  }
1031  print $PageOutput;
1032  if (!$this->SuppressHTML)
1033  {
1034  ?></td></tr></table><?PHP
1035  }
1036  }
1037 
1038  # write out any output buffered from the page code execution complete signal
1039  if (!$this->JumpToPage && !$this->SuppressHTML && strlen($PageCompleteOutput))
1040  {
1041  print $PageCompleteOutput;
1042  }
1043 
1044  # log slow page loads
1045  if ($this->LogSlowPageLoads()
1046  && !$this->DoNotLogSlowPageLoad
1047  && ($this->GetElapsedExecutionTime()
1048  >= ($this->SlowPageLoadThreshold())))
1049  {
1050  $RemoteHost = gethostbyaddr($_SERVER["REMOTE_ADDR"]);
1051  if ($RemoteHost === FALSE)
1052  {
1053  $RemoteHost = $_SERVER["REMOTE_ADDR"];
1054  }
1055  elseif ($RemoteHost != $_SERVER["REMOTE_ADDR"])
1056  {
1057  $RemoteHost .= " (".$_SERVER["REMOTE_ADDR"].")";
1058  }
1059  $SlowPageLoadMsg = "Slow page load ("
1060  .intval($this->GetElapsedExecutionTime())."s) for "
1061  .$this->FullUrl()." from ".$RemoteHost;
1062  $this->LogMessage(self::LOGLVL_INFO, $SlowPageLoadMsg);
1063  }
1064 
1065  # execute callbacks that should not have their output buffered
1066  foreach ($this->UnbufferedCallbacks as $Callback)
1067  {
1068  call_user_func_array($Callback[0], $Callback[1]);
1069  }
1070 
1071  # log high memory usage
1072  if (function_exists("memory_get_peak_usage"))
1073  {
1074  $MemoryThreshold = ($this->HighMemoryUsageThreshold()
1075  * $this->GetPhpMemoryLimit()) / 100;
1076  if ($this->LogHighMemoryUsage()
1077  && (memory_get_peak_usage(TRUE) >= $MemoryThreshold))
1078  {
1079  $HighMemUsageMsg = "High peak memory usage ("
1080  .number_format(memory_get_peak_usage(TRUE)).") for "
1081  .$this->FullUrl()." from "
1082  .$_SERVER["REMOTE_ADDR"];
1083  $this->LogMessage(self::LOGLVL_INFO, $HighMemUsageMsg);
1084  }
1085  }
1086 
1087  $this->UpdateLastUsedTimeForActiveSessions();
1088 
1089  # terminate and stay resident (TSR!) if indicated and HTML has been output
1090  # (only TSR if HTML has been output because otherwise browsers will misbehave)
1091  if ($ShouldTSR) { $this->LaunchTSR(); }
1092  }
1093 
1099  public function GetPageName()
1100  {
1101  return $this->PageName;
1102  }
1103 
1109  public function GetPageLocation()
1110  {
1111  # retrieve current URL
1112  $Url = self::GetScriptUrl();
1113 
1114  # remove the base path if present
1115  $BasePath = $this->Settings["BasePath"];
1116  if (stripos($Url, $BasePath) === 0)
1117  {
1118  $Url = substr($Url, strlen($BasePath));
1119  }
1120 
1121  # if we're being accessed via an alternate domain,
1122  # add the appropriate prefix in
1123  if ($this->HtaccessSupport() &&
1124  self::$RootUrlOverride !== NULL)
1125  {
1126  $VHost = $_SERVER["SERVER_NAME"];
1127  if (isset($this->AlternateDomainPrefixes[$VHost]))
1128  {
1129  $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
1130  $Url = $ThisPrefix."/".$Url;
1131  }
1132  }
1133 
1134  return $Url;
1135  }
1136 
1142  public function GetPageUrl()
1143  {
1144  return self::BaseUrl() . $this->GetPageLocation();
1145  }
1146 
1158  public function SetJumpToPage($Page, $Delay = 0, $IsLiteral = FALSE)
1159  {
1160  if (!is_null($Page)
1161  && (!$IsLiteral)
1162  && (strpos($Page, "?") === FALSE)
1163  && ((strpos($Page, "=") !== FALSE)
1164  || ((stripos($Page, ".php") === FALSE)
1165  && (stripos($Page, ".htm") === FALSE)
1166  && (strpos($Page, "/") === FALSE)))
1167  && (stripos($Page, "http://") !== 0)
1168  && (stripos($Page, "https://") !== 0))
1169  {
1170  $this->JumpToPage = self::BaseUrl() . "index.php?P=".$Page;
1171  }
1172  else
1173  {
1174  $this->JumpToPage = $Page;
1175  }
1176  $this->JumpToPageDelay = $Delay;
1177  }
1178 
1183  public function JumpToPageIsSet()
1184  {
1185  return ($this->JumpToPage === NULL) ? FALSE : TRUE;
1186  }
1187 
1197  public function HtmlCharset($NewSetting = NULL)
1198  {
1199  if ($NewSetting !== NULL) { $this->HtmlCharset = $NewSetting; }
1200  return $this->HtmlCharset;
1201  }
1202 
1212  public function DoNotMinimizeFile($File)
1213  {
1214  if (!is_array($File)) { $File = array($File); }
1215  $this->DoNotMinimizeList = array_merge($this->DoNotMinimizeList, $File);
1216  }
1217 
1228  public function UseBaseTag($NewValue = NULL)
1229  {
1230  if ($NewValue !== NULL) { $this->UseBaseTag = $NewValue ? TRUE : FALSE; }
1231  return $this->UseBaseTag;
1232  }
1233 
1241  public function SuppressHTMLOutput($NewSetting = TRUE)
1242  {
1243  $this->SuppressHTML = $NewSetting;
1244  }
1245 
1253  public function SuppressStandardPageStartAndEnd($NewSetting = TRUE)
1254  {
1255  $this->SuppressStdPageStartAndEnd = $NewSetting;
1256  }
1257 
1263  public static function DefaultUserInterface($UIName = NULL)
1264  {
1265  if ($UIName !== NULL)
1266  {
1267  self::$DefaultUI = $UIName;
1268  }
1269  return self::$DefaultUI;
1270  }
1271 
1278  public static function ActiveUserInterface($UIName = NULL)
1279  {
1280  if ($UIName !== NULL)
1281  {
1282  self::$ActiveUI = preg_replace("/^SPTUI--/", "", $UIName);
1283  }
1284  return self::$ActiveUI;
1285  }
1286 
1297  public function GetUserInterfaces($FilterExp = NULL)
1298  {
1299  if (!isset(self::$UserInterfaceListCache[$FilterExp]))
1300  {
1301  # retrieve paths to user interface directories
1302  $Paths = $this->GetUserInterfacePaths($FilterExp);
1303 
1304  # start out with an empty list
1305  self::$UserInterfaceListCache[$FilterExp] = array();
1306 
1307  # for each possible UI directory
1308  foreach ($Paths as $CanonicalName => $Path)
1309  {
1310  # if name file available
1311  $LabelFile = $Path."/NAME";
1312  if (is_readable($LabelFile))
1313  {
1314  # read the UI name
1315  $Label = file_get_contents($LabelFile);
1316 
1317  # if the UI name looks reasonable
1318  if (strlen(trim($Label)))
1319  {
1320  # use read name
1321  self::$UserInterfaceListCache[$FilterExp][$CanonicalName] =
1322  $Label;
1323  }
1324  }
1325 
1326  # if we do not have a name yet
1327  if (!isset(self::$UserInterfaceListCache[$FilterExp][$CanonicalName]))
1328  {
1329  # use base directory for name
1330  self::$UserInterfaceListCache[$FilterExp][$CanonicalName] =
1331  basename($Path);
1332  }
1333  }
1334  }
1335 
1336  # return list to caller
1337  return self::$UserInterfaceListCache[$FilterExp];
1338  }
1339 
1348  public function GetUserInterfacePaths($FilterExp = NULL)
1349  {
1350  if (!isset(self::$UserInterfacePathsCache[$FilterExp]))
1351  {
1352  # extract possible UI directories from interface directory list
1353  $InterfaceDirs = array();
1354  foreach ($this->ExpandDirectoryList($this->InterfaceDirList) as $Dir)
1355  {
1356  $Matches = array();
1357  if (preg_match("#([a-zA-Z0-9/]*interface)/[a-zA-Z0-9%/]*#",
1358  $Dir, $Matches))
1359  {
1360  $Dir = $Matches[1];
1361  if (!in_array($Dir, $InterfaceDirs))
1362  {
1363  $InterfaceDirs[] = $Dir;
1364  }
1365  }
1366  }
1367 
1368  # reverse order of interface directories so that the directory
1369  # returned is the base directory for the interface
1370  $InterfaceDirs = array_reverse($InterfaceDirs);
1371 
1372  # start out with an empty list
1373  self::$UserInterfacePathsCache[$FilterExp] = array();
1374  $InterfacesFound = array();
1375 
1376  # for each possible UI directory
1377  foreach ($InterfaceDirs as $InterfaceDir)
1378  {
1379  # check if the dir exists
1380  if (!is_dir($InterfaceDir))
1381  {
1382  continue;
1383  }
1384 
1385  $Dir = dir($InterfaceDir);
1386 
1387  # for each file in current directory
1388  while (($DirEntry = $Dir->read()) !== FALSE)
1389  {
1390  $InterfacePath = $InterfaceDir."/".$DirEntry;
1391 
1392  # skip anything we have already found
1393  # or that doesn't have a name in the required format
1394  # or that isn't a directory
1395  # or that doesn't match the filter regex (if supplied)
1396  if (in_array($DirEntry, $InterfacesFound)
1397  || !preg_match('/^[a-zA-Z0-9]+$/', $DirEntry)
1398  || !is_dir($InterfacePath)
1399  || (($FilterExp !== NULL)
1400  && !preg_match($FilterExp, $InterfacePath)))
1401  {
1402  continue;
1403  }
1404 
1405  # add interface to list
1406  self::$UserInterfacePathsCache[$FilterExp][$DirEntry] =
1407  $InterfacePath;
1408  $InterfacesFound[] = $DirEntry;
1409  }
1410 
1411  $Dir->close();
1412  }
1413  }
1414 
1415  # return list to caller
1416  return self::$UserInterfacePathsCache[$FilterExp];
1417  }
1418 
1443  public function AddPostProcessingCall($FunctionName,
1444  &$Arg1 = self::NOVALUE, &$Arg2 = self::NOVALUE, &$Arg3 = self::NOVALUE,
1445  &$Arg4 = self::NOVALUE, &$Arg5 = self::NOVALUE, &$Arg6 = self::NOVALUE,
1446  &$Arg7 = self::NOVALUE, &$Arg8 = self::NOVALUE, &$Arg9 = self::NOVALUE)
1447  {
1448  $FuncIndex = count($this->PostProcessingFuncs);
1449  $this->PostProcessingFuncs[$FuncIndex]["FunctionName"] = $FunctionName;
1450  $this->PostProcessingFuncs[$FuncIndex]["Arguments"] = array();
1451  $Index = 1;
1452  while (isset(${"Arg".$Index}) && (${"Arg".$Index} !== self::NOVALUE))
1453  {
1454  $this->PostProcessingFuncs[$FuncIndex]["Arguments"][$Index]
1455  =& ${"Arg".$Index};
1456  $Index++;
1457  }
1458  }
1459 
1465  public function AddEnvInclude($FileName)
1466  {
1467  $this->EnvIncludes[] = $FileName;
1468  }
1469 
1486  public function SetContextFilter($Context, $NewSetting)
1487  {
1488  if (($NewSetting === TRUE)
1489  || ($NewSetting === FALSE)
1490  || is_array($NewSetting))
1491  {
1492  $this->ContextFilters[$Context] = $NewSetting;
1493  }
1494  elseif (is_string($NewSetting))
1495  {
1496  $this->ContextFilters[$Context] = array($NewSetting);
1497  }
1498  else
1499  {
1500  throw new InvalidArgumentException(
1501  "Invalid setting (".$NewSetting.").");
1502  }
1503  }
1505  const CONTEXT_ENV = 1;
1507  const CONTEXT_PAGE = 2;
1509  const CONTEXT_COMMON = 3;
1513  const CONTEXT_START = 5;
1515  const CONTEXT_END = 6;
1516 
1523  public function GUIFile($FileName)
1524  {
1525  # determine which location to search based on file suffix
1526  $FileType = $this->GetFileType($FileName);
1527  $DirList = ($FileType == self::FT_IMAGE)
1528  ? $this->ImageDirList : $this->IncludeDirList;
1529 
1530  # if directed to use minimized JavaScript file
1531  if (($FileType == self::FT_JAVASCRIPT) && $this->UseMinimizedJavascript())
1532  {
1533  # look for minimized version of file
1534  $MinimizedFileName = substr_replace($FileName, ".min", -3, 0);
1535  $FoundFileName = $this->FindFile($DirList, $MinimizedFileName);
1536 
1537  # if minimized file was not found
1538  if (is_null($FoundFileName))
1539  {
1540  # look for unminimized file
1541  $FoundFileName = $this->FindFile($DirList, $FileName);
1542 
1543  # if unminimized file found
1544  if (!is_null($FoundFileName))
1545  {
1546  # if minimization enabled and supported
1547  if ($this->JavascriptMinimizationEnabled()
1548  && self::JsMinRewriteSupport())
1549  {
1550  # attempt to create minimized file
1551  $MinFileName = $this->MinimizeJavascriptFile(
1552  $FoundFileName);
1553 
1554  # if minimization succeeded
1555  if ($MinFileName !== NULL)
1556  {
1557  # use minimized version
1558  $FoundFileName = $MinFileName;
1559 
1560  # save file modification time if needed for fingerprinting
1561  if ($this->UrlFingerprintingEnabled())
1562  {
1563  $FileMTime = filemtime($FoundFileName);
1564  }
1565 
1566  # strip off the cache location, allowing .htaccess
1567  # to handle that for us
1568  $FoundFileName = str_replace(
1569  self::$JSMinCacheDir."/", "", $FoundFileName);
1570  }
1571  }
1572  }
1573  }
1574  }
1575  # else if directed to use SCSS files
1576  elseif (($FileType == self::FT_CSS) && $this->ScssSupportEnabled())
1577  {
1578  # look for SCSS version of file
1579  $SourceFileName = preg_replace("/.css$/", ".scss", $FileName);
1580  $FoundSourceFileName = $this->FindFile($DirList, $SourceFileName);
1581 
1582  # if SCSS file not found
1583  if ($FoundSourceFileName === NULL)
1584  {
1585  # look for CSS file
1586  $FoundFileName = $this->FindFile($DirList, $FileName);
1587  }
1588  else
1589  {
1590  # compile SCSS file (if updated) and return resulting CSS file
1591  $FoundFileName = $this->CompileScssFile($FoundSourceFileName);
1592 
1593  # save file modification time if needed for fingerprinting
1594  if ($this->UrlFingerprintingEnabled())
1595  {
1596  $FileMTime = filemtime($FoundFileName);
1597  }
1598 
1599  # strip off the cache location, allowing .htaccess to handle that for us
1600  if (self::ScssRewriteSupport())
1601  {
1602  $FoundFileName = str_replace(
1603  self::$ScssCacheDir."/", "", $FoundFileName);
1604  }
1605  }
1606  }
1607  # otherwise just search for the file
1608  else
1609  {
1610  $FoundFileName = $this->FindFile($DirList, $FileName);
1611  }
1612 
1613  # add non-image files to list of found files (used for required files loading)
1614  if ($FileType != self::FT_IMAGE)
1615  { $this->FoundUIFiles[] = basename($FoundFileName); }
1616 
1617  # if UI file fingerprinting is enabled and supported
1618  if ($this->UrlFingerprintingEnabled()
1619  && self::UrlFingerprintingRewriteSupport()
1620  && (isset($FileMTime) || file_exists($FoundFileName)))
1621  {
1622  # if file does not appear to be a server-side inclusion
1623  if (!preg_match('/\.(html|php)$/i', $FoundFileName))
1624  {
1625  # for each URL fingerprinting blacklist entry
1626  $OnBlacklist = FALSE;
1627  foreach ($this->UrlFingerprintBlacklist as $BlacklistEntry)
1628  {
1629  # if entry looks like a regular expression pattern
1630  if ($BlacklistEntry[0] == substr($BlacklistEntry, -1))
1631  {
1632  # check file name against regular expression
1633  if (preg_match($BlacklistEntry, $FoundFileName))
1634  {
1635  $OnBlacklist = TRUE;
1636  break;
1637  }
1638  }
1639  else
1640  {
1641  # check file name directly against entry
1642  if (basename($FoundFileName) == $BlacklistEntry)
1643  {
1644  $OnBlacklist = TRUE;
1645  break;
1646  }
1647  }
1648  }
1649 
1650  # if file was not on blacklist
1651  if (!$OnBlacklist)
1652  {
1653  # get file modification time if not already retrieved
1654  if (!isset($FileMTime))
1655  {
1656  $FileMTime = filemtime($FoundFileName);
1657  }
1658 
1659  # add timestamp fingerprint to file name
1660  $Fingerprint = sprintf("%06X",
1661  ($FileMTime % 0xFFFFFF));
1662  $FoundFileName = preg_replace("/^(.+)\.([a-z]+)$/",
1663  "$1.".$Fingerprint.".$2",
1664  $FoundFileName);
1665  }
1666  }
1667  }
1668 
1669  # return file name to caller
1670  return $FoundFileName;
1671  }
1672 
1681  public function PUIFile($FileName)
1682  {
1683  $FullFileName = $this->GUIFile($FileName);
1684  if ($FullFileName) { print($FullFileName); }
1685  }
1686 
1701  public function IncludeUIFile($FileNames, $AdditionalAttributes = NULL)
1702  {
1703  # convert file name to array if necessary
1704  if (!is_array($FileNames)) { $FileNames = [ $FileNames ]; }
1705 
1706  # pad additional attributes if supplied
1707  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
1708 
1709  # for each file
1710  foreach ($FileNames as $BaseFileName)
1711  {
1712  # retrieve full file name
1713  $FileName = $this->GUIFile($BaseFileName);
1714 
1715  # if file was found
1716  if ($FileName)
1717  {
1718  # print appropriate tag
1719  print $this->GetUIFileLoadingTag($FileName, $AdditionalAttributes);
1720  }
1721 
1722  # if we are not already loading an override file
1723  if (!preg_match("/-Override.(css|scss|js)$/", $BaseFileName))
1724  {
1725  # attempt to load override file if available
1726  $FileType = $this->GetFileType($BaseFileName);
1727  switch ($FileType)
1728  {
1729  case self::FT_CSS:
1730  $OverrideFileName = preg_replace(
1731  "/\.(css|scss)$/", "-Override.$1",
1732  $BaseFileName);
1733  $this->IncludeUIFile($OverrideFileName,
1734  $AdditionalAttributes);
1735  break;
1736 
1737  case self::FT_JAVASCRIPT:
1738  $OverrideFileName = preg_replace(
1739  "/\.js$/", "-Override.js",
1740  $BaseFileName);
1741  $this->IncludeUIFile($OverrideFileName,
1742  $AdditionalAttributes);
1743  break;
1744  }
1745  }
1746  }
1747  }
1748 
1755  public function DoNotUrlFingerprint($Pattern)
1756  {
1757  $this->UrlFingerprintBlacklist[] = $Pattern;
1758  }
1759 
1771  public function RequireUIFile($FileNames, $Order = self::ORDER_MIDDLE)
1772  {
1773  # convert file names to array if necessary
1774  if (!is_array($FileNames)) { $FileNames = [ $FileNames ]; }
1775 
1776  # add file names to list of required files
1777  foreach ($FileNames as $FileName)
1778  {
1779  $this->AdditionalRequiredUIFiles[$FileName] = $Order;
1780  }
1781  }
1782 
1788  public static function GetFileType($FileName)
1789  {
1790  static $FileTypeCache;
1791  if (isset($FileTypeCache[$FileName]))
1792  {
1793  return $FileTypeCache[$FileName];
1794  }
1795 
1796  $FileSuffix = strtolower(substr($FileName, -3));
1797  if ($FileSuffix == "css")
1798  {
1799  $FileTypeCache[$FileName] = self::FT_CSS;
1800  }
1801  elseif ($FileSuffix == ".js")
1802  {
1803  $FileTypeCache[$FileName] = self::FT_JAVASCRIPT;
1804  }
1805  elseif (($FileSuffix == "gif")
1806  || ($FileSuffix == "jpg")
1807  || ($FileSuffix == "png")
1808  || ($FileSuffix == "svg")
1809  || ($FileSuffix == "ico"))
1810  {
1811  $FileTypeCache[$FileName] = self::FT_IMAGE;
1812  }
1813  else
1814  {
1815  $FileTypeCache[$FileName] = self::FT_OTHER;
1816  }
1817 
1818  return $FileTypeCache[$FileName];
1819  }
1821  const FT_OTHER = 0;
1823  const FT_CSS = 1;
1825  const FT_IMAGE = 2;
1827  const FT_JAVASCRIPT = 3;
1828 
1837  public function LoadFunction($Callback)
1838  {
1839  # if specified function is not currently available
1840  if (!is_callable($Callback))
1841  {
1842  # if function info looks legal
1843  if (is_string($Callback) && strlen($Callback))
1844  {
1845  # start with function directory list
1846  $Locations = $this->FunctionDirList;
1847 
1848  # add object directories to list
1849  $Locations = array_merge(
1850  $Locations, array_keys(self::$ObjectDirectories));
1851 
1852  # look for function file
1853  $FunctionFileName = $this->FindFile($Locations, "F-".$Callback,
1854  array("php", "html"));
1855 
1856  # if function file was found
1857  if ($FunctionFileName)
1858  {
1859  # load function file
1860  include_once($FunctionFileName);
1861  }
1862  else
1863  {
1864  # log error indicating function load failed
1865  $this->LogError(self::LOGLVL_ERROR, "Unable to load function"
1866  ." for callback \"".$Callback."\".");
1867  }
1868  }
1869  else
1870  {
1871  # log error indicating specified function info was bad
1872  $this->LogError(self::LOGLVL_ERROR, "Unloadable callback value"
1873  ." (".$Callback.")"
1874  ." passed to AF::LoadFunction() by "
1875  .StdLib::GetMyCaller().".");
1876  }
1877  }
1878 
1879  # report to caller whether function load succeeded
1880  return is_callable($Callback);
1881  }
1882 
1887  public function GetElapsedExecutionTime()
1888  {
1889  return microtime(TRUE) - $this->ExecutionStartTime;
1890  }
1891 
1896  public function GetSecondsBeforeTimeout()
1897  {
1898  return $this->MaxExecutionTime() - $this->GetElapsedExecutionTime();
1899  }
1900 
1905  public function AddMetaTag($Attribs)
1906  {
1907  # add new meta tag to list
1908  $this->MetaTags[] = $Attribs;
1909  }
1910 
1919  public function AddMetaTagOnce($Attribs, $UniqueAttribs = NULL)
1920  {
1921  # add new meta tag to list
1922  $this->UniqueMetaTags[] = [
1923  "Attribs" => $Attribs,
1924  "UniqueAttribs" => $UniqueAttribs,
1925  ];
1926  }
1927 
1928  /*@)*/ /* Application Framework */
1929 
1930 
1931  # ---- Page Caching ------------------------------------------------------
1932  /*@(*/
1934 
1944  public function PageCacheEnabled(
1945  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1946  {
1947  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
1948  }
1949 
1959  public function PageCacheExpirationPeriod(
1960  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1961  {
1962  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
1963  }
1964 
1969  public function DoNotCacheCurrentPage()
1970  {
1971  $this->CacheCurrentPage = FALSE;
1972  }
1973 
1980  public function AddPageCacheTag($Tag, $Pages = NULL)
1981  {
1982  # normalize tag
1983  $Tag = strtolower($Tag);
1984 
1985  # if pages were supplied
1986  if ($Pages !== NULL)
1987  {
1988  # add pages to list for this tag
1989  if (isset($this->PageCacheTags[$Tag]))
1990  {
1991  $this->PageCacheTags[$Tag] = array_merge(
1992  $this->PageCacheTags[$Tag], $Pages);
1993  }
1994  else
1995  {
1996  $this->PageCacheTags[$Tag] = $Pages;
1997  }
1998  }
1999  else
2000  {
2001  # add current page to list for this tag
2002  $this->PageCacheTags[$Tag][] = "CURRENT";
2003  }
2004  }
2005 
2011  public function ClearPageCacheForTag($Tag)
2012  {
2013  # get tag ID
2014  $TagId = $this->GetPageCacheTagId($Tag);
2015 
2016  # delete pages and tag/page connections for specified tag
2017  $this->DB->Query("DELETE CP, CPTI"
2018  ." FROM AF_CachedPages CP, AF_CachedPageTagInts CPTI"
2019  ." WHERE CPTI.TagId = ".intval($TagId)
2020  ." AND CP.CacheId = CPTI.CacheId");
2021  }
2022 
2026  public function ClearPageCache()
2027  {
2028  # clear all page cache tables
2029  $this->DB->Query("TRUNCATE TABLE AF_CachedPages");
2030  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTags");
2031  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTagInts");
2032  }
2033 
2040  public function GetPageCacheInfo()
2041  {
2042  $Length = $this->DB->Query("SELECT COUNT(*) AS CacheLen"
2043  ." FROM AF_CachedPages", "CacheLen");
2044  $Oldest = $this->DB->Query("SELECT CachedAt FROM AF_CachedPages"
2045  ." ORDER BY CachedAt ASC LIMIT 1", "CachedAt");
2046  return array(
2047  "NumberOfEntries" => $Length,
2048  "OldestTimestamp" => strtotime($Oldest),
2049  );
2050  }
2051 
2052  /*@)*/ /* Page Caching */
2053 
2054 
2055  # ---- Logging -----------------------------------------------------------
2056  /*@(*/
2058 
2072  public function LogSlowPageLoads(
2073  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2074  {
2075  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
2076  }
2077 
2088  public function SlowPageLoadThreshold(
2089  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2090  {
2091  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
2092  }
2093 
2107  public function LogHighMemoryUsage(
2108  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2109  {
2110  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
2111  }
2112 
2124  public function HighMemoryUsageThreshold(
2125  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2126  {
2127  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
2128  }
2129 
2143  public function LogError($Level, $Msg)
2144  {
2145  # if error level is at or below current logging level
2146  if ($this->Settings["LoggingLevel"] >= $Level)
2147  {
2148  # attempt to log error message
2149  $Result = $this->LogMessage($Level, $Msg);
2150 
2151  # if logging attempt failed and level indicated significant error
2152  if (($Result === FALSE) && ($Level <= self::LOGLVL_ERROR))
2153  {
2154  # throw exception about inability to log error
2155  static $AlreadyThrewException = FALSE;
2156  if (!$AlreadyThrewException)
2157  {
2158  $AlreadyThrewException = TRUE;
2159  throw new Exception("Unable to log error (".$Level.": ".$Msg
2160  .") to ".$this->LogFileName);
2161  }
2162  }
2163 
2164  # report to caller whether message was logged
2165  return $Result;
2166  }
2167  else
2168  {
2169  # report to caller that message was not logged
2170  return FALSE;
2171  }
2172  }
2173 
2185  public function LogMessage($Level, $Msg)
2186  {
2187  # if message level is at or below current logging level
2188  if ($this->Settings["LoggingLevel"] >= $Level)
2189  {
2190  # attempt to open log file
2191  $FHndl = @fopen($this->LogFileName, "a");
2192 
2193  # if log file could not be open
2194  if ($FHndl === FALSE)
2195  {
2196  # report to caller that message was not logged
2197  return FALSE;
2198  }
2199  else
2200  {
2201  # format log entry
2202  $ErrorAbbrevs = array(
2203  self::LOGLVL_FATAL => "FTL",
2204  self::LOGLVL_ERROR => "ERR",
2205  self::LOGLVL_WARNING => "WRN",
2206  self::LOGLVL_INFO => "INF",
2207  self::LOGLVL_DEBUG => "DBG",
2208  self::LOGLVL_TRACE => "TRC",
2209  );
2210  $Msg = str_replace(array("\n", "\t", "\r"), " ", $Msg);
2211  $Msg = substr(trim($Msg), 0, self::LOGFILE_MAX_LINE_LENGTH);
2212  $LogEntry = date("Y-m-d H:i:s")
2213  ." ".($this->RunningInBackground ? "B" : "F")
2214  ." ".$ErrorAbbrevs[$Level]
2215  ." ".$Msg;
2216 
2217  # write entry to log
2218  $Success = fwrite($FHndl, $LogEntry."\n");
2219 
2220  # close log file
2221  fclose($FHndl);
2222 
2223  # report to caller whether message was logged
2224  return ($Success === FALSE) ? FALSE : TRUE;
2225  }
2226  }
2227  else
2228  {
2229  # report to caller that message was not logged
2230  return FALSE;
2231  }
2232  }
2233 
2258  public function LoggingLevel($NewValue = DB_NOVALUE, $Persistent = FALSE)
2259  {
2260  # constrain new level (if supplied) to within legal bounds
2261  if ($NewValue !== DB_NOVALUE)
2262  {
2263  $NewValue = max(min($NewValue, 6), 1);
2264  }
2265 
2266  # set new logging level (if supplied) and return current level to caller
2267  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
2268  }
2269 
2276  public function LogFile($NewValue = NULL)
2277  {
2278  if ($NewValue !== NULL) { $this->LogFileName = $NewValue; }
2279  return $this->LogFileName;
2280  }
2281 
2291  public function GetLogEntries($Limit = 0)
2292  {
2293  # return no entries if there isn't a log file
2294  # or we can't read it or it's empty
2295  $LogFile = $this->LogFile();
2296  if (!is_readable($LogFile) || !filesize($LogFile))
2297  {
2298  return array();
2299  }
2300 
2301  # if max number of entries specified
2302  if ($Limit > 0)
2303  {
2304  # load lines from file
2305  $FHandle = fopen($LogFile, "r");
2306  $FileSize = filesize($LogFile);
2307  $SeekPosition = max(0,
2308  ($FileSize - (self::LOGFILE_MAX_LINE_LENGTH
2309  * ($Limit + 1))));
2310  fseek($FHandle, $SeekPosition);
2311  $Block = fread($FHandle, ($FileSize - $SeekPosition));
2312  fclose($FHandle);
2313  $Lines = explode(PHP_EOL, $Block);
2314  array_pop($Lines);
2315 
2316  # prune array back to requested number of entries
2317  $Lines = array_slice($Lines, (0 - $Limit));
2318  }
2319  else
2320  {
2321  # load all lines from log file
2322  $Lines = file($LogFile, FILE_IGNORE_NEW_LINES);
2323  if ($Lines === FALSE)
2324  {
2325  return array();
2326  }
2327  }
2328 
2329  # reverse line order
2330  $Lines = array_reverse($Lines);
2331 
2332  # for each log file line
2333  $Entries = array();
2334  foreach ($Lines as $Line)
2335  {
2336  # attempt to parse line into component parts
2337  $Pieces = explode(" ", $Line, 5);
2338  $Date = isset($Pieces[0]) ? $Pieces[0] : "";
2339  $Time = isset($Pieces[1]) ? $Pieces[1] : "";
2340  $Back = isset($Pieces[2]) ? $Pieces[2] : "";
2341  $Level = isset($Pieces[3]) ? $Pieces[3] : "";
2342  $Msg = isset($Pieces[4]) ? $Pieces[4] : "";
2343 
2344  # skip line if it looks invalid
2345  $ErrorAbbrevs = array(
2346  "FTL" => self::LOGLVL_FATAL,
2347  "ERR" => self::LOGLVL_ERROR,
2348  "WRN" => self::LOGLVL_WARNING,
2349  "INF" => self::LOGLVL_INFO,
2350  "DBG" => self::LOGLVL_DEBUG,
2351  "TRC" => self::LOGLVL_TRACE,
2352  );
2353  if ((($Back != "F") && ($Back != "B"))
2354  || !array_key_exists($Level, $ErrorAbbrevs)
2355  || !strlen($Msg))
2356  {
2357  continue;
2358  }
2359 
2360  # convert parts into appropriate values and add to entries
2361  $Entries[] = array(
2362  "Time" => strtotime($Date." ".$Time),
2363  "Background" => ($Back == "B") ? TRUE : FALSE,
2364  "Level" => $ErrorAbbrevs[$Level],
2365  "Message" => $Msg,
2366  );
2367  }
2368 
2369  # return entries to caller
2370  return $Entries;
2371  }
2372 
2377  const LOGLVL_TRACE = 6;
2382  const LOGLVL_DEBUG = 5;
2388  const LOGLVL_INFO = 4;
2393  const LOGLVL_WARNING = 3;
2399  const LOGLVL_ERROR = 2;
2404  const LOGLVL_FATAL = 1;
2405 
2410 
2411  /*@)*/ /* Logging */
2412 
2413 
2414  # ---- Event Handling ----------------------------------------------------
2415  /*@(*/
2417 
2427  const EVENTTYPE_CHAIN = 2;
2433  const EVENTTYPE_FIRST = 3;
2441  const EVENTTYPE_NAMED = 4;
2442 
2444  const ORDER_FIRST = 1;
2446  const ORDER_MIDDLE = 2;
2448  const ORDER_LAST = 3;
2449 
2458  public function RegisterEvent($EventsOrEventName, $EventType = NULL)
2459  {
2460  # convert parameters to array if not already in that form
2461  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2462  : array($EventsOrEventName => $EventType);
2463 
2464  # for each event
2465  foreach ($Events as $Name => $Type)
2466  {
2467  # store event information
2468  $this->RegisteredEvents[$Name]["Type"] = $Type;
2469  $this->RegisteredEvents[$Name]["Hooks"] = array();
2470  }
2471  }
2472 
2479  public function IsRegisteredEvent($EventName)
2480  {
2481  return array_key_exists($EventName, $this->RegisteredEvents)
2482  ? TRUE : FALSE;
2483  }
2484 
2491  public function IsHookedEvent($EventName)
2492  {
2493  # the event isn't hooked to if it isn't even registered
2494  if (!$this->IsRegisteredEvent($EventName))
2495  {
2496  return FALSE;
2497  }
2498 
2499  # return TRUE if there is at least one callback hooked to the event
2500  return count($this->RegisteredEvents[$EventName]["Hooks"]) > 0;
2501  }
2502 
2516  public function HookEvent(
2517  $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2518  {
2519  # convert parameters to array if not already in that form
2520  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2521  : array($EventsOrEventName => $Callback);
2522 
2523  # for each event
2524  $Success = TRUE;
2525  foreach ($Events as $EventName => $EventCallback)
2526  {
2527  # if callback is valid
2528  if (is_callable($EventCallback))
2529  {
2530  # if this is a periodic event we process internally
2531  if (isset($this->PeriodicEvents[$EventName]))
2532  {
2533  # process event now
2534  $this->ProcessPeriodicEvent($EventName, $EventCallback);
2535  }
2536  # if specified event has been registered
2537  elseif (isset($this->RegisteredEvents[$EventName]))
2538  {
2539  # add callback for event
2540  $this->RegisteredEvents[$EventName]["Hooks"][]
2541  = array("Callback" => $EventCallback, "Order" => $Order);
2542 
2543  # sort callbacks by order
2544  if (count($this->RegisteredEvents[$EventName]["Hooks"]) > 1)
2545  {
2546  usort($this->RegisteredEvents[$EventName]["Hooks"],
2547  function ($A, $B) {
2548  return StdLib::SortCompare(
2549  $A["Order"], $B["Order"]);
2550  });
2551  }
2552  }
2553  else
2554  {
2555  $Success = FALSE;
2556  }
2557  }
2558  else
2559  {
2560  $Success = FALSE;
2561  }
2562  }
2563 
2564  # report to caller whether all callbacks were hooked
2565  return $Success;
2566  }
2567 
2581  public function UnhookEvent(
2582  $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2583  {
2584  # convert parameters to array if not already in that form
2585  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2586  : array($EventsOrEventName => $Callback);
2587 
2588  # for each event
2589  $UnhookCount = 0;
2590  foreach ($Events as $EventName => $EventCallback)
2591  {
2592  # if this event has been registered and hooked
2593  if (isset($this->RegisteredEvents[$EventName])
2594  && count($this->RegisteredEvents[$EventName]))
2595  {
2596  # if this callback has been hooked for this event
2597  $CallbackData = array("Callback" => $EventCallback, "Order" => $Order);
2598  if (in_array($CallbackData,
2599  $this->RegisteredEvents[$EventName]["Hooks"]))
2600  {
2601  # unhook callback
2602  $HookIndex = array_search($CallbackData,
2603  $this->RegisteredEvents[$EventName]["Hooks"]);
2604  unset($this->RegisteredEvents[$EventName]["Hooks"][$HookIndex]);
2605  $UnhookCount++;
2606  }
2607  }
2608  }
2609 
2610  # report number of callbacks unhooked to caller
2611  return $UnhookCount;
2612  }
2613 
2624  public function SignalEvent($EventName, $Parameters = NULL)
2625  {
2626  $ReturnValue = NULL;
2627 
2628  # if event has been registered
2629  if (isset($this->RegisteredEvents[$EventName]))
2630  {
2631  # set up default return value (if not NULL)
2632  switch ($this->RegisteredEvents[$EventName]["Type"])
2633  {
2634  case self::EVENTTYPE_CHAIN:
2635  $ReturnValue = $Parameters;
2636  break;
2637 
2638  case self::EVENTTYPE_NAMED:
2639  $ReturnValue = array();
2640  break;
2641  }
2642 
2643  # for each callback for this event
2644  foreach ($this->RegisteredEvents[$EventName]["Hooks"] as $Hook)
2645  {
2646  # invoke callback
2647  $Callback = $Hook["Callback"];
2648  $Result = ($Parameters !== NULL)
2649  ? call_user_func_array($Callback, $Parameters)
2650  : call_user_func($Callback);
2651 
2652  # process return value based on event type
2653  switch ($this->RegisteredEvents[$EventName]["Type"])
2654  {
2655  case self::EVENTTYPE_CHAIN:
2656  if ($Result !== NULL)
2657  {
2658  foreach ($Parameters as $Index => $Value)
2659  {
2660  if (array_key_exists($Index, $Result))
2661  {
2662  $Parameters[$Index] = $Result[$Index];
2663  }
2664  }
2665  $ReturnValue = $Parameters;
2666  }
2667  break;
2668 
2669  case self::EVENTTYPE_FIRST:
2670  if ($Result !== NULL)
2671  {
2672  $ReturnValue = $Result;
2673  break 2;
2674  }
2675  break;
2676 
2677  case self::EVENTTYPE_NAMED:
2678  $CallbackName = is_array($Callback)
2679  ? (is_object($Callback[0])
2680  ? get_class($Callback[0])
2681  : $Callback[0])."::".$Callback[1]
2682  : $Callback;
2683  $ReturnValue[$CallbackName] = $Result;
2684  break;
2685 
2686  default:
2687  break;
2688  }
2689  }
2690  }
2691  else
2692  {
2693  $this->LogError(self::LOGLVL_WARNING,
2694  "Unregistered event (".$EventName.") signaled by "
2695  .StdLib::GetMyCaller().".");
2696  }
2697 
2698  # return value if any to caller
2699  return $ReturnValue;
2700  }
2701 
2707  public function IsStaticOnlyEvent($EventName)
2708  {
2709  return isset($this->PeriodicEvents[$EventName]) ? TRUE : FALSE;
2710  }
2711 
2722  public function EventWillNextRunAt($EventName, $Callback)
2723  {
2724  # if event is not a periodic event report failure to caller
2725  if (!array_key_exists($EventName, $this->EventPeriods)) { return FALSE; }
2726 
2727  # retrieve last execution time for event if available
2728  $Signature = self::GetCallbackSignature($Callback);
2729  $LastRunTime = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
2730  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
2731 
2732  # if event was not found report failure to caller
2733  if ($LastRunTime === NULL) { return FALSE; }
2734 
2735  # calculate next run time based on event period
2736  $NextRunTime = strtotime($LastRunTime) + $this->EventPeriods[$EventName];
2737 
2738  # report next run time to caller
2739  return $NextRunTime;
2740  }
2741 
2757  public function GetKnownPeriodicEvents()
2758  {
2759  # retrieve last execution times
2760  $this->DB->Query("SELECT * FROM PeriodicEvents");
2761  $LastRunTimes = $this->DB->FetchColumn("LastRunAt", "Signature");
2762 
2763  # for each known event
2764  $Events = array();
2765  foreach ($this->KnownPeriodicEvents as $Signature => $Info)
2766  {
2767  # if last run time for event is available
2768  if (array_key_exists($Signature, $LastRunTimes))
2769  {
2770  # calculate next run time for event
2771  $LastRun = strtotime($LastRunTimes[$Signature]);
2772  $NextRun = $LastRun + $this->EventPeriods[$Info["Period"]];
2773  if ($Info["Period"] == "EVENT_PERIODIC") { $LastRun = FALSE; }
2774  }
2775  else
2776  {
2777  # set info to indicate run times are not known
2778  $LastRun = FALSE;
2779  $NextRun = FALSE;
2780  }
2781 
2782  # add event info to list
2783  $Events[$Signature] = $Info;
2784  $Events[$Signature]["LastRun"] = $LastRun;
2785  $Events[$Signature]["NextRun"] = $NextRun;
2786  $Events[$Signature]["Parameters"] = NULL;
2787  }
2788 
2789  # return list of known events to caller
2790  return $Events;
2791  }
2792 
2799  public static function RunPeriodicEvent($EventName, $Callback, $Parameters)
2800  {
2801  static $DB;
2802  if (!isset($DB)) { $DB = new Database(); }
2803 
2804  # run event
2805  $ReturnVal = call_user_func_array($Callback, $Parameters);
2806 
2807  # if event is already in database
2808  $Signature = self::GetCallbackSignature($Callback);
2809  if ($DB->Query("SELECT COUNT(*) AS EventCount FROM PeriodicEvents"
2810  ." WHERE Signature = '".addslashes($Signature)."'", "EventCount"))
2811  {
2812  # update last run time for event
2813  $DB->Query("UPDATE PeriodicEvents SET LastRunAt = "
2814  .(($EventName == "EVENT_PERIODIC")
2815  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
2816  : "NOW()")
2817  ." WHERE Signature = '".addslashes($Signature)."'");
2818  }
2819  else
2820  {
2821  # add last run time for event to database
2822  $DB->Query("INSERT INTO PeriodicEvents (Signature, LastRunAt) VALUES "
2823  ."('".addslashes($Signature)."', "
2824  .(($EventName == "EVENT_PERIODIC")
2825  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
2826  : "NOW()").")");
2827  }
2828  }
2829 
2830  /*@)*/ /* Event Handling */
2831 
2832 
2833  # ---- Task Management ---------------------------------------------------
2834  /*@(*/
2836 
2838  const PRIORITY_HIGH = 1;
2840  const PRIORITY_MEDIUM = 2;
2842  const PRIORITY_LOW = 3;
2845 
2858  public function QueueTask($Callback, $Parameters = NULL,
2859  $Priority = self::PRIORITY_LOW, $Description = "")
2860  {
2861  # pack task info and write to database
2862  if ($Parameters === NULL) { $Parameters = array(); }
2863  $this->DB->Query("INSERT INTO TaskQueue"
2864  ." (Callback, Parameters, Priority, Description)"
2865  ." VALUES ('".addslashes(serialize($Callback))."', '"
2866  .addslashes(serialize($Parameters))."', ".intval($Priority).", '"
2867  .addslashes($Description)."')");
2868  }
2869 
2887  public function QueueUniqueTask($Callback, $Parameters = NULL,
2888  $Priority = self::PRIORITY_LOW, $Description = "")
2889  {
2890  if ($this->TaskIsInQueue($Callback, $Parameters))
2891  {
2892  $QueryResult = $this->DB->Query("SELECT TaskId,Priority FROM TaskQueue"
2893  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2894  .($Parameters ? " AND Parameters = '"
2895  .addslashes(serialize($Parameters))."'" : ""));
2896  if ($QueryResult !== FALSE)
2897  {
2898  $Record = $this->DB->FetchRow();
2899  if ($Record["Priority"] > $Priority)
2900  {
2901  $this->DB->Query("UPDATE TaskQueue"
2902  ." SET Priority = ".intval($Priority)
2903  ." WHERE TaskId = ".intval($Record["TaskId"]));
2904  }
2905  }
2906  return FALSE;
2907  }
2908  else
2909  {
2910  $this->QueueTask($Callback, $Parameters, $Priority, $Description);
2911  return TRUE;
2912  }
2913  }
2914 
2924  public function TaskIsInQueue($Callback, $Parameters = NULL)
2925  {
2926  $QueuedCount = $this->DB->Query(
2927  "SELECT COUNT(*) AS FoundCount FROM TaskQueue"
2928  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2929  .($Parameters ? " AND Parameters = '"
2930  .addslashes(serialize($Parameters))."'" : ""),
2931  "FoundCount");
2932  $RunningCount = $this->DB->Query(
2933  "SELECT COUNT(*) AS FoundCount FROM RunningTasks"
2934  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2935  .($Parameters ? " AND Parameters = '"
2936  .addslashes(serialize($Parameters))."'" : ""),
2937  "FoundCount");
2938  $FoundCount = $QueuedCount + $RunningCount;
2939  return ($FoundCount ? TRUE : FALSE);
2940  }
2941 
2947  public function GetTaskQueueSize($Priority = NULL)
2948  {
2949  return $this->GetQueuedTaskCount(NULL, NULL, $Priority);
2950  }
2951 
2959  public function GetQueuedTaskList($Count = 100, $Offset = 0)
2960  {
2961  return $this->GetTaskList("SELECT * FROM TaskQueue"
2962  ." ORDER BY Priority, TaskId ", $Count, $Offset);
2963  }
2964 
2978  public function GetQueuedTaskCount($Callback = NULL,
2979  $Parameters = NULL, $Priority = NULL, $Description = NULL)
2980  {
2981  $Query = "SELECT COUNT(*) AS TaskCount FROM TaskQueue";
2982  $Sep = " WHERE";
2983  if ($Callback !== NULL)
2984  {
2985  $Query .= $Sep." Callback = '".addslashes(serialize($Callback))."'";
2986  $Sep = " AND";
2987  }
2988  if ($Parameters !== NULL)
2989  {
2990  $Query .= $Sep." Parameters = '".addslashes(serialize($Parameters))."'";
2991  $Sep = " AND";
2992  }
2993  if ($Priority !== NULL)
2994  {
2995  $Query .= $Sep." Priority = ".intval($Priority);
2996  $Sep = " AND";
2997  }
2998  if ($Description !== NULL)
2999  {
3000  $Query .= $Sep." Description = '".addslashes($Description)."'";
3001  }
3002  return $this->DB->Query($Query, "TaskCount");
3003  }
3004 
3012  public function GetRunningTaskList($Count = 100, $Offset = 0)
3013  {
3014  return $this->GetTaskList("SELECT * FROM RunningTasks"
3015  ." WHERE StartedAt >= '".date("Y-m-d H:i:s",
3016  (time() - $this->MaxExecutionTime()))."'"
3017  ." ORDER BY StartedAt", $Count, $Offset);
3018  }
3019 
3027  public function GetOrphanedTaskList($Count = 100, $Offset = 0)
3028  {
3029  return $this->GetTaskList("SELECT * FROM RunningTasks"
3030  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
3031  (time() - $this->MaxExecutionTime()))."'"
3032  ." ORDER BY StartedAt", $Count, $Offset);
3033  }
3034 
3039  public function GetOrphanedTaskCount()
3040  {
3041  return $this->DB->Query("SELECT COUNT(*) AS Count FROM RunningTasks"
3042  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
3043  (time() - $this->MaxExecutionTime()))."'",
3044  "Count");
3045  }
3046 
3052  public function ReQueueOrphanedTask($TaskId, $NewPriority = NULL)
3053  {
3054  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
3055  $this->DB->Query("INSERT INTO TaskQueue"
3056  ." (Callback,Parameters,Priority,Description) "
3057  ."SELECT Callback, Parameters, Priority, Description"
3058  ." FROM RunningTasks WHERE TaskId = ".intval($TaskId));
3059  if ($NewPriority !== NULL)
3060  {
3061  $NewTaskId = $this->DB->LastInsertId();
3062  $this->DB->Query("UPDATE TaskQueue SET Priority = "
3063  .intval($NewPriority)
3064  ." WHERE TaskId = ".intval($NewTaskId));
3065  }
3066  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
3067  $this->DB->Query("UNLOCK TABLES");
3068  }
3069 
3076  public function RequeueCurrentTask($NewValue = TRUE)
3077  {
3078  $this->RequeueCurrentTask = $NewValue;
3079  }
3080 
3086  public function DeleteTask($TaskId)
3087  {
3088  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = ".intval($TaskId));
3089  $TasksRemoved = $this->DB->NumRowsAffected();
3090  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
3091  $TasksRemoved += $this->DB->NumRowsAffected();
3092  return $TasksRemoved;
3093  }
3094 
3102  public function GetTask($TaskId)
3103  {
3104  # assume task will not be found
3105  $Task = NULL;
3106 
3107  # look for task in task queue
3108  $this->DB->Query("SELECT * FROM TaskQueue WHERE TaskId = ".intval($TaskId));
3109 
3110  # if task was not found in queue
3111  if (!$this->DB->NumRowsSelected())
3112  {
3113  # look for task in running task list
3114  $this->DB->Query("SELECT * FROM RunningTasks WHERE TaskId = "
3115  .intval($TaskId));
3116  }
3117 
3118  # if task was found
3119  if ($this->DB->NumRowsSelected())
3120  {
3121  # if task was periodic
3122  $Row = $this->DB->FetchRow();
3123  if ($Row["Callback"] ==
3124  serialize(array("ApplicationFramework", "RunPeriodicEvent")))
3125  {
3126  # unpack periodic task callback
3127  $WrappedCallback = unserialize($Row["Parameters"]);
3128  $Task["Callback"] = $WrappedCallback[1];
3129  $Task["Parameters"] = $WrappedCallback[2];
3130  }
3131  else
3132  {
3133  # unpack task callback and parameters
3134  $Task["Callback"] = unserialize($Row["Callback"]);
3135  $Task["Parameters"] = unserialize($Row["Parameters"]);
3136  }
3137  }
3138 
3139  # return task to caller
3140  return $Task;
3141  }
3142 
3153  public function TaskExecutionEnabled($NewValue = DB_NOVALUE, $Persistent = FALSE)
3154  {
3155  return $this->UpdateSetting(__FUNCTION__, $NewValue, $Persistent);
3156  }
3157 
3166  public function MaxTasks($NewValue = DB_NOVALUE, $Persistent = FALSE)
3167  {
3168  return $this->UpdateSetting("MaxTasksRunning", $NewValue, $Persistent);
3169  }
3170 
3178  public static function GetTaskCallbackSynopsis($TaskInfo)
3179  {
3180  # if task callback is function use function name
3181  $Callback = $TaskInfo["Callback"];
3182  $Name = "";
3183  if (!is_array($Callback))
3184  {
3185  $Name = $Callback;
3186  }
3187  else
3188  {
3189  # if task callback is object
3190  if (is_object($Callback[0]))
3191  {
3192  # if task callback is encapsulated ask encapsulation for name
3193  if (method_exists($Callback[0], "GetCallbackAsText"))
3194  {
3195  $Name = $Callback[0]->GetCallbackAsText();
3196  }
3197  # else assemble name from object
3198  else
3199  {
3200  $Name = get_class($Callback[0]) . "::" . $Callback[1];
3201  }
3202  }
3203  # else assemble name from supplied info
3204  else
3205  {
3206  $Name= $Callback[0] . "::" . $Callback[1];
3207  }
3208  }
3209 
3210  # if parameter array was supplied
3211  $Parameters = $TaskInfo["Parameters"];
3212  $ParameterString = "";
3213  if (is_array($Parameters))
3214  {
3215  # assemble parameter string
3216  $Separator = "";
3217  foreach ($Parameters as $Parameter)
3218  {
3219  $ParameterString .= $Separator;
3220  if (is_int($Parameter) || is_float($Parameter))
3221  {
3222  $ParameterString .= $Parameter;
3223  }
3224  else if (is_string($Parameter))
3225  {
3226  $ParameterString .= "\"".htmlspecialchars($Parameter)."\"";
3227  }
3228  else if (is_array($Parameter))
3229  {
3230  $ParameterString .= "ARRAY";
3231  }
3232  else if (is_object($Parameter))
3233  {
3234  $ParameterString .= "OBJECT";
3235  }
3236  else if (is_null($Parameter))
3237  {
3238  $ParameterString .= "NULL";
3239  }
3240  else if (is_bool($Parameter))
3241  {
3242  $ParameterString .= $Parameter ? "TRUE" : "FALSE";
3243  }
3244  else if (is_resource($Parameter))
3245  {
3246  $ParameterString .= get_resource_type($Parameter);
3247  }
3248  else
3249  {
3250  $ParameterString .= "????";
3251  }
3252  $Separator = ", ";
3253  }
3254  }
3255 
3256  # assemble name and parameters and return result to caller
3257  return $Name."(".$ParameterString.")";
3258  }
3259 
3264  public function IsRunningInBackground()
3265  {
3266  return $this->RunningInBackground;
3267  }
3268 
3275  {
3276  return isset($this->RunningTask)
3277  ? $this->RunningTask["Priority"] : NULL;
3278  }
3279 
3288  public function GetNextHigherBackgroundPriority($Priority = NULL)
3289  {
3290  if ($Priority === NULL)
3291  {
3292  $Priority = $this->GetCurrentBackgroundPriority();
3293  if ($Priority === NULL)
3294  {
3295  return NULL;
3296  }
3297  }
3298  return ($Priority > self::PRIORITY_HIGH)
3299  ? ($Priority - 1) : self::PRIORITY_HIGH;
3300  }
3301 
3310  public function GetNextLowerBackgroundPriority($Priority = NULL)
3311  {
3312  if ($Priority === NULL)
3313  {
3314  $Priority = $this->GetCurrentBackgroundPriority();
3315  if ($Priority === NULL)
3316  {
3317  return NULL;
3318  }
3319  }
3320  return ($Priority < self::PRIORITY_BACKGROUND)
3321  ? ($Priority + 1) : self::PRIORITY_BACKGROUND;
3322  }
3323 
3324  /*@)*/ /* Task Management */
3325 
3326 
3327  # ---- Clean URL Support -------------------------------------------------
3328  /*@(*/
3330 
3357  public function AddCleanUrl($Pattern, $Page, $GetVars = NULL, $Template = NULL)
3358  {
3359  # save clean URL mapping parameters
3360  $this->CleanUrlMappings[] = array(
3361  "Pattern" => $Pattern,
3362  "Page" => $Page,
3363  "GetVars" => $GetVars,
3364  "AddedBy" => StdLib::GetCallerInfo(),
3365  );
3366 
3367  # if replacement template specified
3368  if ($Template !== NULL)
3369  {
3370  # if GET parameters specified
3371  if (count($GetVars))
3372  {
3373  # retrieve all possible permutations of GET parameters
3374  $GetPerms = StdLib::ArrayPermutations(array_keys($GetVars));
3375 
3376  # for each permutation of GET parameters
3377  foreach ($GetPerms as $VarPermutation)
3378  {
3379  # construct search pattern for permutation
3380  $SearchPattern = "/href=([\"'])index\\.php\\?P=".$Page;
3381  $GetVarSegment = "";
3382  foreach ($VarPermutation as $GetVar)
3383  {
3384  if (preg_match("%\\\$[0-9]+%", $GetVars[$GetVar]))
3385  {
3386  $GetVarSegment .= "&amp;".$GetVar."=((?:(?!\\1)[^&])+)";
3387  }
3388  else
3389  {
3390  $GetVarSegment .= "&amp;".$GetVar."=".$GetVars[$GetVar];
3391  }
3392  }
3393  $SearchPattern .= $GetVarSegment."\\1/i";
3394 
3395  # if template is actually a callback
3396  if (is_callable($Template))
3397  {
3398  # add pattern to HTML output mod callbacks list
3399  $this->OutputModificationCallbacks[] = array(
3400  "Pattern" => $Pattern,
3401  "Page" => $Page,
3402  "SearchPattern" => $SearchPattern,
3403  "Callback" => $Template,
3404  );
3405  }
3406  else
3407  {
3408  # construct replacement string for permutation
3409  $Replacement = $Template;
3410  $Index = 2;
3411  foreach ($VarPermutation as $GetVar)
3412  {
3413  $Replacement = str_replace(
3414  "\$".$GetVar, "\$".$Index, $Replacement);
3415  $Index++;
3416  }
3417  $Replacement = "href=\"".$Replacement."\"";
3418 
3419  # add pattern to HTML output modifications list
3420  $this->OutputModificationPatterns[] = $SearchPattern;
3421  $this->OutputModificationReplacements[] = $Replacement;
3422  }
3423  }
3424  }
3425  else
3426  {
3427  # construct search pattern
3428  $SearchPattern = "/href=\"index\\.php\\?P=".$Page."\"/i";
3429 
3430  # if template is actually a callback
3431  if (is_callable($Template))
3432  {
3433  # add pattern to HTML output mod callbacks list
3434  $this->OutputModificationCallbacks[] = array(
3435  "Pattern" => $Pattern,
3436  "Page" => $Page,
3437  "SearchPattern" => $SearchPattern,
3438  "Callback" => $Template,
3439  );
3440  }
3441  else
3442  {
3443  # add simple pattern to HTML output modifications list
3444  $this->OutputModificationPatterns[] = $SearchPattern;
3445  $this->OutputModificationReplacements[] = "href=\"".$Template."\"";
3446  }
3447  }
3448  }
3449  }
3450 
3456  public function CleanUrlIsMapped($Path)
3457  {
3458  foreach ($this->CleanUrlMappings as $Info)
3459  {
3460  if (preg_match($Info["Pattern"], $Path))
3461  {
3462  return TRUE;
3463  }
3464  }
3465  return FALSE;
3466  }
3467 
3477  public function GetCleanUrlForPath($Path)
3478  {
3479  # the search patterns and callbacks require a specific format
3480  $Format = "href=\"".str_replace("&", "&amp;", $Path)."\"";
3481  $Search = $Format;
3482 
3483  # perform any regular expression replacements on the search string
3484  $Search = preg_replace($this->OutputModificationPatterns,
3485  $this->OutputModificationReplacements, $Search);
3486 
3487  # only run the callbacks if a replacement hasn't already been performed
3488  if ($Search == $Format)
3489  {
3490  # perform any callback replacements on the search string
3491  foreach ($this->OutputModificationCallbacks as $Info)
3492  {
3493  # make the information available to the callback
3494  $this->OutputModificationCallbackInfo = $Info;
3495 
3496  # execute the callback
3497  $Search = preg_replace_callback($Info["SearchPattern"],
3498  array($this, "OutputModificationCallbackShell"),
3499  $Search);
3500  }
3501  }
3502 
3503  # return the path untouched if no replacements were performed
3504  if ($Search == $Format)
3505  {
3506  return $Path;
3507  }
3508 
3509  # remove the bits added to the search string to get it recognized by
3510  # the replacement expressions and callbacks
3511  $Result = substr($Search, 6, -1);
3512 
3513  return $Result;
3514  }
3515 
3522  public function GetUncleanUrlForPath($Path)
3523  {
3524  # for each clean URL mapping
3525  foreach ($this->CleanUrlMappings as $Info)
3526  {
3527  # if current path matches the clean URL pattern
3528  if (preg_match($Info["Pattern"], $Path, $Matches))
3529  {
3530  # the GET parameters for the URL, starting with the page name
3531  $GetVars = array("P" => $Info["Page"]);
3532 
3533  # if additional $_GET variables specified for clean URL
3534  if ($Info["GetVars"] !== NULL)
3535  {
3536  # for each $_GET variable specified for clean URL
3537  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
3538  {
3539  # start with template for variable value
3540  $Value = $VarTemplate;
3541 
3542  # for each subpattern matched in current URL
3543  foreach ($Matches as $Index => $Match)
3544  {
3545  # if not first (whole) match
3546  if ($Index > 0)
3547  {
3548  # make any substitutions in template
3549  $Value = str_replace("$".$Index, $Match, $Value);
3550  }
3551  }
3552 
3553  # add the GET variable
3554  $GetVars[$VarName] = $Value;
3555  }
3556  }
3557 
3558  # return the unclean URL
3559  return "index.php?" . http_build_query($GetVars);
3560  }
3561  }
3562 
3563  # return the path unchanged
3564  return $Path;
3565  }
3566 
3572  public function GetCleanUrl()
3573  {
3574  return $this->GetCleanUrlForPath($this->GetUncleanUrl());
3575  }
3576 
3581  public function GetUncleanUrl()
3582  {
3583  $GetVars = array("P" => $this->GetPageName()) + $_GET;
3584  return "index.php?" . http_build_query($GetVars);
3585  }
3586 
3594  public function GetCleanUrlList()
3595  {
3596  return $this->CleanUrlMappings;
3597  }
3598 
3611  public function AddPrefixForAlternateDomain($Domain, $Prefix)
3612  {
3613  $this->AlternateDomainPrefixes[$Domain] = $Prefix;
3614  }
3615 
3616 
3621  public function GetAlternateDomains()
3622  {
3623  return array_keys($this->AlternateDomainPrefixes);
3624  }
3625 
3632  public function GetPrefixForAlternateDomain($Domain)
3633  {
3634  return isset($this->AlternateDomainPrefixes[$Domain]) ?
3635  $this->AlternateDomainPrefixes[$Domain] : NULL;
3636  }
3637 
3638  /*@)*/ /* Clean URL Support */
3639 
3640  # ---- Server Environment ------------------------------------------------
3641  /*@(*/
3643 
3649  public static function SessionLifetime($NewValue = NULL)
3650  {
3651  if ($NewValue !== NULL)
3652  {
3653  self::$SessionLifetime = $NewValue;
3654  }
3655  return self::$SessionLifetime;
3656  }
3657 
3663  public static function HtaccessSupport()
3664  {
3665  return isset($_SERVER["HTACCESS_SUPPORT"])
3666  || isset($_SERVER["REDIRECT_HTACCESS_SUPPORT"]);
3667  }
3668 
3675  public static function UrlFingerprintingRewriteSupport()
3676  {
3677  return isset($_SERVER["URL_FINGERPRINTING_SUPPORT"])
3678  || isset($_SERVER["REDIRECT_URL_FINGERPRINTING_SUPPORT"]);
3679  }
3680 
3687  public static function ScssRewriteSupport()
3688  {
3689  return isset($_SERVER["SCSS_REWRITE_SUPPORT"])
3690  || isset($_SERVER["REDIRECT_SCSS_REWRITE_SUPPORT"]);
3691  }
3692 
3699  public static function JsMinRewriteSupport()
3700  {
3701  return isset($_SERVER["JSMIN_REWRITE_SUPPORT"])
3702  || isset($_SERVER["REDIRECT_JSMIN_REWRITE_SUPPORT"]);
3703  }
3704 
3712  public static function RootUrl()
3713  {
3714  # return override value if one is set
3715  if (self::$RootUrlOverride !== NULL)
3716  {
3717  return self::$RootUrlOverride;
3718  }
3719 
3720  # determine scheme name
3721  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
3722 
3723  # if HTTP_HOST is preferred or SERVER_NAME points to localhost
3724  # and HTTP_HOST is set
3725  if ((self::$PreferHttpHost || ($_SERVER["SERVER_NAME"] == "127.0.0.1"))
3726  && isset($_SERVER["HTTP_HOST"]))
3727  {
3728  # use HTTP_HOST for domain name
3729  $DomainName = $_SERVER["HTTP_HOST"];
3730  }
3731  else
3732  {
3733  # use SERVER_NAME for domain name
3734  $DomainName = $_SERVER["SERVER_NAME"];
3735  }
3736 
3737  # build URL root and return to caller
3738  return $Protocol."://".$DomainName;
3739  }
3740 
3755  public static function RootUrlOverride($NewValue = self::NOVALUE)
3756  {
3757  if ($NewValue !== self::NOVALUE)
3758  {
3759  self::$RootUrlOverride = strlen(trim($NewValue)) ? $NewValue : NULL;
3760  }
3761  return self::$RootUrlOverride;
3762  }
3763 
3773  public static function BaseUrl()
3774  {
3775  $BaseUrl = self::RootUrl().dirname($_SERVER["SCRIPT_NAME"]);
3776  if (substr($BaseUrl, -1) != "/") { $BaseUrl .= "/"; }
3777  return $BaseUrl;
3778  }
3779 
3787  public static function FullUrl()
3788  {
3789  return self::RootUrl().$_SERVER["REQUEST_URI"];
3790  }
3791 
3802  public static function PreferHttpHost($NewValue = NULL)
3803  {
3804  if ($NewValue !== NULL)
3805  {
3806  self::$PreferHttpHost = ($NewValue ? TRUE : FALSE);
3807  }
3808  return self::$PreferHttpHost;
3809  }
3810 
3815  public static function BasePath()
3816  {
3817  $BasePath = dirname($_SERVER["SCRIPT_NAME"]);
3818 
3819  if (substr($BasePath, -1) != "/")
3820  {
3821  $BasePath .= "/";
3822  }
3823 
3824  return $BasePath;
3825  }
3826 
3832  public static function GetScriptUrl()
3833  {
3834  if (array_key_exists("SCRIPT_URL", $_SERVER))
3835  {
3836  return $_SERVER["SCRIPT_URL"];
3837  }
3838  elseif (array_key_exists("REQUEST_URI", $_SERVER))
3839  {
3840  $Pieces = parse_url($_SERVER["REQUEST_URI"]);
3841  return isset($Pieces["path"]) ? $Pieces["path"] : NULL;
3842  }
3843  elseif (array_key_exists("REDIRECT_URL", $_SERVER))
3844  {
3845  return $_SERVER["REDIRECT_URL"];
3846  }
3847  else
3848  {
3849  return NULL;
3850  }
3851  }
3852 
3861  public static function WasUrlRewritten($ScriptName="index.php")
3862  {
3863  # needed to get the path of the URL minus the query and fragment pieces
3864  $Components = parse_url(self::GetScriptUrl());
3865 
3866  # if parsing was successful and a path is set
3867  if (is_array($Components) && isset($Components["path"]))
3868  {
3869  $BasePath = self::BasePath();
3870  $Path = $Components["path"];
3871 
3872  # the URL was rewritten if the path isn't the base path, i.e., the
3873  # home page, and the file in the URL isn't the script generating the
3874  # page
3875  if ($BasePath != $Path && basename($Path) != $ScriptName)
3876  {
3877  return TRUE;
3878  }
3879  }
3880 
3881  # the URL wasn't rewritten
3882  return FALSE;
3883  }
3884 
3894  public static function ReachedViaAjax($NewSetting = NULL)
3895  {
3896  if ($NewSetting !== NULL)
3897  {
3898  self::$IsAjaxPageLoad = $NewSetting;
3899  }
3900 
3901  if (isset(self::$IsAjaxPageLoad))
3902  {
3903  return self::$IsAjaxPageLoad;
3904  }
3905  elseif (isset($_SERVER["HTTP_X_REQUESTED_WITH"])
3906  && (strtolower($_SERVER["HTTP_X_REQUESTED_WITH"])
3907  == "xmlhttprequest"))
3908  {
3909  return TRUE;
3910  }
3911  else
3912  {
3913  return FALSE;
3914  }
3915  }
3916 
3922  public static function GetFreeMemory()
3923  {
3924  return self::GetPhpMemoryLimit() - memory_get_usage(TRUE);
3925  }
3926 
3932  public static function GetPhpMemoryLimit()
3933  {
3934  return self::ConvertPhpIniSizeToBytes(
3935  ini_get("memory_limit"));
3936  }
3937 
3944  public static function GetPhpMaxUploadSize()
3945  {
3946  return min(
3947  self::ConvertPhpIniSizeToBytes(
3948  ini_get("post_max_size")),
3949  self::ConvertPhpIniSizeToBytes(
3950  ini_get("upload_max_filesize")));
3951  }
3952 
3965  public function MaxExecutionTime($NewValue = DB_NOVALUE, $Persistent = FALSE)
3966  {
3967  if ($NewValue !== DB_NOVALUE)
3968  {
3969  $NewValue = max($NewValue, 5);
3970  ini_set("max_execution_time", $NewValue);
3971  set_time_limit($NewValue - $this->GetElapsedExecutionTime());
3972  $this->UpdateSetting("MaxExecTime", $NewValue, $Persistent);
3973  }
3974  return ini_get("max_execution_time");
3975  }
3976 
3977  /*@)*/ /* Server Environment */
3978 
3979 
3980  # ---- Utility -----------------------------------------------------------
3981  /*@(*/
3983 
3995  public function DownloadFile($FilePath, $FileName = NULL, $MimeType = NULL)
3996  {
3997  # check that file is readable
3998  if (!is_readable($FilePath))
3999  {
4000  return FALSE;
4001  }
4002 
4003  # if file name was not supplied
4004  if ($FileName === NULL)
4005  {
4006  # extract file name from path
4007  $FileName = basename($FilePath);
4008  }
4009 
4010  # if MIME type was not supplied
4011  if ($MimeType === NULL)
4012  {
4013  # attempt to determine MIME type
4014  $FInfoHandle = finfo_open(FILEINFO_MIME);
4015  if ($FInfoHandle)
4016  {
4017  $FInfoMime = finfo_file($FInfoHandle, $FilePath);
4018  finfo_close($FInfoHandle);
4019  if ($FInfoMime)
4020  {
4021  $MimeType = $FInfoMime;
4022  }
4023  }
4024 
4025  # use default if unable to determine MIME type
4026  if ($MimeType === NULL)
4027  {
4028  $MimeType = "application/octet-stream";
4029  }
4030  }
4031 
4032  # list of mime types where we allow the browser to decide on
4033  # how to display the item by omitting the Content-Disposition
4034  # header
4035  $InlineTypes = [
4036  "image/gif",
4037  "image/jpeg",
4038  "image/png",
4039  "application/pdf",
4040  ];
4041 
4042  # set headers to download file
4043  header("Content-Type: ".$MimeType);
4044  header("Content-Length: ".filesize($FilePath));
4045  if ($this->CleanUrlRewritePerformed &&
4046  !in_array($MimeType, $InlineTypes))
4047  {
4048  header('Content-Disposition: attachment; filename="'.$FileName.'"');
4049  }
4050 
4051  # make sure that apache does not attempt to compress file
4052  apache_setenv('no-gzip', '1');
4053 
4054  # send file to user, but unbuffered to avoid memory issues
4055  $this->AddUnbufferedCallback(function ($File)
4056  {
4057  $BlockSize = 512000;
4058 
4059  $Handle = @fopen($File, "rb");
4060  if ($Handle === FALSE)
4061  {
4062  return;
4063  }
4064 
4065  # (close out session, making it read-only, so that session file
4066  # lock is released and others are not potentially hanging
4067  # waiting for it while the download completes)
4068  session_write_close();
4069 
4070  while (!feof($Handle))
4071  {
4072  print fread($Handle, $BlockSize);
4073  flush();
4074  }
4075 
4076  fclose($Handle);
4077  }, array($FilePath));
4078 
4079  # prevent HTML output that might interfere with download
4080  $this->SuppressHTMLOutput();
4081 
4082  # set flag to indicate not to log a slow page load in case client
4083  # connection delays PHP execution because of header
4084  $this->DoNotLogSlowPageLoad = TRUE;
4085 
4086  # report no errors found to caller
4087  return TRUE;
4088  }
4089 
4102  public function GetLock($LockName, $Wait = TRUE)
4103  {
4104  # assume we will not get a lock
4105  $GotLock = FALSE;
4106 
4107  # clear out any stale locks
4108  static $CleanupHasBeenDone = FALSE;
4109  if (!$CleanupHasBeenDone)
4110  {
4111  # (margin for clearing stale locks is twice the known
4112  # maximum PHP execution time, because the max time
4113  # techinically does not include external operations
4114  # like database queries)
4115  $ClearLocksObtainedBefore = date(StdLib::SQL_DATE_FORMAT,
4116  (time() - ($this->MaxExecutionTime() * 2)));
4117  $this->DB->Query("DELETE FROM AF_Locks WHERE"
4118  ." ObtainedAt < '".$ClearLocksObtainedBefore."' AND"
4119  ." LockName = '".addslashes($LockName)."'");
4120  }
4121 
4122  do
4123  {
4124  # lock database table so nobody else can try to get a lock
4125  $this->DB->Query("LOCK TABLES AF_Locks WRITE");
4126 
4127  # look for lock with specified name
4128  $FoundCount = $this->DB->Query("SELECT COUNT(*) AS FoundCount"
4129  ." FROM AF_Locks WHERE LockName = '"
4130  .addslashes($LockName)."'", "FoundCount");
4131  $LockFound = ($FoundCount > 0) ? TRUE : FALSE;
4132 
4133  # if lock found
4134  if ($LockFound)
4135  {
4136  # unlock database tables
4137  $this->DB->Query("UNLOCK TABLES");
4138 
4139  # if blocking was requested
4140  if ($Wait)
4141  {
4142  # wait to give someone else a chance to release lock
4143  sleep(2);
4144  }
4145  }
4146  // @codingStandardsIgnoreStart
4147  // (because phpcs does not correctly handle do-while loops)
4148  # while lock was found and blocking was requested
4149  } while ($LockFound && $Wait);
4150  // @codingStandardsIgnoreEnd
4151 
4152  # if lock was not found
4153  if (!$LockFound)
4154  {
4155  # get our lock
4156  $this->DB->Query("INSERT INTO AF_Locks (LockName) VALUES ('"
4157  .addslashes($LockName)."')");
4158  $GotLock = TRUE;
4159 
4160  # unlock database tables
4161  $this->DB->Query("UNLOCK TABLES");
4162  }
4163 
4164  # report to caller whether lock was obtained
4165  return $GotLock;
4166  }
4167 
4175  public function ReleaseLock($LockName)
4176  {
4177  # release any existing locks
4178  $this->DB->Query("DELETE FROM AF_Locks WHERE LockName = '"
4179  .addslashes($LockName)."'");
4180 
4181  # report to caller whether existing lock was released
4182  return $this->DB->NumRowsAffected() ? TRUE : FALSE;
4183  }
4184 
4203  public function BeginAjaxResponse($ResponseType = "JSON", $CloseSession=TRUE)
4204  {
4205  switch ($ResponseType)
4206  {
4207  case "JSON":
4208  $this->SuppressHTMLOutput();
4209  header("Content-Type: application/json; charset="
4210  .$GLOBALS["G_SysConfig"]->DefaultCharacterSet(), TRUE);
4211  break;
4212  case "XML":
4213  $this->SuppressHTMLOutput();
4214  header("Content-Type: application/xml; charset="
4215  .$GLOBALS["G_SysConfig"]->DefaultCharacterSet(), TRUE);
4216  break;
4217  case "HTML":
4218  break;
4219  default:
4220  throw new Exception(
4221  "Unsupported response type: ".$ResponseType);
4222  }
4223 
4225  self::$DefaultBrowserCacheExpiration);
4226 
4227  if ($CloseSession)
4228  {
4229  session_write_close();
4230  }
4231 
4232  }
4233 
4239  public function SetBrowserCacheExpirationTime($MaxAge)
4240  {
4241  # set headers to control caching
4242  header("Expires: ".gmdate("D, d M Y H:i:s \G\M\T", time()+$MaxAge));
4243  header("Cache-Control: private, max-age=".$MaxAge);
4244  header("Pragma:");
4245  }
4246 
4253  public static function ConvertPhpIniSizeToBytes($Size)
4254  {
4255  $Str = strtoupper($Size);
4256 
4257  # trim off 'B' suffix for KB/MB/GB
4258  if (substr($Str, -1) == "B")
4259  {
4260  $Str = substr($Str, 0, strlen($Str) - 1);
4261  }
4262 
4263  # pull out the numeric part of our setting
4264  $Val = intval($Str);
4265 
4266  # adjust it based on the units
4267  switch (substr($Str, -1))
4268  {
4269  case "G":
4270  $Val *= 1073741824;
4271  break;
4272 
4273  case "M":
4274  $Val *= 1048576;
4275  break;
4276 
4277  case "K":
4278  $Val *= 1024;
4279  break;
4280 
4281  default:
4282  break;
4283  }
4284 
4285  return $Val;
4286  }
4287 
4293  public function SessionInUse($InUse = NULL)
4294  {
4295  if ($InUse !== NULL)
4296  {
4297  $this->SessionInUse = $InUse;
4298  }
4299 
4300  return $this->SessionInUse;
4301  }
4302 
4303  /*@)*/ /* Utility */
4304 
4305 
4306  # ---- Backward Compatibility --------------------------------------------
4307  /*@(*/
4309 
4316  public function FindCommonTemplate($BaseName)
4317  {
4318  return $this->FindFile(
4319  $this->IncludeDirList, $BaseName, array("tpl", "html"));
4320  }
4321 
4322  /*@)*/ /* Backward Compatibility */
4323 
4324 
4325  # ---- PRIVATE INTERFACE -------------------------------------------------
4326 
4327  private $AdditionalRequiredUIFiles = array();
4328  private $AlternateDomainPrefixes = array();
4329  private $BackgroundTaskMemLeakLogThreshold = 10; # percentage of max mem
4330  private $BackgroundTaskMinFreeMemPercent = 25;
4331  private $BrowserDetectFunc;
4332  private $CacheCurrentPage = TRUE;
4333  private $CleanUrlMappings = array();
4334  private $CleanUrlRewritePerformed = FALSE;
4335  private $ContextFilters = array(
4336  self::CONTEXT_START => TRUE,
4337  self::CONTEXT_PAGE => array("H_"),
4338  self::CONTEXT_COMMON => array("H_"),
4339  );
4340  private $CssUrlFingerprintPath;
4341  private $DB;
4342  private $DefaultPage = "Home";
4343  private $DoNotMinimizeList = array();
4344  private $DoNotLogSlowPageLoad = FALSE;
4345  private $EnvIncludes = array();
4346  private $ExecutionStartTime;
4347  private $FoundUIFiles = array();
4348  private $HtmlCharset = "UTF-8";
4349  private $InterfaceSettings = array();
4350  private $JSMinimizerJavaScriptPackerAvailable = FALSE;
4351  private $JSMinimizerJShrinkAvailable = TRUE;
4352  private $JumpToPage = NULL;
4353  private $JumpToPageDelay = 0;
4354  private $LogFileName = "local/logs/site.log";
4355  private $MaxRunningTasksToTrack = 250;
4356  private $MetaTags = array();
4357  private $OutputModificationCallbackInfo;
4358  private $OutputModificationCallbacks = array();
4359  private $OutputModificationPatterns = array();
4360  private $OutputModificationReplacements = array();
4361  private $PageCacheTags = array();
4362  private $PageName;
4363  private $PostProcessingFuncs = array();
4364  private $RequeueCurrentTask;
4365  private $RunningInBackground = FALSE;
4366  private $RunningTask;
4367  private $SavedContext;
4368  private $SaveTemplateLocationCache = FALSE;
4369  private $SessionStorage;
4370  private $SessionGcProbability;
4371  private $Settings;
4372  private $SuppressHTML = FALSE;
4373  private $SuppressStdPageStartAndEnd = FALSE;
4374  private $TemplateLocationCache;
4375  private $TemplateLocationCacheInterval = 60; # in minutes
4376  private $TemplateLocationCacheExpiration;
4377  private $UnbufferedCallbacks = array();
4378  private $UniqueMetaTags = array();
4379  private $UrlFingerprintBlacklist = array();
4380  private $UseBaseTag = FALSE;
4381  private $SessionInUse = FALSE;
4382 
4383  private static $ActiveUI = "default";
4384  private static $AppName = "ScoutAF";
4385  private static $DefaultBrowserCacheExpiration = 30; # in seconds
4386  private static $DefaultUI = "default";
4387  private static $IsAjaxPageLoad;
4388  private static $JSMinCacheDir = "local/data/caches/JSMin";
4389  private static $NamespaceDirectories = array();
4390  private static $ObjectDirectories = array();
4391  private static $ObjectLocationCache;
4392  private static $ObjectLocationCacheInterval = 60;
4393  private static $ObjectLocationCacheExpiration;
4394  private static $PreferHttpHost = FALSE;
4395  private static $RootUrlOverride = NULL;
4396  private static $SaveObjectLocationCache = FALSE;
4397  private static $ScssCacheDir = "local/data/caches/SCSS";
4398  private static $SessionLifetime = 1440; # in seconds
4399  private static $UserInterfaceListCache = array();
4400  private static $UserInterfacePathsCache = array();
4401 
4402  # offset used to generate page cache tag IDs from numeric tags
4403  const PAGECACHETAGIDOFFSET = 100000;
4404 
4405  # minimum expired session garbage collection probability
4406  const MIN_GC_PROBABILITY = 0.01;
4407 
4412  private $NoTSR = FALSE;
4413 
4414  private $RegisteredEvents = array();
4415  private $KnownPeriodicEvents = array();
4416  private $PeriodicEvents = array(
4417  "EVENT_HOURLY" => self::EVENTTYPE_DEFAULT,
4418  "EVENT_DAILY" => self::EVENTTYPE_DEFAULT,
4419  "EVENT_WEEKLY" => self::EVENTTYPE_DEFAULT,
4420  "EVENT_MONTHLY" => self::EVENTTYPE_DEFAULT,
4421  "EVENT_PERIODIC" => self::EVENTTYPE_NAMED,
4422  );
4423  private $EventPeriods = array(
4424  "EVENT_HOURLY" => 3600,
4425  "EVENT_DAILY" => 86400,
4426  "EVENT_WEEKLY" => 604800,
4427  "EVENT_MONTHLY" => 2592000,
4428  "EVENT_PERIODIC" => 0,
4429  );
4430  private $UIEvents = array(
4431  "EVENT_PAGE_LOAD" => self::EVENTTYPE_DEFAULT,
4432  "EVENT_PHP_FILE_LOAD" => self::EVENTTYPE_CHAIN,
4433  "EVENT_PHP_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
4434  "EVENT_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
4435  "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
4436  "EVENT_PAGE_OUTPUT_FILTER" => self::EVENTTYPE_CHAIN,
4437  );
4438 
4443  private function LoadSettings()
4444  {
4445  # read settings in from database
4446  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
4447  $this->Settings = $this->DB->FetchRow();
4448 
4449  # if settings were not previously initialized
4450  if ($this->Settings === FALSE)
4451  {
4452  # initialize settings in database
4453  $this->DB->Query("INSERT INTO ApplicationFrameworkSettings"
4454  ." (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");
4455 
4456  # read new settings in from database
4457  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
4458  $this->Settings = $this->DB->FetchRow();
4459 
4460  # bail out if reloading new settings failed
4461  if ($this->Settings === FALSE)
4462  {
4463  throw new Exception(
4464  "Unable to load application framework settings.");
4465  }
4466  }
4467 
4468  # if base path was not previously set or we appear to have moved
4469  if (!array_key_exists("BasePath", $this->Settings)
4470  || (!strlen($this->Settings["BasePath"]))
4471  || (!array_key_exists("BasePathCheck", $this->Settings))
4472  || (__FILE__ != $this->Settings["BasePathCheck"]))
4473  {
4474  # attempt to extract base path from Apache .htaccess file
4475  if (is_readable(".htaccess"))
4476  {
4477  $Lines = file(".htaccess");
4478  foreach ($Lines as $Line)
4479  {
4480  if (preg_match("/\\s*RewriteBase\\s+/", $Line))
4481  {
4482  $Pieces = preg_split(
4483  "/\\s+/", $Line, NULL, PREG_SPLIT_NO_EMPTY);
4484  $BasePath = $Pieces[1];
4485  }
4486  }
4487  }
4488 
4489  # if base path was found
4490  if (isset($BasePath))
4491  {
4492  # save base path locally
4493  $this->Settings["BasePath"] = $BasePath;
4494 
4495  # save base path to database
4496  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
4497  ." SET BasePath = '".addslashes($BasePath)."'"
4498  .", BasePathCheck = '".addslashes(__FILE__)."'");
4499  }
4500  }
4501 
4502  # retrieve template location cache
4503  $this->TemplateLocationCache = unserialize(
4504  $this->Settings["TemplateLocationCache"]);
4505  $this->TemplateLocationCacheInterval =
4506  $this->Settings["TemplateLocationCacheInterval"];
4507  $this->TemplateLocationCacheExpiration =
4508  strtotime($this->Settings["TemplateLocationCacheExpiration"]);
4509 
4510  # if template location cache looks invalid or has expired
4511  $CurrentTime = time();
4512  if (!is_array($this->TemplateLocationCache)
4513  || !count($this->TemplateLocationCache)
4514  || ($CurrentTime >= $this->TemplateLocationCacheExpiration))
4515  {
4516  # clear cache and reset cache expiration
4517  $this->TemplateLocationCache = array();
4518  $this->TemplateLocationCacheExpiration =
4519  $CurrentTime + ($this->TemplateLocationCacheInterval * 60);
4520  $this->SaveTemplateLocationCache = TRUE;
4521  }
4522 
4523  # retrieve object location cache
4524  self::$ObjectLocationCache =
4525  unserialize($this->Settings["ObjectLocationCache"]);
4526  self::$ObjectLocationCacheInterval =
4527  $this->Settings["ObjectLocationCacheInterval"];
4528  self::$ObjectLocationCacheExpiration =
4529  strtotime($this->Settings["ObjectLocationCacheExpiration"]);
4530 
4531  # if object location cache looks invalid or has expired
4532  if (!is_array(self::$ObjectLocationCache)
4533  || !count(self::$ObjectLocationCache)
4534  || ($CurrentTime >= self::$ObjectLocationCacheExpiration))
4535  {
4536  # clear cache and reset cache expiration
4537  self::$ObjectLocationCache = array();
4538  self::$ObjectLocationCacheExpiration =
4539  $CurrentTime + (self::$ObjectLocationCacheInterval * 60);
4540  self::$SaveObjectLocationCache = TRUE;
4541  }
4542  }
4543 
4550  private function RewriteCleanUrls($PageName)
4551  {
4552  # if URL rewriting is supported by the server
4553  if ($this->HtaccessSupport())
4554  {
4555  # retrieve current URL and remove base path if present
4556  $Url = $this->GetPageLocation();
4557 
4558  # for each clean URL mapping
4559  foreach ($this->CleanUrlMappings as $Info)
4560  {
4561  # if current URL matches clean URL pattern
4562  if (preg_match($Info["Pattern"], $Url, $Matches))
4563  {
4564  # set new page
4565  $PageName = $Info["Page"];
4566 
4567  # if $_GET variables specified for clean URL
4568  if ($Info["GetVars"] !== NULL)
4569  {
4570  # for each $_GET variable specified for clean URL
4571  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
4572  {
4573  # start with template for variable value
4574  $Value = $VarTemplate;
4575 
4576  # for each subpattern matched in current URL
4577  foreach ($Matches as $Index => $Match)
4578  {
4579  # if not first (whole) match
4580  if ($Index > 0)
4581  {
4582  # make any substitutions in template
4583  $Value = str_replace("$".$Index, $Match, $Value);
4584  }
4585  }
4586 
4587  # set $_GET variable
4588  $_GET[$VarName] = $Value;
4589  }
4590  }
4591 
4592  # set flag indicating clean URL mapped
4593  $this->CleanUrlRewritePerformed = TRUE;
4594 
4595  # stop looking for a mapping
4596  break;
4597  }
4598  }
4599  }
4600 
4601  # return (possibly) updated page name to caller
4602  return $PageName;
4603  }
4604 
4617  private function RewriteAlternateDomainUrls($Html)
4618  {
4619  # if we were loaded via an alternate domain, and we have a
4620  # RootUrlOverride configured to tell us which domain is the
4621  # primary, and if rewriting support is enabled, then we can
4622  # handle URL Rewriting
4623  if ($this->LoadedViaAlternateDomain() &&
4624  self::$RootUrlOverride !== NULL &&
4625  $this->HtaccessSupport())
4626  {
4627  # pull out the configured prefix for this domain
4628  $VHost = $_SERVER["SERVER_NAME"];
4629  $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
4630 
4631  # get the URL for the primary domain, including the base path
4632  # (usually the part between the host name and the PHP file name)
4633  $RootUrl = $this->RootUrl().self::BasePath();
4634 
4635  # and figure out what protcol we were using
4636  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
4637 
4638  # NB: preg_replace iterates through the configured
4639  # search/replacement pairs, such that the second one
4640  # runs after the first and so on
4641 
4642  # the first n-1 patterns below convert any relative
4643  # links in the generated HTML to absolute links using
4644  # our primary domain (e.g., for stylesheets, javascript,
4645  # images, etc)
4646 
4647  # the nth pattern looks for links that live within the
4648  # path subtree specified by our configured prefix on
4649  # our primary domain, then replaces them with equivalent
4650  # links on our secondary domain
4651 
4652  # for example, if our primary domain is
4653  # example.com/MySite and our secondary domain is
4654  # things.example.org/MySite with 'things' as the
4655  # configured prefix, then this last pattern will look
4656  # for example.com/MySite/things and replace it with
4657  # things.example.org/MySite
4658  $RelativePathPatterns = array(
4659  "%src=\"(?!http://|https://)%i",
4660  "%src='(?!http://|https://)%i",
4661  "%href=\"(?!http://|https://)%i",
4662  "%href='(?!http://|https://)%i",
4663  "%action=\"(?!http://|https://)%i",
4664  "%action='(?!http://|https://)%i",
4665  "%@import\s+url\(\"(?!http://|https://)%i",
4666  "%@import\s+url\('(?!http://|https://)%i",
4667  "%src:\s+url\(\"(?!http://|https://)%i",
4668  "%src:\s+url\('(?!http://|https://)%i",
4669  "%@import\s+\"(?!http://|https://)%i",
4670  "%@import\s+'(?!http://|https://)%i",
4671  "%".preg_quote($RootUrl.$ThisPrefix."/", "%")."%",
4672  );
4673  $RelativePathReplacements = array(
4674  "src=\"".$RootUrl,
4675  "src='".$RootUrl,
4676  "href=\"".$RootUrl,
4677  "href='".$RootUrl,
4678  "action=\"".$RootUrl,
4679  "action='".$RootUrl,
4680  "@import url(\"".$RootUrl,
4681  "@import url('".$RootUrl,
4682  "src: url(\"".$RootUrl,
4683  "src: url('".$RootUrl,
4684  "@import \"".$RootUrl,
4685  "@import '".$RootUrl,
4686  $Protocol."://".$VHost.self::BasePath(),
4687  );
4688 
4689  $NewHtml = preg_replace(
4690  $RelativePathPatterns,
4691  $RelativePathReplacements,
4692  $Html);
4693 
4694  # check to make sure relative path fixes didn't fail
4695  $Html = $this->CheckOutputModification(
4696  $Html, $NewHtml,
4697  "alternate domain substitutions");
4698  }
4699 
4700  return $Html;
4701  }
4702 
4707  private function LoadedViaAlternateDomain()
4708  {
4709  return (isset($_SERVER["SERVER_NAME"]) &&
4710  isset($this->AlternateDomainPrefixes[$_SERVER["SERVER_NAME"]])) ?
4711  TRUE : FALSE ;
4712  }
4713 
4732  private function FindFile($DirectoryList, $BaseName,
4733  $PossibleSuffixes = NULL, $PossiblePrefixes = NULL)
4734  {
4735  # generate template cache index for this page
4736  $CacheIndex = md5(serialize($DirectoryList))
4737  .self::$DefaultUI.self::$ActiveUI.$BaseName;
4738 
4739  # if caching is enabled and we have cached location
4740  if (($this->TemplateLocationCacheInterval > 0)
4741  && array_key_exists($CacheIndex,
4742  $this->TemplateLocationCache))
4743  {
4744  # use template location from cache
4745  $FoundFileName = $this->TemplateLocationCache[$CacheIndex];
4746  }
4747  else
4748  {
4749  # if suffixes specified and base name does not include suffix
4750  if ($PossibleSuffixes !== NULL
4751  && count($PossibleSuffixes)
4752  && !preg_match("/\.[a-zA-Z0-9]+$/", $BaseName))
4753  {
4754  # add versions of file names with suffixes to file name list
4755  $FileNames = array();
4756  foreach ($PossibleSuffixes as $Suffix)
4757  {
4758  $FileNames[] = $BaseName.".".$Suffix;
4759  }
4760  }
4761  else
4762  {
4763  # use base name as file name
4764  $FileNames = array($BaseName);
4765  }
4766 
4767  # if prefixes specified
4768  if ($PossiblePrefixes !== NULL && count($PossiblePrefixes))
4769  {
4770  # add versions of file names with prefixes to file name list
4771  $NewFileNames = array();
4772  foreach ($FileNames as $FileName)
4773  {
4774  foreach ($PossiblePrefixes as $Prefix)
4775  {
4776  $NewFileNames[] = $Prefix.$FileName;
4777  }
4778  }
4779  $FileNames = $NewFileNames;
4780  }
4781 
4782  # expand directory list to include variants
4783  $DirectoryList = $this->ExpandDirectoryList($DirectoryList);
4784 
4785  # for each possible location
4786  $FoundFileName = NULL;
4787  foreach ($DirectoryList as $Dir)
4788  {
4789  # for each possible file name
4790  foreach ($FileNames as $File)
4791  {
4792  # if template is found at location
4793  if (file_exists($Dir.$File))
4794  {
4795  # save full template file name and stop looking
4796  $FoundFileName = $Dir.$File;
4797  break 2;
4798  }
4799  }
4800  }
4801 
4802  # save location in cache
4803  $this->TemplateLocationCache[$CacheIndex]
4804  = $FoundFileName;
4805 
4806  # set flag indicating that cache should be saved
4807  $this->SaveTemplateLocationCache = TRUE;
4808  }
4809 
4810  # return full template file name to caller
4811  return $FoundFileName;
4812  }
4813 
4820  private function ExpandDirectoryList($DirList)
4821  {
4822  # generate lookup for supplied list
4823  $ExpandedListKey = md5(serialize($DirList)
4824  .self::$DefaultUI.self::$ActiveUI);
4825 
4826  # if we already have expanded version of supplied list
4827  if (isset($this->ExpandedDirectoryLists[$ExpandedListKey]))
4828  {
4829  # return expanded version to caller
4830  return $this->ExpandedDirectoryLists[$ExpandedListKey];
4831  }
4832 
4833  # for each directory in list
4834  $ExpDirList = array();
4835  foreach ($DirList as $Dir)
4836  {
4837  # if directory includes substitution keyword
4838  if ((strpos($Dir, "%DEFAULTUI%") !== FALSE)
4839  || (strpos($Dir, "%ACTIVEUI%") !== FALSE))
4840  {
4841  # start with empty new list segment
4842  $ExpDirListSegment = array();
4843 
4844  # use default values for initial parent
4845  $ParentInterface = array(self::$ActiveUI, self::$DefaultUI);
4846 
4847  do
4848  {
4849  # substitute in for keyword on parent
4850  $CurrDir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4851  $ParentInterface, $Dir);
4852 
4853  # add local version of parent directory to new list segment
4854  $ExpDirListSegment[] = "local/".$CurrDir;
4855 
4856  # add parent directory to new list segment
4857  $ExpDirListSegment[] = $CurrDir;
4858 
4859  # look for new parent interface
4860  $ParentInterface = $this->GetInterfaceSetting(
4861  $CurrDir, "ParentInterface");
4862 
4863  # repeat if parent is available
4864  } while (strlen($ParentInterface));
4865 
4866  # add new list segment to expanded list
4867  $ExpDirList = array_merge($ExpDirList, $ExpDirListSegment);
4868  }
4869  else
4870  {
4871  # add local version of directory to expanded list
4872  $ExpDirList[] = "local/".$Dir;
4873 
4874  # add directory to expanded list
4875  $ExpDirList[] = $Dir;
4876  }
4877  }
4878 
4879  # return expanded version to caller
4880  $this->ExpandedDirectoryLists[$ExpandedListKey] = $ExpDirList;
4881  return $this->ExpandedDirectoryLists[$ExpandedListKey];
4882  }
4883 
4892  private function GetInterfaceSetting($InterfaceDir, $SettingName = NULL)
4893  {
4894  # extract canonical interface name and base interface directory
4895  preg_match("%(.*interface/)([^/]+)%", $InterfaceDir, $Matches);
4896  $InterfaceDir = (count($Matches) > 2)
4897  ? $Matches[1].$Matches[2] : $InterfaceDir;
4898  $InterfaceName = (count($Matches) > 2)
4899  ? $Matches[2] : "UNKNOWN";
4900 
4901  # if we do not have settings for interface
4902  if (!isset($this->InterfaceSettings[$InterfaceName]))
4903  {
4904  # load default values for settings
4905  $this->InterfaceSettings[$InterfaceName] = array(
4906  "Source" => "",
4907  );
4908  }
4909 
4910  # if directory takes precedence over existing settings source
4911  # ("takes precendence" == is more local == longer directory length)
4912  if (strlen($InterfaceDir)
4913  > strlen($this->InterfaceSettings[$InterfaceName]["Source"]))
4914  {
4915  # if settings file exists in directory
4916  $SettingsFile = $InterfaceDir."/interface.ini";
4917  if (is_readable($SettingsFile))
4918  {
4919  # read in values from file
4920  $NewSettings = parse_ini_file($SettingsFile);
4921 
4922  # merge in values with existing settings
4923  $this->InterfaceSettings[$InterfaceName] = array_merge(
4924  $this->InterfaceSettings[$InterfaceName], $NewSettings);
4925 
4926  # save new source of settings
4927  $this->InterfaceSettings[$InterfaceName]["Source"] = $InterfaceDir;
4928  }
4929  }
4930 
4931  # return interface settings to caller
4932  return $SettingName
4933  ? (isset($this->InterfaceSettings[$InterfaceName][$SettingName])
4934  ? $this->InterfaceSettings[$InterfaceName][$SettingName]
4935  : NULL)
4936  : $this->InterfaceSettings[$InterfaceName];
4937  }
4938 
4947  private function CompileScssFile($SrcFile)
4948  {
4949  # build path to CSS file
4950  $DstFile = self::$ScssCacheDir."/".dirname($SrcFile)
4951  ."/".basename($SrcFile);
4952  $DstFile = substr_replace($DstFile, "css", -4);
4953 
4954  # if SCSS file is newer than CSS file
4955  if (!file_exists($DstFile)
4956  || (filemtime($SrcFile) > filemtime($DstFile)))
4957  {
4958  # attempt to create CSS cache subdirectory if not present
4959  if (!is_dir(dirname($DstFile)))
4960  {
4961  @mkdir(dirname($DstFile), 0777, TRUE);
4962  }
4963 
4964  # if CSS cache directory and CSS file path appear writable
4965  static $CacheDirIsWritable;
4966  if (!isset($CacheDirIsWritable))
4967  { $CacheDirIsWritable = is_writable(self::$ScssCacheDir); }
4968  if (is_writable($DstFile)
4969  || (!file_exists($DstFile) && $CacheDirIsWritable))
4970  {
4971  # load SCSS and compile to CSS
4972  $ScssCode = file_get_contents($SrcFile);
4973  $ScssCompiler = new scssc();
4974  $ScssCompiler->setFormatter($this->GenerateCompactCss()
4975  ? "scss_formatter_compressed" : "scss_formatter");
4976  try
4977  {
4978  $CssCode = $ScssCompiler->compile($ScssCode);
4979 
4980  # add fingerprinting for URLs in CSS
4981  $this->CssUrlFingerprintPath = dirname($SrcFile);
4982  $CssCode = preg_replace_callback(
4983  "/url\((['\"]*)(.+)\.([a-z]+)(['\"]*)\)/",
4984  array($this, "CssUrlFingerprintInsertion"),
4985  $CssCode);
4986 
4987  # strip out comments from CSS (if requested)
4988  if ($this->GenerateCompactCss())
4989  {
4990  $CssCode = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!',
4991  '', $CssCode);
4992  }
4993 
4994  # write out CSS file
4995  file_put_contents($DstFile, $CssCode);
4996  }
4997  catch (Exception $Ex)
4998  {
4999  $this->LogError(self::LOGLVL_ERROR,
5000  "Error compiling SCSS file ".$SrcFile.": "
5001  .$Ex->getMessage());
5002  $DstFile = NULL;
5003  }
5004  }
5005  else
5006  {
5007  # log error and set CSS file path to indicate failure
5008  $this->LogError(self::LOGLVL_ERROR,
5009  "Unable to write out CSS file (compiled from SCSS) to "
5010  .$DstFile);
5011  $DstFile = NULL;
5012  }
5013  }
5014 
5015  # return CSS file path to caller
5016  return $DstFile;
5017  }
5018 
5026  private function MinimizeJavascriptFile($SrcFile)
5027  {
5028  # bail out if file is on exclusion list
5029  foreach ($this->DoNotMinimizeList as $DNMFile)
5030  {
5031  if (($SrcFile == $DNMFile) || (basename($SrcFile) == $DNMFile))
5032  {
5033  return NULL;
5034  }
5035  }
5036 
5037  # build path to minimized file
5038  $DstFile = self::$JSMinCacheDir."/".dirname($SrcFile)
5039  ."/".basename($SrcFile);
5040  $DstFile = substr_replace($DstFile, ".min", -3, 0);
5041 
5042  # if original file is newer than minimized file
5043  if (!file_exists($DstFile)
5044  || (filemtime($SrcFile) > filemtime($DstFile)))
5045  {
5046  # attempt to create cache subdirectory if not present
5047  if (!is_dir(dirname($DstFile)))
5048  {
5049  @mkdir(dirname($DstFile), 0777, TRUE);
5050  }
5051 
5052  # if cache directory and minimized file path appear writable
5053  static $CacheDirIsWritable;
5054  if (!isset($CacheDirIsWritable))
5055  { $CacheDirIsWritable = is_writable(self::$JSMinCacheDir); }
5056  if (is_writable($DstFile)
5057  || (!file_exists($DstFile) && $CacheDirIsWritable))
5058  {
5059  # load JavaScript code
5060  $Code = file_get_contents($SrcFile);
5061 
5062  # decide which minimizer to use
5063  if ($this->JSMinimizerJavaScriptPackerAvailable
5064  && $this->JSMinimizerJShrinkAvailable)
5065  {
5066  $Minimizer = (strlen($Code) < 5000)
5067  ? "JShrink" : "JavaScriptPacker";
5068  }
5069  elseif ($this->JSMinimizerJShrinkAvailable)
5070  {
5071  $Minimizer = "JShrink";
5072  }
5073  else
5074  {
5075  $Minimizer = "NONE";
5076  }
5077 
5078  # minimize code
5079  switch ($Minimizer)
5080  {
5081  case "JavaScriptMinimizer":
5082  $Packer = new JavaScriptPacker($Code, "Normal");
5083  $MinimizedCode = $Packer->pack();
5084  break;
5085 
5086  case "JShrink":
5087  try
5088  {
5089  $MinimizedCode = \JShrink\Minifier::minify($Code);
5090  }
5091  catch (Exception $Exception)
5092  {
5093  unset($MinimizedCode);
5094  $MinimizeError = $Exception->getMessage();
5095  }
5096  break;
5097  }
5098 
5099  # if minimization succeeded
5100  if (isset($MinimizedCode))
5101  {
5102  # write out minimized file
5103  file_put_contents($DstFile, $MinimizedCode);
5104  }
5105  else
5106  {
5107  # log error and set destination file path to indicate failure
5108  $ErrMsg = "Unable to minimize JavaScript file ".$SrcFile;
5109  if (isset($MinimizeError))
5110  {
5111  $ErrMsg .= " (".$MinimizeError.")";
5112  }
5113  $this->LogError(self::LOGLVL_ERROR, $ErrMsg);
5114  $DstFile = NULL;
5115  }
5116  }
5117  else
5118  {
5119  # log error and set destination file path to indicate failure
5120  $this->LogError(self::LOGLVL_ERROR,
5121  "Unable to write out minimized JavaScript to file ".$DstFile);
5122  $DstFile = NULL;
5123  }
5124  }
5125 
5126  # return CSS file path to caller
5127  return $DstFile;
5128  }
5129 
5137  private function CssUrlFingerprintInsertion($Matches)
5138  {
5139  # generate fingerprint string from CSS file modification time
5140  $FileName = realpath($this->CssUrlFingerprintPath."/".
5141  $Matches[2].".".$Matches[3]);
5142  $MTime = filemtime($FileName);
5143  $Fingerprint = sprintf("%06X", ($MTime % 0xFFFFFF));
5144 
5145  # build URL string with fingerprint and return it to caller
5146  return "url(".$Matches[1].$Matches[2].".".$Fingerprint
5147  .".".$Matches[3].$Matches[4].")";
5148  }
5149 
5157  private function GetRequiredFilesNotYetLoaded($PageContentFile)
5158  {
5159  # start out assuming no files required
5160  $RequiredFiles = array();
5161 
5162  # if page content file supplied
5163  if ($PageContentFile)
5164  {
5165  # if file containing list of required files is available
5166  $Path = dirname($PageContentFile);
5167  $RequireListFile = $Path."/REQUIRES";
5168  if (file_exists($RequireListFile))
5169  {
5170  # read in list of required files
5171  $RequestedFiles = file($RequireListFile);
5172 
5173  # for each line in required file list
5174  foreach ($RequestedFiles as $Line)
5175  {
5176  # if line is not a comment
5177  $Line = trim($Line);
5178  if (!preg_match("/^#/", $Line))
5179  {
5180  # if file has not already been loaded
5181  if (!in_array($Line, $this->FoundUIFiles))
5182  {
5183  # add to list of required files
5184  $RequiredFiles[$Line] = self::ORDER_MIDDLE;
5185  }
5186  }
5187  }
5188  }
5189  }
5190 
5191  # add in additional required files if any
5192  if (count($this->AdditionalRequiredUIFiles))
5193  {
5194  # make sure there are no duplicates
5195  $AdditionalRequiredUIFiles = array_unique(
5196  $this->AdditionalRequiredUIFiles);
5197 
5198  $RequiredFiles = array_merge(
5199  $RequiredFiles, $this->AdditionalRequiredUIFiles);
5200  }
5201 
5202  # return list of required files to caller
5203  return $RequiredFiles;
5204  }
5205 
5214  private function SubBrowserIntoFileNames($FileNames)
5215  {
5216  # if a browser detection function has been made available
5217  $UpdatedFileNames = array();
5218  if (is_callable($this->BrowserDetectFunc))
5219  {
5220  # call function to get browser list
5221  $Browsers = call_user_func($this->BrowserDetectFunc);
5222 
5223  # for each required file
5224  foreach ($FileNames as $FileName => $Value)
5225  {
5226  # if file name includes browser keyword
5227  if (preg_match("/%BROWSER%/", $FileName))
5228  {
5229  # for each browser
5230  foreach ($Browsers as $Browser)
5231  {
5232  # substitute in browser name and add to new file list
5233  $NewFileName = preg_replace(
5234  "/%BROWSER%/", $Browser, $FileName);
5235  $UpdatedFileNames[$NewFileName] = $Value;
5236  }
5237  }
5238  else
5239  {
5240  # add to new file list
5241  $UpdatedFileNames[$FileName] = $Value;
5242  }
5243  }
5244  }
5245  else
5246  {
5247  # filter out any files with browser keyword in their name
5248  foreach ($FileNames as $FileName => $Value)
5249  {
5250  if (!preg_match("/%BROWSER%/", $FileName))
5251  {
5252  $UpdatedFileNames[$FileName] = $Value;
5253  }
5254  }
5255  }
5256 
5257  return $UpdatedFileNames;
5258  }
5259 
5265  private function AddMetaTagsToPageOutput($PageOutput)
5266  {
5267  # start with unconditional (non-unique) tags
5268  $TagsToAdd = $this->MetaTags;
5269 
5270  # for each unique tag
5271  foreach ($this->UniqueMetaTags as $UniqueMetaTag)
5272  {
5273  $Attribs = $UniqueMetaTag["Attribs"];
5274  $UniqueAttribs = $UniqueMetaTag["UniqueAttribs"];
5275 
5276  # if no unique attributes specified
5277  if ($UniqueAttribs === NULL)
5278  {
5279  # use first attribute as unique attribute
5280  $UniqueAttribs = array_slice($Attribs, 0, 1);
5281  }
5282 
5283  # for each already-queued tag
5284  # (look for meta tags that match all attributes in
5285  # the current unique tag)
5286  foreach ($TagsToAdd as $TagAttribs)
5287  {
5288  # for each attribute in current unique tag
5289  # (look for attributes in the current unique tag that do
5290  # not match attributes in the this queued tag)
5291  foreach ($UniqueAttribs as $UniqueName => $UniqueValue)
5292  {
5293  # if unique attribute is not found in queued tag
5294  # or queued tag attribute has a different value
5295  if (!isset($TagAttribs[$UniqueName])
5296  || ($TagAttribs[$UniqueName] != $UniqueValue))
5297  {
5298  # skip to next queued tag
5299  # (some attribute in the current unique tag
5300  # was not found in the queued tag)
5301  continue 2;
5302  }
5303  }
5304 
5305  # skip to next unique tag
5306  # (all attributes in the current unique tag were found
5307  # in the queued tag, so do not queue this unique tag)
5308  continue 2;
5309  }
5310 
5311  # generate potential combinations of unique attributes
5312  $UniqueAttribNameCombos = StdLib::ArrayPermutations(
5313  array_keys($UniqueAttribs));
5314 
5315  # for each combination of unique attributes
5316  foreach ($UniqueAttribNameCombos as $UniqueNameCombo)
5317  {
5318  # for each attribute in combination
5319  $AttribStrings = array();
5320  foreach ($UniqueNameCombo as $UniqueName)
5321  {
5322  # add attrib/value string to list
5323  $AttribStrings[] = $UniqueName."=\""
5324  .htmlspecialchars($UniqueAttribs[$UniqueName])."\"";
5325  }
5326 
5327  # build search string from list of attribute pairs
5328  $SearchString = "<meta ".implode(" ", $AttribStrings);
5329 
5330  # if search string appears in page output
5331  if (strpos($PageOutput, $SearchString) !== FALSE)
5332  {
5333  # skip to next unique tag
5334  continue 2;
5335  }
5336 
5337  # repeat search with single quotes instead of double quotes
5338  $SearchString = strtr($SearchString, '"', "'");
5339  if (strpos($PageOutput, $SearchString) !== FALSE)
5340  {
5341  # skip to next unique tag
5342  continue 2;
5343  }
5344  }
5345 
5346  # unique tag was not found in page output, so add it to inserted tags
5347  $TagsToAdd[] = $Attribs;
5348  }
5349 
5350  # if there are meta tags to be added
5351  if (count($TagsToAdd))
5352  {
5353  # start with an empty segment
5354  $Section = "";
5355 
5356  # for each meta tag
5357  foreach ($TagsToAdd as $Attribs)
5358  {
5359  # assemble tag and add it to the segment
5360  $Section .= "<meta";
5361  foreach ($Attribs as $AttribName => $AttribValue)
5362  {
5363  $Section .= " ".$AttribName."=\""
5364  .htmlspecialchars(trim($AttribValue))."\"";
5365  }
5366  $Section .= " />\n";
5367  }
5368 
5369  # if standard page start and end have been disabled
5370  if ($this->SuppressStdPageStartAndEnd)
5371  {
5372  # add segment to beginning of page output
5373  $PageOutput = $Section.$PageOutput;
5374  }
5375  else
5376  {
5377  # insert segment at beginning of HTML head section in page output
5378  $PageOutput = preg_replace("#<head>#i",
5379  "<head>\n".$Section, $PageOutput, 1);
5380  }
5381  }
5382 
5383  # return (potentially modified) page output to caller
5384  return $PageOutput;
5385  }
5386 
5394  private function AddFileTagsToPageOutput($PageOutput, $Files)
5395  {
5396  # substitute browser name into names of required files as appropriate
5397  $Files = $this->SubBrowserIntoFileNames($Files);
5398 
5399  # initialize content sections
5400  $HeadContent = [
5401  self::ORDER_FIRST => "",
5402  self::ORDER_MIDDLE => "",
5403  self::ORDER_LAST => "",
5404  ];
5405  $BodyContent = [
5406  self::ORDER_FIRST => "",
5407  self::ORDER_MIDDLE => "",
5408  self::ORDER_LAST => "",
5409  ];
5410 
5411  # for each required file
5412  foreach ($Files as $File => $Order)
5413  {
5414  # locate specific file to use
5415  $FilePath = $this->GUIFile($File);
5416 
5417  # if file was found
5418  if ($FilePath)
5419  {
5420  # generate tag for file
5421  $Tag = $this->GetUIFileLoadingTag($FilePath);
5422 
5423  # add file to HTML output based on file type
5424  $FileType = $this->GetFileType($FilePath);
5425  switch ($FileType)
5426  {
5427  case self::FT_CSS:
5428  $HeadContent[$Order] .= $Tag."\n";
5429  break;
5430 
5431  case self::FT_JAVASCRIPT:
5432  $BodyContent[$Order] .= $Tag."\n";
5433  break;
5434  }
5435  }
5436  }
5437 
5438  # add content to head
5439  $Replacement = $HeadContent[self::ORDER_MIDDLE]
5440  .$HeadContent[self::ORDER_LAST];
5441  $UpdatedPageOutput = str_ireplace("</head>",
5442  $Replacement."</head>",
5443  $PageOutput, $ReplacementCount);
5444  # (if no </head> tag was found, just prepend tags to page content)
5445  if ($ReplacementCount == 0)
5446  {
5447  $PageOutput = $Replacement.$PageOutput;
5448  }
5449  # (else if multiple </head> tags found, only prepend tags to the first)
5450  elseif ($ReplacementCount > 1)
5451  {
5452  $PageOutput = preg_replace("#</head>#i",
5453  $Replacement."</head>",
5454  $PageOutput, 1);
5455  }
5456  else
5457  {
5458  $PageOutput = $UpdatedPageOutput;
5459  }
5460  $Replacement = $HeadContent[self::ORDER_FIRST];
5461  $UpdatedPageOutput = str_ireplace("<head>",
5462  "<head>\n".$Replacement,
5463  $PageOutput, $ReplacementCount);
5464  # (if no <head> tag was found, just prepend tags to page content)
5465  if ($ReplacementCount == 0)
5466  {
5467  $PageOutput = $Replacement.$PageOutput;
5468  }
5469  # (else if multiple <head> tags found, only append tags to the first)
5470  elseif ($ReplacementCount > 1)
5471  {
5472  $PageOutput = preg_replace("#<head>#i",
5473  "<head>\n".$Replacement,
5474  $PageOutput, 1);
5475  }
5476  else
5477  {
5478  $PageOutput = $UpdatedPageOutput;
5479  }
5480 
5481  # add content to body
5482  $Replacement = $BodyContent[self::ORDER_FIRST];
5483  $PageOutput = preg_replace("#<body([^>]*)>#i",
5484  "<body\\1>\n".$Replacement,
5485  $PageOutput, 1, $ReplacementCount);
5486  # (if no <body> tag was found, just append tags to page content)
5487  if ($ReplacementCount == 0)
5488  {
5489  $PageOutput = $PageOutput.$Replacement;
5490  }
5491  $Replacement = $BodyContent[self::ORDER_MIDDLE]
5492  .$BodyContent[self::ORDER_LAST];
5493  $UpdatedPageOutput = str_ireplace("</body>",
5494  $Replacement."\n</body>",
5495  $PageOutput, $ReplacementCount);
5496  # (if no </body> tag was found, just append tags to page content)
5497  if ($ReplacementCount == 0)
5498  {
5499  $PageOutput = $PageOutput.$Replacement;
5500  }
5501  # (else if multiple </body> tags found, only prepend tag to the first)
5502  elseif ($ReplacementCount > 1)
5503  {
5504  $PageOutput = preg_replace("#</body>#i",
5505  $Replacement."\n</body>",
5506  $PageOutput, 1);
5507  }
5508  else
5509  {
5510  $PageOutput = $UpdatedPageOutput;
5511  }
5512 
5513  return $PageOutput;
5514  }
5515 
5526  private function GetUIFileLoadingTag($FileName, $AdditionalAttributes = NULL)
5527  {
5528  # pad additional attributes if supplied
5529  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
5530 
5531  # retrieve type of UI file
5532  $FileType = $this->GetFileType($FileName);
5533 
5534  # construct tag based on file type
5535  switch ($FileType)
5536  {
5537  case self::FT_CSS:
5538  $Tag = " <link rel=\"stylesheet\" type=\"text/css\""
5539  ." media=\"all\" href=\"".$FileName."\""
5540  .$AddAttribs." />\n";
5541  break;
5542 
5543  case self::FT_JAVASCRIPT:
5544  $Tag = " <script type=\"text/javascript\""
5545  ." src=\"".$FileName."\""
5546  .$AddAttribs."></script>\n";
5547  break;
5548 
5549  case self::FT_IMAGE:
5550  $Tag = "<img src=\"".$FileName."\"".$AddAttribs.">";
5551  break;
5552 
5553  default:
5554  $Tag = "";
5555  break;
5556  }
5557 
5558  # return constructed tag to caller
5559  return $Tag;
5560  }
5561 
5566  private function AutoloadObjects($ClassName)
5567  {
5568  # if caching is not turned off
5569  # and we have a cached location for class
5570  # and file at cached location is readable
5571  if ((self::$ObjectLocationCacheInterval > 0)
5572  && array_key_exists($ClassName,
5573  self::$ObjectLocationCache)
5574  && is_readable(self::$ObjectLocationCache[$ClassName]))
5575  {
5576  # use object location from cache
5577  require_once(self::$ObjectLocationCache[$ClassName]);
5578  }
5579  else
5580  {
5581  # for each possible object file directory
5582  static $FileLists;
5583  foreach (self::$ObjectDirectories as $Location => $Info)
5584  {
5585  # make any needed replacements in directory path
5586  $Location = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
5587  array(self::$ActiveUI, self::$DefaultUI), $Location);
5588 
5589  # if directory looks valid
5590  if (is_dir($Location))
5591  {
5592  # pass class name through callback (if supplied)
5593  $ClassFileName = $ClassName;
5594  if (is_callable($Info["Callback"]))
5595  {
5596  $ClassFileName = $Info["Callback"]($ClassFileName);
5597  }
5598 
5599  # strip off any namespace prefix
5600  foreach ($Info["NamespacePrefixes"] as $Prefix)
5601  {
5602  if (strpos($ClassFileName, $Prefix) === 0)
5603  {
5604  $ClassFileName = substr($ClassFileName, strlen($Prefix));
5605  break;
5606  }
5607  }
5608 
5609  # strip off any leading namespace separator
5610  if (strpos($ClassFileName, "\\") === 0)
5611  {
5612  $ClassFileName = substr($ClassFileName, 1);
5613  }
5614 
5615  # convert any namespace separators to directory separators
5616  $ClassFileName = str_replace("\\", "/", $ClassFileName);
5617 
5618  # finish building class file name
5619  $ClassFileName = $ClassFileName.".php";
5620 
5621  # read in directory contents if not already retrieved
5622  if (!isset($FileLists[$Location]))
5623  {
5624  $FileLists[$Location] = self::ReadDirectoryTree(
5625  $Location, '/^.+\.php$/i');
5626  }
5627 
5628  # for each file in target directory
5629  foreach ($FileLists[$Location] as $FileName)
5630  {
5631  # if file matches our target object file name
5632  if ($FileName == $ClassFileName)
5633  {
5634  # include object file
5635  require_once($Location.$FileName);
5636 
5637  # save location to cache
5638  self::$ObjectLocationCache[$ClassName]
5639  = $Location.$FileName;
5640 
5641  # set flag indicating that cache should be saved
5642  self::$SaveObjectLocationCache = TRUE;
5643 
5644  # stop looking
5645  break 2;
5646  }
5647  }
5648  }
5649  }
5650  }
5651  }
5652 
5660  private static function ReadDirectoryTree($Directory, $Pattern)
5661  {
5662  $CurrentDir = getcwd();
5663  chdir($Directory);
5664  $DirIter = new RecursiveDirectoryIterator(".");
5665  $IterIter = new RecursiveIteratorIterator($DirIter);
5666  $RegexResults = new RegexIterator($IterIter, $Pattern,
5667  RecursiveRegexIterator::GET_MATCH);
5668  $FileList = array();
5669  foreach ($RegexResults as $Result)
5670  {
5671  $FileList[] = substr($Result[0], 2);
5672  }
5673  chdir($CurrentDir);
5674  return $FileList;
5675  }
5676 
5681  private function LoadUIFunctions()
5682  {
5683  $Dirs = array(
5684  "local/interface/%ACTIVEUI%/include",
5685  "interface/%ACTIVEUI%/include",
5686  "local/interface/%DEFAULTUI%/include",
5687  "interface/%DEFAULTUI%/include",
5688  );
5689  foreach ($Dirs as $Dir)
5690  {
5691  $Dir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
5692  array(self::$ActiveUI, self::$DefaultUI), $Dir);
5693  if (is_dir($Dir))
5694  {
5695  $FileNames = scandir($Dir);
5696  foreach ($FileNames as $FileName)
5697  {
5698  if (preg_match("/^F-([A-Za-z0-9_]+)\.php/",
5699  $FileName, $Matches)
5700  || preg_match("/^F-([A-Za-z0-9_]+)\.html/",
5701  $FileName, $Matches))
5702  {
5703  if (!function_exists($Matches[1]))
5704  {
5705  include_once($Dir."/".$FileName);
5706  }
5707  }
5708  }
5709  }
5710  }
5711  }
5712 
5718  private function ProcessPeriodicEvent($EventName, $Callback)
5719  {
5720  # retrieve last execution time for event if available
5721  $Signature = self::GetCallbackSignature($Callback);
5722  $LastRun = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
5723  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
5724 
5725  # determine whether enough time has passed for event to execute
5726  $ShouldExecute = (($LastRun === NULL)
5727  || (time() > (strtotime($LastRun) + $this->EventPeriods[$EventName])))
5728  ? TRUE : FALSE;
5729 
5730  # if event should run
5731  if ($ShouldExecute)
5732  {
5733  # add event to task queue
5734  $WrapperCallback = array("ApplicationFramework", "RunPeriodicEvent");
5735  $WrapperParameters = array(
5736  $EventName, $Callback, array("LastRunAt" => $LastRun));
5737  $this->QueueUniqueTask($WrapperCallback, $WrapperParameters);
5738  }
5739 
5740  # add event to list of periodic events
5741  $this->KnownPeriodicEvents[$Signature] = array(
5742  "Period" => $EventName,
5743  "Callback" => $Callback,
5744  "Queued" => $ShouldExecute);
5745  }
5746 
5752  private static function GetCallbackSignature($Callback)
5753  {
5754  return !is_array($Callback) ? $Callback
5755  : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
5756  ."::".$Callback[1];
5757  }
5758 
5763  private function PrepForTSR()
5764  {
5765  # if HTML has been output and it's time to launch another task
5766  # (only TSR if HTML has been output because otherwise browsers
5767  # may misbehave after connection is closed)
5768  if ((PHP_SAPI != "cli")
5769  && ($this->JumpToPage || !$this->SuppressHTML)
5770  && !$this->LoadedViaAlternateDomain()
5771  && (time() > (strtotime($this->Settings["LastTaskRunAt"])
5772  + ($this->MaxExecutionTime()
5773  / $this->Settings["MaxTasksRunning"]) + 5))
5774  && $this->GetTaskQueueSize()
5775  && $this->Settings["TaskExecutionEnabled"])
5776  {
5777  # begin buffering output for TSR
5778  ob_start();
5779 
5780  # let caller know it is time to launch another task
5781  return TRUE;
5782  }
5783  else
5784  {
5785  # let caller know it is not time to launch another task
5786  return FALSE;
5787  }
5788  }
5789 
5794  private function LaunchTSR()
5795  {
5796  # set headers to close out connection to browser
5797  if (!$this->NoTSR)
5798  {
5799  ignore_user_abort(TRUE);
5800  header("Connection: close");
5801  header("Content-Length: ".ob_get_length());
5802  }
5803 
5804  # output buffered content
5805  while (ob_get_level()) { ob_end_flush(); }
5806  flush();
5807 
5808  # write out any outstanding data and end HTTP session
5809  session_write_close();
5810 
5811  # set flag indicating that we are now running in background
5812  $this->RunningInBackground = TRUE;
5813 
5814  # handle garbage collection for session data
5815  if (isset($this->SessionStorage) &&
5816  (rand()/getrandmax()) <= $this->SessionGcProbability)
5817  {
5818  # determine when sessions will expire
5819  $ExpiredTime = strtotime("-". self::$SessionLifetime." seconds");
5820 
5821  # iterate over files in the session directory with a DirectoryIterator
5822  # NB: we cannot use scandir() here because it reads the
5823  # entire list of files into memory and may exceed the memory
5824  # limit for directories with very many files
5825  $DI = new DirectoryIterator($this->SessionStorage);
5826  while ($DI->valid())
5827  {
5828  if ((strpos($DI->getFilename(), "sess_") === 0) &&
5829  $DI->isFile() &&
5830  $DI->getCTime() < $ExpiredTime)
5831  {
5832  unlink($DI->getPathname());
5833  }
5834  $DI->next();
5835  }
5836  unset($DI);
5837  }
5838 
5839  # if there is still a task in the queue
5840  if ($this->GetTaskQueueSize())
5841  {
5842  # tell PHP to garbage collect to give as much memory as possible for tasks
5843  gc_collect_cycles();
5844 
5845  # turn on output buffering to (hopefully) record any crash output
5846  ob_start();
5847 
5848  # lock tables and grab last task run time to double check
5849  $this->DB->Query("LOCK TABLES ApplicationFrameworkSettings WRITE");
5850  $this->LoadSettings();
5851 
5852  # if still time to launch another task
5853  if (time() > (strtotime($this->Settings["LastTaskRunAt"])
5854  + ($this->MaxExecutionTime()
5855  / $this->Settings["MaxTasksRunning"]) + 5))
5856  {
5857  # update the "last run" time and release tables
5858  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
5859  ." SET LastTaskRunAt = '".date("Y-m-d H:i:s")."'");
5860  $this->DB->Query("UNLOCK TABLES");
5861 
5862  # run tasks while there is a task in the queue
5863  # and enough time and memory left
5864  do
5865  {
5866  # run the next task
5867  $this->RunNextTask();
5868 
5869  # calculate percentage of memory still available
5870  $PercentFreeMem = (self::GetFreeMemory()
5871  / self::GetPhpMemoryLimit()) * 100;
5872  }
5873  while ($this->GetTaskQueueSize()
5874  && ($this->GetSecondsBeforeTimeout() > 65)
5875  && ($PercentFreeMem > $this->BackgroundTaskMinFreeMemPercent));
5876 
5877  $this->ResetTaskIdGeneratorIfNecessary();
5878  }
5879  else
5880  {
5881  # release tables
5882  $this->DB->Query("UNLOCK TABLES");
5883  }
5884  }
5885  }
5886 
5896  private function GetTaskList($DBQuery, $Count, $Offset)
5897  {
5898  $this->DB->Query($DBQuery." LIMIT ".intval($Offset).",".intval($Count));
5899  $Tasks = array();
5900  while ($Row = $this->DB->FetchRow())
5901  {
5902  $Tasks[$Row["TaskId"]] = $Row;
5903  if ($Row["Callback"] ==
5904  serialize(array("ApplicationFramework", "RunPeriodicEvent")))
5905  {
5906  $WrappedCallback = unserialize($Row["Parameters"]);
5907  $Tasks[$Row["TaskId"]]["Callback"] = $WrappedCallback[1];
5908  $Tasks[$Row["TaskId"]]["Parameters"] = NULL;
5909  }
5910  else
5911  {
5912  $Tasks[$Row["TaskId"]]["Callback"] = unserialize($Row["Callback"]);
5913  $Tasks[$Row["TaskId"]]["Parameters"] = unserialize($Row["Parameters"]);
5914  }
5915  }
5916  return $Tasks;
5917  }
5918 
5922  private function RunNextTask()
5923  {
5924  # lock tables to prevent same task from being run by multiple sessions
5925  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
5926 
5927  # look for task at head of queue
5928  $this->DB->Query("SELECT * FROM TaskQueue ORDER BY Priority, TaskId LIMIT 1");
5929  $Task = $this->DB->FetchRow();
5930 
5931  # if there was a task available
5932  if ($Task)
5933  {
5934  # move task from queue to running tasks list
5935  $this->DB->Query("INSERT INTO RunningTasks "
5936  ."(TaskId,Callback,Parameters,Priority,Description) "
5937  ."SELECT * FROM TaskQueue WHERE TaskId = "
5938  .intval($Task["TaskId"]));
5939  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = "
5940  .intval($Task["TaskId"]));
5941 
5942  # release table locks to again allow other sessions to run tasks
5943  $this->DB->Query("UNLOCK TABLES");
5944 
5945  # unpack stored task info
5946  $TaskId = $Task["TaskId"];
5947  $Callback = unserialize($Task["Callback"]);
5948  $Parameters = unserialize($Task["Parameters"]);
5949 
5950  # attempt to load task callback if not already available
5951  $this->LoadFunction($Callback);
5952 
5953  # clear task requeue flag
5954  $this->RequeueCurrentTask = FALSE;
5955 
5956  # save amount of free memory for later comparison
5957  $BeforeFreeMem = self::GetFreeMemory();
5958 
5959  # run task
5960  $this->RunningTask = $Task;
5961  if ($Parameters)
5962  {
5963  call_user_func_array($Callback, $Parameters);
5964  }
5965  else
5966  {
5967  call_user_func($Callback);
5968  }
5969  unset($this->RunningTask);
5970 
5971  # log if task leaked significant memory
5972  $this->LogTaskMemoryLeakIfAny($TaskId, $BeforeFreeMem);
5973 
5974  # if task requeue requested
5975  if ($this->RequeueCurrentTask)
5976  {
5977  # move task from running tasks list to queue
5978  $this->RequeueRunningTask($TaskId);
5979  }
5980  else
5981  {
5982  # remove task from running tasks list
5983  $this->DB->Query("DELETE FROM RunningTasks"
5984  ." WHERE TaskId = ".intval($TaskId));
5985  }
5986 
5987  # prune running tasks list if necessary
5988  $RunningTasksCount = $this->DB->Query(
5989  "SELECT COUNT(*) AS TaskCount FROM RunningTasks", "TaskCount");
5990  if ($RunningTasksCount > $this->MaxRunningTasksToTrack)
5991  {
5992  $this->DB->Query("DELETE FROM RunningTasks ORDER BY StartedAt"
5993  ." LIMIT ".($RunningTasksCount - $this->MaxRunningTasksToTrack));
5994  }
5995  }
5996  else
5997  {
5998  # release table locks to again allow other sessions to run tasks
5999  $this->DB->Query("UNLOCK TABLES");
6000  }
6001  }
6002 
6008  private function RequeueRunningTask($TaskId)
6009  {
6010  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
6011  $this->DB->Query("INSERT INTO TaskQueue"
6012  ." (Callback,Parameters,Priority,Description)"
6013  ." SELECT Callback,Parameters,Priority,Description"
6014  ." FROM RunningTasks WHERE TaskId = ".intval($TaskId));
6015  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
6016  $this->DB->Query("UNLOCK TABLES");
6017  }
6018 
6024  private function ResetTaskIdGeneratorIfNecessary()
6025  {
6026  $this->DB->Query("LOCK TABLES TaskQueue WRITE");
6027  if ($this->GetTaskQueueSize() == 0 && ($this->GetSecondsBeforeTimeout() > 30) &&
6028  $this->DB->GetNextInsertId("TaskQueue") > 0.90 * Database::INT_MAX_VALUE)
6029  {
6030  $this->DB->Query("TRUNCATE TABLE TaskQueue");
6031  }
6032  $this->DB->Query("UNLOCK TABLES");
6033  }
6034 
6041  private function LogTaskMemoryLeakIfAny($TaskId, $StartingFreeMemory)
6042  {
6043  # tell PHP to garbage collect to free up any memory no longer used
6044  gc_collect_cycles();
6045 
6046  # calculate the logging threshold
6047  $LeakThreshold = self::GetPhpMemoryLimit()
6048  * ($this->BackgroundTaskMemLeakLogThreshold / 100);
6049 
6050  # calculate the amount of memory used by task
6051  $EndingFreeMemory = self::GetFreeMemory();
6052  $MemoryUsed = $StartingFreeMemory - $EndingFreeMemory;
6053 
6054  # if amount of memory used is over threshold
6055  if ($MemoryUsed > $LeakThreshold)
6056  {
6057  # log memory leak
6058  $TaskSynopsis = self::GetTaskCallbackSynopsis($this->GetTask($TaskId));
6059  $this->LogError(self::LOGLVL_DEBUG, "Task ".$TaskSynopsis." leaked "
6060  .number_format($MemoryUsed)." bytes.");
6061  }
6062  }
6063 
6069  public function OnCrash()
6070  {
6071  # attempt to remove any memory limits
6072  $FreeMemory = $this->GetFreeMemory();
6073  ini_set("memory_limit", -1);
6074 
6075  # if there is a background task currently running
6076  if (isset($this->RunningTask))
6077  {
6078  # add info about current page load
6079  $CrashInfo["ElapsedTime"] = $this->GetElapsedExecutionTime();
6080  $CrashInfo["FreeMemory"] = $FreeMemory;
6081  $CrashInfo["REMOTE_ADDR"] = $_SERVER["REMOTE_ADDR"];
6082  $CrashInfo["REQUEST_URI"] = $_SERVER["REQUEST_URI"];
6083  if (isset($_SERVER["REQUEST_TIME"]))
6084  {
6085  $CrashInfo["REQUEST_TIME"] = $_SERVER["REQUEST_TIME"];
6086  }
6087  if (isset($_SERVER["REMOTE_HOST"]))
6088  {
6089  $CrashInfo["REMOTE_HOST"] = $_SERVER["REMOTE_HOST"];
6090  }
6091 
6092  # add info about error that caused crash (if available)
6093  if (function_exists("error_get_last"))
6094  {
6095  $CrashInfo["LastError"] = error_get_last();
6096  }
6097 
6098  # add info about current output buffer contents (if available)
6099  if (ob_get_length() !== FALSE)
6100  {
6101  $CrashInfo["OutputBuffer"] = ob_get_contents();
6102  }
6103 
6104  # if backtrace info is available for the crash
6105  $Backtrace = debug_backtrace();
6106  if (count($Backtrace) > 1)
6107  {
6108  # discard the current context from the backtrace
6109  array_shift($Backtrace);
6110 
6111  # add the backtrace to the crash info
6112  $CrashInfo["Backtrace"] = $Backtrace;
6113  }
6114  # else if saved backtrace info is available
6115  elseif (isset($this->SavedContext))
6116  {
6117  # add the saved backtrace to the crash info
6118  $CrashInfo["Backtrace"] = $this->SavedContext;
6119  }
6120 
6121  # save crash info for currently running task
6122  $DB = new Database();
6123  $DB->Query("UPDATE RunningTasks SET CrashInfo = '"
6124  .addslashes(serialize($CrashInfo))
6125  ."' WHERE TaskId = ".intval($this->RunningTask["TaskId"]));
6126  }
6127 
6128  print("\n");
6129  return;
6130  }
6131 
6148  private function AddToDirList($DirList, $Dir, $SearchLast, $SkipSlashCheck)
6149  {
6150  # convert incoming directory to array of directories (if needed)
6151  $Dirs = is_array($Dir) ? $Dir : array($Dir);
6152 
6153  # reverse array so directories are searched in specified order
6154  $Dirs = array_reverse($Dirs);
6155 
6156  # for each directory
6157  foreach ($Dirs as $Location)
6158  {
6159  # make sure directory includes trailing slash
6160  if (!$SkipSlashCheck)
6161  {
6162  $Location = $Location
6163  .((substr($Location, -1) != "/") ? "/" : "");
6164  }
6165 
6166  # remove directory from list if already present
6167  if (in_array($Location, $DirList))
6168  {
6169  $DirList = array_diff(
6170  $DirList, array($Location));
6171  }
6172 
6173  # add directory to list of directories
6174  if ($SearchLast)
6175  {
6176  array_push($DirList, $Location);
6177  }
6178  else
6179  {
6180  array_unshift($DirList, $Location);
6181  }
6182  }
6183 
6184  # return updated directory list to caller
6185  return $DirList;
6186  }
6187 
6194  private function OutputModificationCallbackShell($Matches)
6195  {
6196  # call previously-stored external function
6197  return call_user_func($this->OutputModificationCallbackInfo["Callback"],
6198  $Matches,
6199  $this->OutputModificationCallbackInfo["Pattern"],
6200  $this->OutputModificationCallbackInfo["Page"],
6201  $this->OutputModificationCallbackInfo["SearchPattern"]);
6202  }
6203 
6212  private function CheckOutputModification($Original, $Modified, $ErrorInfo)
6213  {
6214  # if error was reported by regex engine
6215  if (preg_last_error() !== PREG_NO_ERROR)
6216  {
6217  # log error
6218  $this->LogError(self::LOGLVL_ERROR,
6219  "Error reported by regex engine when modifying output."
6220  ." (".$ErrorInfo.")");
6221 
6222  # use unmodified version of output
6223  $OutputToUse = $Original;
6224  }
6225  # else if modification reduced output by more than threshold
6226  elseif ((strlen(trim($Modified)) / strlen(trim($Original)))
6227  < self::OUTPUT_MODIFICATION_THRESHOLD)
6228  {
6229  # log error
6230  $this->LogError(self::LOGLVL_WARNING,
6231  "Content reduced below acceptable threshold while modifying output."
6232  ." (".$ErrorInfo.")");
6233 
6234  # use unmodified version of output
6235  $OutputToUse = $Original;
6236  }
6237  else
6238  {
6239  # use modified version of output
6240  $OutputToUse = $Modified;
6241  }
6242 
6243  # return output to use to caller
6244  return $OutputToUse;
6245  }
6246 
6249 
6259  private function UpdateSetting(
6260  $FieldName, $NewValue = DB_NOVALUE, $Persistent = TRUE)
6261  {
6262  static $LocalSettings;
6263  if ($NewValue !== DB_NOVALUE)
6264  {
6265  if ($Persistent)
6266  {
6267  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
6268  "ApplicationFrameworkSettings",
6269  $FieldName, $NewValue, NULL, $this->Settings);
6270  }
6271  else
6272  {
6273  $LocalSettings[$FieldName] = $NewValue;
6274  }
6275  }
6276  elseif (!isset($LocalSettings[$FieldName]))
6277  {
6278  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
6279  "ApplicationFrameworkSettings",
6280  $FieldName, $NewValue, NULL, $this->Settings);
6281  }
6282  return $LocalSettings[$FieldName];
6283  }
6284 
6294  private static function IncludeFile($_AF_File, $_AF_ContextVars = array())
6295  {
6296  # set up context
6297  foreach ($_AF_ContextVars as $_AF_VarName => $_AF_VarValue)
6298  {
6299  $$_AF_VarName = $_AF_VarValue;
6300  }
6301  unset($_AF_VarName);
6302  unset($_AF_VarValue);
6303  unset($_AF_ContextVars);
6304 
6305  # add variables to context that we assume are always available
6306  $AF = $GLOBALS["AF"];
6307 
6308  # load file
6309  include($_AF_File);
6310 
6311  # return updated context
6312  $ContextVars = get_defined_vars();
6313  unset($ContextVars["_AF_File"]);
6314  return $ContextVars;
6315  }
6316 
6323  private function FilterContext($Context, $ContextVars)
6324  {
6325  # clear all variables if no setting for context is available
6326  # or setting is FALSE
6327  if (!isset($this->ContextFilters[$Context])
6328  || ($this->ContextFilters[$Context] == FALSE))
6329  {
6330  return array();
6331  }
6332  # keep all variables if setting for context is TRUE
6333  elseif ($this->ContextFilters[$Context] == TRUE)
6334  {
6335  return $ContextVars;
6336  }
6337  else
6338  {
6339  $Prefixes = $this->ContextFilters[$Context];
6340  $FilterFunc = function($VarName) use ($Prefixes) {
6341  foreach ($Prefixes as $Prefix)
6342  {
6343  if (substr($VarName, $Prefix) === 0)
6344  {
6345  return TRUE;
6346  }
6347  }
6348  return FALSE;
6349  };
6350  return array_filter(
6351  $ContextVars, $FilterFunc, ARRAY_FILTER_USE_KEY);
6352  }
6353  }
6354 
6356  private $InterfaceDirList = array(
6357  "interface/%ACTIVEUI%/",
6358  "interface/%DEFAULTUI%/",
6359  );
6364  private $IncludeDirList = array(
6365  "interface/%ACTIVEUI%/include/",
6366  "interface/%ACTIVEUI%/objects/",
6367  "interface/%DEFAULTUI%/include/",
6368  "interface/%DEFAULTUI%/objects/",
6369  );
6371  private $ImageDirList = array(
6372  "interface/%ACTIVEUI%/images/",
6373  "interface/%DEFAULTUI%/images/",
6374  );
6376  private $FunctionDirList = array(
6377  "interface/%ACTIVEUI%/include/",
6378  "interface/%DEFAULTUI%/include/",
6379  "include/",
6380  );
6381 
6382  private $ExpandedDirectoryLists = array();
6383 
6384  const NOVALUE = ".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";
6385 
6386 
6387  # ---- Page Caching (Internal Methods) -----------------------------------
6388 
6394  private function CheckForCachedPage($PageName)
6395  {
6396  # assume no cached page will be found
6397  $CachedPage = NULL;
6398 
6399  # if returning a cached page is allowed
6400  if ($this->CacheCurrentPage)
6401  {
6402  # get fingerprint for requested page
6403  $PageFingerprint = $this->GetPageFingerprint($PageName);
6404 
6405  # look for matching page in cache in database
6406  $this->DB->Query("SELECT * FROM AF_CachedPages"
6407  ." WHERE Fingerprint = '".addslashes($PageFingerprint)."'");
6408 
6409  # if matching page found
6410  if ($this->DB->NumRowsSelected())
6411  {
6412  # if cached page has expired
6413  $Row = $this->DB->FetchRow();
6414  $ExpirationTime = strtotime(
6415  "-".$this->PageCacheExpirationPeriod()." seconds");
6416  if (strtotime($Row["CachedAt"]) < $ExpirationTime)
6417  {
6418  # clear expired pages from cache
6419  $ExpirationTimestamp = date("Y-m-d H:i:s", $ExpirationTime);
6420  $this->DB->Query("DELETE CP, CPTI FROM AF_CachedPages CP,"
6421  ." AF_CachedPageTagInts CPTI"
6422  ." WHERE CP.CachedAt < '".$ExpirationTimestamp."'"
6423  ." AND CPTI.CacheId = CP.CacheId");
6424  $this->DB->Query("DELETE FROM AF_CachedPages "
6425  ." WHERE CachedAt < '".$ExpirationTimestamp."'");
6426  }
6427  else
6428  {
6429  # display cached page and exit
6430  $CachedPage = $Row["PageContent"];
6431  }
6432  }
6433  }
6434 
6435  # return any cached page found to caller
6436  return $CachedPage;
6437  }
6438 
6444  private function UpdatePageCache($PageName, $PageContent)
6445  {
6446  # if page caching is enabled and current page should be cached
6447  if ($this->PageCacheEnabled()
6448  && $this->CacheCurrentPage
6449  && ($PageName != "404"))
6450  {
6451  # if page content looks invalid
6452  if (strlen(trim(strip_tags($PageContent))) == 0)
6453  {
6454  # log error
6455  $LogMsg = "Page not cached because content was empty."
6456  ." (PAGE: ".$PageName.", URL: ".$this->FullUrl().")";
6457  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
6458  }
6459  else
6460  {
6461  # save page to cache
6462  $PageFingerprint = $this->GetPageFingerprint($PageName);
6463  $this->DB->Query("INSERT INTO AF_CachedPages"
6464  ." (Fingerprint, PageContent) VALUES"
6465  ." ('".$this->DB->EscapeString($PageFingerprint)."', '"
6466  .$this->DB->EscapeString($PageContent)."')");
6467  $CacheId = $this->DB->LastInsertId();
6468 
6469  # for each page cache tag that was added
6470  foreach ($this->PageCacheTags as $Tag => $Pages)
6471  {
6472  # if current page is in list for tag
6473  if (in_array("CURRENT", $Pages) || in_array($PageName, $Pages))
6474  {
6475  # look up tag ID
6476  $TagId = $this->GetPageCacheTagId($Tag);
6477 
6478  # mark current page as associated with tag
6479  $this->DB->Query("INSERT INTO AF_CachedPageTagInts"
6480  ." (TagId, CacheId) VALUES "
6481  ." (".intval($TagId).", ".intval($CacheId).")");
6482  }
6483  }
6484  }
6485  }
6486  }
6487 
6493  private function GetPageCacheTagId($Tag)
6494  {
6495  # if tag is a non-negative integer
6496  if (is_numeric($Tag) && ($Tag > 0) && (intval($Tag) == $Tag))
6497  {
6498  # generate ID
6499  $Id = self::PAGECACHETAGIDOFFSET + $Tag;
6500  }
6501  else
6502  {
6503  # look up ID in database
6504  $Id = $this->DB->Query("SELECT TagId FROM AF_CachedPageTags"
6505  ." WHERE Tag = '".addslashes($Tag)."'", "TagId");
6506 
6507  # if ID was not found
6508  if ($Id === NULL)
6509  {
6510  # add tag to database
6511  $this->DB->Query("INSERT INTO AF_CachedPageTags"
6512  ." SET Tag = '".addslashes($Tag)."'");
6513  $Id = $this->DB->LastInsertId();
6514  }
6515  }
6516 
6517  # return tag ID to caller
6518  return $Id;
6519  }
6520 
6526  private function GetPageFingerprint($PageName)
6527  {
6528  # only get the environmental fingerprint once so that it is consistent
6529  # between page construction start and end
6530  static $EnvFingerprint;
6531  if (!isset($EnvFingerprint))
6532  {
6533  $EnvData = json_encode($_GET).json_encode($_POST)
6534  .$_SERVER["SERVER_NAME"];
6535  $EnvFingerprint = md5($EnvData);
6536  }
6537 
6538  # build page fingerprint and return it to caller
6539  return $PageName."-".$EnvFingerprint;
6540  }
6541 
6548  private function UpdateLastUsedTimeForActiveSessions()
6549  {
6550  if ($this->SessionInUse)
6551  {
6552  $_SESSION["AF_SessionLastUsed"] = date("Y-m-d H:i:s");
6553  }
6554  elseif (isset($_SESSION["AF_SessionLastUsed"]))
6555  {
6556  unset($_SESSION["AF_SessionLastUsed"]);
6557  }
6558  }
6559 }
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.
ObjectLocationCacheExpirationInterval($NewInterval=DB_NOVALUE, $Persistent=FALSE)
Get/set object file location cache expiration period in minutes.
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...
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.
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.
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:572
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 INT_MAX_VALUE
Definition: Database.php:1288
const PRIORITY_MEDIUM
Medium (default) priority.
LogError($Level, $Msg)
Write error message to log.
LogHighMemoryUsage($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set whether logging of high memory usage is enabled.
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:721
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.
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:1738
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.
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.
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:880
static GetPhpMaxUploadSize()
Get the maximum size for a file upload in bytes.
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).
MaxExecutionTime($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set maximum PHP execution time.
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.
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.