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  # save execution start time
28  $this->ExecutionStartTime = microtime(TRUE);
29 
30  # set up default object file search locations
31  self::AddObjectDirectory("local/interface/%ACTIVEUI%/objects");
32  self::AddObjectDirectory("interface/%ACTIVEUI%/objects");
33  self::AddObjectDirectory("local/interface/%DEFAULTUI%/objects");
34  self::AddObjectDirectory("interface/%DEFAULTUI%/objects");
35  self::AddObjectDirectory("local/objects");
36  self::AddObjectDirectory("objects");
37 
38  # set up object file autoloader
39  spl_autoload_register(array($this, "AutoloadObjects"));
40 
41  # set up function to output any buffered text in case of crash
42  register_shutdown_function(array($this, "OnCrash"));
43 
44  # if we were not invoked via command line interface
45  if (php_sapi_name() !== "cli")
46  {
47  # build cookie domain string
48  $SessionDomain = isset($_SERVER["SERVER_NAME"]) ? $_SERVER["SERVER_NAME"]
49  : isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"]
50  : php_uname("n");
51 
52  # if it appears our session storage area is writable
53  if (is_writable(session_save_path()))
54  {
55  # store our session files in a subdirectory to avoid
56  # accidentally sharing sessions with other installations
57  # on the same domain
58  $SessionStorage = session_save_path()
59  ."/".self::$AppName."_".md5($SessionDomain.dirname(__FILE__));
60 
61  # create session storage subdirectory if not found
62  if (!is_dir($SessionStorage)) { mkdir($SessionStorage, 0700 ); }
63 
64  # if session storage subdirectory is writable
65  if (is_writable($SessionStorage))
66  {
67  # save parameters of our session storage as instance variables
68  # for later use
69  $this->SessionGcProbability =
70  ini_get("session.gc_probability") / ini_get("session.gc_divisor");
71  # require a gc probability of at least MIN_GC_PROBABILITY
72  if ($this->SessionGcProbability < self::MIN_GC_PROBABILITY)
73  {
74  $this->SessionGcProbability = self::MIN_GC_PROBABILITY;
75  }
76 
77  $this->SessionStorage = $SessionStorage;
78 
79  # set the new session storage location
80  session_save_path($SessionStorage);
81 
82  # disable PHP's garbage collection, as it does not handle
83  # subdirectories (instead, we'll do the cleanup as we run
84  # background tasks)
85  ini_set("session.gc_probability", 0);
86  }
87  }
88 
89  # set garbage collection max period to our session lifetime
90  ini_set("session.gc_maxlifetime", self::$SessionLifetime);
91  session_set_cookie_params(
92  self::$SessionLifetime, "/", $SessionDomain);
93 
94  # attempt to start session
95  $SessionStarted = @session_start();
96 
97  # if session start failed
98  if (!$SessionStarted)
99  {
100  # regenerate session ID and attempt to start session again
101  session_regenerate_id(TRUE);
102  session_start();
103  }
104  }
105 
106  # set up our internal environment
107  $this->DB = new Database();
108 
109  # set up our exception handler
110  set_exception_handler(array($this, "GlobalExceptionHandler"));
111 
112  # perform any work needed to undo PHP magic quotes
113  $this->UndoMagicQuotes();
114 
115  # load our settings from database
116  $this->LoadSettings();
117 
118  # set PHP maximum execution time
119  ini_set("max_execution_time", $this->Settings["MaxExecTime"]);
120  set_time_limit($this->Settings["MaxExecTime"]);
121 
122  # register events we handle internally
123  $this->RegisterEvent($this->PeriodicEvents);
124  $this->RegisterEvent($this->UIEvents);
125 
126  # attempt to create SCSS cache directory if needed and it does not exist
127  if ($this->ScssSupportEnabled() && !is_dir(self::$ScssCacheDir))
128  { @mkdir(self::$ScssCacheDir, 0777, TRUE); }
129 
130  # attempt to create minimized JS cache directory if needed and it does not exist
131  if ($this->UseMinimizedJavascript()
133  && !is_dir(self::$JSMinCacheDir))
134  {
135  @mkdir(self::$JSMinCacheDir, 0777, TRUE);
136  }
137  }
144  public function __destruct()
145  {
146  # if template location cache is flagged to be saved
147  if ($this->SaveTemplateLocationCache)
148  {
149  # write template location cache out and update cache expiration
150  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
151  ." SET TemplateLocationCache = '"
152  .addslashes(serialize(
153  $this->TemplateLocationCache))."',"
154  ." TemplateLocationCacheExpiration = '"
155  .date("Y-m-d H:i:s",
156  $this->TemplateLocationCacheExpiration)."'");
157  }
158 
159  # if object location cache is flagged to be saved
160  if (self::$SaveObjectLocationCache)
161  {
162  # write object location cache out and update cache expiration
163  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
164  ." SET ObjectLocationCache = '"
165  .addslashes(serialize(
166  self::$ObjectLocationCache))."',"
167  ." ObjectLocationCacheExpiration = '"
168  .date("Y-m-d H:i:s",
169  self::$ObjectLocationCacheExpiration)."'");
170  }
171  }
179  public function GlobalExceptionHandler($Exception)
180  {
181  # display exception info
182  $Location = str_replace(getcwd()."/", "",
183  $Exception->getFile()."[".$Exception->getLine()."]");
184  ?><table width="100%" cellpadding="5"
185  style="border: 2px solid #666666; background: #CCCCCC;
186  font-family: Courier New, Courier, monospace;
187  margin-top: 10px;"><tr><td>
188  <div style="color: #666666;">
189  <span style="font-size: 150%;">
190  <b>Uncaught Exception</b></span><br />
191  <b>Message:</b> <i><?PHP print $Exception->getMessage(); ?></i><br />
192  <b>Location:</b> <i><?PHP print $Location; ?></i><br />
193  <b>Trace:</b>
194  <blockquote><pre><?PHP print preg_replace(
195  ":(#[0-9]+) ".getcwd()."/".":", "$1 ",
196  $Exception->getTraceAsString());
197  ?></pre></blockquote>
198  </div>
199  </td></tr></table><?PHP
200 
201  # log exception if possible
202  $TraceString = $Exception->getTraceAsString();
203  $TraceString = str_replace("\n", ", ", $TraceString);
204  $TraceString = preg_replace(":(#[0-9]+) ".getcwd()."/".":", "$1 ", $TraceString);
205  $LogMsg = "Uncaught exception (".$Exception->getMessage().") at "
206  .$Location.". TRACE: ".$TraceString." URL: ".$this->FullUrl();
207  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
208  }
225  public static function AddObjectDirectory(
226  $Dir, $Prefix = "", $ClassPattern = NULL, $ClassReplacement = NULL)
227  {
228  # make sure directory has trailing slash
229  $Dir = $Dir.((substr($Dir, -1) != "/") ? "/" : "");
230 
231  # add directory to directory list
232  self::$ObjectDirectories[$Dir] = array(
233  "Prefix" => $Prefix,
234  "ClassPattern" => $ClassPattern,
235  "ClassReplacement" => $ClassReplacement,
236  );
237  }
238 
258  public function AddImageDirectories(
259  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
260  {
261  # add directories to existing image directory list
262  $this->ImageDirList = $this->AddToDirList(
263  $this->ImageDirList, $Dir, $SearchLast, $SkipSlashCheck);
264  }
265 
286  public function AddIncludeDirectories(
287  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
288  {
289  # add directories to existing image directory list
290  $this->IncludeDirList = $this->AddToDirList(
291  $this->IncludeDirList, $Dir, $SearchLast, $SkipSlashCheck);
292  }
293 
313  public function AddInterfaceDirectories(
314  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
315  {
316  # add directories to existing image directory list
317  $this->InterfaceDirList = $this->AddToDirList(
318  $this->InterfaceDirList, $Dir, $SearchLast, $SkipSlashCheck);
319  }
320 
340  public function AddFunctionDirectories(
341  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
342  {
343  # add directories to existing image directory list
344  $this->FunctionDirList = $this->AddToDirList(
345  $this->FunctionDirList, $Dir, $SearchLast, $SkipSlashCheck);
346  }
347 
353  public function SetBrowserDetectionFunc($DetectionFunc)
354  {
355  $this->BrowserDetectFunc = $DetectionFunc;
356  }
357 
364  public function AddUnbufferedCallback($Callback, $Parameters=array())
365  {
366  if (is_callable($Callback))
367  {
368  $this->UnbufferedCallbacks[] = array($Callback, $Parameters);
369  }
370  }
371 
378  public function TemplateLocationCacheExpirationInterval($NewInterval = DB_NOVALUE)
379  {
380  return $this->UpdateSetting("TemplateLocationCacheInterval", $NewInterval);
381  }
382 
386  public function ClearTemplateLocationCache()
387  {
388  $this->TemplateLocationCache = array();
389  $this->SaveTemplateLocationCache = TRUE;
390  }
391 
398  public function ObjectLocationCacheExpirationInterval($NewInterval = DB_NOVALUE)
399  {
400  return $this->UpdateSetting("ObjectLocationCacheInterval", $NewInterval);
401  }
402 
406  public function ClearObjectLocationCache()
407  {
408  self::$ObjectLocationCache = array();
409  self::$SaveObjectLocationCache = TRUE;
410  }
411 
418  public function UrlFingerprintingEnabled($NewValue = DB_NOVALUE)
419  {
420  return $this->UpdateSetting("UrlFingerprintingEnabled", $NewValue);
421  }
422 
430  public function ScssSupportEnabled($NewValue = DB_NOVALUE)
431  {
432  return $this->UpdateSetting("ScssSupportEnabled", $NewValue);
433  }
434 
443  public function GenerateCompactCss($NewValue = DB_NOVALUE)
444  {
445  return $this->UpdateSetting("GenerateCompactCss", $NewValue);
446  }
447 
456  public function UseMinimizedJavascript($NewValue = DB_NOVALUE)
457  {
458  return $this->UpdateSetting("UseMinimizedJavascript", $NewValue);
459  }
460 
469  public function JavascriptMinimizationEnabled($NewValue = DB_NOVALUE)
470  {
471  return $this->UpdateSetting("JavascriptMinimizationEnabled", $NewValue);
472  }
473 
487  public function RecordContextInCaseOfCrash(
488  $BacktraceOptions = 0, $BacktraceLimit = 0)
489  {
490  if (version_compare(PHP_VERSION, "5.4.0", ">="))
491  {
492  $this->SavedContext = debug_backtrace(
493  $BacktraceOptions, $BacktraceLimit);
494  }
495  else
496  {
497  $this->SavedContext = debug_backtrace($BacktraceOptions);
498  }
499  array_shift($this->SavedContext);
500  }
501 
506  public function LoadPage($PageName)
507  {
508  # perform any clean URL rewriting
509  $PageName = $this->RewriteCleanUrls($PageName);
510 
511  # sanitize incoming page name and save local copy
512  $PageName = preg_replace("/[^a-zA-Z0-9_.-]/", "", $PageName);
513  $this->PageName = $PageName;
514 
515  # if page caching is turned on
516  if ($this->PageCacheEnabled())
517  {
518  # if we have a cached page
519  $CachedPage = $this->CheckForCachedPage($PageName);
520  if ($CachedPage !== NULL)
521  {
522  # set header to indicate cache hit was found
523  header("X-ScoutAF-Cache: HIT");
524 
525  # display cached page and exit
526  print $CachedPage;
527  return;
528  }
529  else
530  {
531  # set header to indicate no cache hit was found
532  header("X-ScoutAF-Cache: MISS");
533  }
534  }
535 
536  # buffer any output from includes or PHP file
537  ob_start();
538 
539  # include any files needed to set up execution environment
540  foreach ($this->EnvIncludes as $IncludeFile)
541  {
542  include($IncludeFile);
543  }
544 
545  # signal page load
546  $this->SignalEvent("EVENT_PAGE_LOAD", array("PageName" => $PageName));
547 
548  # signal PHP file load
549  $SignalResult = $this->SignalEvent("EVENT_PHP_FILE_LOAD", array(
550  "PageName" => $PageName));
551 
552  # if signal handler returned new page name value
553  $NewPageName = $PageName;
554  if (($SignalResult["PageName"] != $PageName)
555  && strlen($SignalResult["PageName"]))
556  {
557  # if new page name value is page file
558  if (file_exists($SignalResult["PageName"]))
559  {
560  # use new value for PHP file name
561  $PageFile = $SignalResult["PageName"];
562  }
563  else
564  {
565  # use new value for page name
566  $NewPageName = $SignalResult["PageName"];
567  }
568 
569  # update local copy of page name
570  $this->PageName = $NewPageName;
571  }
572 
573  # if we do not already have a PHP file
574  if (!isset($PageFile))
575  {
576  # look for PHP file for page
577  $OurPageFile = "pages/".$NewPageName.".php";
578  $LocalPageFile = "local/pages/".$NewPageName.".php";
579  $PageFile = file_exists($LocalPageFile) ? $LocalPageFile
580  : (file_exists($OurPageFile) ? $OurPageFile
581  : "pages/".$this->DefaultPage.".php");
582  }
583 
584  # load PHP file
585  include($PageFile);
586 
587  # save buffered output to be displayed later after HTML file loads
588  $PageOutput = ob_get_contents();
589  ob_end_clean();
590 
591  # signal PHP file load is complete
592  ob_start();
593  $Context["Variables"] = get_defined_vars();
594  $this->SignalEvent("EVENT_PHP_FILE_LOAD_COMPLETE",
595  array("PageName" => $PageName, "Context" => $Context));
596  $PageCompleteOutput = ob_get_contents();
597  ob_end_clean();
598 
599  # set up for possible TSR (Terminate and Stay Resident :))
600  $ShouldTSR = $this->PrepForTSR();
601 
602  # if PHP file indicated we should autorefresh to somewhere else
603  if (($this->JumpToPage) && ($this->JumpToPageDelay == 0))
604  {
605  if (!strlen(trim($PageOutput)))
606  {
607  # if client supports HTTP/1.1, use a 303 as it is most accurate
608  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.1")
609  {
610  header("HTTP/1.1 303 See Other");
611  header("Location: ".$this->JumpToPage);
612  }
613  else
614  {
615  # if the request was an HTTP/1.0 GET or HEAD, then
616  # use a 302 response code.
617 
618  # NB: both RFC 2616 (HTTP/1.1) and RFC1945 (HTTP/1.0)
619  # explicitly prohibit automatic redirection via a 302
620  # if the request was not GET or HEAD.
621  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.0" &&
622  ($_SERVER["REQUEST_METHOD"] == "GET" ||
623  $_SERVER["REQUEST_METHOD"] == "HEAD") )
624  {
625  header("HTTP/1.0 302 Found");
626  header("Location: ".$this->JumpToPage);
627  }
628 
629  # otherwise, fall back to a meta refresh
630  else
631  {
632  print '<html><head><meta http-equiv="refresh" '
633  .'content="0; URL='.$this->JumpToPage.'">'
634  .'</head><body></body></html>';
635  }
636  }
637  }
638  }
639  # else if HTML loading is not suppressed
640  elseif (!$this->SuppressHTML)
641  {
642  # set content-type to get rid of diacritic errors
643  header("Content-Type: text/html; charset="
644  .$this->HtmlCharset, TRUE);
645 
646  # load common HTML file (defines common functions) if available
647  $CommonHtmlFile = $this->FindFile($this->IncludeDirList,
648  "Common", array("tpl", "html"));
649  if ($CommonHtmlFile) { include($CommonHtmlFile); }
650 
651  # load UI functions
652  $this->LoadUIFunctions();
653 
654  # begin buffering content
655  ob_start();
656 
657  # signal HTML file load
658  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD", array(
659  "PageName" => $PageName));
660 
661  # if signal handler returned new page name value
662  $NewPageName = $PageName;
663  $PageContentFile = NULL;
664  if (($SignalResult["PageName"] != $PageName)
665  && strlen($SignalResult["PageName"]))
666  {
667  # if new page name value is HTML file
668  if (file_exists($SignalResult["PageName"]))
669  {
670  # use new value for HTML file name
671  $PageContentFile = $SignalResult["PageName"];
672  }
673  else
674  {
675  # use new value for page name
676  $NewPageName = $SignalResult["PageName"];
677  }
678  }
679 
680  # load page content HTML file if available
681  if ($PageContentFile === NULL)
682  {
683  $PageContentFile = $this->FindFile(
684  $this->InterfaceDirList, $NewPageName,
685  array("tpl", "html"));
686  }
687  if ($PageContentFile)
688  {
689  include($PageContentFile);
690  }
691  else
692  {
693  print "<h2>ERROR: No HTML/TPL template found"
694  ." for this page (".$NewPageName.").</h2>";
695  }
696 
697  # signal HTML file load complete
698  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD_COMPLETE");
699 
700  # stop buffering and save output
701  $PageContentOutput = ob_get_contents();
702  ob_end_clean();
703 
704  # load page start HTML file if available
705  ob_start();
706  $PageStartFile = $this->FindFile($this->IncludeDirList, "Start",
707  array("tpl", "html"), array("StdPage", "StandardPage"));
708  if ($PageStartFile) { include($PageStartFile); }
709  $PageStartOutput = ob_get_contents();
710  ob_end_clean();
711 
712  # if page auto-refresh requested
713  if ($this->JumpToPage)
714  {
715  $RefreshLine = '<meta http-equiv="refresh" content="'
716  .$this->JumpToPageDelay.'; url='.$this->JumpToPage.'">';
717  $PageStartOutput = str_replace("<head>", "<head>\n".$RefreshLine,
718  $PageStartOutput);
719  }
720 
721  # load page end HTML file if available
722  ob_start();
723  $PageEndFile = $this->FindFile($this->IncludeDirList, "End",
724  array("tpl", "html"), array("StdPage", "StandardPage"));
725  if ($PageEndFile) { include($PageEndFile); }
726  $PageEndOutput = ob_get_contents();
727  ob_end_clean();
728 
729  # get list of any required files not loaded
730  $RequiredFiles = $this->GetRequiredFilesNotYetLoaded($PageContentFile);
731 
732  # if a browser detection function has been made available
733  if (is_callable($this->BrowserDetectFunc))
734  {
735  # call function to get browser list
736  $Browsers = call_user_func($this->BrowserDetectFunc);
737 
738  # for each required file
739  $NewRequiredFiles = array();
740  foreach ($RequiredFiles as $File)
741  {
742  # if file name includes browser keyword
743  if (preg_match("/%BROWSER%/", $File))
744  {
745  # for each browser
746  foreach ($Browsers as $Browser)
747  {
748  # substitute in browser name and add to new file list
749  $NewRequiredFiles[] = preg_replace(
750  "/%BROWSER%/", $Browser, $File);
751  }
752  }
753  else
754  {
755  # add to new file list
756  $NewRequiredFiles[] = $File;
757  }
758  }
759  $RequiredFiles = $NewRequiredFiles;
760  }
761  else
762  {
763  # filter out any files with browser keyword in their name
764  $NewRequiredFiles = array();
765  foreach ($RequiredFiles as $File)
766  {
767  if (!preg_match("/%BROWSER%/", $File))
768  {
769  $NewRequiredFiles[] = $File;
770  }
771  }
772  $RequiredFiles = $NewRequiredFiles;
773  }
774 
775  # for each required file
776  foreach ($RequiredFiles as $File)
777  {
778  # locate specific file to use
779  $FilePath = $this->GUIFile($File);
780 
781  # if file was found
782  if ($FilePath)
783  {
784  # generate tag for file
785  $Tag = $this->GetUIFileLoadingTag($FilePath);
786 
787  # add file to HTML output based on file type
788  $FileType = $this->GetFileType($FilePath);
789  switch ($FileType)
790  {
791  case self::FT_CSS:
792  $PageStartOutput = preg_replace(
793  "#</head>#i", $Tag."\n</head>", $PageStartOutput, 1);
794  break;
795 
796  case self::FT_JAVASCRIPT:
797  $PageEndOutput = preg_replace(
798  "#</body>#i", $Tag."\n</body>", $PageEndOutput, 1);
799  break;
800  }
801  }
802  }
803 
804  # assemble full page
805  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
806 
807  # perform any regular expression replacements in output
808  $NewFullPageOutput = preg_replace($this->OutputModificationPatterns,
809  $this->OutputModificationReplacements, $FullPageOutput);
810 
811  # check to make sure replacements didn't fail
812  $FullPageOutput = $this->CheckOutputModification(
813  $FullPageOutput, $NewFullPageOutput,
814  "regular expression replacements");
815 
816  # for each registered output modification callback
817  foreach ($this->OutputModificationCallbacks as $Info)
818  {
819  # set up data for callback
820  $this->OutputModificationCallbackInfo = $Info;
821 
822  # perform output modification
823  $NewFullPageOutput = preg_replace_callback($Info["SearchPattern"],
824  array($this, "OutputModificationCallbackShell"),
825  $FullPageOutput);
826 
827  # check to make sure modification didn't fail
828  $ErrorInfo = "callback info: ".print_r($Info, TRUE);
829  $FullPageOutput = $this->CheckOutputModification(
830  $FullPageOutput, $NewFullPageOutput, $ErrorInfo);
831  }
832 
833  # provide the opportunity to modify full page output
834  $SignalResult = $this->SignalEvent("EVENT_PAGE_OUTPUT_FILTER", array(
835  "PageOutput" => $FullPageOutput));
836  if (isset($SignalResult["PageOutput"])
837  && strlen(trim($SignalResult["PageOutput"])))
838  {
839  $FullPageOutput = $SignalResult["PageOutput"];
840  }
841 
842  # if relative paths may not work because we were invoked via clean URL
843  if ($this->CleanUrlRewritePerformed || self::WasUrlRewritten())
844  {
845  # if using the <base> tag is okay
846  $BaseUrl = $this->BaseUrl();
847  if ($this->UseBaseTag)
848  {
849  # add <base> tag to header
850  $PageStartOutput = str_replace("<head>",
851  "<head><base href=\"".$BaseUrl."\" />",
852  $PageStartOutput);
853 
854  # re-assemble full page with new header
855  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
856 
857  # the absolute URL to the current page
858  $FullUrl = $BaseUrl . $this->GetPageLocation();
859 
860  # make HREF attribute values with just a fragment ID
861  # absolute since they don't work with the <base> tag because
862  # they are relative to the current page/URL, not the site
863  # root
864  $NewFullPageOutput = preg_replace(
865  array("%href=\"(#[^:\" ]+)\"%i", "%href='(#[^:' ]+)'%i"),
866  array("href=\"".$FullUrl."$1\"", "href='".$FullUrl."$1'"),
867  $FullPageOutput);
868 
869  # check to make sure HREF cleanup didn't fail
870  $FullPageOutput = $this->CheckOutputModification(
871  $FullPageOutput, $NewFullPageOutput,
872  "HREF cleanup");
873  }
874  else
875  {
876  # try to fix any relative paths throughout code
877  $RelativePathPatterns = array(
878  "%src=\"/?([^?*:;{}\\\\\" ]+)\.(js|css|gif|png|jpg)\"%i",
879  "%src='/?([^?*:;{}\\\\' ]+)\.(js|css|gif|png|jpg)'%i",
880  # don't rewrite HREF attributes that are just
881  # fragment IDs because they are relative to the
882  # current page/URL, not the site root
883  "%href=\"/?([^#][^:\" ]*)\"%i",
884  "%href='/?([^#][^:' ]*)'%i",
885  "%action=\"/?([^#][^:\" ]*)\"%i",
886  "%action='/?([^#][^:' ]*)'%i",
887  "%@import\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
888  "%@import\s+url\('/?([^:\" ]+)'\s*\)%i",
889  "%src:\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
890  "%src:\s+url\('/?([^:\" ]+)'\s*\)%i",
891  "%@import\s+\"/?([^:\" ]+)\"\s*%i",
892  "%@import\s+'/?([^:\" ]+)'\s*%i",
893  );
894  $RelativePathReplacements = array(
895  "src=\"".$BaseUrl."$1.$2\"",
896  "src=\"".$BaseUrl."$1.$2\"",
897  "href=\"".$BaseUrl."$1\"",
898  "href=\"".$BaseUrl."$1\"",
899  "action=\"".$BaseUrl."$1\"",
900  "action=\"".$BaseUrl."$1\"",
901  "@import url(\"".$BaseUrl."$1\")",
902  "@import url('".$BaseUrl."$1')",
903  "src: url(\"".$BaseUrl."$1\")",
904  "src: url('".$BaseUrl."$1')",
905  "@import \"".$BaseUrl."$1\"",
906  "@import '".$BaseUrl."$1'",
907  );
908  $NewFullPageOutput = preg_replace($RelativePathPatterns,
909  $RelativePathReplacements, $FullPageOutput);
910 
911  # check to make sure relative path fixes didn't fail
912  $FullPageOutput = $this->CheckOutputModification(
913  $FullPageOutput, $NewFullPageOutput,
914  "relative path fixes");
915  }
916  }
917 
918  # handle any necessary alternate domain rewriting
919  $FullPageOutput = $this->RewriteAlternateDomainUrls($FullPageOutput);
920 
921  # update page cache for this page
922  $this->UpdatePageCache($PageName, $FullPageOutput);
923 
924  # write out full page
925  print $FullPageOutput;
926  }
927 
928  # run any post-processing routines
929  foreach ($this->PostProcessingFuncs as $Func)
930  {
931  call_user_func_array($Func["FunctionName"], $Func["Arguments"]);
932  }
933 
934  # write out any output buffered from page code execution
935  if (strlen($PageOutput))
936  {
937  if (!$this->SuppressHTML)
938  {
939  ?><table width="100%" cellpadding="5"
940  style="border: 2px solid #666666; background: #CCCCCC;
941  font-family: Courier New, Courier, monospace;
942  margin-top: 10px;"><tr><td><?PHP
943  }
944  if ($this->JumpToPage)
945  {
946  ?><div style="color: #666666;"><span style="font-size: 150%;">
947  <b>Page Jump Aborted</b></span>
948  (because of error or other unexpected output)<br />
949  <b>Jump Target:</b>
950  <i><?PHP print($this->JumpToPage); ?></i></div><?PHP
951  }
952  print $PageOutput;
953  if (!$this->SuppressHTML)
954  {
955  ?></td></tr></table><?PHP
956  }
957  }
958 
959  # write out any output buffered from the page code execution complete signal
960  if (!$this->JumpToPage && !$this->SuppressHTML && strlen($PageCompleteOutput))
961  {
962  print $PageCompleteOutput;
963  }
964 
965  # log slow page loads
966  if ($this->LogSlowPageLoads()
967  && !$this->DoNotLogSlowPageLoad
968  && ($this->GetElapsedExecutionTime()
969  >= ($this->SlowPageLoadThreshold())))
970  {
971  $RemoteHost = gethostbyaddr($_SERVER["REMOTE_ADDR"]);
972  if ($RemoteHost === FALSE)
973  {
974  $RemoteHost = $_SERVER["REMOTE_ADDR"];
975  }
976  elseif ($RemoteHost != $_SERVER["REMOTE_ADDR"])
977  {
978  $RemoteHost .= " (".$_SERVER["REMOTE_ADDR"].")";
979  }
980  $SlowPageLoadMsg = "Slow page load ("
981  .intval($this->GetElapsedExecutionTime())."s) for "
982  .$this->FullUrl()." from ".$RemoteHost;
983  $this->LogMessage(self::LOGLVL_INFO, $SlowPageLoadMsg);
984  }
985 
986  # execute callbacks that should not have their output buffered
987  foreach ($this->UnbufferedCallbacks as $Callback)
988  {
989  call_user_func_array($Callback[0], $Callback[1]);
990  }
991 
992  # log high memory usage
993  if (function_exists("memory_get_peak_usage"))
994  {
995  $MemoryThreshold = ($this->HighMemoryUsageThreshold()
996  * $this->GetPhpMemoryLimit()) / 100;
997  if ($this->LogHighMemoryUsage()
998  && (memory_get_peak_usage() >= $MemoryThreshold))
999  {
1000  $HighMemUsageMsg = "High peak memory usage ("
1001  .intval(memory_get_peak_usage()).") for "
1002  .$this->FullUrl()." from "
1003  .$_SERVER["REMOTE_ADDR"];
1004  $this->LogMessage(self::LOGLVL_INFO, $HighMemUsageMsg);
1005  }
1006  }
1007 
1008  # terminate and stay resident (TSR!) if indicated and HTML has been output
1009  # (only TSR if HTML has been output because otherwise browsers will misbehave)
1010  if ($ShouldTSR) { $this->LaunchTSR(); }
1011  }
1012 
1018  public function GetPageName()
1019  {
1020  return $this->PageName;
1021  }
1022 
1028  public function GetPageLocation()
1029  {
1030  # retrieve current URL
1031  $Url = self::GetScriptUrl();
1032 
1033  # remove the base path if present
1034  $BasePath = $this->Settings["BasePath"];
1035  if (stripos($Url, $BasePath) === 0)
1036  {
1037  $Url = substr($Url, strlen($BasePath));
1038  }
1039 
1040  # if we're being accessed via an alternate domain,
1041  # add the appropriate prefix in
1042  if ($this->HtaccessSupport() &&
1043  self::$RootUrlOverride !== NULL)
1044  {
1045  $VHost = $_SERVER["SERVER_NAME"];
1046  if (isset($this->AlternateDomainPrefixes[$VHost]))
1047  {
1048  $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
1049  $Url = $ThisPrefix."/".$Url;
1050  }
1051  }
1052 
1053  return $Url;
1054  }
1055 
1061  public function GetPageUrl()
1062  {
1063  return self::BaseUrl() . $this->GetPageLocation();
1064  }
1065 
1077  public function SetJumpToPage($Page, $Delay = 0, $IsLiteral = FALSE)
1078  {
1079  if (!is_null($Page)
1080  && (!$IsLiteral)
1081  && (strpos($Page, "?") === FALSE)
1082  && ((strpos($Page, "=") !== FALSE)
1083  || ((stripos($Page, ".php") === FALSE)
1084  && (stripos($Page, ".htm") === FALSE)
1085  && (strpos($Page, "/") === FALSE)))
1086  && (stripos($Page, "http://") !== 0)
1087  && (stripos($Page, "https://") !== 0))
1088  {
1089  $this->JumpToPage = self::BaseUrl() . "index.php?P=".$Page;
1090  }
1091  else
1092  {
1093  $this->JumpToPage = $Page;
1094  }
1095  $this->JumpToPageDelay = $Delay;
1096  }
1097 
1102  public function JumpToPageIsSet()
1103  {
1104  return ($this->JumpToPage === NULL) ? FALSE : TRUE;
1105  }
1106 
1116  public function HtmlCharset($NewSetting = NULL)
1117  {
1118  if ($NewSetting !== NULL) { $this->HtmlCharset = $NewSetting; }
1119  return $this->HtmlCharset;
1120  }
1121 
1131  public function DoNotMinimizeFile($File)
1132  {
1133  if (!is_array($File)) { $File = array($File); }
1134  $this->DoNotMinimizeList = array_merge($this->DoNotMinimizeList, $File);
1135  }
1136 
1147  public function UseBaseTag($NewValue = NULL)
1148  {
1149  if ($NewValue !== NULL) { $this->UseBaseTag = $NewValue ? TRUE : FALSE; }
1150  return $this->UseBaseTag;
1151  }
1152 
1159  public function SuppressHTMLOutput($NewSetting = TRUE)
1160  {
1161  $this->SuppressHTML = $NewSetting;
1162  }
1163 
1169  public static function DefaultUserInterface($UIName = NULL)
1170  {
1171  if ($UIName !== NULL)
1172  {
1173  self::$DefaultUI = $UIName;
1174  }
1175  return self::$DefaultUI;
1176  }
1177 
1184  public static function ActiveUserInterface($UIName = NULL)
1185  {
1186  if ($UIName !== NULL)
1187  {
1188  self::$ActiveUI = preg_replace("/^SPTUI--/", "", $UIName);
1189  }
1190  return self::$ActiveUI;
1191  }
1192 
1203  public function GetUserInterfaces($FilterExp = NULL)
1204  {
1205  static $Interfaces;
1206 
1207  if (!isset($Interfaces[$FilterExp]))
1208  {
1209  # retrieve paths to user interface directories
1210  $Paths = $this->GetUserInterfacePaths($FilterExp);
1211 
1212  # start out with an empty list
1213  $Interfaces[$FilterExp] = array();
1214 
1215  # for each possible UI directory
1216  foreach ($Paths as $CanonicalName => $Path)
1217  {
1218  # if name file available
1219  $LabelFile = $Path."/NAME";
1220  if (is_readable($LabelFile))
1221  {
1222  # read the UI name
1223  $Label = file_get_contents($LabelFile);
1224 
1225  # if the UI name looks reasonable
1226  if (strlen(trim($Label)))
1227  {
1228  # use read name
1229  $Interfaces[$FilterExp][$CanonicalName] = $Label;
1230  }
1231  }
1232 
1233  # if we do not have a name yet
1234  if (!isset($Interfaces[$FilterExp][$CanonicalName]))
1235  {
1236  # use base directory for name
1237  $Interfaces[$FilterExp][$CanonicalName] = basename($Path);
1238  }
1239  }
1240  }
1241 
1242  # return list to caller
1243  return $Interfaces[$FilterExp];
1244  }
1245 
1254  public function GetUserInterfacePaths($FilterExp = NULL)
1255  {
1256  static $InterfacePaths;
1257 
1258  if (!isset($InterfacePaths[$FilterExp]))
1259  {
1260  # extract possible UI directories from interface directory list
1261  $InterfaceDirs = array();
1262  foreach ($this->InterfaceDirList as $Dir)
1263  {
1264  $Matches = array();
1265  if (preg_match("#([a-zA-Z0-9/]*interface)/[a-zA-Z0-9%/]*#",
1266  $Dir, $Matches))
1267  {
1268  $Dir = $Matches[1];
1269  if (!in_array($Dir, $InterfaceDirs))
1270  {
1271  $InterfaceDirs[] = $Dir;
1272  }
1273  }
1274  }
1275 
1276  # reverse order of interface directories so that the directory
1277  # returned is the base directory for the interface
1278  $InterfaceDirs = array_reverse($InterfaceDirs);
1279 
1280  # start out with an empty list
1281  $InterfacePaths[$FilterExp] = array();
1282  $InterfacesFound = array();
1283 
1284  # for each possible UI directory
1285  foreach ($InterfaceDirs as $InterfaceDir)
1286  {
1287  $Dir = dir($InterfaceDir);
1288 
1289  # for each file in current directory
1290  while (($DirEntry = $Dir->read()) !== FALSE)
1291  {
1292  $InterfacePath = $InterfaceDir."/".$DirEntry;
1293 
1294  # skip anything we have already found
1295  # or that doesn't have a name in the required format
1296  # or that isn't a directory
1297  # or that doesn't match the filter regex (if supplied)
1298  if (in_array($DirEntry, $InterfacesFound)
1299  || !preg_match('/^[a-zA-Z0-9]+$/', $DirEntry)
1300  || !is_dir($InterfacePath)
1301  || (($FilterExp !== NULL)
1302  && !preg_match($FilterExp, $InterfacePath)))
1303  {
1304  continue;
1305  }
1306 
1307  # add interface to list
1308  $InterfacePaths[$FilterExp][$DirEntry] = $InterfacePath;
1309  $InterfacesFound[] = $DirEntry;
1310  }
1311 
1312  $Dir->close();
1313  }
1314  }
1315 
1316  # return list to caller
1317  return $InterfacePaths[$FilterExp];
1318  }
1319 
1344  public function AddPostProcessingCall($FunctionName,
1345  &$Arg1 = self::NOVALUE, &$Arg2 = self::NOVALUE, &$Arg3 = self::NOVALUE,
1346  &$Arg4 = self::NOVALUE, &$Arg5 = self::NOVALUE, &$Arg6 = self::NOVALUE,
1347  &$Arg7 = self::NOVALUE, &$Arg8 = self::NOVALUE, &$Arg9 = self::NOVALUE)
1348  {
1349  $FuncIndex = count($this->PostProcessingFuncs);
1350  $this->PostProcessingFuncs[$FuncIndex]["FunctionName"] = $FunctionName;
1351  $this->PostProcessingFuncs[$FuncIndex]["Arguments"] = array();
1352  $Index = 1;
1353  while (isset(${"Arg".$Index}) && (${"Arg".$Index} !== self::NOVALUE))
1354  {
1355  $this->PostProcessingFuncs[$FuncIndex]["Arguments"][$Index]
1356  =& ${"Arg".$Index};
1357  $Index++;
1358  }
1359  }
1360 
1366  public function AddEnvInclude($FileName)
1367  {
1368  $this->EnvIncludes[] = $FileName;
1369  }
1370 
1377  public function GUIFile($FileName)
1378  {
1379  # determine which location to search based on file suffix
1380  $FileType = $this->GetFileType($FileName);
1381  $DirList = ($FileType == self::FT_IMAGE)
1382  ? $this->ImageDirList : $this->IncludeDirList;
1383 
1384  # if directed to use minimized JavaScript file
1385  if (($FileType == self::FT_JAVASCRIPT) && $this->UseMinimizedJavascript())
1386  {
1387  # look for minimized version of file
1388  $MinimizedFileName = substr_replace($FileName, ".min", -3, 0);
1389  $FoundFileName = $this->FindFile($DirList, $MinimizedFileName);
1390 
1391  # if minimized file was not found
1392  if (is_null($FoundFileName))
1393  {
1394  # look for unminimized file
1395  $FoundFileName = $this->FindFile($DirList, $FileName);
1396 
1397  # if unminimized file found
1398  if (!is_null($FoundFileName))
1399  {
1400  # if minimization enabled and supported
1401  if ($this->JavascriptMinimizationEnabled()
1402  && self::JsMinRewriteSupport())
1403  {
1404  # attempt to create minimized file
1405  $MinFileName = $this->MinimizeJavascriptFile(
1406  $FoundFileName);
1407 
1408  # if minimization succeeded
1409  if ($MinFileName !== NULL)
1410  {
1411  # use minimized version
1412  $FoundFileName = $MinFileName;
1413 
1414  # save file modification time if needed for fingerprinting
1415  if ($this->UrlFingerprintingEnabled())
1416  {
1417  $FileMTime = filemtime($FoundFileName);
1418  }
1419 
1420  # strip off the cache location, allowing .htaccess
1421  # to handle that for us
1422  $FoundFileName = str_replace(
1423  self::$JSMinCacheDir."/", "", $FoundFileName);
1424  }
1425  }
1426  }
1427  }
1428  }
1429  # else if directed to use SCSS files
1430  elseif (($FileType == self::FT_CSS) && $this->ScssSupportEnabled())
1431  {
1432  # look for SCSS version of file
1433  $SourceFileName = preg_replace("/.css$/", ".scss", $FileName);
1434  $FoundSourceFileName = $this->FindFile($DirList, $SourceFileName);
1435 
1436  # if SCSS file not found
1437  if ($FoundSourceFileName === NULL)
1438  {
1439  # look for CSS file
1440  $FoundFileName = $this->FindFile($DirList, $FileName);
1441  }
1442  else
1443  {
1444  # compile SCSS file (if updated) and return resulting CSS file
1445  $FoundFileName = $this->CompileScssFile($FoundSourceFileName);
1446 
1447  # save file modification time if needed for fingerprinting
1448  if ($this->UrlFingerprintingEnabled())
1449  {
1450  $FileMTime = filemtime($FoundFileName);
1451  }
1452 
1453  # strip off the cache location, allowing .htaccess to handle that for us
1454  if (self::ScssRewriteSupport())
1455  {
1456  $FoundFileName = str_replace(
1457  self::$ScssCacheDir."/", "", $FoundFileName);
1458  }
1459  }
1460  }
1461  # otherwise just search for the file
1462  else
1463  {
1464  $FoundFileName = $this->FindFile($DirList, $FileName);
1465  }
1466 
1467  # add non-image files to list of found files (used for required files loading)
1468  if ($FileType != self::FT_IMAGE)
1469  { $this->FoundUIFiles[] = basename($FoundFileName); }
1470 
1471  # if UI file fingerprinting is enabled and supported
1472  if ($this->UrlFingerprintingEnabled()
1473  && self::UrlFingerprintingRewriteSupport()
1474  && (isset($FileMTime) || file_exists($FoundFileName)))
1475  {
1476  # if file does not appear to be a server-side inclusion
1477  if (!preg_match('/\.(html|php)$/i', $FoundFileName))
1478  {
1479  # for each URL fingerprinting blacklist entry
1480  $OnBlacklist = FALSE;
1481  foreach ($this->UrlFingerprintBlacklist as $BlacklistEntry)
1482  {
1483  # if entry looks like a regular expression pattern
1484  if ($BlacklistEntry[0] == substr($BlacklistEntry, -1))
1485  {
1486  # check file name against regular expression
1487  if (preg_match($BlacklistEntry, $FoundFileName))
1488  {
1489  $OnBlacklist = TRUE;
1490  break;
1491  }
1492  }
1493  else
1494  {
1495  # check file name directly against entry
1496  if (basename($FoundFileName) == $BlacklistEntry)
1497  {
1498  $OnBlacklist = TRUE;
1499  break;
1500  }
1501  }
1502  }
1503 
1504  # if file was not on blacklist
1505  if (!$OnBlacklist)
1506  {
1507  # get file modification time if not already retrieved
1508  if (!isset($FileMTime))
1509  {
1510  $FileMTime = filemtime($FoundFileName);
1511  }
1512 
1513  # add timestamp fingerprint to file name
1514  $Fingerprint = sprintf("%06X",
1515  ($FileMTime % 0xFFFFFF));
1516  $FoundFileName = preg_replace("/^(.+)\.([a-z]+)$/",
1517  "$1.".$Fingerprint.".$2",
1518  $FoundFileName);
1519  }
1520  }
1521  }
1522 
1523  # return file name to caller
1524  return $FoundFileName;
1525  }
1526 
1535  public function PUIFile($FileName)
1536  {
1537  $FullFileName = $this->GUIFile($FileName);
1538  if ($FullFileName) { print($FullFileName); }
1539  }
1540 
1555  public function IncludeUIFile($FileNames, $AdditionalAttributes = NULL)
1556  {
1557  # convert file name to array if necessary
1558  if (!is_array($FileNames)) { $FileNames = array($FileNames); }
1559 
1560  # pad additional attributes if supplied
1561  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
1562 
1563  # for each file
1564  foreach ($FileNames as $BaseFileName)
1565  {
1566  # retrieve full file name
1567  $FileName = $this->GUIFile($BaseFileName);
1568 
1569  # if file was found
1570  if ($FileName)
1571  {
1572  # print appropriate tag
1573  print $this->GetUIFileLoadingTag(
1574  $FileName, $AdditionalAttributes);
1575  }
1576 
1577  # if we are not already loading an override file
1578  if (!preg_match("/-Override.(css|scss|js)$/", $BaseFileName))
1579  {
1580  # attempt to load override file if available
1581  $FileType = $this->GetFileType($BaseFileName);
1582  switch ($FileType)
1583  {
1584  case self::FT_CSS:
1585  $OverrideFileName = preg_replace(
1586  "/\.(css|scss)$/", "-Override.$1",
1587  $BaseFileName);
1588  $this->IncludeUIFile($OverrideFileName,
1589  $AdditionalAttributes);
1590  break;
1591 
1592  case self::FT_JAVASCRIPT:
1593  $OverrideFileName = preg_replace(
1594  "/\.js$/", "-Override.js",
1595  $BaseFileName);
1596  $this->IncludeUIFile($OverrideFileName,
1597  $AdditionalAttributes);
1598  break;
1599  }
1600  }
1601  }
1602  }
1603 
1610  public function DoNotUrlFingerprint($Pattern)
1611  {
1612  $this->UrlFingerprintBlacklist[] = $Pattern;
1613  }
1614 
1622  public function RequireUIFile($FileName)
1623  {
1624  $this->AdditionalRequiredUIFiles[] = $FileName;
1625  }
1626 
1632  public static function GetFileType($FileName)
1633  {
1634  static $FileTypeCache;
1635  if (isset($FileTypeCache[$FileName]))
1636  {
1637  return $FileTypeCache[$FileName];
1638  }
1639 
1640  $FileSuffix = strtolower(substr($FileName, -3));
1641  if ($FileSuffix == "css")
1642  {
1643  $FileTypeCache[$FileName] = self::FT_CSS;
1644  }
1645  elseif ($FileSuffix == ".js")
1646  {
1647  $FileTypeCache[$FileName] = self::FT_JAVASCRIPT;
1648  }
1649  elseif (($FileSuffix == "gif")
1650  || ($FileSuffix == "jpg")
1651  || ($FileSuffix == "png"))
1652  {
1653  $FileTypeCache[$FileName] = self::FT_IMAGE;
1654  }
1655  else
1656  {
1657  $FileTypeCache[$FileName] = self::FT_OTHER;
1658  }
1659 
1660  return $FileTypeCache[$FileName];
1661  }
1663  const FT_OTHER = 0;
1665  const FT_CSS = 1;
1667  const FT_IMAGE = 2;
1669  const FT_JAVASCRIPT = 3;
1670 
1679  public function LoadFunction($Callback)
1680  {
1681  # if specified function is not currently available
1682  if (!is_callable($Callback))
1683  {
1684  # if function info looks legal
1685  if (is_string($Callback) && strlen($Callback))
1686  {
1687  # start with function directory list
1688  $Locations = $this->FunctionDirList;
1689 
1690  # add object directories to list
1691  $Locations = array_merge(
1692  $Locations, array_keys(self::$ObjectDirectories));
1693 
1694  # look for function file
1695  $FunctionFileName = $this->FindFile($Locations, "F-".$Callback,
1696  array("php", "html"));
1697 
1698  # if function file was found
1699  if ($FunctionFileName)
1700  {
1701  # load function file
1702  include_once($FunctionFileName);
1703  }
1704  else
1705  {
1706  # log error indicating function load failed
1707  $this->LogError(self::LOGLVL_ERROR, "Unable to load function"
1708  ." for callback \"".$Callback."\".");
1709  }
1710  }
1711  else
1712  {
1713  # log error indicating specified function info was bad
1714  $this->LogError(self::LOGLVL_ERROR, "Unloadable callback value"
1715  ." (".$Callback.")"
1716  ." passed to AF::LoadFunction() by "
1717  .StdLib::GetMyCaller().".");
1718  }
1719  }
1720 
1721  # report to caller whether function load succeeded
1722  return is_callable($Callback);
1723  }
1724 
1729  public function GetElapsedExecutionTime()
1730  {
1731  return microtime(TRUE) - $this->ExecutionStartTime;
1732  }
1733 
1738  public function GetSecondsBeforeTimeout()
1739  {
1740  return $this->MaxExecutionTime() - $this->GetElapsedExecutionTime();
1741  }
1742 
1743  /*@)*/ /* Application Framework */
1744 
1745 
1746  # ---- Page Caching ------------------------------------------------------
1747  /*@(*/
1749 
1756  public function PageCacheEnabled($NewValue = DB_NOVALUE)
1757  {
1758  return $this->UpdateSetting("PageCacheEnabled", $NewValue);
1759  }
1760 
1767  public function PageCacheExpirationPeriod($NewValue = DB_NOVALUE)
1768  {
1769  return $this->UpdateSetting("PageCacheExpirationPeriod", $NewValue);
1770  }
1771 
1776  public function DoNotCacheCurrentPage()
1777  {
1778  $this->CacheCurrentPage = FALSE;
1779  }
1780 
1787  public function AddPageCacheTag($Tag, $Pages = NULL)
1788  {
1789  # normalize tag
1790  $Tag = strtolower($Tag);
1791 
1792  # if pages were supplied
1793  if ($Pages !== NULL)
1794  {
1795  # add pages to list for this tag
1796  if (isset($this->PageCacheTags[$Tag]))
1797  {
1798  $this->PageCacheTags[$Tag] = array_merge(
1799  $this->PageCacheTags[$Tag], $Pages);
1800  }
1801  else
1802  {
1803  $this->PageCacheTags[$Tag] = $Pages;
1804  }
1805  }
1806  else
1807  {
1808  # add current page to list for this tag
1809  $this->PageCacheTags[$Tag][] = "CURRENT";
1810  }
1811  }
1812 
1818  public function ClearPageCacheForTag($Tag)
1819  {
1820  # get tag ID
1821  $TagId = $this->GetPageCacheTagId($Tag);
1822 
1823  # delete pages and tag/page connections for specified tag
1824  $this->DB->Query("DELETE CP, CPTI"
1825  ." FROM AF_CachedPages CP, AF_CachedPageTagInts CPTI"
1826  ." WHERE CPTI.TagId = ".intval($TagId)
1827  ." AND CP.CacheId = CPTI.CacheId");
1828  }
1829 
1833  public function ClearPageCache()
1834  {
1835  # clear all page cache tables
1836  $this->DB->Query("TRUNCATE TABLE AF_CachedPages");
1837  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTags");
1838  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTagInts");
1839  }
1840 
1847  public function GetPageCacheInfo()
1848  {
1849  $Length = $this->DB->Query("SELECT COUNT(*) AS CacheLen"
1850  ." FROM AF_CachedPages", "CacheLen");
1851  $Oldest = $this->DB->Query("SELECT CachedAt FROM AF_CachedPages"
1852  ." ORDER BY CachedAt ASC LIMIT 1", "CachedAt");
1853  return array(
1854  "NumberOfEntries" => $Length,
1855  "OldestTimestamp" => strtotime($Oldest),
1856  );
1857  }
1858 
1859  /*@)*/ /* Page Caching */
1860 
1861 
1862  # ---- Logging -----------------------------------------------------------
1863  /*@(*/
1865 
1879  public function LogSlowPageLoads(
1880  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1881  {
1882  return $this->UpdateSetting(
1883  "LogSlowPageLoads", $NewValue, $Persistent);
1884  }
1885 
1896  public function SlowPageLoadThreshold(
1897  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1898  {
1899  return $this->UpdateSetting(
1900  "SlowPageLoadThreshold", $NewValue, $Persistent);
1901  }
1902 
1916  public function LogHighMemoryUsage(
1917  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1918  {
1919  return $this->UpdateSetting(
1920  "LogHighMemoryUsage", $NewValue, $Persistent);
1921  }
1922 
1934  public function HighMemoryUsageThreshold(
1935  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1936  {
1937  return $this->UpdateSetting(
1938  "HighMemoryUsageThreshold", $NewValue, $Persistent);
1939  }
1940 
1954  public function LogError($Level, $Msg)
1955  {
1956  # if error level is at or below current logging level
1957  if ($this->Settings["LoggingLevel"] >= $Level)
1958  {
1959  # attempt to log error message
1960  $Result = $this->LogMessage($Level, $Msg);
1961 
1962  # if logging attempt failed and level indicated significant error
1963  if (($Result === FALSE) && ($Level <= self::LOGLVL_ERROR))
1964  {
1965  # throw exception about inability to log error
1966  static $AlreadyThrewException = FALSE;
1967  if (!$AlreadyThrewException)
1968  {
1969  $AlreadyThrewException = TRUE;
1970  throw new Exception("Unable to log error (".$Level.": ".$Msg
1971  .") to ".$this->LogFileName);
1972  }
1973  }
1974 
1975  # report to caller whether message was logged
1976  return $Result;
1977  }
1978  else
1979  {
1980  # report to caller that message was not logged
1981  return FALSE;
1982  }
1983  }
1984 
1996  public function LogMessage($Level, $Msg)
1997  {
1998  # if message level is at or below current logging level
1999  if ($this->Settings["LoggingLevel"] >= $Level)
2000  {
2001  # attempt to open log file
2002  $FHndl = @fopen($this->LogFileName, "a");
2003 
2004  # if log file could not be open
2005  if ($FHndl === FALSE)
2006  {
2007  # report to caller that message was not logged
2008  return FALSE;
2009  }
2010  else
2011  {
2012  # format log entry
2013  $ErrorAbbrevs = array(
2014  self::LOGLVL_FATAL => "FTL",
2015  self::LOGLVL_ERROR => "ERR",
2016  self::LOGLVL_WARNING => "WRN",
2017  self::LOGLVL_INFO => "INF",
2018  self::LOGLVL_DEBUG => "DBG",
2019  self::LOGLVL_TRACE => "TRC",
2020  );
2021  $Msg = str_replace(array("\n", "\t", "\r"), " ", $Msg);
2022  $Msg = substr(trim($Msg), 0, self::LOGFILE_MAX_LINE_LENGTH);
2023  $LogEntry = date("Y-m-d H:i:s")
2024  ." ".($this->RunningInBackground ? "B" : "F")
2025  ." ".$ErrorAbbrevs[$Level]
2026  ." ".$Msg;
2027 
2028  # write entry to log
2029  $Success = fwrite($FHndl, $LogEntry."\n");
2030 
2031  # close log file
2032  fclose($FHndl);
2033 
2034  # report to caller whether message was logged
2035  return ($Success === FALSE) ? FALSE : TRUE;
2036  }
2037  }
2038  else
2039  {
2040  # report to caller that message was not logged
2041  return FALSE;
2042  }
2043  }
2044 
2066  public function LoggingLevel($NewValue = DB_NOVALUE)
2067  {
2068  # constrain new level (if supplied) to within legal bounds
2069  if ($NewValue !== DB_NOVALUE)
2070  {
2071  $NewValue = max(min($NewValue, 6), 1);
2072  }
2073 
2074  # set new logging level (if supplied) and return current level to caller
2075  return $this->UpdateSetting("LoggingLevel", $NewValue);
2076  }
2077 
2084  public function LogFile($NewValue = NULL)
2085  {
2086  if ($NewValue !== NULL) { $this->LogFileName = $NewValue; }
2087  return $this->LogFileName;
2088  }
2089 
2099  public function GetLogEntries($Limit = 0)
2100  {
2101  # return no entries if there isn't a log file or we can't read it
2102  if (!is_readable($this->LogFile()))
2103  {
2104  return array();
2105  }
2106 
2107  # if max number of entries specified
2108  if ($Limit > 0)
2109  {
2110  # load lines from file
2111  $FHandle = fopen($this->LogFile(), "r");
2112  $FileSize = filesize($this->LogFile());
2113  $SeekPosition = max(0,
2114  ($FileSize - (self::LOGFILE_MAX_LINE_LENGTH * ($Limit + 1))));
2115  fseek($FHandle, $SeekPosition);
2116  $Block = fread($FHandle, ($FileSize - $SeekPosition));
2117  fclose($FHandle);
2118  $Lines = explode(PHP_EOL, $Block);
2119  array_shift($Lines);
2120 
2121  # prune array back to requested number of entries
2122  $Lines = array_slice($Lines, (0 - $Limit));
2123  }
2124  else
2125  {
2126  # load all lines from log file
2127  $Lines = file($this->LogFile(), FILE_IGNORE_NEW_LINES);
2128  if ($Lines === FALSE)
2129  {
2130  return array();
2131  }
2132  }
2133 
2134  # reverse line order
2135  $Lines = array_reverse($Lines);
2136 
2137  # for each log file line
2138  $Entries = array();
2139  foreach ($Lines as $Line)
2140  {
2141  # attempt to parse line into component parts
2142  $Pieces = explode(" ", $Line, 5);
2143  $Date = isset($Pieces[0]) ? $Pieces[0] : "";
2144  $Time = isset($Pieces[1]) ? $Pieces[1] : "";
2145  $Back = isset($Pieces[2]) ? $Pieces[2] : "";
2146  $Level = isset($Pieces[3]) ? $Pieces[3] : "";
2147  $Msg = isset($Pieces[4]) ? $Pieces[4] : "";
2148 
2149  # skip line if it looks invalid
2150  $ErrorAbbrevs = array(
2151  "FTL" => self::LOGLVL_FATAL,
2152  "ERR" => self::LOGLVL_ERROR,
2153  "WRN" => self::LOGLVL_WARNING,
2154  "INF" => self::LOGLVL_INFO,
2155  "DBG" => self::LOGLVL_DEBUG,
2156  "TRC" => self::LOGLVL_TRACE,
2157  );
2158  if ((($Back != "F") && ($Back != "B"))
2159  || !array_key_exists($Level, $ErrorAbbrevs)
2160  || !strlen($Msg))
2161  {
2162  continue;
2163  }
2164 
2165  # convert parts into appropriate values and add to entries
2166  $Entries[] = array(
2167  "Time" => strtotime($Date." ".$Time),
2168  "Background" => ($Back == "B") ? TRUE : FALSE,
2169  "Level" => $ErrorAbbrevs[$Level],
2170  "Message" => $Msg,
2171  );
2172  }
2173 
2174  # return entries to caller
2175  return $Entries;
2176  }
2177 
2182  const LOGLVL_TRACE = 6;
2187  const LOGLVL_DEBUG = 5;
2193  const LOGLVL_INFO = 4;
2198  const LOGLVL_WARNING = 3;
2204  const LOGLVL_ERROR = 2;
2209  const LOGLVL_FATAL = 1;
2210 
2215 
2216  /*@)*/ /* Logging */
2217 
2218 
2219  # ---- Event Handling ----------------------------------------------------
2220  /*@(*/
2222 
2232  const EVENTTYPE_CHAIN = 2;
2238  const EVENTTYPE_FIRST = 3;
2246  const EVENTTYPE_NAMED = 4;
2247 
2249  const ORDER_FIRST = 1;
2251  const ORDER_MIDDLE = 2;
2253  const ORDER_LAST = 3;
2254 
2263  public function RegisterEvent($EventsOrEventName, $EventType = NULL)
2264  {
2265  # convert parameters to array if not already in that form
2266  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2267  : array($EventsOrEventName => $EventType);
2268 
2269  # for each event
2270  foreach ($Events as $Name => $Type)
2271  {
2272  # store event information
2273  $this->RegisteredEvents[$Name]["Type"] = $Type;
2274  $this->RegisteredEvents[$Name]["Hooks"] = array();
2275  }
2276  }
2277 
2284  public function IsRegisteredEvent($EventName)
2285  {
2286  return array_key_exists($EventName, $this->RegisteredEvents)
2287  ? TRUE : FALSE;
2288  }
2289 
2296  public function IsHookedEvent($EventName)
2297  {
2298  # the event isn't hooked to if it isn't even registered
2299  if (!$this->IsRegisteredEvent($EventName))
2300  {
2301  return FALSE;
2302  }
2303 
2304  # return TRUE if there is at least one callback hooked to the event
2305  return count($this->RegisteredEvents[$EventName]["Hooks"]) > 0;
2306  }
2307 
2321  public function HookEvent(
2322  $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2323  {
2324  # convert parameters to array if not already in that form
2325  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2326  : array($EventsOrEventName => $Callback);
2327 
2328  # for each event
2329  $Success = TRUE;
2330  foreach ($Events as $EventName => $EventCallback)
2331  {
2332  # if callback is valid
2333  if (is_callable($EventCallback))
2334  {
2335  # if this is a periodic event we process internally
2336  if (isset($this->PeriodicEvents[$EventName]))
2337  {
2338  # process event now
2339  $this->ProcessPeriodicEvent($EventName, $EventCallback);
2340  }
2341  # if specified event has been registered
2342  elseif (isset($this->RegisteredEvents[$EventName]))
2343  {
2344  # add callback for event
2345  $this->RegisteredEvents[$EventName]["Hooks"][]
2346  = array("Callback" => $EventCallback, "Order" => $Order);
2347 
2348  # sort callbacks by order
2349  if (count($this->RegisteredEvents[$EventName]["Hooks"]) > 1)
2350  {
2351  usort($this->RegisteredEvents[$EventName]["Hooks"],
2352  function ($A, $B) {
2353  return StdLib::SortCompare(
2354  $A["Order"], $B["Order"]);
2355  });
2356  }
2357  }
2358  else
2359  {
2360  $Success = FALSE;
2361  }
2362  }
2363  else
2364  {
2365  $Success = FALSE;
2366  }
2367  }
2368 
2369  # report to caller whether all callbacks were hooked
2370  return $Success;
2371  }
2372 
2386  public function UnhookEvent(
2387  $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2388  {
2389  # convert parameters to array if not already in that form
2390  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2391  : array($EventsOrEventName => $Callback);
2392 
2393  # for each event
2394  $UnhookCount = 0;
2395  foreach ($Events as $EventName => $EventCallback)
2396  {
2397  # if this event has been registered and hooked
2398  if (isset($this->RegisteredEvents[$EventName])
2399  && count($this->RegisteredEvents[$EventName]))
2400  {
2401  # if this callback has been hooked for this event
2402  $CallbackData = array("Callback" => $EventCallback, "Order" => $Order);
2403  if (in_array($CallbackData,
2404  $this->RegisteredEvents[$EventName]["Hooks"]))
2405  {
2406  # unhook callback
2407  $HookIndex = array_search($CallbackData,
2408  $this->RegisteredEvents[$EventName]["Hooks"]);
2409  unset($this->RegisteredEvents[$EventName]["Hooks"][$HookIndex]);
2410  $UnhookCount++;
2411  }
2412  }
2413  }
2414 
2415  # report number of callbacks unhooked to caller
2416  return $UnhookCount;
2417  }
2418 
2429  public function SignalEvent($EventName, $Parameters = NULL)
2430  {
2431  $ReturnValue = NULL;
2432 
2433  # if event has been registered
2434  if (isset($this->RegisteredEvents[$EventName]))
2435  {
2436  # set up default return value (if not NULL)
2437  switch ($this->RegisteredEvents[$EventName]["Type"])
2438  {
2439  case self::EVENTTYPE_CHAIN:
2440  $ReturnValue = $Parameters;
2441  break;
2442 
2443  case self::EVENTTYPE_NAMED:
2444  $ReturnValue = array();
2445  break;
2446  }
2447 
2448  # for each callback for this event
2449  foreach ($this->RegisteredEvents[$EventName]["Hooks"] as $Hook)
2450  {
2451  # invoke callback
2452  $Callback = $Hook["Callback"];
2453  $Result = ($Parameters !== NULL)
2454  ? call_user_func_array($Callback, $Parameters)
2455  : call_user_func($Callback);
2456 
2457  # process return value based on event type
2458  switch ($this->RegisteredEvents[$EventName]["Type"])
2459  {
2460  case self::EVENTTYPE_CHAIN:
2461  if ($Result !== NULL)
2462  {
2463  foreach ($Parameters as $Index => $Value)
2464  {
2465  if (array_key_exists($Index, $Result))
2466  {
2467  $Parameters[$Index] = $Result[$Index];
2468  }
2469  }
2470  $ReturnValue = $Parameters;
2471  }
2472  break;
2473 
2474  case self::EVENTTYPE_FIRST:
2475  if ($Result !== NULL)
2476  {
2477  $ReturnValue = $Result;
2478  break 2;
2479  }
2480  break;
2481 
2482  case self::EVENTTYPE_NAMED:
2483  $CallbackName = is_array($Callback)
2484  ? (is_object($Callback[0])
2485  ? get_class($Callback[0])
2486  : $Callback[0])."::".$Callback[1]
2487  : $Callback;
2488  $ReturnValue[$CallbackName] = $Result;
2489  break;
2490 
2491  default:
2492  break;
2493  }
2494  }
2495  }
2496  else
2497  {
2498  $this->LogError(self::LOGLVL_WARNING,
2499  "Unregistered event (".$EventName.") signaled by "
2500  .StdLib::GetMyCaller().".");
2501  }
2502 
2503  # return value if any to caller
2504  return $ReturnValue;
2505  }
2506 
2512  public function IsStaticOnlyEvent($EventName)
2513  {
2514  return isset($this->PeriodicEvents[$EventName]) ? TRUE : FALSE;
2515  }
2516 
2527  public function EventWillNextRunAt($EventName, $Callback)
2528  {
2529  # if event is not a periodic event report failure to caller
2530  if (!array_key_exists($EventName, $this->EventPeriods)) { return FALSE; }
2531 
2532  # retrieve last execution time for event if available
2533  $Signature = self::GetCallbackSignature($Callback);
2534  $LastRunTime = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
2535  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
2536 
2537  # if event was not found report failure to caller
2538  if ($LastRunTime === NULL) { return FALSE; }
2539 
2540  # calculate next run time based on event period
2541  $NextRunTime = strtotime($LastRunTime) + $this->EventPeriods[$EventName];
2542 
2543  # report next run time to caller
2544  return $NextRunTime;
2545  }
2546 
2562  public function GetKnownPeriodicEvents()
2563  {
2564  # retrieve last execution times
2565  $this->DB->Query("SELECT * FROM PeriodicEvents");
2566  $LastRunTimes = $this->DB->FetchColumn("LastRunAt", "Signature");
2567 
2568  # for each known event
2569  $Events = array();
2570  foreach ($this->KnownPeriodicEvents as $Signature => $Info)
2571  {
2572  # if last run time for event is available
2573  if (array_key_exists($Signature, $LastRunTimes))
2574  {
2575  # calculate next run time for event
2576  $LastRun = strtotime($LastRunTimes[$Signature]);
2577  $NextRun = $LastRun + $this->EventPeriods[$Info["Period"]];
2578  if ($Info["Period"] == "EVENT_PERIODIC") { $LastRun = FALSE; }
2579  }
2580  else
2581  {
2582  # set info to indicate run times are not known
2583  $LastRun = FALSE;
2584  $NextRun = FALSE;
2585  }
2586 
2587  # add event info to list
2588  $Events[$Signature] = $Info;
2589  $Events[$Signature]["LastRun"] = $LastRun;
2590  $Events[$Signature]["NextRun"] = $NextRun;
2591  $Events[$Signature]["Parameters"] = NULL;
2592  }
2593 
2594  # return list of known events to caller
2595  return $Events;
2596  }
2597 
2598  /*@)*/ /* Event Handling */
2599 
2600 
2601  # ---- Task Management ---------------------------------------------------
2602  /*@(*/
2604 
2606  const PRIORITY_HIGH = 1;
2608  const PRIORITY_MEDIUM = 2;
2610  const PRIORITY_LOW = 3;
2613 
2626  public function QueueTask($Callback, $Parameters = NULL,
2627  $Priority = self::PRIORITY_LOW, $Description = "")
2628  {
2629  # pack task info and write to database
2630  if ($Parameters === NULL) { $Parameters = array(); }
2631  $this->DB->Query("INSERT INTO TaskQueue"
2632  ." (Callback, Parameters, Priority, Description)"
2633  ." VALUES ('".addslashes(serialize($Callback))."', '"
2634  .addslashes(serialize($Parameters))."', ".intval($Priority).", '"
2635  .addslashes($Description)."')");
2636  }
2637 
2655  public function QueueUniqueTask($Callback, $Parameters = NULL,
2656  $Priority = self::PRIORITY_LOW, $Description = "")
2657  {
2658  if ($this->TaskIsInQueue($Callback, $Parameters))
2659  {
2660  $QueryResult = $this->DB->Query("SELECT TaskId,Priority FROM TaskQueue"
2661  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2662  .($Parameters ? " AND Parameters = '"
2663  .addslashes(serialize($Parameters))."'" : ""));
2664  if ($QueryResult !== FALSE)
2665  {
2666  $Record = $this->DB->FetchRow();
2667  if ($Record["Priority"] > $Priority)
2668  {
2669  $this->DB->Query("UPDATE TaskQueue"
2670  ." SET Priority = ".intval($Priority)
2671  ." WHERE TaskId = ".intval($Record["TaskId"]));
2672  }
2673  }
2674  return FALSE;
2675  }
2676  else
2677  {
2678  $this->QueueTask($Callback, $Parameters, $Priority, $Description);
2679  return TRUE;
2680  }
2681  }
2682 
2692  public function TaskIsInQueue($Callback, $Parameters = NULL)
2693  {
2694  $QueuedCount = $this->DB->Query(
2695  "SELECT COUNT(*) AS FoundCount FROM TaskQueue"
2696  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2697  .($Parameters ? " AND Parameters = '"
2698  .addslashes(serialize($Parameters))."'" : ""),
2699  "FoundCount");
2700  $RunningCount = $this->DB->Query(
2701  "SELECT COUNT(*) AS FoundCount FROM RunningTasks"
2702  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2703  .($Parameters ? " AND Parameters = '"
2704  .addslashes(serialize($Parameters))."'" : ""),
2705  "FoundCount");
2706  $FoundCount = $QueuedCount + $RunningCount;
2707  return ($FoundCount ? TRUE : FALSE);
2708  }
2709 
2715  public function GetTaskQueueSize($Priority = NULL)
2716  {
2717  return $this->GetQueuedTaskCount(NULL, NULL, $Priority);
2718  }
2719 
2727  public function GetQueuedTaskList($Count = 100, $Offset = 0)
2728  {
2729  return $this->GetTaskList("SELECT * FROM TaskQueue"
2730  ." ORDER BY Priority, TaskId ", $Count, $Offset);
2731  }
2732 
2746  public function GetQueuedTaskCount($Callback = NULL,
2747  $Parameters = NULL, $Priority = NULL, $Description = NULL)
2748  {
2749  $Query = "SELECT COUNT(*) AS TaskCount FROM TaskQueue";
2750  $Sep = " WHERE";
2751  if ($Callback !== NULL)
2752  {
2753  $Query .= $Sep." Callback = '".addslashes(serialize($Callback))."'";
2754  $Sep = " AND";
2755  }
2756  if ($Parameters !== NULL)
2757  {
2758  $Query .= $Sep." Parameters = '".addslashes(serialize($Parameters))."'";
2759  $Sep = " AND";
2760  }
2761  if ($Priority !== NULL)
2762  {
2763  $Query .= $Sep." Priority = ".intval($Priority);
2764  $Sep = " AND";
2765  }
2766  if ($Description !== NULL)
2767  {
2768  $Query .= $Sep." Description = '".addslashes($Description)."'";
2769  }
2770  return $this->DB->Query($Query, "TaskCount");
2771  }
2772 
2780  public function GetRunningTaskList($Count = 100, $Offset = 0)
2781  {
2782  return $this->GetTaskList("SELECT * FROM RunningTasks"
2783  ." WHERE StartedAt >= '".date("Y-m-d H:i:s",
2784  (time() - $this->MaxExecutionTime()))."'"
2785  ." ORDER BY StartedAt", $Count, $Offset);
2786  }
2787 
2795  public function GetOrphanedTaskList($Count = 100, $Offset = 0)
2796  {
2797  return $this->GetTaskList("SELECT * FROM RunningTasks"
2798  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
2799  (time() - $this->MaxExecutionTime()))."'"
2800  ." ORDER BY StartedAt", $Count, $Offset);
2801  }
2802 
2807  public function GetOrphanedTaskCount()
2808  {
2809  return $this->DB->Query("SELECT COUNT(*) AS Count FROM RunningTasks"
2810  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
2811  (time() - $this->MaxExecutionTime()))."'",
2812  "Count");
2813  }
2814 
2820  public function ReQueueOrphanedTask($TaskId, $NewPriority = NULL)
2821  {
2822  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
2823  $this->DB->Query("INSERT INTO TaskQueue"
2824  ." (Callback,Parameters,Priority,Description) "
2825  ."SELECT Callback, Parameters, Priority, Description"
2826  ." FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2827  if ($NewPriority !== NULL)
2828  {
2829  $NewTaskId = $this->DB->LastInsertId();
2830  $this->DB->Query("UPDATE TaskQueue SET Priority = "
2831  .intval($NewPriority)
2832  ." WHERE TaskId = ".intval($NewTaskId));
2833  }
2834  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2835  $this->DB->Query("UNLOCK TABLES");
2836  }
2837 
2842  public function DeleteTask($TaskId)
2843  {
2844  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = ".intval($TaskId));
2845  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2846  }
2847 
2855  public function GetTask($TaskId)
2856  {
2857  # assume task will not be found
2858  $Task = NULL;
2859 
2860  # look for task in task queue
2861  $this->DB->Query("SELECT * FROM TaskQueue WHERE TaskId = ".intval($TaskId));
2862 
2863  # if task was not found in queue
2864  if (!$this->DB->NumRowsSelected())
2865  {
2866  # look for task in running task list
2867  $this->DB->Query("SELECT * FROM RunningTasks WHERE TaskId = "
2868  .intval($TaskId));
2869  }
2870 
2871  # if task was found
2872  if ($this->DB->NumRowsSelected())
2873  {
2874  # if task was periodic
2875  $Row = $this->DB->FetchRow();
2876  if ($Row["Callback"] ==
2877  serialize(array("ApplicationFramework", "PeriodicEventWrapper")))
2878  {
2879  # unpack periodic task callback
2880  $WrappedCallback = unserialize($Row["Parameters"]);
2881  $Task["Callback"] = $WrappedCallback[1];
2882  $Task["Parameters"] = $WrappedCallback[2];
2883  }
2884  else
2885  {
2886  # unpack task callback and parameters
2887  $Task["Callback"] = unserialize($Row["Callback"]);
2888  $Task["Parameters"] = unserialize($Row["Parameters"]);
2889  }
2890  }
2891 
2892  # return task to caller
2893  return $Task;
2894  }
2895 
2903  public function TaskExecutionEnabled($NewValue = DB_NOVALUE)
2904  {
2905  return $this->UpdateSetting("TaskExecutionEnabled", $NewValue);
2906  }
2907 
2913  public function MaxTasks($NewValue = DB_NOVALUE)
2914  {
2915  return $this->UpdateSetting("MaxTasksRunning", $NewValue);
2916  }
2917 
2925  public static function GetTaskCallbackSynopsis($TaskInfo)
2926  {
2927  # if task callback is function use function name
2928  $Callback = $TaskInfo["Callback"];
2929  $Name = "";
2930  if (!is_array($Callback))
2931  {
2932  $Name = $Callback;
2933  }
2934  else
2935  {
2936  # if task callback is object
2937  if (is_object($Callback[0]))
2938  {
2939  # if task callback is encapsulated ask encapsulation for name
2940  if (method_exists($Callback[0], "GetCallbackAsText"))
2941  {
2942  $Name = $Callback[0]->GetCallbackAsText();
2943  }
2944  # else assemble name from object
2945  else
2946  {
2947  $Name = get_class($Callback[0]) . "::" . $Callback[1];
2948  }
2949  }
2950  # else assemble name from supplied info
2951  else
2952  {
2953  $Name= $Callback[0] . "::" . $Callback[1];
2954  }
2955  }
2956 
2957  # if parameter array was supplied
2958  $Parameters = $TaskInfo["Parameters"];
2959  $ParameterString = "";
2960  if (is_array($Parameters))
2961  {
2962  # assemble parameter string
2963  $Separator = "";
2964  foreach ($Parameters as $Parameter)
2965  {
2966  $ParameterString .= $Separator;
2967  if (is_int($Parameter) || is_float($Parameter))
2968  {
2969  $ParameterString .= $Parameter;
2970  }
2971  else if (is_string($Parameter))
2972  {
2973  $ParameterString .= "\"".htmlspecialchars($Parameter)."\"";
2974  }
2975  else if (is_array($Parameter))
2976  {
2977  $ParameterString .= "ARRAY";
2978  }
2979  else if (is_object($Parameter))
2980  {
2981  $ParameterString .= "OBJECT";
2982  }
2983  else if (is_null($Parameter))
2984  {
2985  $ParameterString .= "NULL";
2986  }
2987  else if (is_bool($Parameter))
2988  {
2989  $ParameterString .= $Parameter ? "TRUE" : "FALSE";
2990  }
2991  else if (is_resource($Parameter))
2992  {
2993  $ParameterString .= get_resource_type($Parameter);
2994  }
2995  else
2996  {
2997  $ParameterString .= "????";
2998  }
2999  $Separator = ", ";
3000  }
3001  }
3002 
3003  # assemble name and parameters and return result to caller
3004  return $Name."(".$ParameterString.")";
3005  }
3006 
3011  public function IsRunningInBackground()
3012  {
3013  return $this->RunningInBackground;
3014  }
3015 
3022  {
3023  return isset($this->RunningTask)
3024  ? $this->RunningTask["Priority"] : NULL;
3025  }
3026 
3035  public function GetNextHigherBackgroundPriority($Priority = NULL)
3036  {
3037  if ($Priority === NULL)
3038  {
3039  $Priority = $this->GetCurrentBackgroundPriority();
3040  if ($Priority === NULL)
3041  {
3042  return NULL;
3043  }
3044  }
3045  return ($Priority > self::PRIORITY_HIGH)
3046  ? ($Priority - 1) : self::PRIORITY_HIGH;
3047  }
3048 
3057  public function GetNextLowerBackgroundPriority($Priority = NULL)
3058  {
3059  if ($Priority === NULL)
3060  {
3061  $Priority = $this->GetCurrentBackgroundPriority();
3062  if ($Priority === NULL)
3063  {
3064  return NULL;
3065  }
3066  }
3067  return ($Priority < self::PRIORITY_BACKGROUND)
3068  ? ($Priority + 1) : self::PRIORITY_BACKGROUND;
3069  }
3070 
3071  /*@)*/ /* Task Management */
3072 
3073 
3074  # ---- Clean URL Support -------------------------------------------------
3075  /*@(*/
3077 
3104  public function AddCleanUrl($Pattern, $Page, $GetVars = NULL, $Template = NULL)
3105  {
3106  # save clean URL mapping parameters
3107  $this->CleanUrlMappings[] = array(
3108  "Pattern" => $Pattern,
3109  "Page" => $Page,
3110  "GetVars" => $GetVars,
3111  );
3112 
3113  # if replacement template specified
3114  if ($Template !== NULL)
3115  {
3116  # if GET parameters specified
3117  if (count($GetVars))
3118  {
3119  # retrieve all possible permutations of GET parameters
3120  $GetPerms = $this->ArrayPermutations(array_keys($GetVars));
3121 
3122  # for each permutation of GET parameters
3123  foreach ($GetPerms as $VarPermutation)
3124  {
3125  # construct search pattern for permutation
3126  $SearchPattern = "/href=([\"'])index\\.php\\?P=".$Page;
3127  $GetVarSegment = "";
3128  foreach ($VarPermutation as $GetVar)
3129  {
3130  if (preg_match("%\\\$[0-9]+%", $GetVars[$GetVar]))
3131  {
3132  $GetVarSegment .= "&amp;".$GetVar."=((?:(?!\\1)[^&])+)";
3133  }
3134  else
3135  {
3136  $GetVarSegment .= "&amp;".$GetVar."=".$GetVars[$GetVar];
3137  }
3138  }
3139  $SearchPattern .= $GetVarSegment."\\1/i";
3140 
3141  # if template is actually a callback
3142  if (is_callable($Template))
3143  {
3144  # add pattern to HTML output mod callbacks list
3145  $this->OutputModificationCallbacks[] = array(
3146  "Pattern" => $Pattern,
3147  "Page" => $Page,
3148  "SearchPattern" => $SearchPattern,
3149  "Callback" => $Template,
3150  );
3151  }
3152  else
3153  {
3154  # construct replacement string for permutation
3155  $Replacement = $Template;
3156  $Index = 2;
3157  foreach ($VarPermutation as $GetVar)
3158  {
3159  $Replacement = str_replace(
3160  "\$".$GetVar, "\$".$Index, $Replacement);
3161  $Index++;
3162  }
3163  $Replacement = "href=\"".$Replacement."\"";
3164 
3165  # add pattern to HTML output modifications list
3166  $this->OutputModificationPatterns[] = $SearchPattern;
3167  $this->OutputModificationReplacements[] = $Replacement;
3168  }
3169  }
3170  }
3171  else
3172  {
3173  # construct search pattern
3174  $SearchPattern = "/href=\"index\\.php\\?P=".$Page."\"/i";
3175 
3176  # if template is actually a callback
3177  if (is_callable($Template))
3178  {
3179  # add pattern to HTML output mod callbacks list
3180  $this->OutputModificationCallbacks[] = array(
3181  "Pattern" => $Pattern,
3182  "Page" => $Page,
3183  "SearchPattern" => $SearchPattern,
3184  "Callback" => $Template,
3185  );
3186  }
3187  else
3188  {
3189  # add simple pattern to HTML output modifications list
3190  $this->OutputModificationPatterns[] = $SearchPattern;
3191  $this->OutputModificationReplacements[] = "href=\"".$Template."\"";
3192  }
3193  }
3194  }
3195  }
3196 
3202  public function CleanUrlIsMapped($Path)
3203  {
3204  foreach ($this->CleanUrlMappings as $Info)
3205  {
3206  if (preg_match($Info["Pattern"], $Path))
3207  {
3208  return TRUE;
3209  }
3210  }
3211  return FALSE;
3212  }
3213 
3223  public function GetCleanUrlForPath($Path)
3224  {
3225  # the search patterns and callbacks require a specific format
3226  $Format = "href=\"".str_replace("&", "&amp;", $Path)."\"";
3227  $Search = $Format;
3228 
3229  # perform any regular expression replacements on the search string
3230  $Search = preg_replace($this->OutputModificationPatterns,
3231  $this->OutputModificationReplacements, $Search);
3232 
3233  # only run the callbacks if a replacement hasn't already been performed
3234  if ($Search == $Format)
3235  {
3236  # perform any callback replacements on the search string
3237  foreach ($this->OutputModificationCallbacks as $Info)
3238  {
3239  # make the information available to the callback
3240  $this->OutputModificationCallbackInfo = $Info;
3241 
3242  # execute the callback
3243  $Search = preg_replace_callback($Info["SearchPattern"],
3244  array($this, "OutputModificationCallbackShell"),
3245  $Search);
3246  }
3247  }
3248 
3249  # return the path untouched if no replacements were performed
3250  if ($Search == $Format)
3251  {
3252  return $Path;
3253  }
3254 
3255  # remove the bits added to the search string to get it recognized by
3256  # the replacement expressions and callbacks
3257  $Result = substr($Search, 6, -1);
3258 
3259  return $Result;
3260  }
3261 
3268  public function GetUncleanUrlForPath($Path)
3269  {
3270  # for each clean URL mapping
3271  foreach ($this->CleanUrlMappings as $Info)
3272  {
3273  # if current path matches the clean URL pattern
3274  if (preg_match($Info["Pattern"], $Path, $Matches))
3275  {
3276  # the GET parameters for the URL, starting with the page name
3277  $GetVars = array("P" => $Info["Page"]);
3278 
3279  # if additional $_GET variables specified for clean URL
3280  if ($Info["GetVars"] !== NULL)
3281  {
3282  # for each $_GET variable specified for clean URL
3283  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
3284  {
3285  # start with template for variable value
3286  $Value = $VarTemplate;
3287 
3288  # for each subpattern matched in current URL
3289  foreach ($Matches as $Index => $Match)
3290  {
3291  # if not first (whole) match
3292  if ($Index > 0)
3293  {
3294  # make any substitutions in template
3295  $Value = str_replace("$".$Index, $Match, $Value);
3296  }
3297  }
3298 
3299  # add the GET variable
3300  $GetVars[$VarName] = $Value;
3301  }
3302  }
3303 
3304  # return the unclean URL
3305  return "index.php?" . http_build_query($GetVars);
3306  }
3307  }
3308 
3309  # return the path unchanged
3310  return $Path;
3311  }
3312 
3318  public function GetCleanUrl()
3319  {
3320  return $this->GetCleanUrlForPath($this->GetUncleanUrl());
3321  }
3322 
3327  public function GetUncleanUrl()
3328  {
3329  $GetVars = array("P" => $this->GetPageName()) + $_GET;
3330  return "index.php?" . http_build_query($GetVars);
3331  }
3332 
3344  public function AddPrefixForAlternateDomain($Domain, $Prefix)
3345  {
3346  $this->AlternateDomainPrefixes[$Domain] = $Prefix;
3347  }
3348 
3349  /*@)*/ /* Clean URL Support */
3350 
3351  # ---- Server Environment ------------------------------------------------
3352  /*@(*/
3354 
3360  public static function SessionLifetime($NewValue = NULL)
3361  {
3362  if ($NewValue !== NULL)
3363  {
3364  self::$SessionLifetime = $NewValue;
3365  }
3366  return self::$SessionLifetime;
3367  }
3368 
3374  public static function HtaccessSupport()
3375  {
3376  return isset($_SERVER["HTACCESS_SUPPORT"])
3377  || isset($_SERVER["REDIRECT_HTACCESS_SUPPORT"]);
3378  }
3379 
3386  public static function UrlFingerprintingRewriteSupport()
3387  {
3388  return isset($_SERVER["URL_FINGERPRINTING_SUPPORT"])
3389  || isset($_SERVER["REDIRECT_URL_FINGERPRINTING_SUPPORT"]);
3390  }
3391 
3398  public static function ScssRewriteSupport()
3399  {
3400  return isset($_SERVER["SCSS_REWRITE_SUPPORT"])
3401  || isset($_SERVER["REDIRECT_SCSS_REWRITE_SUPPORT"]);
3402  }
3403 
3410  public static function JsMinRewriteSupport()
3411  {
3412  return isset($_SERVER["JSMIN_REWRITE_SUPPORT"])
3413  || isset($_SERVER["REDIRECT_JSMIN_REWRITE_SUPPORT"]);
3414  }
3415 
3423  public static function RootUrl()
3424  {
3425  # return override value if one is set
3426  if (self::$RootUrlOverride !== NULL)
3427  {
3428  return self::$RootUrlOverride;
3429  }
3430 
3431  # determine scheme name
3432  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
3433 
3434  # if HTTP_HOST is preferred or SERVER_NAME points to localhost
3435  # and HTTP_HOST is set
3436  if ((self::$PreferHttpHost || ($_SERVER["SERVER_NAME"] == "127.0.0.1"))
3437  && isset($_SERVER["HTTP_HOST"]))
3438  {
3439  # use HTTP_HOST for domain name
3440  $DomainName = $_SERVER["HTTP_HOST"];
3441  }
3442  else
3443  {
3444  # use SERVER_NAME for domain name
3445  $DomainName = $_SERVER["HTTP_HOST"];
3446  }
3447 
3448  # build URL root and return to caller
3449  return $Protocol."://".$DomainName;
3450  }
3451 
3466  public static function RootUrlOverride($NewValue = self::NOVALUE)
3467  {
3468  if ($NewValue !== self::NOVALUE)
3469  {
3470  self::$RootUrlOverride = strlen(trim($NewValue)) ? $NewValue : NULL;
3471  }
3472  return self::$RootUrlOverride;
3473  }
3474 
3484  public static function BaseUrl()
3485  {
3486  $BaseUrl = self::RootUrl().dirname($_SERVER["SCRIPT_NAME"]);
3487  if (substr($BaseUrl, -1) != "/") { $BaseUrl .= "/"; }
3488  return $BaseUrl;
3489  }
3490 
3498  public static function FullUrl()
3499  {
3500  return self::RootUrl().$_SERVER["REQUEST_URI"];
3501  }
3502 
3513  public static function PreferHttpHost($NewValue = NULL)
3514  {
3515  if ($NewValue !== NULL)
3516  {
3517  self::$PreferHttpHost = ($NewValue ? TRUE : FALSE);
3518  }
3519  return self::$PreferHttpHost;
3520  }
3521 
3526  public static function BasePath()
3527  {
3528  $BasePath = dirname($_SERVER["SCRIPT_NAME"]);
3529 
3530  if (substr($BasePath, -1) != "/")
3531  {
3532  $BasePath .= "/";
3533  }
3534 
3535  return $BasePath;
3536  }
3537 
3543  public static function GetScriptUrl()
3544  {
3545  if (array_key_exists("SCRIPT_URL", $_SERVER))
3546  {
3547  return $_SERVER["SCRIPT_URL"];
3548  }
3549  elseif (array_key_exists("REQUEST_URI", $_SERVER))
3550  {
3551  $Pieces = parse_url($_SERVER["REQUEST_URI"]);
3552  return isset($Pieces["path"]) ? $Pieces["path"] : NULL;
3553  }
3554  elseif (array_key_exists("REDIRECT_URL", $_SERVER))
3555  {
3556  return $_SERVER["REDIRECT_URL"];
3557  }
3558  else
3559  {
3560  return NULL;
3561  }
3562  }
3563 
3572  public static function WasUrlRewritten($ScriptName="index.php")
3573  {
3574  # needed to get the path of the URL minus the query and fragment pieces
3575  $Components = parse_url(self::GetScriptUrl());
3576 
3577  # if parsing was successful and a path is set
3578  if (is_array($Components) && isset($Components["path"]))
3579  {
3580  $BasePath = self::BasePath();
3581  $Path = $Components["path"];
3582 
3583  # the URL was rewritten if the path isn't the base path, i.e., the
3584  # home page, and the file in the URL isn't the script generating the
3585  # page
3586  if ($BasePath != $Path && basename($Path) != $ScriptName)
3587  {
3588  return TRUE;
3589  }
3590  }
3591 
3592  # the URL wasn't rewritten
3593  return FALSE;
3594  }
3595 
3601  public static function GetFreeMemory()
3602  {
3603  return self::GetPhpMemoryLimit() - memory_get_usage();
3604  }
3605 
3611  public static function GetPhpMemoryLimit()
3612  {
3613  $Str = strtoupper(ini_get("memory_limit"));
3614  if (substr($Str, -1) == "B") { $Str = substr($Str, 0, strlen($Str) - 1); }
3615  switch (substr($Str, -1))
3616  {
3617  case "K":
3618  $MemoryLimit = (int)$Str * 1024;
3619  break;
3620 
3621  case "M":
3622  $MemoryLimit = (int)$Str * 1048576;
3623  break;
3624 
3625  case "G":
3626  $MemoryLimit = (int)$Str * 1073741824;
3627  break;
3628 
3629  default:
3630  $MemoryLimit = (int)$Str;
3631  break;
3632  }
3633  return $MemoryLimit;
3634  }
3635 
3648  public function MaxExecutionTime($NewValue = DB_NOVALUE, $Persistent = FALSE)
3649  {
3650  if ($NewValue !== DB_NOVALUE)
3651  {
3652  $NewValue = max($NewValue, 5);
3653  ini_set("max_execution_time", $NewValue);
3654  set_time_limit($NewValue - $this->GetElapsedExecutionTime());
3655  $this->UpdateSetting("MaxExecTime", $NewValue, $Persistent);
3656  }
3657  return ini_get("max_execution_time");
3658  }
3659 
3660  /*@)*/ /* Server Environment */
3661 
3662 
3663  # ---- Utility -----------------------------------------------------------
3664  /*@(*/
3666 
3678  public function DownloadFile($FilePath, $FileName = NULL, $MimeType = NULL)
3679  {
3680  # check that file is readable
3681  if (!is_readable($FilePath))
3682  {
3683  return FALSE;
3684  }
3685 
3686  # if file name was not supplied
3687  if ($FileName === NULL)
3688  {
3689  # extract file name from path
3690  $FileName = basename($FilePath);
3691  }
3692 
3693  # if MIME type was not supplied
3694  if ($MimeType === NULL)
3695  {
3696  # attempt to determine MIME type
3697  $FInfoHandle = finfo_open(FILEINFO_MIME);
3698  if ($FInfoHandle)
3699  {
3700  $FInfoMime = finfo_file($FInfoHandle, $FilePath);
3701  finfo_close($FInfoHandle);
3702  if ($FInfoMime)
3703  {
3704  $MimeType = $FInfoMime;
3705  }
3706  }
3707 
3708  # use default if unable to determine MIME type
3709  if ($MimeType === NULL)
3710  {
3711  $MimeType = "application/octet-stream";
3712  }
3713  }
3714 
3715  # set headers to download file
3716  header("Content-Type: ".$MimeType);
3717  header("Content-Length: ".filesize($FilePath));
3718  if ($this->CleanUrlRewritePerformed)
3719  {
3720  header('Content-Disposition: attachment; filename="'.$FileName.'"');
3721  }
3722 
3723  # make sure that apache does not attempt to compress file
3724  apache_setenv('no-gzip', '1');
3725 
3726  # send file to user, but unbuffered to avoid memory issues
3727  $this->AddUnbufferedCallback(function ($File)
3728  {
3729  $BlockSize = 512000;
3730  $Handle = @fopen($File, "rb");
3731  if ($Handle === FALSE)
3732  {
3733  return;
3734  }
3735  while (!feof($Handle))
3736  {
3737  print fread($Handle, $BlockSize);
3738  flush();
3739  }
3740  fclose($Handle);
3741  }, array($FilePath));
3742 
3743  # prevent HTML output that might interfere with download
3744  $this->SuppressHTMLOutput();
3745 
3746  # set flag to indicate not to log a slow page load in case client
3747  # connection delays PHP execution because of header
3748  $this->DoNotLogSlowPageLoad = TRUE;
3749 
3750  # report no errors found to caller
3751  return TRUE;
3752  }
3753 
3766  public function GetLock($LockName, $Wait = TRUE)
3767  {
3768  # assume we will not get a lock
3769  $GotLock = FALSE;
3770 
3771  # clear out any stale locks
3772  static $CleanupHasBeenDone = FALSE;
3773  if (!$CleanupHasBeenDone)
3774  {
3775  # (margin for clearing stale locks is twice the known
3776  # maximum PHP execution time, because the max time
3777  # techinically does not include external operations
3778  # like database queries)
3779  $ClearLocksObtainedBefore = date(StdLib::SQL_DATE_FORMAT,
3780  (time() - ($this->MaxExecutionTime() * 2)));
3781  $this->DB->Query("DELETE FROM AF_Locks WHERE"
3782  ." ObtainedAt < '".$ClearLocksObtainedBefore."' AND"
3783  ." LockName = '".addslashes($LockName)."'");
3784  }
3785 
3786  do
3787  {
3788  # lock database table so nobody else can try to get a lock
3789  $this->DB->Query("LOCK TABLES AF_Locks WRITE");
3790 
3791  # look for lock with specified name
3792  $FoundCount = $this->DB->Query("SELECT COUNT(*) AS FoundCount"
3793  ." FROM AF_Locks WHERE LockName = '"
3794  .addslashes($LockName)."'", "FoundCount");
3795  $LockFound = ($FoundCount > 0) ? TRUE : FALSE;
3796 
3797  # if lock found
3798  if ($LockFound)
3799  {
3800  # unlock database tables
3801  $this->DB->Query("UNLOCK TABLES");
3802 
3803  # if blocking was requested
3804  if ($Wait)
3805  {
3806  # wait to give someone else a chance to release lock
3807  sleep(2);
3808  }
3809  }
3810  // @codingStandardsIgnoreStart
3811  // (because phpcs does not correctly handle do-while loops)
3812  # while lock was found and blocking was requested
3813  } while ($LockFound && $Wait);
3814  // @codingStandardsIgnoreEnd
3815 
3816  # if lock was not found
3817  if (!$LockFound)
3818  {
3819  # get our lock
3820  $this->DB->Query("INSERT INTO AF_Locks (LockName) VALUES ('"
3821  .addslashes($LockName)."')");
3822  $GotLock = TRUE;
3823 
3824  # unlock database tables
3825  $this->DB->Query("UNLOCK TABLES");
3826  }
3827 
3828  # report to caller whether lock was obtained
3829  return $GotLock;
3830  }
3831 
3839  public function ReleaseLock($LockName)
3840  {
3841  # release any existing locks
3842  $this->DB->Query("DELETE FROM AF_Locks WHERE LockName = '"
3843  .addslashes($LockName)."'");
3844 
3845  # report to caller whether existing lock was released
3846  return $this->DB->NumRowsAffected() ? TRUE : FALSE;
3847  }
3848 
3849  /*@)*/ /* Utility */
3850 
3851 
3852  # ---- Backward Compatibility --------------------------------------------
3853  /*@(*/
3855 
3862  public function FindCommonTemplate($BaseName)
3863  {
3864  return $this->FindFile(
3865  $this->IncludeDirList, $BaseName, array("tpl", "html"));
3866  }
3867 
3868  /*@)*/ /* Backward Compatibility */
3869 
3870 
3871  # ---- PRIVATE INTERFACE -------------------------------------------------
3872 
3873  private $AdditionalRequiredUIFiles = array();
3874  private $BackgroundTaskMemLeakLogThreshold = 10; # percentage of max mem
3875  private $BackgroundTaskMinFreeMemPercent = 25;
3876  private $BrowserDetectFunc;
3877  private $CacheCurrentPage = TRUE;
3878  private $AlternateDomainPrefixes = array();
3879  private $CleanUrlMappings = array();
3880  private $CleanUrlRewritePerformed = FALSE;
3881  private $CssUrlFingerprintPath;
3882  private $DB;
3883  private $DefaultPage = "Home";
3884  private $DoNotMinimizeList = array();
3885  private $DoNotLogSlowPageLoad = FALSE;
3886  private $EnvIncludes = array();
3887  private $ExecutionStartTime;
3888  private $FoundUIFiles = array();
3889  private $HtmlCharset = "UTF-8";
3890  private $JSMinimizerJavaScriptPackerAvailable = FALSE;
3891  private $JSMinimizerJShrinkAvailable = TRUE;
3892  private $JumpToPage = NULL;
3893  private $JumpToPageDelay = 0;
3894  private $LogFileName = "local/logs/site.log";
3895  private $MaxRunningTasksToTrack = 250;
3896  private $OutputModificationCallbackInfo;
3897  private $OutputModificationCallbacks = array();
3898  private $OutputModificationPatterns = array();
3899  private $OutputModificationReplacements = array();
3900  private $PageCacheTags = array();
3901  private $PageName;
3902  private $PostProcessingFuncs = array();
3903  private $RunningInBackground = FALSE;
3904  private $RunningTask;
3905  private $SavedContext;
3906  private $SaveTemplateLocationCache = FALSE;
3907  private $SessionStorage;
3908  private $SessionGcProbability;
3909  private $Settings;
3910  private $SuppressHTML = FALSE;
3911  private $TemplateLocationCache;
3912  private $TemplateLocationCacheInterval = 60; # in minutes
3913  private $TemplateLocationCacheExpiration;
3914  private $UnbufferedCallbacks = array();
3915  private $UrlFingerprintBlacklist = array();
3916  private $UseBaseTag = FALSE;
3917 
3918  private static $ActiveUI = "default";
3919  private static $AppName = "ScoutAF";
3920  private static $DefaultUI = "default";
3921  private static $JSMinCacheDir = "local/data/caches/JSMin";
3922  private static $ObjectDirectories = array();
3923  private static $ObjectLocationCache;
3924  private static $ObjectLocationCacheInterval = 60;
3925  private static $ObjectLocationCacheExpiration;
3926  private static $PreferHttpHost = FALSE;
3927  private static $RootUrlOverride = NULL;
3928  private static $SaveObjectLocationCache = FALSE;
3929  private static $ScssCacheDir = "local/data/caches/SCSS";
3930  private static $SessionLifetime = 1440; # in seconds
3931 
3932  # offset used to generate page cache tag IDs from numeric tags
3933  const PAGECACHETAGIDOFFSET = 100000;
3934 
3935  # minimum expired session garbage collection probability
3936  const MIN_GC_PROBABILITY = 0.01;
3937 
3942  private $NoTSR = FALSE;
3943 
3944  private $KnownPeriodicEvents = array();
3945  private $PeriodicEvents = array(
3946  "EVENT_HOURLY" => self::EVENTTYPE_DEFAULT,
3947  "EVENT_DAILY" => self::EVENTTYPE_DEFAULT,
3948  "EVENT_WEEKLY" => self::EVENTTYPE_DEFAULT,
3949  "EVENT_MONTHLY" => self::EVENTTYPE_DEFAULT,
3950  "EVENT_PERIODIC" => self::EVENTTYPE_NAMED,
3951  );
3952  private $EventPeriods = array(
3953  "EVENT_HOURLY" => 3600,
3954  "EVENT_DAILY" => 86400,
3955  "EVENT_WEEKLY" => 604800,
3956  "EVENT_MONTHLY" => 2592000,
3957  "EVENT_PERIODIC" => 0,
3958  );
3959  private $UIEvents = array(
3960  "EVENT_PAGE_LOAD" => self::EVENTTYPE_DEFAULT,
3961  "EVENT_PHP_FILE_LOAD" => self::EVENTTYPE_CHAIN,
3962  "EVENT_PHP_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
3963  "EVENT_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
3964  "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
3965  "EVENT_PAGE_OUTPUT_FILTER" => self::EVENTTYPE_CHAIN,
3966  );
3967 
3972  private function LoadSettings()
3973  {
3974  # read settings in from database
3975  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
3976  $this->Settings = $this->DB->FetchRow();
3977 
3978  # if settings were not previously initialized
3979  if ($this->Settings === FALSE)
3980  {
3981  # initialize settings in database
3982  $this->DB->Query("INSERT INTO ApplicationFrameworkSettings"
3983  ." (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");
3984 
3985  # read new settings in from database
3986  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
3987  $this->Settings = $this->DB->FetchRow();
3988 
3989  # bail out if reloading new settings failed
3990  if ($this->Settings === FALSE)
3991  {
3992  throw new Exception(
3993  "Unable to load application framework settings.");
3994  }
3995  }
3996 
3997  # if base path was not previously set or we appear to have moved
3998  if (!array_key_exists("BasePath", $this->Settings)
3999  || (!strlen($this->Settings["BasePath"]))
4000  || (!array_key_exists("BasePathCheck", $this->Settings))
4001  || (__FILE__ != $this->Settings["BasePathCheck"]))
4002  {
4003  # attempt to extract base path from Apache .htaccess file
4004  if (is_readable(".htaccess"))
4005  {
4006  $Lines = file(".htaccess");
4007  foreach ($Lines as $Line)
4008  {
4009  if (preg_match("/\\s*RewriteBase\\s+/", $Line))
4010  {
4011  $Pieces = preg_split(
4012  "/\\s+/", $Line, NULL, PREG_SPLIT_NO_EMPTY);
4013  $BasePath = $Pieces[1];
4014  }
4015  }
4016  }
4017 
4018  # if base path was found
4019  if (isset($BasePath))
4020  {
4021  # save base path locally
4022  $this->Settings["BasePath"] = $BasePath;
4023 
4024  # save base path to database
4025  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
4026  ." SET BasePath = '".addslashes($BasePath)."'"
4027  .", BasePathCheck = '".addslashes(__FILE__)."'");
4028  }
4029  }
4030 
4031  # retrieve template location cache
4032  $this->TemplateLocationCache = unserialize(
4033  $this->Settings["TemplateLocationCache"]);
4034  $this->TemplateLocationCacheInterval =
4035  $this->Settings["TemplateLocationCacheInterval"];
4036  $this->TemplateLocationCacheExpiration =
4037  strtotime($this->Settings["TemplateLocationCacheExpiration"]);
4038 
4039  # if template location cache looks invalid or has expired
4040  $CurrentTime = time();
4041  if (!count($this->TemplateLocationCache)
4042  || ($CurrentTime >= $this->TemplateLocationCacheExpiration))
4043  {
4044  # clear cache and reset cache expiration
4045  $this->TemplateLocationCache = array();
4046  $this->TemplateLocationCacheExpiration =
4047  $CurrentTime + ($this->TemplateLocationCacheInterval * 60);
4048  $this->SaveTemplateLocationCache = TRUE;
4049  }
4050 
4051  # retrieve object location cache
4052  self::$ObjectLocationCache =
4053  unserialize($this->Settings["ObjectLocationCache"]);
4054  self::$ObjectLocationCacheInterval =
4055  $this->Settings["ObjectLocationCacheInterval"];
4056  self::$ObjectLocationCacheExpiration =
4057  strtotime($this->Settings["ObjectLocationCacheExpiration"]);
4058 
4059  # if object location cache looks invalid or has expired
4060  if (!count(self::$ObjectLocationCache)
4061  || ($CurrentTime >= self::$ObjectLocationCacheExpiration))
4062  {
4063  # clear cache and reset cache expiration
4064  self::$ObjectLocationCache = array();
4065  self::$ObjectLocationCacheExpiration =
4066  $CurrentTime + (self::$ObjectLocationCacheInterval * 60);
4067  self::$SaveObjectLocationCache = TRUE;
4068  }
4069  }
4070 
4077  private function RewriteCleanUrls($PageName)
4078  {
4079  # if URL rewriting is supported by the server
4080  if ($this->HtaccessSupport())
4081  {
4082  # retrieve current URL and remove base path if present
4083  $Url = $this->GetPageLocation();
4084 
4085  # for each clean URL mapping
4086  foreach ($this->CleanUrlMappings as $Info)
4087  {
4088  # if current URL matches clean URL pattern
4089  if (preg_match($Info["Pattern"], $Url, $Matches))
4090  {
4091  # set new page
4092  $PageName = $Info["Page"];
4093 
4094  # if $_GET variables specified for clean URL
4095  if ($Info["GetVars"] !== NULL)
4096  {
4097  # for each $_GET variable specified for clean URL
4098  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
4099  {
4100  # start with template for variable value
4101  $Value = $VarTemplate;
4102 
4103  # for each subpattern matched in current URL
4104  foreach ($Matches as $Index => $Match)
4105  {
4106  # if not first (whole) match
4107  if ($Index > 0)
4108  {
4109  # make any substitutions in template
4110  $Value = str_replace("$".$Index, $Match, $Value);
4111  }
4112  }
4113 
4114  # set $_GET variable
4115  $_GET[$VarName] = $Value;
4116  }
4117  }
4118 
4119  # set flag indicating clean URL mapped
4120  $this->CleanUrlRewritePerformed = TRUE;
4121 
4122  # stop looking for a mapping
4123  break;
4124  }
4125  }
4126  }
4127 
4128  # return (possibly) updated page name to caller
4129  return $PageName;
4130  }
4131 
4144  private function RewriteAlternateDomainUrls($Html)
4145  {
4146  if ($this->HtaccessSupport() &&
4147  self::$RootUrlOverride !== NULL)
4148  {
4149  $VHost = $_SERVER["SERVER_NAME"];
4150  if (isset($this->AlternateDomainPrefixes[$VHost]))
4151  {
4152  $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
4153 
4154  # get the URL for the primary domain
4155  $RootUrl = $this->RootUrl()."/";
4156 
4157  # and figure out what protcol we were using
4158  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
4159 
4160  # convert all relative URLs absolute URLs within our
4161  # primary domain, then substitute in the alternate domain
4162  # for paths inside our configured prefix
4163  $RelativePathPatterns = array(
4164  "%src=\"(?!http://|https://)%i",
4165  "%src='(?!http://|https://)%i",
4166  "%href=\"(?!http://|https://)%i",
4167  "%href='(?!http://|https://)%i",
4168  "%action=\"(?!http://|https://)%i",
4169  "%action='(?!http://|https://)%i",
4170  "%@import\s+url\(\"(?!http://|https://)%i",
4171  "%@import\s+url\('(?!http://|https://)%i",
4172  "%src:\s+url\(\"(?!http://|https://)%i",
4173  "%src:\s+url\('(?!http://|https://)%i",
4174  "%@import\s+\"(?!http://|https://)%i",
4175  "%@import\s+'(?!http://|https://)%i",
4176  "%".preg_quote($RootUrl.$ThisPrefix."/", "%")."%",
4177  );
4178  $RelativePathReplacements = array(
4179  "src=\"".$RootUrl,
4180  "src='".$RootUrl,
4181  "href=\"".$RootUrl,
4182  "href='".$RootUrl,
4183  "action=\"".$RootUrl,
4184  "action='".$RootUrl,
4185  "@import url(\"".$RootUrl,
4186  "@import url('".$RootUrl,
4187  "src: url(\"".$RootUrl,
4188  "src: url('".$RootUrl,
4189  "@import \"".$RootUrl,
4190  "@import '".$RootUrl,
4191  $Protocol."://".$VHost."/",
4192  );
4193 
4194  $NewHtml = preg_replace(
4195  $RelativePathPatterns,
4196  $RelativePathReplacements,
4197  $Html);
4198 
4199  # check to make sure relative path fixes didn't fail
4200  $Html = $this->CheckOutputModification(
4201  $Html, $NewHtml,
4202  "alternate domain substitutions");
4203  }
4204  }
4205 
4206  return $Html;
4207  }
4208 
4227  private function FindFile($DirectoryList, $BaseName,
4228  $PossibleSuffixes = NULL, $PossiblePrefixes = NULL)
4229  {
4230  # generate template cache index for this page
4231  $CacheIndex = md5(serialize($DirectoryList))
4232  .":".self::$ActiveUI.":".$BaseName;
4233 
4234  # if caching is enabled and we have cached location
4235  if (($this->TemplateLocationCacheInterval > 0)
4236  && array_key_exists($CacheIndex,
4237  $this->TemplateLocationCache))
4238  {
4239  # use template location from cache
4240  $FoundFileName = $this->TemplateLocationCache[$CacheIndex];
4241  }
4242  else
4243  {
4244  # if suffixes specified and base name does not include suffix
4245  if (count($PossibleSuffixes)
4246  && !preg_match("/\.[a-zA-Z0-9]+$/", $BaseName))
4247  {
4248  # add versions of file names with suffixes to file name list
4249  $FileNames = array();
4250  foreach ($PossibleSuffixes as $Suffix)
4251  {
4252  $FileNames[] = $BaseName.".".$Suffix;
4253  }
4254  }
4255  else
4256  {
4257  # use base name as file name
4258  $FileNames = array($BaseName);
4259  }
4260 
4261  # if prefixes specified
4262  if (count($PossiblePrefixes))
4263  {
4264  # add versions of file names with prefixes to file name list
4265  $NewFileNames = array();
4266  foreach ($FileNames as $FileName)
4267  {
4268  foreach ($PossiblePrefixes as $Prefix)
4269  {
4270  $NewFileNames[] = $Prefix.$FileName;
4271  }
4272  }
4273  $FileNames = $NewFileNames;
4274  }
4275 
4276  # for each possible location
4277  $FoundFileName = NULL;
4278  foreach ($DirectoryList as $Dir)
4279  {
4280  # substitute active or default UI name into path
4281  $Dir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4282  array(self::$ActiveUI, self::$DefaultUI), $Dir);
4283 
4284  # for each possible file name
4285  foreach ($FileNames as $File)
4286  {
4287  # if template is found at location
4288  if (file_exists($Dir.$File))
4289  {
4290  # save full template file name and stop looking
4291  $FoundFileName = $Dir.$File;
4292  break 2;
4293  }
4294  }
4295  }
4296 
4297  # save location in cache
4298  $this->TemplateLocationCache[$CacheIndex]
4299  = $FoundFileName;
4300 
4301  # set flag indicating that cache should be saved
4302  $this->SaveTemplateLocationCache = TRUE;
4303  }
4304 
4305  # return full template file name to caller
4306  return $FoundFileName;
4307  }
4308 
4317  private function CompileScssFile($SrcFile)
4318  {
4319  # build path to CSS file
4320  $DstFile = self::$ScssCacheDir."/".dirname($SrcFile)
4321  ."/".basename($SrcFile);
4322  $DstFile = substr_replace($DstFile, "css", -4);
4323 
4324  # if SCSS file is newer than CSS file
4325  if (!file_exists($DstFile)
4326  || (filemtime($SrcFile) > filemtime($DstFile)))
4327  {
4328  # attempt to create CSS cache subdirectory if not present
4329  if (!is_dir(dirname($DstFile)))
4330  {
4331  @mkdir(dirname($DstFile), 0777, TRUE);
4332  }
4333 
4334  # if CSS cache directory and CSS file path appear writable
4335  static $CacheDirIsWritable;
4336  if (!isset($CacheDirIsWritable))
4337  { $CacheDirIsWritable = is_writable(self::$ScssCacheDir); }
4338  if (is_writable($DstFile)
4339  || (!file_exists($DstFile) && $CacheDirIsWritable))
4340  {
4341  # load SCSS and compile to CSS
4342  $ScssCode = file_get_contents($SrcFile);
4343  $ScssCompiler = new scssc();
4344  $ScssCompiler->setFormatter($this->GenerateCompactCss()
4345  ? "scss_formatter_compressed" : "scss_formatter");
4346  try
4347  {
4348  $CssCode = $ScssCompiler->compile($ScssCode);
4349 
4350  # add fingerprinting for URLs in CSS
4351  $this->CssUrlFingerprintPath = dirname($SrcFile);
4352  $CssCode = preg_replace_callback(
4353  "/url\((['\"]*)(.+)\.([a-z]+)(['\"]*)\)/",
4354  array($this, "CssUrlFingerprintInsertion"),
4355  $CssCode);
4356 
4357  # strip out comments from CSS (if requested)
4358  if ($this->GenerateCompactCss())
4359  {
4360  $CssCode = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!',
4361  '', $CssCode);
4362  }
4363 
4364  # write out CSS file
4365  file_put_contents($DstFile, $CssCode);
4366  }
4367  catch (Exception $Ex)
4368  {
4369  $this->LogError(self::LOGLVL_ERROR,
4370  "Error compiling SCSS file ".$SrcFile.": "
4371  .$Ex->getMessage());
4372  $DstFile = NULL;
4373  }
4374  }
4375  else
4376  {
4377  # log error and set CSS file path to indicate failure
4378  $this->LogError(self::LOGLVL_ERROR,
4379  "Unable to write out CSS file (compiled from SCSS) to "
4380  .$DstFile);
4381  $DstFile = NULL;
4382  }
4383  }
4384 
4385  # return CSS file path to caller
4386  return $DstFile;
4387  }
4388 
4396  private function MinimizeJavascriptFile($SrcFile)
4397  {
4398  # bail out if file is on exclusion list
4399  foreach ($this->DoNotMinimizeList as $DNMFile)
4400  {
4401  if (($SrcFile == $DNMFile) || (basename($SrcFile) == $DNMFile))
4402  {
4403  return NULL;
4404  }
4405  }
4406 
4407  # build path to minimized file
4408  $DstFile = self::$JSMinCacheDir."/".dirname($SrcFile)
4409  ."/".basename($SrcFile);
4410  $DstFile = substr_replace($DstFile, ".min", -3, 0);
4411 
4412  # if original file is newer than minimized file
4413  if (!file_exists($DstFile)
4414  || (filemtime($SrcFile) > filemtime($DstFile)))
4415  {
4416  # attempt to create cache subdirectory if not present
4417  if (!is_dir(dirname($DstFile)))
4418  {
4419  @mkdir(dirname($DstFile), 0777, TRUE);
4420  }
4421 
4422  # if cache directory and minimized file path appear writable
4423  static $CacheDirIsWritable;
4424  if (!isset($CacheDirIsWritable))
4425  { $CacheDirIsWritable = is_writable(self::$JSMinCacheDir); }
4426  if (is_writable($DstFile)
4427  || (!file_exists($DstFile) && $CacheDirIsWritable))
4428  {
4429  # load JavaScript code
4430  $Code = file_get_contents($SrcFile);
4431 
4432  # decide which minimizer to use
4433  if ($this->JSMinimizerJavaScriptPackerAvailable
4434  && $this->JSMinimizerJShrinkAvailable)
4435  {
4436  $Minimizer = (strlen($Code) < 5000)
4437  ? "JShrink" : "JavaScriptPacker";
4438  }
4439  elseif ($this->JSMinimizerJShrinkAvailable)
4440  {
4441  $Minimizer = "JShrink";
4442  }
4443  else
4444  {
4445  $Minimizer = "NONE";
4446  }
4447 
4448  # minimize code
4449  switch ($Minimizer)
4450  {
4451  case "JavaScriptMinimizer":
4452  $Packer = new JavaScriptPacker($Code, "Normal");
4453  $MinimizedCode = $Packer->pack();
4454  break;
4455 
4456  case "JShrink":
4457  try
4458  {
4459  $MinimizedCode = \JShrink\Minifier::minify($Code);
4460  }
4461  catch (Exception $Exception)
4462  {
4463  unset($MinimizedCode);
4464  $MinimizeError = $Exception->getMessage();
4465  }
4466  break;
4467  }
4468 
4469  # if minimization succeeded
4470  if (isset($MinimizedCode))
4471  {
4472  # write out minimized file
4473  file_put_contents($DstFile, $MinimizedCode);
4474  }
4475  else
4476  {
4477  # log error and set destination file path to indicate failure
4478  $ErrMsg = "Unable to minimize JavaScript file ".$SrcFile;
4479  if (isset($MinimizeError))
4480  {
4481  $ErrMsg .= " (".$MinimizeError.")";
4482  }
4483  $this->LogError(self::LOGLVL_ERROR, $ErrMsg);
4484  $DstFile = NULL;
4485  }
4486  }
4487  else
4488  {
4489  # log error and set destination file path to indicate failure
4490  $this->LogError(self::LOGLVL_ERROR,
4491  "Unable to write out minimized JavaScript to file ".$DstFile);
4492  $DstFile = NULL;
4493  }
4494  }
4495 
4496  # return CSS file path to caller
4497  return $DstFile;
4498  }
4499 
4507  private function CssUrlFingerprintInsertion($Matches)
4508  {
4509  # generate fingerprint string from CSS file modification time
4510  $FileName = realpath($this->CssUrlFingerprintPath."/".
4511  $Matches[2].".".$Matches[3]);
4512  $MTime = filemtime($FileName);
4513  $Fingerprint = sprintf("%06X", ($MTime % 0xFFFFFF));
4514 
4515  # build URL string with fingerprint and return it to caller
4516  return "url(".$Matches[1].$Matches[2].".".$Fingerprint
4517  .".".$Matches[3].$Matches[4].")";
4518  }
4519 
4526  private function GetRequiredFilesNotYetLoaded($PageContentFile)
4527  {
4528  # start out assuming no files required
4529  $RequiredFiles = array();
4530 
4531  # if page content file supplied
4532  if ($PageContentFile)
4533  {
4534  # if file containing list of required files is available
4535  $Path = dirname($PageContentFile);
4536  $RequireListFile = $Path."/REQUIRES";
4537  if (file_exists($RequireListFile))
4538  {
4539  # read in list of required files
4540  $RequestedFiles = file($RequireListFile);
4541 
4542  # for each line in required file list
4543  foreach ($RequestedFiles as $Line)
4544  {
4545  # if line is not a comment
4546  $Line = trim($Line);
4547  if (!preg_match("/^#/", $Line))
4548  {
4549  # if file has not already been loaded
4550  if (!in_array($Line, $this->FoundUIFiles))
4551  {
4552  # add to list of required files
4553  $RequiredFiles[] = $Line;
4554  }
4555  }
4556  }
4557  }
4558  }
4559 
4560  # add in additional required files if any
4561  if (count($this->AdditionalRequiredUIFiles))
4562  {
4563  # make sure there are no duplicates
4564  $AdditionalRequiredUIFiles = array_unique(
4565  $this->AdditionalRequiredUIFiles);
4566 
4567  $RequiredFiles = array_merge(
4568  $RequiredFiles, $AdditionalRequiredUIFiles);
4569  }
4570 
4571  # return list of required files to caller
4572  return $RequiredFiles;
4573  }
4574 
4585  private function GetUIFileLoadingTag($FileName, $AdditionalAttributes = NULL)
4586  {
4587  # pad additional attributes if supplied
4588  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
4589 
4590  # retrieve type of UI file
4591  $FileType = $this->GetFileType($FileName);
4592 
4593  # construct tag based on file type
4594  switch ($FileType)
4595  {
4596  case self::FT_CSS:
4597  $Tag = "<link rel=\"stylesheet\" type=\"text/css\""
4598  ." media=\"all\" href=\"".$FileName."\""
4599  .$AddAttribs." />";
4600  break;
4601 
4602  case self::FT_JAVASCRIPT:
4603  $Tag = "<script type=\"text/javascript\""
4604  ." src=\"".$FileName."\""
4605  .$AddAttribs."></script>";
4606  break;
4607 
4608  default:
4609  $Tag = "";
4610  break;
4611  }
4612 
4613  # return constructed tag to caller
4614  return $Tag;
4615  }
4616 
4621  private function AutoloadObjects($ClassName)
4622  {
4623  # if caching is not turned off
4624  # and we have a cached location for class
4625  # and file at cached location is readable
4626  if ((self::$ObjectLocationCacheInterval > 0)
4627  && array_key_exists($ClassName,
4628  self::$ObjectLocationCache)
4629  && is_readable(self::$ObjectLocationCache[$ClassName]))
4630  {
4631  # use object location from cache
4632  require_once(self::$ObjectLocationCache[$ClassName]);
4633  }
4634  else
4635  {
4636  # convert any namespace separators in class name
4637  $ClassName = str_replace("\\", "-", $ClassName);
4638 
4639  # for each possible object file directory
4640  static $FileLists;
4641  foreach (self::$ObjectDirectories as $Location => $Info)
4642  {
4643  # make any needed replacements in directory path
4644  $Location = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4645  array(self::$ActiveUI, self::$DefaultUI), $Location);
4646 
4647  # if directory looks valid
4648  if (is_dir($Location))
4649  {
4650  # build class file name
4651  $NewClassName = ($Info["ClassPattern"] && $Info["ClassReplacement"])
4652  ? preg_replace($Info["ClassPattern"],
4653  $Info["ClassReplacement"], $ClassName)
4654  : $ClassName;
4655 
4656  # read in directory contents if not already retrieved
4657  if (!isset($FileLists[$Location]))
4658  {
4659  $FileLists[$Location] = self::ReadDirectoryTree(
4660  $Location, '/^.+\.php$/i');
4661  }
4662 
4663  # for each file in target directory
4664  $FileNames = $FileLists[$Location];
4665  $TargetName = strtolower($Info["Prefix"].$NewClassName.".php");
4666  foreach ($FileNames as $FileName)
4667  {
4668  # if file matches our target object file name
4669  if (strtolower($FileName) == $TargetName)
4670  {
4671  # include object file
4672  require_once($Location.$FileName);
4673 
4674  # save location to cache
4675  self::$ObjectLocationCache[$ClassName]
4676  = $Location.$FileName;
4677 
4678  # set flag indicating that cache should be saved
4679  self::$SaveObjectLocationCache = TRUE;
4680 
4681  # stop looking
4682  break 2;
4683  }
4684  }
4685  }
4686  }
4687  }
4688  }
4689 
4697  private static function ReadDirectoryTree($Directory, $Pattern)
4698  {
4699  $CurrentDir = getcwd();
4700  chdir($Directory);
4701  $DirIter = new RecursiveDirectoryIterator(".");
4702  $IterIter = new RecursiveIteratorIterator($DirIter);
4703  $RegexResults = new RegexIterator($IterIter, $Pattern,
4704  RecursiveRegexIterator::GET_MATCH);
4705  $FileList = array();
4706  foreach ($RegexResults as $Result)
4707  {
4708  $FileList[] = substr($Result[0], 2);
4709  }
4710  chdir($CurrentDir);
4711  return $FileList;
4712  }
4713 
4717  private function UndoMagicQuotes()
4718  {
4719  # if this PHP version has magic quotes support
4720  if (version_compare(PHP_VERSION, "5.4.0", "<"))
4721  {
4722  # turn off runtime magic quotes if on
4723  if (get_magic_quotes_runtime())
4724  {
4725  // @codingStandardsIgnoreStart
4726  set_magic_quotes_runtime(FALSE);
4727  // @codingStandardsIgnoreEnd
4728  }
4729 
4730  # if magic quotes GPC is on
4731  if (get_magic_quotes_gpc())
4732  {
4733  # strip added slashes from incoming variables
4734  $GPC = array(&$_GET, &$_POST, &$_COOKIE, &$_REQUEST);
4735  array_walk_recursive($GPC,
4736  array($this, "UndoMagicQuotes_StripCallback"));
4737  }
4738  }
4739  }
4744  private function UndoMagicQuotes_StripCallback(&$Value)
4745  {
4746  $Value = stripslashes($Value);
4747  }
4748 
4753  private function LoadUIFunctions()
4754  {
4755  $Dirs = array(
4756  "local/interface/%ACTIVEUI%/include",
4757  "interface/%ACTIVEUI%/include",
4758  "local/interface/%DEFAULTUI%/include",
4759  "interface/%DEFAULTUI%/include",
4760  );
4761  foreach ($Dirs as $Dir)
4762  {
4763  $Dir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4764  array(self::$ActiveUI, self::$DefaultUI), $Dir);
4765  if (is_dir($Dir))
4766  {
4767  $FileNames = scandir($Dir);
4768  foreach ($FileNames as $FileName)
4769  {
4770  if (preg_match("/^F-([A-Za-z0-9_]+)\.php/",
4771  $FileName, $Matches)
4772  || preg_match("/^F-([A-Za-z0-9_]+)\.html/",
4773  $FileName, $Matches))
4774  {
4775  if (!function_exists($Matches[1]))
4776  {
4777  include_once($Dir."/".$FileName);
4778  }
4779  }
4780  }
4781  }
4782  }
4783  }
4784 
4790  private function ProcessPeriodicEvent($EventName, $Callback)
4791  {
4792  # retrieve last execution time for event if available
4793  $Signature = self::GetCallbackSignature($Callback);
4794  $LastRun = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
4795  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
4796 
4797  # determine whether enough time has passed for event to execute
4798  $ShouldExecute = (($LastRun === NULL)
4799  || (time() > (strtotime($LastRun) + $this->EventPeriods[$EventName])))
4800  ? TRUE : FALSE;
4801 
4802  # if event should run
4803  if ($ShouldExecute)
4804  {
4805  # add event to task queue
4806  $WrapperCallback = array("ApplicationFramework", "PeriodicEventWrapper");
4807  $WrapperParameters = array(
4808  $EventName, $Callback, array("LastRunAt" => $LastRun));
4809  $this->QueueUniqueTask($WrapperCallback, $WrapperParameters);
4810  }
4811 
4812  # add event to list of periodic events
4813  $this->KnownPeriodicEvents[$Signature] = array(
4814  "Period" => $EventName,
4815  "Callback" => $Callback,
4816  "Queued" => $ShouldExecute);
4817  }
4818 
4826  private static function PeriodicEventWrapper($EventName, $Callback, $Parameters)
4827  {
4828  static $DB;
4829  if (!isset($DB)) { $DB = new Database(); }
4830 
4831  # run event
4832  $ReturnVal = call_user_func_array($Callback, $Parameters);
4833 
4834  # if event is already in database
4835  $Signature = self::GetCallbackSignature($Callback);
4836  if ($DB->Query("SELECT COUNT(*) AS EventCount FROM PeriodicEvents"
4837  ." WHERE Signature = '".addslashes($Signature)."'", "EventCount"))
4838  {
4839  # update last run time for event
4840  $DB->Query("UPDATE PeriodicEvents SET LastRunAt = "
4841  .(($EventName == "EVENT_PERIODIC")
4842  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
4843  : "NOW()")
4844  ." WHERE Signature = '".addslashes($Signature)."'");
4845  }
4846  else
4847  {
4848  # add last run time for event to database
4849  $DB->Query("INSERT INTO PeriodicEvents (Signature, LastRunAt) VALUES "
4850  ."('".addslashes($Signature)."', "
4851  .(($EventName == "EVENT_PERIODIC")
4852  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
4853  : "NOW()").")");
4854  }
4855  }
4856 
4862  private static function GetCallbackSignature($Callback)
4863  {
4864  return !is_array($Callback) ? $Callback
4865  : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
4866  ."::".$Callback[1];
4867  }
4868 
4873  private function PrepForTSR()
4874  {
4875  # if HTML has been output and it's time to launch another task
4876  # (only TSR if HTML has been output because otherwise browsers
4877  # may misbehave after connection is closed)
4878  if ((PHP_SAPI != "cli")
4879  && ($this->JumpToPage || !$this->SuppressHTML)
4880  && (time() > (strtotime($this->Settings["LastTaskRunAt"])
4881  + ($this->MaxExecutionTime()
4882  / $this->Settings["MaxTasksRunning"]) + 5))
4883  && $this->GetTaskQueueSize()
4884  && $this->Settings["TaskExecutionEnabled"])
4885  {
4886  # begin buffering output for TSR
4887  ob_start();
4888 
4889  # let caller know it is time to launch another task
4890  return TRUE;
4891  }
4892  else
4893  {
4894  # let caller know it is not time to launch another task
4895  return FALSE;
4896  }
4897  }
4898 
4903  private function LaunchTSR()
4904  {
4905  # set headers to close out connection to browser
4906  if (!$this->NoTSR)
4907  {
4908  ignore_user_abort(TRUE);
4909  header("Connection: close");
4910  header("Content-Length: ".ob_get_length());
4911  }
4912 
4913  # output buffered content
4914  while (ob_get_level()) { ob_end_flush(); }
4915  flush();
4916 
4917  # write out any outstanding data and end HTTP session
4918  session_write_close();
4919 
4920  # set flag indicating that we are now running in background
4921  $this->RunningInBackground = TRUE;
4922 
4923  # handle garbage collection for session data
4924  if (isset($this->SessionStorage) &&
4925  (rand()/getrandmax()) <= $this->SessionGcProbability)
4926  {
4927  # determine when sessions will expire
4928  $ExpiredTime = strtotime("-". self::$SessionLifetime." seconds");
4929 
4930  # iterate over files in the session directory with a DirectoryIterator
4931  # NB: we cannot use scandir() here because it reads the
4932  # entire list of files into memory and may exceed the memory
4933  # limit for directories with very many files
4934  $DI = new DirectoryIterator($this->SessionStorage);
4935  while ($DI->valid())
4936  {
4937  if ((strpos($DI->getFilename(), "sess_") === 0) &&
4938  $DI->isFile() &&
4939  $DI->getCTime() < $ExpiredTime)
4940  {
4941  unlink($DI->getPathname());
4942  }
4943  $DI->next();
4944  }
4945  }
4946 
4947  # if there is still a task in the queue
4948  if ($this->GetTaskQueueSize())
4949  {
4950  # garbage collect to give as much memory as possible for tasks
4951  if (function_exists("gc_collect_cycles")) { gc_collect_cycles(); }
4952 
4953  # turn on output buffering to (hopefully) record any crash output
4954  ob_start();
4955 
4956  # lock tables and grab last task run time to double check
4957  $this->DB->Query("LOCK TABLES ApplicationFrameworkSettings WRITE");
4958  $this->LoadSettings();
4959 
4960  # if still time to launch another task
4961  if (time() > (strtotime($this->Settings["LastTaskRunAt"])
4962  + ($this->MaxExecutionTime()
4963  / $this->Settings["MaxTasksRunning"]) + 5))
4964  {
4965  # update the "last run" time and release tables
4966  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
4967  ." SET LastTaskRunAt = '".date("Y-m-d H:i:s")."'");
4968  $this->DB->Query("UNLOCK TABLES");
4969 
4970  # run tasks while there is a task in the queue
4971  # and enough time and memory left
4972  do
4973  {
4974  # run the next task
4975  $this->RunNextTask();
4976 
4977  # calculate percentage of memory still available
4978  $PercentFreeMem = (self::GetFreeMemory()
4979  / self::GetPhpMemoryLimit()) * 100;
4980  }
4981  while ($this->GetTaskQueueSize()
4982  && ($this->GetSecondsBeforeTimeout() > 65)
4983  && ($PercentFreeMem > $this->BackgroundTaskMinFreeMemPercent));
4984  }
4985  else
4986  {
4987  # release tables
4988  $this->DB->Query("UNLOCK TABLES");
4989  }
4990  }
4991  }
4992 
5002  private function GetTaskList($DBQuery, $Count, $Offset)
5003  {
5004  $this->DB->Query($DBQuery." LIMIT ".intval($Offset).",".intval($Count));
5005  $Tasks = array();
5006  while ($Row = $this->DB->FetchRow())
5007  {
5008  $Tasks[$Row["TaskId"]] = $Row;
5009  if ($Row["Callback"] ==
5010  serialize(array("ApplicationFramework", "PeriodicEventWrapper")))
5011  {
5012  $WrappedCallback = unserialize($Row["Parameters"]);
5013  $Tasks[$Row["TaskId"]]["Callback"] = $WrappedCallback[1];
5014  $Tasks[$Row["TaskId"]]["Parameters"] = NULL;
5015  }
5016  else
5017  {
5018  $Tasks[$Row["TaskId"]]["Callback"] = unserialize($Row["Callback"]);
5019  $Tasks[$Row["TaskId"]]["Parameters"] = unserialize($Row["Parameters"]);
5020  }
5021  }
5022  return $Tasks;
5023  }
5024 
5028  private function RunNextTask()
5029  {
5030  # lock tables to prevent same task from being run by multiple sessions
5031  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
5032 
5033  # look for task at head of queue
5034  $this->DB->Query("SELECT * FROM TaskQueue ORDER BY Priority, TaskId LIMIT 1");
5035  $Task = $this->DB->FetchRow();
5036 
5037  # if there was a task available
5038  if ($Task)
5039  {
5040  # move task from queue to running tasks list
5041  $this->DB->Query("INSERT INTO RunningTasks "
5042  ."(TaskId,Callback,Parameters,Priority,Description) "
5043  ."SELECT * FROM TaskQueue WHERE TaskId = "
5044  .intval($Task["TaskId"]));
5045  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = "
5046  .intval($Task["TaskId"]));
5047 
5048  # release table locks to again allow other sessions to run tasks
5049  $this->DB->Query("UNLOCK TABLES");
5050 
5051  # unpack stored task info
5052  $Callback = unserialize($Task["Callback"]);
5053  $Parameters = unserialize($Task["Parameters"]);
5054 
5055  # attempt to load task callback if not already available
5056  $this->LoadFunction($Callback);
5057 
5058  # save amount of free memory for later comparison
5059  $BeforeFreeMem = self::GetFreeMemory();
5060 
5061  # run task
5062  $this->RunningTask = $Task;
5063  if ($Parameters)
5064  {
5065  call_user_func_array($Callback, $Parameters);
5066  }
5067  else
5068  {
5069  call_user_func($Callback);
5070  }
5071  unset($this->RunningTask);
5072 
5073  # log if task leaked significant memory
5074  if (function_exists("gc_collect_cycles")) { gc_collect_cycles(); }
5075  $AfterFreeMem = self::GetFreeMemory();
5076  $LeakThreshold = self::GetPhpMemoryLimit()
5077  * ($this->BackgroundTaskMemLeakLogThreshold / 100);
5078  if (($BeforeFreeMem - $AfterFreeMem) > $LeakThreshold)
5079  {
5080  $this->LogError(self::LOGLVL_DEBUG, "Task "
5081  .self::GetTaskCallbackSynopsis(
5082  $this->GetTask($Task["TaskId"]))." leaked "
5083  .number_format($BeforeFreeMem - $AfterFreeMem)." bytes.");
5084  }
5085 
5086  # remove task from running tasks list
5087  $this->DB->Query("DELETE FROM RunningTasks"
5088  ." WHERE TaskId = ".intval($Task["TaskId"]));
5089 
5090  # prune running tasks list if necessary
5091  $RunningTasksCount = $this->DB->Query(
5092  "SELECT COUNT(*) AS TaskCount FROM RunningTasks", "TaskCount");
5093  if ($RunningTasksCount > $this->MaxRunningTasksToTrack)
5094  {
5095  $this->DB->Query("DELETE FROM RunningTasks ORDER BY StartedAt"
5096  ." LIMIT ".($RunningTasksCount - $this->MaxRunningTasksToTrack));
5097  }
5098  }
5099  else
5100  {
5101  # release table locks to again allow other sessions to run tasks
5102  $this->DB->Query("UNLOCK TABLES");
5103  }
5104  }
5105 
5111  public function OnCrash()
5112  {
5113  # attempt to remove any memory limits
5114  $FreeMemory = $this->GetFreeMemory();
5115  ini_set("memory_limit", -1);
5116 
5117  # if there is a background task currently running
5118  if (isset($this->RunningTask))
5119  {
5120  # add info about current page load
5121  $CrashInfo["ElapsedTime"] = $this->GetElapsedExecutionTime();
5122  $CrashInfo["FreeMemory"] = $FreeMemory;
5123  $CrashInfo["REMOTE_ADDR"] = $_SERVER["REMOTE_ADDR"];
5124  $CrashInfo["REQUEST_URI"] = $_SERVER["REQUEST_URI"];
5125  if (isset($_SERVER["REQUEST_TIME"]))
5126  {
5127  $CrashInfo["REQUEST_TIME"] = $_SERVER["REQUEST_TIME"];
5128  }
5129  if (isset($_SERVER["REMOTE_HOST"]))
5130  {
5131  $CrashInfo["REMOTE_HOST"] = $_SERVER["REMOTE_HOST"];
5132  }
5133 
5134  # add info about error that caused crash (if available)
5135  if (function_exists("error_get_last"))
5136  {
5137  $CrashInfo["LastError"] = error_get_last();
5138  }
5139 
5140  # add info about current output buffer contents (if available)
5141  if (ob_get_length() !== FALSE)
5142  {
5143  $CrashInfo["OutputBuffer"] = ob_get_contents();
5144  }
5145 
5146  # if backtrace info is available for the crash
5147  $Backtrace = debug_backtrace();
5148  if (count($Backtrace) > 1)
5149  {
5150  # discard the current context from the backtrace
5151  array_shift($Backtrace);
5152 
5153  # add the backtrace to the crash info
5154  $CrashInfo["Backtrace"] = $Backtrace;
5155  }
5156  # else if saved backtrace info is available
5157  elseif (isset($this->SavedContext))
5158  {
5159  # add the saved backtrace to the crash info
5160  $CrashInfo["Backtrace"] = $this->SavedContext;
5161  }
5162 
5163  # save crash info for currently running task
5164  $DB = new Database();
5165  $DB->Query("UPDATE RunningTasks SET CrashInfo = '"
5166  .addslashes(serialize($CrashInfo))
5167  ."' WHERE TaskId = ".intval($this->RunningTask["TaskId"]));
5168  }
5169 
5170  print("\n");
5171  return;
5172  }
5173 
5190  private function AddToDirList($DirList, $Dir, $SearchLast, $SkipSlashCheck)
5191  {
5192  # convert incoming directory to array of directories (if needed)
5193  $Dirs = is_array($Dir) ? $Dir : array($Dir);
5194 
5195  # reverse array so directories are searched in specified order
5196  $Dirs = array_reverse($Dirs);
5197 
5198  # for each directory
5199  foreach ($Dirs as $Location)
5200  {
5201  # make sure directory includes trailing slash
5202  if (!$SkipSlashCheck)
5203  {
5204  $Location = $Location
5205  .((substr($Location, -1) != "/") ? "/" : "");
5206  }
5207 
5208  # remove directory from list if already present
5209  if (in_array($Location, $DirList))
5210  {
5211  $DirList = array_diff(
5212  $DirList, array($Location));
5213  }
5214 
5215  # add directory to list of directories
5216  if ($SearchLast)
5217  {
5218  array_push($DirList, $Location);
5219  }
5220  else
5221  {
5222  array_unshift($DirList, $Location);
5223  }
5224  }
5225 
5226  # return updated directory list to caller
5227  return $DirList;
5228  }
5229 
5237  private function ArrayPermutations($Items, $Perms = array())
5238  {
5239  if (empty($Items))
5240  {
5241  $Result = array($Perms);
5242  }
5243  else
5244  {
5245  $Result = array();
5246  for ($Index = count($Items) - 1; $Index >= 0; --$Index)
5247  {
5248  $NewItems = $Items;
5249  $NewPerms = $Perms;
5250  list($Segment) = array_splice($NewItems, $Index, 1);
5251  array_unshift($NewPerms, $Segment);
5252  $Result = array_merge($Result,
5253  $this->ArrayPermutations($NewItems, $NewPerms));
5254  }
5255  }
5256  return $Result;
5257  }
5258 
5265  private function OutputModificationCallbackShell($Matches)
5266  {
5267  # call previously-stored external function
5268  return call_user_func($this->OutputModificationCallbackInfo["Callback"],
5269  $Matches,
5270  $this->OutputModificationCallbackInfo["Pattern"],
5271  $this->OutputModificationCallbackInfo["Page"],
5272  $this->OutputModificationCallbackInfo["SearchPattern"]);
5273  }
5274 
5283  private function CheckOutputModification($Original, $Modified, $ErrorInfo)
5284  {
5285  # if error was reported by regex engine
5286  if (preg_last_error() !== PREG_NO_ERROR)
5287  {
5288  # log error
5289  $this->LogError(self::LOGLVL_ERROR,
5290  "Error reported by regex engine when modifying output."
5291  ." (".$ErrorInfo.")");
5292 
5293  # use unmodified version of output
5294  $OutputToUse = $Original;
5295  }
5296  # else if modification reduced output by more than threshold
5297  elseif ((strlen(trim($Modified)) / strlen(trim($Original)))
5298  < self::OUTPUT_MODIFICATION_THRESHOLD)
5299  {
5300  # log error
5301  $this->LogError(self::LOGLVL_WARNING,
5302  "Content reduced below acceptable threshold while modifying output."
5303  ." (".$ErrorInfo.")");
5304 
5305  # use unmodified version of output
5306  $OutputToUse = $Original;
5307  }
5308  else
5309  {
5310  # use modified version of output
5311  $OutputToUse = $Modified;
5312  }
5313 
5314  # return output to use to caller
5315  return $OutputToUse;
5316  }
5317 
5320 
5330  private function UpdateSetting(
5331  $FieldName, $NewValue = DB_NOVALUE, $Persistent = TRUE)
5332  {
5333  static $LocalSettings;
5334  if ($NewValue !== DB_NOVALUE)
5335  {
5336  if ($Persistent)
5337  {
5338  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5339  "ApplicationFrameworkSettings",
5340  $FieldName, $NewValue, NULL, $this->Settings);
5341  }
5342  else
5343  {
5344  $LocalSettings[$FieldName] = $NewValue;
5345  }
5346  }
5347  elseif (!isset($LocalSettings[$FieldName]))
5348  {
5349  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5350  "ApplicationFrameworkSettings",
5351  $FieldName, $NewValue, NULL, $this->Settings);
5352  }
5353  return $LocalSettings[$FieldName];
5354  }
5355 
5357  private $InterfaceDirList = array(
5358  "local/interface/%ACTIVEUI%/",
5359  "interface/%ACTIVEUI%/",
5360  "local/interface/%DEFAULTUI%/",
5361  "interface/%DEFAULTUI%/",
5362  );
5367  private $IncludeDirList = array(
5368  "local/interface/%ACTIVEUI%/include/",
5369  "interface/%ACTIVEUI%/include/",
5370  "interface/%ACTIVEUI%/objects/",
5371  "local/interface/%DEFAULTUI%/include/",
5372  "interface/%DEFAULTUI%/include/",
5373  "interface/%DEFAULTUI%/objects/",
5374  );
5376  private $ImageDirList = array(
5377  "local/interface/%ACTIVEUI%/images/",
5378  "interface/%ACTIVEUI%/images/",
5379  "local/interface/%DEFAULTUI%/images/",
5380  "interface/%DEFAULTUI%/images/",
5381  );
5383  private $FunctionDirList = array(
5384  "local/interface/%ACTIVEUI%/include/",
5385  "interface/%ACTIVEUI%/include/",
5386  "local/interface/%DEFAULTUI%/include/",
5387  "interface/%DEFAULTUI%/include/",
5388  "local/include/",
5389  "include/",
5390  );
5391 
5392  const NOVALUE = ".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";
5393 
5394 
5395  # ---- Page Caching (Internal Methods) -----------------------------------
5396 
5402  private function CheckForCachedPage($PageName)
5403  {
5404  # assume no cached page will be found
5405  $CachedPage = NULL;
5406 
5407  # if returning a cached page is allowed
5408  if ($this->CacheCurrentPage)
5409  {
5410  # get fingerprint for requested page
5411  $PageFingerprint = $this->GetPageFingerprint($PageName);
5412 
5413  # look for matching page in cache in database
5414  $this->DB->Query("SELECT * FROM AF_CachedPages"
5415  ." WHERE Fingerprint = '".addslashes($PageFingerprint)."'");
5416 
5417  # if matching page found
5418  if ($this->DB->NumRowsSelected())
5419  {
5420  # if cached page has expired
5421  $Row = $this->DB->FetchRow();
5422  $ExpirationTime = strtotime(
5423  "-".$this->PageCacheExpirationPeriod()." seconds");
5424  if (strtotime($Row["CachedAt"]) < $ExpirationTime)
5425  {
5426  # clear expired pages from cache
5427  $ExpirationTimestamp = date("Y-m-d H:i:s", $ExpirationTime);
5428  $this->DB->Query("DELETE CP, CPTI FROM AF_CachedPages CP,"
5429  ." AF_CachedPageTagInts CPTI"
5430  ." WHERE CP.CachedAt < '".$ExpirationTimestamp."'"
5431  ." AND CPTI.CacheId = CP.CacheId");
5432  $this->DB->Query("DELETE FROM AF_CachedPages "
5433  ." WHERE CachedAt < '".$ExpirationTimestamp."'");
5434  }
5435  else
5436  {
5437  # display cached page and exit
5438  $CachedPage = $Row["PageContent"];
5439  }
5440  }
5441  }
5442 
5443  # return any cached page found to caller
5444  return $CachedPage;
5445  }
5446 
5452  private function UpdatePageCache($PageName, $PageContent)
5453  {
5454  # if page caching is enabled and current page should be cached
5455  if ($this->PageCacheEnabled()
5456  && $this->CacheCurrentPage
5457  && ($PageName != "404"))
5458  {
5459  # if page content looks invalid
5460  if (strlen(trim(strip_tags($PageContent))) == 0)
5461  {
5462  # log error
5463  $LogMsg = "Page not cached because content was empty."
5464  ." (PAGE: ".$PageName.", URL: ".$this->FullUrl().")";
5465  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
5466  }
5467  else
5468  {
5469  # save page to cache
5470  $PageFingerprint = $this->GetPageFingerprint($PageName);
5471  $this->DB->Query("INSERT INTO AF_CachedPages"
5472  ." (Fingerprint, PageContent) VALUES"
5473  ." ('".$this->DB->EscapeString($PageFingerprint)."', '"
5474  .$this->DB->EscapeString($PageContent)."')");
5475  $CacheId = $this->DB->LastInsertId();
5476 
5477  # for each page cache tag that was added
5478  foreach ($this->PageCacheTags as $Tag => $Pages)
5479  {
5480  # if current page is in list for tag
5481  if (in_array("CURRENT", $Pages) || in_array($PageName, $Pages))
5482  {
5483  # look up tag ID
5484  $TagId = $this->GetPageCacheTagId($Tag);
5485 
5486  # mark current page as associated with tag
5487  $this->DB->Query("INSERT INTO AF_CachedPageTagInts"
5488  ." (TagId, CacheId) VALUES "
5489  ." (".intval($TagId).", ".intval($CacheId).")");
5490  }
5491  }
5492  }
5493  }
5494  }
5495 
5501  private function GetPageCacheTagId($Tag)
5502  {
5503  # if tag is a non-negative integer
5504  if (is_numeric($Tag) && ($Tag > 0) && (intval($Tag) == $Tag))
5505  {
5506  # generate ID
5507  $Id = self::PAGECACHETAGIDOFFSET + $Tag;
5508  }
5509  else
5510  {
5511  # look up ID in database
5512  $Id = $this->DB->Query("SELECT TagId FROM AF_CachedPageTags"
5513  ." WHERE Tag = '".addslashes($Tag)."'", "TagId");
5514 
5515  # if ID was not found
5516  if ($Id === NULL)
5517  {
5518  # add tag to database
5519  $this->DB->Query("INSERT INTO AF_CachedPageTags"
5520  ." SET Tag = '".addslashes($Tag)."'");
5521  $Id = $this->DB->LastInsertId();
5522  }
5523  }
5524 
5525  # return tag ID to caller
5526  return $Id;
5527  }
5528 
5534  private function GetPageFingerprint($PageName)
5535  {
5536  # only get the environmental fingerprint once so that it is consistent
5537  # between page construction start and end
5538  static $EnvFingerprint;
5539  if (!isset($EnvFingerprint))
5540  {
5541  $EnvData = json_encode($_GET).json_encode($_POST);
5542 
5543  # if alternate domain support is enabled
5544  if ($this->HtaccessSupport() && self::$RootUrlOverride !== NULL)
5545  {
5546  # and if we were accessed via an alternate domain
5547  $VHost = $_SERVER["SERVER_NAME"];
5548  if (isset($this->AlternateDomainPrefixes[$VHost]))
5549  {
5550  # then add the alternate domain that was used to our
5551  # environment data
5552  $EnvData .= $VHost;
5553  }
5554  }
5555 
5556  $EnvFingerprint = md5($EnvData);
5557 
5558  }
5559 
5560  # build page fingerprint and return it to caller
5561  return $PageName."-".$EnvFingerprint;
5562  }
5563 }
UrlFingerprintingEnabled($NewValue=DB_NOVALUE)
Get/set whether URL fingerprinting is enabled.
MaxTasks($NewValue=DB_NOVALUE)
Get/set maximum number of tasks to have running simultaneously.
const LOGLVL_ERROR
ERROR error logging level.
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 in queue.
SuppressHTMLOutput($NewSetting=TRUE)
Suppress loading of HTML files.
AddInterfaceDirectories($Dir, $SearchLast=FALSE, $SkipSlashCheck=FALSE)
Add additional directory(s) to be searched for user interface (HTML/TPL) files.
AddIncludeDirectories($Dir, $SearchLast=FALSE, $SkipSlashCheck=FALSE)
Add additional directory(s) to be searched for user interface include (CSS, JavaScript, common PHP, common HTML, etc) files.
static GetScriptUrl()
Retrieve SCRIPT_URL server value, pulling it from elsewhere if that variable isn&#39;t set...
const LOGLVL_INFO
INFO error logging level.
AddUnbufferedCallback($Callback, $Parameters=array())
Add a callback that will be executed after buffered content has been output and that won&#39;t have its o...
DoNotUrlFingerprint($Pattern)
Specify file or file name pattern to exclude from URL fingerprinting.
QueueUniqueTask($Callback, $Parameters=NULL, $Priority=self::PRIORITY_LOW, $Description="")
Add task to queue if not already in queue or currently running.
AddPrefixForAlternateDomain($Domain, $Prefix)
Add an alternate domain for the site which should map to a path tree under the main site URL...
const LOGLVL_FATAL
FATAL error logging level.
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.
JavascriptMinimizationEnabled($NewValue=DB_NOVALUE)
Get/set whether the application framework will attempt to generate minimized JavaScript.
LogFile($NewValue=NULL)
Get/set log file name.
GetCleanUrl()
Get the clean URL for the current page if one is available.
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.
RequireUIFile($FileName)
Add file to list of required UI files.
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.
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:179
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.
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...
IsStaticOnlyEvent($EventName)
Report whether specified event only allows static callbacks.
IsRegisteredEvent($EventName)
Check if event has been registered (is available to be signaled).
static AddObjectDirectory($Dir, $Prefix="", $ClassPattern=NULL, $ClassReplacement=NULL)
Add directory to be searched for object files when autoloading.
ReleaseLock($LockName)
Release lock with specified name.
ScssSupportEnabled($NewValue=DB_NOVALUE)
Get/set whether SCSS compilation support is enabled.
PageCacheEnabled($NewValue=DB_NOVALUE)
Enable/disable page caching.
SignalEvent($EventName, $Parameters=NULL)
Signal that an event has occured.
static BasePath()
Get current base path (usually the part after the host name).
const LOGLVL_TRACE
TRACE error logging level.
const LOGLVL_WARNING
WARNING error logging level.
const PRIORITY_MEDIUM
Medium (default) priority.
LogError($Level, $Msg)
Write error message to log.
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.
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...
ClearPageCacheForTag($Tag)
Clear all cached pages associated with specified tag.
DoNotCacheCurrentPage()
Prevent the current page from being cached.
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.
AddPageCacheTag($Tag, $Pages=NULL)
Add caching tag for current page or specified pages.
LoggingLevel($NewValue=DB_NOVALUE)
Get/set logging level.
GetPageCacheInfo()
Get page cache information.
PageCacheExpirationPeriod($NewValue=DB_NOVALUE)
Get/set page cache expiration period in seconds.
const ORDER_MIDDLE
Run hooked function after ORDER_FIRST and before ORDER_LAST events.
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.
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:1541
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 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 in queue.
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.
GenerateCompactCss($NewValue=DB_NOVALUE)
Get/set whether generating compact CSS (when compiling SCSS) is enabled.
const FT_JAVASCRIPT
JavaScript file type.
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
Run hooked function first (i.e.
const SQL_DATE_FORMAT
Format to feed to date() to get SQL-compatible date/time string.
Definition: StdLib.php:323
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.
UseMinimizedJavascript($NewValue=DB_NOVALUE)
Get/set whether minimized JavaScript will be searched for and used if found.
TemplateLocationCacheExpirationInterval($NewInterval=DB_NOVALUE)
Get/set UI template location cache expiration period in minutes.
ClearTemplateLocationCache()
Clear template location cache.
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
Run hooked function last (i.e.
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).
TaskExecutionEnabled($NewValue=DB_NOVALUE)
Get/set whether automatic task execution is enabled.
MaxExecutionTime($NewValue=DB_NOVALUE, $Persistent=FALSE)
Get/set maximum PHP execution time.
static GetPhpMemoryLimit()
Get PHP memory limit in bytes.
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.
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.
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...
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.
ObjectLocationCacheExpirationInterval($NewInterval=DB_NOVALUE)
Get/set object file location cache expiration period in minutes.
GetElapsedExecutionTime()
Get time elapsed since constructor was called.
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.
const PRIORITY_BACKGROUND
Lowest priority.
LogMessage($Level, $Msg)
Write status message to log.
const FT_OTHER
File type other than CSS, image, or JavaScript.