CWIS Developer Documentation
ApplicationFramework.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: ApplicationFramework.php
4 #
5 # Part of the ScoutLib application support library
6 # Copyright 2009-2016 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu
8 #
9 
14 class ApplicationFramework
15 {
16 
17  # ---- PUBLIC INTERFACE --------------------------------------------------
18  /*@(*/
20 
25  public function __construct()
26  {
27  # make sure default time zone is set
28  # (using CST if nothing set because we have to use something
29  # and Scout is based in Madison, WI, which is in CST)
30  if ((ini_get("date.timezone") === NULL)
31  || !strlen(ini_get("date.timezone")))
32  {
33  ini_set("date.timezone", "America/Chicago");
34  }
35 
36  # save execution start time
37  $this->ExecutionStartTime = microtime(TRUE);
38 
39  # set up default object file search locations
40  self::AddObjectDirectory("local/interface/%ACTIVEUI%/objects");
41  self::AddObjectDirectory("interface/%ACTIVEUI%/objects");
42  self::AddObjectDirectory("local/interface/%DEFAULTUI%/objects");
43  self::AddObjectDirectory("interface/%DEFAULTUI%/objects");
44  self::AddObjectDirectory("local/objects");
45  self::AddObjectDirectory("objects");
46 
47  # set up object file autoloader
48  spl_autoload_register(array($this, "AutoloadObjects"));
49 
50  # set up function to output any buffered text in case of crash
51  register_shutdown_function(array($this, "OnCrash"));
52 
53  # if we were not invoked via command line interface
54  if (php_sapi_name() !== "cli")
55  {
56  # build cookie domain string
57  $SessionDomain = isset($_SERVER["SERVER_NAME"]) ? $_SERVER["SERVER_NAME"]
58  : isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"]
59  : php_uname("n");
60 
61  # include a leading period so that older browsers implementing
62  # rfc2109 do not reject our cookie
63  $SessionDomain = ".".$SessionDomain;
64 
65  # if it appears our session storage area is writable
66  if (is_writable(session_save_path()))
67  {
68  # store our session files in a subdirectory to avoid
69  # accidentally sharing sessions with other installations
70  # on the same domain
71  $SessionStorage = session_save_path()
72  ."/".self::$AppName."_".md5($SessionDomain.dirname(__FILE__));
73 
74  # create session storage subdirectory if not found
75  if (!is_dir($SessionStorage)) { mkdir($SessionStorage, 0700 ); }
76 
77  # if session storage subdirectory is writable
78  if (is_writable($SessionStorage))
79  {
80  # save parameters of our session storage as instance variables
81  # for later use
82  $this->SessionGcProbability =
83  ini_get("session.gc_probability") / ini_get("session.gc_divisor");
84  # require a gc probability of at least MIN_GC_PROBABILITY
85  if ($this->SessionGcProbability < self::MIN_GC_PROBABILITY)
86  {
87  $this->SessionGcProbability = self::MIN_GC_PROBABILITY;
88  }
89 
90  $this->SessionStorage = $SessionStorage;
91 
92  # set the new session storage location
93  session_save_path($SessionStorage);
94 
95  # disable PHP's garbage collection, as it does not handle
96  # subdirectories (instead, we'll do the cleanup as we run
97  # background tasks)
98  ini_set("session.gc_probability", 0);
99  }
100  }
101 
102  # set garbage collection max period to our session lifetime
103  ini_set("session.gc_maxlifetime", self::$SessionLifetime);
104 
105  # Cookies lacking embedded dots are... fun.
106  # rfc2109 sec 4.3.2 says to reject them
107  # rfc2965 sec 3.3.2 says to reject them
108  # rfc6265 sec 4.1.2.3 says only that "public suffixes"
109  # should be rejected. They reference Mozilla's
110  # publicsuffix.org, which does not contain 'localhost'.
111  # However, empirically in early 2017 Firefox still rejects
112  # 'localhost'.
113  # Therefore, don't set a cookie domain if we're running on
114  # localhost to avoid this problem.
115  if (preg_match('/^\.localhost(:[0-9]+)?$/', $SessionDomain))
116  {
117  session_set_cookie_params(
118  self::$SessionLifetime, "/", "");
119  }
120  else
121  {
122  session_set_cookie_params(
123  self::$SessionLifetime, "/", $SessionDomain);
124  }
125 
126  # attempt to start session
127  $SessionStarted = @session_start();
128 
129  # if session start failed
130  if (!$SessionStarted)
131  {
132  # regenerate session ID and attempt to start session again
133  session_regenerate_id(TRUE);
134  session_start();
135  }
136  }
137 
138  # set up our internal environment
139  $this->DB = new Database();
140 
141  # set up our exception handler
142  set_exception_handler(array($this, "GlobalExceptionHandler"));
143 
144  # perform any work needed to undo PHP magic quotes
145  $this->UndoMagicQuotes();
146 
147  # load our settings from database
148  $this->LoadSettings();
149 
150  # set PHP maximum execution time
151  ini_set("max_execution_time", $this->Settings["MaxExecTime"]);
152  set_time_limit($this->Settings["MaxExecTime"]);
153 
154  # register events we handle internally
155  $this->RegisterEvent($this->PeriodicEvents);
156  $this->RegisterEvent($this->UIEvents);
157 
158  # attempt to create SCSS cache directory if needed and it does not exist
159  if ($this->ScssSupportEnabled() && !is_dir(self::$ScssCacheDir))
160  { @mkdir(self::$ScssCacheDir, 0777, TRUE); }
161 
162  # attempt to create minimized JS cache directory if needed and it does not exist
163  if ($this->UseMinimizedJavascript()
164  && $this->JavascriptMinimizationEnabled()
165  && !is_dir(self::$JSMinCacheDir))
166  {
167  @mkdir(self::$JSMinCacheDir, 0777, TRUE);
168  }
169  }
176  public function __destruct()
177  {
178  # if template location cache is flagged to be saved
179  if ($this->SaveTemplateLocationCache)
180  {
181  # write template location cache out and update cache expiration
182  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
183  ." SET TemplateLocationCache = '"
184  .addslashes(serialize(
185  $this->TemplateLocationCache))."',"
186  ." TemplateLocationCacheExpiration = '"
187  .date("Y-m-d H:i:s",
188  $this->TemplateLocationCacheExpiration)."'");
189  }
190 
191  # if object location cache is flagged to be saved
192  if (self::$SaveObjectLocationCache)
193  {
194  # write object location cache out and update cache expiration
195  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
196  ." SET ObjectLocationCache = '"
197  .addslashes(serialize(
198  self::$ObjectLocationCache))."',"
199  ." ObjectLocationCacheExpiration = '"
200  .date("Y-m-d H:i:s",
201  self::$ObjectLocationCacheExpiration)."'");
202  }
203  }
211  public function GlobalExceptionHandler($Exception)
212  {
213  # display exception info
214  $Message = $Exception->getMessage();
215  $Location = str_replace(getcwd()."/", "",
216  $Exception->getFile()."[".$Exception->getLine()."]");
217  $Trace = preg_replace(":(#[0-9]+) ".getcwd()."/".":", "$1 ",
218  $Exception->getTraceAsString());
219  if (php_sapi_name() == "cli")
220  {
221  print "Uncaught Exception\n"
222  ."Message: ".$Message."\n"
223  ."Location: ".$Location."\n"
224  ."Trace: \n"
225  .$Trace."\n";
226  }
227  else
228  {
229  ?><table width="100%" cellpadding="5"
230  style="border: 2px solid #666666; background: #CCCCCC;
231  font-family: Courier New, Courier, monospace;
232  margin-top: 10px;"><tr><td>
233  <div style="color: #666666;">
234  <span style="font-size: 150%;">
235  <b>Uncaught Exception</b></span><br />
236  <b>Message:</b> <i><?= $Message ?></i><br />
237  <b>Location:</b> <i><?= $Location ?></i><br />
238  <b>Trace:</b> <blockquote><pre><?= $Trace ?></pre></blockquote>
239  </div>
240  </td></tr></table><?PHP
241  }
242 
243  # log exception if not running from command line
244  if (php_sapi_name() !== "cli")
245  {
246  $TraceString = $Exception->getTraceAsString();
247  $TraceString = str_replace("\n", ", ", $TraceString);
248  $TraceString = preg_replace(":(#[0-9]+) ".getcwd()."/".":",
249  "$1 ", $TraceString);
250  $LogMsg = "Uncaught exception (".$Exception->getMessage().")"
251  ." at ".$Location."."
252  ." TRACE: ".$TraceString
253  ." URL: ".$this->FullUrl();
254  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
255  }
256  }
273  public static function AddObjectDirectory(
274  $Dir, $Prefix = "", $ClassPattern = NULL, $ClassReplacement = NULL)
275  {
276  # make sure directory has trailing slash
277  $Dir = $Dir.((substr($Dir, -1) != "/") ? "/" : "");
278 
279  # add directory to directory list
280  self::$ObjectDirectories[$Dir] = array(
281  "Prefix" => $Prefix,
282  "ClassPattern" => $ClassPattern,
283  "ClassReplacement" => $ClassReplacement,
284  );
285  }
286 
306  public function AddImageDirectories(
307  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
308  {
309  # add directories to existing image directory list
310  $this->ImageDirList = $this->AddToDirList(
311  $this->ImageDirList, $Dir, $SearchLast, $SkipSlashCheck);
312  }
313 
334  public function AddIncludeDirectories(
335  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
336  {
337  # add directories to existing image directory list
338  $this->IncludeDirList = $this->AddToDirList(
339  $this->IncludeDirList, $Dir, $SearchLast, $SkipSlashCheck);
340  }
341 
361  public function AddInterfaceDirectories(
362  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
363  {
364  # add directories to existing image directory list
365  $this->InterfaceDirList = $this->AddToDirList(
366  $this->InterfaceDirList, $Dir, $SearchLast, $SkipSlashCheck);
367  }
368 
388  public function AddFunctionDirectories(
389  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
390  {
391  # add directories to existing image directory list
392  $this->FunctionDirList = $this->AddToDirList(
393  $this->FunctionDirList, $Dir, $SearchLast, $SkipSlashCheck);
394  }
395 
401  public function SetBrowserDetectionFunc($DetectionFunc)
402  {
403  $this->BrowserDetectFunc = $DetectionFunc;
404  }
405 
412  public function AddUnbufferedCallback($Callback, $Parameters=array())
413  {
414  if (is_callable($Callback))
415  {
416  $this->UnbufferedCallbacks[] = array($Callback, $Parameters);
417  }
418  }
419 
426  public function TemplateLocationCacheExpirationInterval($NewInterval = DB_NOVALUE)
427  {
428  return $this->UpdateSetting("TemplateLocationCacheInterval", $NewInterval);
429  }
430 
434  public function ClearTemplateLocationCache()
435  {
436  $this->TemplateLocationCache = array();
437  $this->SaveTemplateLocationCache = TRUE;
438  }
439 
446  public function ObjectLocationCacheExpirationInterval($NewInterval = DB_NOVALUE)
447  {
448  return $this->UpdateSetting("ObjectLocationCacheInterval", $NewInterval);
449  }
450 
454  public function ClearObjectLocationCache()
455  {
456  self::$ObjectLocationCache = array();
457  self::$SaveObjectLocationCache = TRUE;
458  }
459 
466  public function UrlFingerprintingEnabled($NewValue = DB_NOVALUE)
467  {
468  return $this->UpdateSetting("UrlFingerprintingEnabled", $NewValue);
469  }
470 
478  public function ScssSupportEnabled($NewValue = DB_NOVALUE)
479  {
480  return $this->UpdateSetting("ScssSupportEnabled", $NewValue);
481  }
482 
491  public function GenerateCompactCss($NewValue = DB_NOVALUE)
492  {
493  return $this->UpdateSetting("GenerateCompactCss", $NewValue);
494  }
495 
504  public function UseMinimizedJavascript($NewValue = DB_NOVALUE)
505  {
506  return $this->UpdateSetting("UseMinimizedJavascript", $NewValue);
507  }
508 
517  public function JavascriptMinimizationEnabled($NewValue = DB_NOVALUE)
518  {
519  return $this->UpdateSetting("JavascriptMinimizationEnabled", $NewValue);
520  }
521 
535  public function RecordContextInCaseOfCrash(
536  $BacktraceOptions = 0, $BacktraceLimit = 0)
537  {
538  if (version_compare(PHP_VERSION, "5.4.0", ">="))
539  {
540  $this->SavedContext = debug_backtrace(
541  $BacktraceOptions, $BacktraceLimit);
542  }
543  else
544  {
545  $this->SavedContext = debug_backtrace($BacktraceOptions);
546  }
547  array_shift($this->SavedContext);
548  }
549 
554  public function LoadPage($PageName)
555  {
556  # perform any clean URL rewriting
557  $PageName = $this->RewriteCleanUrls($PageName);
558 
559  # sanitize incoming page name and save local copy
560  $PageName = preg_replace("/[^a-zA-Z0-9_.-]/", "", $PageName);
561  $this->PageName = $PageName;
562 
563  # if page caching is turned on
564  if ($this->PageCacheEnabled())
565  {
566  # if we have a cached page
567  $CachedPage = $this->CheckForCachedPage($PageName);
568  if ($CachedPage !== NULL)
569  {
570  # set header to indicate cache hit was found
571  header("X-ScoutAF-Cache: HIT");
572 
573  # display cached page and exit
574  print $CachedPage;
575  return;
576  }
577  else
578  {
579  # set header to indicate no cache hit was found
580  header("X-ScoutAF-Cache: MISS");
581  }
582  }
583 
584  # buffer any output from includes or PHP file
585  ob_start();
586 
587  # include any files needed to set up execution environment
588  $IncludeFileContext = array();
589  foreach ($this->EnvIncludes as $IncludeFile)
590  {
591  $IncludeFileContext = $this->FilterContext(self::CONTEXT_ENV,
592  self::IncludeFile($IncludeFile, $IncludeFileContext));
593  }
594 
595  # signal page load
596  $this->SignalEvent("EVENT_PAGE_LOAD", array("PageName" => $PageName));
597 
598  # signal PHP file load
599  $SignalResult = $this->SignalEvent("EVENT_PHP_FILE_LOAD", array(
600  "PageName" => $PageName));
601 
602  # if signal handler returned new page name value
603  $NewPageName = $PageName;
604  if (($SignalResult["PageName"] != $PageName)
605  && strlen($SignalResult["PageName"]))
606  {
607  # if new page name value is page file
608  if (file_exists($SignalResult["PageName"]))
609  {
610  # use new value for PHP file name
611  $PageFile = $SignalResult["PageName"];
612  }
613  else
614  {
615  # use new value for page name
616  $NewPageName = $SignalResult["PageName"];
617  }
618 
619  # update local copy of page name
620  $this->PageName = $NewPageName;
621  }
622 
623  # if we do not already have a PHP file
624  if (!isset($PageFile))
625  {
626  # look for PHP file for page
627  $OurPageFile = "pages/".$NewPageName.".php";
628  $LocalPageFile = "local/pages/".$NewPageName.".php";
629  $PageFile = file_exists($LocalPageFile) ? $LocalPageFile
630  : (file_exists($OurPageFile) ? $OurPageFile
631  : "pages/".$this->DefaultPage.".php");
632  }
633 
634  # load PHP file
635  $IncludeFileContext = $this->FilterContext(self::CONTEXT_PAGE,
636  self::IncludeFile($PageFile, $IncludeFileContext));
637 
638  # save buffered output to be displayed later after HTML file loads
639  $PageOutput = ob_get_contents();
640  ob_end_clean();
641 
642  # signal PHP file load is complete
643  ob_start();
644  $Context["Variables"] = $IncludeFileContext;
645  $this->SignalEvent("EVENT_PHP_FILE_LOAD_COMPLETE",
646  array("PageName" => $PageName, "Context" => $Context));
647  $PageCompleteOutput = ob_get_contents();
648  ob_end_clean();
649 
650  # set up for possible TSR (Terminate and Stay Resident :))
651  $ShouldTSR = $this->PrepForTSR();
652 
653  # if PHP file indicated we should autorefresh to somewhere else
654  if (($this->JumpToPage) && ($this->JumpToPageDelay == 0))
655  {
656  if (!strlen(trim($PageOutput)))
657  {
658  # if client supports HTTP/1.1, use a 303 as it is most accurate
659  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.1")
660  {
661  header("HTTP/1.1 303 See Other");
662  header("Location: ".$this->JumpToPage);
663  }
664  else
665  {
666  # if the request was an HTTP/1.0 GET or HEAD, then
667  # use a 302 response code.
668 
669  # NB: both RFC 2616 (HTTP/1.1) and RFC1945 (HTTP/1.0)
670  # explicitly prohibit automatic redirection via a 302
671  # if the request was not GET or HEAD.
672  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.0" &&
673  ($_SERVER["REQUEST_METHOD"] == "GET" ||
674  $_SERVER["REQUEST_METHOD"] == "HEAD") )
675  {
676  header("HTTP/1.0 302 Found");
677  header("Location: ".$this->JumpToPage);
678  }
679 
680  # otherwise, fall back to a meta refresh
681  else
682  {
683  print '<html><head><meta http-equiv="refresh" '
684  .'content="0; URL='.$this->JumpToPage.'">'
685  .'</head><body></body></html>';
686  }
687  }
688  }
689  }
690  # else if HTML loading is not suppressed
691  elseif (!$this->SuppressHTML)
692  {
693  # set content-type to get rid of diacritic errors
694  header("Content-Type: text/html; charset="
695  .$this->HtmlCharset, TRUE);
696 
697  # load common HTML file (defines common functions) if available
698  $CommonHtmlFile = $this->FindFile($this->IncludeDirList,
699  "Common", array("tpl", "html"));
700  if ($CommonHtmlFile)
701  {
702  $IncludeFileContext = $this->FilterContext(self::CONTEXT_COMMON,
703  self::IncludeFile($CommonHtmlFile, $IncludeFileContext));
704  }
705 
706  # load UI functions
707  $this->LoadUIFunctions();
708 
709  # begin buffering content
710  ob_start();
711 
712  # signal HTML file load
713  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD", array(
714  "PageName" => $PageName));
715 
716  # if signal handler returned new page name value
717  $NewPageName = $PageName;
718  $PageContentFile = NULL;
719  if (($SignalResult["PageName"] != $PageName)
720  && strlen($SignalResult["PageName"]))
721  {
722  # if new page name value is HTML file
723  if (file_exists($SignalResult["PageName"]))
724  {
725  # use new value for HTML file name
726  $PageContentFile = $SignalResult["PageName"];
727  }
728  else
729  {
730  # use new value for page name
731  $NewPageName = $SignalResult["PageName"];
732  }
733  }
734 
735  # load page content HTML file if available
736  if ($PageContentFile === NULL)
737  {
738  $PageContentFile = $this->FindFile(
739  $this->InterfaceDirList, $NewPageName,
740  array("tpl", "html"));
741  }
742  if ($PageContentFile)
743  {
744  $IncludeFileContext = $this->FilterContext(self::CONTEXT_INTERFACE,
745  self::IncludeFile($PageContentFile, $IncludeFileContext));
746  }
747  else
748  {
749  print "<h2>ERROR: No HTML/TPL template found"
750  ." for this page (".$NewPageName.").</h2>";
751  }
752 
753  # signal HTML file load complete
754  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD_COMPLETE");
755 
756  # stop buffering and save output
757  $PageContentOutput = ob_get_contents();
758  ob_end_clean();
759 
760  # if standard page start/end have not been suppressed
761  $PageStartOutput = "";
762  $PageEndOutput = "";
763  if (!$this->SuppressStdPageStartAndEnd)
764  {
765  # load page start HTML file if available
766  $PageStartFile = $this->FindFile($this->IncludeDirList, "Start",
767  array("tpl", "html"), array("StdPage", "StandardPage"));
768  if ($PageStartFile)
769  {
770  ob_start();
771  $IncludeFileContext = self::IncludeFile(
772  $PageStartFile, $IncludeFileContext);
773  $PageStartOutput = ob_get_contents();
774  ob_end_clean();
775  }
776  $IncludeFileContext = $this->FilterContext(
777  self::CONTEXT_START, $IncludeFileContext);
778 
779  # load page end HTML file if available
780  $PageEndFile = $this->FindFile($this->IncludeDirList, "End",
781  array("tpl", "html"), array("StdPage", "StandardPage"));
782  if ($PageEndFile)
783  {
784  ob_start();
785  self::IncludeFile($PageEndFile, $IncludeFileContext);
786  $PageEndOutput = ob_get_contents();
787  ob_end_clean();
788  }
789  }
790 
791  # clear include file context because it may be large and is no longer needed
792  unset($IncludeFileContext);
793 
794  # if page auto-refresh requested
795  if ($this->JumpToPage)
796  {
797  # add auto-refresh tag to page
798  $this->AddMetaTag([
799  "http-equiv" => "refresh",
800  "content" => $this->JumpToPageDelay,
801  "url" => $this->JumpToPage,
802  ]);
803  }
804 
805  # assemble full page
806  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
807 
808  # get list of any required files not loaded
809  $RequiredFiles = $this->GetRequiredFilesNotYetLoaded($PageContentFile);
810 
811  # add file loading tags to page
812  $FullPageOutput = $this->AddFileTagsToPageOutput(
813  $FullPageOutput, $RequiredFiles);
814 
815  # add any requested meta tags to page
816  $FullPageOutput = $this->AddMetaTagsToPageOutput($FullPageOutput);
817 
818  # perform any regular expression replacements in output
819  $NewFullPageOutput = preg_replace($this->OutputModificationPatterns,
820  $this->OutputModificationReplacements, $FullPageOutput);
821 
822  # check to make sure replacements didn't fail
823  $FullPageOutput = $this->CheckOutputModification(
824  $FullPageOutput, $NewFullPageOutput,
825  "regular expression replacements");
826 
827  # for each registered output modification callback
828  foreach ($this->OutputModificationCallbacks as $Info)
829  {
830  # set up data for callback
831  $this->OutputModificationCallbackInfo = $Info;
832 
833  # perform output modification
834  $NewFullPageOutput = preg_replace_callback($Info["SearchPattern"],
835  array($this, "OutputModificationCallbackShell"),
836  $FullPageOutput);
837 
838  # check to make sure modification didn't fail
839  $ErrorInfo = "callback info: ".print_r($Info, TRUE);
840  $FullPageOutput = $this->CheckOutputModification(
841  $FullPageOutput, $NewFullPageOutput, $ErrorInfo);
842  }
843 
844  # provide the opportunity to modify full page output
845  $SignalResult = $this->SignalEvent("EVENT_PAGE_OUTPUT_FILTER", array(
846  "PageOutput" => $FullPageOutput));
847  if (isset($SignalResult["PageOutput"])
848  && strlen(trim($SignalResult["PageOutput"])))
849  {
850  $FullPageOutput = $SignalResult["PageOutput"];
851  }
852 
853  # if relative paths may not work because we were invoked via clean URL
854  if ($this->CleanUrlRewritePerformed || self::WasUrlRewritten())
855  {
856  # if using the <base> tag is okay
857  $BaseUrl = $this->BaseUrl();
858  if ($this->UseBaseTag)
859  {
860  # add <base> tag to header
861  $PageStartOutput = str_replace("<head>",
862  "<head><base href=\"".$BaseUrl."\" />",
863  $PageStartOutput);
864 
865  # re-assemble full page with new header
866  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
867 
868  # the absolute URL to the current page
869  $FullUrl = $BaseUrl . $this->GetPageLocation();
870 
871  # make HREF attribute values with just a fragment ID
872  # absolute since they don't work with the <base> tag because
873  # they are relative to the current page/URL, not the site
874  # root
875  $NewFullPageOutput = preg_replace(
876  array("%href=\"(#[^:\" ]+)\"%i", "%href='(#[^:' ]+)'%i"),
877  array("href=\"".$FullUrl."$1\"", "href='".$FullUrl."$1'"),
878  $FullPageOutput);
879 
880  # check to make sure HREF cleanup didn't fail
881  $FullPageOutput = $this->CheckOutputModification(
882  $FullPageOutput, $NewFullPageOutput,
883  "HREF cleanup");
884  }
885  else
886  {
887  # try to fix any relative paths throughout code
888  $RelativePathPatterns = array(
889  "%src=\"/?([^?*:;{}\\\\\" ]+)\.(js|css|gif|png|jpg)\"%i",
890  "%src='/?([^?*:;{}\\\\' ]+)\.(js|css|gif|png|jpg)'%i",
891  # don't rewrite HREF attributes that are just
892  # fragment IDs because they are relative to the
893  # current page/URL, not the site root
894  "%href=\"/?([^#][^:\" ]*)\"%i",
895  "%href='/?([^#][^:' ]*)'%i",
896  "%action=\"/?([^#][^:\" ]*)\"%i",
897  "%action='/?([^#][^:' ]*)'%i",
898  "%@import\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
899  "%@import\s+url\('/?([^:\" ]+)'\s*\)%i",
900  "%src:\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
901  "%src:\s+url\('/?([^:\" ]+)'\s*\)%i",
902  "%@import\s+\"/?([^:\" ]+)\"\s*%i",
903  "%@import\s+'/?([^:\" ]+)'\s*%i",
904  );
905  $RelativePathReplacements = array(
906  "src=\"".$BaseUrl."$1.$2\"",
907  "src=\"".$BaseUrl."$1.$2\"",
908  "href=\"".$BaseUrl."$1\"",
909  "href=\"".$BaseUrl."$1\"",
910  "action=\"".$BaseUrl."$1\"",
911  "action=\"".$BaseUrl."$1\"",
912  "@import url(\"".$BaseUrl."$1\")",
913  "@import url('".$BaseUrl."$1')",
914  "src: url(\"".$BaseUrl."$1\")",
915  "src: url('".$BaseUrl."$1')",
916  "@import \"".$BaseUrl."$1\"",
917  "@import '".$BaseUrl."$1'",
918  );
919  $NewFullPageOutput = preg_replace($RelativePathPatterns,
920  $RelativePathReplacements, $FullPageOutput);
921 
922  # check to make sure relative path fixes didn't fail
923  $FullPageOutput = $this->CheckOutputModification(
924  $FullPageOutput, $NewFullPageOutput,
925  "relative path fixes");
926  }
927  }
928 
929  # handle any necessary alternate domain rewriting
930  $FullPageOutput = $this->RewriteAlternateDomainUrls($FullPageOutput);
931 
932  # update page cache for this page
933  $this->UpdatePageCache($PageName, $FullPageOutput);
934 
935  # write out full page
936  print $FullPageOutput;
937  }
938 
939  # run any post-processing routines
940  foreach ($this->PostProcessingFuncs as $Func)
941  {
942  call_user_func_array($Func["FunctionName"], $Func["Arguments"]);
943  }
944 
945  # write out any output buffered from page code execution
946  if (strlen($PageOutput))
947  {
948  if (!$this->SuppressHTML)
949  {
950  ?><table width="100%" cellpadding="5"
951  style="border: 2px solid #666666; background: #CCCCCC;
952  font-family: Courier New, Courier, monospace;
953  margin-top: 10px;"><tr><td><?PHP
954  }
955  if ($this->JumpToPage)
956  {
957  ?><div style="color: #666666;"><span style="font-size: 150%;">
958  <b>Page Jump Aborted</b></span>
959  (because of error or other unexpected output)<br />
960  <b>Jump Target:</b>
961  <i><?PHP print($this->JumpToPage); ?></i></div><?PHP
962  }
963  print $PageOutput;
964  if (!$this->SuppressHTML)
965  {
966  ?></td></tr></table><?PHP
967  }
968  }
969 
970  # write out any output buffered from the page code execution complete signal
971  if (!$this->JumpToPage && !$this->SuppressHTML && strlen($PageCompleteOutput))
972  {
973  print $PageCompleteOutput;
974  }
975 
976  # log slow page loads
977  if ($this->LogSlowPageLoads()
978  && !$this->DoNotLogSlowPageLoad
979  && ($this->GetElapsedExecutionTime()
980  >= ($this->SlowPageLoadThreshold())))
981  {
982  $RemoteHost = gethostbyaddr($_SERVER["REMOTE_ADDR"]);
983  if ($RemoteHost === FALSE)
984  {
985  $RemoteHost = $_SERVER["REMOTE_ADDR"];
986  }
987  elseif ($RemoteHost != $_SERVER["REMOTE_ADDR"])
988  {
989  $RemoteHost .= " (".$_SERVER["REMOTE_ADDR"].")";
990  }
991  $SlowPageLoadMsg = "Slow page load ("
992  .intval($this->GetElapsedExecutionTime())."s) for "
993  .$this->FullUrl()." from ".$RemoteHost;
994  $this->LogMessage(self::LOGLVL_INFO, $SlowPageLoadMsg);
995  }
996 
997  # execute callbacks that should not have their output buffered
998  foreach ($this->UnbufferedCallbacks as $Callback)
999  {
1000  call_user_func_array($Callback[0], $Callback[1]);
1001  }
1002 
1003  # log high memory usage
1004  if (function_exists("memory_get_peak_usage"))
1005  {
1006  $MemoryThreshold = ($this->HighMemoryUsageThreshold()
1007  * $this->GetPhpMemoryLimit()) / 100;
1008  if ($this->LogHighMemoryUsage()
1009  && (memory_get_peak_usage(TRUE) >= $MemoryThreshold))
1010  {
1011  $HighMemUsageMsg = "High peak memory usage ("
1012  .number_format(memory_get_peak_usage(TRUE)).") for "
1013  .$this->FullUrl()." from "
1014  .$_SERVER["REMOTE_ADDR"];
1015  $this->LogMessage(self::LOGLVL_INFO, $HighMemUsageMsg);
1016  }
1017  }
1018 
1019  # terminate and stay resident (TSR!) if indicated and HTML has been output
1020  # (only TSR if HTML has been output because otherwise browsers will misbehave)
1021  if ($ShouldTSR) { $this->LaunchTSR(); }
1022  }
1023 
1029  public function GetPageName()
1030  {
1031  return $this->PageName;
1032  }
1033 
1039  public function GetPageLocation()
1040  {
1041  # retrieve current URL
1042  $Url = self::GetScriptUrl();
1043 
1044  # remove the base path if present
1045  $BasePath = $this->Settings["BasePath"];
1046  if (stripos($Url, $BasePath) === 0)
1047  {
1048  $Url = substr($Url, strlen($BasePath));
1049  }
1050 
1051  # if we're being accessed via an alternate domain,
1052  # add the appropriate prefix in
1053  if ($this->HtaccessSupport() &&
1054  self::$RootUrlOverride !== NULL)
1055  {
1056  $VHost = $_SERVER["SERVER_NAME"];
1057  if (isset($this->AlternateDomainPrefixes[$VHost]))
1058  {
1059  $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
1060  $Url = $ThisPrefix."/".$Url;
1061  }
1062  }
1063 
1064  return $Url;
1065  }
1066 
1072  public function GetPageUrl()
1073  {
1074  return self::BaseUrl() . $this->GetPageLocation();
1075  }
1076 
1088  public function SetJumpToPage($Page, $Delay = 0, $IsLiteral = FALSE)
1089  {
1090  if (!is_null($Page)
1091  && (!$IsLiteral)
1092  && (strpos($Page, "?") === FALSE)
1093  && ((strpos($Page, "=") !== FALSE)
1094  || ((stripos($Page, ".php") === FALSE)
1095  && (stripos($Page, ".htm") === FALSE)
1096  && (strpos($Page, "/") === FALSE)))
1097  && (stripos($Page, "http://") !== 0)
1098  && (stripos($Page, "https://") !== 0))
1099  {
1100  $this->JumpToPage = self::BaseUrl() . "index.php?P=".$Page;
1101  }
1102  else
1103  {
1104  $this->JumpToPage = $Page;
1105  }
1106  $this->JumpToPageDelay = $Delay;
1107  }
1108 
1113  public function JumpToPageIsSet()
1114  {
1115  return ($this->JumpToPage === NULL) ? FALSE : TRUE;
1116  }
1117 
1127  public function HtmlCharset($NewSetting = NULL)
1128  {
1129  if ($NewSetting !== NULL) { $this->HtmlCharset = $NewSetting; }
1130  return $this->HtmlCharset;
1131  }
1132 
1142  public function DoNotMinimizeFile($File)
1143  {
1144  if (!is_array($File)) { $File = array($File); }
1145  $this->DoNotMinimizeList = array_merge($this->DoNotMinimizeList, $File);
1146  }
1147 
1158  public function UseBaseTag($NewValue = NULL)
1159  {
1160  if ($NewValue !== NULL) { $this->UseBaseTag = $NewValue ? TRUE : FALSE; }
1161  return $this->UseBaseTag;
1162  }
1163 
1171  public function SuppressHTMLOutput($NewSetting = TRUE)
1172  {
1173  $this->SuppressHTML = $NewSetting;
1174  }
1175 
1183  public function SuppressStandardPageStartAndEnd($NewSetting = TRUE)
1184  {
1185  $this->SuppressStdPageStartAndEnd = $NewSetting;
1186  }
1187 
1193  public static function DefaultUserInterface($UIName = NULL)
1194  {
1195  if ($UIName !== NULL)
1196  {
1197  self::$DefaultUI = $UIName;
1198  }
1199  return self::$DefaultUI;
1200  }
1201 
1208  public static function ActiveUserInterface($UIName = NULL)
1209  {
1210  if ($UIName !== NULL)
1211  {
1212  self::$ActiveUI = preg_replace("/^SPTUI--/", "", $UIName);
1213  }
1214  return self::$ActiveUI;
1215  }
1216 
1227  public function GetUserInterfaces($FilterExp = NULL)
1228  {
1229  static $Interfaces;
1230 
1231  if (!isset($Interfaces[$FilterExp]))
1232  {
1233  # retrieve paths to user interface directories
1234  $Paths = $this->GetUserInterfacePaths($FilterExp);
1235 
1236  # start out with an empty list
1237  $Interfaces[$FilterExp] = array();
1238 
1239  # for each possible UI directory
1240  foreach ($Paths as $CanonicalName => $Path)
1241  {
1242  # if name file available
1243  $LabelFile = $Path."/NAME";
1244  if (is_readable($LabelFile))
1245  {
1246  # read the UI name
1247  $Label = file_get_contents($LabelFile);
1248 
1249  # if the UI name looks reasonable
1250  if (strlen(trim($Label)))
1251  {
1252  # use read name
1253  $Interfaces[$FilterExp][$CanonicalName] = $Label;
1254  }
1255  }
1256 
1257  # if we do not have a name yet
1258  if (!isset($Interfaces[$FilterExp][$CanonicalName]))
1259  {
1260  # use base directory for name
1261  $Interfaces[$FilterExp][$CanonicalName] = basename($Path);
1262  }
1263  }
1264  }
1265 
1266  # return list to caller
1267  return $Interfaces[$FilterExp];
1268  }
1269 
1278  public function GetUserInterfacePaths($FilterExp = NULL)
1279  {
1280  static $InterfacePaths;
1281 
1282  if (!isset($InterfacePaths[$FilterExp]))
1283  {
1284  # extract possible UI directories from interface directory list
1285  $InterfaceDirs = array();
1286  foreach ($this->ExpandDirectoryList($this->InterfaceDirList) as $Dir)
1287  {
1288  $Matches = array();
1289  if (preg_match("#([a-zA-Z0-9/]*interface)/[a-zA-Z0-9%/]*#",
1290  $Dir, $Matches))
1291  {
1292  $Dir = $Matches[1];
1293  if (!in_array($Dir, $InterfaceDirs))
1294  {
1295  $InterfaceDirs[] = $Dir;
1296  }
1297  }
1298  }
1299 
1300  # reverse order of interface directories so that the directory
1301  # returned is the base directory for the interface
1302  $InterfaceDirs = array_reverse($InterfaceDirs);
1303 
1304  # start out with an empty list
1305  $InterfacePaths[$FilterExp] = array();
1306  $InterfacesFound = array();
1307 
1308  # for each possible UI directory
1309  foreach ($InterfaceDirs as $InterfaceDir)
1310  {
1311  # check if the dir exists
1312  if (!is_dir($InterfaceDir))
1313  {
1314  continue;
1315  }
1316 
1317  $Dir = dir($InterfaceDir);
1318 
1319  # for each file in current directory
1320  while (($DirEntry = $Dir->read()) !== FALSE)
1321  {
1322  $InterfacePath = $InterfaceDir."/".$DirEntry;
1323 
1324  # skip anything we have already found
1325  # or that doesn't have a name in the required format
1326  # or that isn't a directory
1327  # or that doesn't match the filter regex (if supplied)
1328  if (in_array($DirEntry, $InterfacesFound)
1329  || !preg_match('/^[a-zA-Z0-9]+$/', $DirEntry)
1330  || !is_dir($InterfacePath)
1331  || (($FilterExp !== NULL)
1332  && !preg_match($FilterExp, $InterfacePath)))
1333  {
1334  continue;
1335  }
1336 
1337  # add interface to list
1338  $InterfacePaths[$FilterExp][$DirEntry] = $InterfacePath;
1339  $InterfacesFound[] = $DirEntry;
1340  }
1341 
1342  $Dir->close();
1343  }
1344  }
1345 
1346  # return list to caller
1347  return $InterfacePaths[$FilterExp];
1348  }
1349 
1374  public function AddPostProcessingCall($FunctionName,
1375  &$Arg1 = self::NOVALUE, &$Arg2 = self::NOVALUE, &$Arg3 = self::NOVALUE,
1376  &$Arg4 = self::NOVALUE, &$Arg5 = self::NOVALUE, &$Arg6 = self::NOVALUE,
1377  &$Arg7 = self::NOVALUE, &$Arg8 = self::NOVALUE, &$Arg9 = self::NOVALUE)
1378  {
1379  $FuncIndex = count($this->PostProcessingFuncs);
1380  $this->PostProcessingFuncs[$FuncIndex]["FunctionName"] = $FunctionName;
1381  $this->PostProcessingFuncs[$FuncIndex]["Arguments"] = array();
1382  $Index = 1;
1383  while (isset(${"Arg".$Index}) && (${"Arg".$Index} !== self::NOVALUE))
1384  {
1385  $this->PostProcessingFuncs[$FuncIndex]["Arguments"][$Index]
1386  =& ${"Arg".$Index};
1387  $Index++;
1388  }
1389  }
1390 
1396  public function AddEnvInclude($FileName)
1397  {
1398  $this->EnvIncludes[] = $FileName;
1399  }
1400 
1417  public function SetContextFilter($Context, $NewSetting)
1418  {
1419  if (($NewSetting === TRUE)
1420  || ($NewSetting === FALSE)
1421  || is_array($NewSetting))
1422  {
1423  $this->ContextFilters[$Context] = $NewSetting;
1424  }
1425  elseif (is_string($NewSetting))
1426  {
1427  $this->ContextFilters[$Context] = array($NewSetting);
1428  }
1429  else
1430  {
1431  throw new InvalidArgumentException(
1432  "Invalid setting (".$NewSetting.").");
1433  }
1434  }
1436  const CONTEXT_ENV = 1;
1438  const CONTEXT_PAGE = 2;
1440  const CONTEXT_COMMON = 3;
1442  const CONTEXT_INTERFACE = 4;
1444  const CONTEXT_START = 5;
1446  const CONTEXT_END = 6;
1447 
1454  public function GUIFile($FileName)
1455  {
1456  # determine which location to search based on file suffix
1457  $FileType = $this->GetFileType($FileName);
1458  $DirList = ($FileType == self::FT_IMAGE)
1459  ? $this->ImageDirList : $this->IncludeDirList;
1460 
1461  # if directed to use minimized JavaScript file
1462  if (($FileType == self::FT_JAVASCRIPT) && $this->UseMinimizedJavascript())
1463  {
1464  # look for minimized version of file
1465  $MinimizedFileName = substr_replace($FileName, ".min", -3, 0);
1466  $FoundFileName = $this->FindFile($DirList, $MinimizedFileName);
1467 
1468  # if minimized file was not found
1469  if (is_null($FoundFileName))
1470  {
1471  # look for unminimized file
1472  $FoundFileName = $this->FindFile($DirList, $FileName);
1473 
1474  # if unminimized file found
1475  if (!is_null($FoundFileName))
1476  {
1477  # if minimization enabled and supported
1478  if ($this->JavascriptMinimizationEnabled()
1479  && self::JsMinRewriteSupport())
1480  {
1481  # attempt to create minimized file
1482  $MinFileName = $this->MinimizeJavascriptFile(
1483  $FoundFileName);
1484 
1485  # if minimization succeeded
1486  if ($MinFileName !== NULL)
1487  {
1488  # use minimized version
1489  $FoundFileName = $MinFileName;
1490 
1491  # save file modification time if needed for fingerprinting
1492  if ($this->UrlFingerprintingEnabled())
1493  {
1494  $FileMTime = filemtime($FoundFileName);
1495  }
1496 
1497  # strip off the cache location, allowing .htaccess
1498  # to handle that for us
1499  $FoundFileName = str_replace(
1500  self::$JSMinCacheDir."/", "", $FoundFileName);
1501  }
1502  }
1503  }
1504  }
1505  }
1506  # else if directed to use SCSS files
1507  elseif (($FileType == self::FT_CSS) && $this->ScssSupportEnabled())
1508  {
1509  # look for SCSS version of file
1510  $SourceFileName = preg_replace("/.css$/", ".scss", $FileName);
1511  $FoundSourceFileName = $this->FindFile($DirList, $SourceFileName);
1512 
1513  # if SCSS file not found
1514  if ($FoundSourceFileName === NULL)
1515  {
1516  # look for CSS file
1517  $FoundFileName = $this->FindFile($DirList, $FileName);
1518  }
1519  else
1520  {
1521  # compile SCSS file (if updated) and return resulting CSS file
1522  $FoundFileName = $this->CompileScssFile($FoundSourceFileName);
1523 
1524  # save file modification time if needed for fingerprinting
1525  if ($this->UrlFingerprintingEnabled())
1526  {
1527  $FileMTime = filemtime($FoundFileName);
1528  }
1529 
1530  # strip off the cache location, allowing .htaccess to handle that for us
1531  if (self::ScssRewriteSupport())
1532  {
1533  $FoundFileName = str_replace(
1534  self::$ScssCacheDir."/", "", $FoundFileName);
1535  }
1536  }
1537  }
1538  # otherwise just search for the file
1539  else
1540  {
1541  $FoundFileName = $this->FindFile($DirList, $FileName);
1542  }
1543 
1544  # add non-image files to list of found files (used for required files loading)
1545  if ($FileType != self::FT_IMAGE)
1546  { $this->FoundUIFiles[] = basename($FoundFileName); }
1547 
1548  # if UI file fingerprinting is enabled and supported
1549  if ($this->UrlFingerprintingEnabled()
1550  && self::UrlFingerprintingRewriteSupport()
1551  && (isset($FileMTime) || file_exists($FoundFileName)))
1552  {
1553  # if file does not appear to be a server-side inclusion
1554  if (!preg_match('/\.(html|php)$/i', $FoundFileName))
1555  {
1556  # for each URL fingerprinting blacklist entry
1557  $OnBlacklist = FALSE;
1558  foreach ($this->UrlFingerprintBlacklist as $BlacklistEntry)
1559  {
1560  # if entry looks like a regular expression pattern
1561  if ($BlacklistEntry[0] == substr($BlacklistEntry, -1))
1562  {
1563  # check file name against regular expression
1564  if (preg_match($BlacklistEntry, $FoundFileName))
1565  {
1566  $OnBlacklist = TRUE;
1567  break;
1568  }
1569  }
1570  else
1571  {
1572  # check file name directly against entry
1573  if (basename($FoundFileName) == $BlacklistEntry)
1574  {
1575  $OnBlacklist = TRUE;
1576  break;
1577  }
1578  }
1579  }
1580 
1581  # if file was not on blacklist
1582  if (!$OnBlacklist)
1583  {
1584  # get file modification time if not already retrieved
1585  if (!isset($FileMTime))
1586  {
1587  $FileMTime = filemtime($FoundFileName);
1588  }
1589 
1590  # add timestamp fingerprint to file name
1591  $Fingerprint = sprintf("%06X",
1592  ($FileMTime % 0xFFFFFF));
1593  $FoundFileName = preg_replace("/^(.+)\.([a-z]+)$/",
1594  "$1.".$Fingerprint.".$2",
1595  $FoundFileName);
1596  }
1597  }
1598  }
1599 
1600  # return file name to caller
1601  return $FoundFileName;
1602  }
1603 
1612  public function PUIFile($FileName)
1613  {
1614  $FullFileName = $this->GUIFile($FileName);
1615  if ($FullFileName) { print($FullFileName); }
1616  }
1617 
1632  public function IncludeUIFile($FileNames, $AdditionalAttributes = NULL)
1633  {
1634  # convert file name to array if necessary
1635  if (!is_array($FileNames)) { $FileNames = array($FileNames); }
1636 
1637  # pad additional attributes if supplied
1638  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
1639 
1640  # for each file
1641  foreach ($FileNames as $BaseFileName)
1642  {
1643  # retrieve full file name
1644  $FileName = $this->GUIFile($BaseFileName);
1645 
1646  # if file was found
1647  if ($FileName)
1648  {
1649  # print appropriate tag
1650  print $this->GetUIFileLoadingTag($FileName, $AdditionalAttributes);
1651  }
1652 
1653  # if we are not already loading an override file
1654  if (!preg_match("/-Override.(css|scss|js)$/", $BaseFileName))
1655  {
1656  # attempt to load override file if available
1657  $FileType = $this->GetFileType($BaseFileName);
1658  switch ($FileType)
1659  {
1660  case self::FT_CSS:
1661  $OverrideFileName = preg_replace(
1662  "/\.(css|scss)$/", "-Override.$1",
1663  $BaseFileName);
1664  $this->IncludeUIFile($OverrideFileName,
1665  $AdditionalAttributes);
1666  break;
1667 
1668  case self::FT_JAVASCRIPT:
1669  $OverrideFileName = preg_replace(
1670  "/\.js$/", "-Override.js",
1671  $BaseFileName);
1672  $this->IncludeUIFile($OverrideFileName,
1673  $AdditionalAttributes);
1674  break;
1675  }
1676  }
1677  }
1678  }
1679 
1686  public function DoNotUrlFingerprint($Pattern)
1687  {
1688  $this->UrlFingerprintBlacklist[] = $Pattern;
1689  }
1690 
1701  public function RequireUIFile($FileName, $Order = self::ORDER_MIDDLE)
1702  {
1703  $this->AdditionalRequiredUIFiles[$FileName] = $Order;
1704  }
1705 
1711  public static function GetFileType($FileName)
1712  {
1713  static $FileTypeCache;
1714  if (isset($FileTypeCache[$FileName]))
1715  {
1716  return $FileTypeCache[$FileName];
1717  }
1718 
1719  $FileSuffix = strtolower(substr($FileName, -3));
1720  if ($FileSuffix == "css")
1721  {
1722  $FileTypeCache[$FileName] = self::FT_CSS;
1723  }
1724  elseif ($FileSuffix == ".js")
1725  {
1726  $FileTypeCache[$FileName] = self::FT_JAVASCRIPT;
1727  }
1728  elseif (($FileSuffix == "gif")
1729  || ($FileSuffix == "jpg")
1730  || ($FileSuffix == "png"))
1731  {
1732  $FileTypeCache[$FileName] = self::FT_IMAGE;
1733  }
1734  else
1735  {
1736  $FileTypeCache[$FileName] = self::FT_OTHER;
1737  }
1738 
1739  return $FileTypeCache[$FileName];
1740  }
1742  const FT_OTHER = 0;
1744  const FT_CSS = 1;
1746  const FT_IMAGE = 2;
1748  const FT_JAVASCRIPT = 3;
1749 
1758  public function LoadFunction($Callback)
1759  {
1760  # if specified function is not currently available
1761  if (!is_callable($Callback))
1762  {
1763  # if function info looks legal
1764  if (is_string($Callback) && strlen($Callback))
1765  {
1766  # start with function directory list
1767  $Locations = $this->FunctionDirList;
1768 
1769  # add object directories to list
1770  $Locations = array_merge(
1771  $Locations, array_keys(self::$ObjectDirectories));
1772 
1773  # look for function file
1774  $FunctionFileName = $this->FindFile($Locations, "F-".$Callback,
1775  array("php", "html"));
1776 
1777  # if function file was found
1778  if ($FunctionFileName)
1779  {
1780  # load function file
1781  include_once($FunctionFileName);
1782  }
1783  else
1784  {
1785  # log error indicating function load failed
1786  $this->LogError(self::LOGLVL_ERROR, "Unable to load function"
1787  ." for callback \"".$Callback."\".");
1788  }
1789  }
1790  else
1791  {
1792  # log error indicating specified function info was bad
1793  $this->LogError(self::LOGLVL_ERROR, "Unloadable callback value"
1794  ." (".$Callback.")"
1795  ." passed to AF::LoadFunction() by "
1796  .StdLib::GetMyCaller().".");
1797  }
1798  }
1799 
1800  # report to caller whether function load succeeded
1801  return is_callable($Callback);
1802  }
1803 
1808  public function GetElapsedExecutionTime()
1809  {
1810  return microtime(TRUE) - $this->ExecutionStartTime;
1811  }
1812 
1817  public function GetSecondsBeforeTimeout()
1818  {
1819  return $this->MaxExecutionTime() - $this->GetElapsedExecutionTime();
1820  }
1821 
1826  public function AddMetaTag($Attribs)
1827  {
1828  # add new meta tag to list
1829  $this->MetaTags[] = $Attribs;
1830  }
1831 
1832  /*@)*/ /* Application Framework */
1833 
1834 
1835  # ---- Page Caching ------------------------------------------------------
1836  /*@(*/
1838 
1845  public function PageCacheEnabled($NewValue = DB_NOVALUE)
1846  {
1847  return $this->UpdateSetting("PageCacheEnabled", $NewValue);
1848  }
1849 
1856  public function PageCacheExpirationPeriod($NewValue = DB_NOVALUE)
1857  {
1858  return $this->UpdateSetting("PageCacheExpirationPeriod", $NewValue);
1859  }
1860 
1865  public function DoNotCacheCurrentPage()
1866  {
1867  $this->CacheCurrentPage = FALSE;
1868  }
1869 
1876  public function AddPageCacheTag($Tag, $Pages = NULL)
1877  {
1878  # normalize tag
1879  $Tag = strtolower($Tag);
1880 
1881  # if pages were supplied
1882  if ($Pages !== NULL)
1883  {
1884  # add pages to list for this tag
1885  if (isset($this->PageCacheTags[$Tag]))
1886  {
1887  $this->PageCacheTags[$Tag] = array_merge(
1888  $this->PageCacheTags[$Tag], $Pages);
1889  }
1890  else
1891  {
1892  $this->PageCacheTags[$Tag] = $Pages;
1893  }
1894  }
1895  else
1896  {
1897  # add current page to list for this tag
1898  $this->PageCacheTags[$Tag][] = "CURRENT";
1899  }
1900  }
1901 
1907  public function ClearPageCacheForTag($Tag)
1908  {
1909  # get tag ID
1910  $TagId = $this->GetPageCacheTagId($Tag);
1911 
1912  # delete pages and tag/page connections for specified tag
1913  $this->DB->Query("DELETE CP, CPTI"
1914  ." FROM AF_CachedPages CP, AF_CachedPageTagInts CPTI"
1915  ." WHERE CPTI.TagId = ".intval($TagId)
1916  ." AND CP.CacheId = CPTI.CacheId");
1917  }
1918 
1922  public function ClearPageCache()
1923  {
1924  # clear all page cache tables
1925  $this->DB->Query("TRUNCATE TABLE AF_CachedPages");
1926  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTags");
1927  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTagInts");
1928  }
1929 
1936  public function GetPageCacheInfo()
1937  {
1938  $Length = $this->DB->Query("SELECT COUNT(*) AS CacheLen"
1939  ." FROM AF_CachedPages", "CacheLen");
1940  $Oldest = $this->DB->Query("SELECT CachedAt FROM AF_CachedPages"
1941  ." ORDER BY CachedAt ASC LIMIT 1", "CachedAt");
1942  return array(
1943  "NumberOfEntries" => $Length,
1944  "OldestTimestamp" => strtotime($Oldest),
1945  );
1946  }
1947 
1948  /*@)*/ /* Page Caching */
1949 
1950 
1951  # ---- Logging -----------------------------------------------------------
1952  /*@(*/
1954 
1968  public function LogSlowPageLoads(
1969  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1970  {
1971  return $this->UpdateSetting(
1972  "LogSlowPageLoads", $NewValue, $Persistent);
1973  }
1974 
1985  public function SlowPageLoadThreshold(
1986  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1987  {
1988  return $this->UpdateSetting(
1989  "SlowPageLoadThreshold", $NewValue, $Persistent);
1990  }
1991 
2005  public function LogHighMemoryUsage(
2006  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2007  {
2008  return $this->UpdateSetting(
2009  "LogHighMemoryUsage", $NewValue, $Persistent);
2010  }
2011 
2023  public function HighMemoryUsageThreshold(
2024  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2025  {
2026  return $this->UpdateSetting(
2027  "HighMemoryUsageThreshold", $NewValue, $Persistent);
2028  }
2029 
2043  public function LogError($Level, $Msg)
2044  {
2045  # if error level is at or below current logging level
2046  if ($this->Settings["LoggingLevel"] >= $Level)
2047  {
2048  # attempt to log error message
2049  $Result = $this->LogMessage($Level, $Msg);
2050 
2051  # if logging attempt failed and level indicated significant error
2052  if (($Result === FALSE) && ($Level <= self::LOGLVL_ERROR))
2053  {
2054  # throw exception about inability to log error
2055  static $AlreadyThrewException = FALSE;
2056  if (!$AlreadyThrewException)
2057  {
2058  $AlreadyThrewException = TRUE;
2059  throw new Exception("Unable to log error (".$Level.": ".$Msg
2060  .") to ".$this->LogFileName);
2061  }
2062  }
2063 
2064  # report to caller whether message was logged
2065  return $Result;
2066  }
2067  else
2068  {
2069  # report to caller that message was not logged
2070  return FALSE;
2071  }
2072  }
2073 
2085  public function LogMessage($Level, $Msg)
2086  {
2087  # if message level is at or below current logging level
2088  if ($this->Settings["LoggingLevel"] >= $Level)
2089  {
2090  # attempt to open log file
2091  $FHndl = @fopen($this->LogFileName, "a");
2092 
2093  # if log file could not be open
2094  if ($FHndl === FALSE)
2095  {
2096  # report to caller that message was not logged
2097  return FALSE;
2098  }
2099  else
2100  {
2101  # format log entry
2102  $ErrorAbbrevs = array(
2103  self::LOGLVL_FATAL => "FTL",
2104  self::LOGLVL_ERROR => "ERR",
2105  self::LOGLVL_WARNING => "WRN",
2106  self::LOGLVL_INFO => "INF",
2107  self::LOGLVL_DEBUG => "DBG",
2108  self::LOGLVL_TRACE => "TRC",
2109  );
2110  $Msg = str_replace(array("\n", "\t", "\r"), " ", $Msg);
2111  $Msg = substr(trim($Msg), 0, self::LOGFILE_MAX_LINE_LENGTH);
2112  $LogEntry = date("Y-m-d H:i:s")
2113  ." ".($this->RunningInBackground ? "B" : "F")
2114  ." ".$ErrorAbbrevs[$Level]
2115  ." ".$Msg;
2116 
2117  # write entry to log
2118  $Success = fwrite($FHndl, $LogEntry."\n");
2119 
2120  # close log file
2121  fclose($FHndl);
2122 
2123  # report to caller whether message was logged
2124  return ($Success === FALSE) ? FALSE : TRUE;
2125  }
2126  }
2127  else
2128  {
2129  # report to caller that message was not logged
2130  return FALSE;
2131  }
2132  }
2133 
2155  public function LoggingLevel($NewValue = DB_NOVALUE)
2156  {
2157  # constrain new level (if supplied) to within legal bounds
2158  if ($NewValue !== DB_NOVALUE)
2159  {
2160  $NewValue = max(min($NewValue, 6), 1);
2161  }
2162 
2163  # set new logging level (if supplied) and return current level to caller
2164  return $this->UpdateSetting("LoggingLevel", $NewValue);
2165  }
2166 
2173  public function LogFile($NewValue = NULL)
2174  {
2175  if ($NewValue !== NULL) { $this->LogFileName = $NewValue; }
2176  return $this->LogFileName;
2177  }
2178 
2188  public function GetLogEntries($Limit = 0)
2189  {
2190  # return no entries if there isn't a log file
2191  # or we can't read it or it's empty
2192  $LogFile = $this->LogFile();
2193  if (!is_readable($LogFile) || !filesize($LogFile))
2194  {
2195  return array();
2196  }
2197 
2198  # if max number of entries specified
2199  if ($Limit > 0)
2200  {
2201  # load lines from file
2202  $FHandle = fopen($LogFile, "r");
2203  $FileSize = filesize($LogFile);
2204  $SeekPosition = max(0,
2205  ($FileSize - (self::LOGFILE_MAX_LINE_LENGTH
2206  * ($Limit + 1))));
2207  fseek($FHandle, $SeekPosition);
2208  $Block = fread($FHandle, ($FileSize - $SeekPosition));
2209  fclose($FHandle);
2210  $Lines = explode(PHP_EOL, $Block);
2211  array_pop($Lines);
2212 
2213  # prune array back to requested number of entries
2214  $Lines = array_slice($Lines, (0 - $Limit));
2215  }
2216  else
2217  {
2218  # load all lines from log file
2219  $Lines = file($LogFile, FILE_IGNORE_NEW_LINES);
2220  if ($Lines === FALSE)
2221  {
2222  return array();
2223  }
2224  }
2225 
2226  # reverse line order
2227  $Lines = array_reverse($Lines);
2228 
2229  # for each log file line
2230  $Entries = array();
2231  foreach ($Lines as $Line)
2232  {
2233  # attempt to parse line into component parts
2234  $Pieces = explode(" ", $Line, 5);
2235  $Date = isset($Pieces[0]) ? $Pieces[0] : "";
2236  $Time = isset($Pieces[1]) ? $Pieces[1] : "";
2237  $Back = isset($Pieces[2]) ? $Pieces[2] : "";
2238  $Level = isset($Pieces[3]) ? $Pieces[3] : "";
2239  $Msg = isset($Pieces[4]) ? $Pieces[4] : "";
2240 
2241  # skip line if it looks invalid
2242  $ErrorAbbrevs = array(
2243  "FTL" => self::LOGLVL_FATAL,
2244  "ERR" => self::LOGLVL_ERROR,
2245  "WRN" => self::LOGLVL_WARNING,
2246  "INF" => self::LOGLVL_INFO,
2247  "DBG" => self::LOGLVL_DEBUG,
2248  "TRC" => self::LOGLVL_TRACE,
2249  );
2250  if ((($Back != "F") && ($Back != "B"))
2251  || !array_key_exists($Level, $ErrorAbbrevs)
2252  || !strlen($Msg))
2253  {
2254  continue;
2255  }
2256 
2257  # convert parts into appropriate values and add to entries
2258  $Entries[] = array(
2259  "Time" => strtotime($Date." ".$Time),
2260  "Background" => ($Back == "B") ? TRUE : FALSE,
2261  "Level" => $ErrorAbbrevs[$Level],
2262  "Message" => $Msg,
2263  );
2264  }
2265 
2266  # return entries to caller
2267  return $Entries;
2268  }
2269 
2274  const LOGLVL_TRACE = 6;
2279  const LOGLVL_DEBUG = 5;
2285  const LOGLVL_INFO = 4;
2290  const LOGLVL_WARNING = 3;
2296  const LOGLVL_ERROR = 2;
2301  const LOGLVL_FATAL = 1;
2302 
2306  const LOGFILE_MAX_LINE_LENGTH = 2048;
2307 
2308  /*@)*/ /* Logging */
2309 
2310 
2311  # ---- Event Handling ----------------------------------------------------
2312  /*@(*/
2314 
2318  const EVENTTYPE_DEFAULT = 1;
2324  const EVENTTYPE_CHAIN = 2;
2330  const EVENTTYPE_FIRST = 3;
2338  const EVENTTYPE_NAMED = 4;
2339 
2341  const ORDER_FIRST = 1;
2343  const ORDER_MIDDLE = 2;
2345  const ORDER_LAST = 3;
2346 
2355  public function RegisterEvent($EventsOrEventName, $EventType = NULL)
2356  {
2357  # convert parameters to array if not already in that form
2358  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2359  : array($EventsOrEventName => $EventType);
2360 
2361  # for each event
2362  foreach ($Events as $Name => $Type)
2363  {
2364  # store event information
2365  $this->RegisteredEvents[$Name]["Type"] = $Type;
2366  $this->RegisteredEvents[$Name]["Hooks"] = array();
2367  }
2368  }
2369 
2376  public function IsRegisteredEvent($EventName)
2377  {
2378  return array_key_exists($EventName, $this->RegisteredEvents)
2379  ? TRUE : FALSE;
2380  }
2381 
2388  public function IsHookedEvent($EventName)
2389  {
2390  # the event isn't hooked to if it isn't even registered
2391  if (!$this->IsRegisteredEvent($EventName))
2392  {
2393  return FALSE;
2394  }
2395 
2396  # return TRUE if there is at least one callback hooked to the event
2397  return count($this->RegisteredEvents[$EventName]["Hooks"]) > 0;
2398  }
2399 
2413  public function HookEvent(
2414  $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2415  {
2416  # convert parameters to array if not already in that form
2417  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2418  : array($EventsOrEventName => $Callback);
2419 
2420  # for each event
2421  $Success = TRUE;
2422  foreach ($Events as $EventName => $EventCallback)
2423  {
2424  # if callback is valid
2425  if (is_callable($EventCallback))
2426  {
2427  # if this is a periodic event we process internally
2428  if (isset($this->PeriodicEvents[$EventName]))
2429  {
2430  # process event now
2431  $this->ProcessPeriodicEvent($EventName, $EventCallback);
2432  }
2433  # if specified event has been registered
2434  elseif (isset($this->RegisteredEvents[$EventName]))
2435  {
2436  # add callback for event
2437  $this->RegisteredEvents[$EventName]["Hooks"][]
2438  = array("Callback" => $EventCallback, "Order" => $Order);
2439 
2440  # sort callbacks by order
2441  if (count($this->RegisteredEvents[$EventName]["Hooks"]) > 1)
2442  {
2443  usort($this->RegisteredEvents[$EventName]["Hooks"],
2444  function ($A, $B) {
2445  return StdLib::SortCompare(
2446  $A["Order"], $B["Order"]);
2447  });
2448  }
2449  }
2450  else
2451  {
2452  $Success = FALSE;
2453  }
2454  }
2455  else
2456  {
2457  $Success = FALSE;
2458  }
2459  }
2460 
2461  # report to caller whether all callbacks were hooked
2462  return $Success;
2463  }
2464 
2478  public function UnhookEvent(
2479  $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2480  {
2481  # convert parameters to array if not already in that form
2482  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2483  : array($EventsOrEventName => $Callback);
2484 
2485  # for each event
2486  $UnhookCount = 0;
2487  foreach ($Events as $EventName => $EventCallback)
2488  {
2489  # if this event has been registered and hooked
2490  if (isset($this->RegisteredEvents[$EventName])
2491  && count($this->RegisteredEvents[$EventName]))
2492  {
2493  # if this callback has been hooked for this event
2494  $CallbackData = array("Callback" => $EventCallback, "Order" => $Order);
2495  if (in_array($CallbackData,
2496  $this->RegisteredEvents[$EventName]["Hooks"]))
2497  {
2498  # unhook callback
2499  $HookIndex = array_search($CallbackData,
2500  $this->RegisteredEvents[$EventName]["Hooks"]);
2501  unset($this->RegisteredEvents[$EventName]["Hooks"][$HookIndex]);
2502  $UnhookCount++;
2503  }
2504  }
2505  }
2506 
2507  # report number of callbacks unhooked to caller
2508  return $UnhookCount;
2509  }
2510 
2521  public function SignalEvent($EventName, $Parameters = NULL)
2522  {
2523  $ReturnValue = NULL;
2524 
2525  # if event has been registered
2526  if (isset($this->RegisteredEvents[$EventName]))
2527  {
2528  # set up default return value (if not NULL)
2529  switch ($this->RegisteredEvents[$EventName]["Type"])
2530  {
2531  case self::EVENTTYPE_CHAIN:
2532  $ReturnValue = $Parameters;
2533  break;
2534 
2535  case self::EVENTTYPE_NAMED:
2536  $ReturnValue = array();
2537  break;
2538  }
2539 
2540  # for each callback for this event
2541  foreach ($this->RegisteredEvents[$EventName]["Hooks"] as $Hook)
2542  {
2543  # invoke callback
2544  $Callback = $Hook["Callback"];
2545  $Result = ($Parameters !== NULL)
2546  ? call_user_func_array($Callback, $Parameters)
2547  : call_user_func($Callback);
2548 
2549  # process return value based on event type
2550  switch ($this->RegisteredEvents[$EventName]["Type"])
2551  {
2552  case self::EVENTTYPE_CHAIN:
2553  if ($Result !== NULL)
2554  {
2555  foreach ($Parameters as $Index => $Value)
2556  {
2557  if (array_key_exists($Index, $Result))
2558  {
2559  $Parameters[$Index] = $Result[$Index];
2560  }
2561  }
2562  $ReturnValue = $Parameters;
2563  }
2564  break;
2565 
2566  case self::EVENTTYPE_FIRST:
2567  if ($Result !== NULL)
2568  {
2569  $ReturnValue = $Result;
2570  break 2;
2571  }
2572  break;
2573 
2574  case self::EVENTTYPE_NAMED:
2575  $CallbackName = is_array($Callback)
2576  ? (is_object($Callback[0])
2577  ? get_class($Callback[0])
2578  : $Callback[0])."::".$Callback[1]
2579  : $Callback;
2580  $ReturnValue[$CallbackName] = $Result;
2581  break;
2582 
2583  default:
2584  break;
2585  }
2586  }
2587  }
2588  else
2589  {
2590  $this->LogError(self::LOGLVL_WARNING,
2591  "Unregistered event (".$EventName.") signaled by "
2592  .StdLib::GetMyCaller().".");
2593  }
2594 
2595  # return value if any to caller
2596  return $ReturnValue;
2597  }
2598 
2604  public function IsStaticOnlyEvent($EventName)
2605  {
2606  return isset($this->PeriodicEvents[$EventName]) ? TRUE : FALSE;
2607  }
2608 
2619  public function EventWillNextRunAt($EventName, $Callback)
2620  {
2621  # if event is not a periodic event report failure to caller
2622  if (!array_key_exists($EventName, $this->EventPeriods)) { return FALSE; }
2623 
2624  # retrieve last execution time for event if available
2625  $Signature = self::GetCallbackSignature($Callback);
2626  $LastRunTime = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
2627  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
2628 
2629  # if event was not found report failure to caller
2630  if ($LastRunTime === NULL) { return FALSE; }
2631 
2632  # calculate next run time based on event period
2633  $NextRunTime = strtotime($LastRunTime) + $this->EventPeriods[$EventName];
2634 
2635  # report next run time to caller
2636  return $NextRunTime;
2637  }
2638 
2654  public function GetKnownPeriodicEvents()
2655  {
2656  # retrieve last execution times
2657  $this->DB->Query("SELECT * FROM PeriodicEvents");
2658  $LastRunTimes = $this->DB->FetchColumn("LastRunAt", "Signature");
2659 
2660  # for each known event
2661  $Events = array();
2662  foreach ($this->KnownPeriodicEvents as $Signature => $Info)
2663  {
2664  # if last run time for event is available
2665  if (array_key_exists($Signature, $LastRunTimes))
2666  {
2667  # calculate next run time for event
2668  $LastRun = strtotime($LastRunTimes[$Signature]);
2669  $NextRun = $LastRun + $this->EventPeriods[$Info["Period"]];
2670  if ($Info["Period"] == "EVENT_PERIODIC") { $LastRun = FALSE; }
2671  }
2672  else
2673  {
2674  # set info to indicate run times are not known
2675  $LastRun = FALSE;
2676  $NextRun = FALSE;
2677  }
2678 
2679  # add event info to list
2680  $Events[$Signature] = $Info;
2681  $Events[$Signature]["LastRun"] = $LastRun;
2682  $Events[$Signature]["NextRun"] = $NextRun;
2683  $Events[$Signature]["Parameters"] = NULL;
2684  }
2685 
2686  # return list of known events to caller
2687  return $Events;
2688  }
2689 
2696  public static function RunPeriodicEvent($EventName, $Callback, $Parameters)
2697  {
2698  static $DB;
2699  if (!isset($DB)) { $DB = new Database(); }
2700 
2701  # run event
2702  $ReturnVal = call_user_func_array($Callback, $Parameters);
2703 
2704  # if event is already in database
2705  $Signature = self::GetCallbackSignature($Callback);
2706  if ($DB->Query("SELECT COUNT(*) AS EventCount FROM PeriodicEvents"
2707  ." WHERE Signature = '".addslashes($Signature)."'", "EventCount"))
2708  {
2709  # update last run time for event
2710  $DB->Query("UPDATE PeriodicEvents SET LastRunAt = "
2711  .(($EventName == "EVENT_PERIODIC")
2712  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
2713  : "NOW()")
2714  ." WHERE Signature = '".addslashes($Signature)."'");
2715  }
2716  else
2717  {
2718  # add last run time for event to database
2719  $DB->Query("INSERT INTO PeriodicEvents (Signature, LastRunAt) VALUES "
2720  ."('".addslashes($Signature)."', "
2721  .(($EventName == "EVENT_PERIODIC")
2722  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
2723  : "NOW()").")");
2724  }
2725  }
2726 
2727  /*@)*/ /* Event Handling */
2728 
2729 
2730  # ---- Task Management ---------------------------------------------------
2731  /*@(*/
2733 
2735  const PRIORITY_HIGH = 1;
2737  const PRIORITY_MEDIUM = 2;
2739  const PRIORITY_LOW = 3;
2741  const PRIORITY_BACKGROUND = 4;
2742 
2755  public function QueueTask($Callback, $Parameters = NULL,
2756  $Priority = self::PRIORITY_LOW, $Description = "")
2757  {
2758  # pack task info and write to database
2759  if ($Parameters === NULL) { $Parameters = array(); }
2760  $this->DB->Query("INSERT INTO TaskQueue"
2761  ." (Callback, Parameters, Priority, Description)"
2762  ." VALUES ('".addslashes(serialize($Callback))."', '"
2763  .addslashes(serialize($Parameters))."', ".intval($Priority).", '"
2764  .addslashes($Description)."')");
2765  }
2766 
2784  public function QueueUniqueTask($Callback, $Parameters = NULL,
2785  $Priority = self::PRIORITY_LOW, $Description = "")
2786  {
2787  if ($this->TaskIsInQueue($Callback, $Parameters))
2788  {
2789  $QueryResult = $this->DB->Query("SELECT TaskId,Priority FROM TaskQueue"
2790  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2791  .($Parameters ? " AND Parameters = '"
2792  .addslashes(serialize($Parameters))."'" : ""));
2793  if ($QueryResult !== FALSE)
2794  {
2795  $Record = $this->DB->FetchRow();
2796  if ($Record["Priority"] > $Priority)
2797  {
2798  $this->DB->Query("UPDATE TaskQueue"
2799  ." SET Priority = ".intval($Priority)
2800  ." WHERE TaskId = ".intval($Record["TaskId"]));
2801  }
2802  }
2803  return FALSE;
2804  }
2805  else
2806  {
2807  $this->QueueTask($Callback, $Parameters, $Priority, $Description);
2808  return TRUE;
2809  }
2810  }
2811 
2821  public function TaskIsInQueue($Callback, $Parameters = NULL)
2822  {
2823  $QueuedCount = $this->DB->Query(
2824  "SELECT COUNT(*) AS FoundCount FROM TaskQueue"
2825  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2826  .($Parameters ? " AND Parameters = '"
2827  .addslashes(serialize($Parameters))."'" : ""),
2828  "FoundCount");
2829  $RunningCount = $this->DB->Query(
2830  "SELECT COUNT(*) AS FoundCount FROM RunningTasks"
2831  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2832  .($Parameters ? " AND Parameters = '"
2833  .addslashes(serialize($Parameters))."'" : ""),
2834  "FoundCount");
2835  $FoundCount = $QueuedCount + $RunningCount;
2836  return ($FoundCount ? TRUE : FALSE);
2837  }
2838 
2844  public function GetTaskQueueSize($Priority = NULL)
2845  {
2846  return $this->GetQueuedTaskCount(NULL, NULL, $Priority);
2847  }
2848 
2856  public function GetQueuedTaskList($Count = 100, $Offset = 0)
2857  {
2858  return $this->GetTaskList("SELECT * FROM TaskQueue"
2859  ." ORDER BY Priority, TaskId ", $Count, $Offset);
2860  }
2861 
2875  public function GetQueuedTaskCount($Callback = NULL,
2876  $Parameters = NULL, $Priority = NULL, $Description = NULL)
2877  {
2878  $Query = "SELECT COUNT(*) AS TaskCount FROM TaskQueue";
2879  $Sep = " WHERE";
2880  if ($Callback !== NULL)
2881  {
2882  $Query .= $Sep." Callback = '".addslashes(serialize($Callback))."'";
2883  $Sep = " AND";
2884  }
2885  if ($Parameters !== NULL)
2886  {
2887  $Query .= $Sep." Parameters = '".addslashes(serialize($Parameters))."'";
2888  $Sep = " AND";
2889  }
2890  if ($Priority !== NULL)
2891  {
2892  $Query .= $Sep." Priority = ".intval($Priority);
2893  $Sep = " AND";
2894  }
2895  if ($Description !== NULL)
2896  {
2897  $Query .= $Sep." Description = '".addslashes($Description)."'";
2898  }
2899  return $this->DB->Query($Query, "TaskCount");
2900  }
2901 
2909  public function GetRunningTaskList($Count = 100, $Offset = 0)
2910  {
2911  return $this->GetTaskList("SELECT * FROM RunningTasks"
2912  ." WHERE StartedAt >= '".date("Y-m-d H:i:s",
2913  (time() - $this->MaxExecutionTime()))."'"
2914  ." ORDER BY StartedAt", $Count, $Offset);
2915  }
2916 
2924  public function GetOrphanedTaskList($Count = 100, $Offset = 0)
2925  {
2926  return $this->GetTaskList("SELECT * FROM RunningTasks"
2927  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
2928  (time() - $this->MaxExecutionTime()))."'"
2929  ." ORDER BY StartedAt", $Count, $Offset);
2930  }
2931 
2936  public function GetOrphanedTaskCount()
2937  {
2938  return $this->DB->Query("SELECT COUNT(*) AS Count FROM RunningTasks"
2939  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
2940  (time() - $this->MaxExecutionTime()))."'",
2941  "Count");
2942  }
2943 
2949  public function ReQueueOrphanedTask($TaskId, $NewPriority = NULL)
2950  {
2951  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
2952  $this->DB->Query("INSERT INTO TaskQueue"
2953  ." (Callback,Parameters,Priority,Description) "
2954  ."SELECT Callback, Parameters, Priority, Description"
2955  ." FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2956  if ($NewPriority !== NULL)
2957  {
2958  $NewTaskId = $this->DB->LastInsertId();
2959  $this->DB->Query("UPDATE TaskQueue SET Priority = "
2960  .intval($NewPriority)
2961  ." WHERE TaskId = ".intval($NewTaskId));
2962  }
2963  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2964  $this->DB->Query("UNLOCK TABLES");
2965  }
2966 
2973  public function RequeueCurrentTask($NewValue = TRUE)
2974  {
2975  $this->RequeueCurrentTask = $NewValue;
2976  }
2977 
2983  public function DeleteTask($TaskId)
2984  {
2985  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = ".intval($TaskId));
2986  $TasksRemoved = $this->DB->NumRowsAffected();
2987  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2988  $TasksRemoved += $this->DB->NumRowsAffected();
2989  return $TasksRemoved;
2990  }
2991 
2999  public function GetTask($TaskId)
3000  {
3001  # assume task will not be found
3002  $Task = NULL;
3003 
3004  # look for task in task queue
3005  $this->DB->Query("SELECT * FROM TaskQueue WHERE TaskId = ".intval($TaskId));
3006 
3007  # if task was not found in queue
3008  if (!$this->DB->NumRowsSelected())
3009  {
3010  # look for task in running task list
3011  $this->DB->Query("SELECT * FROM RunningTasks WHERE TaskId = "
3012  .intval($TaskId));
3013  }
3014 
3015  # if task was found
3016  if ($this->DB->NumRowsSelected())
3017  {
3018  # if task was periodic
3019  $Row = $this->DB->FetchRow();
3020  if ($Row["Callback"] ==
3021  serialize(array("ApplicationFramework", "RunPeriodicEvent")))
3022  {
3023  # unpack periodic task callback
3024  $WrappedCallback = unserialize($Row["Parameters"]);
3025  $Task["Callback"] = $WrappedCallback[1];
3026  $Task["Parameters"] = $WrappedCallback[2];
3027  }
3028  else
3029  {
3030  # unpack task callback and parameters
3031  $Task["Callback"] = unserialize($Row["Callback"]);
3032  $Task["Parameters"] = unserialize($Row["Parameters"]);
3033  }
3034  }
3035 
3036  # return task to caller
3037  return $Task;
3038  }
3039 
3047  public function TaskExecutionEnabled($NewValue = DB_NOVALUE)
3048  {
3049  return $this->UpdateSetting("TaskExecutionEnabled", $NewValue);
3050  }
3051 
3057  public function MaxTasks($NewValue = DB_NOVALUE)
3058  {
3059  return $this->UpdateSetting("MaxTasksRunning", $NewValue);
3060  }
3061 
3069  public static function GetTaskCallbackSynopsis($TaskInfo)
3070  {
3071  # if task callback is function use function name
3072  $Callback = $TaskInfo["Callback"];
3073  $Name = "";
3074  if (!is_array($Callback))
3075  {
3076  $Name = $Callback;
3077  }
3078  else
3079  {
3080  # if task callback is object
3081  if (is_object($Callback[0]))
3082  {
3083  # if task callback is encapsulated ask encapsulation for name
3084  if (method_exists($Callback[0], "GetCallbackAsText"))
3085  {
3086  $Name = $Callback[0]->GetCallbackAsText();
3087  }
3088  # else assemble name from object
3089  else
3090  {
3091  $Name = get_class($Callback[0]) . "::" . $Callback[1];
3092  }
3093  }
3094  # else assemble name from supplied info
3095  else
3096  {
3097  $Name= $Callback[0] . "::" . $Callback[1];
3098  }
3099  }
3100 
3101  # if parameter array was supplied
3102  $Parameters = $TaskInfo["Parameters"];
3103  $ParameterString = "";
3104  if (is_array($Parameters))
3105  {
3106  # assemble parameter string
3107  $Separator = "";
3108  foreach ($Parameters as $Parameter)
3109  {
3110  $ParameterString .= $Separator;
3111  if (is_int($Parameter) || is_float($Parameter))
3112  {
3113  $ParameterString .= $Parameter;
3114  }
3115  else if (is_string($Parameter))
3116  {
3117  $ParameterString .= "\"".htmlspecialchars($Parameter)."\"";
3118  }
3119  else if (is_array($Parameter))
3120  {
3121  $ParameterString .= "ARRAY";
3122  }
3123  else if (is_object($Parameter))
3124  {
3125  $ParameterString .= "OBJECT";
3126  }
3127  else if (is_null($Parameter))
3128  {
3129  $ParameterString .= "NULL";
3130  }
3131  else if (is_bool($Parameter))
3132  {
3133  $ParameterString .= $Parameter ? "TRUE" : "FALSE";
3134  }
3135  else if (is_resource($Parameter))
3136  {
3137  $ParameterString .= get_resource_type($Parameter);
3138  }
3139  else
3140  {
3141  $ParameterString .= "????";
3142  }
3143  $Separator = ", ";
3144  }
3145  }
3146 
3147  # assemble name and parameters and return result to caller
3148  return $Name."(".$ParameterString.")";
3149  }
3150 
3155  public function IsRunningInBackground()
3156  {
3157  return $this->RunningInBackground;
3158  }
3159 
3165  public function GetCurrentBackgroundPriority()
3166  {
3167  return isset($this->RunningTask)
3168  ? $this->RunningTask["Priority"] : NULL;
3169  }
3170 
3179  public function GetNextHigherBackgroundPriority($Priority = NULL)
3180  {
3181  if ($Priority === NULL)
3182  {
3183  $Priority = $this->GetCurrentBackgroundPriority();
3184  if ($Priority === NULL)
3185  {
3186  return NULL;
3187  }
3188  }
3189  return ($Priority > self::PRIORITY_HIGH)
3190  ? ($Priority - 1) : self::PRIORITY_HIGH;
3191  }
3192 
3201  public function GetNextLowerBackgroundPriority($Priority = NULL)
3202  {
3203  if ($Priority === NULL)
3204  {
3205  $Priority = $this->GetCurrentBackgroundPriority();
3206  if ($Priority === NULL)
3207  {
3208  return NULL;
3209  }
3210  }
3211  return ($Priority < self::PRIORITY_BACKGROUND)
3212  ? ($Priority + 1) : self::PRIORITY_BACKGROUND;
3213  }
3214 
3215  /*@)*/ /* Task Management */
3216 
3217 
3218  # ---- Clean URL Support -------------------------------------------------
3219  /*@(*/
3221 
3248  public function AddCleanUrl($Pattern, $Page, $GetVars = NULL, $Template = NULL)
3249  {
3250  # save clean URL mapping parameters
3251  $this->CleanUrlMappings[] = array(
3252  "Pattern" => $Pattern,
3253  "Page" => $Page,
3254  "GetVars" => $GetVars,
3255  );
3256 
3257  # if replacement template specified
3258  if ($Template !== NULL)
3259  {
3260  # if GET parameters specified
3261  if (count($GetVars))
3262  {
3263  # retrieve all possible permutations of GET parameters
3264  $GetPerms = StdLib::ArrayPermutations(array_keys($GetVars));
3265 
3266  # for each permutation of GET parameters
3267  foreach ($GetPerms as $VarPermutation)
3268  {
3269  # construct search pattern for permutation
3270  $SearchPattern = "/href=([\"'])index\\.php\\?P=".$Page;
3271  $GetVarSegment = "";
3272  foreach ($VarPermutation as $GetVar)
3273  {
3274  if (preg_match("%\\\$[0-9]+%", $GetVars[$GetVar]))
3275  {
3276  $GetVarSegment .= "&amp;".$GetVar."=((?:(?!\\1)[^&])+)";
3277  }
3278  else
3279  {
3280  $GetVarSegment .= "&amp;".$GetVar."=".$GetVars[$GetVar];
3281  }
3282  }
3283  $SearchPattern .= $GetVarSegment."\\1/i";
3284 
3285  # if template is actually a callback
3286  if (is_callable($Template))
3287  {
3288  # add pattern to HTML output mod callbacks list
3289  $this->OutputModificationCallbacks[] = array(
3290  "Pattern" => $Pattern,
3291  "Page" => $Page,
3292  "SearchPattern" => $SearchPattern,
3293  "Callback" => $Template,
3294  );
3295  }
3296  else
3297  {
3298  # construct replacement string for permutation
3299  $Replacement = $Template;
3300  $Index = 2;
3301  foreach ($VarPermutation as $GetVar)
3302  {
3303  $Replacement = str_replace(
3304  "\$".$GetVar, "\$".$Index, $Replacement);
3305  $Index++;
3306  }
3307  $Replacement = "href=\"".$Replacement."\"";
3308 
3309  # add pattern to HTML output modifications list
3310  $this->OutputModificationPatterns[] = $SearchPattern;
3311  $this->OutputModificationReplacements[] = $Replacement;
3312  }
3313  }
3314  }
3315  else
3316  {
3317  # construct search pattern
3318  $SearchPattern = "/href=\"index\\.php\\?P=".$Page."\"/i";
3319 
3320  # if template is actually a callback
3321  if (is_callable($Template))
3322  {
3323  # add pattern to HTML output mod callbacks list
3324  $this->OutputModificationCallbacks[] = array(
3325  "Pattern" => $Pattern,
3326  "Page" => $Page,
3327  "SearchPattern" => $SearchPattern,
3328  "Callback" => $Template,
3329  );
3330  }
3331  else
3332  {
3333  # add simple pattern to HTML output modifications list
3334  $this->OutputModificationPatterns[] = $SearchPattern;
3335  $this->OutputModificationReplacements[] = "href=\"".$Template."\"";
3336  }
3337  }
3338  }
3339  }
3340 
3346  public function CleanUrlIsMapped($Path)
3347  {
3348  foreach ($this->CleanUrlMappings as $Info)
3349  {
3350  if (preg_match($Info["Pattern"], $Path))
3351  {
3352  return TRUE;
3353  }
3354  }
3355  return FALSE;
3356  }
3357 
3367  public function GetCleanUrlForPath($Path)
3368  {
3369  # the search patterns and callbacks require a specific format
3370  $Format = "href=\"".str_replace("&", "&amp;", $Path)."\"";
3371  $Search = $Format;
3372 
3373  # perform any regular expression replacements on the search string
3374  $Search = preg_replace($this->OutputModificationPatterns,
3375  $this->OutputModificationReplacements, $Search);
3376 
3377  # only run the callbacks if a replacement hasn't already been performed
3378  if ($Search == $Format)
3379  {
3380  # perform any callback replacements on the search string
3381  foreach ($this->OutputModificationCallbacks as $Info)
3382  {
3383  # make the information available to the callback
3384  $this->OutputModificationCallbackInfo = $Info;
3385 
3386  # execute the callback
3387  $Search = preg_replace_callback($Info["SearchPattern"],
3388  array($this, "OutputModificationCallbackShell"),
3389  $Search);
3390  }
3391  }
3392 
3393  # return the path untouched if no replacements were performed
3394  if ($Search == $Format)
3395  {
3396  return $Path;
3397  }
3398 
3399  # remove the bits added to the search string to get it recognized by
3400  # the replacement expressions and callbacks
3401  $Result = substr($Search, 6, -1);
3402 
3403  return $Result;
3404  }
3405 
3412  public function GetUncleanUrlForPath($Path)
3413  {
3414  # for each clean URL mapping
3415  foreach ($this->CleanUrlMappings as $Info)
3416  {
3417  # if current path matches the clean URL pattern
3418  if (preg_match($Info["Pattern"], $Path, $Matches))
3419  {
3420  # the GET parameters for the URL, starting with the page name
3421  $GetVars = array("P" => $Info["Page"]);
3422 
3423  # if additional $_GET variables specified for clean URL
3424  if ($Info["GetVars"] !== NULL)
3425  {
3426  # for each $_GET variable specified for clean URL
3427  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
3428  {
3429  # start with template for variable value
3430  $Value = $VarTemplate;
3431 
3432  # for each subpattern matched in current URL
3433  foreach ($Matches as $Index => $Match)
3434  {
3435  # if not first (whole) match
3436  if ($Index > 0)
3437  {
3438  # make any substitutions in template
3439  $Value = str_replace("$".$Index, $Match, $Value);
3440  }
3441  }
3442 
3443  # add the GET variable
3444  $GetVars[$VarName] = $Value;
3445  }
3446  }
3447 
3448  # return the unclean URL
3449  return "index.php?" . http_build_query($GetVars);
3450  }
3451  }
3452 
3453  # return the path unchanged
3454  return $Path;
3455  }
3456 
3462  public function GetCleanUrl()
3463  {
3464  return $this->GetCleanUrlForPath($this->GetUncleanUrl());
3465  }
3466 
3471  public function GetUncleanUrl()
3472  {
3473  $GetVars = array("P" => $this->GetPageName()) + $_GET;
3474  return "index.php?" . http_build_query($GetVars);
3475  }
3476 
3488  public function AddPrefixForAlternateDomain($Domain, $Prefix)
3489  {
3490  $this->AlternateDomainPrefixes[$Domain] = $Prefix;
3491  }
3492 
3493  /*@)*/ /* Clean URL Support */
3494 
3495  # ---- Server Environment ------------------------------------------------
3496  /*@(*/
3498 
3504  public static function SessionLifetime($NewValue = NULL)
3505  {
3506  if ($NewValue !== NULL)
3507  {
3508  self::$SessionLifetime = $NewValue;
3509  }
3510  return self::$SessionLifetime;
3511  }
3512 
3518  public static function HtaccessSupport()
3519  {
3520  return isset($_SERVER["HTACCESS_SUPPORT"])
3521  || isset($_SERVER["REDIRECT_HTACCESS_SUPPORT"]);
3522  }
3523 
3530  public static function UrlFingerprintingRewriteSupport()
3531  {
3532  return isset($_SERVER["URL_FINGERPRINTING_SUPPORT"])
3533  || isset($_SERVER["REDIRECT_URL_FINGERPRINTING_SUPPORT"]);
3534  }
3535 
3542  public static function ScssRewriteSupport()
3543  {
3544  return isset($_SERVER["SCSS_REWRITE_SUPPORT"])
3545  || isset($_SERVER["REDIRECT_SCSS_REWRITE_SUPPORT"]);
3546  }
3547 
3554  public static function JsMinRewriteSupport()
3555  {
3556  return isset($_SERVER["JSMIN_REWRITE_SUPPORT"])
3557  || isset($_SERVER["REDIRECT_JSMIN_REWRITE_SUPPORT"]);
3558  }
3559 
3567  public static function RootUrl()
3568  {
3569  # return override value if one is set
3570  if (self::$RootUrlOverride !== NULL)
3571  {
3572  return self::$RootUrlOverride;
3573  }
3574 
3575  # determine scheme name
3576  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
3577 
3578  # if HTTP_HOST is preferred or SERVER_NAME points to localhost
3579  # and HTTP_HOST is set
3580  if ((self::$PreferHttpHost || ($_SERVER["SERVER_NAME"] == "127.0.0.1"))
3581  && isset($_SERVER["HTTP_HOST"]))
3582  {
3583  # use HTTP_HOST for domain name
3584  $DomainName = $_SERVER["HTTP_HOST"];
3585  }
3586  else
3587  {
3588  # use SERVER_NAME for domain name
3589  $DomainName = $_SERVER["SERVER_NAME"];
3590  }
3591 
3592  # build URL root and return to caller
3593  return $Protocol."://".$DomainName;
3594  }
3595 
3610  public static function RootUrlOverride($NewValue = self::NOVALUE)
3611  {
3612  if ($NewValue !== self::NOVALUE)
3613  {
3614  self::$RootUrlOverride = strlen(trim($NewValue)) ? $NewValue : NULL;
3615  }
3616  return self::$RootUrlOverride;
3617  }
3618 
3628  public static function BaseUrl()
3629  {
3630  $BaseUrl = self::RootUrl().dirname($_SERVER["SCRIPT_NAME"]);
3631  if (substr($BaseUrl, -1) != "/") { $BaseUrl .= "/"; }
3632  return $BaseUrl;
3633  }
3634 
3642  public static function FullUrl()
3643  {
3644  return self::RootUrl().$_SERVER["REQUEST_URI"];
3645  }
3646 
3657  public static function PreferHttpHost($NewValue = NULL)
3658  {
3659  if ($NewValue !== NULL)
3660  {
3661  self::$PreferHttpHost = ($NewValue ? TRUE : FALSE);
3662  }
3663  return self::$PreferHttpHost;
3664  }
3665 
3670  public static function BasePath()
3671  {
3672  $BasePath = dirname($_SERVER["SCRIPT_NAME"]);
3673 
3674  if (substr($BasePath, -1) != "/")
3675  {
3676  $BasePath .= "/";
3677  }
3678 
3679  return $BasePath;
3680  }
3681 
3687  public static function GetScriptUrl()
3688  {
3689  if (array_key_exists("SCRIPT_URL", $_SERVER))
3690  {
3691  return $_SERVER["SCRIPT_URL"];
3692  }
3693  elseif (array_key_exists("REQUEST_URI", $_SERVER))
3694  {
3695  $Pieces = parse_url($_SERVER["REQUEST_URI"]);
3696  return isset($Pieces["path"]) ? $Pieces["path"] : NULL;
3697  }
3698  elseif (array_key_exists("REDIRECT_URL", $_SERVER))
3699  {
3700  return $_SERVER["REDIRECT_URL"];
3701  }
3702  else
3703  {
3704  return NULL;
3705  }
3706  }
3707 
3716  public static function WasUrlRewritten($ScriptName="index.php")
3717  {
3718  # needed to get the path of the URL minus the query and fragment pieces
3719  $Components = parse_url(self::GetScriptUrl());
3720 
3721  # if parsing was successful and a path is set
3722  if (is_array($Components) && isset($Components["path"]))
3723  {
3724  $BasePath = self::BasePath();
3725  $Path = $Components["path"];
3726 
3727  # the URL was rewritten if the path isn't the base path, i.e., the
3728  # home page, and the file in the URL isn't the script generating the
3729  # page
3730  if ($BasePath != $Path && basename($Path) != $ScriptName)
3731  {
3732  return TRUE;
3733  }
3734  }
3735 
3736  # the URL wasn't rewritten
3737  return FALSE;
3738  }
3739 
3746  public static function ReachedViaAjax()
3747  {
3748  return (isset($_SERVER["HTTP_X_REQUESTED_WITH"])
3749  && (strtolower($_SERVER["HTTP_X_REQUESTED_WITH"])
3750  == "xmlhttprequest"))
3751  ? TRUE : FALSE;
3752  }
3753 
3759  public static function GetFreeMemory()
3760  {
3761  return self::GetPhpMemoryLimit() - memory_get_usage(TRUE);
3762  }
3763 
3769  public static function GetPhpMemoryLimit()
3770  {
3771  $Str = strtoupper(ini_get("memory_limit"));
3772  if (substr($Str, -1) == "B") { $Str = substr($Str, 0, strlen($Str) - 1); }
3773  switch (substr($Str, -1))
3774  {
3775  case "K":
3776  $MemoryLimit = (int)$Str * 1024;
3777  break;
3778 
3779  case "M":
3780  $MemoryLimit = (int)$Str * 1048576;
3781  break;
3782 
3783  case "G":
3784  $MemoryLimit = (int)$Str * 1073741824;
3785  break;
3786 
3787  default:
3788  $MemoryLimit = (int)$Str;
3789  break;
3790  }
3791  return $MemoryLimit;
3792  }
3793 
3806  public function MaxExecutionTime($NewValue = DB_NOVALUE, $Persistent = FALSE)
3807  {
3808  if ($NewValue !== DB_NOVALUE)
3809  {
3810  $NewValue = max($NewValue, 5);
3811  ini_set("max_execution_time", $NewValue);
3812  set_time_limit($NewValue - $this->GetElapsedExecutionTime());
3813  $this->UpdateSetting("MaxExecTime", $NewValue, $Persistent);
3814  }
3815  return ini_get("max_execution_time");
3816  }
3817 
3818  /*@)*/ /* Server Environment */
3819 
3820 
3821  # ---- Utility -----------------------------------------------------------
3822  /*@(*/
3824 
3836  public function DownloadFile($FilePath, $FileName = NULL, $MimeType = NULL)
3837  {
3838  # check that file is readable
3839  if (!is_readable($FilePath))
3840  {
3841  return FALSE;
3842  }
3843 
3844  # if file name was not supplied
3845  if ($FileName === NULL)
3846  {
3847  # extract file name from path
3848  $FileName = basename($FilePath);
3849  }
3850 
3851  # if MIME type was not supplied
3852  if ($MimeType === NULL)
3853  {
3854  # attempt to determine MIME type
3855  $FInfoHandle = finfo_open(FILEINFO_MIME);
3856  if ($FInfoHandle)
3857  {
3858  $FInfoMime = finfo_file($FInfoHandle, $FilePath);
3859  finfo_close($FInfoHandle);
3860  if ($FInfoMime)
3861  {
3862  $MimeType = $FInfoMime;
3863  }
3864  }
3865 
3866  # use default if unable to determine MIME type
3867  if ($MimeType === NULL)
3868  {
3869  $MimeType = "application/octet-stream";
3870  }
3871  }
3872 
3873  # set headers to download file
3874  header("Content-Type: ".$MimeType);
3875  header("Content-Length: ".filesize($FilePath));
3876  if ($this->CleanUrlRewritePerformed)
3877  {
3878  header('Content-Disposition: attachment; filename="'.$FileName.'"');
3879  }
3880 
3881  # make sure that apache does not attempt to compress file
3882  apache_setenv('no-gzip', '1');
3883 
3884  # send file to user, but unbuffered to avoid memory issues
3885  $this->AddUnbufferedCallback(function ($File)
3886  {
3887  $BlockSize = 512000;
3888 
3889  $Handle = @fopen($File, "rb");
3890  if ($Handle === FALSE)
3891  {
3892  return;
3893  }
3894 
3895  # (close out session, making it read-only, so that session file
3896  # lock is released and others are not potentially hanging
3897  # waiting for it while the download completes)
3898  session_write_close();
3899 
3900  while (!feof($Handle))
3901  {
3902  print fread($Handle, $BlockSize);
3903  flush();
3904  }
3905 
3906  fclose($Handle);
3907  }, array($FilePath));
3908 
3909  # prevent HTML output that might interfere with download
3910  $this->SuppressHTMLOutput();
3911 
3912  # set flag to indicate not to log a slow page load in case client
3913  # connection delays PHP execution because of header
3914  $this->DoNotLogSlowPageLoad = TRUE;
3915 
3916  # report no errors found to caller
3917  return TRUE;
3918  }
3919 
3932  public function GetLock($LockName, $Wait = TRUE)
3933  {
3934  # assume we will not get a lock
3935  $GotLock = FALSE;
3936 
3937  # clear out any stale locks
3938  static $CleanupHasBeenDone = FALSE;
3939  if (!$CleanupHasBeenDone)
3940  {
3941  # (margin for clearing stale locks is twice the known
3942  # maximum PHP execution time, because the max time
3943  # techinically does not include external operations
3944  # like database queries)
3945  $ClearLocksObtainedBefore = date(StdLib::SQL_DATE_FORMAT,
3946  (time() - ($this->MaxExecutionTime() * 2)));
3947  $this->DB->Query("DELETE FROM AF_Locks WHERE"
3948  ." ObtainedAt < '".$ClearLocksObtainedBefore."' AND"
3949  ." LockName = '".addslashes($LockName)."'");
3950  }
3951 
3952  do
3953  {
3954  # lock database table so nobody else can try to get a lock
3955  $this->DB->Query("LOCK TABLES AF_Locks WRITE");
3956 
3957  # look for lock with specified name
3958  $FoundCount = $this->DB->Query("SELECT COUNT(*) AS FoundCount"
3959  ." FROM AF_Locks WHERE LockName = '"
3960  .addslashes($LockName)."'", "FoundCount");
3961  $LockFound = ($FoundCount > 0) ? TRUE : FALSE;
3962 
3963  # if lock found
3964  if ($LockFound)
3965  {
3966  # unlock database tables
3967  $this->DB->Query("UNLOCK TABLES");
3968 
3969  # if blocking was requested
3970  if ($Wait)
3971  {
3972  # wait to give someone else a chance to release lock
3973  sleep(2);
3974  }
3975  }
3976  // @codingStandardsIgnoreStart
3977  // (because phpcs does not correctly handle do-while loops)
3978  # while lock was found and blocking was requested
3979  } while ($LockFound && $Wait);
3980  // @codingStandardsIgnoreEnd
3981 
3982  # if lock was not found
3983  if (!$LockFound)
3984  {
3985  # get our lock
3986  $this->DB->Query("INSERT INTO AF_Locks (LockName) VALUES ('"
3987  .addslashes($LockName)."')");
3988  $GotLock = TRUE;
3989 
3990  # unlock database tables
3991  $this->DB->Query("UNLOCK TABLES");
3992  }
3993 
3994  # report to caller whether lock was obtained
3995  return $GotLock;
3996  }
3997 
4005  public function ReleaseLock($LockName)
4006  {
4007  # release any existing locks
4008  $this->DB->Query("DELETE FROM AF_Locks WHERE LockName = '"
4009  .addslashes($LockName)."'");
4010 
4011  # report to caller whether existing lock was released
4012  return $this->DB->NumRowsAffected() ? TRUE : FALSE;
4013  }
4014 
4015  /*@)*/ /* Utility */
4016 
4017 
4018  # ---- Backward Compatibility --------------------------------------------
4019  /*@(*/
4021 
4028  public function FindCommonTemplate($BaseName)
4029  {
4030  return $this->FindFile(
4031  $this->IncludeDirList, $BaseName, array("tpl", "html"));
4032  }
4033 
4034  /*@)*/ /* Backward Compatibility */
4035 
4036 
4037  # ---- PRIVATE INTERFACE -------------------------------------------------
4038 
4039  private $AdditionalRequiredUIFiles = array();
4040  private $BackgroundTaskMemLeakLogThreshold = 10; # percentage of max mem
4041  private $BackgroundTaskMinFreeMemPercent = 25;
4042  private $BrowserDetectFunc;
4043  private $CacheCurrentPage = TRUE;
4044  private $AlternateDomainPrefixes = array();
4045  private $CleanUrlMappings = array();
4046  private $CleanUrlRewritePerformed = FALSE;
4047  private $ContextFilters = array(
4048  self::CONTEXT_START => TRUE,
4049  self::CONTEXT_PAGE => array("H_"),
4050  self::CONTEXT_COMMON => array("H_"),
4051  );
4052  private $CssUrlFingerprintPath;
4053  private $DB;
4054  private $DefaultPage = "Home";
4055  private $DoNotMinimizeList = array();
4056  private $DoNotLogSlowPageLoad = FALSE;
4057  private $EnvIncludes = array();
4058  private $ExecutionStartTime;
4059  private $FoundUIFiles = array();
4060  private $HtmlCharset = "UTF-8";
4061  private $InterfaceSettings = array();
4062  private $JSMinimizerJavaScriptPackerAvailable = FALSE;
4063  private $JSMinimizerJShrinkAvailable = TRUE;
4064  private $JumpToPage = NULL;
4065  private $JumpToPageDelay = 0;
4066  private $LogFileName = "local/logs/site.log";
4067  private $MaxRunningTasksToTrack = 250;
4068  private $MetaTags;
4069  private $OutputModificationCallbackInfo;
4070  private $OutputModificationCallbacks = array();
4071  private $OutputModificationPatterns = array();
4072  private $OutputModificationReplacements = array();
4073  private $PageCacheTags = array();
4074  private $PageName;
4075  private $PostProcessingFuncs = array();
4076  private $RequeueCurrentTask;
4077  private $RunningInBackground = FALSE;
4078  private $RunningTask;
4079  private $SavedContext;
4080  private $SaveTemplateLocationCache = FALSE;
4081  private $SessionStorage;
4082  private $SessionGcProbability;
4083  private $Settings;
4084  private $SuppressHTML = FALSE;
4085  private $SuppressStdPageStartAndEnd = FALSE;
4086  private $TemplateLocationCache;
4087  private $TemplateLocationCacheInterval = 60; # in minutes
4088  private $TemplateLocationCacheExpiration;
4089  private $UnbufferedCallbacks = array();
4090  private $UrlFingerprintBlacklist = array();
4091  private $UseBaseTag = FALSE;
4092 
4093  private static $ActiveUI = "default";
4094  private static $AppName = "ScoutAF";
4095  private static $DefaultUI = "default";
4096  private static $JSMinCacheDir = "local/data/caches/JSMin";
4097  private static $ObjectDirectories = array();
4098  private static $ObjectLocationCache;
4099  private static $ObjectLocationCacheInterval = 60;
4100  private static $ObjectLocationCacheExpiration;
4101  private static $PreferHttpHost = FALSE;
4102  private static $RootUrlOverride = NULL;
4103  private static $SaveObjectLocationCache = FALSE;
4104  private static $ScssCacheDir = "local/data/caches/SCSS";
4105  private static $SessionLifetime = 1440; # in seconds
4106 
4107  # offset used to generate page cache tag IDs from numeric tags
4108  const PAGECACHETAGIDOFFSET = 100000;
4109 
4110  # minimum expired session garbage collection probability
4111  const MIN_GC_PROBABILITY = 0.01;
4112 
4117  private $NoTSR = FALSE;
4118 
4119  private $KnownPeriodicEvents = array();
4120  private $PeriodicEvents = array(
4121  "EVENT_HOURLY" => self::EVENTTYPE_DEFAULT,
4122  "EVENT_DAILY" => self::EVENTTYPE_DEFAULT,
4123  "EVENT_WEEKLY" => self::EVENTTYPE_DEFAULT,
4124  "EVENT_MONTHLY" => self::EVENTTYPE_DEFAULT,
4125  "EVENT_PERIODIC" => self::EVENTTYPE_NAMED,
4126  );
4127  private $EventPeriods = array(
4128  "EVENT_HOURLY" => 3600,
4129  "EVENT_DAILY" => 86400,
4130  "EVENT_WEEKLY" => 604800,
4131  "EVENT_MONTHLY" => 2592000,
4132  "EVENT_PERIODIC" => 0,
4133  );
4134  private $UIEvents = array(
4135  "EVENT_PAGE_LOAD" => self::EVENTTYPE_DEFAULT,
4136  "EVENT_PHP_FILE_LOAD" => self::EVENTTYPE_CHAIN,
4137  "EVENT_PHP_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
4138  "EVENT_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
4139  "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
4140  "EVENT_PAGE_OUTPUT_FILTER" => self::EVENTTYPE_CHAIN,
4141  );
4142 
4147  private function LoadSettings()
4148  {
4149  # read settings in from database
4150  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
4151  $this->Settings = $this->DB->FetchRow();
4152 
4153  # if settings were not previously initialized
4154  if ($this->Settings === FALSE)
4155  {
4156  # initialize settings in database
4157  $this->DB->Query("INSERT INTO ApplicationFrameworkSettings"
4158  ." (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");
4159 
4160  # read new settings in from database
4161  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
4162  $this->Settings = $this->DB->FetchRow();
4163 
4164  # bail out if reloading new settings failed
4165  if ($this->Settings === FALSE)
4166  {
4167  throw new Exception(
4168  "Unable to load application framework settings.");
4169  }
4170  }
4171 
4172  # if base path was not previously set or we appear to have moved
4173  if (!array_key_exists("BasePath", $this->Settings)
4174  || (!strlen($this->Settings["BasePath"]))
4175  || (!array_key_exists("BasePathCheck", $this->Settings))
4176  || (__FILE__ != $this->Settings["BasePathCheck"]))
4177  {
4178  # attempt to extract base path from Apache .htaccess file
4179  if (is_readable(".htaccess"))
4180  {
4181  $Lines = file(".htaccess");
4182  foreach ($Lines as $Line)
4183  {
4184  if (preg_match("/\\s*RewriteBase\\s+/", $Line))
4185  {
4186  $Pieces = preg_split(
4187  "/\\s+/", $Line, NULL, PREG_SPLIT_NO_EMPTY);
4188  $BasePath = $Pieces[1];
4189  }
4190  }
4191  }
4192 
4193  # if base path was found
4194  if (isset($BasePath))
4195  {
4196  # save base path locally
4197  $this->Settings["BasePath"] = $BasePath;
4198 
4199  # save base path to database
4200  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
4201  ." SET BasePath = '".addslashes($BasePath)."'"
4202  .", BasePathCheck = '".addslashes(__FILE__)."'");
4203  }
4204  }
4205 
4206  # retrieve template location cache
4207  $this->TemplateLocationCache = unserialize(
4208  $this->Settings["TemplateLocationCache"]);
4209  $this->TemplateLocationCacheInterval =
4210  $this->Settings["TemplateLocationCacheInterval"];
4211  $this->TemplateLocationCacheExpiration =
4212  strtotime($this->Settings["TemplateLocationCacheExpiration"]);
4213 
4214  # if template location cache looks invalid or has expired
4215  $CurrentTime = time();
4216  if (!count($this->TemplateLocationCache)
4217  || ($CurrentTime >= $this->TemplateLocationCacheExpiration))
4218  {
4219  # clear cache and reset cache expiration
4220  $this->TemplateLocationCache = array();
4221  $this->TemplateLocationCacheExpiration =
4222  $CurrentTime + ($this->TemplateLocationCacheInterval * 60);
4223  $this->SaveTemplateLocationCache = TRUE;
4224  }
4225 
4226  # retrieve object location cache
4227  self::$ObjectLocationCache =
4228  unserialize($this->Settings["ObjectLocationCache"]);
4229  self::$ObjectLocationCacheInterval =
4230  $this->Settings["ObjectLocationCacheInterval"];
4231  self::$ObjectLocationCacheExpiration =
4232  strtotime($this->Settings["ObjectLocationCacheExpiration"]);
4233 
4234  # if object location cache looks invalid or has expired
4235  if (!count(self::$ObjectLocationCache)
4236  || ($CurrentTime >= self::$ObjectLocationCacheExpiration))
4237  {
4238  # clear cache and reset cache expiration
4239  self::$ObjectLocationCache = array();
4240  self::$ObjectLocationCacheExpiration =
4241  $CurrentTime + (self::$ObjectLocationCacheInterval * 60);
4242  self::$SaveObjectLocationCache = TRUE;
4243  }
4244  }
4245 
4252  private function RewriteCleanUrls($PageName)
4253  {
4254  # if URL rewriting is supported by the server
4255  if ($this->HtaccessSupport())
4256  {
4257  # retrieve current URL and remove base path if present
4258  $Url = $this->GetPageLocation();
4259 
4260  # for each clean URL mapping
4261  foreach ($this->CleanUrlMappings as $Info)
4262  {
4263  # if current URL matches clean URL pattern
4264  if (preg_match($Info["Pattern"], $Url, $Matches))
4265  {
4266  # set new page
4267  $PageName = $Info["Page"];
4268 
4269  # if $_GET variables specified for clean URL
4270  if ($Info["GetVars"] !== NULL)
4271  {
4272  # for each $_GET variable specified for clean URL
4273  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
4274  {
4275  # start with template for variable value
4276  $Value = $VarTemplate;
4277 
4278  # for each subpattern matched in current URL
4279  foreach ($Matches as $Index => $Match)
4280  {
4281  # if not first (whole) match
4282  if ($Index > 0)
4283  {
4284  # make any substitutions in template
4285  $Value = str_replace("$".$Index, $Match, $Value);
4286  }
4287  }
4288 
4289  # set $_GET variable
4290  $_GET[$VarName] = $Value;
4291  }
4292  }
4293 
4294  # set flag indicating clean URL mapped
4295  $this->CleanUrlRewritePerformed = TRUE;
4296 
4297  # stop looking for a mapping
4298  break;
4299  }
4300  }
4301  }
4302 
4303  # return (possibly) updated page name to caller
4304  return $PageName;
4305  }
4306 
4319  private function RewriteAlternateDomainUrls($Html)
4320  {
4321  # if we have htaccess support, and if we've been told via a
4322  # RootUrlOverride which of our domains is the primary one
4323  if ($this->HtaccessSupport() &&
4324  self::$RootUrlOverride !== NULL)
4325  {
4326  # see what domain this request was accessed under
4327  $VHost = $_SERVER["SERVER_NAME"];
4328 
4329  # if it was a domain for which we have a prefix
4330  # configured, we'll need to do some rewriting
4331  if (isset($this->AlternateDomainPrefixes[$VHost]))
4332  {
4333  # pull out the configured prefix for this domain
4334  $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
4335 
4336  # get the URL for the primary domain, including the base path
4337  # (usually the part between the host name and the PHP file name)
4338  $RootUrl = $this->RootUrl().self::BasePath();
4339 
4340  # and figure out what protcol we were using
4341  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
4342 
4343  # NB: preg_replace iterates through the configured
4344  # search/replacement pairs, such that the second one
4345  # runs after the first and so on
4346 
4347  # the first n-1 patterns below convert any relative
4348  # links in the generated HTML to absolute links using
4349  # our primary domain (e.g., for stylesheets, javascript,
4350  # images, etc)
4351 
4352  # the nth pattern looks for links that live within the
4353  # path subtree specified by our configured prefix on
4354  # our primary domain, then replaces them with equivalent
4355  # links on our secondary domain
4356 
4357  # for example, if our primary domain is
4358  # example.com/MySite and our secondary domain is
4359  # things.example.org/MySite with 'things' as the
4360  # configured prefix, then this last pattern will look
4361  # for example.com/MySite/things and replace it with
4362  # things.example.org/MySite
4363  $RelativePathPatterns = array(
4364  "%src=\"(?!http://|https://)%i",
4365  "%src='(?!http://|https://)%i",
4366  "%href=\"(?!http://|https://)%i",
4367  "%href='(?!http://|https://)%i",
4368  "%action=\"(?!http://|https://)%i",
4369  "%action='(?!http://|https://)%i",
4370  "%@import\s+url\(\"(?!http://|https://)%i",
4371  "%@import\s+url\('(?!http://|https://)%i",
4372  "%src:\s+url\(\"(?!http://|https://)%i",
4373  "%src:\s+url\('(?!http://|https://)%i",
4374  "%@import\s+\"(?!http://|https://)%i",
4375  "%@import\s+'(?!http://|https://)%i",
4376  "%".preg_quote($RootUrl.$ThisPrefix."/", "%")."%",
4377  );
4378  $RelativePathReplacements = array(
4379  "src=\"".$RootUrl,
4380  "src='".$RootUrl,
4381  "href=\"".$RootUrl,
4382  "href='".$RootUrl,
4383  "action=\"".$RootUrl,
4384  "action='".$RootUrl,
4385  "@import url(\"".$RootUrl,
4386  "@import url('".$RootUrl,
4387  "src: url(\"".$RootUrl,
4388  "src: url('".$RootUrl,
4389  "@import \"".$RootUrl,
4390  "@import '".$RootUrl,
4391  $Protocol."://".$VHost.self::BasePath(),
4392  );
4393 
4394  $NewHtml = preg_replace(
4395  $RelativePathPatterns,
4396  $RelativePathReplacements,
4397  $Html);
4398 
4399  # check to make sure relative path fixes didn't fail
4400  $Html = $this->CheckOutputModification(
4401  $Html, $NewHtml,
4402  "alternate domain substitutions");
4403  }
4404  }
4405 
4406  return $Html;
4407  }
4408 
4427  private function FindFile($DirectoryList, $BaseName,
4428  $PossibleSuffixes = NULL, $PossiblePrefixes = NULL)
4429  {
4430  # generate template cache index for this page
4431  $CacheIndex = md5(serialize($DirectoryList))
4432  .self::$DefaultUI.self::$ActiveUI.$BaseName;
4433 
4434  # if caching is enabled and we have cached location
4435  if (($this->TemplateLocationCacheInterval > 0)
4436  && array_key_exists($CacheIndex,
4437  $this->TemplateLocationCache))
4438  {
4439  # use template location from cache
4440  $FoundFileName = $this->TemplateLocationCache[$CacheIndex];
4441  }
4442  else
4443  {
4444  # if suffixes specified and base name does not include suffix
4445  if (count($PossibleSuffixes)
4446  && !preg_match("/\.[a-zA-Z0-9]+$/", $BaseName))
4447  {
4448  # add versions of file names with suffixes to file name list
4449  $FileNames = array();
4450  foreach ($PossibleSuffixes as $Suffix)
4451  {
4452  $FileNames[] = $BaseName.".".$Suffix;
4453  }
4454  }
4455  else
4456  {
4457  # use base name as file name
4458  $FileNames = array($BaseName);
4459  }
4460 
4461  # if prefixes specified
4462  if (count($PossiblePrefixes))
4463  {
4464  # add versions of file names with prefixes to file name list
4465  $NewFileNames = array();
4466  foreach ($FileNames as $FileName)
4467  {
4468  foreach ($PossiblePrefixes as $Prefix)
4469  {
4470  $NewFileNames[] = $Prefix.$FileName;
4471  }
4472  }
4473  $FileNames = $NewFileNames;
4474  }
4475 
4476  # expand directory list to include variants
4477  $DirectoryList = $this->ExpandDirectoryList($DirectoryList);
4478 
4479  # for each possible location
4480  $FoundFileName = NULL;
4481  foreach ($DirectoryList as $Dir)
4482  {
4483  # for each possible file name
4484  foreach ($FileNames as $File)
4485  {
4486  # if template is found at location
4487  if (file_exists($Dir.$File))
4488  {
4489  # save full template file name and stop looking
4490  $FoundFileName = $Dir.$File;
4491  break 2;
4492  }
4493  }
4494  }
4495 
4496  # save location in cache
4497  $this->TemplateLocationCache[$CacheIndex]
4498  = $FoundFileName;
4499 
4500  # set flag indicating that cache should be saved
4501  $this->SaveTemplateLocationCache = TRUE;
4502  }
4503 
4504  # return full template file name to caller
4505  return $FoundFileName;
4506  }
4507 
4514  private function ExpandDirectoryList($DirList)
4515  {
4516  # generate lookup for supplied list
4517  $ExpandedListKey = md5(serialize($DirList)
4518  .self::$DefaultUI.self::$ActiveUI);
4519 
4520  # if we already have expanded version of supplied list
4521  if (isset($this->ExpandedDirectoryLists[$ExpandedListKey]))
4522  {
4523  # return expanded version to caller
4524  return $this->ExpandedDirectoryLists[$ExpandedListKey];
4525  }
4526 
4527  # for each directory in list
4528  $ExpDirList = array();
4529  foreach ($DirList as $Dir)
4530  {
4531  # if directory includes substitution keyword
4532  if ((strpos($Dir, "%DEFAULTUI%") !== FALSE)
4533  || (strpos($Dir, "%ACTIVEUI%") !== FALSE))
4534  {
4535  # start with empty new list segment
4536  $ExpDirListSegment = array();
4537 
4538  # use default values for initial parent
4539  $ParentInterface = array(self::$ActiveUI, self::$DefaultUI);
4540 
4541  do
4542  {
4543  # substitute in for keyword on parent
4544  $CurrDir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4545  $ParentInterface, $Dir);
4546 
4547  # add local version of parent directory to new list segment
4548  $ExpDirListSegment[] = "local/".$CurrDir;
4549 
4550  # add parent directory to new list segment
4551  $ExpDirListSegment[] = $CurrDir;
4552 
4553  # look for new parent interface
4554  $ParentInterface = $this->GetInterfaceSetting(
4555  $CurrDir, "ParentInterface");
4556 
4557  # repeat if parent is available
4558  } while (strlen($ParentInterface));
4559 
4560  # add new list segment to expanded list
4561  $ExpDirList = array_merge($ExpDirList, $ExpDirListSegment);
4562  }
4563  else
4564  {
4565  # add local version of directory to expanded list
4566  $ExpDirList[] = "local/".$Dir;
4567 
4568  # add directory to expanded list
4569  $ExpDirList[] = $Dir;
4570  }
4571  }
4572 
4573  # return expanded version to caller
4574  $this->ExpandedDirectoryLists[$ExpandedListKey] = $ExpDirList;
4575  return $this->ExpandedDirectoryLists[$ExpandedListKey];
4576  }
4577 
4586  private function GetInterfaceSetting($InterfaceDir, $SettingName = NULL)
4587  {
4588  # extract canonical interface name and base interface directory
4589  preg_match("%(.*interface/)([^/]+)%", $InterfaceDir, $Matches);
4590  $InterfaceDir = (count($Matches) > 2)
4591  ? $Matches[1].$Matches[2] : $InterfaceDir;
4592  $InterfaceName = (count($Matches) > 2)
4593  ? $Matches[2] : "UNKNOWN";
4594 
4595  # if we do not have settings for interface
4596  if (!isset($this->InterfaceSettings[$InterfaceName]))
4597  {
4598  # load default values for settings
4599  $this->InterfaceSettings[$InterfaceName] = array(
4600  "Source" => "",
4601  );
4602  }
4603 
4604  # if directory takes precedence over existing settings source
4605  # ("takes precendence" == is more local == longer directory length)
4606  if (strlen($InterfaceDir)
4607  > strlen($this->InterfaceSettings[$InterfaceName]["Source"]))
4608  {
4609  # if settings file exists in directory
4610  $SettingsFile = $InterfaceDir."/interface.ini";
4611  if (is_readable($SettingsFile))
4612  {
4613  # read in values from file
4614  $NewSettings = parse_ini_file($SettingsFile);
4615 
4616  # merge in values with existing settings
4617  $this->InterfaceSettings[$InterfaceName] = array_merge(
4618  $this->InterfaceSettings[$InterfaceName], $NewSettings);
4619 
4620  # save new source of settings
4621  $this->InterfaceSettings[$InterfaceName]["Source"] = $InterfaceDir;
4622  }
4623  }
4624 
4625  # return interface settings to caller
4626  return $SettingName
4627  ? (isset($this->InterfaceSettings[$InterfaceName][$SettingName])
4628  ? $this->InterfaceSettings[$InterfaceName][$SettingName]
4629  : NULL)
4630  : $this->InterfaceSettings[$InterfaceName];
4631  }
4632 
4641  private function CompileScssFile($SrcFile)
4642  {
4643  # build path to CSS file
4644  $DstFile = self::$ScssCacheDir."/".dirname($SrcFile)
4645  ."/".basename($SrcFile);
4646  $DstFile = substr_replace($DstFile, "css", -4);
4647 
4648  # if SCSS file is newer than CSS file
4649  if (!file_exists($DstFile)
4650  || (filemtime($SrcFile) > filemtime($DstFile)))
4651  {
4652  # attempt to create CSS cache subdirectory if not present
4653  if (!is_dir(dirname($DstFile)))
4654  {
4655  @mkdir(dirname($DstFile), 0777, TRUE);
4656  }
4657 
4658  # if CSS cache directory and CSS file path appear writable
4659  static $CacheDirIsWritable;
4660  if (!isset($CacheDirIsWritable))
4661  { $CacheDirIsWritable = is_writable(self::$ScssCacheDir); }
4662  if (is_writable($DstFile)
4663  || (!file_exists($DstFile) && $CacheDirIsWritable))
4664  {
4665  # load SCSS and compile to CSS
4666  $ScssCode = file_get_contents($SrcFile);
4667  $ScssCompiler = new scssc();
4668  $ScssCompiler->setFormatter($this->GenerateCompactCss()
4669  ? "scss_formatter_compressed" : "scss_formatter");
4670  try
4671  {
4672  $CssCode = $ScssCompiler->compile($ScssCode);
4673 
4674  # add fingerprinting for URLs in CSS
4675  $this->CssUrlFingerprintPath = dirname($SrcFile);
4676  $CssCode = preg_replace_callback(
4677  "/url\((['\"]*)(.+)\.([a-z]+)(['\"]*)\)/",
4678  array($this, "CssUrlFingerprintInsertion"),
4679  $CssCode);
4680 
4681  # strip out comments from CSS (if requested)
4682  if ($this->GenerateCompactCss())
4683  {
4684  $CssCode = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!',
4685  '', $CssCode);
4686  }
4687 
4688  # write out CSS file
4689  file_put_contents($DstFile, $CssCode);
4690  }
4691  catch (Exception $Ex)
4692  {
4693  $this->LogError(self::LOGLVL_ERROR,
4694  "Error compiling SCSS file ".$SrcFile.": "
4695  .$Ex->getMessage());
4696  $DstFile = NULL;
4697  }
4698  }
4699  else
4700  {
4701  # log error and set CSS file path to indicate failure
4702  $this->LogError(self::LOGLVL_ERROR,
4703  "Unable to write out CSS file (compiled from SCSS) to "
4704  .$DstFile);
4705  $DstFile = NULL;
4706  }
4707  }
4708 
4709  # return CSS file path to caller
4710  return $DstFile;
4711  }
4712 
4720  private function MinimizeJavascriptFile($SrcFile)
4721  {
4722  # bail out if file is on exclusion list
4723  foreach ($this->DoNotMinimizeList as $DNMFile)
4724  {
4725  if (($SrcFile == $DNMFile) || (basename($SrcFile) == $DNMFile))
4726  {
4727  return NULL;
4728  }
4729  }
4730 
4731  # build path to minimized file
4732  $DstFile = self::$JSMinCacheDir."/".dirname($SrcFile)
4733  ."/".basename($SrcFile);
4734  $DstFile = substr_replace($DstFile, ".min", -3, 0);
4735 
4736  # if original file is newer than minimized file
4737  if (!file_exists($DstFile)
4738  || (filemtime($SrcFile) > filemtime($DstFile)))
4739  {
4740  # attempt to create cache subdirectory if not present
4741  if (!is_dir(dirname($DstFile)))
4742  {
4743  @mkdir(dirname($DstFile), 0777, TRUE);
4744  }
4745 
4746  # if cache directory and minimized file path appear writable
4747  static $CacheDirIsWritable;
4748  if (!isset($CacheDirIsWritable))
4749  { $CacheDirIsWritable = is_writable(self::$JSMinCacheDir); }
4750  if (is_writable($DstFile)
4751  || (!file_exists($DstFile) && $CacheDirIsWritable))
4752  {
4753  # load JavaScript code
4754  $Code = file_get_contents($SrcFile);
4755 
4756  # decide which minimizer to use
4757  if ($this->JSMinimizerJavaScriptPackerAvailable
4758  && $this->JSMinimizerJShrinkAvailable)
4759  {
4760  $Minimizer = (strlen($Code) < 5000)
4761  ? "JShrink" : "JavaScriptPacker";
4762  }
4763  elseif ($this->JSMinimizerJShrinkAvailable)
4764  {
4765  $Minimizer = "JShrink";
4766  }
4767  else
4768  {
4769  $Minimizer = "NONE";
4770  }
4771 
4772  # minimize code
4773  switch ($Minimizer)
4774  {
4775  case "JavaScriptMinimizer":
4776  $Packer = new JavaScriptPacker($Code, "Normal");
4777  $MinimizedCode = $Packer->pack();
4778  break;
4779 
4780  case "JShrink":
4781  try
4782  {
4783  $MinimizedCode = \JShrink\Minifier::minify($Code);
4784  }
4785  catch (Exception $Exception)
4786  {
4787  unset($MinimizedCode);
4788  $MinimizeError = $Exception->getMessage();
4789  }
4790  break;
4791  }
4792 
4793  # if minimization succeeded
4794  if (isset($MinimizedCode))
4795  {
4796  # write out minimized file
4797  file_put_contents($DstFile, $MinimizedCode);
4798  }
4799  else
4800  {
4801  # log error and set destination file path to indicate failure
4802  $ErrMsg = "Unable to minimize JavaScript file ".$SrcFile;
4803  if (isset($MinimizeError))
4804  {
4805  $ErrMsg .= " (".$MinimizeError.")";
4806  }
4807  $this->LogError(self::LOGLVL_ERROR, $ErrMsg);
4808  $DstFile = NULL;
4809  }
4810  }
4811  else
4812  {
4813  # log error and set destination file path to indicate failure
4814  $this->LogError(self::LOGLVL_ERROR,
4815  "Unable to write out minimized JavaScript to file ".$DstFile);
4816  $DstFile = NULL;
4817  }
4818  }
4819 
4820  # return CSS file path to caller
4821  return $DstFile;
4822  }
4823 
4831  private function CssUrlFingerprintInsertion($Matches)
4832  {
4833  # generate fingerprint string from CSS file modification time
4834  $FileName = realpath($this->CssUrlFingerprintPath."/".
4835  $Matches[2].".".$Matches[3]);
4836  $MTime = filemtime($FileName);
4837  $Fingerprint = sprintf("%06X", ($MTime % 0xFFFFFF));
4838 
4839  # build URL string with fingerprint and return it to caller
4840  return "url(".$Matches[1].$Matches[2].".".$Fingerprint
4841  .".".$Matches[3].$Matches[4].")";
4842  }
4843 
4851  private function GetRequiredFilesNotYetLoaded($PageContentFile)
4852  {
4853  # start out assuming no files required
4854  $RequiredFiles = array();
4855 
4856  # if page content file supplied
4857  if ($PageContentFile)
4858  {
4859  # if file containing list of required files is available
4860  $Path = dirname($PageContentFile);
4861  $RequireListFile = $Path."/REQUIRES";
4862  if (file_exists($RequireListFile))
4863  {
4864  # read in list of required files
4865  $RequestedFiles = file($RequireListFile);
4866 
4867  # for each line in required file list
4868  foreach ($RequestedFiles as $Line)
4869  {
4870  # if line is not a comment
4871  $Line = trim($Line);
4872  if (!preg_match("/^#/", $Line))
4873  {
4874  # if file has not already been loaded
4875  if (!in_array($Line, $this->FoundUIFiles))
4876  {
4877  # add to list of required files
4878  $RequiredFiles[$Line] = self::ORDER_MIDDLE;
4879  }
4880  }
4881  }
4882  }
4883  }
4884 
4885  # add in additional required files if any
4886  if (count($this->AdditionalRequiredUIFiles))
4887  {
4888  $RequiredFiles = array_merge(
4889  $RequiredFiles, $this->AdditionalRequiredUIFiles);
4890  }
4891 
4892  # return list of required files to caller
4893  return $RequiredFiles;
4894  }
4895 
4904  private function SubBrowserIntoFileNames($FileNames)
4905  {
4906  # if a browser detection function has been made available
4907  $UpdatedFileNames = array();
4908  if (is_callable($this->BrowserDetectFunc))
4909  {
4910  # call function to get browser list
4911  $Browsers = call_user_func($this->BrowserDetectFunc);
4912 
4913  # for each required file
4914  foreach ($FileNames as $FileName => $Value)
4915  {
4916  # if file name includes browser keyword
4917  if (preg_match("/%BROWSER%/", $FileName))
4918  {
4919  # for each browser
4920  foreach ($Browsers as $Browser)
4921  {
4922  # substitute in browser name and add to new file list
4923  $NewFileName = preg_replace(
4924  "/%BROWSER%/", $Browser, $FileName);
4925  $UpdatedFileNames[$NewFileName] = $Value;
4926  }
4927  }
4928  else
4929  {
4930  # add to new file list
4931  $UpdatedFileNames[$FileName] = $Value;
4932  }
4933  }
4934  }
4935  else
4936  {
4937  # filter out any files with browser keyword in their name
4938  foreach ($FileNames as $FileName => $Value)
4939  {
4940  if (!preg_match("/%BROWSER%/", $FileName))
4941  {
4942  $UpdatedFileNames[$FileName] = $Value;
4943  }
4944  }
4945  }
4946 
4947  return $UpdatedFileNames;
4948  }
4949 
4955  private function AddMetaTagsToPageOutput($PageOutput)
4956  {
4957  if (isset($this->MetaTags))
4958  {
4959  $MetaTagSection = "";
4960  foreach ($this->MetaTags as $MetaTagAttribs)
4961  {
4962  $MetaTagSection .= "<meta";
4963  foreach ($MetaTagAttribs as
4964  $MetaTagAttribName => $MetaTagAttribValue)
4965  {
4966  $MetaTagSection .= " ".$MetaTagAttribName."=\""
4967  .htmlspecialchars(trim($MetaTagAttribValue))."\"";
4968  }
4969  $MetaTagSection .= " />\n";
4970  }
4971 
4972  if ($this->SuppressStdPageStartAndEnd)
4973  {
4974  $PageOutput = $MetaTagSection.$PageOutput;
4975  }
4976  else
4977  {
4978  $PageOutput = preg_replace("#<head>#i",
4979  "<head>\n".$MetaTagSection, $PageOutput, 1);
4980  }
4981  }
4982 
4983  return $PageOutput;
4984  }
4985 
4993  private function AddFileTagsToPageOutput($PageOutput, $Files)
4994  {
4995  # substitute browser name into names of required files as appropriate
4996  $Files = $this->SubBrowserIntoFileNames($Files);
4997 
4998  # initialize content sections
4999  $HeadContent = [
5000  self::ORDER_FIRST => "",
5001  self::ORDER_MIDDLE => "",
5002  self::ORDER_LAST => "",
5003  ];
5004  $BodyContent = [
5005  self::ORDER_FIRST => "",
5006  self::ORDER_MIDDLE => "",
5007  self::ORDER_LAST => "",
5008  ];
5009 
5010  # for each required file
5011  foreach ($Files as $File => $Order)
5012  {
5013  # locate specific file to use
5014  $FilePath = $this->GUIFile($File);
5015 
5016  # if file was found
5017  if ($FilePath)
5018  {
5019  # generate tag for file
5020  $Tag = $this->GetUIFileLoadingTag($FilePath);
5021 
5022  # add file to HTML output based on file type
5023  $FileType = $this->GetFileType($FilePath);
5024  switch ($FileType)
5025  {
5026  case self::FT_CSS:
5027  $HeadContent[$Order] .= $Tag."\n";
5028  break;
5029 
5030  case self::FT_JAVASCRIPT:
5031  $BodyContent[$Order] .= $Tag."\n";
5032  break;
5033  }
5034  }
5035  }
5036 
5037  # add content to head
5038  $Replacement = $HeadContent[self::ORDER_MIDDLE]
5039  .$HeadContent[self::ORDER_LAST];
5040  $UpdatedPageOutput = str_ireplace("</head>",
5041  $Replacement."</head>",
5042  $PageOutput, $ReplacementCount);
5043  # (if no </head> tag was found, just prepend tags to page content)
5044  if ($ReplacementCount == 0)
5045  {
5046  $PageOutput = $Replacement.$PageOutput;
5047  }
5048  # (else if multiple </head> tags found, only prepend tags to the first)
5049  elseif ($ReplacementCount > 1)
5050  {
5051  $PageOutput = preg_replace("#</head>#i",
5052  $Replacement."</head>",
5053  $PageOutput, 1);
5054  }
5055  else
5056  {
5057  $PageOutput = $UpdatedPageOutput;
5058  }
5059  $Replacement = $HeadContent[self::ORDER_FIRST];
5060  $UpdatedPageOutput = str_ireplace("<head>",
5061  "<head>\n".$Replacement,
5062  $PageOutput, $ReplacementCount);
5063  # (if no <head> tag was found, just prepend tags to page content)
5064  if ($ReplacementCount == 0)
5065  {
5066  $PageOutput = $Replacement.$PageOutput;
5067  }
5068  # (else if multiple <head> tags found, only append tags to the first)
5069  elseif ($ReplacementCount > 1)
5070  {
5071  $PageOutput = preg_replace("#<head>#i",
5072  "<head>\n".$Replacement,
5073  $PageOutput, 1);
5074  }
5075  else
5076  {
5077  $PageOutput = $UpdatedPageOutput;
5078  }
5079 
5080  # add content to body
5081  $Replacement = $BodyContent[self::ORDER_FIRST];
5082  $PageOutput = preg_replace("#<body([^>]*)>#i",
5083  "<body\\1>\n".$Replacement,
5084  $PageOutput, 1, $ReplacementCount);
5085  # (if no <body> tag was found, just append tags to page content)
5086  if ($ReplacementCount == 0)
5087  {
5088  $PageOutput = $PageOutput.$Replacement;
5089  }
5090  $Replacement = $BodyContent[self::ORDER_MIDDLE]
5091  .$BodyContent[self::ORDER_LAST];
5092  $UpdatedPageOutput = str_ireplace("</body>",
5093  $Replacement."\n</body>",
5094  $PageOutput, $ReplacementCount);
5095  # (if no </body> tag was found, just append tags to page content)
5096  if ($ReplacementCount == 0)
5097  {
5098  $PageOutput = $PageOutput.$Replacement;
5099  }
5100  # (else if multiple </body> tags found, only prepend tag to the first)
5101  elseif ($ReplacementCount > 1)
5102  {
5103  $PageOutput = preg_replace("#</body>#i",
5104  $Replacement."\n</body>",
5105  $PageOutput, 1);
5106  }
5107  else
5108  {
5109  $PageOutput = $UpdatedPageOutput;
5110  }
5111 
5112  return $PageOutput;
5113  }
5114 
5125  private function GetUIFileLoadingTag($FileName, $AdditionalAttributes = NULL)
5126  {
5127  # pad additional attributes if supplied
5128  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
5129 
5130  # retrieve type of UI file
5131  $FileType = $this->GetFileType($FileName);
5132 
5133  # construct tag based on file type
5134  switch ($FileType)
5135  {
5136  case self::FT_CSS:
5137  $Tag = " <link rel=\"stylesheet\" type=\"text/css\""
5138  ." media=\"all\" href=\"".$FileName."\""
5139  .$AddAttribs." />\n";
5140  break;
5141 
5142  case self::FT_JAVASCRIPT:
5143  $Tag = " <script type=\"text/javascript\""
5144  ." src=\"".$FileName."\""
5145  .$AddAttribs."></script>\n";
5146  break;
5147 
5148  case self::FT_IMAGE:
5149  $Tag = "<img src=\"".$FileName."\"".$AddAttribs.">";
5150  break;
5151 
5152  default:
5153  $Tag = "";
5154  break;
5155  }
5156 
5157  # return constructed tag to caller
5158  return $Tag;
5159  }
5160 
5165  private function AutoloadObjects($ClassName)
5166  {
5167  # if caching is not turned off
5168  # and we have a cached location for class
5169  # and file at cached location is readable
5170  if ((self::$ObjectLocationCacheInterval > 0)
5171  && array_key_exists($ClassName,
5172  self::$ObjectLocationCache)
5173  && is_readable(self::$ObjectLocationCache[$ClassName]))
5174  {
5175  # use object location from cache
5176  require_once(self::$ObjectLocationCache[$ClassName]);
5177  }
5178  else
5179  {
5180  # convert any namespace separators in class name
5181  $ClassName = str_replace("\\", "-", $ClassName);
5182 
5183  # for each possible object file directory
5184  static $FileLists;
5185  foreach (self::$ObjectDirectories as $Location => $Info)
5186  {
5187  # make any needed replacements in directory path
5188  $Location = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
5189  array(self::$ActiveUI, self::$DefaultUI), $Location);
5190 
5191  # if directory looks valid
5192  if (is_dir($Location))
5193  {
5194  # build class file name
5195  $NewClassName = ($Info["ClassPattern"] && $Info["ClassReplacement"])
5196  ? preg_replace($Info["ClassPattern"],
5197  $Info["ClassReplacement"], $ClassName)
5198  : $ClassName;
5199 
5200  # read in directory contents if not already retrieved
5201  if (!isset($FileLists[$Location]))
5202  {
5203  $FileLists[$Location] = self::ReadDirectoryTree(
5204  $Location, '/^.+\.php$/i');
5205  }
5206 
5207  # for each file in target directory
5208  $FileNames = $FileLists[$Location];
5209  $TargetName = strtolower($Info["Prefix"].$NewClassName.".php");
5210  foreach ($FileNames as $FileName)
5211  {
5212  # if file matches our target object file name
5213  if (strtolower($FileName) == $TargetName)
5214  {
5215  # include object file
5216  require_once($Location.$FileName);
5217 
5218  # save location to cache
5219  self::$ObjectLocationCache[$ClassName]
5220  = $Location.$FileName;
5221 
5222  # set flag indicating that cache should be saved
5223  self::$SaveObjectLocationCache = TRUE;
5224 
5225  # stop looking
5226  break 2;
5227  }
5228  }
5229  }
5230  }
5231  }
5232  }
5233 
5241  private static function ReadDirectoryTree($Directory, $Pattern)
5242  {
5243  $CurrentDir = getcwd();
5244  chdir($Directory);
5245  $DirIter = new RecursiveDirectoryIterator(".");
5246  $IterIter = new RecursiveIteratorIterator($DirIter);
5247  $RegexResults = new RegexIterator($IterIter, $Pattern,
5248  RecursiveRegexIterator::GET_MATCH);
5249  $FileList = array();
5250  foreach ($RegexResults as $Result)
5251  {
5252  $FileList[] = substr($Result[0], 2);
5253  }
5254  chdir($CurrentDir);
5255  return $FileList;
5256  }
5257 
5261  private function UndoMagicQuotes()
5262  {
5263  # if this PHP version has magic quotes support
5264  if (version_compare(PHP_VERSION, "5.4.0", "<"))
5265  {
5266  # turn off runtime magic quotes if on
5267  if (get_magic_quotes_runtime())
5268  {
5269  // @codingStandardsIgnoreStart
5270  set_magic_quotes_runtime(FALSE);
5271  // @codingStandardsIgnoreEnd
5272  }
5273 
5274  # if magic quotes GPC is on
5275  if (get_magic_quotes_gpc())
5276  {
5277  # strip added slashes from incoming variables
5278  $GPC = array(&$_GET, &$_POST, &$_COOKIE, &$_REQUEST);
5279  array_walk_recursive($GPC,
5280  array($this, "UndoMagicQuotes_StripCallback"));
5281  }
5282  }
5283  }
5288  private function UndoMagicQuotes_StripCallback(&$Value)
5289  {
5290  $Value = stripslashes($Value);
5291  }
5292 
5297  private function LoadUIFunctions()
5298  {
5299  $Dirs = array(
5300  "local/interface/%ACTIVEUI%/include",
5301  "interface/%ACTIVEUI%/include",
5302  "local/interface/%DEFAULTUI%/include",
5303  "interface/%DEFAULTUI%/include",
5304  );
5305  foreach ($Dirs as $Dir)
5306  {
5307  $Dir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
5308  array(self::$ActiveUI, self::$DefaultUI), $Dir);
5309  if (is_dir($Dir))
5310  {
5311  $FileNames = scandir($Dir);
5312  foreach ($FileNames as $FileName)
5313  {
5314  if (preg_match("/^F-([A-Za-z0-9_]+)\.php/",
5315  $FileName, $Matches)
5316  || preg_match("/^F-([A-Za-z0-9_]+)\.html/",
5317  $FileName, $Matches))
5318  {
5319  if (!function_exists($Matches[1]))
5320  {
5321  include_once($Dir."/".$FileName);
5322  }
5323  }
5324  }
5325  }
5326  }
5327  }
5328 
5334  private function ProcessPeriodicEvent($EventName, $Callback)
5335  {
5336  # retrieve last execution time for event if available
5337  $Signature = self::GetCallbackSignature($Callback);
5338  $LastRun = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
5339  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
5340 
5341  # determine whether enough time has passed for event to execute
5342  $ShouldExecute = (($LastRun === NULL)
5343  || (time() > (strtotime($LastRun) + $this->EventPeriods[$EventName])))
5344  ? TRUE : FALSE;
5345 
5346  # if event should run
5347  if ($ShouldExecute)
5348  {
5349  # add event to task queue
5350  $WrapperCallback = array("ApplicationFramework", "RunPeriodicEvent");
5351  $WrapperParameters = array(
5352  $EventName, $Callback, array("LastRunAt" => $LastRun));
5353  $this->QueueUniqueTask($WrapperCallback, $WrapperParameters);
5354  }
5355 
5356  # add event to list of periodic events
5357  $this->KnownPeriodicEvents[$Signature] = array(
5358  "Period" => $EventName,
5359  "Callback" => $Callback,
5360  "Queued" => $ShouldExecute);
5361  }
5362 
5368  private static function GetCallbackSignature($Callback)
5369  {
5370  return !is_array($Callback) ? $Callback
5371  : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
5372  ."::".$Callback[1];
5373  }
5374 
5379  private function PrepForTSR()
5380  {
5381  # if HTML has been output and it's time to launch another task
5382  # (only TSR if HTML has been output because otherwise browsers
5383  # may misbehave after connection is closed)
5384  if ((PHP_SAPI != "cli")
5385  && ($this->JumpToPage || !$this->SuppressHTML)
5386  && (time() > (strtotime($this->Settings["LastTaskRunAt"])
5387  + ($this->MaxExecutionTime()
5388  / $this->Settings["MaxTasksRunning"]) + 5))
5389  && $this->GetTaskQueueSize()
5390  && $this->Settings["TaskExecutionEnabled"])
5391  {
5392  # begin buffering output for TSR
5393  ob_start();
5394 
5395  # let caller know it is time to launch another task
5396  return TRUE;
5397  }
5398  else
5399  {
5400  # let caller know it is not time to launch another task
5401  return FALSE;
5402  }
5403  }
5404 
5409  private function LaunchTSR()
5410  {
5411  # set headers to close out connection to browser
5412  if (!$this->NoTSR)
5413  {
5414  ignore_user_abort(TRUE);
5415  header("Connection: close");
5416  header("Content-Length: ".ob_get_length());
5417  }
5418 
5419  # output buffered content
5420  while (ob_get_level()) { ob_end_flush(); }
5421  flush();
5422 
5423  # write out any outstanding data and end HTTP session
5424  session_write_close();
5425 
5426  # set flag indicating that we are now running in background
5427  $this->RunningInBackground = TRUE;
5428 
5429  # handle garbage collection for session data
5430  if (isset($this->SessionStorage) &&
5431  (rand()/getrandmax()) <= $this->SessionGcProbability)
5432  {
5433  # determine when sessions will expire
5434  $ExpiredTime = strtotime("-". self::$SessionLifetime." seconds");
5435 
5436  # iterate over files in the session directory with a DirectoryIterator
5437  # NB: we cannot use scandir() here because it reads the
5438  # entire list of files into memory and may exceed the memory
5439  # limit for directories with very many files
5440  $DI = new DirectoryIterator($this->SessionStorage);
5441  while ($DI->valid())
5442  {
5443  if ((strpos($DI->getFilename(), "sess_") === 0) &&
5444  $DI->isFile() &&
5445  $DI->getCTime() < $ExpiredTime)
5446  {
5447  unlink($DI->getPathname());
5448  }
5449  $DI->next();
5450  }
5451  unset($DI);
5452  }
5453 
5454  # if there is still a task in the queue
5455  if ($this->GetTaskQueueSize())
5456  {
5457  # garbage collect to give as much memory as possible for tasks
5458  if (function_exists("gc_collect_cycles")) { gc_collect_cycles(); }
5459 
5460  # turn on output buffering to (hopefully) record any crash output
5461  ob_start();
5462 
5463  # lock tables and grab last task run time to double check
5464  $this->DB->Query("LOCK TABLES ApplicationFrameworkSettings WRITE");
5465  $this->LoadSettings();
5466 
5467  # if still time to launch another task
5468  if (time() > (strtotime($this->Settings["LastTaskRunAt"])
5469  + ($this->MaxExecutionTime()
5470  / $this->Settings["MaxTasksRunning"]) + 5))
5471  {
5472  # update the "last run" time and release tables
5473  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
5474  ." SET LastTaskRunAt = '".date("Y-m-d H:i:s")."'");
5475  $this->DB->Query("UNLOCK TABLES");
5476 
5477  # run tasks while there is a task in the queue
5478  # and enough time and memory left
5479  do
5480  {
5481  # run the next task
5482  $this->RunNextTask();
5483 
5484  # calculate percentage of memory still available
5485  $PercentFreeMem = (self::GetFreeMemory()
5486  / self::GetPhpMemoryLimit()) * 100;
5487  }
5488  while ($this->GetTaskQueueSize()
5489  && ($this->GetSecondsBeforeTimeout() > 65)
5490  && ($PercentFreeMem > $this->BackgroundTaskMinFreeMemPercent));
5491  }
5492  else
5493  {
5494  # release tables
5495  $this->DB->Query("UNLOCK TABLES");
5496  }
5497  }
5498  }
5499 
5509  private function GetTaskList($DBQuery, $Count, $Offset)
5510  {
5511  $this->DB->Query($DBQuery." LIMIT ".intval($Offset).",".intval($Count));
5512  $Tasks = array();
5513  while ($Row = $this->DB->FetchRow())
5514  {
5515  $Tasks[$Row["TaskId"]] = $Row;
5516  if ($Row["Callback"] ==
5517  serialize(array("ApplicationFramework", "RunPeriodicEvent")))
5518  {
5519  $WrappedCallback = unserialize($Row["Parameters"]);
5520  $Tasks[$Row["TaskId"]]["Callback"] = $WrappedCallback[1];
5521  $Tasks[$Row["TaskId"]]["Parameters"] = NULL;
5522  }
5523  else
5524  {
5525  $Tasks[$Row["TaskId"]]["Callback"] = unserialize($Row["Callback"]);
5526  $Tasks[$Row["TaskId"]]["Parameters"] = unserialize($Row["Parameters"]);
5527  }
5528  }
5529  return $Tasks;
5530  }
5531 
5535  private function RunNextTask()
5536  {
5537  # lock tables to prevent same task from being run by multiple sessions
5538  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
5539 
5540  # look for task at head of queue
5541  $this->DB->Query("SELECT * FROM TaskQueue ORDER BY Priority, TaskId LIMIT 1");
5542  $Task = $this->DB->FetchRow();
5543 
5544  # if there was a task available
5545  if ($Task)
5546  {
5547  # move task from queue to running tasks list
5548  $this->DB->Query("INSERT INTO RunningTasks "
5549  ."(TaskId,Callback,Parameters,Priority,Description) "
5550  ."SELECT * FROM TaskQueue WHERE TaskId = "
5551  .intval($Task["TaskId"]));
5552  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = "
5553  .intval($Task["TaskId"]));
5554 
5555  # release table locks to again allow other sessions to run tasks
5556  $this->DB->Query("UNLOCK TABLES");
5557 
5558  # unpack stored task info
5559  $Callback = unserialize($Task["Callback"]);
5560  $Parameters = unserialize($Task["Parameters"]);
5561 
5562  # attempt to load task callback if not already available
5563  $this->LoadFunction($Callback);
5564 
5565  # clear task requeue flag
5566  $this->RequeueCurrentTask = FALSE;
5567 
5568  # save amount of free memory for later comparison
5569  $BeforeFreeMem = self::GetFreeMemory();
5570 
5571  # run task
5572  $this->RunningTask = $Task;
5573  if ($Parameters)
5574  {
5575  call_user_func_array($Callback, $Parameters);
5576  }
5577  else
5578  {
5579  call_user_func($Callback);
5580  }
5581  unset($this->RunningTask);
5582 
5583  # log if task leaked significant memory
5584  if (function_exists("gc_collect_cycles")) { gc_collect_cycles(); }
5585  $AfterFreeMem = self::GetFreeMemory();
5586  $LeakThreshold = self::GetPhpMemoryLimit()
5587  * ($this->BackgroundTaskMemLeakLogThreshold / 100);
5588  if (($BeforeFreeMem - $AfterFreeMem) > $LeakThreshold)
5589  {
5590  $this->LogError(self::LOGLVL_DEBUG, "Task "
5591  .self::GetTaskCallbackSynopsis(
5592  $this->GetTask($Task["TaskId"]))." leaked "
5593  .number_format($BeforeFreeMem - $AfterFreeMem)." bytes.");
5594  }
5595 
5596  # if task requeue requested
5597  if ($this->RequeueCurrentTask)
5598  {
5599  # move task from running tasks list to queue
5600  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
5601  $this->DB->Query("INSERT INTO TaskQueue"
5602  ." (Callback,Parameters,Priority,Description)"
5603  ." SELECT Callback,Parameters,Priority,Description"
5604  ." FROM RunningTasks WHERE TaskId = "
5605  .intval($Task["TaskId"]));
5606  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = "
5607  .intval($Task["TaskId"]));
5608  $this->DB->Query("UNLOCK TABLES");
5609  }
5610  else
5611  {
5612  # remove task from running tasks list
5613  $this->DB->Query("DELETE FROM RunningTasks"
5614  ." WHERE TaskId = ".intval($Task["TaskId"]));
5615  }
5616 
5617  # prune running tasks list if necessary
5618  $RunningTasksCount = $this->DB->Query(
5619  "SELECT COUNT(*) AS TaskCount FROM RunningTasks", "TaskCount");
5620  if ($RunningTasksCount > $this->MaxRunningTasksToTrack)
5621  {
5622  $this->DB->Query("DELETE FROM RunningTasks ORDER BY StartedAt"
5623  ." LIMIT ".($RunningTasksCount - $this->MaxRunningTasksToTrack));
5624  }
5625  }
5626  else
5627  {
5628  # release table locks to again allow other sessions to run tasks
5629  $this->DB->Query("UNLOCK TABLES");
5630  }
5631  }
5632 
5638  public function OnCrash()
5639  {
5640  # attempt to remove any memory limits
5641  $FreeMemory = $this->GetFreeMemory();
5642  ini_set("memory_limit", -1);
5643 
5644  # if there is a background task currently running
5645  if (isset($this->RunningTask))
5646  {
5647  # add info about current page load
5648  $CrashInfo["ElapsedTime"] = $this->GetElapsedExecutionTime();
5649  $CrashInfo["FreeMemory"] = $FreeMemory;
5650  $CrashInfo["REMOTE_ADDR"] = $_SERVER["REMOTE_ADDR"];
5651  $CrashInfo["REQUEST_URI"] = $_SERVER["REQUEST_URI"];
5652  if (isset($_SERVER["REQUEST_TIME"]))
5653  {
5654  $CrashInfo["REQUEST_TIME"] = $_SERVER["REQUEST_TIME"];
5655  }
5656  if (isset($_SERVER["REMOTE_HOST"]))
5657  {
5658  $CrashInfo["REMOTE_HOST"] = $_SERVER["REMOTE_HOST"];
5659  }
5660 
5661  # add info about error that caused crash (if available)
5662  if (function_exists("error_get_last"))
5663  {
5664  $CrashInfo["LastError"] = error_get_last();
5665  }
5666 
5667  # add info about current output buffer contents (if available)
5668  if (ob_get_length() !== FALSE)
5669  {
5670  $CrashInfo["OutputBuffer"] = ob_get_contents();
5671  }
5672 
5673  # if backtrace info is available for the crash
5674  $Backtrace = debug_backtrace();
5675  if (count($Backtrace) > 1)
5676  {
5677  # discard the current context from the backtrace
5678  array_shift($Backtrace);
5679 
5680  # add the backtrace to the crash info
5681  $CrashInfo["Backtrace"] = $Backtrace;
5682  }
5683  # else if saved backtrace info is available
5684  elseif (isset($this->SavedContext))
5685  {
5686  # add the saved backtrace to the crash info
5687  $CrashInfo["Backtrace"] = $this->SavedContext;
5688  }
5689 
5690  # save crash info for currently running task
5691  $DB = new Database();
5692  $DB->Query("UPDATE RunningTasks SET CrashInfo = '"
5693  .addslashes(serialize($CrashInfo))
5694  ."' WHERE TaskId = ".intval($this->RunningTask["TaskId"]));
5695  }
5696 
5697  print("\n");
5698  return;
5699  }
5700 
5717  private function AddToDirList($DirList, $Dir, $SearchLast, $SkipSlashCheck)
5718  {
5719  # convert incoming directory to array of directories (if needed)
5720  $Dirs = is_array($Dir) ? $Dir : array($Dir);
5721 
5722  # reverse array so directories are searched in specified order
5723  $Dirs = array_reverse($Dirs);
5724 
5725  # for each directory
5726  foreach ($Dirs as $Location)
5727  {
5728  # make sure directory includes trailing slash
5729  if (!$SkipSlashCheck)
5730  {
5731  $Location = $Location
5732  .((substr($Location, -1) != "/") ? "/" : "");
5733  }
5734 
5735  # remove directory from list if already present
5736  if (in_array($Location, $DirList))
5737  {
5738  $DirList = array_diff(
5739  $DirList, array($Location));
5740  }
5741 
5742  # add directory to list of directories
5743  if ($SearchLast)
5744  {
5745  array_push($DirList, $Location);
5746  }
5747  else
5748  {
5749  array_unshift($DirList, $Location);
5750  }
5751  }
5752 
5753  # return updated directory list to caller
5754  return $DirList;
5755  }
5756 
5763  private function OutputModificationCallbackShell($Matches)
5764  {
5765  # call previously-stored external function
5766  return call_user_func($this->OutputModificationCallbackInfo["Callback"],
5767  $Matches,
5768  $this->OutputModificationCallbackInfo["Pattern"],
5769  $this->OutputModificationCallbackInfo["Page"],
5770  $this->OutputModificationCallbackInfo["SearchPattern"]);
5771  }
5772 
5781  private function CheckOutputModification($Original, $Modified, $ErrorInfo)
5782  {
5783  # if error was reported by regex engine
5784  if (preg_last_error() !== PREG_NO_ERROR)
5785  {
5786  # log error
5787  $this->LogError(self::LOGLVL_ERROR,
5788  "Error reported by regex engine when modifying output."
5789  ." (".$ErrorInfo.")");
5790 
5791  # use unmodified version of output
5792  $OutputToUse = $Original;
5793  }
5794  # else if modification reduced output by more than threshold
5795  elseif ((strlen(trim($Modified)) / strlen(trim($Original)))
5796  < self::OUTPUT_MODIFICATION_THRESHOLD)
5797  {
5798  # log error
5799  $this->LogError(self::LOGLVL_WARNING,
5800  "Content reduced below acceptable threshold while modifying output."
5801  ." (".$ErrorInfo.")");
5802 
5803  # use unmodified version of output
5804  $OutputToUse = $Original;
5805  }
5806  else
5807  {
5808  # use modified version of output
5809  $OutputToUse = $Modified;
5810  }
5811 
5812  # return output to use to caller
5813  return $OutputToUse;
5814  }
5815 
5817  const OUTPUT_MODIFICATION_THRESHOLD = 0.10;
5818 
5828  private function UpdateSetting(
5829  $FieldName, $NewValue = DB_NOVALUE, $Persistent = TRUE)
5830  {
5831  static $LocalSettings;
5832  if ($NewValue !== DB_NOVALUE)
5833  {
5834  if ($Persistent)
5835  {
5836  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5837  "ApplicationFrameworkSettings",
5838  $FieldName, $NewValue, NULL, $this->Settings);
5839  }
5840  else
5841  {
5842  $LocalSettings[$FieldName] = $NewValue;
5843  }
5844  }
5845  elseif (!isset($LocalSettings[$FieldName]))
5846  {
5847  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5848  "ApplicationFrameworkSettings",
5849  $FieldName, $NewValue, NULL, $this->Settings);
5850  }
5851  return $LocalSettings[$FieldName];
5852  }
5853 
5863  private static function IncludeFile($_AF_File, $_AF_ContextVars = array())
5864  {
5865  # set up context
5866  foreach ($_AF_ContextVars as $_AF_VarName => $_AF_VarValue)
5867  {
5868  $$_AF_VarName = $_AF_VarValue;
5869  }
5870  unset($_AF_VarName);
5871  unset($_AF_VarValue);
5872  unset($_AF_ContextVars);
5873 
5874  # add variables to context that we assume are always available
5875  $AF = $GLOBALS["AF"];
5876 
5877  # load file
5878  include($_AF_File);
5879 
5880  # return updated context
5881  $ContextVars = get_defined_vars();
5882  unset($ContextVars["_AF_File"]);
5883  return $ContextVars;
5884  }
5885 
5892  private function FilterContext($Context, $ContextVars)
5893  {
5894  # clear all variables if no setting for context is available
5895  # or setting is FALSE
5896  if (!isset($this->ContextFilters[$Context])
5897  || ($this->ContextFilters[$Context] == FALSE))
5898  {
5899  return array();
5900  }
5901  # keep all variables if setting for context is TRUE
5902  elseif ($this->ContextFilters[$Context] == TRUE)
5903  {
5904  return $ContextVars;
5905  }
5906  else
5907  {
5908  $Prefixes = $this->ContextFilters[$Context];
5909  $FilterFunc = function($VarName) use ($Prefixes) {
5910  foreach ($Prefixes as $Prefix)
5911  {
5912  if (substr($VarName, $Prefix) === 0)
5913  {
5914  return TRUE;
5915  }
5916  }
5917  return FALSE;
5918  };
5919  return array_filter(
5920  $ContextVars, $FilterFunc, ARRAY_FILTER_USE_KEY);
5921  }
5922  }
5923 
5925  private $InterfaceDirList = array(
5926  "interface/%ACTIVEUI%/",
5927  "interface/%DEFAULTUI%/",
5928  );
5933  private $IncludeDirList = array(
5934  "interface/%ACTIVEUI%/include/",
5935  "interface/%ACTIVEUI%/objects/",
5936  "interface/%DEFAULTUI%/include/",
5937  "interface/%DEFAULTUI%/objects/",
5938  );
5940  private $ImageDirList = array(
5941  "interface/%ACTIVEUI%/images/",
5942  "interface/%DEFAULTUI%/images/",
5943  );
5945  private $FunctionDirList = array(
5946  "interface/%ACTIVEUI%/include/",
5947  "interface/%DEFAULTUI%/include/",
5948  "include/",
5949  );
5950 
5951  const NOVALUE = ".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";
5952 
5953 
5954  # ---- Page Caching (Internal Methods) -----------------------------------
5955 
5961  private function CheckForCachedPage($PageName)
5962  {
5963  # assume no cached page will be found
5964  $CachedPage = NULL;
5965 
5966  # if returning a cached page is allowed
5967  if ($this->CacheCurrentPage)
5968  {
5969  # get fingerprint for requested page
5970  $PageFingerprint = $this->GetPageFingerprint($PageName);
5971 
5972  # look for matching page in cache in database
5973  $this->DB->Query("SELECT * FROM AF_CachedPages"
5974  ." WHERE Fingerprint = '".addslashes($PageFingerprint)."'");
5975 
5976  # if matching page found
5977  if ($this->DB->NumRowsSelected())
5978  {
5979  # if cached page has expired
5980  $Row = $this->DB->FetchRow();
5981  $ExpirationTime = strtotime(
5982  "-".$this->PageCacheExpirationPeriod()." seconds");
5983  if (strtotime($Row["CachedAt"]) < $ExpirationTime)
5984  {
5985  # clear expired pages from cache
5986  $ExpirationTimestamp = date("Y-m-d H:i:s", $ExpirationTime);
5987  $this->DB->Query("DELETE CP, CPTI FROM AF_CachedPages CP,"
5988  ." AF_CachedPageTagInts CPTI"
5989  ." WHERE CP.CachedAt < '".$ExpirationTimestamp."'"
5990  ." AND CPTI.CacheId = CP.CacheId");
5991  $this->DB->Query("DELETE FROM AF_CachedPages "
5992  ." WHERE CachedAt < '".$ExpirationTimestamp."'");
5993  }
5994  else
5995  {
5996  # display cached page and exit
5997  $CachedPage = $Row["PageContent"];
5998  }
5999  }
6000  }
6001 
6002  # return any cached page found to caller
6003  return $CachedPage;
6004  }
6005 
6011  private function UpdatePageCache($PageName, $PageContent)
6012  {
6013  # if page caching is enabled and current page should be cached
6014  if ($this->PageCacheEnabled()
6015  && $this->CacheCurrentPage
6016  && ($PageName != "404"))
6017  {
6018  # if page content looks invalid
6019  if (strlen(trim(strip_tags($PageContent))) == 0)
6020  {
6021  # log error
6022  $LogMsg = "Page not cached because content was empty."
6023  ." (PAGE: ".$PageName.", URL: ".$this->FullUrl().")";
6024  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
6025  }
6026  else
6027  {
6028  # save page to cache
6029  $PageFingerprint = $this->GetPageFingerprint($PageName);
6030  $this->DB->Query("INSERT INTO AF_CachedPages"
6031  ." (Fingerprint, PageContent) VALUES"
6032  ." ('".$this->DB->EscapeString($PageFingerprint)."', '"
6033  .$this->DB->EscapeString($PageContent)."')");
6034  $CacheId = $this->DB->LastInsertId();
6035 
6036  # for each page cache tag that was added
6037  foreach ($this->PageCacheTags as $Tag => $Pages)
6038  {
6039  # if current page is in list for tag
6040  if (in_array("CURRENT", $Pages) || in_array($PageName, $Pages))
6041  {
6042  # look up tag ID
6043  $TagId = $this->GetPageCacheTagId($Tag);
6044 
6045  # mark current page as associated with tag
6046  $this->DB->Query("INSERT INTO AF_CachedPageTagInts"
6047  ." (TagId, CacheId) VALUES "
6048  ." (".intval($TagId).", ".intval($CacheId).")");
6049  }
6050  }
6051  }
6052  }
6053  }
6054 
6060  private function GetPageCacheTagId($Tag)
6061  {
6062  # if tag is a non-negative integer
6063  if (is_numeric($Tag) && ($Tag > 0) && (intval($Tag) == $Tag))
6064  {
6065  # generate ID
6066  $Id = self::PAGECACHETAGIDOFFSET + $Tag;
6067  }
6068  else
6069  {
6070  # look up ID in database
6071  $Id = $this->DB->Query("SELECT TagId FROM AF_CachedPageTags"
6072  ." WHERE Tag = '".addslashes($Tag)."'", "TagId");
6073 
6074  # if ID was not found
6075  if ($Id === NULL)
6076  {
6077  # add tag to database
6078  $this->DB->Query("INSERT INTO AF_CachedPageTags"
6079  ." SET Tag = '".addslashes($Tag)."'");
6080  $Id = $this->DB->LastInsertId();
6081  }
6082  }
6083 
6084  # return tag ID to caller
6085  return $Id;
6086  }
6087 
6093  private function GetPageFingerprint($PageName)
6094  {
6095  # only get the environmental fingerprint once so that it is consistent
6096  # between page construction start and end
6097  static $EnvFingerprint;
6098  if (!isset($EnvFingerprint))
6099  {
6100  $EnvData = json_encode($_GET).json_encode($_POST);
6101 
6102  # if alternate domain support is enabled
6103  if ($this->HtaccessSupport() && self::$RootUrlOverride !== NULL)
6104  {
6105  # and if we were accessed via an alternate domain
6106  $VHost = $_SERVER["SERVER_NAME"];
6107  if (isset($this->AlternateDomainPrefixes[$VHost]))
6108  {
6109  # then add the alternate domain that was used to our
6110  # environment data
6111  $EnvData .= $VHost;
6112  }
6113  }
6114 
6115  $EnvFingerprint = md5($EnvData);
6116 
6117  }
6118 
6119  # build page fingerprint and return it to caller
6120  return $PageName."-".$EnvFingerprint;
6121  }
6122 }
Abstraction for forum messages and resource comments.
Definition: Message.php:14
SQL database abstraction object with smart query caching.
Definition: Database.php:22
static SortCompare($A, $B)
Perform compare and return value appropriate for sort function callbacks.
Definition: StdLib.php:466
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
static ArrayPermutations($Items, $Perms=array())
Return all possible permutations of a given array.
Definition: StdLib.php:615
const DB_NOVALUE
Definition: Database.php:1675
const SQL_DATE_FORMAT
Format to feed to date() to get SQL-compatible date/time string.
Definition: StdLib.php:701