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-2014 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu
8 #
9 
14 class ApplicationFramework {
15 
16  # ---- PUBLIC INTERFACE --------------------------------------------------
17  /*@(*/
19 
24  function __construct()
25  {
26  # save execution start time
27  $this->ExecutionStartTime = microtime(TRUE);
28 
29  # begin/restore PHP session
30  $SessionDomain = isset($_SERVER["SERVER_NAME"]) ? $_SERVER["SERVER_NAME"]
31  : isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"]
32  : php_uname("n");
33  if (is_writable(session_save_path()))
34  {
35  $SessionStorage = session_save_path()
36  ."/".self::$AppName."_".md5($SessionDomain.dirname(__FILE__));
37  if (!is_dir($SessionStorage)) { mkdir($SessionStorage, 0700 ); }
38  if (is_writable($SessionStorage))
39  {
40  # save params of our session storage as instance variables for later use
41  $this->SessionGcProbability =
42  ini_get("session.gc_probability") / ini_get("session.gc_divisor");
43  $this->SessionStorage = $SessionStorage;
44 
45  # store our session files in a subdir to avoid
46  # accidentally sharing sessions with other CWIS installs
47  # on the same domain
48  session_save_path($SessionStorage);
49 
50  # set PHP's gc not to run, as it doesn't handle subdirectories anyway
51  # instead, we'll do the cleanup as we run background tasks
52  ini_set("session.gc_probability", 0);
53  }
54  }
55  ini_set("session.gc_maxlifetime", self::$SessionLifetime);
56  session_set_cookie_params(
57  self::$SessionLifetime, "/", $SessionDomain);
58  session_start();
59 
60  # set up default object file search locations
61  self::AddObjectDirectory("local/objects");
62  self::AddObjectDirectory("objects");
63  self::AddObjectDirectory("local/interface/%ACTIVEUI%/objects");
64  self::AddObjectDirectory("interface/%ACTIVEUI%/objects");
65  self::AddObjectDirectory("local/interface/default/objects");
66  self::AddObjectDirectory("interface/default/objects");
67 
68  # set up object file autoloader
69  $this->SetUpObjectAutoloading();
70 
71  # set up function to output any buffered text in case of crash
72  register_shutdown_function(array($this, "OnCrash"));
73 
74  # set up our internal environment
75  $this->DB = new Database();
76 
77  # set up our exception handler
78  set_exception_handler(array($this, "GlobalExceptionHandler"));
79 
80  # perform any work needed to undo PHP magic quotes
81  $this->UndoMagicQuotes();
82 
83  # load our settings from database
84  $this->LoadSettings();
85 
86  # set PHP maximum execution time
87  $this->MaxExecutionTime($this->Settings["MaxExecTime"]);
88 
89  # register events we handle internally
90  $this->RegisterEvent($this->PeriodicEvents);
91  $this->RegisterEvent($this->UIEvents);
92 
93  # attempt to create SCSS cache directory if needed and it does not exist
94  if ($this->ScssSupportEnabled() && !is_dir(self::$ScssCacheDir))
95  { @mkdir(self::$ScssCacheDir, 0777, TRUE); }
96 
97  # attempt to create minimized JS cache directory if needed and it does not exist
98  if ($this->UseMinimizedJavascript()
99  && $this->JavascriptMinimizationEnabled()
100  && !is_dir(self::$JSMinCacheDir))
101  {
102  @mkdir(self::$JSMinCacheDir, 0777, TRUE);
103  }
104  }
111  function __destruct()
112  {
113  # if template location cache is flagged to be saved
114  if ($this->SaveTemplateLocationCache)
115  {
116  # write template location cache out and update cache expiration
117  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
118  ." SET TemplateLocationCache = '"
119  .addslashes(serialize(
120  $this->TemplateLocationCache))."',"
121  ." TemplateLocationCacheExpiration = '"
122  .date("Y-m-d H:i:s",
123  $this->TemplateLocationCacheExpiration)."'");
124  }
125 
126  # if object location cache is flagged to be saved
127  if (self::$SaveObjectLocationCache)
128  {
129  # write object location cache out and update cache expiration
130  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
131  ." SET ObjectLocationCache = '"
132  .addslashes(serialize(
133  self::$ObjectLocationCache))."',"
134  ." ObjectLocationCacheExpiration = '"
135  .date("Y-m-d H:i:s",
136  self::$ObjectLocationCacheExpiration)."'");
137  }
138  }
145  function GlobalExceptionHandler($Exception)
146  {
147  # display exception info
148  $Location = $Exception->getFile()."[".$Exception->getLine()."]";
149  ?><table width="100%" cellpadding="5"
150  style="border: 2px solid #666666; background: #CCCCCC;
151  font-family: Courier New, Courier, monospace;
152  margin-top: 10px;"><tr><td>
153  <div style="color: #666666;">
154  <span style="font-size: 150%;">
155  <b>Uncaught Exception</b></span><br />
156  <b>Message:</b> <i><?PHP print $Exception->getMessage(); ?></i><br />
157  <b>Location:</b> <i><?PHP print $Location; ?></i><br />
158  <b>Trace:</b>
159  <blockquote><pre><?PHP print $Exception->getTraceAsString();
160  ?></pre></blockquote>
161  </div>
162  </td></tr></table><?PHP
163 
164  # log exception if possible
165  $LogMsg = "Uncaught exception (".$Exception->getMessage().").";
166  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
167  }
184  static function AddObjectDirectory(
185  $Dir, $Prefix = "", $ClassPattern = NULL, $ClassReplacement = NULL)
186  {
187  # make sure directory has trailing slash
188  $Dir = $Dir.((substr($Dir, -1) != "/") ? "/" : "");
189 
190  # add directory to directory list
191  self::$ObjectDirectories = array_merge(
192  array($Dir => array(
193  "Prefix" => $Prefix,
194  "ClassPattern" => $ClassPattern,
195  "ClassReplacement" => $ClassReplacement,
196  )),
197  self::$ObjectDirectories);
198  }
199 
219  function AddImageDirectories($Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
220  {
221  # add directories to existing image directory list
222  $this->ImageDirList = $this->AddToDirList(
223  $this->ImageDirList, $Dir, $SearchLast, $SkipSlashCheck);
224  }
225 
246  function AddIncludeDirectories($Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
247  {
248  # add directories to existing image directory list
249  $this->IncludeDirList = $this->AddToDirList(
250  $this->IncludeDirList, $Dir, $SearchLast, $SkipSlashCheck);
251  }
252 
272  function AddInterfaceDirectories($Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
273  {
274  # add directories to existing image directory list
275  $this->InterfaceDirList = $this->AddToDirList(
276  $this->InterfaceDirList, $Dir, $SearchLast, $SkipSlashCheck);
277  }
278 
298  function AddFunctionDirectories($Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
299  {
300  # add directories to existing image directory list
301  $this->FunctionDirList = $this->AddToDirList(
302  $this->FunctionDirList, $Dir, $SearchLast, $SkipSlashCheck);
303  }
304 
310  function SetBrowserDetectionFunc($DetectionFunc)
311  {
312  $this->BrowserDetectFunc = $DetectionFunc;
313  }
314 
321  function AddUnbufferedCallback($Callback, $Parameters=array())
322  {
323  if (is_callable($Callback))
324  {
325  $this->UnbufferedCallbacks[] = array($Callback, $Parameters);
326  }
327  }
328 
335  function TemplateLocationCacheExpirationInterval($NewInterval = DB_NOVALUE)
336  {
337  return $this->UpdateSetting("TemplateLocationCacheInterval", $NewInterval);
338  }
339 
346  function ObjectLocationCacheExpirationInterval($NewInterval = DB_NOVALUE)
347  {
348  return $this->UpdateSetting("ObjectLocationCacheInterval", $NewInterval);
349  }
350 
357  function UrlFingerprintingEnabled($NewValue = DB_NOVALUE)
358  {
359  return $this->UpdateSetting("UrlFingerprintingEnabled", $NewValue);
360  }
361 
369  function ScssSupportEnabled($NewValue = DB_NOVALUE)
370  {
371  return $this->UpdateSetting("ScssSupportEnabled", $NewValue);
372  }
373 
382  function GenerateCompactCss($NewValue = DB_NOVALUE)
383  {
384  return $this->UpdateSetting("GenerateCompactCss", $NewValue);
385  }
386 
395  function UseMinimizedJavascript($NewValue = DB_NOVALUE)
396  {
397  return $this->UpdateSetting("UseMinimizedJavascript", $NewValue);
398  }
399 
408  function JavascriptMinimizationEnabled($NewValue = DB_NOVALUE)
409  {
410  return $this->UpdateSetting("JavascriptMinimizationEnabled", $NewValue);
411  }
412 
426  function RecordContextInCaseOfCrash(
427  $BacktraceOptions = 0, $BacktraceLimit = 0)
428  {
429  if (version_compare(PHP_VERSION, "5.4.0", ">="))
430  {
431  $this->SavedContext = debug_backtrace(
432  $BacktraceOptions, $BacktraceLimit);
433  }
434  else
435  {
436  $this->SavedContext = debug_backtrace($BacktraceOptions);
437  }
438  array_shift($this->SavedContext);
439  }
440 
445  function LoadPage($PageName)
446  {
447  # perform any clean URL rewriting
448  $PageName = $this->RewriteCleanUrls($PageName);
449 
450  # sanitize incoming page name and save local copy
451  $PageName = preg_replace("/[^a-zA-Z0-9_.-]/", "", $PageName);
452  $this->PageName = $PageName;
453 
454  # if page caching is turned on
455  if ($this->PageCacheEnabled())
456  {
457  # if we have a cached page
458  $CachedPage = $this->CheckForCachedPage($PageName);
459  if ($CachedPage !== NULL)
460  {
461  # set header to indicate cache hit was found
462  header("X-ScoutAF-Cache: HIT");
463 
464  # display cached page and exit
465  print $CachedPage;
466  return;
467  }
468  else
469  {
470  # set header to indicate no cache hit was found
471  header("X-ScoutAF-Cache: MISS");
472  }
473  }
474 
475  # buffer any output from includes or PHP file
476  ob_start();
477 
478  # include any files needed to set up execution environment
479  foreach ($this->EnvIncludes as $IncludeFile)
480  {
481  include($IncludeFile);
482  }
483 
484  # signal page load
485  $this->SignalEvent("EVENT_PAGE_LOAD", array("PageName" => $PageName));
486 
487  # signal PHP file load
488  $SignalResult = $this->SignalEvent("EVENT_PHP_FILE_LOAD", array(
489  "PageName" => $PageName));
490 
491  # if signal handler returned new page name value
492  $NewPageName = $PageName;
493  if (($SignalResult["PageName"] != $PageName)
494  && strlen($SignalResult["PageName"]))
495  {
496  # if new page name value is page file
497  if (file_exists($SignalResult["PageName"]))
498  {
499  # use new value for PHP file name
500  $PageFile = $SignalResult["PageName"];
501  }
502  else
503  {
504  # use new value for page name
505  $NewPageName = $SignalResult["PageName"];
506  }
507 
508  # update local copy of page name
509  $this->PageName = $NewPageName;
510  }
511 
512  # if we do not already have a PHP file
513  if (!isset($PageFile))
514  {
515  # look for PHP file for page
516  $OurPageFile = "pages/".$NewPageName.".php";
517  $LocalPageFile = "local/pages/".$NewPageName.".php";
518  $PageFile = file_exists($LocalPageFile) ? $LocalPageFile
519  : (file_exists($OurPageFile) ? $OurPageFile
520  : "pages/".$this->DefaultPage.".php");
521  }
522 
523  # load PHP file
524  include($PageFile);
525 
526  # save buffered output to be displayed later after HTML file loads
527  $PageOutput = ob_get_contents();
528  ob_end_clean();
529 
530  # signal PHP file load is complete
531  ob_start();
532  $Context["Variables"] = get_defined_vars();
533  $this->SignalEvent("EVENT_PHP_FILE_LOAD_COMPLETE",
534  array("PageName" => $PageName, "Context" => $Context));
535  $PageCompleteOutput = ob_get_contents();
536  ob_end_clean();
537 
538  # set up for possible TSR (Terminate and Stay Resident :))
539  $ShouldTSR = $this->PrepForTSR();
540 
541  # if PHP file indicated we should autorefresh to somewhere else
542  if ($this->JumpToPage)
543  {
544  if (!strlen(trim($PageOutput)))
545  {
546  # if client supports HTTP/1.1, use a 303 as it is most accurate
547  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.1")
548  {
549  header("HTTP/1.1 303 See Other");
550  header("Location: ".$this->JumpToPage);
551  }
552  else
553  {
554  # if the request was an HTTP/1.0 GET or HEAD, then
555  # use a 302 response code.
556 
557  # NB: both RFC 2616 (HTTP/1.1) and RFC1945 (HTTP/1.0)
558  # explicitly prohibit automatic redirection via a 302
559  # if the request was not GET or HEAD.
560  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.0" &&
561  ($_SERVER["REQUEST_METHOD"] == "GET" ||
562  $_SERVER["REQUEST_METHOD"] == "HEAD") )
563  {
564  header("HTTP/1.0 302 Found");
565  header("Location: ".$this->JumpToPage);
566  }
567 
568  # otherwise, fall back to a meta refresh
569  else
570  {
571  print '<html><head><meta http-equiv="refresh" '
572  .'content="0; URL='.$this->JumpToPage.'">'
573  .'</head><body></body></html>';
574  }
575  }
576  }
577  }
578  # else if HTML loading is not suppressed
579  elseif (!$this->SuppressHTML)
580  {
581  # set content-type to get rid of diacritic errors
582  header("Content-Type: text/html; charset="
583  .$this->HtmlCharset, TRUE);
584 
585  # load common HTML file (defines common functions) if available
586  $CommonHtmlFile = $this->FindFile($this->IncludeDirList,
587  "Common", array("tpl", "html"));
588  if ($CommonHtmlFile) { include($CommonHtmlFile); }
589 
590  # load UI functions
591  $this->LoadUIFunctions();
592 
593  # begin buffering content
594  ob_start();
595 
596  # signal HTML file load
597  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD", array(
598  "PageName" => $PageName));
599 
600  # if signal handler returned new page name value
601  $NewPageName = $PageName;
602  $PageContentFile = NULL;
603  if (($SignalResult["PageName"] != $PageName)
604  && strlen($SignalResult["PageName"]))
605  {
606  # if new page name value is HTML file
607  if (file_exists($SignalResult["PageName"]))
608  {
609  # use new value for HTML file name
610  $PageContentFile = $SignalResult["PageName"];
611  }
612  else
613  {
614  # use new value for page name
615  $NewPageName = $SignalResult["PageName"];
616  }
617  }
618 
619  # load page content HTML file if available
620  if ($PageContentFile === NULL)
621  {
622  $PageContentFile = $this->FindFile(
623  $this->InterfaceDirList, $NewPageName,
624  array("tpl", "html"));
625  }
626  if ($PageContentFile)
627  {
628  include($PageContentFile);
629  }
630  else
631  {
632  print "<h2>ERROR: No HTML/TPL template found"
633  ." for this page (".$NewPageName.").</h2>";
634  }
635 
636  # signal HTML file load complete
637  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD_COMPLETE");
638 
639  # stop buffering and save output
640  $PageContentOutput = ob_get_contents();
641  ob_end_clean();
642 
643  # load page start HTML file if available
644  ob_start();
645  $PageStartFile = $this->FindFile($this->IncludeDirList, "Start",
646  array("tpl", "html"), array("StdPage", "StandardPage"));
647  if ($PageStartFile) { include($PageStartFile); }
648  $PageStartOutput = ob_get_contents();
649  ob_end_clean();
650 
651  # load page end HTML file if available
652  ob_start();
653  $PageEndFile = $this->FindFile($this->IncludeDirList, "End",
654  array("tpl", "html"), array("StdPage", "StandardPage"));
655  if ($PageEndFile) { include($PageEndFile); }
656  $PageEndOutput = ob_get_contents();
657  ob_end_clean();
658 
659  # get list of any required files not loaded
660  $RequiredFiles = $this->GetRequiredFilesNotYetLoaded($PageContentFile);
661 
662  # if a browser detection function has been made available
663  if (is_callable($this->BrowserDetectFunc))
664  {
665  # call function to get browser list
666  $Browsers = call_user_func($this->BrowserDetectFunc);
667 
668  # for each required file
669  $NewRequiredFiles = array();
670  foreach ($RequiredFiles as $File)
671  {
672  # if file name includes browser keyword
673  if (preg_match("/%BROWSER%/", $File))
674  {
675  # for each browser
676  foreach ($Browsers as $Browser)
677  {
678  # substitute in browser name and add to new file list
679  $NewRequiredFiles[] = preg_replace(
680  "/%BROWSER%/", $Browser, $File);
681  }
682  }
683  else
684  {
685  # add to new file list
686  $NewRequiredFiles[] = $File;
687  }
688  }
689  $RequiredFiles = $NewRequiredFiles;
690  }
691  else
692  {
693  # filter out any files with browser keyword in their name
694  $NewRequiredFiles = array();
695  foreach ($RequiredFiles as $File)
696  {
697  if (!preg_match("/%BROWSER%/", $File))
698  {
699  $NewRequiredFiles[] = $File;
700  }
701  }
702  $RequiredFiles = $NewRequiredFiles;
703  }
704 
705  # for each required file
706  foreach ($RequiredFiles as $File)
707  {
708  # locate specific file to use
709  $FilePath = $this->GUIFile($File);
710 
711  # if file was found
712  if ($FilePath)
713  {
714  # determine file type
715  $NamePieces = explode(".", $File);
716  $FileSuffix = strtolower(array_pop($NamePieces));
717 
718  # add file to HTML output based on file type
719  $FilePath = htmlspecialchars($FilePath);
720  switch ($FileSuffix)
721  {
722  case "js":
723  $Tag = '<script type="text/javascript" src="'
724  .$FilePath.'"></script>';
725  $PageEndOutput = preg_replace(
726  "#</body>#i", $Tag."\n</body>", $PageEndOutput, 1);
727  break;
728 
729  case "css":
730  $Tag = '<link rel="stylesheet" type="text/css"'
731  .' media="all" href="'.$FilePath.'">';
732  $PageStartOutput = preg_replace(
733  "#</head>#i", $Tag."\n</head>", $PageStartOutput, 1);
734  break;
735  }
736  }
737  }
738 
739  # assemble full page
740  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
741 
742  # perform any regular expression replacements in output
743  $FullPageOutput = preg_replace($this->OutputModificationPatterns,
744  $this->OutputModificationReplacements, $FullPageOutput);
745 
746  # perform any callback replacements in output
747  foreach ($this->OutputModificationCallbacks as $Info)
748  {
749  $this->OutputModificationCallbackInfo = $Info;
750  $FullPageOutput = preg_replace_callback($Info["SearchPattern"],
751  array($this, "OutputModificationCallbackShell"),
752  $FullPageOutput);
753  }
754 
755  # provide the opportunity to modify full page output
756  $SignalResult = $this->SignalEvent("EVENT_PAGE_OUTPUT_FILTER", array(
757  "PageOutput" => $FullPageOutput));
758  if (isset($SignalResult["PageOutput"])
759  && strlen($SignalResult["PageOutput"]))
760  {
761  $FullPageOutput = $SignalResult["PageOutput"];
762  }
763 
764  # if relative paths may not work because we were invoked via clean URL
765  if ($this->CleanUrlRewritePerformed || self::WasUrlRewritten())
766  {
767  # if using the <base> tag is okay
768  $BaseUrl = $this->BaseUrl();
769  if ($this->UseBaseTag)
770  {
771  # add <base> tag to header
772  $PageStartOutput = preg_replace("%<head>%",
773  "<head><base href=\"".$BaseUrl."\" />",
774  $PageStartOutput);
775 
776  # re-assemble full page with new header
777  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
778 
779  # the absolute URL to the current page
780  $FullUrl = $BaseUrl . $this->GetPageLocation();
781 
782  # make HREF attribute values with just a fragment ID
783  # absolute since they don't work with the <base> tag because
784  # they are relative to the current page/URL, not the site
785  # root
786  $FullPageOutput = preg_replace(
787  array("%href=\"(#[^:\" ]+)\"%i", "%href='(#[^:' ]+)'%i"),
788  array("href=\"".$FullUrl."$1\"", "href='".$FullUrl."$1'"),
789  $FullPageOutput);
790  }
791  else
792  {
793  # try to fix any relative paths throughout code
794  $FullPageOutput = preg_replace(array(
795  "%src=\"/?([^?*:;{}\\\\\" ]+)\.(js|css|gif|png|jpg)\"%i",
796  "%src='/?([^?*:;{}\\\\' ]+)\.(js|css|gif|png|jpg)'%i",
797  # don't rewrite HREF attributes that are just
798  # fragment IDs because they are relative to the
799  # current page/URL, not the site root
800  "%href=\"/?([^#][^:\" ]*)\"%i",
801  "%href='/?([^#][^:' ]*)'%i",
802  "%action=\"/?([^#][^:\" ]*)\"%i",
803  "%action='/?([^#][^:' ]*)'%i",
804  "%@import\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
805  "%@import\s+url\('/?([^:\" ]+)'\s*\)%i",
806  "%src:\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
807  "%src:\s+url\('/?([^:\" ]+)'\s*\)%i",
808  "%@import\s+\"/?([^:\" ]+)\"\s*%i",
809  "%@import\s+'/?([^:\" ]+)'\s*%i",
810  ),
811  array(
812  "src=\"".$BaseUrl."$1.$2\"",
813  "src=\"".$BaseUrl."$1.$2\"",
814  "href=\"".$BaseUrl."$1\"",
815  "href=\"".$BaseUrl."$1\"",
816  "action=\"".$BaseUrl."$1\"",
817  "action=\"".$BaseUrl."$1\"",
818  "@import url(\"".$BaseUrl."$1\")",
819  "@import url('".$BaseUrl."$1')",
820  "src: url(\"".$BaseUrl."$1\")",
821  "src: url('".$BaseUrl."$1')",
822  "@import \"".$BaseUrl."$1\"",
823  "@import '".$BaseUrl."$1'",
824  ),
825  $FullPageOutput);
826  }
827  }
828 
829  # write out full page
830  print $FullPageOutput;
831 
832  # update page cache for this page
833  $this->UpdatePageCache($PageName, $FullPageOutput);
834  }
835 
836  # run any post-processing routines
837  foreach ($this->PostProcessingFuncs as $Func)
838  {
839  call_user_func_array($Func["FunctionName"], $Func["Arguments"]);
840  }
841 
842  # write out any output buffered from page code execution
843  if (strlen($PageOutput))
844  {
845  if (!$this->SuppressHTML)
846  {
847  ?><table width="100%" cellpadding="5"
848  style="border: 2px solid #666666; background: #CCCCCC;
849  font-family: Courier New, Courier, monospace;
850  margin-top: 10px;"><tr><td><?PHP
851  }
852  if ($this->JumpToPage)
853  {
854  ?><div style="color: #666666;"><span style="font-size: 150%;">
855  <b>Page Jump Aborted</b></span>
856  (because of error or other unexpected output)<br />
857  <b>Jump Target:</b>
858  <i><?PHP print($this->JumpToPage); ?></i></div><?PHP
859  }
860  print $PageOutput;
861  if (!$this->SuppressHTML)
862  {
863  ?></td></tr></table><?PHP
864  }
865  }
866 
867  # write out any output buffered from the page code execution complete signal
868  if (!$this->JumpToPage && !$this->SuppressHTML && strlen($PageCompleteOutput))
869  {
870  print $PageCompleteOutput;
871  }
872 
873  # execute callbacks that should not have their output buffered
874  foreach ($this->UnbufferedCallbacks as $Callback)
875  {
876  call_user_func_array($Callback[0], $Callback[1]);
877  }
878 
879  # log high memory usage
880  if (function_exists("memory_get_peak_usage"))
881  {
882  $MemoryThreshold = ($this->HighMemoryUsageThreshold()
883  * $this->GetPhpMemoryLimit()) / 100;
884  if ($this->LogHighMemoryUsage()
885  && (memory_get_peak_usage() >= $MemoryThreshold))
886  {
887  $HighMemUsageMsg = "High peak memory usage ("
888  .intval(memory_get_peak_usage()).") for "
889  .$this->FullUrl()." from "
890  .$_SERVER["REMOTE_ADDR"];
891  $this->LogMessage(self::LOGLVL_INFO, $HighMemUsageMsg);
892  }
893  }
894 
895  # log slow page loads
896  if ($this->LogSlowPageLoads()
897  && ($this->GetElapsedExecutionTime()
898  >= ($this->SlowPageLoadThreshold())))
899  {
900  $SlowPageLoadMsg = "Slow page load ("
901  .intval($this->GetElapsedExecutionTime())."s) for "
902  .$this->FullUrl()." from "
903  .$_SERVER["REMOTE_ADDR"];
904  $this->LogMessage(self::LOGLVL_INFO, $SlowPageLoadMsg);
905  }
906 
907  # terminate and stay resident (TSR!) if indicated and HTML has been output
908  # (only TSR if HTML has been output because otherwise browsers will misbehave)
909  if ($ShouldTSR) { $this->LaunchTSR(); }
910  }
911 
917  function GetPageName()
918  {
919  return $this->PageName;
920  }
921 
927  function GetPageLocation()
928  {
929  # retrieve current URL
930  $Url = $this->GetScriptUrl();
931 
932  # remove the base path if present
933  $BasePath = $this->Settings["BasePath"];
934  if (stripos($Url, $BasePath) === 0)
935  {
936  $Url = substr($Url, strlen($BasePath));
937  }
938 
939  return $Url;
940  }
941 
947  function GetPageUrl()
948  {
949  return self::BaseUrl() . $this->GetPageLocation();
950  }
951 
960  function SetJumpToPage($Page, $IsLiteral = FALSE)
961  {
962  if (!is_null($Page)
963  && (!$IsLiteral)
964  && (strpos($Page, "?") === FALSE)
965  && ((strpos($Page, "=") !== FALSE)
966  || ((stripos($Page, ".php") === FALSE)
967  && (stripos($Page, ".htm") === FALSE)
968  && (strpos($Page, "/") === FALSE)))
969  && (stripos($Page, "http://") !== 0)
970  && (stripos($Page, "https://") !== 0))
971  {
972  $this->JumpToPage = self::BaseUrl() . "index.php?P=".$Page;
973  }
974  else
975  {
976  $this->JumpToPage = $Page;
977  }
978  }
979 
984  function JumpToPageIsSet()
985  {
986  return ($this->JumpToPage === NULL) ? FALSE : TRUE;
987  }
988 
998  function HtmlCharset($NewSetting = NULL)
999  {
1000  if ($NewSetting !== NULL) { $this->HtmlCharset = $NewSetting; }
1001  return $this->HtmlCharset;
1002  }
1003 
1013  function DoNotMinimizeFile($File)
1014  {
1015  if (!is_array($File)) { $File = array($File); }
1016  $this->DoNotMinimizeList = array_merge($this->DoNotMinimizeList, $File);
1017  }
1018 
1029  function UseBaseTag($NewValue = NULL)
1030  {
1031  if ($NewValue !== NULL) { $this->UseBaseTag = $NewValue ? TRUE : FALSE; }
1032  return $this->UseBaseTag;
1033  }
1034 
1041  function SuppressHTMLOutput($NewSetting = TRUE)
1042  {
1043  $this->SuppressHTML = $NewSetting;
1044  }
1045 
1052  static function ActiveUserInterface($UIName = NULL)
1053  {
1054  if ($UIName !== NULL)
1055  {
1056  self::$ActiveUI = preg_replace("/^SPTUI--/", "", $UIName);
1057  }
1058  return self::$ActiveUI;
1059  }
1060 
1066  function GetUserInterfaces()
1067  {
1068  # possible UI directories
1069  $InterfaceDirs = array(
1070  "interface",
1071  "local/interface");
1072 
1073  # start out with an empty list
1074  $Interfaces = array();
1075 
1076  # for each possible UI directory
1077  foreach ($InterfaceDirs as $InterfaceDir)
1078  {
1079  $Dir = dir($InterfaceDir);
1080 
1081  # for each file in current directory
1082  while (($DirEntry = $Dir->read()) !== FALSE)
1083  {
1084  $InterfacePath = $InterfaceDir."/".$DirEntry;
1085 
1086  # skip anything that doesn't have a name in the required format
1087  if (!preg_match('/^[a-zA-Z0-9]+$/', $DirEntry))
1088  {
1089  continue;
1090  }
1091 
1092  # skip anything that isn't a directory
1093  if (!is_dir($InterfacePath))
1094  {
1095  continue;
1096  }
1097 
1098  # read the UI name (if available)
1099  $UIName = @file_get_contents($InterfacePath."/NAME");
1100 
1101  # use the directory name if the UI name isn't available
1102  if ($UIName === FALSE || !strlen($UIName))
1103  {
1104  $UIName = $DirEntry;
1105  }
1106 
1107  $Interfaces[$InterfacePath] = $UIName;
1108  }
1109 
1110  $Dir->close();
1111  }
1112 
1113  # return list to caller
1114  return $Interfaces;
1115  }
1116 
1141  function AddPostProcessingCall($FunctionName,
1142  &$Arg1 = self::NOVALUE, &$Arg2 = self::NOVALUE, &$Arg3 = self::NOVALUE,
1143  &$Arg4 = self::NOVALUE, &$Arg5 = self::NOVALUE, &$Arg6 = self::NOVALUE,
1144  &$Arg7 = self::NOVALUE, &$Arg8 = self::NOVALUE, &$Arg9 = self::NOVALUE)
1145  {
1146  $FuncIndex = count($this->PostProcessingFuncs);
1147  $this->PostProcessingFuncs[$FuncIndex]["FunctionName"] = $FunctionName;
1148  $this->PostProcessingFuncs[$FuncIndex]["Arguments"] = array();
1149  $Index = 1;
1150  while (isset(${"Arg".$Index}) && (${"Arg".$Index} !== self::NOVALUE))
1151  {
1152  $this->PostProcessingFuncs[$FuncIndex]["Arguments"][$Index]
1153  =& ${"Arg".$Index};
1154  $Index++;
1155  }
1156  }
1157 
1163  function AddEnvInclude($FileName)
1164  {
1165  $this->EnvIncludes[] = $FileName;
1166  }
1167 
1174  function GUIFile($FileName)
1175  {
1176  # determine which location to search based on file suffix
1177  $FileType = $this->GetFileType($FileName);
1178  $DirList = ($FileType == self::FT_IMAGE)
1179  ? $this->ImageDirList : $this->IncludeDirList;
1180 
1181  # if directed to use minimized JavaScript file
1182  if (($FileType == self::FT_JAVASCRIPT) && $this->UseMinimizedJavascript())
1183  {
1184  # look for minimized version of file
1185  $MinimizedFileName = substr_replace($FileName, ".min", -3, 0);
1186  $FoundFileName = $this->FindFile($DirList, $MinimizedFileName);
1187 
1188  # if minimized file was not found
1189  if (is_null($FoundFileName))
1190  {
1191  # look for unminimized file
1192  $FoundFileName = $this->FindFile($DirList, $FileName);
1193 
1194  # if unminimized file found
1195  if (!is_null($FoundFileName))
1196  {
1197  # if minimization enabled and supported
1198  if ($this->JavascriptMinimizationEnabled()
1199  && isset($_SERVER["JSMIN_REWRITE_SUPPORT"]))
1200  {
1201  # attempt to create minimized file
1202  $MinFileName = $this->MinimizeJavascriptFile(
1203  $FoundFileName);
1204 
1205  # if minimization succeeded
1206  if ($MinFileName !== NULL)
1207  {
1208  # use minimized version
1209  $FoundFileName = $MinFileName;
1210 
1211  # save file modification time if needed for fingerprinting
1212  if ($this->UrlFingerprintingEnabled())
1213  {
1214  $FileMTime = filemtime($FoundFileName);
1215  }
1216 
1217  # strip off the cache location, allowing .htaccess
1218  # to handle that for us
1219  $FoundFileName = str_replace(
1220  self::$JSMinCacheDir."/", "", $FoundFileName);
1221  }
1222  }
1223  }
1224  }
1225  }
1226  # else if directed to use SCSS files
1227  elseif (($FileType == self::FT_CSS) && $this->ScssSupportEnabled())
1228  {
1229  # look for SCSS version of file
1230  $SourceFileName = preg_replace("/.css$/", ".scss", $FileName);
1231  $FoundSourceFileName = $this->FindFile($DirList, $SourceFileName);
1232 
1233  # if SCSS file not found
1234  if ($FoundSourceFileName === NULL)
1235  {
1236  # look for CSS file
1237  $FoundFileName = $this->FindFile($DirList, $FileName);
1238  }
1239  else
1240  {
1241  # compile SCSS file (if updated) and return resulting CSS file
1242  $FoundFileName = $this->CompileScssFile($FoundSourceFileName);
1243 
1244  # save file modification time if needed for fingerprinting
1245  if ($this->UrlFingerprintingEnabled())
1246  {
1247  $FileMTime = filemtime($FoundFileName);
1248  }
1249 
1250  # strip off the cache location, allowing .htaccess to handle that for us
1251  if (isset($_SERVER["SCSS_REWRITE_SUPPORT"]))
1252  {
1253  $FoundFileName = str_replace(
1254  self::$ScssCacheDir."/", "", $FoundFileName);
1255  }
1256  }
1257  }
1258  # otherwise just search for the file
1259  else
1260  {
1261  $FoundFileName = $this->FindFile($DirList, $FileName);
1262  }
1263 
1264  # add non-image files to list of found files (used for required files loading)
1265  if ($FileType != self::FT_IMAGE)
1266  { $this->FoundUIFiles[] = basename($FoundFileName); }
1267 
1268  # if UI file fingerprinting is enabled and supported
1269  if ($this->UrlFingerprintingEnabled()
1270  && array_key_exists("URL_FINGERPRINTING_SUPPORT", $_SERVER)
1271  && $_SERVER["URL_FINGERPRINTING_SUPPORT"]
1272  && (isset($FileMTime) || file_exists($FoundFileName)))
1273  {
1274  # if file does not appear to be a server-side inclusion
1275  if (!preg_match('/\.(html|php)$/i', $FoundFileName))
1276  {
1277  # for each URL fingerprinting blacklist entry
1278  $OnBlacklist = FALSE;
1279  foreach ($this->UrlFingerprintBlacklist as $BlacklistEntry)
1280  {
1281  # if entry looks like a regular expression pattern
1282  if ($BlacklistEntry[0] == substr($BlacklistEntry, -1))
1283  {
1284  # check file name against regular expression
1285  if (preg_match($BlacklistEntry, $FoundFileName))
1286  {
1287  $OnBlacklist = TRUE;
1288  break;
1289  }
1290  }
1291  else
1292  {
1293  # check file name directly against entry
1294  if (basename($FoundFileName) == $BlacklistEntry)
1295  {
1296  $OnBlacklist = TRUE;
1297  break;
1298  }
1299  }
1300  }
1301 
1302  # if file was not on blacklist
1303  if (!$OnBlacklist)
1304  {
1305  # get file modification time if not already retrieved
1306  if (!isset($FileMTime))
1307  {
1308  $FileMTime = filemtime($FoundFileName);
1309  }
1310 
1311  # add timestamp fingerprint to file name
1312  $Fingerprint = sprintf("%06X",
1313  ($FileMTime % 0xFFFFFF));
1314  $FoundFileName = preg_replace("/^(.+)\.([a-z]+)$/",
1315  "$1.".$Fingerprint.".$2",
1316  $FoundFileName);
1317  }
1318  }
1319  }
1320 
1321  # return file name to caller
1322  return $FoundFileName;
1323  }
1324 
1333  function PUIFile($FileName)
1334  {
1335  $FullFileName = $this->GUIFile($FileName);
1336  if ($FullFileName) { print($FullFileName); }
1337  }
1338 
1353  function IncludeUIFile($FileNames, $AdditionalAttributes = NULL)
1354  {
1355  # convert file name to array if necessary
1356  if (!is_array($FileNames)) { $FileNames = array($FileNames); }
1357 
1358  # pad additional attributes if supplied
1359  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
1360 
1361  # for each file
1362  foreach ($FileNames as $BaseFileName)
1363  {
1364  # retrieve full file name
1365  $FileName = $this->GUIFile($BaseFileName);
1366 
1367  # if file was found
1368  if ($FileName)
1369  {
1370  # print appropriate tag based on file type
1371  $FileType = $this->GetFileType($FileName);
1372  switch ($FileType)
1373  {
1374  case self::FT_CSS:
1375  print "<link rel=\"stylesheet\" type=\"text/css\""
1376  ." media=\"all\" href=\"".$FileName."\""
1377  .$AdditionalAttributes." />";
1378  $OverrideFileName = preg_replace(
1379  "/\.(css|scss)$/", "-Override.$1",
1380  $BaseFileName);
1381  $this->IncludeUIFile($OverrideFileName,
1382  $AdditionalAttributes);
1383  break;
1384 
1385  case self::FT_JAVASCRIPT:
1386  print "<script type=\"text/javascript\""
1387  ." src=\"".$FileName."\""
1388  .$AddAttribs."></script>";
1389  $OverrideFileName = preg_replace(
1390  "/\.js$/", "-Override.js",
1391  $BaseFileName);
1392  $this->IncludeUIFile($OverrideFileName,
1393  $AdditionalAttributes);
1394  break;
1395 
1396  case self::FT_IMAGE:
1397  break;
1398  }
1399  }
1400  }
1401  }
1402 
1409  function DoNotUrlFingerprint($Pattern)
1410  {
1411  $this->UrlFingerprintBlacklist[] = $Pattern;
1412  }
1413 
1421  function RequireUIFile($FileName)
1422  {
1423  $this->AdditionalRequiredUIFiles[] = $FileName;
1424  }
1425 
1431  static function GetFileType($FileName)
1432  {
1433  $FileSuffix = strtolower(substr($FileName, -3));
1434  if ($FileSuffix == "css") { return self::FT_CSS; }
1435  elseif ($FileSuffix == ".js") { return self::FT_JAVASCRIPT; }
1436  elseif (($FileSuffix == "gif")
1437  || ($FileSuffix == "jpg")
1438  || ($FileSuffix == "png")) { return self::FT_IMAGE; }
1439  return self::FT_OTHER;
1440  }
1442  const FT_OTHER = 0;
1444  const FT_CSS = 1;
1446  const FT_IMAGE = 2;
1448  const FT_JAVASCRIPT = 3;
1449 
1458  function LoadFunction($Callback)
1459  {
1460  # if specified function is not currently available
1461  if (!is_callable($Callback))
1462  {
1463  # if function info looks legal
1464  if (is_string($Callback) && strlen($Callback))
1465  {
1466  # start with function directory list
1467  $Locations = $this->FunctionDirList;
1468 
1469  # add object directories to list
1470  $Locations = array_merge(
1471  $Locations, array_keys(self::$ObjectDirectories));
1472 
1473  # look for function file
1474  $FunctionFileName = $this->FindFile($Locations, "F-".$Callback,
1475  array("php", "html"));
1476 
1477  # if function file was found
1478  if ($FunctionFileName)
1479  {
1480  # load function file
1481  include_once($FunctionFileName);
1482  }
1483  else
1484  {
1485  # log error indicating function load failed
1486  $this->LogError(self::LOGLVL_ERROR, "Unable to load function"
1487  ." for callback \"".$Callback."\".");
1488  }
1489  }
1490  else
1491  {
1492  # log error indicating specified function info was bad
1493  $this->LogError(self::LOGLVL_ERROR, "Unloadable callback value"
1494  ." (".$Callback.")"
1495  ." passed to AF::LoadFunction().");
1496  }
1497  }
1498 
1499  # report to caller whether function load succeeded
1500  return is_callable($Callback);
1501  }
1502 
1507  function GetElapsedExecutionTime()
1508  {
1509  return microtime(TRUE) - $this->ExecutionStartTime;
1510  }
1511 
1516  function GetSecondsBeforeTimeout()
1517  {
1518  return ini_get("max_execution_time") - $this->GetElapsedExecutionTime();
1519  }
1520 
1521  /*@)*/ /* Application Framework */
1522 
1523 
1524  # ---- Page Caching ------------------------------------------------------
1525  /*@(*/
1527 
1534  function PageCacheEnabled($NewValue = DB_NOVALUE)
1535  {
1536  return $this->UpdateSetting("PageCacheEnabled", $NewValue);
1537  }
1538 
1545  function PageCacheExpirationPeriod($NewValue = DB_NOVALUE)
1546  {
1547  return $this->UpdateSetting("PageCacheExpirationPeriod", $NewValue);
1548  }
1549 
1554  function DoNotCacheCurrentPage()
1555  {
1556  $this->CacheCurrentPage = FALSE;
1557  }
1558 
1565  function AddPageCacheTag($Tag, $Pages = NULL)
1566  {
1567  # normalize tag
1568  $Tag = strtolower($Tag);
1569 
1570  # if pages were supplied
1571  if ($Pages !== NULL)
1572  {
1573  # add pages to list for this tag
1574  if (isset($this->PageCacheTags[$Tag]))
1575  {
1576  $this->PageCacheTags[$Tag] = array_merge(
1577  $this->PageCacheTags[$Tag], $Pages);
1578  }
1579  else
1580  {
1581  $this->PageCacheTags[$Tag] = $Pages;
1582  }
1583  }
1584  else
1585  {
1586  # add current page to list for this tag
1587  $this->PageCacheTags[$Tag][] = "CURRENT";
1588  }
1589  }
1590 
1596  function ClearPageCacheForTag($Tag)
1597  {
1598  # get tag ID
1599  $TagId = $this->GetPageCacheTagId($Tag);
1600 
1601  # delete pages and tag/page connections for specified tag
1602  $this->DB->Query("DELETE CP, CPTI"
1603  ." FROM AF_CachedPages CP, AF_CachedPageTagInts CPTI"
1604  ." WHERE CPTI.TagId = ".intval($TagId)
1605  ." AND CP.CacheId = CPTI.CacheId");
1606  }
1607 
1611  function ClearPageCache()
1612  {
1613  # clear all page cache tables
1614  $this->DB->Query("TRUNCATE TABLE AF_CachedPages");
1615  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTags");
1616  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTagInts");
1617  }
1618 
1625  function GetPageCacheInfo()
1626  {
1627  $Length = $this->DB->Query("SELECT COUNT(*) AS CacheLen"
1628  ." FROM AF_CachedPages", "CacheLen");
1629  $Oldest = $this->DB->Query("SELECT CachedAt FROM AF_CachedPages"
1630  ." ORDER BY CachedAt ASC LIMIT 1", "CachedAt");
1631  return array(
1632  "NumberOfEntries" => $Length,
1633  "OldestTimestamp" => strtotime($Oldest),
1634  );
1635  }
1636 
1637  /*@)*/ /* Page Caching */
1638 
1639 
1640  # ---- Logging -----------------------------------------------------------
1641  /*@(*/
1643 
1654  function LogSlowPageLoads($NewValue = DB_NOVALUE)
1655  {
1656  return $this->UpdateSetting("LogSlowPageLoads", $NewValue);
1657  }
1658 
1666  function SlowPageLoadThreshold($NewValue = DB_NOVALUE)
1667  {
1668  return $this->UpdateSetting("SlowPageLoadThreshold", $NewValue);
1669  }
1670 
1681  function LogHighMemoryUsage($NewValue = DB_NOVALUE)
1682  {
1683  return $this->UpdateSetting("LogHighMemoryUsage", $NewValue);
1684  }
1685 
1694  function HighMemoryUsageThreshold($NewValue = DB_NOVALUE)
1695  {
1696  return $this->UpdateSetting("HighMemoryUsageThreshold", $NewValue);
1697  }
1698 
1712  function LogError($Level, $Msg)
1713  {
1714  # if error level is at or below current logging level
1715  if ($this->Settings["LoggingLevel"] >= $Level)
1716  {
1717  # attempt to log error message
1718  $Result = $this->LogMessage($Level, $Msg);
1719 
1720  # if logging attempt failed and level indicated significant error
1721  if (($Result === FALSE) && ($Level <= self::LOGLVL_ERROR))
1722  {
1723  # throw exception about inability to log error
1724  static $AlreadyThrewException = FALSE;
1725  if (!$AlreadyThrewException)
1726  {
1727  $AlreadyThrewException = TRUE;
1728  throw new Exception("Unable to log error (".$Level.": ".$Msg
1729  .") to ".$this->LogFileName);
1730  }
1731  }
1732 
1733  # report to caller whether message was logged
1734  return $Result;
1735  }
1736  else
1737  {
1738  # report to caller that message was not logged
1739  return FALSE;
1740  }
1741  }
1742 
1754  function LogMessage($Level, $Msg)
1755  {
1756  # if message level is at or below current logging level
1757  if ($this->Settings["LoggingLevel"] >= $Level)
1758  {
1759  # attempt to open log file
1760  $FHndl = @fopen($this->LogFileName, "a");
1761 
1762  # if log file could not be open
1763  if ($FHndl === FALSE)
1764  {
1765  # report to caller that message was not logged
1766  return FALSE;
1767  }
1768  else
1769  {
1770  # format log entry
1771  $ErrorAbbrevs = array(
1772  self::LOGLVL_FATAL => "FTL",
1773  self::LOGLVL_ERROR => "ERR",
1774  self::LOGLVL_WARNING => "WRN",
1775  self::LOGLVL_INFO => "INF",
1776  self::LOGLVL_DEBUG => "DBG",
1777  self::LOGLVL_TRACE => "TRC",
1778  );
1779  $LogEntry = date("Y-m-d H:i:s")
1780  ." ".($this->RunningInBackground ? "B" : "F")
1781  ." ".$ErrorAbbrevs[$Level]
1782  ." ".$Msg;
1783 
1784  # write entry to log
1785  $Success = fwrite($FHndl, $LogEntry."\n");
1786 
1787  # close log file
1788  fclose($FHndl);
1789 
1790  # report to caller whether message was logged
1791  return ($Success === FALSE) ? FALSE : TRUE;
1792  }
1793  }
1794  else
1795  {
1796  # report to caller that message was not logged
1797  return FALSE;
1798  }
1799  }
1800 
1822  function LoggingLevel($NewValue = DB_NOVALUE)
1823  {
1824  # constrain new level (if supplied) to within legal bounds
1825  if ($NewValue !== DB_NOVALUE)
1826  {
1827  $NewValue = max(min($NewValue, 6), 1);
1828  }
1829 
1830  # set new logging level (if supplied) and return current level to caller
1831  return $this->UpdateSetting("LoggingLevel", $NewValue);
1832  }
1833 
1840  function LogFile($NewValue = NULL)
1841  {
1842  if ($NewValue !== NULL) { $this->LogFileName = $NewValue; }
1843  return $this->LogFileName;
1844  }
1845 
1850  const LOGLVL_TRACE = 6;
1855  const LOGLVL_DEBUG = 5;
1861  const LOGLVL_INFO = 4;
1866  const LOGLVL_WARNING = 3;
1872  const LOGLVL_ERROR = 2;
1877  const LOGLVL_FATAL = 1;
1878 
1879  /*@)*/ /* Logging */
1880 
1881 
1882  # ---- Event Handling ----------------------------------------------------
1883  /*@(*/
1885 
1889  const EVENTTYPE_DEFAULT = 1;
1895  const EVENTTYPE_CHAIN = 2;
1901  const EVENTTYPE_FIRST = 3;
1909  const EVENTTYPE_NAMED = 4;
1910 
1912  const ORDER_FIRST = 1;
1914  const ORDER_MIDDLE = 2;
1916  const ORDER_LAST = 3;
1917 
1926  function RegisterEvent($EventsOrEventName, $EventType = NULL)
1927  {
1928  # convert parameters to array if not already in that form
1929  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
1930  : array($EventsOrEventName => $EventType);
1931 
1932  # for each event
1933  foreach ($Events as $Name => $Type)
1934  {
1935  # store event information
1936  $this->RegisteredEvents[$Name]["Type"] = $Type;
1937  $this->RegisteredEvents[$Name]["Hooks"] = array();
1938  }
1939  }
1940 
1947  function IsRegisteredEvent($EventName)
1948  {
1949  return array_key_exists($EventName, $this->RegisteredEvents)
1950  ? TRUE : FALSE;
1951  }
1952 
1959  function IsHookedEvent($EventName)
1960  {
1961  # the event isn't hooked to if it isn't even registered
1962  if (!$this->IsRegisteredEvent($EventName))
1963  {
1964  return FALSE;
1965  }
1966 
1967  # return TRUE if there is at least one callback hooked to the event
1968  return count($this->RegisteredEvents[$EventName]["Hooks"]) > 0;
1969  }
1970 
1984  function HookEvent($EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
1985  {
1986  # convert parameters to array if not already in that form
1987  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
1988  : array($EventsOrEventName => $Callback);
1989 
1990  # for each event
1991  $Success = TRUE;
1992  foreach ($Events as $EventName => $EventCallback)
1993  {
1994  # if callback is valid
1995  if (is_callable($EventCallback))
1996  {
1997  # if this is a periodic event we process internally
1998  if (isset($this->PeriodicEvents[$EventName]))
1999  {
2000  # process event now
2001  $this->ProcessPeriodicEvent($EventName, $EventCallback);
2002  }
2003  # if specified event has been registered
2004  elseif (isset($this->RegisteredEvents[$EventName]))
2005  {
2006  # add callback for event
2007  $this->RegisteredEvents[$EventName]["Hooks"][]
2008  = array("Callback" => $EventCallback, "Order" => $Order);
2009 
2010  # sort callbacks by order
2011  if (count($this->RegisteredEvents[$EventName]["Hooks"]) > 1)
2012  {
2013  usort($this->RegisteredEvents[$EventName]["Hooks"],
2014  array("ApplicationFramework", "HookEvent_OrderCompare"));
2015  }
2016  }
2017  else
2018  {
2019  $Success = FALSE;
2020  }
2021  }
2022  else
2023  {
2024  $Success = FALSE;
2025  }
2026  }
2027 
2028  # report to caller whether all callbacks were hooked
2029  return $Success;
2030  }
2032  private static function HookEvent_OrderCompare($A, $B)
2033  {
2034  if ($A["Order"] == $B["Order"]) { return 0; }
2035  return ($A["Order"] < $B["Order"]) ? -1 : 1;
2036  }
2037 
2048  function SignalEvent($EventName, $Parameters = NULL)
2049  {
2050  $ReturnValue = NULL;
2051 
2052  # if event has been registered
2053  if (isset($this->RegisteredEvents[$EventName]))
2054  {
2055  # set up default return value (if not NULL)
2056  switch ($this->RegisteredEvents[$EventName]["Type"])
2057  {
2058  case self::EVENTTYPE_CHAIN:
2059  $ReturnValue = $Parameters;
2060  break;
2061 
2062  case self::EVENTTYPE_NAMED:
2063  $ReturnValue = array();
2064  break;
2065  }
2066 
2067  # for each callback for this event
2068  foreach ($this->RegisteredEvents[$EventName]["Hooks"] as $Hook)
2069  {
2070  # invoke callback
2071  $Callback = $Hook["Callback"];
2072  $Result = ($Parameters !== NULL)
2073  ? call_user_func_array($Callback, $Parameters)
2074  : call_user_func($Callback);
2075 
2076  # process return value based on event type
2077  switch ($this->RegisteredEvents[$EventName]["Type"])
2078  {
2079  case self::EVENTTYPE_CHAIN:
2080  if ($Result !== NULL)
2081  {
2082  foreach ($Parameters as $Index => $Value)
2083  {
2084  if (array_key_exists($Index, $Result))
2085  {
2086  $Parameters[$Index] = $Result[$Index];
2087  }
2088  }
2089  $ReturnValue = $Parameters;
2090  }
2091  break;
2092 
2093  case self::EVENTTYPE_FIRST:
2094  if ($Result !== NULL)
2095  {
2096  $ReturnValue = $Result;
2097  break 2;
2098  }
2099  break;
2100 
2101  case self::EVENTTYPE_NAMED:
2102  $CallbackName = is_array($Callback)
2103  ? (is_object($Callback[0])
2104  ? get_class($Callback[0])
2105  : $Callback[0])."::".$Callback[1]
2106  : $Callback;
2107  $ReturnValue[$CallbackName] = $Result;
2108  break;
2109 
2110  default:
2111  break;
2112  }
2113  }
2114  }
2115  else
2116  {
2117  $this->LogError(self::LOGLVL_WARNING,
2118  "Unregistered event signaled (".$EventName.").");
2119  }
2120 
2121  # return value if any to caller
2122  return $ReturnValue;
2123  }
2124 
2130  function IsStaticOnlyEvent($EventName)
2131  {
2132  return isset($this->PeriodicEvents[$EventName]) ? TRUE : FALSE;
2133  }
2134 
2145  function EventWillNextRunAt($EventName, $Callback)
2146  {
2147  # if event is not a periodic event report failure to caller
2148  if (!array_key_exists($EventName, $this->EventPeriods)) { return FALSE; }
2149 
2150  # retrieve last execution time for event if available
2151  $Signature = self::GetCallbackSignature($Callback);
2152  $LastRunTime = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
2153  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
2154 
2155  # if event was not found report failure to caller
2156  if ($LastRunTime === NULL) { return FALSE; }
2157 
2158  # calculate next run time based on event period
2159  $NextRunTime = strtotime($LastRunTime) + $this->EventPeriods[$EventName];
2160 
2161  # report next run time to caller
2162  return $NextRunTime;
2163  }
2164 
2180  function GetKnownPeriodicEvents()
2181  {
2182  # retrieve last execution times
2183  $this->DB->Query("SELECT * FROM PeriodicEvents");
2184  $LastRunTimes = $this->DB->FetchColumn("LastRunAt", "Signature");
2185 
2186  # for each known event
2187  $Events = array();
2188  foreach ($this->KnownPeriodicEvents as $Signature => $Info)
2189  {
2190  # if last run time for event is available
2191  if (array_key_exists($Signature, $LastRunTimes))
2192  {
2193  # calculate next run time for event
2194  $LastRun = strtotime($LastRunTimes[$Signature]);
2195  $NextRun = $LastRun + $this->EventPeriods[$Info["Period"]];
2196  if ($Info["Period"] == "EVENT_PERIODIC") { $LastRun = FALSE; }
2197  }
2198  else
2199  {
2200  # set info to indicate run times are not known
2201  $LastRun = FALSE;
2202  $NextRun = FALSE;
2203  }
2204 
2205  # add event info to list
2206  $Events[$Signature] = $Info;
2207  $Events[$Signature]["LastRun"] = $LastRun;
2208  $Events[$Signature]["NextRun"] = $NextRun;
2209  $Events[$Signature]["Parameters"] = NULL;
2210  }
2211 
2212  # return list of known events to caller
2213  return $Events;
2214  }
2215 
2216  /*@)*/ /* Event Handling */
2217 
2218 
2219  # ---- Task Management ---------------------------------------------------
2220  /*@(*/
2222 
2224  const PRIORITY_HIGH = 1;
2226  const PRIORITY_MEDIUM = 2;
2228  const PRIORITY_LOW = 3;
2230  const PRIORITY_BACKGROUND = 4;
2231 
2244  function QueueTask($Callback, $Parameters = NULL,
2245  $Priority = self::PRIORITY_LOW, $Description = "")
2246  {
2247  # pack task info and write to database
2248  if ($Parameters === NULL) { $Parameters = array(); }
2249  $this->DB->Query("INSERT INTO TaskQueue"
2250  ." (Callback, Parameters, Priority, Description)"
2251  ." VALUES ('".addslashes(serialize($Callback))."', '"
2252  .addslashes(serialize($Parameters))."', ".intval($Priority).", '"
2253  .addslashes($Description)."')");
2254  }
2255 
2273  function QueueUniqueTask($Callback, $Parameters = NULL,
2274  $Priority = self::PRIORITY_LOW, $Description = "")
2275  {
2276  if ($this->TaskIsInQueue($Callback, $Parameters))
2277  {
2278  $QueryResult = $this->DB->Query("SELECT TaskId,Priority FROM TaskQueue"
2279  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2280  .($Parameters ? " AND Parameters = '"
2281  .addslashes(serialize($Parameters))."'" : ""));
2282  if ($QueryResult !== FALSE)
2283  {
2284  $Record = $this->DB->FetchRow();
2285  if ($Record["Priority"] > $Priority)
2286  {
2287  $this->DB->Query("UPDATE TaskQueue"
2288  ." SET Priority = ".intval($Priority)
2289  ." WHERE TaskId = ".intval($Record["TaskId"]));
2290  }
2291  }
2292  return FALSE;
2293  }
2294  else
2295  {
2296  $this->QueueTask($Callback, $Parameters, $Priority, $Description);
2297  return TRUE;
2298  }
2299  }
2300 
2310  function TaskIsInQueue($Callback, $Parameters = NULL)
2311  {
2312  $QueuedCount = $this->DB->Query(
2313  "SELECT COUNT(*) AS FoundCount FROM TaskQueue"
2314  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2315  .($Parameters ? " AND Parameters = '"
2316  .addslashes(serialize($Parameters))."'" : ""),
2317  "FoundCount");
2318  $RunningCount = $this->DB->Query(
2319  "SELECT COUNT(*) AS FoundCount FROM RunningTasks"
2320  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2321  .($Parameters ? " AND Parameters = '"
2322  .addslashes(serialize($Parameters))."'" : ""),
2323  "FoundCount");
2324  $FoundCount = $QueuedCount + $RunningCount;
2325  return ($FoundCount ? TRUE : FALSE);
2326  }
2327 
2333  function GetTaskQueueSize($Priority = NULL)
2334  {
2335  return $this->GetQueuedTaskCount(NULL, NULL, $Priority);
2336  }
2337 
2345  function GetQueuedTaskList($Count = 100, $Offset = 0)
2346  {
2347  return $this->GetTaskList("SELECT * FROM TaskQueue"
2348  ." ORDER BY Priority, TaskId ", $Count, $Offset);
2349  }
2350 
2364  function GetQueuedTaskCount($Callback = NULL,
2365  $Parameters = NULL, $Priority = NULL, $Description = NULL)
2366  {
2367  $Query = "SELECT COUNT(*) AS TaskCount FROM TaskQueue";
2368  $Sep = " WHERE";
2369  if ($Callback !== NULL)
2370  {
2371  $Query .= $Sep." Callback = '".addslashes(serialize($Callback))."'";
2372  $Sep = " AND";
2373  }
2374  if ($Parameters !== NULL)
2375  {
2376  $Query .= $Sep." Parameters = '".addslashes(serialize($Parameters))."'";
2377  $Sep = " AND";
2378  }
2379  if ($Priority !== NULL)
2380  {
2381  $Query .= $Sep." Priority = ".intval($Priority);
2382  $Sep = " AND";
2383  }
2384  if ($Description !== NULL)
2385  {
2386  $Query .= $Sep." Description = '".addslashes($Description)."'";
2387  }
2388  return $this->DB->Query($Query, "TaskCount");
2389  }
2390 
2398  function GetRunningTaskList($Count = 100, $Offset = 0)
2399  {
2400  return $this->GetTaskList("SELECT * FROM RunningTasks"
2401  ." WHERE StartedAt >= '".date("Y-m-d H:i:s",
2402  (time() - ini_get("max_execution_time")))."'"
2403  ." ORDER BY StartedAt", $Count, $Offset);
2404  }
2405 
2413  function GetOrphanedTaskList($Count = 100, $Offset = 0)
2414  {
2415  return $this->GetTaskList("SELECT * FROM RunningTasks"
2416  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
2417  (time() - ini_get("max_execution_time")))."'"
2418  ." ORDER BY StartedAt", $Count, $Offset);
2419  }
2420 
2425  function GetOrphanedTaskCount()
2426  {
2427  return $this->DB->Query("SELECT COUNT(*) AS Count FROM RunningTasks"
2428  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
2429  (time() - ini_get("max_execution_time")))."'",
2430  "Count");
2431  }
2432 
2438  function ReQueueOrphanedTask($TaskId, $NewPriority = NULL)
2439  {
2440  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
2441  $this->DB->Query("INSERT INTO TaskQueue"
2442  ." (Callback,Parameters,Priority,Description) "
2443  ."SELECT Callback, Parameters, Priority, Description"
2444  ." FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2445  if ($NewPriority !== NULL)
2446  {
2447  $NewTaskId = $this->DB->LastInsertId();
2448  $this->DB->Query("UPDATE TaskQueue SET Priority = "
2449  .intval($NewPriority)
2450  ." WHERE TaskId = ".intval($NewTaskId));
2451  }
2452  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2453  $this->DB->Query("UNLOCK TABLES");
2454  }
2455 
2460  function DeleteTask($TaskId)
2461  {
2462  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = ".intval($TaskId));
2463  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2464  }
2465 
2473  function GetTask($TaskId)
2474  {
2475  # assume task will not be found
2476  $Task = NULL;
2477 
2478  # look for task in task queue
2479  $this->DB->Query("SELECT * FROM TaskQueue WHERE TaskId = ".intval($TaskId));
2480 
2481  # if task was not found in queue
2482  if (!$this->DB->NumRowsSelected())
2483  {
2484  # look for task in running task list
2485  $this->DB->Query("SELECT * FROM RunningTasks WHERE TaskId = "
2486  .intval($TaskId));
2487  }
2488 
2489  # if task was found
2490  if ($this->DB->NumRowsSelected())
2491  {
2492  # if task was periodic
2493  $Row = $this->DB->FetchRow();
2494  if ($Row["Callback"] ==
2495  serialize(array("ApplicationFramework", "PeriodicEventWrapper")))
2496  {
2497  # unpack periodic task callback
2498  $WrappedCallback = unserialize($Row["Parameters"]);
2499  $Task["Callback"] = $WrappedCallback[1];
2500  $Task["Parameters"] = $WrappedCallback[2];
2501  }
2502  else
2503  {
2504  # unpack task callback and parameters
2505  $Task["Callback"] = unserialize($Row["Callback"]);
2506  $Task["Parameters"] = unserialize($Row["Parameters"]);
2507  }
2508  }
2509 
2510  # return task to caller
2511  return $Task;
2512  }
2513 
2521  function TaskExecutionEnabled($NewValue = DB_NOVALUE)
2522  {
2523  return $this->UpdateSetting("TaskExecutionEnabled", $NewValue);
2524  }
2525 
2531  function MaxTasks($NewValue = DB_NOVALUE)
2532  {
2533  return $this->UpdateSetting("MaxTasksRunning", $NewValue);
2534  }
2535 
2543  static function GetTaskCallbackSynopsis($TaskInfo)
2544  {
2545  # if task callback is function use function name
2546  $Callback = $TaskInfo["Callback"];
2547  $Name = "";
2548  if (!is_array($Callback))
2549  {
2550  $Name = $Callback;
2551  }
2552  else
2553  {
2554  # if task callback is object
2555  if (is_object($Callback[0]))
2556  {
2557  # if task callback is encapsulated ask encapsulation for name
2558  if (method_exists($Callback[0], "GetCallbackAsText"))
2559  {
2560  $Name = $Callback[0]->GetCallbackAsText();
2561  }
2562  # else assemble name from object
2563  else
2564  {
2565  $Name = get_class($Callback[0]) . "::" . $Callback[1];
2566  }
2567  }
2568  # else assemble name from supplied info
2569  else
2570  {
2571  $Name= $Callback[0] . "::" . $Callback[1];
2572  }
2573  }
2574 
2575  # if parameter array was supplied
2576  $Parameters = $TaskInfo["Parameters"];
2577  $ParameterString = "";
2578  if (is_array($Parameters))
2579  {
2580  # assemble parameter string
2581  $Separator = "";
2582  foreach ($Parameters as $Parameter)
2583  {
2584  $ParameterString .= $Separator;
2585  if (is_int($Parameter) || is_float($Parameter))
2586  {
2587  $ParameterString .= $Parameter;
2588  }
2589  else if (is_string($Parameter))
2590  {
2591  $ParameterString .= "\"".htmlspecialchars($Parameter)."\"";
2592  }
2593  else if (is_array($Parameter))
2594  {
2595  $ParameterString .= "ARRAY";
2596  }
2597  else if (is_object($Parameter))
2598  {
2599  $ParameterString .= "OBJECT";
2600  }
2601  else if (is_null($Parameter))
2602  {
2603  $ParameterString .= "NULL";
2604  }
2605  else if (is_bool($Parameter))
2606  {
2607  $ParameterString .= $Parameter ? "TRUE" : "FALSE";
2608  }
2609  else if (is_resource($Parameter))
2610  {
2611  $ParameterString .= get_resource_type($Parameter);
2612  }
2613  else
2614  {
2615  $ParameterString .= "????";
2616  }
2617  $Separator = ", ";
2618  }
2619  }
2620 
2621  # assemble name and parameters and return result to caller
2622  return $Name."(".$ParameterString.")";
2623  }
2624 
2625  /*@)*/ /* Task Management */
2626 
2627 
2628  # ---- Clean URL Support -------------------------------------------------
2629  /*@(*/
2631 
2658  function AddCleanUrl($Pattern, $Page, $GetVars = NULL, $Template = NULL)
2659  {
2660  # save clean URL mapping parameters
2661  $this->CleanUrlMappings[] = array(
2662  "Pattern" => $Pattern,
2663  "Page" => $Page,
2664  "GetVars" => $GetVars,
2665  );
2666 
2667  # if replacement template specified
2668  if ($Template !== NULL)
2669  {
2670  # if GET parameters specified
2671  if (count($GetVars))
2672  {
2673  # retrieve all possible permutations of GET parameters
2674  $GetPerms = $this->ArrayPermutations(array_keys($GetVars));
2675 
2676  # for each permutation of GET parameters
2677  foreach ($GetPerms as $VarPermutation)
2678  {
2679  # construct search pattern for permutation
2680  $SearchPattern = "/href=([\"'])index\\.php\\?P=".$Page;
2681  $GetVarSegment = "";
2682  foreach ($VarPermutation as $GetVar)
2683  {
2684  if (preg_match("%\\\$[0-9]+%", $GetVars[$GetVar]))
2685  {
2686  $GetVarSegment .= "&amp;".$GetVar."=((?:(?!\\1)[^&])+)";
2687  }
2688  else
2689  {
2690  $GetVarSegment .= "&amp;".$GetVar."=".$GetVars[$GetVar];
2691  }
2692  }
2693  $SearchPattern .= $GetVarSegment."\\1/i";
2694 
2695  # if template is actually a callback
2696  if (is_callable($Template))
2697  {
2698  # add pattern to HTML output mod callbacks list
2699  $this->OutputModificationCallbacks[] = array(
2700  "Pattern" => $Pattern,
2701  "Page" => $Page,
2702  "SearchPattern" => $SearchPattern,
2703  "Callback" => $Template,
2704  );
2705  }
2706  else
2707  {
2708  # construct replacement string for permutation
2709  $Replacement = $Template;
2710  $Index = 2;
2711  foreach ($VarPermutation as $GetVar)
2712  {
2713  $Replacement = str_replace(
2714  "\$".$GetVar, "\$".$Index, $Replacement);
2715  $Index++;
2716  }
2717  $Replacement = "href=\"".$Replacement."\"";
2718 
2719  # add pattern to HTML output modifications list
2720  $this->OutputModificationPatterns[] = $SearchPattern;
2721  $this->OutputModificationReplacements[] = $Replacement;
2722  }
2723  }
2724  }
2725  else
2726  {
2727  # construct search pattern
2728  $SearchPattern = "/href=\"index\\.php\\?P=".$Page."\"/i";
2729 
2730  # if template is actually a callback
2731  if (is_callable($Template))
2732  {
2733  # add pattern to HTML output mod callbacks list
2734  $this->OutputModificationCallbacks[] = array(
2735  "Pattern" => $Pattern,
2736  "Page" => $Page,
2737  "SearchPattern" => $SearchPattern,
2738  "Callback" => $Template,
2739  );
2740  }
2741  else
2742  {
2743  # add simple pattern to HTML output modifications list
2744  $this->OutputModificationPatterns[] = $SearchPattern;
2745  $this->OutputModificationReplacements[] = "href=\"".$Template."\"";
2746  }
2747  }
2748  }
2749  }
2750 
2756  function CleanUrlIsMapped($Path)
2757  {
2758  foreach ($this->CleanUrlMappings as $Info)
2759  {
2760  if (preg_match($Info["Pattern"], $Path))
2761  {
2762  return TRUE;
2763  }
2764  }
2765  return FALSE;
2766  }
2767 
2777  function GetCleanUrlForPath($Path)
2778  {
2779  # the search patterns and callbacks require a specific format
2780  $Format = "href=\"".str_replace("&", "&amp;", $Path)."\"";
2781  $Search = $Format;
2782 
2783  # perform any regular expression replacements on the search string
2784  $Search = preg_replace(
2785  $this->OutputModificationPatterns,
2786  $this->OutputModificationReplacements,
2787  $Search);
2788 
2789  # only run the callbacks if a replacement hasn't already been performed
2790  if ($Search == $Format)
2791  {
2792  # perform any callback replacements on the search string
2793  foreach ($this->OutputModificationCallbacks as $Info)
2794  {
2795  # make the information available to the callback
2796  $this->OutputModificationCallbackInfo = $Info;
2797 
2798  # execute the callback
2799  $Search = preg_replace_callback(
2800  $Info["SearchPattern"],
2801  array($this, "OutputModificationCallbackShell"),
2802  $Search);
2803  }
2804  }
2805 
2806  # return the path untouched if no replacements were performed
2807  if ($Search == $Format)
2808  {
2809  return $Path;
2810  }
2811 
2812  # remove the bits added to the search string to get it recognized by
2813  # the replacement expressions and callbacks
2814  $Result = substr($Search, 6, -1);
2815 
2816  return $Result;
2817  }
2818 
2825  function GetUncleanUrlForPath($Path)
2826  {
2827  # for each clean URL mapping
2828  foreach ($this->CleanUrlMappings as $Info)
2829  {
2830  # if current path matches the clean URL pattern
2831  if (preg_match($Info["Pattern"], $Path, $Matches))
2832  {
2833  # the GET parameters for the URL, starting with the page name
2834  $GetVars = array("P" => $Info["Page"]);
2835 
2836  # if additional $_GET variables specified for clean URL
2837  if ($Info["GetVars"] !== NULL)
2838  {
2839  # for each $_GET variable specified for clean URL
2840  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
2841  {
2842  # start with template for variable value
2843  $Value = $VarTemplate;
2844 
2845  # for each subpattern matched in current URL
2846  foreach ($Matches as $Index => $Match)
2847  {
2848  # if not first (whole) match
2849  if ($Index > 0)
2850  {
2851  # make any substitutions in template
2852  $Value = str_replace("$".$Index, $Match, $Value);
2853  }
2854  }
2855 
2856  # add the GET variable
2857  $GetVars[$VarName] = $Value;
2858  }
2859  }
2860 
2861  # return the unclean URL
2862  return "index.php?" . http_build_query($GetVars);
2863  }
2864  }
2865 
2866  # return the path unchanged
2867  return $Path;
2868  }
2869 
2875  function GetCleanUrl()
2876  {
2877  return $this->GetCleanUrlForPath($this->GetUncleanUrl());
2878  }
2879 
2884  function GetUncleanUrl()
2885  {
2886  $GetVars = array("P" => $this->GetPageName()) + $_GET;
2887  return "index.php?" . http_build_query($GetVars);
2888  }
2889 
2890  /*@)*/ /* Clean URL Support */
2891 
2892 
2893  # ---- Server Environment ------------------------------------------------
2894  /*@(*/
2896 
2902  static function SessionLifetime($NewValue = NULL)
2903  {
2904  if ($NewValue !== NULL)
2905  {
2906  self::$SessionLifetime = $NewValue;
2907  }
2908  return self::$SessionLifetime;
2909  }
2910 
2916  static function HtaccessSupport()
2917  {
2918  # HTACCESS_SUPPORT is set in the .htaccess file
2919  return isset($_SERVER["HTACCESS_SUPPORT"]);
2920  }
2921 
2929  static function RootUrl()
2930  {
2931  # return override value if one is set
2932  if (self::$RootUrlOverride !== NULL)
2933  {
2934  return self::$RootUrlOverride;
2935  }
2936 
2937  # determine scheme name
2938  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
2939 
2940  # if HTTP_HOST is preferred or SERVER_NAME points to localhost
2941  # and HTTP_HOST is set
2942  if ((self::$PreferHttpHost || ($_SERVER["SERVER_NAME"] == "127.0.0.1"))
2943  && isset($_SERVER["HTTP_HOST"]))
2944  {
2945  # use HTTP_HOST for domain name
2946  $DomainName = $_SERVER["HTTP_HOST"];
2947  }
2948  else
2949  {
2950  # use SERVER_NAME for domain name
2951  $DomainName = $_SERVER["HTTP_HOST"];
2952  }
2953 
2954  # build URL root and return to caller
2955  return $Protocol."://".$DomainName;
2956  }
2957 
2972  static function RootUrlOverride($NewValue = self::NOVALUE)
2973  {
2974  if ($NewValue !== self::NOVALUE)
2975  {
2976  self::$RootUrlOverride = strlen(trim($NewValue)) ? $NewValue : NULL;
2977  }
2978  return self::$RootUrlOverride;
2979  }
2980 
2990  static function BaseUrl()
2991  {
2992  $BaseUrl = self::RootUrl().dirname($_SERVER["SCRIPT_NAME"]);
2993  if (substr($BaseUrl, -1) != "/") { $BaseUrl .= "/"; }
2994  return $BaseUrl;
2995  }
2996 
3004  static function FullUrl()
3005  {
3006  return self::RootUrl().$_SERVER["REQUEST_URI"];
3007  }
3008 
3019  static function PreferHttpHost($NewValue = NULL)
3020  {
3021  if ($NewValue !== NULL)
3022  {
3023  self::$PreferHttpHost = ($NewValue ? TRUE : FALSE);
3024  }
3025  return self::$PreferHttpHost;
3026  }
3027 
3032  static function BasePath()
3033  {
3034  $BasePath = dirname($_SERVER["SCRIPT_NAME"]);
3035 
3036  if (substr($BasePath, -1) != "/")
3037  {
3038  $BasePath .= "/";
3039  }
3040 
3041  return $BasePath;
3042  }
3043 
3049  static function GetScriptUrl()
3050  {
3051  if (array_key_exists("SCRIPT_URL", $_SERVER))
3052  {
3053  return $_SERVER["SCRIPT_URL"];
3054  }
3055  elseif (array_key_exists("REDIRECT_URL", $_SERVER))
3056  {
3057  return $_SERVER["REDIRECT_URL"];
3058  }
3059  elseif (array_key_exists("REQUEST_URI", $_SERVER))
3060  {
3061  $Pieces = parse_url($_SERVER["REQUEST_URI"]);
3062  return $Pieces["path"];
3063  }
3064  else
3065  {
3066  return NULL;
3067  }
3068  }
3069 
3078  static function WasUrlRewritten($ScriptName="index.php")
3079  {
3080  # needed to get the path of the URL minus the query and fragment pieces
3081  $Components = parse_url(self::GetScriptUrl());
3082 
3083  # if parsing was successful and a path is set
3084  if (is_array($Components) && isset($Components["path"]))
3085  {
3086  $BasePath = self::BasePath();
3087  $Path = $Components["path"];
3088 
3089  # the URL was rewritten if the path isn't the base path, i.e., the
3090  # home page, and the file in the URL isn't the script generating the
3091  # page
3092  if ($BasePath != $Path && basename($Path) != $ScriptName)
3093  {
3094  return TRUE;
3095  }
3096  }
3097 
3098  # the URL wasn't rewritten
3099  return FALSE;
3100  }
3101 
3107  static function GetFreeMemory()
3108  {
3109  return self::GetPhpMemoryLimit() - memory_get_usage();
3110  }
3111 
3117  static function GetPhpMemoryLimit()
3118  {
3119  $Str = strtoupper(ini_get("memory_limit"));
3120  if (substr($Str, -1) == "B") { $Str = substr($Str, 0, strlen($Str) - 1); }
3121  switch (substr($Str, -1))
3122  {
3123  case "K": $MemoryLimit = (int)$Str * 1024; break;
3124  case "M": $MemoryLimit = (int)$Str * 1048576; break;
3125  case "G": $MemoryLimit = (int)$Str * 1073741824; break;
3126  default: $MemoryLimit = (int)$Str; break;
3127  }
3128  return $MemoryLimit;
3129  }
3130 
3138  function MaxExecutionTime($NewValue = NULL)
3139  {
3140  if (func_num_args() && !ini_get("safe_mode"))
3141  {
3142  if ($NewValue != $this->Settings["MaxExecTime"])
3143  {
3144  $this->Settings["MaxExecTime"] = max($NewValue, 5);
3145  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
3146  ." SET MaxExecTime = '"
3147  .intval($this->Settings["MaxExecTime"])."'");
3148  }
3149  ini_set("max_execution_time", $this->Settings["MaxExecTime"]);
3150  set_time_limit($this->Settings["MaxExecTime"]);
3151  }
3152  return ini_get("max_execution_time");
3153  }
3154 
3155  /*@)*/ /* Server Environment */
3156 
3157 
3158  # ---- Backward Compatibility --------------------------------------------
3159  /*@(*/
3161 
3166  function FindCommonTemplate($BaseName)
3167  {
3168  return $this->FindFile(
3169  $this->IncludeDirList, $BaseName, array("tpl", "html"));
3170  }
3171 
3172  /*@)*/ /* Backward Compatibility */
3173 
3174 
3175  # ---- PRIVATE INTERFACE -------------------------------------------------
3176 
3177  private $AdditionalRequiredUIFiles = array();
3178  private $BackgroundTaskMemLeakLogThreshold = 10; # percentage of max mem
3179  private $BackgroundTaskMinFreeMemPercent = 25;
3180  private $BrowserDetectFunc;
3181  private $CacheCurrentPage = TRUE;
3182  private $CleanUrlMappings = array();
3183  private $CleanUrlRewritePerformed = FALSE;
3184  private $CssUrlFingerprintPath;
3185  private $DB;
3186  private $DefaultPage = "Home";
3187  private $DoNotMinimizeList = array();
3188  private $EnvIncludes = array();
3189  private $ExecutionStartTime;
3190  private $FoundUIFiles = array();
3191  private $HtmlCharset = "UTF-8";
3192  private $JSMinimizerJavaScriptPackerAvailable = FALSE;
3193  private $JSMinimizerJShrinkAvailable = TRUE;
3194  private $JumpToPage = NULL;
3195  private $LogFileName = "local/logs/site.log";
3196  private $MaxRunningTasksToTrack = 250;
3197  private $OutputModificationCallbackInfo;
3198  private $OutputModificationCallbacks = array();
3199  private $OutputModificationPatterns = array();
3200  private $OutputModificationReplacements = array();
3201  private $PageCacheTags = array();
3202  private $PageName;
3203  private $PostProcessingFuncs = array();
3204  private $RunningInBackground = FALSE;
3205  private $RunningTask;
3206  private $SavedContext;
3207  private $SaveTemplateLocationCache = FALSE;
3208  private $SessionStorage;
3209  private $SessionGcProbability;
3210  private $Settings;
3211  private $SuppressHTML = FALSE;
3212  private $TemplateLocationCache;
3213  private $TemplateLocationCacheInterval = 60; # in minutes
3214  private $TemplateLocationCacheExpiration;
3215  private $UnbufferedCallbacks = array();
3216  private $UrlFingerprintBlacklist = array();
3217  private $UseBaseTag = FALSE;
3218 
3219  private static $ActiveUI = "default";
3220  private static $AppName = "ScoutAF";
3221  private static $JSMinCacheDir = "local/data/caches/JSMin";
3222  private static $ObjectDirectories = array();
3223  private static $ObjectLocationCache;
3224  private static $ObjectLocationCacheInterval = 60;
3225  private static $ObjectLocationCacheExpiration;
3226  private static $PreferHttpHost = FALSE;
3227  private static $RootUrlOverride = NULL;
3228  private static $SaveObjectLocationCache = FALSE;
3229  private static $ScssCacheDir = "local/data/caches/SCSS";
3230  private static $SessionLifetime = 1440; # in seconds
3231 
3232  # offset used to generate page cache tag IDs from numeric tags
3233  const PAGECACHETAGIDOFFSET = 100000;
3234 
3239  private $NoTSR = FALSE;
3240 
3241  private $KnownPeriodicEvents = array();
3242  private $PeriodicEvents = array(
3243  "EVENT_HOURLY" => self::EVENTTYPE_DEFAULT,
3244  "EVENT_DAILY" => self::EVENTTYPE_DEFAULT,
3245  "EVENT_WEEKLY" => self::EVENTTYPE_DEFAULT,
3246  "EVENT_MONTHLY" => self::EVENTTYPE_DEFAULT,
3247  "EVENT_PERIODIC" => self::EVENTTYPE_NAMED,
3248  );
3249  private $EventPeriods = array(
3250  "EVENT_HOURLY" => 3600,
3251  "EVENT_DAILY" => 86400,
3252  "EVENT_WEEKLY" => 604800,
3253  "EVENT_MONTHLY" => 2592000,
3254  "EVENT_PERIODIC" => 0,
3255  );
3256  private $UIEvents = array(
3257  "EVENT_PAGE_LOAD" => self::EVENTTYPE_DEFAULT,
3258  "EVENT_PHP_FILE_LOAD" => self::EVENTTYPE_CHAIN,
3259  "EVENT_PHP_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
3260  "EVENT_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
3261  "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
3262  "EVENT_PAGE_OUTPUT_FILTER" => self::EVENTTYPE_CHAIN,
3263  );
3264 
3269  private function LoadSettings()
3270  {
3271  # read settings in from database
3272  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
3273  $this->Settings = $this->DB->FetchRow();
3274 
3275  # if settings were not previously initialized
3276  if ($this->Settings === FALSE)
3277  {
3278  # initialize settings in database
3279  $this->DB->Query("INSERT INTO ApplicationFrameworkSettings"
3280  ." (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");
3281 
3282  # read new settings in from database
3283  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
3284  $this->Settings = $this->DB->FetchRow();
3285 
3286  # bail out if reloading new settings failed
3287  if ($this->Settings === FALSE)
3288  {
3289  throw new Exception(
3290  "Unable to load application framework settings.");
3291  }
3292  }
3293 
3294  # if base path was not previously set or we appear to have moved
3295  if (!array_key_exists("BasePath", $this->Settings)
3296  || (!strlen($this->Settings["BasePath"]))
3297  || (!array_key_exists("BasePathCheck", $this->Settings))
3298  || (__FILE__ != $this->Settings["BasePathCheck"]))
3299  {
3300  # attempt to extract base path from Apache .htaccess file
3301  if (is_readable(".htaccess"))
3302  {
3303  $Lines = file(".htaccess");
3304  foreach ($Lines as $Line)
3305  {
3306  if (preg_match("/\\s*RewriteBase\\s+/", $Line))
3307  {
3308  $Pieces = preg_split(
3309  "/\\s+/", $Line, NULL, PREG_SPLIT_NO_EMPTY);
3310  $BasePath = $Pieces[1];
3311  }
3312  }
3313  }
3314 
3315  # if base path was found
3316  if (isset($BasePath))
3317  {
3318  # save base path locally
3319  $this->Settings["BasePath"] = $BasePath;
3320 
3321  # save base path to database
3322  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
3323  ." SET BasePath = '".addslashes($BasePath)."'"
3324  .", BasePathCheck = '".addslashes(__FILE__)."'");
3325  }
3326  }
3327 
3328  # retrieve template location cache
3329  $this->TemplateLocationCache = unserialize(
3330  $this->Settings["TemplateLocationCache"]);
3331  $this->TemplateLocationCacheInterval =
3332  $this->Settings["TemplateLocationCacheInterval"];
3333  $this->TemplateLocationCacheExpiration =
3334  strtotime($this->Settings["TemplateLocationCacheExpiration"]);
3335 
3336  # if template location cache looks invalid or has expired
3337  $CurrentTime = time();
3338  if (!count($this->TemplateLocationCache)
3339  || ($CurrentTime >= $this->TemplateLocationCacheExpiration))
3340  {
3341  # clear cache and reset cache expiration
3342  $this->TemplateLocationCache = array();
3343  $this->TemplateLocationCacheExpiration =
3344  $CurrentTime + ($this->TemplateLocationCacheInterval * 60);
3345  $this->SaveTemplateLocationCache = TRUE;
3346  }
3347 
3348  # retrieve object location cache
3349  self::$ObjectLocationCache =
3350  unserialize($this->Settings["ObjectLocationCache"]);
3351  self::$ObjectLocationCacheInterval =
3352  $this->Settings["ObjectLocationCacheInterval"];
3353  self::$ObjectLocationCacheExpiration =
3354  strtotime($this->Settings["ObjectLocationCacheExpiration"]);
3355 
3356  # if object location cache looks invalid or has expired
3357  if (!count(self::$ObjectLocationCache)
3358  || ($CurrentTime >= self::$ObjectLocationCacheExpiration))
3359  {
3360  # clear cache and reset cache expiration
3361  self::$ObjectLocationCache = array();
3362  self::$ObjectLocationCacheExpiration =
3363  $CurrentTime + (self::$ObjectLocationCacheInterval * 60);
3364  self::$SaveObjectLocationCache = TRUE;
3365  }
3366  }
3367 
3374  private function RewriteCleanUrls($PageName)
3375  {
3376  # if URL rewriting is supported by the server
3377  if ($this->HtaccessSupport())
3378  {
3379  # retrieve current URL and remove base path if present
3380  $Url = $this->GetPageLocation();
3381 
3382  # for each clean URL mapping
3383  foreach ($this->CleanUrlMappings as $Info)
3384  {
3385  # if current URL matches clean URL pattern
3386  if (preg_match($Info["Pattern"], $Url, $Matches))
3387  {
3388  # set new page
3389  $PageName = $Info["Page"];
3390 
3391  # if $_GET variables specified for clean URL
3392  if ($Info["GetVars"] !== NULL)
3393  {
3394  # for each $_GET variable specified for clean URL
3395  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
3396  {
3397  # start with template for variable value
3398  $Value = $VarTemplate;
3399 
3400  # for each subpattern matched in current URL
3401  foreach ($Matches as $Index => $Match)
3402  {
3403  # if not first (whole) match
3404  if ($Index > 0)
3405  {
3406  # make any substitutions in template
3407  $Value = str_replace("$".$Index, $Match, $Value);
3408  }
3409  }
3410 
3411  # set $_GET variable
3412  $_GET[$VarName] = $Value;
3413  }
3414  }
3415 
3416  # set flag indicating clean URL mapped
3417  $this->CleanUrlRewritePerformed = TRUE;
3418 
3419  # stop looking for a mapping
3420  break;
3421  }
3422  }
3423  }
3424 
3425  # return (possibly) updated page name to caller
3426  return $PageName;
3427  }
3428 
3447  private function FindFile($DirectoryList, $BaseName,
3448  $PossibleSuffixes = NULL, $PossiblePrefixes = NULL)
3449  {
3450  # generate template cache index for this page
3451  $CacheIndex = md5(serialize($DirectoryList))
3452  .":".self::$ActiveUI.":".$BaseName;
3453 
3454  # if caching is enabled and we have cached location
3455  if (($this->TemplateLocationCacheInterval > 0)
3456  && array_key_exists($CacheIndex,
3457  $this->TemplateLocationCache))
3458  {
3459  # use template location from cache
3460  $FoundFileName = $this->TemplateLocationCache[$CacheIndex];
3461  }
3462  else
3463  {
3464  # if suffixes specified and base name does not include suffix
3465  if (count($PossibleSuffixes)
3466  && !preg_match("/\.[a-zA-Z0-9]+$/", $BaseName))
3467  {
3468  # add versions of file names with suffixes to file name list
3469  $FileNames = array();
3470  foreach ($PossibleSuffixes as $Suffix)
3471  {
3472  $FileNames[] = $BaseName.".".$Suffix;
3473  }
3474  }
3475  else
3476  {
3477  # use base name as file name
3478  $FileNames = array($BaseName);
3479  }
3480 
3481  # if prefixes specified
3482  if (count($PossiblePrefixes))
3483  {
3484  # add versions of file names with prefixes to file name list
3485  $NewFileNames = array();
3486  foreach ($FileNames as $FileName)
3487  {
3488  foreach ($PossiblePrefixes as $Prefix)
3489  {
3490  $NewFileNames[] = $Prefix.$FileName;
3491  }
3492  }
3493  $FileNames = $NewFileNames;
3494  }
3495 
3496  # for each possible location
3497  $FoundFileName = NULL;
3498  foreach ($DirectoryList as $Dir)
3499  {
3500  # substitute active UI name into path
3501  $Dir = str_replace("%ACTIVEUI%", self::$ActiveUI, $Dir);
3502 
3503  # for each possible file name
3504  foreach ($FileNames as $File)
3505  {
3506  # if template is found at location
3507  if (file_exists($Dir.$File))
3508  {
3509  # save full template file name and stop looking
3510  $FoundFileName = $Dir.$File;
3511  break 2;
3512  }
3513  }
3514  }
3515 
3516  # save location in cache
3517  $this->TemplateLocationCache[$CacheIndex]
3518  = $FoundFileName;
3519 
3520  # set flag indicating that cache should be saved
3521  $this->SaveTemplateLocationCache = TRUE;
3522  }
3523 
3524  # return full template file name to caller
3525  return $FoundFileName;
3526  }
3527 
3536  private function CompileScssFile($SrcFile)
3537  {
3538  # build path to CSS file
3539  $DstFile = self::$ScssCacheDir."/".dirname($SrcFile)
3540  ."/".basename($SrcFile);
3541  $DstFile = substr_replace($DstFile, "css", -4);
3542 
3543  # if SCSS file is newer than CSS file
3544  if (!file_exists($DstFile)
3545  || (filemtime($SrcFile) > filemtime($DstFile)))
3546  {
3547  # attempt to create CSS cache subdirectory if not present
3548  if (!is_dir(dirname($DstFile)))
3549  {
3550  @mkdir(dirname($DstFile), 0777, TRUE);
3551  }
3552 
3553  # if CSS cache directory and CSS file path appear writable
3554  static $CacheDirIsWritable;
3555  if (!isset($CacheDirIsWritable))
3556  { $CacheDirIsWritable = is_writable(self::$ScssCacheDir); }
3557  if (is_writable($DstFile)
3558  || (!file_exists($DstFile) && $CacheDirIsWritable))
3559  {
3560  # load SCSS and compile to CSS
3561  $ScssCode = file_get_contents($SrcFile);
3562  $ScssCompiler = new scssc();
3563  $ScssCompiler->setFormatter($this->GenerateCompactCss()
3564  ? "scss_formatter_compressed" : "scss_formatter");
3565  try
3566  {
3567  $CssCode = $ScssCompiler->compile($ScssCode);
3568 
3569  # add fingerprinting for URLs in CSS
3570  $this->CssUrlFingerprintPath = dirname($SrcFile);
3571  $CssCode = preg_replace_callback(
3572  "/url\((['\"]*)(.+)\.([a-z]+)(['\"]*)\)/",
3573  array($this, "CssUrlFingerprintInsertion"),
3574  $CssCode);
3575 
3576  # write out CSS file
3577  file_put_contents($DstFile, $CssCode);
3578  }
3579  catch (Exception $Ex)
3580  {
3581  $this->LogError(self::LOGLVL_ERROR,
3582  "Error compiling SCSS file ".$SrcFile.": "
3583  .$Ex->getMessage());
3584  $DstFile = NULL;
3585  }
3586  }
3587  else
3588  {
3589  # log error and set CSS file path to indicate failure
3590  $this->LogError(self::LOGLVL_ERROR,
3591  "Unable to write out CSS file for SCSS file ".$SrcFile);
3592  $DstFile = NULL;
3593  }
3594  }
3595 
3596  # return CSS file path to caller
3597  return $DstFile;
3598  }
3599 
3607  private function MinimizeJavascriptFile($SrcFile)
3608  {
3609  # bail out if file is on exclusion list
3610  foreach ($this->DoNotMinimizeList as $DNMFile)
3611  {
3612  if (($SrcFile == $DNMFile) || (basename($SrcFile) == $DNMFile))
3613  {
3614  return NULL;
3615  }
3616  }
3617 
3618  # build path to minimized file
3619  $DstFile = self::$JSMinCacheDir."/".dirname($SrcFile)
3620  ."/".basename($SrcFile);
3621  $DstFile = substr_replace($DstFile, ".min", -3, 0);
3622 
3623  # if original file is newer than minimized file
3624  if (!file_exists($DstFile)
3625  || (filemtime($SrcFile) > filemtime($DstFile)))
3626  {
3627  # attempt to create cache subdirectory if not present
3628  if (!is_dir(dirname($DstFile)))
3629  {
3630  @mkdir(dirname($DstFile), 0777, TRUE);
3631  }
3632 
3633  # if cache directory and minimized file path appear writable
3634  static $CacheDirIsWritable;
3635  if (!isset($CacheDirIsWritable))
3636  { $CacheDirIsWritable = is_writable(self::$JSMinCacheDir); }
3637  if (is_writable($DstFile)
3638  || (!file_exists($DstFile) && $CacheDirIsWritable))
3639  {
3640  # load JavaScript code
3641  $Code = file_get_contents($SrcFile);
3642 
3643  # decide which minimizer to use
3644  if ($this->JSMinimizerJavaScriptPackerAvailable
3645  && $this->JSMinimizerJShrinkAvailable)
3646  {
3647  $Minimizer = (strlen($Code) < 5000)
3648  ? "JShrink" : "JavaScriptPacker";
3649  }
3650  elseif ($this->JSMinimizerJShrinkAvailable)
3651  {
3652  $Minimizer = "JShrink";
3653  }
3654  else
3655  {
3656  $Minimizer = "NONE";
3657  }
3658 
3659  # minimize code
3660  switch ($Minimizer)
3661  {
3662  case "JavaScriptMinimizer":
3663  $Packer = new JavaScriptPacker($Code, "Normal");
3664  $MinimizedCode = $Packer->pack();
3665  break;
3666 
3667  case "JShrink":
3668  try
3669  {
3670  $MinimizedCode = \JShrink\Minifier::minify($Code);
3671  }
3672  catch (Exception $Exception)
3673  {
3674  unset($MinimizedCode);
3675  $MinimizeError = $Exception->getMessage();
3676  }
3677  break;
3678  }
3679 
3680  # if minimization succeeded
3681  if (isset($MinimizedCode))
3682  {
3683  # write out minimized file
3684  file_put_contents($DstFile, $MinimizedCode);
3685  }
3686  else
3687  {
3688  # log error and set destination file path to indicate failure
3689  $ErrMsg = "Unable to minimize JavaScript file ".$SrcFile;
3690  if (isset($MinimizeError))
3691  {
3692  $ErrMsg .= " (".$MinimizeError.")";
3693  }
3694  $this->LogError(self::LOGLVL_ERROR, $ErrMsg);
3695  $DstFile = NULL;
3696  }
3697  }
3698  else
3699  {
3700  # log error and set destination file path to indicate failure
3701  $this->LogError(self::LOGLVL_ERROR,
3702  "Unable to write out minimized JavaScript for file ".$SrcFile);
3703  $DstFile = NULL;
3704  }
3705  }
3706 
3707  # return CSS file path to caller
3708  return $DstFile;
3709  }
3710 
3718  private function CssUrlFingerprintInsertion($Matches)
3719  {
3720  # generate fingerprint string from CSS file modification time
3721  $FileName = realpath($this->CssUrlFingerprintPath."/".
3722  $Matches[2].".".$Matches[3]);
3723  $MTime = filemtime($FileName);
3724  $Fingerprint = sprintf("%06X", ($MTime % 0xFFFFFF));
3725 
3726  # build URL string with fingerprint and return it to caller
3727  return "url(".$Matches[1].$Matches[2].".".$Fingerprint
3728  .".".$Matches[3].$Matches[4].")";
3729  }
3730 
3737  private function GetRequiredFilesNotYetLoaded($PageContentFile)
3738  {
3739  # start out assuming no files required
3740  $RequiredFiles = array();
3741 
3742  # if page content file supplied
3743  if ($PageContentFile)
3744  {
3745  # if file containing list of required files is available
3746  $Path = dirname($PageContentFile);
3747  $RequireListFile = $Path."/REQUIRES";
3748  if (file_exists($RequireListFile))
3749  {
3750  # read in list of required files
3751  $RequestedFiles = file($RequireListFile);
3752 
3753  # for each line in required file list
3754  foreach ($RequestedFiles as $Line)
3755  {
3756  # if line is not a comment
3757  $Line = trim($Line);
3758  if (!preg_match("/^#/", $Line))
3759  {
3760  # if file has not already been loaded
3761  if (!in_array($Line, $this->FoundUIFiles))
3762  {
3763  # add to list of required files
3764  $RequiredFiles[] = $Line;
3765  }
3766  }
3767  }
3768  }
3769  }
3770 
3771  # add in additional required files if any
3772  if (count($this->AdditionalRequiredUIFiles))
3773  {
3774  # make sure there are no duplicates
3775  $AdditionalRequiredUIFiles = array_unique(
3776  $this->AdditionalRequiredUIFiles);
3777 
3778  $RequiredFiles = array_merge(
3779  $RequiredFiles, $AdditionalRequiredUIFiles);
3780  }
3781 
3782  # return list of required files to caller
3783  return $RequiredFiles;
3784  }
3785 
3789  private function SetUpObjectAutoloading()
3790  {
3792  function __autoload($ClassName)
3793  {
3794  // @codingStandardsIgnoreStart
3795  ApplicationFramework::AutoloadObjects($ClassName);
3796  // @codingStandardsIgnoreEnd
3797  }
3798  }
3799 
3805  static function AutoloadObjects($ClassName)
3806  {
3807  # if caching is not turned off
3808  # and we have a cached location for class
3809  # and file at cached location is readable
3810  if ((self::$ObjectLocationCacheInterval > 0)
3811  && array_key_exists($ClassName,
3812  self::$ObjectLocationCache)
3813  && is_readable(self::$ObjectLocationCache[$ClassName]))
3814  {
3815  # use object location from cache
3816  require_once(self::$ObjectLocationCache[$ClassName]);
3817  }
3818  else
3819  {
3820  # convert any namespace separators in class name
3821  $ClassName = str_replace("\\", "-", $ClassName);
3822 
3823  # for each possible object file directory
3824  static $FileLists;
3825  foreach (self::$ObjectDirectories as $Location => $Info)
3826  {
3827  # make any needed replacements in directory path
3828  $Location = str_replace("%ACTIVEUI%", self::$ActiveUI, $Location);
3829 
3830  # if directory looks valid
3831  if (is_dir($Location))
3832  {
3833  # build class file name
3834  $NewClassName = ($Info["ClassPattern"] && $Info["ClassReplacement"])
3835  ? preg_replace($Info["ClassPattern"],
3836  $Info["ClassReplacement"], $ClassName)
3837  : $ClassName;
3838 
3839  # read in directory contents if not already retrieved
3840  if (!isset($FileLists[$Location]))
3841  {
3842  $FileLists[$Location] = self::ReadDirectoryTree(
3843  $Location, '/^.+\.php$/i');
3844  }
3845 
3846  # for each file in target directory
3847  $FileNames = $FileLists[$Location];
3848  $TargetName = strtolower($Info["Prefix"].$NewClassName.".php");
3849  foreach ($FileNames as $FileName)
3850  {
3851  # if file matches our target object file name
3852  if (strtolower($FileName) == $TargetName)
3853  {
3854  # include object file
3855  require_once($Location.$FileName);
3856 
3857  # save location to cache
3858  self::$ObjectLocationCache[$ClassName]
3859  = $Location.$FileName;
3860 
3861  # set flag indicating that cache should be saved
3862  self::$SaveObjectLocationCache = TRUE;
3863 
3864  # stop looking
3865  break 2;
3866  }
3867  }
3868  }
3869  }
3870  }
3871  }
3881  private static function ReadDirectoryTree($Directory, $Pattern)
3882  {
3883  $CurrentDir = getcwd();
3884  chdir($Directory);
3885  $DirIter = new RecursiveDirectoryIterator(".");
3886  $IterIter = new RecursiveIteratorIterator($DirIter);
3887  $RegexResults = new RegexIterator($IterIter, $Pattern,
3888  RecursiveRegexIterator::GET_MATCH);
3889  $FileList = array();
3890  foreach ($RegexResults as $Result)
3891  {
3892  $FileList[] = substr($Result[0], 2);
3893  }
3894  chdir($CurrentDir);
3895  return $FileList;
3896  }
3897 
3901  private function UndoMagicQuotes()
3902  {
3903  # if this PHP version has magic quotes support
3904  if (version_compare(PHP_VERSION, "5.4.0", "<"))
3905  {
3906  # turn off runtime magic quotes if on
3907  if (get_magic_quotes_runtime())
3908  {
3909  // @codingStandardsIgnoreStart
3910  set_magic_quotes_runtime(FALSE);
3911  // @codingStandardsIgnoreEnd
3912  }
3913 
3914  # if magic quotes GPC is on
3915  if (get_magic_quotes_gpc())
3916  {
3917  # strip added slashes from incoming variables
3918  $GPC = array(&$_GET, &$_POST, &$_COOKIE, &$_REQUEST);
3919  array_walk_recursive($GPC,
3920  array($this, "UndoMagicQuotes_StripCallback"));
3921  }
3922  }
3923  }
3924  private function UndoMagicQuotes_StripCallback(&$Value)
3925  {
3926  $Value = stripslashes($Value);
3927  }
3928 
3933  private function LoadUIFunctions()
3934  {
3935  $Dirs = array(
3936  "local/interface/%ACTIVEUI%/include",
3937  "interface/%ACTIVEUI%/include",
3938  "local/interface/default/include",
3939  "interface/default/include",
3940  );
3941  foreach ($Dirs as $Dir)
3942  {
3943  $Dir = str_replace("%ACTIVEUI%", self::$ActiveUI, $Dir);
3944  if (is_dir($Dir))
3945  {
3946  $FileNames = scandir($Dir);
3947  foreach ($FileNames as $FileName)
3948  {
3949  if (preg_match("/^F-([A-Za-z0-9_]+)\.php/",
3950  $FileName, $Matches)
3951  || preg_match("/^F-([A-Za-z0-9_]+)\.html/",
3952  $FileName, $Matches))
3953  {
3954  if (!function_exists($Matches[1]))
3955  {
3956  include_once($Dir."/".$FileName);
3957  }
3958  }
3959  }
3960  }
3961  }
3962  }
3963 
3969  private function ProcessPeriodicEvent($EventName, $Callback)
3970  {
3971  # retrieve last execution time for event if available
3972  $Signature = self::GetCallbackSignature($Callback);
3973  $LastRun = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
3974  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
3975 
3976  # determine whether enough time has passed for event to execute
3977  $ShouldExecute = (($LastRun === NULL)
3978  || (time() > (strtotime($LastRun) + $this->EventPeriods[$EventName])))
3979  ? TRUE : FALSE;
3980 
3981  # if event should run
3982  if ($ShouldExecute)
3983  {
3984  # add event to task queue
3985  $WrapperCallback = array("ApplicationFramework", "PeriodicEventWrapper");
3986  $WrapperParameters = array(
3987  $EventName, $Callback, array("LastRunAt" => $LastRun));
3988  $this->QueueUniqueTask($WrapperCallback, $WrapperParameters);
3989  }
3990 
3991  # add event to list of periodic events
3992  $this->KnownPeriodicEvents[$Signature] = array(
3993  "Period" => $EventName,
3994  "Callback" => $Callback,
3995  "Queued" => $ShouldExecute);
3996  }
3997 
4005  private static function PeriodicEventWrapper($EventName, $Callback, $Parameters)
4006  {
4007  static $DB;
4008  if (!isset($DB)) { $DB = new Database(); }
4009 
4010  # run event
4011  $ReturnVal = call_user_func_array($Callback, $Parameters);
4012 
4013  # if event is already in database
4014  $Signature = self::GetCallbackSignature($Callback);
4015  if ($DB->Query("SELECT COUNT(*) AS EventCount FROM PeriodicEvents"
4016  ." WHERE Signature = '".addslashes($Signature)."'", "EventCount"))
4017  {
4018  # update last run time for event
4019  $DB->Query("UPDATE PeriodicEvents SET LastRunAt = "
4020  .(($EventName == "EVENT_PERIODIC")
4021  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
4022  : "NOW()")
4023  ." WHERE Signature = '".addslashes($Signature)."'");
4024  }
4025  else
4026  {
4027  # add last run time for event to database
4028  $DB->Query("INSERT INTO PeriodicEvents (Signature, LastRunAt) VALUES "
4029  ."('".addslashes($Signature)."', "
4030  .(($EventName == "EVENT_PERIODIC")
4031  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
4032  : "NOW()").")");
4033  }
4034  }
4035 
4041  private static function GetCallbackSignature($Callback)
4042  {
4043  return !is_array($Callback) ? $Callback
4044  : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
4045  ."::".$Callback[1];
4046  }
4047 
4052  private function PrepForTSR()
4053  {
4054  # if HTML has been output and it's time to launch another task
4055  # (only TSR if HTML has been output because otherwise browsers
4056  # may misbehave after connection is closed)
4057  if (($this->JumpToPage || !$this->SuppressHTML)
4058  && (time() > (strtotime($this->Settings["LastTaskRunAt"])
4059  + (ini_get("max_execution_time")
4060  / $this->Settings["MaxTasksRunning"]) + 5))
4061  && $this->GetTaskQueueSize()
4062  && $this->Settings["TaskExecutionEnabled"])
4063  {
4064  # begin buffering output for TSR
4065  ob_start();
4066 
4067  # let caller know it is time to launch another task
4068  return TRUE;
4069  }
4070  else
4071  {
4072  # let caller know it is not time to launch another task
4073  return FALSE;
4074  }
4075  }
4076 
4081  private function LaunchTSR()
4082  {
4083  # set headers to close out connection to browser
4084  if (!$this->NoTSR)
4085  {
4086  ignore_user_abort(TRUE);
4087  header("Connection: close");
4088  header("Content-Length: ".ob_get_length());
4089  }
4090 
4091  # output buffered content
4092  while (ob_get_level()) { ob_end_flush(); }
4093  flush();
4094 
4095  # write out any outstanding data and end HTTP session
4096  session_write_close();
4097 
4098  # set flag indicating that we are now running in background
4099  $this->RunningInBackground = TRUE;
4100 
4101  # handle garbage collection for session data
4102  if (isset($this->SessionStorage) &&
4103  (rand()/getrandmax()) <= $this->SessionGcProbability)
4104  {
4105  # determine when sessions will expire
4106  $ExpiredTime = strtotime("-". self::$SessionLifetime." seconds");
4107 
4108  # iterate over the files in our session storage, deleting the expired
4109  # ones (sess_ is php's session prefix)
4110  foreach (scandir($this->SessionStorage) as $TgtFile)
4111  {
4112  $FullPath = $this->SessionStorage."/".$TgtFile;
4113  if ( is_file($FullPath) &&
4114  preg_match("/^sess_/", $TgtFile) &&
4115  filectime($FullPath) < $ExpiredTime)
4116  {
4117  unlink($FullPath);
4118  }
4119  }
4120  }
4121 
4122  # if there is still a task in the queue
4123  if ($this->GetTaskQueueSize())
4124  {
4125  # garbage collect to give as much memory as possible for tasks
4126  if (function_exists("gc_collect_cycles")) { gc_collect_cycles(); }
4127 
4128  # turn on output buffering to (hopefully) record any crash output
4129  ob_start();
4130 
4131  # lock tables and grab last task run time to double check
4132  $this->DB->Query("LOCK TABLES ApplicationFrameworkSettings WRITE");
4133  $this->LoadSettings();
4134 
4135  # if still time to launch another task
4136  if (time() > (strtotime($this->Settings["LastTaskRunAt"])
4137  + (ini_get("max_execution_time")
4138  / $this->Settings["MaxTasksRunning"]) + 5))
4139  {
4140  # update the "last run" time and release tables
4141  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
4142  ." SET LastTaskRunAt = '".date("Y-m-d H:i:s")."'");
4143  $this->DB->Query("UNLOCK TABLES");
4144 
4145  # run tasks while there is a task in the queue
4146  # and enough time and memory left
4147  do
4148  {
4149  # run the next task
4150  $this->RunNextTask();
4151 
4152  # calculate percentage of memory still available
4153  $PercentFreeMem = (self::GetFreeMemory()
4154  / self::GetPhpMemoryLimit()) * 100;
4155  }
4156  while ($this->GetTaskQueueSize()
4157  && ($this->GetSecondsBeforeTimeout() > 65)
4158  && ($PercentFreeMem > $this->BackgroundTaskMinFreeMemPercent));
4159  }
4160  else
4161  {
4162  # release tables
4163  $this->DB->Query("UNLOCK TABLES");
4164  }
4165  }
4166  }
4167 
4175  private function GetTaskList($DBQuery, $Count, $Offset)
4176  {
4177  $this->DB->Query($DBQuery." LIMIT ".intval($Offset).",".intval($Count));
4178  $Tasks = array();
4179  while ($Row = $this->DB->FetchRow())
4180  {
4181  $Tasks[$Row["TaskId"]] = $Row;
4182  if ($Row["Callback"] ==
4183  serialize(array("ApplicationFramework", "PeriodicEventWrapper")))
4184  {
4185  $WrappedCallback = unserialize($Row["Parameters"]);
4186  $Tasks[$Row["TaskId"]]["Callback"] = $WrappedCallback[1];
4187  $Tasks[$Row["TaskId"]]["Parameters"] = NULL;
4188  }
4189  else
4190  {
4191  $Tasks[$Row["TaskId"]]["Callback"] = unserialize($Row["Callback"]);
4192  $Tasks[$Row["TaskId"]]["Parameters"] = unserialize($Row["Parameters"]);
4193  }
4194  }
4195  return $Tasks;
4196  }
4197 
4201  private function RunNextTask()
4202  {
4203  # lock tables to prevent same task from being run by multiple sessions
4204  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
4205 
4206  # look for task at head of queue
4207  $this->DB->Query("SELECT * FROM TaskQueue ORDER BY Priority, TaskId LIMIT 1");
4208  $Task = $this->DB->FetchRow();
4209 
4210  # if there was a task available
4211  if ($Task)
4212  {
4213  # move task from queue to running tasks list
4214  $this->DB->Query("INSERT INTO RunningTasks "
4215  ."(TaskId,Callback,Parameters,Priority,Description) "
4216  ."SELECT * FROM TaskQueue WHERE TaskId = "
4217  .intval($Task["TaskId"]));
4218  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = "
4219  .intval($Task["TaskId"]));
4220 
4221  # release table locks to again allow other sessions to run tasks
4222  $this->DB->Query("UNLOCK TABLES");
4223 
4224  # unpack stored task info
4225  $Callback = unserialize($Task["Callback"]);
4226  $Parameters = unserialize($Task["Parameters"]);
4227 
4228  # attempt to load task callback if not already available
4229  $this->LoadFunction($Callback);
4230 
4231  # save amount of free memory for later comparison
4232  $BeforeFreeMem = self::GetFreeMemory();
4233 
4234  # run task
4235  $this->RunningTask = $Task;
4236  if ($Parameters)
4237  {
4238  call_user_func_array($Callback, $Parameters);
4239  }
4240  else
4241  {
4242  call_user_func($Callback);
4243  }
4244  unset($this->RunningTask);
4245 
4246  # log if task leaked significant memory
4247  if (function_exists("gc_collect_cycles")) { gc_collect_cycles(); }
4248  $AfterFreeMem = self::GetFreeMemory();
4249  $LeakThreshold = self::GetPhpMemoryLimit()
4250  * ($this->BackgroundTaskMemLeakLogThreshold / 100);
4251  if (($BeforeFreeMem - $AfterFreeMem) > $LeakThreshold)
4252  {
4253  $this->LogError(self::LOGLVL_DEBUG, "Task "
4254  .self::GetTaskCallbackSynopsis(
4255  $this->GetTask($Task["TaskId"]))." leaked "
4256  .number_format($BeforeFreeMem - $AfterFreeMem)." bytes.");
4257  }
4258 
4259  # remove task from running tasks list
4260  $this->DB->Query("DELETE FROM RunningTasks"
4261  ." WHERE TaskId = ".intval($Task["TaskId"]));
4262 
4263  # prune running tasks list if necessary
4264  $RunningTasksCount = $this->DB->Query(
4265  "SELECT COUNT(*) AS TaskCount FROM RunningTasks", "TaskCount");
4266  if ($RunningTasksCount > $this->MaxRunningTasksToTrack)
4267  {
4268  $this->DB->Query("DELETE FROM RunningTasks ORDER BY StartedAt"
4269  ." LIMIT ".($RunningTasksCount - $this->MaxRunningTasksToTrack));
4270  }
4271  }
4272  else
4273  {
4274  # release table locks to again allow other sessions to run tasks
4275  $this->DB->Query("UNLOCK TABLES");
4276  }
4277  }
4278 
4284  function OnCrash()
4285  {
4286  # attempt to remove any memory limits
4287  $FreeMemory = $this->GetFreeMemory();
4288  ini_set("memory_limit", -1);
4289 
4290  # if there is a background task currently running
4291  if (isset($this->RunningTask))
4292  {
4293  # add info about current page load
4294  $CrashInfo["ElapsedTime"] = $this->GetElapsedExecutionTime();
4295  $CrashInfo["FreeMemory"] = $FreeMemory;
4296  $CrashInfo["REMOTE_ADDR"] = $_SERVER["REMOTE_ADDR"];
4297  $CrashInfo["REQUEST_URI"] = $_SERVER["REQUEST_URI"];
4298  if (isset($_SERVER["REQUEST_TIME"]))
4299  {
4300  $CrashInfo["REQUEST_TIME"] = $_SERVER["REQUEST_TIME"];
4301  }
4302  if (isset($_SERVER["REMOTE_HOST"]))
4303  {
4304  $CrashInfo["REMOTE_HOST"] = $_SERVER["REMOTE_HOST"];
4305  }
4306 
4307  # add info about error that caused crash (if available)
4308  if (function_exists("error_get_last"))
4309  {
4310  $CrashInfo["LastError"] = error_get_last();
4311  }
4312 
4313  # add info about current output buffer contents (if available)
4314  if (ob_get_length() !== FALSE)
4315  {
4316  $CrashInfo["OutputBuffer"] = ob_get_contents();
4317  }
4318 
4319  # if backtrace info is available for the crash
4320  $Backtrace = debug_backtrace();
4321  if (count($Backtrace) > 1)
4322  {
4323  # discard the current context from the backtrace
4324  array_shift($Backtrace);
4325 
4326  # add the backtrace to the crash info
4327  $CrashInfo["Backtrace"] = $Backtrace;
4328  }
4329  # else if saved backtrace info is available
4330  elseif (isset($this->SavedContext))
4331  {
4332  # add the saved backtrace to the crash info
4333  $CrashInfo["Backtrace"] = $this->SavedContext;
4334  }
4335 
4336  # save crash info for currently running task
4337  $DB = new Database();
4338  $DB->Query("UPDATE RunningTasks SET CrashInfo = '"
4339  .addslashes(serialize($CrashInfo))
4340  ."' WHERE TaskId = ".intval($this->RunningTask["TaskId"]));
4341  }
4342 
4343  print("\n");
4344  return;
4345  }
4346 
4363  private function AddToDirList($DirList, $Dir, $SearchLast, $SkipSlashCheck)
4364  {
4365  # convert incoming directory to array of directories (if needed)
4366  $Dirs = is_array($Dir) ? $Dir : array($Dir);
4367 
4368  # reverse array so directories are searched in specified order
4369  $Dirs = array_reverse($Dirs);
4370 
4371  # for each directory
4372  foreach ($Dirs as $Location)
4373  {
4374  # make sure directory includes trailing slash
4375  if (!$SkipSlashCheck)
4376  {
4377  $Location = $Location
4378  .((substr($Location, -1) != "/") ? "/" : "");
4379  }
4380 
4381  # remove directory from list if already present
4382  if (in_array($Location, $DirList))
4383  {
4384  $DirList = array_diff(
4385  $DirList, array($Location));
4386  }
4387 
4388  # add directory to list of directories
4389  if ($SearchLast)
4390  {
4391  array_push($DirList, $Location);
4392  }
4393  else
4394  {
4395  array_unshift($DirList, $Location);
4396  }
4397  }
4398 
4399  # return updated directory list to caller
4400  return $DirList;
4401  }
4402 
4410  private function ArrayPermutations($Items, $Perms = array())
4411  {
4412  if (empty($Items))
4413  {
4414  $Result = array($Perms);
4415  }
4416  else
4417  {
4418  $Result = array();
4419  for ($Index = count($Items) - 1; $Index >= 0; --$Index)
4420  {
4421  $NewItems = $Items;
4422  $NewPerms = $Perms;
4423  list($Segment) = array_splice($NewItems, $Index, 1);
4424  array_unshift($NewPerms, $Segment);
4425  $Result = array_merge($Result,
4426  $this->ArrayPermutations($NewItems, $NewPerms));
4427  }
4428  }
4429  return $Result;
4430  }
4431 
4438  private function OutputModificationCallbackShell($Matches)
4439  {
4440  # call previously-stored external function
4441  return call_user_func($this->OutputModificationCallbackInfo["Callback"],
4442  $Matches,
4443  $this->OutputModificationCallbackInfo["Pattern"],
4444  $this->OutputModificationCallbackInfo["Page"],
4445  $this->OutputModificationCallbackInfo["SearchPattern"]);
4446  }
4447 
4454  function UpdateSetting($FieldName, $NewValue = DB_NOVALUE)
4455  {
4456  return $this->DB->UpdateValue("ApplicationFrameworkSettings",
4457  $FieldName, $NewValue, NULL, $this->Settings);
4458  }
4459 
4461  private $InterfaceDirList = array(
4462  "local/interface/%ACTIVEUI%/",
4463  "interface/%ACTIVEUI%/",
4464  "local/interface/default/",
4465  "interface/default/",
4466  );
4471  private $IncludeDirList = array(
4472  "local/interface/%ACTIVEUI%/include/",
4473  "interface/%ACTIVEUI%/include/",
4474  "interface/%ACTIVEUI%/objects/",
4475  "local/interface/default/include/",
4476  "interface/default/include/",
4477  "interface/default/objects/",
4478  );
4480  private $ImageDirList = array(
4481  "local/interface/%ACTIVEUI%/images/",
4482  "interface/%ACTIVEUI%/images/",
4483  "local/interface/default/images/",
4484  "interface/default/images/",
4485  );
4487  private $FunctionDirList = array(
4488  "local/interface/%ACTIVEUI%/include/",
4489  "interface/%ACTIVEUI%/include/",
4490  "local/interface/default/include/",
4491  "interface/default/include/",
4492  "local/include/",
4493  "include/",
4494  );
4495 
4496  const NOVALUE = ".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";
4497 
4498 
4499  # ---- Page Caching (Internal Methods) -----------------------------------
4500 
4506  private function CheckForCachedPage($PageName)
4507  {
4508  # assume no cached page will be found
4509  $CachedPage = NULL;
4510 
4511  # if returning a cached page is allowed
4512  if ($this->CacheCurrentPage)
4513  {
4514  # get fingerprint for requested page
4515  $PageFingerprint = $this->GetPageFingerprint($PageName);
4516 
4517  # look for matching page in cache in database
4518  $this->DB->Query("SELECT * FROM AF_CachedPages"
4519  ." WHERE Fingerprint = '".addslashes($PageFingerprint)."'");
4520 
4521  # if matching page found
4522  if ($this->DB->NumRowsSelected())
4523  {
4524  # if cached page has expired
4525  $Row = $this->DB->FetchRow();
4526  $ExpirationTime = strtotime(
4527  "-".$this->PageCacheExpirationPeriod()." seconds");
4528  if (strtotime($Row["CachedAt"]) < $ExpirationTime)
4529  {
4530  # clear expired pages from cache
4531  $ExpirationTimestamp = date("Y-m-d H:i:s", $ExpirationTime);
4532  $this->DB->Query("DELETE CP, CPTI FROM AF_CachedPages CP,"
4533  ." AF_CachedPageTagInts CPTI"
4534  ." WHERE CP.CachedAt < '".$ExpirationTimestamp."'"
4535  ." AND CPTI.CacheId = CP.CacheId");
4536  $this->DB->Query("DELETE FROM AF_CachedPages "
4537  ." WHERE CachedAt < '".$ExpirationTimestamp."'");
4538  }
4539  else
4540  {
4541  # display cached page and exit
4542  $CachedPage = $Row["PageContent"];
4543  }
4544  }
4545  }
4546 
4547  # return any cached page found to caller
4548  return $CachedPage;
4549  }
4550 
4556  private function UpdatePageCache($PageName, $PageContent)
4557  {
4558  # if page caching is enabled and current page should be cached
4559  if ($this->PageCacheEnabled()
4560  && $this->CacheCurrentPage
4561  && ($PageName != "404"))
4562  {
4563  # save page to cache
4564  $PageFingerprint = $this->GetPageFingerprint($PageName);
4565  $this->DB->Query("INSERT INTO AF_CachedPages"
4566  ." (Fingerprint, PageContent) VALUES"
4567  ." ('".$this->DB->EscapeString($PageFingerprint)."', '"
4568  .$this->DB->EscapeString($PageContent)."')");
4569  $CacheId = $this->DB->LastInsertId();
4570 
4571  # for each page cache tag that was added
4572  foreach ($this->PageCacheTags as $Tag => $Pages)
4573  {
4574  # if current page is in list for tag
4575  if (in_array("CURRENT", $Pages) || in_array($PageName, $Pages))
4576  {
4577  # look up tag ID
4578  $TagId = $this->GetPageCacheTagId($Tag);
4579 
4580  # mark current page as associated with tag
4581  $this->DB->Query("INSERT INTO AF_CachedPageTagInts"
4582  ." (TagId, CacheId) VALUES "
4583  ." (".intval($TagId).", ".intval($CacheId).")");
4584  }
4585  }
4586  }
4587  }
4588 
4594  private function GetPageCacheTagId($Tag)
4595  {
4596  # if tag is a non-negative integer
4597  if (is_numeric($Tag) && ($Tag > 0) && (intval($Tag) == $Tag))
4598  {
4599  # generate ID
4600  $Id = self::PAGECACHETAGIDOFFSET + $Tag;
4601  }
4602  else
4603  {
4604  # look up ID in database
4605  $Id = $this->DB->Query("SELECT TagId FROM AF_CachedPageTags"
4606  ." WHERE Tag = '".addslashes($Tag)."'", "TagId");
4607 
4608  # if ID was not found
4609  if ($Id === NULL)
4610  {
4611  # add tag to database
4612  $this->DB->Query("INSERT INTO AF_CachedPageTags"
4613  ." SET Tag = '".addslashes($Tag)."'");
4614  $Id = $this->DB->LastInsertId();
4615  }
4616  }
4617 
4618  # return tag ID to caller
4619  return $Id;
4620  }
4621 
4627  private function GetPageFingerprint($PageName)
4628  {
4629  # only get the environmental fingerprint once so that it is consistent
4630  # between page construction start and end
4631  static $EnvFingerprint;
4632  if (!isset($EnvFingerprint))
4633  {
4634  $EnvFingerprint = md5(json_encode($_GET).json_encode($_POST));
4635  }
4636 
4637  # build page fingerprint and return it to caller
4638  return $PageName."-".$EnvFingerprint;
4639  }
4640 }
Abstraction for forum messages and resource comments.
Definition: Message.php:15
SQL database abstraction object with smart query caching.
const DB_NOVALUE
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