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 
14 class ApplicationFramework
15 {
16 
17  # ---- PUBLIC INTERFACE --------------------------------------------------
18  /*@(*/
20 
25  public function __construct()
26  {
27  # save execution start time
28  $this->ExecutionStartTime = microtime(TRUE);
29 
30  # begin/restore PHP session
31  $SessionDomain = isset($_SERVER["SERVER_NAME"]) ? $_SERVER["SERVER_NAME"]
32  : isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"]
33  : php_uname("n");
34  if (is_writable(session_save_path()))
35  {
36  $SessionStorage = session_save_path()
37  ."/".self::$AppName."_".md5($SessionDomain.dirname(__FILE__));
38  if (!is_dir($SessionStorage)) { mkdir($SessionStorage, 0700 ); }
39  if (is_writable($SessionStorage))
40  {
41  # save params of our session storage as instance variables for later use
42  $this->SessionGcProbability =
43  ini_get("session.gc_probability") / ini_get("session.gc_divisor");
44  $this->SessionStorage = $SessionStorage;
45 
46  # store our session files in a subdir to avoid
47  # accidentally sharing sessions with other CWIS installs
48  # on the same domain
49  session_save_path($SessionStorage);
50 
51  # set PHP's gc not to run, as it doesn't handle subdirectories anyway
52  # instead, we'll do the cleanup as we run background tasks
53  ini_set("session.gc_probability", 0);
54  }
55  }
56  ini_set("session.gc_maxlifetime", self::$SessionLifetime);
57  session_set_cookie_params(
58  self::$SessionLifetime, "/", $SessionDomain);
59  if (php_sapi_name() !== "cli")
60  {
61  session_start();
62  }
63 
64  # set up default object file search locations
65  self::AddObjectDirectory("local/interface/%ACTIVEUI%/objects");
66  self::AddObjectDirectory("interface/%ACTIVEUI%/objects");
67  self::AddObjectDirectory("local/interface/%DEFAULTUI%/objects");
68  self::AddObjectDirectory("interface/%DEFAULTUI%/objects");
69  self::AddObjectDirectory("local/objects");
70  self::AddObjectDirectory("objects");
71 
72  # set up object file autoloader
73  spl_autoload_register("ApplicationFramework::AutoloadObjects");
74 
75  # set up function to output any buffered text in case of crash
76  register_shutdown_function(array($this, "OnCrash"));
77 
78  # set up our internal environment
79  $this->DB = new Database();
80 
81  # set up our exception handler
82  set_exception_handler(array($this, "GlobalExceptionHandler"));
83 
84  # perform any work needed to undo PHP magic quotes
85  $this->UndoMagicQuotes();
86 
87  # load our settings from database
88  $this->LoadSettings();
89 
90  # set PHP maximum execution time
91  $this->MaxExecutionTime($this->Settings["MaxExecTime"]);
92 
93  # register events we handle internally
94  $this->RegisterEvent($this->PeriodicEvents);
95  $this->RegisterEvent($this->UIEvents);
96 
97  # attempt to create SCSS cache directory if needed and it does not exist
98  if ($this->ScssSupportEnabled() && !is_dir(self::$ScssCacheDir))
99  { @mkdir(self::$ScssCacheDir, 0777, TRUE); }
100 
101  # attempt to create minimized JS cache directory if needed and it does not exist
102  if ($this->UseMinimizedJavascript()
103  && $this->JavascriptMinimizationEnabled()
104  && !is_dir(self::$JSMinCacheDir))
105  {
106  @mkdir(self::$JSMinCacheDir, 0777, TRUE);
107  }
108  }
115  public function __destruct()
116  {
117  # if template location cache is flagged to be saved
118  if ($this->SaveTemplateLocationCache)
119  {
120  # write template location cache out and update cache expiration
121  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
122  ." SET TemplateLocationCache = '"
123  .addslashes(serialize(
124  $this->TemplateLocationCache))."',"
125  ." TemplateLocationCacheExpiration = '"
126  .date("Y-m-d H:i:s",
127  $this->TemplateLocationCacheExpiration)."'");
128  }
129 
130  # if object location cache is flagged to be saved
131  if (self::$SaveObjectLocationCache)
132  {
133  # write object location cache out and update cache expiration
134  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
135  ." SET ObjectLocationCache = '"
136  .addslashes(serialize(
137  self::$ObjectLocationCache))."',"
138  ." ObjectLocationCacheExpiration = '"
139  .date("Y-m-d H:i:s",
140  self::$ObjectLocationCacheExpiration)."'");
141  }
142  }
150  public function GlobalExceptionHandler($Exception)
151  {
152  # display exception info
153  $Location = str_replace(getcwd()."/", "",
154  $Exception->getFile()."[".$Exception->getLine()."]");
155  ?><table width="100%" cellpadding="5"
156  style="border: 2px solid #666666; background: #CCCCCC;
157  font-family: Courier New, Courier, monospace;
158  margin-top: 10px;"><tr><td>
159  <div style="color: #666666;">
160  <span style="font-size: 150%;">
161  <b>Uncaught Exception</b></span><br />
162  <b>Message:</b> <i><?PHP print $Exception->getMessage(); ?></i><br />
163  <b>Location:</b> <i><?PHP print $Location; ?></i><br />
164  <b>Trace:</b>
165  <blockquote><pre><?PHP print preg_replace(
166  ":(#[0-9]+) ".getcwd()."/".":", "$1 ",
167  $Exception->getTraceAsString());
168  ?></pre></blockquote>
169  </div>
170  </td></tr></table><?PHP
171 
172  # log exception if possible
173  $TraceString = $Exception->getTraceAsString();
174  $TraceString = str_replace("\n", ", ", $TraceString);
175  $TraceString = preg_replace(":(#[0-9]+) ".getcwd()."/".":", "$1 ", $TraceString);
176  $LogMsg = "Uncaught exception (".$Exception->getMessage().") at "
177  .$Location.". TRACE: ".$TraceString." URL: ".$this->FullUrl();
178  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
179  }
196  public static function AddObjectDirectory(
197  $Dir, $Prefix = "", $ClassPattern = NULL, $ClassReplacement = NULL)
198  {
199  # make sure directory has trailing slash
200  $Dir = $Dir.((substr($Dir, -1) != "/") ? "/" : "");
201 
202  # add directory to directory list
203  self::$ObjectDirectories[$Dir] = array(
204  "Prefix" => $Prefix,
205  "ClassPattern" => $ClassPattern,
206  "ClassReplacement" => $ClassReplacement,
207  );
208  }
209 
229  public function AddImageDirectories(
230  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
231  {
232  # add directories to existing image directory list
233  $this->ImageDirList = $this->AddToDirList(
234  $this->ImageDirList, $Dir, $SearchLast, $SkipSlashCheck);
235  }
236 
257  public function AddIncludeDirectories(
258  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
259  {
260  # add directories to existing image directory list
261  $this->IncludeDirList = $this->AddToDirList(
262  $this->IncludeDirList, $Dir, $SearchLast, $SkipSlashCheck);
263  }
264 
284  public function AddInterfaceDirectories(
285  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
286  {
287  # add directories to existing image directory list
288  $this->InterfaceDirList = $this->AddToDirList(
289  $this->InterfaceDirList, $Dir, $SearchLast, $SkipSlashCheck);
290  }
291 
311  public function AddFunctionDirectories(
312  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
313  {
314  # add directories to existing image directory list
315  $this->FunctionDirList = $this->AddToDirList(
316  $this->FunctionDirList, $Dir, $SearchLast, $SkipSlashCheck);
317  }
318 
324  public function SetBrowserDetectionFunc($DetectionFunc)
325  {
326  $this->BrowserDetectFunc = $DetectionFunc;
327  }
328 
335  public function AddUnbufferedCallback($Callback, $Parameters=array())
336  {
337  if (is_callable($Callback))
338  {
339  $this->UnbufferedCallbacks[] = array($Callback, $Parameters);
340  }
341  }
342 
349  public function TemplateLocationCacheExpirationInterval($NewInterval = DB_NOVALUE)
350  {
351  return $this->UpdateSetting("TemplateLocationCacheInterval", $NewInterval);
352  }
353 
357  public function ClearTemplateLocationCache()
358  {
359  $this->TemplateLocationCache = array();
360  $this->SaveTemplateLocationCache = TRUE;
361  }
362 
369  public function ObjectLocationCacheExpirationInterval($NewInterval = DB_NOVALUE)
370  {
371  return $this->UpdateSetting("ObjectLocationCacheInterval", $NewInterval);
372  }
373 
377  public function ClearObjectLocationCache()
378  {
379  self::$ObjectLocationCache = array();
380  self::$SaveObjectLocationCache = TRUE;
381  }
382 
389  public function UrlFingerprintingEnabled($NewValue = DB_NOVALUE)
390  {
391  return $this->UpdateSetting("UrlFingerprintingEnabled", $NewValue);
392  }
393 
401  public function ScssSupportEnabled($NewValue = DB_NOVALUE)
402  {
403  return $this->UpdateSetting("ScssSupportEnabled", $NewValue);
404  }
405 
414  public function GenerateCompactCss($NewValue = DB_NOVALUE)
415  {
416  return $this->UpdateSetting("GenerateCompactCss", $NewValue);
417  }
418 
427  public function UseMinimizedJavascript($NewValue = DB_NOVALUE)
428  {
429  return $this->UpdateSetting("UseMinimizedJavascript", $NewValue);
430  }
431 
440  public function JavascriptMinimizationEnabled($NewValue = DB_NOVALUE)
441  {
442  return $this->UpdateSetting("JavascriptMinimizationEnabled", $NewValue);
443  }
444 
458  public function RecordContextInCaseOfCrash(
459  $BacktraceOptions = 0, $BacktraceLimit = 0)
460  {
461  if (version_compare(PHP_VERSION, "5.4.0", ">="))
462  {
463  $this->SavedContext = debug_backtrace(
464  $BacktraceOptions, $BacktraceLimit);
465  }
466  else
467  {
468  $this->SavedContext = debug_backtrace($BacktraceOptions);
469  }
470  array_shift($this->SavedContext);
471  }
472 
477  public function LoadPage($PageName)
478  {
479  # perform any clean URL rewriting
480  $PageName = $this->RewriteCleanUrls($PageName);
481 
482  # sanitize incoming page name and save local copy
483  $PageName = preg_replace("/[^a-zA-Z0-9_.-]/", "", $PageName);
484  $this->PageName = $PageName;
485 
486  # if page caching is turned on
487  if ($this->PageCacheEnabled())
488  {
489  # if we have a cached page
490  $CachedPage = $this->CheckForCachedPage($PageName);
491  if ($CachedPage !== NULL)
492  {
493  # set header to indicate cache hit was found
494  header("X-ScoutAF-Cache: HIT");
495 
496  # display cached page and exit
497  print $CachedPage;
498  return;
499  }
500  else
501  {
502  # set header to indicate no cache hit was found
503  header("X-ScoutAF-Cache: MISS");
504  }
505  }
506 
507  # buffer any output from includes or PHP file
508  ob_start();
509 
510  # include any files needed to set up execution environment
511  foreach ($this->EnvIncludes as $IncludeFile)
512  {
513  include($IncludeFile);
514  }
515 
516  # signal page load
517  $this->SignalEvent("EVENT_PAGE_LOAD", array("PageName" => $PageName));
518 
519  # signal PHP file load
520  $SignalResult = $this->SignalEvent("EVENT_PHP_FILE_LOAD", array(
521  "PageName" => $PageName));
522 
523  # if signal handler returned new page name value
524  $NewPageName = $PageName;
525  if (($SignalResult["PageName"] != $PageName)
526  && strlen($SignalResult["PageName"]))
527  {
528  # if new page name value is page file
529  if (file_exists($SignalResult["PageName"]))
530  {
531  # use new value for PHP file name
532  $PageFile = $SignalResult["PageName"];
533  }
534  else
535  {
536  # use new value for page name
537  $NewPageName = $SignalResult["PageName"];
538  }
539 
540  # update local copy of page name
541  $this->PageName = $NewPageName;
542  }
543 
544  # if we do not already have a PHP file
545  if (!isset($PageFile))
546  {
547  # look for PHP file for page
548  $OurPageFile = "pages/".$NewPageName.".php";
549  $LocalPageFile = "local/pages/".$NewPageName.".php";
550  $PageFile = file_exists($LocalPageFile) ? $LocalPageFile
551  : (file_exists($OurPageFile) ? $OurPageFile
552  : "pages/".$this->DefaultPage.".php");
553  }
554 
555  # load PHP file
556  include($PageFile);
557 
558  # save buffered output to be displayed later after HTML file loads
559  $PageOutput = ob_get_contents();
560  ob_end_clean();
561 
562  # signal PHP file load is complete
563  ob_start();
564  $Context["Variables"] = get_defined_vars();
565  $this->SignalEvent("EVENT_PHP_FILE_LOAD_COMPLETE",
566  array("PageName" => $PageName, "Context" => $Context));
567  $PageCompleteOutput = ob_get_contents();
568  ob_end_clean();
569 
570  # set up for possible TSR (Terminate and Stay Resident :))
571  $ShouldTSR = $this->PrepForTSR();
572 
573  # if PHP file indicated we should autorefresh to somewhere else
574  if (($this->JumpToPage) && ($this->JumpToPageDelay == 0))
575  {
576  if (!strlen(trim($PageOutput)))
577  {
578  # if client supports HTTP/1.1, use a 303 as it is most accurate
579  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.1")
580  {
581  header("HTTP/1.1 303 See Other");
582  header("Location: ".$this->JumpToPage);
583  }
584  else
585  {
586  # if the request was an HTTP/1.0 GET or HEAD, then
587  # use a 302 response code.
588 
589  # NB: both RFC 2616 (HTTP/1.1) and RFC1945 (HTTP/1.0)
590  # explicitly prohibit automatic redirection via a 302
591  # if the request was not GET or HEAD.
592  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.0" &&
593  ($_SERVER["REQUEST_METHOD"] == "GET" ||
594  $_SERVER["REQUEST_METHOD"] == "HEAD") )
595  {
596  header("HTTP/1.0 302 Found");
597  header("Location: ".$this->JumpToPage);
598  }
599 
600  # otherwise, fall back to a meta refresh
601  else
602  {
603  print '<html><head><meta http-equiv="refresh" '
604  .'content="0; URL='.$this->JumpToPage.'">'
605  .'</head><body></body></html>';
606  }
607  }
608  }
609  }
610  # else if HTML loading is not suppressed
611  elseif (!$this->SuppressHTML)
612  {
613  # set content-type to get rid of diacritic errors
614  header("Content-Type: text/html; charset="
615  .$this->HtmlCharset, TRUE);
616 
617  # load common HTML file (defines common functions) if available
618  $CommonHtmlFile = $this->FindFile($this->IncludeDirList,
619  "Common", array("tpl", "html"));
620  if ($CommonHtmlFile) { include($CommonHtmlFile); }
621 
622  # load UI functions
623  $this->LoadUIFunctions();
624 
625  # begin buffering content
626  ob_start();
627 
628  # signal HTML file load
629  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD", array(
630  "PageName" => $PageName));
631 
632  # if signal handler returned new page name value
633  $NewPageName = $PageName;
634  $PageContentFile = NULL;
635  if (($SignalResult["PageName"] != $PageName)
636  && strlen($SignalResult["PageName"]))
637  {
638  # if new page name value is HTML file
639  if (file_exists($SignalResult["PageName"]))
640  {
641  # use new value for HTML file name
642  $PageContentFile = $SignalResult["PageName"];
643  }
644  else
645  {
646  # use new value for page name
647  $NewPageName = $SignalResult["PageName"];
648  }
649  }
650 
651  # load page content HTML file if available
652  if ($PageContentFile === NULL)
653  {
654  $PageContentFile = $this->FindFile(
655  $this->InterfaceDirList, $NewPageName,
656  array("tpl", "html"));
657  }
658  if ($PageContentFile)
659  {
660  include($PageContentFile);
661  }
662  else
663  {
664  print "<h2>ERROR: No HTML/TPL template found"
665  ." for this page (".$NewPageName.").</h2>";
666  }
667 
668  # signal HTML file load complete
669  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD_COMPLETE");
670 
671  # stop buffering and save output
672  $PageContentOutput = ob_get_contents();
673  ob_end_clean();
674 
675  # load page start HTML file if available
676  ob_start();
677  $PageStartFile = $this->FindFile($this->IncludeDirList, "Start",
678  array("tpl", "html"), array("StdPage", "StandardPage"));
679  if ($PageStartFile) { include($PageStartFile); }
680  $PageStartOutput = ob_get_contents();
681  ob_end_clean();
682 
683  # if page auto-refresh requested
684  if ($this->JumpToPage)
685  {
686  $RefreshLine = '<meta http-equiv="refresh" content="'
687  .$this->JumpToPageDelay.'; url='.$this->JumpToPage.'">';
688  $PageStartOutput = str_replace("<head>", "<head>\n".$RefreshLine,
689  $PageStartOutput);
690  }
691 
692  # load page end HTML file if available
693  ob_start();
694  $PageEndFile = $this->FindFile($this->IncludeDirList, "End",
695  array("tpl", "html"), array("StdPage", "StandardPage"));
696  if ($PageEndFile) { include($PageEndFile); }
697  $PageEndOutput = ob_get_contents();
698  ob_end_clean();
699 
700  # get list of any required files not loaded
701  $RequiredFiles = $this->GetRequiredFilesNotYetLoaded($PageContentFile);
702 
703  # if a browser detection function has been made available
704  if (is_callable($this->BrowserDetectFunc))
705  {
706  # call function to get browser list
707  $Browsers = call_user_func($this->BrowserDetectFunc);
708 
709  # for each required file
710  $NewRequiredFiles = array();
711  foreach ($RequiredFiles as $File)
712  {
713  # if file name includes browser keyword
714  if (preg_match("/%BROWSER%/", $File))
715  {
716  # for each browser
717  foreach ($Browsers as $Browser)
718  {
719  # substitute in browser name and add to new file list
720  $NewRequiredFiles[] = preg_replace(
721  "/%BROWSER%/", $Browser, $File);
722  }
723  }
724  else
725  {
726  # add to new file list
727  $NewRequiredFiles[] = $File;
728  }
729  }
730  $RequiredFiles = $NewRequiredFiles;
731  }
732  else
733  {
734  # filter out any files with browser keyword in their name
735  $NewRequiredFiles = array();
736  foreach ($RequiredFiles as $File)
737  {
738  if (!preg_match("/%BROWSER%/", $File))
739  {
740  $NewRequiredFiles[] = $File;
741  }
742  }
743  $RequiredFiles = $NewRequiredFiles;
744  }
745 
746  # for each required file
747  foreach ($RequiredFiles as $File)
748  {
749  # locate specific file to use
750  $FilePath = $this->GUIFile($File);
751 
752  # if file was found
753  if ($FilePath)
754  {
755  # generate tag for file
756  $Tag = $this->GetUIFileLoadingTag($FilePath);
757 
758  # add file to HTML output based on file type
759  $FileType = $this->GetFileType($FilePath);
760  switch ($FileType)
761  {
762  case self::FT_CSS:
763  $PageStartOutput = preg_replace(
764  "#</head>#i", $Tag."\n</head>", $PageStartOutput, 1);
765  break;
766 
767  case self::FT_JAVASCRIPT:
768  $PageEndOutput = preg_replace(
769  "#</body>#i", $Tag."\n</body>", $PageEndOutput, 1);
770  break;
771  }
772  }
773  }
774 
775  # assemble full page
776  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
777 
778  # perform any regular expression replacements in output
779  $NewFullPageOutput = preg_replace($this->OutputModificationPatterns,
780  $this->OutputModificationReplacements, $FullPageOutput);
781 
782  # check to make sure replacements didn't fail
783  $FullPageOutput = $this->CheckOutputModification(
784  $FullPageOutput, $NewFullPageOutput,
785  "regular expression replacements");
786 
787  # for each registered output modification callback
788  foreach ($this->OutputModificationCallbacks as $Info)
789  {
790  # set up data for callback
791  $this->OutputModificationCallbackInfo = $Info;
792 
793  # perform output modification
794  $NewFullPageOutput = preg_replace_callback($Info["SearchPattern"],
795  array($this, "OutputModificationCallbackShell"),
796  $FullPageOutput);
797 
798  # check to make sure modification didn't fail
799  $ErrorInfo = "callback info: ".print_r($Info, TRUE);
800  $FullPageOutput = $this->CheckOutputModification(
801  $FullPageOutput, $NewFullPageOutput, $ErrorInfo);
802  }
803 
804  # provide the opportunity to modify full page output
805  $SignalResult = $this->SignalEvent("EVENT_PAGE_OUTPUT_FILTER", array(
806  "PageOutput" => $FullPageOutput));
807  if (isset($SignalResult["PageOutput"])
808  && strlen(trim($SignalResult["PageOutput"])))
809  {
810  $FullPageOutput = $SignalResult["PageOutput"];
811  }
812 
813  # if relative paths may not work because we were invoked via clean URL
814  if ($this->CleanUrlRewritePerformed || self::WasUrlRewritten())
815  {
816  # if using the <base> tag is okay
817  $BaseUrl = $this->BaseUrl();
818  if ($this->UseBaseTag)
819  {
820  # add <base> tag to header
821  $PageStartOutput = str_replace("<head>",
822  "<head><base href=\"".$BaseUrl."\" />",
823  $PageStartOutput);
824 
825  # re-assemble full page with new header
826  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
827 
828  # the absolute URL to the current page
829  $FullUrl = $BaseUrl . $this->GetPageLocation();
830 
831  # make HREF attribute values with just a fragment ID
832  # absolute since they don't work with the <base> tag because
833  # they are relative to the current page/URL, not the site
834  # root
835  $NewFullPageOutput = preg_replace(
836  array("%href=\"(#[^:\" ]+)\"%i", "%href='(#[^:' ]+)'%i"),
837  array("href=\"".$FullUrl."$1\"", "href='".$FullUrl."$1'"),
838  $FullPageOutput);
839 
840  # check to make sure HREF cleanup didn't fail
841  $FullPageOutput = $this->CheckOutputModification(
842  $FullPageOutput, $NewFullPageOutput,
843  "HREF cleanup");
844  }
845  else
846  {
847  # try to fix any relative paths throughout code
848  $RelativePathPatterns = array(
849  "%src=\"/?([^?*:;{}\\\\\" ]+)\.(js|css|gif|png|jpg)\"%i",
850  "%src='/?([^?*:;{}\\\\' ]+)\.(js|css|gif|png|jpg)'%i",
851  # don't rewrite HREF attributes that are just
852  # fragment IDs because they are relative to the
853  # current page/URL, not the site root
854  "%href=\"/?([^#][^:\" ]*)\"%i",
855  "%href='/?([^#][^:' ]*)'%i",
856  "%action=\"/?([^#][^:\" ]*)\"%i",
857  "%action='/?([^#][^:' ]*)'%i",
858  "%@import\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
859  "%@import\s+url\('/?([^:\" ]+)'\s*\)%i",
860  "%src:\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
861  "%src:\s+url\('/?([^:\" ]+)'\s*\)%i",
862  "%@import\s+\"/?([^:\" ]+)\"\s*%i",
863  "%@import\s+'/?([^:\" ]+)'\s*%i",
864  );
865  $RelativePathReplacements = array(
866  "src=\"".$BaseUrl."$1.$2\"",
867  "src=\"".$BaseUrl."$1.$2\"",
868  "href=\"".$BaseUrl."$1\"",
869  "href=\"".$BaseUrl."$1\"",
870  "action=\"".$BaseUrl."$1\"",
871  "action=\"".$BaseUrl."$1\"",
872  "@import url(\"".$BaseUrl."$1\")",
873  "@import url('".$BaseUrl."$1')",
874  "src: url(\"".$BaseUrl."$1\")",
875  "src: url('".$BaseUrl."$1')",
876  "@import \"".$BaseUrl."$1\"",
877  "@import '".$BaseUrl."$1'",
878  );
879  $NewFullPageOutput = preg_replace($RelativePathPatterns,
880  $RelativePathReplacements, $FullPageOutput);
881 
882  # check to make sure relative path fixes didn't fail
883  $FullPageOutput = $this->CheckOutputModification(
884  $FullPageOutput, $NewFullPageOutput,
885  "relative path fixes");
886  }
887  }
888 
889  # handle any necessary alternate domain rewriting
890  $FullPageOutput = $this->RewriteAlternateDomainUrls($FullPageOutput);
891 
892  # update page cache for this page
893  $this->UpdatePageCache($PageName, $FullPageOutput);
894 
895  # write out full page
896  print $FullPageOutput;
897  }
898 
899  # run any post-processing routines
900  foreach ($this->PostProcessingFuncs as $Func)
901  {
902  call_user_func_array($Func["FunctionName"], $Func["Arguments"]);
903  }
904 
905  # write out any output buffered from page code execution
906  if (strlen($PageOutput))
907  {
908  if (!$this->SuppressHTML)
909  {
910  ?><table width="100%" cellpadding="5"
911  style="border: 2px solid #666666; background: #CCCCCC;
912  font-family: Courier New, Courier, monospace;
913  margin-top: 10px;"><tr><td><?PHP
914  }
915  if ($this->JumpToPage)
916  {
917  ?><div style="color: #666666;"><span style="font-size: 150%;">
918  <b>Page Jump Aborted</b></span>
919  (because of error or other unexpected output)<br />
920  <b>Jump Target:</b>
921  <i><?PHP print($this->JumpToPage); ?></i></div><?PHP
922  }
923  print $PageOutput;
924  if (!$this->SuppressHTML)
925  {
926  ?></td></tr></table><?PHP
927  }
928  }
929 
930  # write out any output buffered from the page code execution complete signal
931  if (!$this->JumpToPage && !$this->SuppressHTML && strlen($PageCompleteOutput))
932  {
933  print $PageCompleteOutput;
934  }
935 
936  # log slow page loads
937  if ($this->LogSlowPageLoads()
938  && !$this->DoNotLogSlowPageLoad
939  && ($this->GetElapsedExecutionTime()
940  >= ($this->SlowPageLoadThreshold())))
941  {
942  $RemoteHost = gethostbyaddr($_SERVER["REMOTE_ADDR"]);
943  if ($RemoteHost === FALSE)
944  {
945  $RemoteHost = $_SERVER["REMOTE_ADDR"];
946  }
947  elseif ($RemoteHost != $_SERVER["REMOTE_ADDR"])
948  {
949  $RemoteHost .= " (".$_SERVER["REMOTE_ADDR"].")";
950  }
951  $SlowPageLoadMsg = "Slow page load ("
952  .intval($this->GetElapsedExecutionTime())."s) for "
953  .$this->FullUrl()." from ".$RemoteHost;
954  $this->LogMessage(self::LOGLVL_INFO, $SlowPageLoadMsg);
955  }
956 
957  # execute callbacks that should not have their output buffered
958  foreach ($this->UnbufferedCallbacks as $Callback)
959  {
960  call_user_func_array($Callback[0], $Callback[1]);
961  }
962 
963  # log high memory usage
964  if (function_exists("memory_get_peak_usage"))
965  {
966  $MemoryThreshold = ($this->HighMemoryUsageThreshold()
967  * $this->GetPhpMemoryLimit()) / 100;
968  if ($this->LogHighMemoryUsage()
969  && (memory_get_peak_usage() >= $MemoryThreshold))
970  {
971  $HighMemUsageMsg = "High peak memory usage ("
972  .intval(memory_get_peak_usage()).") for "
973  .$this->FullUrl()." from "
974  .$_SERVER["REMOTE_ADDR"];
975  $this->LogMessage(self::LOGLVL_INFO, $HighMemUsageMsg);
976  }
977  }
978 
979  # terminate and stay resident (TSR!) if indicated and HTML has been output
980  # (only TSR if HTML has been output because otherwise browsers will misbehave)
981  if ($ShouldTSR) { $this->LaunchTSR(); }
982  }
983 
989  public function GetPageName()
990  {
991  return $this->PageName;
992  }
993 
999  public function GetPageLocation()
1000  {
1001  # retrieve current URL
1002  $Url = $this->GetScriptUrl();
1003 
1004  # remove the base path if present
1005  $BasePath = $this->Settings["BasePath"];
1006  if (stripos($Url, $BasePath) === 0)
1007  {
1008  $Url = substr($Url, strlen($BasePath));
1009  }
1010 
1011  # if we're being accessed via an alternate domain,
1012  # add the appropriate prefix in
1013  if ($this->HtaccessSupport() &&
1014  self::$RootUrlOverride !== NULL)
1015  {
1016  $VHost = $_SERVER["SERVER_NAME"];
1017  if (isset($this->AlternateDomainPrefixes[$VHost]))
1018  {
1019  $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
1020  $Url = $ThisPrefix."/".$Url;
1021  }
1022  }
1023 
1024  return $Url;
1025  }
1026 
1032  public function GetPageUrl()
1033  {
1034  return self::BaseUrl() . $this->GetPageLocation();
1035  }
1036 
1048  public function SetJumpToPage($Page, $Delay = 0, $IsLiteral = FALSE)
1049  {
1050  if (!is_null($Page)
1051  && (!$IsLiteral)
1052  && (strpos($Page, "?") === FALSE)
1053  && ((strpos($Page, "=") !== FALSE)
1054  || ((stripos($Page, ".php") === FALSE)
1055  && (stripos($Page, ".htm") === FALSE)
1056  && (strpos($Page, "/") === FALSE)))
1057  && (stripos($Page, "http://") !== 0)
1058  && (stripos($Page, "https://") !== 0))
1059  {
1060  $this->JumpToPage = self::BaseUrl() . "index.php?P=".$Page;
1061  }
1062  else
1063  {
1064  $this->JumpToPage = $Page;
1065  }
1066  $this->JumpToPageDelay = $Delay;
1067  }
1068 
1073  public function JumpToPageIsSet()
1074  {
1075  return ($this->JumpToPage === NULL) ? FALSE : TRUE;
1076  }
1077 
1087  public function HtmlCharset($NewSetting = NULL)
1088  {
1089  if ($NewSetting !== NULL) { $this->HtmlCharset = $NewSetting; }
1090  return $this->HtmlCharset;
1091  }
1092 
1102  public function DoNotMinimizeFile($File)
1103  {
1104  if (!is_array($File)) { $File = array($File); }
1105  $this->DoNotMinimizeList = array_merge($this->DoNotMinimizeList, $File);
1106  }
1107 
1118  public function UseBaseTag($NewValue = NULL)
1119  {
1120  if ($NewValue !== NULL) { $this->UseBaseTag = $NewValue ? TRUE : FALSE; }
1121  return $this->UseBaseTag;
1122  }
1123 
1130  public function SuppressHTMLOutput($NewSetting = TRUE)
1131  {
1132  $this->SuppressHTML = $NewSetting;
1133  }
1134 
1140  public static function DefaultUserInterface($UIName = NULL)
1141  {
1142  if ($UIName !== NULL)
1143  {
1144  self::$DefaultUI = $UIName;
1145  }
1146  return self::$DefaultUI;
1147  }
1148 
1155  public static function ActiveUserInterface($UIName = NULL)
1156  {
1157  if ($UIName !== NULL)
1158  {
1159  self::$ActiveUI = preg_replace("/^SPTUI--/", "", $UIName);
1160  }
1161  return self::$ActiveUI;
1162  }
1163 
1174  public function GetUserInterfaces($FilterExp = NULL)
1175  {
1176  static $Interfaces;
1177 
1178  if (!isset($Interfaces[$FilterExp]))
1179  {
1180  # retrieve paths to user interface directories
1181  $Paths = $this->GetUserInterfacePaths($FilterExp);
1182 
1183  # start out with an empty list
1184  $Interfaces[$FilterExp] = array();
1185 
1186  # for each possible UI directory
1187  foreach ($Paths as $CanonicalName => $Path)
1188  {
1189  # if name file available
1190  $LabelFile = $Path."/NAME";
1191  if (is_readable($LabelFile))
1192  {
1193  # read the UI name
1194  $Label = file_get_contents($LabelFile);
1195 
1196  # if the UI name looks reasonable
1197  if (strlen(trim($Label)))
1198  {
1199  # use read name
1200  $Interfaces[$FilterExp][$CanonicalName] = $Label;
1201  }
1202  }
1203 
1204  # if we do not have a name yet
1205  if (!isset($Interfaces[$FilterExp][$CanonicalName]))
1206  {
1207  # use base directory for name
1208  $Interfaces[$FilterExp][$CanonicalName] = basename($Path);
1209  }
1210  }
1211  }
1212 
1213  # return list to caller
1214  return $Interfaces[$FilterExp];
1215  }
1216 
1225  public function GetUserInterfacePaths($FilterExp = NULL)
1226  {
1227  static $InterfacePaths;
1228 
1229  if (!isset($InterfacePaths[$FilterExp]))
1230  {
1231  # extract possible UI directories from interface directory list
1232  $InterfaceDirs = array();
1233  foreach ($this->InterfaceDirList as $Dir)
1234  {
1235  $Matches = array();
1236  if (preg_match("#([a-zA-Z0-9/]*interface)/[a-zA-Z0-9%/]*#",
1237  $Dir, $Matches))
1238  {
1239  $Dir = $Matches[1];
1240  if (!in_array($Dir, $InterfaceDirs))
1241  {
1242  $InterfaceDirs[] = $Dir;
1243  }
1244  }
1245  }
1246 
1247  # reverse order of interface directories so that the directory
1248  # returned is the base directory for the interface
1249  $InterfaceDirs = array_reverse($InterfaceDirs);
1250 
1251  # start out with an empty list
1252  $InterfacePaths[$FilterExp] = array();
1253  $InterfacesFound = array();
1254 
1255  # for each possible UI directory
1256  foreach ($InterfaceDirs as $InterfaceDir)
1257  {
1258  $Dir = dir($InterfaceDir);
1259 
1260  # for each file in current directory
1261  while (($DirEntry = $Dir->read()) !== FALSE)
1262  {
1263  $InterfacePath = $InterfaceDir."/".$DirEntry;
1264 
1265  # skip anything we have already found
1266  # or that doesn't have a name in the required format
1267  # or that isn't a directory
1268  # or that doesn't match the filter regex (if supplied)
1269  if (in_array($DirEntry, $InterfacesFound)
1270  || !preg_match('/^[a-zA-Z0-9]+$/', $DirEntry)
1271  || !is_dir($InterfacePath)
1272  || (($FilterExp !== NULL)
1273  && !preg_match($FilterExp, $InterfacePath)))
1274  {
1275  continue;
1276  }
1277 
1278  # add interface to list
1279  $InterfacePaths[$FilterExp][$DirEntry] = $InterfacePath;
1280  $InterfacesFound[] = $DirEntry;
1281  }
1282 
1283  $Dir->close();
1284  }
1285  }
1286 
1287  # return list to caller
1288  return $InterfacePaths[$FilterExp];
1289  }
1290 
1315  public function AddPostProcessingCall($FunctionName,
1316  &$Arg1 = self::NOVALUE, &$Arg2 = self::NOVALUE, &$Arg3 = self::NOVALUE,
1317  &$Arg4 = self::NOVALUE, &$Arg5 = self::NOVALUE, &$Arg6 = self::NOVALUE,
1318  &$Arg7 = self::NOVALUE, &$Arg8 = self::NOVALUE, &$Arg9 = self::NOVALUE)
1319  {
1320  $FuncIndex = count($this->PostProcessingFuncs);
1321  $this->PostProcessingFuncs[$FuncIndex]["FunctionName"] = $FunctionName;
1322  $this->PostProcessingFuncs[$FuncIndex]["Arguments"] = array();
1323  $Index = 1;
1324  while (isset(${"Arg".$Index}) && (${"Arg".$Index} !== self::NOVALUE))
1325  {
1326  $this->PostProcessingFuncs[$FuncIndex]["Arguments"][$Index]
1327  =& ${"Arg".$Index};
1328  $Index++;
1329  }
1330  }
1331 
1337  public function AddEnvInclude($FileName)
1338  {
1339  $this->EnvIncludes[] = $FileName;
1340  }
1341 
1348  public function GUIFile($FileName)
1349  {
1350  # determine which location to search based on file suffix
1351  $FileType = $this->GetFileType($FileName);
1352  $DirList = ($FileType == self::FT_IMAGE)
1353  ? $this->ImageDirList : $this->IncludeDirList;
1354 
1355  # if directed to use minimized JavaScript file
1356  if (($FileType == self::FT_JAVASCRIPT) && $this->UseMinimizedJavascript())
1357  {
1358  # look for minimized version of file
1359  $MinimizedFileName = substr_replace($FileName, ".min", -3, 0);
1360  $FoundFileName = $this->FindFile($DirList, $MinimizedFileName);
1361 
1362  # if minimized file was not found
1363  if (is_null($FoundFileName))
1364  {
1365  # look for unminimized file
1366  $FoundFileName = $this->FindFile($DirList, $FileName);
1367 
1368  # if unminimized file found
1369  if (!is_null($FoundFileName))
1370  {
1371  # if minimization enabled and supported
1372  if ($this->JavascriptMinimizationEnabled()
1373  && self::JsMinRewriteSupport())
1374  {
1375  # attempt to create minimized file
1376  $MinFileName = $this->MinimizeJavascriptFile(
1377  $FoundFileName);
1378 
1379  # if minimization succeeded
1380  if ($MinFileName !== NULL)
1381  {
1382  # use minimized version
1383  $FoundFileName = $MinFileName;
1384 
1385  # save file modification time if needed for fingerprinting
1386  if ($this->UrlFingerprintingEnabled())
1387  {
1388  $FileMTime = filemtime($FoundFileName);
1389  }
1390 
1391  # strip off the cache location, allowing .htaccess
1392  # to handle that for us
1393  $FoundFileName = str_replace(
1394  self::$JSMinCacheDir."/", "", $FoundFileName);
1395  }
1396  }
1397  }
1398  }
1399  }
1400  # else if directed to use SCSS files
1401  elseif (($FileType == self::FT_CSS) && $this->ScssSupportEnabled())
1402  {
1403  # look for SCSS version of file
1404  $SourceFileName = preg_replace("/.css$/", ".scss", $FileName);
1405  $FoundSourceFileName = $this->FindFile($DirList, $SourceFileName);
1406 
1407  # if SCSS file not found
1408  if ($FoundSourceFileName === NULL)
1409  {
1410  # look for CSS file
1411  $FoundFileName = $this->FindFile($DirList, $FileName);
1412  }
1413  else
1414  {
1415  # compile SCSS file (if updated) and return resulting CSS file
1416  $FoundFileName = $this->CompileScssFile($FoundSourceFileName);
1417 
1418  # save file modification time if needed for fingerprinting
1419  if ($this->UrlFingerprintingEnabled())
1420  {
1421  $FileMTime = filemtime($FoundFileName);
1422  }
1423 
1424  # strip off the cache location, allowing .htaccess to handle that for us
1425  if (self::ScssRewriteSupport())
1426  {
1427  $FoundFileName = str_replace(
1428  self::$ScssCacheDir."/", "", $FoundFileName);
1429  }
1430  }
1431  }
1432  # otherwise just search for the file
1433  else
1434  {
1435  $FoundFileName = $this->FindFile($DirList, $FileName);
1436  }
1437 
1438  # add non-image files to list of found files (used for required files loading)
1439  if ($FileType != self::FT_IMAGE)
1440  { $this->FoundUIFiles[] = basename($FoundFileName); }
1441 
1442  # if UI file fingerprinting is enabled and supported
1443  if ($this->UrlFingerprintingEnabled()
1444  && self::UrlFingerprintingRewriteSupport()
1445  && (isset($FileMTime) || file_exists($FoundFileName)))
1446  {
1447  # if file does not appear to be a server-side inclusion
1448  if (!preg_match('/\.(html|php)$/i', $FoundFileName))
1449  {
1450  # for each URL fingerprinting blacklist entry
1451  $OnBlacklist = FALSE;
1452  foreach ($this->UrlFingerprintBlacklist as $BlacklistEntry)
1453  {
1454  # if entry looks like a regular expression pattern
1455  if ($BlacklistEntry[0] == substr($BlacklistEntry, -1))
1456  {
1457  # check file name against regular expression
1458  if (preg_match($BlacklistEntry, $FoundFileName))
1459  {
1460  $OnBlacklist = TRUE;
1461  break;
1462  }
1463  }
1464  else
1465  {
1466  # check file name directly against entry
1467  if (basename($FoundFileName) == $BlacklistEntry)
1468  {
1469  $OnBlacklist = TRUE;
1470  break;
1471  }
1472  }
1473  }
1474 
1475  # if file was not on blacklist
1476  if (!$OnBlacklist)
1477  {
1478  # get file modification time if not already retrieved
1479  if (!isset($FileMTime))
1480  {
1481  $FileMTime = filemtime($FoundFileName);
1482  }
1483 
1484  # add timestamp fingerprint to file name
1485  $Fingerprint = sprintf("%06X",
1486  ($FileMTime % 0xFFFFFF));
1487  $FoundFileName = preg_replace("/^(.+)\.([a-z]+)$/",
1488  "$1.".$Fingerprint.".$2",
1489  $FoundFileName);
1490  }
1491  }
1492  }
1493 
1494  # return file name to caller
1495  return $FoundFileName;
1496  }
1497 
1506  public function PUIFile($FileName)
1507  {
1508  $FullFileName = $this->GUIFile($FileName);
1509  if ($FullFileName) { print($FullFileName); }
1510  }
1511 
1526  public function IncludeUIFile($FileNames, $AdditionalAttributes = NULL)
1527  {
1528  # convert file name to array if necessary
1529  if (!is_array($FileNames)) { $FileNames = array($FileNames); }
1530 
1531  # pad additional attributes if supplied
1532  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
1533 
1534  # for each file
1535  foreach ($FileNames as $BaseFileName)
1536  {
1537  # retrieve full file name
1538  $FileName = $this->GUIFile($BaseFileName);
1539 
1540  # if file was found
1541  if ($FileName)
1542  {
1543  # print appropriate tag
1544  print $this->GetUIFileLoadingTag(
1545  $FileName, $AdditionalAttributes);
1546  }
1547 
1548  # if we are not already loading an override file
1549  if (!preg_match("/-Override.(css|scss|js)$/", $BaseFileName))
1550  {
1551  # attempt to load override file if available
1552  $FileType = $this->GetFileType($BaseFileName);
1553  switch ($FileType)
1554  {
1555  case self::FT_CSS:
1556  $OverrideFileName = preg_replace(
1557  "/\.(css|scss)$/", "-Override.$1",
1558  $BaseFileName);
1559  $this->IncludeUIFile($OverrideFileName,
1560  $AdditionalAttributes);
1561  break;
1562 
1563  case self::FT_JAVASCRIPT:
1564  $OverrideFileName = preg_replace(
1565  "/\.js$/", "-Override.js",
1566  $BaseFileName);
1567  $this->IncludeUIFile($OverrideFileName,
1568  $AdditionalAttributes);
1569  break;
1570  }
1571  }
1572  }
1573  }
1574 
1581  public function DoNotUrlFingerprint($Pattern)
1582  {
1583  $this->UrlFingerprintBlacklist[] = $Pattern;
1584  }
1585 
1593  public function RequireUIFile($FileName)
1594  {
1595  $this->AdditionalRequiredUIFiles[] = $FileName;
1596  }
1597 
1603  public static function GetFileType($FileName)
1604  {
1605  static $FileTypeCache;
1606  if (isset($FileTypeCache[$FileName]))
1607  {
1608  return $FileTypeCache[$FileName];
1609  }
1610 
1611  $FileSuffix = strtolower(substr($FileName, -3));
1612  if ($FileSuffix == "css")
1613  {
1614  $FileTypeCache[$FileName] = self::FT_CSS;
1615  }
1616  elseif ($FileSuffix == ".js")
1617  {
1618  $FileTypeCache[$FileName] = self::FT_JAVASCRIPT;
1619  }
1620  elseif (($FileSuffix == "gif")
1621  || ($FileSuffix == "jpg")
1622  || ($FileSuffix == "png"))
1623  {
1624  $FileTypeCache[$FileName] = self::FT_IMAGE;
1625  }
1626  else
1627  {
1628  $FileTypeCache[$FileName] = self::FT_OTHER;
1629  }
1630 
1631  return $FileTypeCache[$FileName];
1632  }
1634  const FT_OTHER = 0;
1636  const FT_CSS = 1;
1638  const FT_IMAGE = 2;
1640  const FT_JAVASCRIPT = 3;
1641 
1650  public function LoadFunction($Callback)
1651  {
1652  # if specified function is not currently available
1653  if (!is_callable($Callback))
1654  {
1655  # if function info looks legal
1656  if (is_string($Callback) && strlen($Callback))
1657  {
1658  # start with function directory list
1659  $Locations = $this->FunctionDirList;
1660 
1661  # add object directories to list
1662  $Locations = array_merge(
1663  $Locations, array_keys(self::$ObjectDirectories));
1664 
1665  # look for function file
1666  $FunctionFileName = $this->FindFile($Locations, "F-".$Callback,
1667  array("php", "html"));
1668 
1669  # if function file was found
1670  if ($FunctionFileName)
1671  {
1672  # load function file
1673  include_once($FunctionFileName);
1674  }
1675  else
1676  {
1677  # log error indicating function load failed
1678  $this->LogError(self::LOGLVL_ERROR, "Unable to load function"
1679  ." for callback \"".$Callback."\".");
1680  }
1681  }
1682  else
1683  {
1684  # log error indicating specified function info was bad
1685  $this->LogError(self::LOGLVL_ERROR, "Unloadable callback value"
1686  ." (".$Callback.")"
1687  ." passed to AF::LoadFunction() by "
1688  .StdLib::GetMyCaller().".");
1689  }
1690  }
1691 
1692  # report to caller whether function load succeeded
1693  return is_callable($Callback);
1694  }
1695 
1700  public function GetElapsedExecutionTime()
1701  {
1702  return microtime(TRUE) - $this->ExecutionStartTime;
1703  }
1704 
1709  public function GetSecondsBeforeTimeout()
1710  {
1711  return ini_get("max_execution_time") - $this->GetElapsedExecutionTime();
1712  }
1713 
1714  /*@)*/ /* Application Framework */
1715 
1716 
1717  # ---- Page Caching ------------------------------------------------------
1718  /*@(*/
1720 
1727  public function PageCacheEnabled($NewValue = DB_NOVALUE)
1728  {
1729  return $this->UpdateSetting("PageCacheEnabled", $NewValue);
1730  }
1731 
1738  public function PageCacheExpirationPeriod($NewValue = DB_NOVALUE)
1739  {
1740  return $this->UpdateSetting("PageCacheExpirationPeriod", $NewValue);
1741  }
1742 
1747  public function DoNotCacheCurrentPage()
1748  {
1749  $this->CacheCurrentPage = FALSE;
1750  }
1751 
1758  public function AddPageCacheTag($Tag, $Pages = NULL)
1759  {
1760  # normalize tag
1761  $Tag = strtolower($Tag);
1762 
1763  # if pages were supplied
1764  if ($Pages !== NULL)
1765  {
1766  # add pages to list for this tag
1767  if (isset($this->PageCacheTags[$Tag]))
1768  {
1769  $this->PageCacheTags[$Tag] = array_merge(
1770  $this->PageCacheTags[$Tag], $Pages);
1771  }
1772  else
1773  {
1774  $this->PageCacheTags[$Tag] = $Pages;
1775  }
1776  }
1777  else
1778  {
1779  # add current page to list for this tag
1780  $this->PageCacheTags[$Tag][] = "CURRENT";
1781  }
1782  }
1783 
1789  public function ClearPageCacheForTag($Tag)
1790  {
1791  # get tag ID
1792  $TagId = $this->GetPageCacheTagId($Tag);
1793 
1794  # delete pages and tag/page connections for specified tag
1795  $this->DB->Query("DELETE CP, CPTI"
1796  ." FROM AF_CachedPages CP, AF_CachedPageTagInts CPTI"
1797  ." WHERE CPTI.TagId = ".intval($TagId)
1798  ." AND CP.CacheId = CPTI.CacheId");
1799  }
1800 
1804  public function ClearPageCache()
1805  {
1806  # clear all page cache tables
1807  $this->DB->Query("TRUNCATE TABLE AF_CachedPages");
1808  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTags");
1809  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTagInts");
1810  }
1811 
1818  public function GetPageCacheInfo()
1819  {
1820  $Length = $this->DB->Query("SELECT COUNT(*) AS CacheLen"
1821  ." FROM AF_CachedPages", "CacheLen");
1822  $Oldest = $this->DB->Query("SELECT CachedAt FROM AF_CachedPages"
1823  ." ORDER BY CachedAt ASC LIMIT 1", "CachedAt");
1824  return array(
1825  "NumberOfEntries" => $Length,
1826  "OldestTimestamp" => strtotime($Oldest),
1827  );
1828  }
1829 
1830  /*@)*/ /* Page Caching */
1831 
1832 
1833  # ---- Logging -----------------------------------------------------------
1834  /*@(*/
1836 
1850  public function LogSlowPageLoads(
1851  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1852  {
1853  return $this->UpdateSetting(
1854  "LogSlowPageLoads", $NewValue, $Persistent);
1855  }
1856 
1867  public function SlowPageLoadThreshold(
1868  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1869  {
1870  return $this->UpdateSetting(
1871  "SlowPageLoadThreshold", $NewValue, $Persistent);
1872  }
1873 
1887  public function LogHighMemoryUsage(
1888  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1889  {
1890  return $this->UpdateSetting(
1891  "LogHighMemoryUsage", $NewValue, $Persistent);
1892  }
1893 
1905  public function HighMemoryUsageThreshold(
1906  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1907  {
1908  return $this->UpdateSetting(
1909  "HighMemoryUsageThreshold", $NewValue, $Persistent);
1910  }
1911 
1925  public function LogError($Level, $Msg)
1926  {
1927  # if error level is at or below current logging level
1928  if ($this->Settings["LoggingLevel"] >= $Level)
1929  {
1930  # attempt to log error message
1931  $Result = $this->LogMessage($Level, $Msg);
1932 
1933  # if logging attempt failed and level indicated significant error
1934  if (($Result === FALSE) && ($Level <= self::LOGLVL_ERROR))
1935  {
1936  # throw exception about inability to log error
1937  static $AlreadyThrewException = FALSE;
1938  if (!$AlreadyThrewException)
1939  {
1940  $AlreadyThrewException = TRUE;
1941  throw new Exception("Unable to log error (".$Level.": ".$Msg
1942  .") to ".$this->LogFileName);
1943  }
1944  }
1945 
1946  # report to caller whether message was logged
1947  return $Result;
1948  }
1949  else
1950  {
1951  # report to caller that message was not logged
1952  return FALSE;
1953  }
1954  }
1955 
1967  public function LogMessage($Level, $Msg)
1968  {
1969  # if message level is at or below current logging level
1970  if ($this->Settings["LoggingLevel"] >= $Level)
1971  {
1972  # attempt to open log file
1973  $FHndl = @fopen($this->LogFileName, "a");
1974 
1975  # if log file could not be open
1976  if ($FHndl === FALSE)
1977  {
1978  # report to caller that message was not logged
1979  return FALSE;
1980  }
1981  else
1982  {
1983  # format log entry
1984  $ErrorAbbrevs = array(
1985  self::LOGLVL_FATAL => "FTL",
1986  self::LOGLVL_ERROR => "ERR",
1987  self::LOGLVL_WARNING => "WRN",
1988  self::LOGLVL_INFO => "INF",
1989  self::LOGLVL_DEBUG => "DBG",
1990  self::LOGLVL_TRACE => "TRC",
1991  );
1992  $Msg = str_replace(array("\n", "\t", "\r"), " ", $Msg);
1993  $Msg = substr(trim($Msg), 0, self::LOGFILE_MAX_LINE_LENGTH);
1994  $LogEntry = date("Y-m-d H:i:s")
1995  ." ".($this->RunningInBackground ? "B" : "F")
1996  ." ".$ErrorAbbrevs[$Level]
1997  ." ".$Msg;
1998 
1999  # write entry to log
2000  $Success = fwrite($FHndl, $LogEntry."\n");
2001 
2002  # close log file
2003  fclose($FHndl);
2004 
2005  # report to caller whether message was logged
2006  return ($Success === FALSE) ? FALSE : TRUE;
2007  }
2008  }
2009  else
2010  {
2011  # report to caller that message was not logged
2012  return FALSE;
2013  }
2014  }
2015 
2037  public function LoggingLevel($NewValue = DB_NOVALUE)
2038  {
2039  # constrain new level (if supplied) to within legal bounds
2040  if ($NewValue !== DB_NOVALUE)
2041  {
2042  $NewValue = max(min($NewValue, 6), 1);
2043  }
2044 
2045  # set new logging level (if supplied) and return current level to caller
2046  return $this->UpdateSetting("LoggingLevel", $NewValue);
2047  }
2048 
2055  public function LogFile($NewValue = NULL)
2056  {
2057  if ($NewValue !== NULL) { $this->LogFileName = $NewValue; }
2058  return $this->LogFileName;
2059  }
2060 
2070  public function GetLogEntries($Limit = 0)
2071  {
2072  # return no entries if there isn't a log file or we can't read it
2073  if (!is_readable($this->LogFile()))
2074  {
2075  return array();
2076  }
2077 
2078  # if max number of entries specified
2079  if ($Limit > 0)
2080  {
2081  # load lines from file
2082  $FHandle = fopen($this->LogFile(), "r");
2083  $FileSize = filesize($this->LogFile());
2084  $SeekPosition = max(0,
2085  ($FileSize - (self::LOGFILE_MAX_LINE_LENGTH * ($Limit + 1))));
2086  fseek($FHandle, $SeekPosition);
2087  $Block = fread($FHandle, ($FileSize - $SeekPosition));
2088  fclose($FHandle);
2089  $Lines = explode(PHP_EOL, $Block);
2090  array_shift($Lines);
2091 
2092  # prune array back to requested number of entries
2093  $Lines = array_slice($Lines, (0 - $Limit));
2094  }
2095  else
2096  {
2097  # load all lines from log file
2098  $Lines = file($this->LogFile(), FILE_IGNORE_NEW_LINES);
2099  if ($Lines === FALSE)
2100  {
2101  return array();
2102  }
2103  }
2104 
2105  # reverse line order
2106  $Lines = array_reverse($Lines);
2107 
2108  # for each log file line
2109  $Entries = array();
2110  foreach ($Lines as $Line)
2111  {
2112  # attempt to parse line into component parts
2113  $Pieces = explode(" ", $Line, 5);
2114  $Date = isset($Pieces[0]) ? $Pieces[0] : "";
2115  $Time = isset($Pieces[1]) ? $Pieces[1] : "";
2116  $Back = isset($Pieces[2]) ? $Pieces[2] : "";
2117  $Level = isset($Pieces[3]) ? $Pieces[3] : "";
2118  $Msg = isset($Pieces[4]) ? $Pieces[4] : "";
2119 
2120  # skip line if it looks invalid
2121  $ErrorAbbrevs = array(
2122  "FTL" => self::LOGLVL_FATAL,
2123  "ERR" => self::LOGLVL_ERROR,
2124  "WRN" => self::LOGLVL_WARNING,
2125  "INF" => self::LOGLVL_INFO,
2126  "DBG" => self::LOGLVL_DEBUG,
2127  "TRC" => self::LOGLVL_TRACE,
2128  );
2129  if ((($Back != "F") && ($Back != "B"))
2130  || !array_key_exists($Level, $ErrorAbbrevs)
2131  || !strlen($Msg))
2132  {
2133  continue;
2134  }
2135 
2136  # convert parts into appropriate values and add to entries
2137  $Entries[] = array(
2138  "Time" => strtotime($Date." ".$Time),
2139  "Background" => ($Back == "B") ? TRUE : FALSE,
2140  "Level" => $ErrorAbbrevs[$Level],
2141  "Message" => $Msg,
2142  );
2143  }
2144 
2145  # return entries to caller
2146  return $Entries;
2147  }
2148 
2153  const LOGLVL_TRACE = 6;
2158  const LOGLVL_DEBUG = 5;
2164  const LOGLVL_INFO = 4;
2169  const LOGLVL_WARNING = 3;
2175  const LOGLVL_ERROR = 2;
2180  const LOGLVL_FATAL = 1;
2181 
2185  const LOGFILE_MAX_LINE_LENGTH = 2048;
2186 
2187  /*@)*/ /* Logging */
2188 
2189 
2190  # ---- Event Handling ----------------------------------------------------
2191  /*@(*/
2193 
2197  const EVENTTYPE_DEFAULT = 1;
2203  const EVENTTYPE_CHAIN = 2;
2209  const EVENTTYPE_FIRST = 3;
2217  const EVENTTYPE_NAMED = 4;
2218 
2220  const ORDER_FIRST = 1;
2222  const ORDER_MIDDLE = 2;
2224  const ORDER_LAST = 3;
2225 
2234  public function RegisterEvent($EventsOrEventName, $EventType = NULL)
2235  {
2236  # convert parameters to array if not already in that form
2237  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2238  : array($EventsOrEventName => $EventType);
2239 
2240  # for each event
2241  foreach ($Events as $Name => $Type)
2242  {
2243  # store event information
2244  $this->RegisteredEvents[$Name]["Type"] = $Type;
2245  $this->RegisteredEvents[$Name]["Hooks"] = array();
2246  }
2247  }
2248 
2255  public function IsRegisteredEvent($EventName)
2256  {
2257  return array_key_exists($EventName, $this->RegisteredEvents)
2258  ? TRUE : FALSE;
2259  }
2260 
2267  public function IsHookedEvent($EventName)
2268  {
2269  # the event isn't hooked to if it isn't even registered
2270  if (!$this->IsRegisteredEvent($EventName))
2271  {
2272  return FALSE;
2273  }
2274 
2275  # return TRUE if there is at least one callback hooked to the event
2276  return count($this->RegisteredEvents[$EventName]["Hooks"]) > 0;
2277  }
2278 
2292  public function HookEvent(
2293  $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2294  {
2295  # convert parameters to array if not already in that form
2296  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2297  : array($EventsOrEventName => $Callback);
2298 
2299  # for each event
2300  $Success = TRUE;
2301  foreach ($Events as $EventName => $EventCallback)
2302  {
2303  # if callback is valid
2304  if (is_callable($EventCallback))
2305  {
2306  # if this is a periodic event we process internally
2307  if (isset($this->PeriodicEvents[$EventName]))
2308  {
2309  # process event now
2310  $this->ProcessPeriodicEvent($EventName, $EventCallback);
2311  }
2312  # if specified event has been registered
2313  elseif (isset($this->RegisteredEvents[$EventName]))
2314  {
2315  # add callback for event
2316  $this->RegisteredEvents[$EventName]["Hooks"][]
2317  = array("Callback" => $EventCallback, "Order" => $Order);
2318 
2319  # sort callbacks by order
2320  if (count($this->RegisteredEvents[$EventName]["Hooks"]) > 1)
2321  {
2322  usort($this->RegisteredEvents[$EventName]["Hooks"],
2323  array("ApplicationFramework", "HookEvent_OrderCompare"));
2324  }
2325  }
2326  else
2327  {
2328  $Success = FALSE;
2329  }
2330  }
2331  else
2332  {
2333  $Success = FALSE;
2334  }
2335  }
2336 
2337  # report to caller whether all callbacks were hooked
2338  return $Success;
2339  }
2346  private static function HookEvent_OrderCompare($A, $B)
2347  {
2348  if ($A["Order"] == $B["Order"]) { return 0; }
2349  return ($A["Order"] < $B["Order"]) ? -1 : 1;
2350  }
2351 
2362  public function SignalEvent($EventName, $Parameters = NULL)
2363  {
2364  $ReturnValue = NULL;
2365 
2366  # if event has been registered
2367  if (isset($this->RegisteredEvents[$EventName]))
2368  {
2369  # set up default return value (if not NULL)
2370  switch ($this->RegisteredEvents[$EventName]["Type"])
2371  {
2372  case self::EVENTTYPE_CHAIN:
2373  $ReturnValue = $Parameters;
2374  break;
2375 
2376  case self::EVENTTYPE_NAMED:
2377  $ReturnValue = array();
2378  break;
2379  }
2380 
2381  # for each callback for this event
2382  foreach ($this->RegisteredEvents[$EventName]["Hooks"] as $Hook)
2383  {
2384  # invoke callback
2385  $Callback = $Hook["Callback"];
2386  $Result = ($Parameters !== NULL)
2387  ? call_user_func_array($Callback, $Parameters)
2388  : call_user_func($Callback);
2389 
2390  # process return value based on event type
2391  switch ($this->RegisteredEvents[$EventName]["Type"])
2392  {
2393  case self::EVENTTYPE_CHAIN:
2394  if ($Result !== NULL)
2395  {
2396  foreach ($Parameters as $Index => $Value)
2397  {
2398  if (array_key_exists($Index, $Result))
2399  {
2400  $Parameters[$Index] = $Result[$Index];
2401  }
2402  }
2403  $ReturnValue = $Parameters;
2404  }
2405  break;
2406 
2407  case self::EVENTTYPE_FIRST:
2408  if ($Result !== NULL)
2409  {
2410  $ReturnValue = $Result;
2411  break 2;
2412  }
2413  break;
2414 
2415  case self::EVENTTYPE_NAMED:
2416  $CallbackName = is_array($Callback)
2417  ? (is_object($Callback[0])
2418  ? get_class($Callback[0])
2419  : $Callback[0])."::".$Callback[1]
2420  : $Callback;
2421  $ReturnValue[$CallbackName] = $Result;
2422  break;
2423 
2424  default:
2425  break;
2426  }
2427  }
2428  }
2429  else
2430  {
2431  $this->LogError(self::LOGLVL_WARNING,
2432  "Unregistered event (".$EventName.") signaled by "
2433  .StdLib::GetMyCaller().".");
2434  }
2435 
2436  # return value if any to caller
2437  return $ReturnValue;
2438  }
2439 
2445  public function IsStaticOnlyEvent($EventName)
2446  {
2447  return isset($this->PeriodicEvents[$EventName]) ? TRUE : FALSE;
2448  }
2449 
2460  public function EventWillNextRunAt($EventName, $Callback)
2461  {
2462  # if event is not a periodic event report failure to caller
2463  if (!array_key_exists($EventName, $this->EventPeriods)) { return FALSE; }
2464 
2465  # retrieve last execution time for event if available
2466  $Signature = self::GetCallbackSignature($Callback);
2467  $LastRunTime = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
2468  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
2469 
2470  # if event was not found report failure to caller
2471  if ($LastRunTime === NULL) { return FALSE; }
2472 
2473  # calculate next run time based on event period
2474  $NextRunTime = strtotime($LastRunTime) + $this->EventPeriods[$EventName];
2475 
2476  # report next run time to caller
2477  return $NextRunTime;
2478  }
2479 
2495  public function GetKnownPeriodicEvents()
2496  {
2497  # retrieve last execution times
2498  $this->DB->Query("SELECT * FROM PeriodicEvents");
2499  $LastRunTimes = $this->DB->FetchColumn("LastRunAt", "Signature");
2500 
2501  # for each known event
2502  $Events = array();
2503  foreach ($this->KnownPeriodicEvents as $Signature => $Info)
2504  {
2505  # if last run time for event is available
2506  if (array_key_exists($Signature, $LastRunTimes))
2507  {
2508  # calculate next run time for event
2509  $LastRun = strtotime($LastRunTimes[$Signature]);
2510  $NextRun = $LastRun + $this->EventPeriods[$Info["Period"]];
2511  if ($Info["Period"] == "EVENT_PERIODIC") { $LastRun = FALSE; }
2512  }
2513  else
2514  {
2515  # set info to indicate run times are not known
2516  $LastRun = FALSE;
2517  $NextRun = FALSE;
2518  }
2519 
2520  # add event info to list
2521  $Events[$Signature] = $Info;
2522  $Events[$Signature]["LastRun"] = $LastRun;
2523  $Events[$Signature]["NextRun"] = $NextRun;
2524  $Events[$Signature]["Parameters"] = NULL;
2525  }
2526 
2527  # return list of known events to caller
2528  return $Events;
2529  }
2530 
2531  /*@)*/ /* Event Handling */
2532 
2533 
2534  # ---- Task Management ---------------------------------------------------
2535  /*@(*/
2537 
2539  const PRIORITY_HIGH = 1;
2541  const PRIORITY_MEDIUM = 2;
2543  const PRIORITY_LOW = 3;
2545  const PRIORITY_BACKGROUND = 4;
2546 
2559  public function QueueTask($Callback, $Parameters = NULL,
2560  $Priority = self::PRIORITY_LOW, $Description = "")
2561  {
2562  # pack task info and write to database
2563  if ($Parameters === NULL) { $Parameters = array(); }
2564  $this->DB->Query("INSERT INTO TaskQueue"
2565  ." (Callback, Parameters, Priority, Description)"
2566  ." VALUES ('".addslashes(serialize($Callback))."', '"
2567  .addslashes(serialize($Parameters))."', ".intval($Priority).", '"
2568  .addslashes($Description)."')");
2569  }
2570 
2588  public function QueueUniqueTask($Callback, $Parameters = NULL,
2589  $Priority = self::PRIORITY_LOW, $Description = "")
2590  {
2591  if ($this->TaskIsInQueue($Callback, $Parameters))
2592  {
2593  $QueryResult = $this->DB->Query("SELECT TaskId,Priority FROM TaskQueue"
2594  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2595  .($Parameters ? " AND Parameters = '"
2596  .addslashes(serialize($Parameters))."'" : ""));
2597  if ($QueryResult !== FALSE)
2598  {
2599  $Record = $this->DB->FetchRow();
2600  if ($Record["Priority"] > $Priority)
2601  {
2602  $this->DB->Query("UPDATE TaskQueue"
2603  ." SET Priority = ".intval($Priority)
2604  ." WHERE TaskId = ".intval($Record["TaskId"]));
2605  }
2606  }
2607  return FALSE;
2608  }
2609  else
2610  {
2611  $this->QueueTask($Callback, $Parameters, $Priority, $Description);
2612  return TRUE;
2613  }
2614  }
2615 
2625  public function TaskIsInQueue($Callback, $Parameters = NULL)
2626  {
2627  $QueuedCount = $this->DB->Query(
2628  "SELECT COUNT(*) AS FoundCount FROM TaskQueue"
2629  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2630  .($Parameters ? " AND Parameters = '"
2631  .addslashes(serialize($Parameters))."'" : ""),
2632  "FoundCount");
2633  $RunningCount = $this->DB->Query(
2634  "SELECT COUNT(*) AS FoundCount FROM RunningTasks"
2635  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2636  .($Parameters ? " AND Parameters = '"
2637  .addslashes(serialize($Parameters))."'" : ""),
2638  "FoundCount");
2639  $FoundCount = $QueuedCount + $RunningCount;
2640  return ($FoundCount ? TRUE : FALSE);
2641  }
2642 
2648  public function GetTaskQueueSize($Priority = NULL)
2649  {
2650  return $this->GetQueuedTaskCount(NULL, NULL, $Priority);
2651  }
2652 
2660  public function GetQueuedTaskList($Count = 100, $Offset = 0)
2661  {
2662  return $this->GetTaskList("SELECT * FROM TaskQueue"
2663  ." ORDER BY Priority, TaskId ", $Count, $Offset);
2664  }
2665 
2679  public function GetQueuedTaskCount($Callback = NULL,
2680  $Parameters = NULL, $Priority = NULL, $Description = NULL)
2681  {
2682  $Query = "SELECT COUNT(*) AS TaskCount FROM TaskQueue";
2683  $Sep = " WHERE";
2684  if ($Callback !== NULL)
2685  {
2686  $Query .= $Sep." Callback = '".addslashes(serialize($Callback))."'";
2687  $Sep = " AND";
2688  }
2689  if ($Parameters !== NULL)
2690  {
2691  $Query .= $Sep." Parameters = '".addslashes(serialize($Parameters))."'";
2692  $Sep = " AND";
2693  }
2694  if ($Priority !== NULL)
2695  {
2696  $Query .= $Sep." Priority = ".intval($Priority);
2697  $Sep = " AND";
2698  }
2699  if ($Description !== NULL)
2700  {
2701  $Query .= $Sep." Description = '".addslashes($Description)."'";
2702  }
2703  return $this->DB->Query($Query, "TaskCount");
2704  }
2705 
2713  public function GetRunningTaskList($Count = 100, $Offset = 0)
2714  {
2715  return $this->GetTaskList("SELECT * FROM RunningTasks"
2716  ." WHERE StartedAt >= '".date("Y-m-d H:i:s",
2717  (time() - ini_get("max_execution_time")))."'"
2718  ." ORDER BY StartedAt", $Count, $Offset);
2719  }
2720 
2728  public function GetOrphanedTaskList($Count = 100, $Offset = 0)
2729  {
2730  return $this->GetTaskList("SELECT * FROM RunningTasks"
2731  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
2732  (time() - ini_get("max_execution_time")))."'"
2733  ." ORDER BY StartedAt", $Count, $Offset);
2734  }
2735 
2740  public function GetOrphanedTaskCount()
2741  {
2742  return $this->DB->Query("SELECT COUNT(*) AS Count FROM RunningTasks"
2743  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
2744  (time() - ini_get("max_execution_time")))."'",
2745  "Count");
2746  }
2747 
2753  public function ReQueueOrphanedTask($TaskId, $NewPriority = NULL)
2754  {
2755  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
2756  $this->DB->Query("INSERT INTO TaskQueue"
2757  ." (Callback,Parameters,Priority,Description) "
2758  ."SELECT Callback, Parameters, Priority, Description"
2759  ." FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2760  if ($NewPriority !== NULL)
2761  {
2762  $NewTaskId = $this->DB->LastInsertId();
2763  $this->DB->Query("UPDATE TaskQueue SET Priority = "
2764  .intval($NewPriority)
2765  ." WHERE TaskId = ".intval($NewTaskId));
2766  }
2767  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2768  $this->DB->Query("UNLOCK TABLES");
2769  }
2770 
2775  public function DeleteTask($TaskId)
2776  {
2777  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = ".intval($TaskId));
2778  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2779  }
2780 
2788  public function GetTask($TaskId)
2789  {
2790  # assume task will not be found
2791  $Task = NULL;
2792 
2793  # look for task in task queue
2794  $this->DB->Query("SELECT * FROM TaskQueue WHERE TaskId = ".intval($TaskId));
2795 
2796  # if task was not found in queue
2797  if (!$this->DB->NumRowsSelected())
2798  {
2799  # look for task in running task list
2800  $this->DB->Query("SELECT * FROM RunningTasks WHERE TaskId = "
2801  .intval($TaskId));
2802  }
2803 
2804  # if task was found
2805  if ($this->DB->NumRowsSelected())
2806  {
2807  # if task was periodic
2808  $Row = $this->DB->FetchRow();
2809  if ($Row["Callback"] ==
2810  serialize(array("ApplicationFramework", "PeriodicEventWrapper")))
2811  {
2812  # unpack periodic task callback
2813  $WrappedCallback = unserialize($Row["Parameters"]);
2814  $Task["Callback"] = $WrappedCallback[1];
2815  $Task["Parameters"] = $WrappedCallback[2];
2816  }
2817  else
2818  {
2819  # unpack task callback and parameters
2820  $Task["Callback"] = unserialize($Row["Callback"]);
2821  $Task["Parameters"] = unserialize($Row["Parameters"]);
2822  }
2823  }
2824 
2825  # return task to caller
2826  return $Task;
2827  }
2828 
2836  public function TaskExecutionEnabled($NewValue = DB_NOVALUE)
2837  {
2838  return $this->UpdateSetting("TaskExecutionEnabled", $NewValue);
2839  }
2840 
2846  public function MaxTasks($NewValue = DB_NOVALUE)
2847  {
2848  return $this->UpdateSetting("MaxTasksRunning", $NewValue);
2849  }
2850 
2858  public static function GetTaskCallbackSynopsis($TaskInfo)
2859  {
2860  # if task callback is function use function name
2861  $Callback = $TaskInfo["Callback"];
2862  $Name = "";
2863  if (!is_array($Callback))
2864  {
2865  $Name = $Callback;
2866  }
2867  else
2868  {
2869  # if task callback is object
2870  if (is_object($Callback[0]))
2871  {
2872  # if task callback is encapsulated ask encapsulation for name
2873  if (method_exists($Callback[0], "GetCallbackAsText"))
2874  {
2875  $Name = $Callback[0]->GetCallbackAsText();
2876  }
2877  # else assemble name from object
2878  else
2879  {
2880  $Name = get_class($Callback[0]) . "::" . $Callback[1];
2881  }
2882  }
2883  # else assemble name from supplied info
2884  else
2885  {
2886  $Name= $Callback[0] . "::" . $Callback[1];
2887  }
2888  }
2889 
2890  # if parameter array was supplied
2891  $Parameters = $TaskInfo["Parameters"];
2892  $ParameterString = "";
2893  if (is_array($Parameters))
2894  {
2895  # assemble parameter string
2896  $Separator = "";
2897  foreach ($Parameters as $Parameter)
2898  {
2899  $ParameterString .= $Separator;
2900  if (is_int($Parameter) || is_float($Parameter))
2901  {
2902  $ParameterString .= $Parameter;
2903  }
2904  else if (is_string($Parameter))
2905  {
2906  $ParameterString .= "\"".htmlspecialchars($Parameter)."\"";
2907  }
2908  else if (is_array($Parameter))
2909  {
2910  $ParameterString .= "ARRAY";
2911  }
2912  else if (is_object($Parameter))
2913  {
2914  $ParameterString .= "OBJECT";
2915  }
2916  else if (is_null($Parameter))
2917  {
2918  $ParameterString .= "NULL";
2919  }
2920  else if (is_bool($Parameter))
2921  {
2922  $ParameterString .= $Parameter ? "TRUE" : "FALSE";
2923  }
2924  else if (is_resource($Parameter))
2925  {
2926  $ParameterString .= get_resource_type($Parameter);
2927  }
2928  else
2929  {
2930  $ParameterString .= "????";
2931  }
2932  $Separator = ", ";
2933  }
2934  }
2935 
2936  # assemble name and parameters and return result to caller
2937  return $Name."(".$ParameterString.")";
2938  }
2939 
2944  public function IsRunningInBackground()
2945  {
2946  return $this->RunningInBackground;
2947  }
2948 
2954  public function GetCurrentBackgroundPriority()
2955  {
2956  return isset($this->RunningTask)
2957  ? $this->RunningTask["Priority"] : NULL;
2958  }
2959 
2968  public function GetNextHigherBackgroundPriority($Priority = NULL)
2969  {
2970  if ($Priority === NULL)
2971  {
2972  $Priority = $this->GetCurrentBackgroundPriority();
2973  if ($Priority === NULL)
2974  {
2975  return NULL;
2976  }
2977  }
2978  return ($Priority > self::PRIORITY_HIGH)
2979  ? ($Priority - 1) : self::PRIORITY_HIGH;
2980  }
2981 
2990  public function GetNextLowerBackgroundPriority($Priority = NULL)
2991  {
2992  if ($Priority === NULL)
2993  {
2994  $Priority = $this->GetCurrentBackgroundPriority();
2995  if ($Priority === NULL)
2996  {
2997  return NULL;
2998  }
2999  }
3000  return ($Priority < self::PRIORITY_BACKGROUND)
3001  ? ($Priority + 1) : self::PRIORITY_BACKGROUND;
3002  }
3003 
3004  /*@)*/ /* Task Management */
3005 
3006 
3007  # ---- Clean URL Support -------------------------------------------------
3008  /*@(*/
3010 
3037  public function AddCleanUrl($Pattern, $Page, $GetVars = NULL, $Template = NULL)
3038  {
3039  # save clean URL mapping parameters
3040  $this->CleanUrlMappings[] = array(
3041  "Pattern" => $Pattern,
3042  "Page" => $Page,
3043  "GetVars" => $GetVars,
3044  );
3045 
3046  # if replacement template specified
3047  if ($Template !== NULL)
3048  {
3049  # if GET parameters specified
3050  if (count($GetVars))
3051  {
3052  # retrieve all possible permutations of GET parameters
3053  $GetPerms = $this->ArrayPermutations(array_keys($GetVars));
3054 
3055  # for each permutation of GET parameters
3056  foreach ($GetPerms as $VarPermutation)
3057  {
3058  # construct search pattern for permutation
3059  $SearchPattern = "/href=([\"'])index\\.php\\?P=".$Page;
3060  $GetVarSegment = "";
3061  foreach ($VarPermutation as $GetVar)
3062  {
3063  if (preg_match("%\\\$[0-9]+%", $GetVars[$GetVar]))
3064  {
3065  $GetVarSegment .= "&amp;".$GetVar."=((?:(?!\\1)[^&])+)";
3066  }
3067  else
3068  {
3069  $GetVarSegment .= "&amp;".$GetVar."=".$GetVars[$GetVar];
3070  }
3071  }
3072  $SearchPattern .= $GetVarSegment."\\1/i";
3073 
3074  # if template is actually a callback
3075  if (is_callable($Template))
3076  {
3077  # add pattern to HTML output mod callbacks list
3078  $this->OutputModificationCallbacks[] = array(
3079  "Pattern" => $Pattern,
3080  "Page" => $Page,
3081  "SearchPattern" => $SearchPattern,
3082  "Callback" => $Template,
3083  );
3084  }
3085  else
3086  {
3087  # construct replacement string for permutation
3088  $Replacement = $Template;
3089  $Index = 2;
3090  foreach ($VarPermutation as $GetVar)
3091  {
3092  $Replacement = str_replace(
3093  "\$".$GetVar, "\$".$Index, $Replacement);
3094  $Index++;
3095  }
3096  $Replacement = "href=\"".$Replacement."\"";
3097 
3098  # add pattern to HTML output modifications list
3099  $this->OutputModificationPatterns[] = $SearchPattern;
3100  $this->OutputModificationReplacements[] = $Replacement;
3101  }
3102  }
3103  }
3104  else
3105  {
3106  # construct search pattern
3107  $SearchPattern = "/href=\"index\\.php\\?P=".$Page."\"/i";
3108 
3109  # if template is actually a callback
3110  if (is_callable($Template))
3111  {
3112  # add pattern to HTML output mod callbacks list
3113  $this->OutputModificationCallbacks[] = array(
3114  "Pattern" => $Pattern,
3115  "Page" => $Page,
3116  "SearchPattern" => $SearchPattern,
3117  "Callback" => $Template,
3118  );
3119  }
3120  else
3121  {
3122  # add simple pattern to HTML output modifications list
3123  $this->OutputModificationPatterns[] = $SearchPattern;
3124  $this->OutputModificationReplacements[] = "href=\"".$Template."\"";
3125  }
3126  }
3127  }
3128  }
3129 
3135  public function CleanUrlIsMapped($Path)
3136  {
3137  foreach ($this->CleanUrlMappings as $Info)
3138  {
3139  if (preg_match($Info["Pattern"], $Path))
3140  {
3141  return TRUE;
3142  }
3143  }
3144  return FALSE;
3145  }
3146 
3156  public function GetCleanUrlForPath($Path)
3157  {
3158  # the search patterns and callbacks require a specific format
3159  $Format = "href=\"".str_replace("&", "&amp;", $Path)."\"";
3160  $Search = $Format;
3161 
3162  # perform any regular expression replacements on the search string
3163  $Search = preg_replace($this->OutputModificationPatterns,
3164  $this->OutputModificationReplacements, $Search);
3165 
3166  # only run the callbacks if a replacement hasn't already been performed
3167  if ($Search == $Format)
3168  {
3169  # perform any callback replacements on the search string
3170  foreach ($this->OutputModificationCallbacks as $Info)
3171  {
3172  # make the information available to the callback
3173  $this->OutputModificationCallbackInfo = $Info;
3174 
3175  # execute the callback
3176  $Search = preg_replace_callback($Info["SearchPattern"],
3177  array($this, "OutputModificationCallbackShell"),
3178  $Search);
3179  }
3180  }
3181 
3182  # return the path untouched if no replacements were performed
3183  if ($Search == $Format)
3184  {
3185  return $Path;
3186  }
3187 
3188  # remove the bits added to the search string to get it recognized by
3189  # the replacement expressions and callbacks
3190  $Result = substr($Search, 6, -1);
3191 
3192  return $Result;
3193  }
3194 
3201  public function GetUncleanUrlForPath($Path)
3202  {
3203  # for each clean URL mapping
3204  foreach ($this->CleanUrlMappings as $Info)
3205  {
3206  # if current path matches the clean URL pattern
3207  if (preg_match($Info["Pattern"], $Path, $Matches))
3208  {
3209  # the GET parameters for the URL, starting with the page name
3210  $GetVars = array("P" => $Info["Page"]);
3211 
3212  # if additional $_GET variables specified for clean URL
3213  if ($Info["GetVars"] !== NULL)
3214  {
3215  # for each $_GET variable specified for clean URL
3216  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
3217  {
3218  # start with template for variable value
3219  $Value = $VarTemplate;
3220 
3221  # for each subpattern matched in current URL
3222  foreach ($Matches as $Index => $Match)
3223  {
3224  # if not first (whole) match
3225  if ($Index > 0)
3226  {
3227  # make any substitutions in template
3228  $Value = str_replace("$".$Index, $Match, $Value);
3229  }
3230  }
3231 
3232  # add the GET variable
3233  $GetVars[$VarName] = $Value;
3234  }
3235  }
3236 
3237  # return the unclean URL
3238  return "index.php?" . http_build_query($GetVars);
3239  }
3240  }
3241 
3242  # return the path unchanged
3243  return $Path;
3244  }
3245 
3251  public function GetCleanUrl()
3252  {
3253  return $this->GetCleanUrlForPath($this->GetUncleanUrl());
3254  }
3255 
3260  public function GetUncleanUrl()
3261  {
3262  $GetVars = array("P" => $this->GetPageName()) + $_GET;
3263  return "index.php?" . http_build_query($GetVars);
3264  }
3265 
3277  public function AddPrefixForAlternateDomain($Domain, $Prefix)
3278  {
3279  $this->AlternateDomainPrefixes[$Domain] = $Prefix;
3280  }
3281 
3282  /*@)*/ /* Clean URL Support */
3283 
3284  # ---- Server Environment ------------------------------------------------
3285  /*@(*/
3287 
3293  public static function SessionLifetime($NewValue = NULL)
3294  {
3295  if ($NewValue !== NULL)
3296  {
3297  self::$SessionLifetime = $NewValue;
3298  }
3299  return self::$SessionLifetime;
3300  }
3301 
3307  public static function HtaccessSupport()
3308  {
3309  return isset($_SERVER["HTACCESS_SUPPORT"])
3310  || isset($_SERVER["REDIRECT_HTACCESS_SUPPORT"]);
3311  }
3312 
3319  public static function UrlFingerprintingRewriteSupport()
3320  {
3321  return isset($_SERVER["URL_FINGERPRINTING_SUPPORT"])
3322  || isset($_SERVER["REDIRECT_URL_FINGERPRINTING_SUPPORT"]);
3323  }
3324 
3331  public static function ScssRewriteSupport()
3332  {
3333  return isset($_SERVER["SCSS_REWRITE_SUPPORT"])
3334  || isset($_SERVER["REDIRECT_SCSS_REWRITE_SUPPORT"]);
3335  }
3336 
3343  public static function JsMinRewriteSupport()
3344  {
3345  return isset($_SERVER["JSMIN_REWRITE_SUPPORT"])
3346  || isset($_SERVER["REDIRECT_JSMIN_REWRITE_SUPPORT"]);
3347  }
3348 
3356  public static function RootUrl()
3357  {
3358  # return override value if one is set
3359  if (self::$RootUrlOverride !== NULL)
3360  {
3361  return self::$RootUrlOverride;
3362  }
3363 
3364  # determine scheme name
3365  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
3366 
3367  # if HTTP_HOST is preferred or SERVER_NAME points to localhost
3368  # and HTTP_HOST is set
3369  if ((self::$PreferHttpHost || ($_SERVER["SERVER_NAME"] == "127.0.0.1"))
3370  && isset($_SERVER["HTTP_HOST"]))
3371  {
3372  # use HTTP_HOST for domain name
3373  $DomainName = $_SERVER["HTTP_HOST"];
3374  }
3375  else
3376  {
3377  # use SERVER_NAME for domain name
3378  $DomainName = $_SERVER["HTTP_HOST"];
3379  }
3380 
3381  # build URL root and return to caller
3382  return $Protocol."://".$DomainName;
3383  }
3384 
3399  public static function RootUrlOverride($NewValue = self::NOVALUE)
3400  {
3401  if ($NewValue !== self::NOVALUE)
3402  {
3403  self::$RootUrlOverride = strlen(trim($NewValue)) ? $NewValue : NULL;
3404  }
3405  return self::$RootUrlOverride;
3406  }
3407 
3417  public static function BaseUrl()
3418  {
3419  $BaseUrl = self::RootUrl().dirname($_SERVER["SCRIPT_NAME"]);
3420  if (substr($BaseUrl, -1) != "/") { $BaseUrl .= "/"; }
3421  return $BaseUrl;
3422  }
3423 
3431  public static function FullUrl()
3432  {
3433  return self::RootUrl().$_SERVER["REQUEST_URI"];
3434  }
3435 
3446  public static function PreferHttpHost($NewValue = NULL)
3447  {
3448  if ($NewValue !== NULL)
3449  {
3450  self::$PreferHttpHost = ($NewValue ? TRUE : FALSE);
3451  }
3452  return self::$PreferHttpHost;
3453  }
3454 
3459  public static function BasePath()
3460  {
3461  $BasePath = dirname($_SERVER["SCRIPT_NAME"]);
3462 
3463  if (substr($BasePath, -1) != "/")
3464  {
3465  $BasePath .= "/";
3466  }
3467 
3468  return $BasePath;
3469  }
3470 
3476  public static function GetScriptUrl()
3477  {
3478  if (array_key_exists("SCRIPT_URL", $_SERVER))
3479  {
3480  return $_SERVER["SCRIPT_URL"];
3481  }
3482  elseif (array_key_exists("REDIRECT_URL", $_SERVER))
3483  {
3484  return $_SERVER["REDIRECT_URL"];
3485  }
3486  elseif (array_key_exists("REQUEST_URI", $_SERVER))
3487  {
3488  $Pieces = parse_url($_SERVER["REQUEST_URI"]);
3489  return isset($Pieces["path"]) ? $Pieces["path"] : NULL;
3490  }
3491  else
3492  {
3493  return NULL;
3494  }
3495  }
3496 
3505  public static function WasUrlRewritten($ScriptName="index.php")
3506  {
3507  # needed to get the path of the URL minus the query and fragment pieces
3508  $Components = parse_url(self::GetScriptUrl());
3509 
3510  # if parsing was successful and a path is set
3511  if (is_array($Components) && isset($Components["path"]))
3512  {
3513  $BasePath = self::BasePath();
3514  $Path = $Components["path"];
3515 
3516  # the URL was rewritten if the path isn't the base path, i.e., the
3517  # home page, and the file in the URL isn't the script generating the
3518  # page
3519  if ($BasePath != $Path && basename($Path) != $ScriptName)
3520  {
3521  return TRUE;
3522  }
3523  }
3524 
3525  # the URL wasn't rewritten
3526  return FALSE;
3527  }
3528 
3534  public static function GetFreeMemory()
3535  {
3536  return self::GetPhpMemoryLimit() - memory_get_usage();
3537  }
3538 
3544  public static function GetPhpMemoryLimit()
3545  {
3546  $Str = strtoupper(ini_get("memory_limit"));
3547  if (substr($Str, -1) == "B") { $Str = substr($Str, 0, strlen($Str) - 1); }
3548  switch (substr($Str, -1))
3549  {
3550  case "K":
3551  $MemoryLimit = (int)$Str * 1024;
3552  break;
3553 
3554  case "M":
3555  $MemoryLimit = (int)$Str * 1048576;
3556  break;
3557 
3558  case "G":
3559  $MemoryLimit = (int)$Str * 1073741824;
3560  break;
3561 
3562  default:
3563  $MemoryLimit = (int)$Str;
3564  break;
3565  }
3566  return $MemoryLimit;
3567  }
3568 
3576  public function MaxExecutionTime($NewValue = NULL)
3577  {
3578  if (func_num_args() && !ini_get("safe_mode"))
3579  {
3580  if ($NewValue != $this->Settings["MaxExecTime"])
3581  {
3582  $this->Settings["MaxExecTime"] = max($NewValue, 5);
3583  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
3584  ." SET MaxExecTime = '"
3585  .intval($this->Settings["MaxExecTime"])."'");
3586  }
3587  ini_set("max_execution_time", $this->Settings["MaxExecTime"]);
3588  set_time_limit($this->Settings["MaxExecTime"]);
3589  }
3590  return ini_get("max_execution_time");
3591  }
3592 
3593  /*@)*/ /* Server Environment */
3594 
3595 
3596  # ---- Utility -----------------------------------------------------------
3597  /*@(*/
3599 
3611  public function DownloadFile($FilePath, $FileName = NULL, $MimeType = NULL)
3612  {
3613  # check that file is readable
3614  if (!is_readable($FilePath))
3615  {
3616  return FALSE;
3617  }
3618 
3619  # if file name was not supplied
3620  if ($FileName === NULL)
3621  {
3622  # extract file name from path
3623  $FileName = basename($FilePath);
3624  }
3625 
3626  # if MIME type was not supplied
3627  if ($MimeType === NULL)
3628  {
3629  # attempt to determine MIME type
3630  $FInfoHandle = finfo_open(FILEINFO_MIME);
3631  if ($FInfoHandle)
3632  {
3633  $FInfoMime = finfo_file($FInfoHandle, $FilePath);
3634  finfo_close($FInfoHandle);
3635  if ($FInfoMime)
3636  {
3637  $MimeType = $FInfoMime;
3638  }
3639  }
3640 
3641  # use default if unable to determine MIME type
3642  if ($MimeType === NULL)
3643  {
3644  $MimeType = "application/octet-stream";
3645  }
3646  }
3647 
3648  # set headers to download file
3649  header("Content-Type: ".$MimeType);
3650  header("Content-Length: ".filesize($FilePath));
3651  if ($this->CleanUrlRewritePerformed)
3652  {
3653  header('Content-Disposition: attachment; filename="'.$FileName.'"');
3654  }
3655 
3656  # make sure that apache does not attempt to compress file
3657  apache_setenv('no-gzip', '1');
3658 
3659  # send file to user, but unbuffered to avoid memory issues
3660  $this->AddUnbufferedCallback(function ($File)
3661  {
3662  $BlockSize = 512000;
3663  $Handle = @fopen($File, "rb");
3664  if ($Handle === FALSE)
3665  {
3666  return;
3667  }
3668  while (!feof($Handle))
3669  {
3670  print fread($Handle, $BlockSize);
3671  flush();
3672  }
3673  fclose($Handle);
3674  }, array($FilePath));
3675 
3676  # prevent HTML output that might interfere with download
3677  $this->SuppressHTMLOutput();
3678 
3679  # set flag to indicate not to log a slow page load in case client
3680  # connection delays PHP execution because of header
3681  $this->DoNotLogSlowPageLoad = TRUE;
3682 
3683  # report no errors found to caller
3684  return TRUE;
3685  }
3686 
3687  /*@)*/ /* Utility */
3688 
3689 
3690  # ---- Backward Compatibility --------------------------------------------
3691  /*@(*/
3693 
3700  public function FindCommonTemplate($BaseName)
3701  {
3702  return $this->FindFile(
3703  $this->IncludeDirList, $BaseName, array("tpl", "html"));
3704  }
3705 
3706  /*@)*/ /* Backward Compatibility */
3707 
3708 
3709  # ---- PRIVATE INTERFACE -------------------------------------------------
3710 
3711  private $AdditionalRequiredUIFiles = array();
3712  private $BackgroundTaskMemLeakLogThreshold = 10; # percentage of max mem
3713  private $BackgroundTaskMinFreeMemPercent = 25;
3714  private $BrowserDetectFunc;
3715  private $CacheCurrentPage = TRUE;
3716  private $AlternateDomainPrefixes = array();
3717  private $CleanUrlMappings = array();
3718  private $CleanUrlRewritePerformed = FALSE;
3719  private $CssUrlFingerprintPath;
3720  private $DB;
3721  private $DefaultPage = "Home";
3722  private $DoNotMinimizeList = array();
3723  private $DoNotLogSlowPageLoad = FALSE;
3724  private $EnvIncludes = array();
3725  private $ExecutionStartTime;
3726  private $FoundUIFiles = array();
3727  private $HtmlCharset = "UTF-8";
3728  private $JSMinimizerJavaScriptPackerAvailable = FALSE;
3729  private $JSMinimizerJShrinkAvailable = TRUE;
3730  private $JumpToPage = NULL;
3731  private $JumpToPageDelay = 0;
3732  private $LogFileName = "local/logs/site.log";
3733  private $MaxRunningTasksToTrack = 250;
3734  private $OutputModificationCallbackInfo;
3735  private $OutputModificationCallbacks = array();
3736  private $OutputModificationPatterns = array();
3737  private $OutputModificationReplacements = array();
3738  private $PageCacheTags = array();
3739  private $PageName;
3740  private $PostProcessingFuncs = array();
3741  private $RunningInBackground = FALSE;
3742  private $RunningTask;
3743  private $SavedContext;
3744  private $SaveTemplateLocationCache = FALSE;
3745  private $SessionStorage;
3746  private $SessionGcProbability;
3747  private $Settings;
3748  private $SuppressHTML = FALSE;
3749  private $TemplateLocationCache;
3750  private $TemplateLocationCacheInterval = 60; # in minutes
3751  private $TemplateLocationCacheExpiration;
3752  private $UnbufferedCallbacks = array();
3753  private $UrlFingerprintBlacklist = array();
3754  private $UseBaseTag = FALSE;
3755 
3756  private static $ActiveUI = "default";
3757  private static $AppName = "ScoutAF";
3758  private static $DefaultUI = "default";
3759  private static $JSMinCacheDir = "local/data/caches/JSMin";
3760  private static $ObjectDirectories = array();
3761  private static $ObjectLocationCache;
3762  private static $ObjectLocationCacheInterval = 60;
3763  private static $ObjectLocationCacheExpiration;
3764  private static $PreferHttpHost = FALSE;
3765  private static $RootUrlOverride = NULL;
3766  private static $SaveObjectLocationCache = FALSE;
3767  private static $ScssCacheDir = "local/data/caches/SCSS";
3768  private static $SessionLifetime = 1440; # in seconds
3769 
3770  # offset used to generate page cache tag IDs from numeric tags
3771  const PAGECACHETAGIDOFFSET = 100000;
3772 
3777  private $NoTSR = FALSE;
3778 
3779  private $KnownPeriodicEvents = array();
3780  private $PeriodicEvents = array(
3781  "EVENT_HOURLY" => self::EVENTTYPE_DEFAULT,
3782  "EVENT_DAILY" => self::EVENTTYPE_DEFAULT,
3783  "EVENT_WEEKLY" => self::EVENTTYPE_DEFAULT,
3784  "EVENT_MONTHLY" => self::EVENTTYPE_DEFAULT,
3785  "EVENT_PERIODIC" => self::EVENTTYPE_NAMED,
3786  );
3787  private $EventPeriods = array(
3788  "EVENT_HOURLY" => 3600,
3789  "EVENT_DAILY" => 86400,
3790  "EVENT_WEEKLY" => 604800,
3791  "EVENT_MONTHLY" => 2592000,
3792  "EVENT_PERIODIC" => 0,
3793  );
3794  private $UIEvents = array(
3795  "EVENT_PAGE_LOAD" => self::EVENTTYPE_DEFAULT,
3796  "EVENT_PHP_FILE_LOAD" => self::EVENTTYPE_CHAIN,
3797  "EVENT_PHP_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
3798  "EVENT_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
3799  "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
3800  "EVENT_PAGE_OUTPUT_FILTER" => self::EVENTTYPE_CHAIN,
3801  );
3802 
3807  private function LoadSettings()
3808  {
3809  # read settings in from database
3810  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
3811  $this->Settings = $this->DB->FetchRow();
3812 
3813  # if settings were not previously initialized
3814  if ($this->Settings === FALSE)
3815  {
3816  # initialize settings in database
3817  $this->DB->Query("INSERT INTO ApplicationFrameworkSettings"
3818  ." (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");
3819 
3820  # read new settings in from database
3821  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
3822  $this->Settings = $this->DB->FetchRow();
3823 
3824  # bail out if reloading new settings failed
3825  if ($this->Settings === FALSE)
3826  {
3827  throw new Exception(
3828  "Unable to load application framework settings.");
3829  }
3830  }
3831 
3832  # if base path was not previously set or we appear to have moved
3833  if (!array_key_exists("BasePath", $this->Settings)
3834  || (!strlen($this->Settings["BasePath"]))
3835  || (!array_key_exists("BasePathCheck", $this->Settings))
3836  || (__FILE__ != $this->Settings["BasePathCheck"]))
3837  {
3838  # attempt to extract base path from Apache .htaccess file
3839  if (is_readable(".htaccess"))
3840  {
3841  $Lines = file(".htaccess");
3842  foreach ($Lines as $Line)
3843  {
3844  if (preg_match("/\\s*RewriteBase\\s+/", $Line))
3845  {
3846  $Pieces = preg_split(
3847  "/\\s+/", $Line, NULL, PREG_SPLIT_NO_EMPTY);
3848  $BasePath = $Pieces[1];
3849  }
3850  }
3851  }
3852 
3853  # if base path was found
3854  if (isset($BasePath))
3855  {
3856  # save base path locally
3857  $this->Settings["BasePath"] = $BasePath;
3858 
3859  # save base path to database
3860  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
3861  ." SET BasePath = '".addslashes($BasePath)."'"
3862  .", BasePathCheck = '".addslashes(__FILE__)."'");
3863  }
3864  }
3865 
3866  # retrieve template location cache
3867  $this->TemplateLocationCache = unserialize(
3868  $this->Settings["TemplateLocationCache"]);
3869  $this->TemplateLocationCacheInterval =
3870  $this->Settings["TemplateLocationCacheInterval"];
3871  $this->TemplateLocationCacheExpiration =
3872  strtotime($this->Settings["TemplateLocationCacheExpiration"]);
3873 
3874  # if template location cache looks invalid or has expired
3875  $CurrentTime = time();
3876  if (!count($this->TemplateLocationCache)
3877  || ($CurrentTime >= $this->TemplateLocationCacheExpiration))
3878  {
3879  # clear cache and reset cache expiration
3880  $this->TemplateLocationCache = array();
3881  $this->TemplateLocationCacheExpiration =
3882  $CurrentTime + ($this->TemplateLocationCacheInterval * 60);
3883  $this->SaveTemplateLocationCache = TRUE;
3884  }
3885 
3886  # retrieve object location cache
3887  self::$ObjectLocationCache =
3888  unserialize($this->Settings["ObjectLocationCache"]);
3889  self::$ObjectLocationCacheInterval =
3890  $this->Settings["ObjectLocationCacheInterval"];
3891  self::$ObjectLocationCacheExpiration =
3892  strtotime($this->Settings["ObjectLocationCacheExpiration"]);
3893 
3894  # if object location cache looks invalid or has expired
3895  if (!count(self::$ObjectLocationCache)
3896  || ($CurrentTime >= self::$ObjectLocationCacheExpiration))
3897  {
3898  # clear cache and reset cache expiration
3899  self::$ObjectLocationCache = array();
3900  self::$ObjectLocationCacheExpiration =
3901  $CurrentTime + (self::$ObjectLocationCacheInterval * 60);
3902  self::$SaveObjectLocationCache = TRUE;
3903  }
3904  }
3905 
3912  private function RewriteCleanUrls($PageName)
3913  {
3914  # if URL rewriting is supported by the server
3915  if ($this->HtaccessSupport())
3916  {
3917  # retrieve current URL and remove base path if present
3918  $Url = $this->GetPageLocation();
3919 
3920  # for each clean URL mapping
3921  foreach ($this->CleanUrlMappings as $Info)
3922  {
3923  # if current URL matches clean URL pattern
3924  if (preg_match($Info["Pattern"], $Url, $Matches))
3925  {
3926  # set new page
3927  $PageName = $Info["Page"];
3928 
3929  # if $_GET variables specified for clean URL
3930  if ($Info["GetVars"] !== NULL)
3931  {
3932  # for each $_GET variable specified for clean URL
3933  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
3934  {
3935  # start with template for variable value
3936  $Value = $VarTemplate;
3937 
3938  # for each subpattern matched in current URL
3939  foreach ($Matches as $Index => $Match)
3940  {
3941  # if not first (whole) match
3942  if ($Index > 0)
3943  {
3944  # make any substitutions in template
3945  $Value = str_replace("$".$Index, $Match, $Value);
3946  }
3947  }
3948 
3949  # set $_GET variable
3950  $_GET[$VarName] = $Value;
3951  }
3952  }
3953 
3954  # set flag indicating clean URL mapped
3955  $this->CleanUrlRewritePerformed = TRUE;
3956 
3957  # stop looking for a mapping
3958  break;
3959  }
3960  }
3961  }
3962 
3963  # return (possibly) updated page name to caller
3964  return $PageName;
3965  }
3966 
3979  private function RewriteAlternateDomainUrls($Html)
3980  {
3981  if ($this->HtaccessSupport() &&
3982  self::$RootUrlOverride !== NULL)
3983  {
3984  $VHost = $_SERVER["SERVER_NAME"];
3985  if (isset($this->AlternateDomainPrefixes[$VHost]))
3986  {
3987  $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
3988 
3989  # get the URL for the primary domain
3990  $RootUrl = $this->RootUrl()."/";
3991 
3992  # and figure out what protcol we were using
3993  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
3994 
3995  # convert all relative URLs absolute URLs within our
3996  # primary domain, then substitute in the alternate domain
3997  # for paths inside our configured prefix
3998  $RelativePathPatterns = array(
3999  "%src=\"(?!http://|https://)%i",
4000  "%src='(?!http://|https://)%i",
4001  "%href=\"(?!http://|https://)%i",
4002  "%href='(?!http://|https://)%i",
4003  "%action=\"(?!http://|https://)%i",
4004  "%action='(?!http://|https://)%i",
4005  "%@import\s+url\(\"(?!http://|https://)%i",
4006  "%@import\s+url\('(?!http://|https://)%i",
4007  "%src:\s+url\(\"(?!http://|https://)%i",
4008  "%src:\s+url\('(?!http://|https://)%i",
4009  "%@import\s+\"(?!http://|https://)%i",
4010  "%@import\s+'(?!http://|https://)%i",
4011  "%".preg_quote($RootUrl.$ThisPrefix."/", "%")."%",
4012  );
4013  $RelativePathReplacements = array(
4014  "src=\"".$RootUrl,
4015  "src='".$RootUrl,
4016  "href=\"".$RootUrl,
4017  "href='".$RootUrl,
4018  "action=\"".$RootUrl,
4019  "action='".$RootUrl,
4020  "@import url(\"".$RootUrl,
4021  "@import url('".$RootUrl,
4022  "src: url(\"".$RootUrl,
4023  "src: url('".$RootUrl,
4024  "@import \"".$RootUrl,
4025  "@import '".$RootUrl,
4026  $Protocol."://".$VHost."/",
4027  );
4028 
4029  $NewHtml = preg_replace(
4030  $RelativePathPatterns,
4031  $RelativePathReplacements,
4032  $Html);
4033 
4034  # check to make sure relative path fixes didn't fail
4035  $Html = $this->CheckOutputModification(
4036  $Html, $NewHtml,
4037  "alternate domain substitutions");
4038  }
4039  }
4040 
4041  return $Html;
4042  }
4043 
4062  private function FindFile($DirectoryList, $BaseName,
4063  $PossibleSuffixes = NULL, $PossiblePrefixes = NULL)
4064  {
4065  # generate template cache index for this page
4066  $CacheIndex = md5(serialize($DirectoryList))
4067  .":".self::$ActiveUI.":".$BaseName;
4068 
4069  # if caching is enabled and we have cached location
4070  if (($this->TemplateLocationCacheInterval > 0)
4071  && array_key_exists($CacheIndex,
4072  $this->TemplateLocationCache))
4073  {
4074  # use template location from cache
4075  $FoundFileName = $this->TemplateLocationCache[$CacheIndex];
4076  }
4077  else
4078  {
4079  # if suffixes specified and base name does not include suffix
4080  if (count($PossibleSuffixes)
4081  && !preg_match("/\.[a-zA-Z0-9]+$/", $BaseName))
4082  {
4083  # add versions of file names with suffixes to file name list
4084  $FileNames = array();
4085  foreach ($PossibleSuffixes as $Suffix)
4086  {
4087  $FileNames[] = $BaseName.".".$Suffix;
4088  }
4089  }
4090  else
4091  {
4092  # use base name as file name
4093  $FileNames = array($BaseName);
4094  }
4095 
4096  # if prefixes specified
4097  if (count($PossiblePrefixes))
4098  {
4099  # add versions of file names with prefixes to file name list
4100  $NewFileNames = array();
4101  foreach ($FileNames as $FileName)
4102  {
4103  foreach ($PossiblePrefixes as $Prefix)
4104  {
4105  $NewFileNames[] = $Prefix.$FileName;
4106  }
4107  }
4108  $FileNames = $NewFileNames;
4109  }
4110 
4111  # for each possible location
4112  $FoundFileName = NULL;
4113  foreach ($DirectoryList as $Dir)
4114  {
4115  # substitute active or default UI name into path
4116  $Dir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4117  array(self::$ActiveUI, self::$DefaultUI), $Dir);
4118 
4119  # for each possible file name
4120  foreach ($FileNames as $File)
4121  {
4122  # if template is found at location
4123  if (file_exists($Dir.$File))
4124  {
4125  # save full template file name and stop looking
4126  $FoundFileName = $Dir.$File;
4127  break 2;
4128  }
4129  }
4130  }
4131 
4132  # save location in cache
4133  $this->TemplateLocationCache[$CacheIndex]
4134  = $FoundFileName;
4135 
4136  # set flag indicating that cache should be saved
4137  $this->SaveTemplateLocationCache = TRUE;
4138  }
4139 
4140  # return full template file name to caller
4141  return $FoundFileName;
4142  }
4143 
4152  private function CompileScssFile($SrcFile)
4153  {
4154  # build path to CSS file
4155  $DstFile = self::$ScssCacheDir."/".dirname($SrcFile)
4156  ."/".basename($SrcFile);
4157  $DstFile = substr_replace($DstFile, "css", -4);
4158 
4159  # if SCSS file is newer than CSS file
4160  if (!file_exists($DstFile)
4161  || (filemtime($SrcFile) > filemtime($DstFile)))
4162  {
4163  # attempt to create CSS cache subdirectory if not present
4164  if (!is_dir(dirname($DstFile)))
4165  {
4166  @mkdir(dirname($DstFile), 0777, TRUE);
4167  }
4168 
4169  # if CSS cache directory and CSS file path appear writable
4170  static $CacheDirIsWritable;
4171  if (!isset($CacheDirIsWritable))
4172  { $CacheDirIsWritable = is_writable(self::$ScssCacheDir); }
4173  if (is_writable($DstFile)
4174  || (!file_exists($DstFile) && $CacheDirIsWritable))
4175  {
4176  # load SCSS and compile to CSS
4177  $ScssCode = file_get_contents($SrcFile);
4178  $ScssCompiler = new scssc();
4179  $ScssCompiler->setFormatter($this->GenerateCompactCss()
4180  ? "scss_formatter_compressed" : "scss_formatter");
4181  try
4182  {
4183  $CssCode = $ScssCompiler->compile($ScssCode);
4184 
4185  # add fingerprinting for URLs in CSS
4186  $this->CssUrlFingerprintPath = dirname($SrcFile);
4187  $CssCode = preg_replace_callback(
4188  "/url\((['\"]*)(.+)\.([a-z]+)(['\"]*)\)/",
4189  array($this, "CssUrlFingerprintInsertion"),
4190  $CssCode);
4191 
4192  # strip out comments from CSS (if requested)
4193  if ($this->GenerateCompactCss())
4194  {
4195  $CssCode = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!',
4196  '', $CssCode);
4197  }
4198 
4199  # write out CSS file
4200  file_put_contents($DstFile, $CssCode);
4201  }
4202  catch (Exception $Ex)
4203  {
4204  $this->LogError(self::LOGLVL_ERROR,
4205  "Error compiling SCSS file ".$SrcFile.": "
4206  .$Ex->getMessage());
4207  $DstFile = NULL;
4208  }
4209  }
4210  else
4211  {
4212  # log error and set CSS file path to indicate failure
4213  $this->LogError(self::LOGLVL_ERROR,
4214  "Unable to write out CSS file (compiled from SCSS) to "
4215  .$DstFile);
4216  $DstFile = NULL;
4217  }
4218  }
4219 
4220  # return CSS file path to caller
4221  return $DstFile;
4222  }
4223 
4231  private function MinimizeJavascriptFile($SrcFile)
4232  {
4233  # bail out if file is on exclusion list
4234  foreach ($this->DoNotMinimizeList as $DNMFile)
4235  {
4236  if (($SrcFile == $DNMFile) || (basename($SrcFile) == $DNMFile))
4237  {
4238  return NULL;
4239  }
4240  }
4241 
4242  # build path to minimized file
4243  $DstFile = self::$JSMinCacheDir."/".dirname($SrcFile)
4244  ."/".basename($SrcFile);
4245  $DstFile = substr_replace($DstFile, ".min", -3, 0);
4246 
4247  # if original file is newer than minimized file
4248  if (!file_exists($DstFile)
4249  || (filemtime($SrcFile) > filemtime($DstFile)))
4250  {
4251  # attempt to create cache subdirectory if not present
4252  if (!is_dir(dirname($DstFile)))
4253  {
4254  @mkdir(dirname($DstFile), 0777, TRUE);
4255  }
4256 
4257  # if cache directory and minimized file path appear writable
4258  static $CacheDirIsWritable;
4259  if (!isset($CacheDirIsWritable))
4260  { $CacheDirIsWritable = is_writable(self::$JSMinCacheDir); }
4261  if (is_writable($DstFile)
4262  || (!file_exists($DstFile) && $CacheDirIsWritable))
4263  {
4264  # load JavaScript code
4265  $Code = file_get_contents($SrcFile);
4266 
4267  # decide which minimizer to use
4268  if ($this->JSMinimizerJavaScriptPackerAvailable
4269  && $this->JSMinimizerJShrinkAvailable)
4270  {
4271  $Minimizer = (strlen($Code) < 5000)
4272  ? "JShrink" : "JavaScriptPacker";
4273  }
4274  elseif ($this->JSMinimizerJShrinkAvailable)
4275  {
4276  $Minimizer = "JShrink";
4277  }
4278  else
4279  {
4280  $Minimizer = "NONE";
4281  }
4282 
4283  # minimize code
4284  switch ($Minimizer)
4285  {
4286  case "JavaScriptMinimizer":
4287  $Packer = new JavaScriptPacker($Code, "Normal");
4288  $MinimizedCode = $Packer->pack();
4289  break;
4290 
4291  case "JShrink":
4292  try
4293  {
4294  $MinimizedCode = \JShrink\Minifier::minify($Code);
4295  }
4296  catch (Exception $Exception)
4297  {
4298  unset($MinimizedCode);
4299  $MinimizeError = $Exception->getMessage();
4300  }
4301  break;
4302  }
4303 
4304  # if minimization succeeded
4305  if (isset($MinimizedCode))
4306  {
4307  # write out minimized file
4308  file_put_contents($DstFile, $MinimizedCode);
4309  }
4310  else
4311  {
4312  # log error and set destination file path to indicate failure
4313  $ErrMsg = "Unable to minimize JavaScript file ".$SrcFile;
4314  if (isset($MinimizeError))
4315  {
4316  $ErrMsg .= " (".$MinimizeError.")";
4317  }
4318  $this->LogError(self::LOGLVL_ERROR, $ErrMsg);
4319  $DstFile = NULL;
4320  }
4321  }
4322  else
4323  {
4324  # log error and set destination file path to indicate failure
4325  $this->LogError(self::LOGLVL_ERROR,
4326  "Unable to write out minimized JavaScript to file ".$DstFile);
4327  $DstFile = NULL;
4328  }
4329  }
4330 
4331  # return CSS file path to caller
4332  return $DstFile;
4333  }
4334 
4342  private function CssUrlFingerprintInsertion($Matches)
4343  {
4344  # generate fingerprint string from CSS file modification time
4345  $FileName = realpath($this->CssUrlFingerprintPath."/".
4346  $Matches[2].".".$Matches[3]);
4347  $MTime = filemtime($FileName);
4348  $Fingerprint = sprintf("%06X", ($MTime % 0xFFFFFF));
4349 
4350  # build URL string with fingerprint and return it to caller
4351  return "url(".$Matches[1].$Matches[2].".".$Fingerprint
4352  .".".$Matches[3].$Matches[4].")";
4353  }
4354 
4361  private function GetRequiredFilesNotYetLoaded($PageContentFile)
4362  {
4363  # start out assuming no files required
4364  $RequiredFiles = array();
4365 
4366  # if page content file supplied
4367  if ($PageContentFile)
4368  {
4369  # if file containing list of required files is available
4370  $Path = dirname($PageContentFile);
4371  $RequireListFile = $Path."/REQUIRES";
4372  if (file_exists($RequireListFile))
4373  {
4374  # read in list of required files
4375  $RequestedFiles = file($RequireListFile);
4376 
4377  # for each line in required file list
4378  foreach ($RequestedFiles as $Line)
4379  {
4380  # if line is not a comment
4381  $Line = trim($Line);
4382  if (!preg_match("/^#/", $Line))
4383  {
4384  # if file has not already been loaded
4385  if (!in_array($Line, $this->FoundUIFiles))
4386  {
4387  # add to list of required files
4388  $RequiredFiles[] = $Line;
4389  }
4390  }
4391  }
4392  }
4393  }
4394 
4395  # add in additional required files if any
4396  if (count($this->AdditionalRequiredUIFiles))
4397  {
4398  # make sure there are no duplicates
4399  $AdditionalRequiredUIFiles = array_unique(
4400  $this->AdditionalRequiredUIFiles);
4401 
4402  $RequiredFiles = array_merge(
4403  $RequiredFiles, $AdditionalRequiredUIFiles);
4404  }
4405 
4406  # return list of required files to caller
4407  return $RequiredFiles;
4408  }
4409 
4420  private function GetUIFileLoadingTag($FileName, $AdditionalAttributes = NULL)
4421  {
4422  # pad additional attributes if supplied
4423  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
4424 
4425  # retrieve type of UI file
4426  $FileType = $this->GetFileType($FileName);
4427 
4428  # construct tag based on file type
4429  switch ($FileType)
4430  {
4431  case self::FT_CSS:
4432  $Tag = "<link rel=\"stylesheet\" type=\"text/css\""
4433  ." media=\"all\" href=\"".$FileName."\""
4434  .$AddAttribs." />";
4435  break;
4436 
4437  case self::FT_JAVASCRIPT:
4438  $Tag = "<script type=\"text/javascript\""
4439  ." src=\"".$FileName."\""
4440  .$AddAttribs."></script>";
4441  break;
4442 
4443  default:
4444  $Tag = "";
4445  break;
4446  }
4447 
4448  # return constructed tag to caller
4449  return $Tag;
4450  }
4451 
4457  public static function AutoloadObjects($ClassName)
4458  {
4459  # if caching is not turned off
4460  # and we have a cached location for class
4461  # and file at cached location is readable
4462  if ((self::$ObjectLocationCacheInterval > 0)
4463  && array_key_exists($ClassName,
4464  self::$ObjectLocationCache)
4465  && is_readable(self::$ObjectLocationCache[$ClassName]))
4466  {
4467  # use object location from cache
4468  require_once(self::$ObjectLocationCache[$ClassName]);
4469  }
4470  else
4471  {
4472  # convert any namespace separators in class name
4473  $ClassName = str_replace("\\", "-", $ClassName);
4474 
4475  # for each possible object file directory
4476  static $FileLists;
4477  foreach (self::$ObjectDirectories as $Location => $Info)
4478  {
4479  # make any needed replacements in directory path
4480  $Location = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4481  array(self::$ActiveUI, self::$DefaultUI), $Location);
4482 
4483  # if directory looks valid
4484  if (is_dir($Location))
4485  {
4486  # build class file name
4487  $NewClassName = ($Info["ClassPattern"] && $Info["ClassReplacement"])
4488  ? preg_replace($Info["ClassPattern"],
4489  $Info["ClassReplacement"], $ClassName)
4490  : $ClassName;
4491 
4492  # read in directory contents if not already retrieved
4493  if (!isset($FileLists[$Location]))
4494  {
4495  $FileLists[$Location] = self::ReadDirectoryTree(
4496  $Location, '/^.+\.php$/i');
4497  }
4498 
4499  # for each file in target directory
4500  $FileNames = $FileLists[$Location];
4501  $TargetName = strtolower($Info["Prefix"].$NewClassName.".php");
4502  foreach ($FileNames as $FileName)
4503  {
4504  # if file matches our target object file name
4505  if (strtolower($FileName) == $TargetName)
4506  {
4507  # include object file
4508  require_once($Location.$FileName);
4509 
4510  # save location to cache
4511  self::$ObjectLocationCache[$ClassName]
4512  = $Location.$FileName;
4513 
4514  # set flag indicating that cache should be saved
4515  self::$SaveObjectLocationCache = TRUE;
4516 
4517  # stop looking
4518  break 2;
4519  }
4520  }
4521  }
4522  }
4523  }
4524  }
4534  private static function ReadDirectoryTree($Directory, $Pattern)
4535  {
4536  $CurrentDir = getcwd();
4537  chdir($Directory);
4538  $DirIter = new RecursiveDirectoryIterator(".");
4539  $IterIter = new RecursiveIteratorIterator($DirIter);
4540  $RegexResults = new RegexIterator($IterIter, $Pattern,
4541  RecursiveRegexIterator::GET_MATCH);
4542  $FileList = array();
4543  foreach ($RegexResults as $Result)
4544  {
4545  $FileList[] = substr($Result[0], 2);
4546  }
4547  chdir($CurrentDir);
4548  return $FileList;
4549  }
4550 
4554  private function UndoMagicQuotes()
4555  {
4556  # if this PHP version has magic quotes support
4557  if (version_compare(PHP_VERSION, "5.4.0", "<"))
4558  {
4559  # turn off runtime magic quotes if on
4560  if (get_magic_quotes_runtime())
4561  {
4562  // @codingStandardsIgnoreStart
4563  set_magic_quotes_runtime(FALSE);
4564  // @codingStandardsIgnoreEnd
4565  }
4566 
4567  # if magic quotes GPC is on
4568  if (get_magic_quotes_gpc())
4569  {
4570  # strip added slashes from incoming variables
4571  $GPC = array(&$_GET, &$_POST, &$_COOKIE, &$_REQUEST);
4572  array_walk_recursive($GPC,
4573  array($this, "UndoMagicQuotes_StripCallback"));
4574  }
4575  }
4576  }
4581  private function UndoMagicQuotes_StripCallback(&$Value)
4582  {
4583  $Value = stripslashes($Value);
4584  }
4585 
4590  private function LoadUIFunctions()
4591  {
4592  $Dirs = array(
4593  "local/interface/%ACTIVEUI%/include",
4594  "interface/%ACTIVEUI%/include",
4595  "local/interface/%DEFAULTUI%/include",
4596  "interface/%DEFAULTUI%/include",
4597  );
4598  foreach ($Dirs as $Dir)
4599  {
4600  $Dir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4601  array(self::$ActiveUI, self::$DefaultUI), $Dir);
4602  if (is_dir($Dir))
4603  {
4604  $FileNames = scandir($Dir);
4605  foreach ($FileNames as $FileName)
4606  {
4607  if (preg_match("/^F-([A-Za-z0-9_]+)\.php/",
4608  $FileName, $Matches)
4609  || preg_match("/^F-([A-Za-z0-9_]+)\.html/",
4610  $FileName, $Matches))
4611  {
4612  if (!function_exists($Matches[1]))
4613  {
4614  include_once($Dir."/".$FileName);
4615  }
4616  }
4617  }
4618  }
4619  }
4620  }
4621 
4627  private function ProcessPeriodicEvent($EventName, $Callback)
4628  {
4629  # retrieve last execution time for event if available
4630  $Signature = self::GetCallbackSignature($Callback);
4631  $LastRun = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
4632  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
4633 
4634  # determine whether enough time has passed for event to execute
4635  $ShouldExecute = (($LastRun === NULL)
4636  || (time() > (strtotime($LastRun) + $this->EventPeriods[$EventName])))
4637  ? TRUE : FALSE;
4638 
4639  # if event should run
4640  if ($ShouldExecute)
4641  {
4642  # add event to task queue
4643  $WrapperCallback = array("ApplicationFramework", "PeriodicEventWrapper");
4644  $WrapperParameters = array(
4645  $EventName, $Callback, array("LastRunAt" => $LastRun));
4646  $this->QueueUniqueTask($WrapperCallback, $WrapperParameters);
4647  }
4648 
4649  # add event to list of periodic events
4650  $this->KnownPeriodicEvents[$Signature] = array(
4651  "Period" => $EventName,
4652  "Callback" => $Callback,
4653  "Queued" => $ShouldExecute);
4654  }
4655 
4663  private static function PeriodicEventWrapper($EventName, $Callback, $Parameters)
4664  {
4665  static $DB;
4666  if (!isset($DB)) { $DB = new Database(); }
4667 
4668  # run event
4669  $ReturnVal = call_user_func_array($Callback, $Parameters);
4670 
4671  # if event is already in database
4672  $Signature = self::GetCallbackSignature($Callback);
4673  if ($DB->Query("SELECT COUNT(*) AS EventCount FROM PeriodicEvents"
4674  ." WHERE Signature = '".addslashes($Signature)."'", "EventCount"))
4675  {
4676  # update last run time for event
4677  $DB->Query("UPDATE PeriodicEvents SET LastRunAt = "
4678  .(($EventName == "EVENT_PERIODIC")
4679  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
4680  : "NOW()")
4681  ." WHERE Signature = '".addslashes($Signature)."'");
4682  }
4683  else
4684  {
4685  # add last run time for event to database
4686  $DB->Query("INSERT INTO PeriodicEvents (Signature, LastRunAt) VALUES "
4687  ."('".addslashes($Signature)."', "
4688  .(($EventName == "EVENT_PERIODIC")
4689  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
4690  : "NOW()").")");
4691  }
4692  }
4693 
4699  private static function GetCallbackSignature($Callback)
4700  {
4701  return !is_array($Callback) ? $Callback
4702  : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
4703  ."::".$Callback[1];
4704  }
4705 
4710  private function PrepForTSR()
4711  {
4712  # if HTML has been output and it's time to launch another task
4713  # (only TSR if HTML has been output because otherwise browsers
4714  # may misbehave after connection is closed)
4715  if ((PHP_SAPI != "cli")
4716  && ($this->JumpToPage || !$this->SuppressHTML)
4717  && (time() > (strtotime($this->Settings["LastTaskRunAt"])
4718  + (ini_get("max_execution_time")
4719  / $this->Settings["MaxTasksRunning"]) + 5))
4720  && $this->GetTaskQueueSize()
4721  && $this->Settings["TaskExecutionEnabled"])
4722  {
4723  # begin buffering output for TSR
4724  ob_start();
4725 
4726  # let caller know it is time to launch another task
4727  return TRUE;
4728  }
4729  else
4730  {
4731  # let caller know it is not time to launch another task
4732  return FALSE;
4733  }
4734  }
4735 
4740  private function LaunchTSR()
4741  {
4742  # set headers to close out connection to browser
4743  if (!$this->NoTSR)
4744  {
4745  ignore_user_abort(TRUE);
4746  header("Connection: close");
4747  header("Content-Length: ".ob_get_length());
4748  }
4749 
4750  # output buffered content
4751  while (ob_get_level()) { ob_end_flush(); }
4752  flush();
4753 
4754  # write out any outstanding data and end HTTP session
4755  session_write_close();
4756 
4757  # set flag indicating that we are now running in background
4758  $this->RunningInBackground = TRUE;
4759 
4760  # handle garbage collection for session data
4761  if (isset($this->SessionStorage) &&
4762  (rand()/getrandmax()) <= $this->SessionGcProbability)
4763  {
4764  # determine when sessions will expire
4765  $ExpiredTime = strtotime("-". self::$SessionLifetime." seconds");
4766 
4767  # iterate over the files in our session storage, deleting the expired
4768  # ones (sess_ is php's session prefix)
4769  foreach (scandir($this->SessionStorage) as $TgtFile)
4770  {
4771  $FullPath = $this->SessionStorage."/".$TgtFile;
4772  if ( is_file($FullPath) &&
4773  preg_match("/^sess_/", $TgtFile) &&
4774  filectime($FullPath) < $ExpiredTime)
4775  {
4776  unlink($FullPath);
4777  }
4778  }
4779  }
4780 
4781  # if there is still a task in the queue
4782  if ($this->GetTaskQueueSize())
4783  {
4784  # garbage collect to give as much memory as possible for tasks
4785  if (function_exists("gc_collect_cycles")) { gc_collect_cycles(); }
4786 
4787  # turn on output buffering to (hopefully) record any crash output
4788  ob_start();
4789 
4790  # lock tables and grab last task run time to double check
4791  $this->DB->Query("LOCK TABLES ApplicationFrameworkSettings WRITE");
4792  $this->LoadSettings();
4793 
4794  # if still time to launch another task
4795  if (time() > (strtotime($this->Settings["LastTaskRunAt"])
4796  + (ini_get("max_execution_time")
4797  / $this->Settings["MaxTasksRunning"]) + 5))
4798  {
4799  # update the "last run" time and release tables
4800  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
4801  ." SET LastTaskRunAt = '".date("Y-m-d H:i:s")."'");
4802  $this->DB->Query("UNLOCK TABLES");
4803 
4804  # run tasks while there is a task in the queue
4805  # and enough time and memory left
4806  do
4807  {
4808  # run the next task
4809  $this->RunNextTask();
4810 
4811  # calculate percentage of memory still available
4812  $PercentFreeMem = (self::GetFreeMemory()
4813  / self::GetPhpMemoryLimit()) * 100;
4814  }
4815  while ($this->GetTaskQueueSize()
4816  && ($this->GetSecondsBeforeTimeout() > 65)
4817  && ($PercentFreeMem > $this->BackgroundTaskMinFreeMemPercent));
4818  }
4819  else
4820  {
4821  # release tables
4822  $this->DB->Query("UNLOCK TABLES");
4823  }
4824  }
4825  }
4826 
4836  private function GetTaskList($DBQuery, $Count, $Offset)
4837  {
4838  $this->DB->Query($DBQuery." LIMIT ".intval($Offset).",".intval($Count));
4839  $Tasks = array();
4840  while ($Row = $this->DB->FetchRow())
4841  {
4842  $Tasks[$Row["TaskId"]] = $Row;
4843  if ($Row["Callback"] ==
4844  serialize(array("ApplicationFramework", "PeriodicEventWrapper")))
4845  {
4846  $WrappedCallback = unserialize($Row["Parameters"]);
4847  $Tasks[$Row["TaskId"]]["Callback"] = $WrappedCallback[1];
4848  $Tasks[$Row["TaskId"]]["Parameters"] = NULL;
4849  }
4850  else
4851  {
4852  $Tasks[$Row["TaskId"]]["Callback"] = unserialize($Row["Callback"]);
4853  $Tasks[$Row["TaskId"]]["Parameters"] = unserialize($Row["Parameters"]);
4854  }
4855  }
4856  return $Tasks;
4857  }
4858 
4862  private function RunNextTask()
4863  {
4864  # lock tables to prevent same task from being run by multiple sessions
4865  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
4866 
4867  # look for task at head of queue
4868  $this->DB->Query("SELECT * FROM TaskQueue ORDER BY Priority, TaskId LIMIT 1");
4869  $Task = $this->DB->FetchRow();
4870 
4871  # if there was a task available
4872  if ($Task)
4873  {
4874  # move task from queue to running tasks list
4875  $this->DB->Query("INSERT INTO RunningTasks "
4876  ."(TaskId,Callback,Parameters,Priority,Description) "
4877  ."SELECT * FROM TaskQueue WHERE TaskId = "
4878  .intval($Task["TaskId"]));
4879  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = "
4880  .intval($Task["TaskId"]));
4881 
4882  # release table locks to again allow other sessions to run tasks
4883  $this->DB->Query("UNLOCK TABLES");
4884 
4885  # unpack stored task info
4886  $Callback = unserialize($Task["Callback"]);
4887  $Parameters = unserialize($Task["Parameters"]);
4888 
4889  # attempt to load task callback if not already available
4890  $this->LoadFunction($Callback);
4891 
4892  # save amount of free memory for later comparison
4893  $BeforeFreeMem = self::GetFreeMemory();
4894 
4895  # run task
4896  $this->RunningTask = $Task;
4897  if ($Parameters)
4898  {
4899  call_user_func_array($Callback, $Parameters);
4900  }
4901  else
4902  {
4903  call_user_func($Callback);
4904  }
4905  unset($this->RunningTask);
4906 
4907  # log if task leaked significant memory
4908  if (function_exists("gc_collect_cycles")) { gc_collect_cycles(); }
4909  $AfterFreeMem = self::GetFreeMemory();
4910  $LeakThreshold = self::GetPhpMemoryLimit()
4911  * ($this->BackgroundTaskMemLeakLogThreshold / 100);
4912  if (($BeforeFreeMem - $AfterFreeMem) > $LeakThreshold)
4913  {
4914  $this->LogError(self::LOGLVL_DEBUG, "Task "
4915  .self::GetTaskCallbackSynopsis(
4916  $this->GetTask($Task["TaskId"]))." leaked "
4917  .number_format($BeforeFreeMem - $AfterFreeMem)." bytes.");
4918  }
4919 
4920  # remove task from running tasks list
4921  $this->DB->Query("DELETE FROM RunningTasks"
4922  ." WHERE TaskId = ".intval($Task["TaskId"]));
4923 
4924  # prune running tasks list if necessary
4925  $RunningTasksCount = $this->DB->Query(
4926  "SELECT COUNT(*) AS TaskCount FROM RunningTasks", "TaskCount");
4927  if ($RunningTasksCount > $this->MaxRunningTasksToTrack)
4928  {
4929  $this->DB->Query("DELETE FROM RunningTasks ORDER BY StartedAt"
4930  ." LIMIT ".($RunningTasksCount - $this->MaxRunningTasksToTrack));
4931  }
4932  }
4933  else
4934  {
4935  # release table locks to again allow other sessions to run tasks
4936  $this->DB->Query("UNLOCK TABLES");
4937  }
4938  }
4939 
4945  public function OnCrash()
4946  {
4947  # attempt to remove any memory limits
4948  $FreeMemory = $this->GetFreeMemory();
4949  ini_set("memory_limit", -1);
4950 
4951  # if there is a background task currently running
4952  if (isset($this->RunningTask))
4953  {
4954  # add info about current page load
4955  $CrashInfo["ElapsedTime"] = $this->GetElapsedExecutionTime();
4956  $CrashInfo["FreeMemory"] = $FreeMemory;
4957  $CrashInfo["REMOTE_ADDR"] = $_SERVER["REMOTE_ADDR"];
4958  $CrashInfo["REQUEST_URI"] = $_SERVER["REQUEST_URI"];
4959  if (isset($_SERVER["REQUEST_TIME"]))
4960  {
4961  $CrashInfo["REQUEST_TIME"] = $_SERVER["REQUEST_TIME"];
4962  }
4963  if (isset($_SERVER["REMOTE_HOST"]))
4964  {
4965  $CrashInfo["REMOTE_HOST"] = $_SERVER["REMOTE_HOST"];
4966  }
4967 
4968  # add info about error that caused crash (if available)
4969  if (function_exists("error_get_last"))
4970  {
4971  $CrashInfo["LastError"] = error_get_last();
4972  }
4973 
4974  # add info about current output buffer contents (if available)
4975  if (ob_get_length() !== FALSE)
4976  {
4977  $CrashInfo["OutputBuffer"] = ob_get_contents();
4978  }
4979 
4980  # if backtrace info is available for the crash
4981  $Backtrace = debug_backtrace();
4982  if (count($Backtrace) > 1)
4983  {
4984  # discard the current context from the backtrace
4985  array_shift($Backtrace);
4986 
4987  # add the backtrace to the crash info
4988  $CrashInfo["Backtrace"] = $Backtrace;
4989  }
4990  # else if saved backtrace info is available
4991  elseif (isset($this->SavedContext))
4992  {
4993  # add the saved backtrace to the crash info
4994  $CrashInfo["Backtrace"] = $this->SavedContext;
4995  }
4996 
4997  # save crash info for currently running task
4998  $DB = new Database();
4999  $DB->Query("UPDATE RunningTasks SET CrashInfo = '"
5000  .addslashes(serialize($CrashInfo))
5001  ."' WHERE TaskId = ".intval($this->RunningTask["TaskId"]));
5002  }
5003 
5004  print("\n");
5005  return;
5006  }
5007 
5024  private function AddToDirList($DirList, $Dir, $SearchLast, $SkipSlashCheck)
5025  {
5026  # convert incoming directory to array of directories (if needed)
5027  $Dirs = is_array($Dir) ? $Dir : array($Dir);
5028 
5029  # reverse array so directories are searched in specified order
5030  $Dirs = array_reverse($Dirs);
5031 
5032  # for each directory
5033  foreach ($Dirs as $Location)
5034  {
5035  # make sure directory includes trailing slash
5036  if (!$SkipSlashCheck)
5037  {
5038  $Location = $Location
5039  .((substr($Location, -1) != "/") ? "/" : "");
5040  }
5041 
5042  # remove directory from list if already present
5043  if (in_array($Location, $DirList))
5044  {
5045  $DirList = array_diff(
5046  $DirList, array($Location));
5047  }
5048 
5049  # add directory to list of directories
5050  if ($SearchLast)
5051  {
5052  array_push($DirList, $Location);
5053  }
5054  else
5055  {
5056  array_unshift($DirList, $Location);
5057  }
5058  }
5059 
5060  # return updated directory list to caller
5061  return $DirList;
5062  }
5063 
5071  private function ArrayPermutations($Items, $Perms = array())
5072  {
5073  if (empty($Items))
5074  {
5075  $Result = array($Perms);
5076  }
5077  else
5078  {
5079  $Result = array();
5080  for ($Index = count($Items) - 1; $Index >= 0; --$Index)
5081  {
5082  $NewItems = $Items;
5083  $NewPerms = $Perms;
5084  list($Segment) = array_splice($NewItems, $Index, 1);
5085  array_unshift($NewPerms, $Segment);
5086  $Result = array_merge($Result,
5087  $this->ArrayPermutations($NewItems, $NewPerms));
5088  }
5089  }
5090  return $Result;
5091  }
5092 
5099  private function OutputModificationCallbackShell($Matches)
5100  {
5101  # call previously-stored external function
5102  return call_user_func($this->OutputModificationCallbackInfo["Callback"],
5103  $Matches,
5104  $this->OutputModificationCallbackInfo["Pattern"],
5105  $this->OutputModificationCallbackInfo["Page"],
5106  $this->OutputModificationCallbackInfo["SearchPattern"]);
5107  }
5108 
5117  private function CheckOutputModification($Original, $Modified, $ErrorInfo)
5118  {
5119  # if error was reported by regex engine
5120  if (preg_last_error() !== PREG_NO_ERROR)
5121  {
5122  # log error
5123  $this->LogError(
5124  "Error reported by regex engine when modifying output."
5125  ." (".$ErrorInfo.")");
5126 
5127  # use unmodified version of output
5128  $OutputToUse = $Original;
5129  }
5130  # else if modification reduced output by more than threshold
5131  elseif ((strlen(trim($Modified)) / strlen(trim($Original)))
5132  < self::OUTPUT_MODIFICATION_THRESHOLD)
5133  {
5134  # log error
5135  $this->LogError(
5136  "Content reduced below acceptable threshold while modifying output."
5137  ." (".$ErrorInfo.")");
5138 
5139  # use unmodified version of output
5140  $OutputToUse = $Original;
5141  }
5142  else
5143  {
5144  # use modified version of output
5145  $OutputToUse = $Modified;
5146  }
5147 
5148  # return output to use to caller
5149  return $OutputToUse;
5150  }
5151 
5153  const OUTPUT_MODIFICATION_THRESHOLD = 0.10;
5154 
5164  private function UpdateSetting(
5165  $FieldName, $NewValue = DB_NOVALUE, $Persistent = TRUE)
5166  {
5167  static $LocalSettings;
5168  if ($NewValue !== DB_NOVALUE)
5169  {
5170  if ($Persistent)
5171  {
5172  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5173  "ApplicationFrameworkSettings",
5174  $FieldName, $NewValue, NULL, $this->Settings);
5175  }
5176  else
5177  {
5178  $LocalSettings[$FieldName] = $NewValue;
5179  }
5180  }
5181  elseif (!isset($LocalSettings[$FieldName]))
5182  {
5183  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5184  "ApplicationFrameworkSettings",
5185  $FieldName, $NewValue, NULL, $this->Settings);
5186  }
5187  return $LocalSettings[$FieldName];
5188  }
5189 
5191  private $InterfaceDirList = array(
5192  "local/interface/%ACTIVEUI%/",
5193  "interface/%ACTIVEUI%/",
5194  "local/interface/%DEFAULTUI%/",
5195  "interface/%DEFAULTUI%/",
5196  );
5201  private $IncludeDirList = array(
5202  "local/interface/%ACTIVEUI%/include/",
5203  "interface/%ACTIVEUI%/include/",
5204  "interface/%ACTIVEUI%/objects/",
5205  "local/interface/%DEFAULTUI%/include/",
5206  "interface/%DEFAULTUI%/include/",
5207  "interface/%DEFAULTUI%/objects/",
5208  );
5210  private $ImageDirList = array(
5211  "local/interface/%ACTIVEUI%/images/",
5212  "interface/%ACTIVEUI%/images/",
5213  "local/interface/%DEFAULTUI%/images/",
5214  "interface/%DEFAULTUI%/images/",
5215  );
5217  private $FunctionDirList = array(
5218  "local/interface/%ACTIVEUI%/include/",
5219  "interface/%ACTIVEUI%/include/",
5220  "local/interface/%DEFAULTUI%/include/",
5221  "interface/%DEFAULTUI%/include/",
5222  "local/include/",
5223  "include/",
5224  );
5225 
5226  const NOVALUE = ".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";
5227 
5228 
5229  # ---- Page Caching (Internal Methods) -----------------------------------
5230 
5236  private function CheckForCachedPage($PageName)
5237  {
5238  # assume no cached page will be found
5239  $CachedPage = NULL;
5240 
5241  # if returning a cached page is allowed
5242  if ($this->CacheCurrentPage)
5243  {
5244  # get fingerprint for requested page
5245  $PageFingerprint = $this->GetPageFingerprint($PageName);
5246 
5247  # look for matching page in cache in database
5248  $this->DB->Query("SELECT * FROM AF_CachedPages"
5249  ." WHERE Fingerprint = '".addslashes($PageFingerprint)."'");
5250 
5251  # if matching page found
5252  if ($this->DB->NumRowsSelected())
5253  {
5254  # if cached page has expired
5255  $Row = $this->DB->FetchRow();
5256  $ExpirationTime = strtotime(
5257  "-".$this->PageCacheExpirationPeriod()." seconds");
5258  if (strtotime($Row["CachedAt"]) < $ExpirationTime)
5259  {
5260  # clear expired pages from cache
5261  $ExpirationTimestamp = date("Y-m-d H:i:s", $ExpirationTime);
5262  $this->DB->Query("DELETE CP, CPTI FROM AF_CachedPages CP,"
5263  ." AF_CachedPageTagInts CPTI"
5264  ." WHERE CP.CachedAt < '".$ExpirationTimestamp."'"
5265  ." AND CPTI.CacheId = CP.CacheId");
5266  $this->DB->Query("DELETE FROM AF_CachedPages "
5267  ." WHERE CachedAt < '".$ExpirationTimestamp."'");
5268  }
5269  else
5270  {
5271  # display cached page and exit
5272  $CachedPage = $Row["PageContent"];
5273  }
5274  }
5275  }
5276 
5277  # return any cached page found to caller
5278  return $CachedPage;
5279  }
5280 
5286  private function UpdatePageCache($PageName, $PageContent)
5287  {
5288  # if page caching is enabled and current page should be cached
5289  if ($this->PageCacheEnabled()
5290  && $this->CacheCurrentPage
5291  && ($PageName != "404"))
5292  {
5293  # if page content looks invalid
5294  if (strlen(trim(strip_tags($PageContent))) == 0)
5295  {
5296  # log error
5297  $LogMsg = "Page not cached because content was empty."
5298  ." (PAGE: ".$PageName.", URL: ".$this->FullUrl().")";
5299  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
5300  }
5301  else
5302  {
5303  # save page to cache
5304  $PageFingerprint = $this->GetPageFingerprint($PageName);
5305  $this->DB->Query("INSERT INTO AF_CachedPages"
5306  ." (Fingerprint, PageContent) VALUES"
5307  ." ('".$this->DB->EscapeString($PageFingerprint)."', '"
5308  .$this->DB->EscapeString($PageContent)."')");
5309  $CacheId = $this->DB->LastInsertId();
5310 
5311  # for each page cache tag that was added
5312  foreach ($this->PageCacheTags as $Tag => $Pages)
5313  {
5314  # if current page is in list for tag
5315  if (in_array("CURRENT", $Pages) || in_array($PageName, $Pages))
5316  {
5317  # look up tag ID
5318  $TagId = $this->GetPageCacheTagId($Tag);
5319 
5320  # mark current page as associated with tag
5321  $this->DB->Query("INSERT INTO AF_CachedPageTagInts"
5322  ." (TagId, CacheId) VALUES "
5323  ." (".intval($TagId).", ".intval($CacheId).")");
5324  }
5325  }
5326  }
5327  }
5328  }
5329 
5335  private function GetPageCacheTagId($Tag)
5336  {
5337  # if tag is a non-negative integer
5338  if (is_numeric($Tag) && ($Tag > 0) && (intval($Tag) == $Tag))
5339  {
5340  # generate ID
5341  $Id = self::PAGECACHETAGIDOFFSET + $Tag;
5342  }
5343  else
5344  {
5345  # look up ID in database
5346  $Id = $this->DB->Query("SELECT TagId FROM AF_CachedPageTags"
5347  ." WHERE Tag = '".addslashes($Tag)."'", "TagId");
5348 
5349  # if ID was not found
5350  if ($Id === NULL)
5351  {
5352  # add tag to database
5353  $this->DB->Query("INSERT INTO AF_CachedPageTags"
5354  ." SET Tag = '".addslashes($Tag)."'");
5355  $Id = $this->DB->LastInsertId();
5356  }
5357  }
5358 
5359  # return tag ID to caller
5360  return $Id;
5361  }
5362 
5368  private function GetPageFingerprint($PageName)
5369  {
5370  # only get the environmental fingerprint once so that it is consistent
5371  # between page construction start and end
5372  static $EnvFingerprint;
5373  if (!isset($EnvFingerprint))
5374  {
5375  $EnvData = json_encode($_GET).json_encode($_POST);
5376 
5377  # if alternate domain support is enabled
5378  if ($this->HtaccessSupport() && self::$RootUrlOverride !== NULL)
5379  {
5380  # and if we were accessed via an alternate domain
5381  $VHost = $_SERVER["SERVER_NAME"];
5382  if (isset($this->AlternateDomainPrefixes[$VHost]))
5383  {
5384  # then add the alternate domain that was used to our
5385  # environment data
5386  $EnvData .= $VHost;
5387  }
5388  }
5389 
5390  $EnvFingerprint = md5($EnvData);
5391 
5392  }
5393 
5394  # build page fingerprint and return it to caller
5395  return $PageName."-".$EnvFingerprint;
5396  }
5397 }
Abstraction for forum messages and resource comments.
Definition: Message.php:15
SQL database abstraction object with smart query caching.
Definition: Database.php:22
static minify($js, $options=array())
Takes a string containing javascript and removes unneeded characters in order to shrink the code with...
SCSS compiler written in PHP.
Definition: scssc.php:45
const DB_NOVALUE
Definition: Database.php:1396