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  # limit cookie to secure connection if we are running over same
106  $SecureCookie = isset($_SERVER["HTTPS"]) ? TRUE : FALSE;
107 
108  # Cookies lacking embedded dots are... fun.
109  # rfc2109 sec 4.3.2 says to reject them
110  # rfc2965 sec 3.3.2 says to reject them
111  # rfc6265 sec 4.1.2.3 says only that "public suffixes"
112  # should be rejected. They reference Mozilla's
113  # publicsuffix.org, which does not contain 'localhost'.
114  # However, empirically in early 2017 Firefox still rejects
115  # 'localhost'.
116  # Therefore, don't set a cookie domain if we're running on
117  # localhost to avoid this problem.
118  if (!preg_match('/^\.localhost(:[0-9]+)?$/', $SessionDomain))
119  {
120  $SessionDomain = "";
121  }
122  session_set_cookie_params(self::$SessionLifetime, "/",
123  $SessionDomain, $SecureCookie, TRUE);
124 
125  # attempt to start session
126  $SessionStarted = @session_start();
127 
128  # if session start failed
129  if (!$SessionStarted)
130  {
131  # regenerate session ID and attempt to start session again
132  session_regenerate_id(TRUE);
133  session_start();
134  }
135  }
136 
137  # set up our internal environment
138  $this->DB = new Database();
139 
140  # set up our exception handler
141  set_exception_handler(array($this, "GlobalExceptionHandler"));
142 
143  # load our settings from database
144  $this->LoadSettings();
145 
146  # set PHP maximum execution time
147  ini_set("max_execution_time", $this->Settings["MaxExecTime"]);
148  set_time_limit($this->Settings["MaxExecTime"]);
149 
150  # register events we handle internally
151  $this->RegisterEvent($this->PeriodicEvents);
152  $this->RegisterEvent($this->UIEvents);
153 
154  # attempt to create SCSS cache directory if needed and it does not exist
155  if ($this->ScssSupportEnabled() && !is_dir(self::$ScssCacheDir))
156  { @mkdir(self::$ScssCacheDir, 0777, TRUE); }
157 
158  # attempt to create minimized JS cache directory if needed and it does not exist
159  if ($this->UseMinimizedJavascript()
160  && $this->JavascriptMinimizationEnabled()
161  && !is_dir(self::$JSMinCacheDir))
162  {
163  @mkdir(self::$JSMinCacheDir, 0777, TRUE);
164  }
165  }
172  public function __destruct()
173  {
174  # if template location cache is flagged to be saved
175  if ($this->SaveTemplateLocationCache)
176  {
177  # write template location cache out and update cache expiration
178  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
179  ." SET TemplateLocationCache = '"
180  .addslashes(serialize(
181  $this->TemplateLocationCache))."',"
182  ." TemplateLocationCacheExpiration = '"
183  .date("Y-m-d H:i:s",
184  $this->TemplateLocationCacheExpiration)."'");
185  }
186 
187  # if object location cache is flagged to be saved
188  if (self::$SaveObjectLocationCache)
189  {
190  # write object location cache out and update cache expiration
191  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
192  ." SET ObjectLocationCache = '"
193  .addslashes(serialize(
194  self::$ObjectLocationCache))."',"
195  ." ObjectLocationCacheExpiration = '"
196  .date("Y-m-d H:i:s",
197  self::$ObjectLocationCacheExpiration)."'");
198  }
199  }
207  public function GlobalExceptionHandler($Exception)
208  {
209  # display exception info
210  $Message = $Exception->getMessage();
211  $Location = str_replace(getcwd()."/", "",
212  $Exception->getFile()."[".$Exception->getLine()."]");
213  $Trace = preg_replace(":(#[0-9]+) ".getcwd()."/".":", "$1 ",
214  $Exception->getTraceAsString());
215  if (php_sapi_name() == "cli")
216  {
217  print "Uncaught Exception\n"
218  ."Message: ".$Message."\n"
219  ."Location: ".$Location."\n"
220  ."Trace: \n"
221  .$Trace."\n";
222  }
223  else
224  {
225  ?><table width="100%" cellpadding="5"
226  style="border: 2px solid #666666; background: #CCCCCC;
227  font-family: Courier New, Courier, monospace;
228  margin-top: 10px;"><tr><td>
229  <div style="color: #666666;">
230  <span style="font-size: 150%;">
231  <b>Uncaught Exception</b></span><br />
232  <b>Message:</b> <i><?= $Message ?></i><br />
233  <b>Location:</b> <i><?= $Location ?></i><br />
234  <b>Trace:</b> <blockquote><pre><?= $Trace ?></pre></blockquote>
235  </div>
236  </td></tr></table><?PHP
237  }
238 
239  # log exception if not running from command line
240  if (php_sapi_name() !== "cli")
241  {
242  $TraceString = $Exception->getTraceAsString();
243  $TraceString = str_replace("\n", ", ", $TraceString);
244  $TraceString = preg_replace(":(#[0-9]+) ".getcwd()."/".":",
245  "$1 ", $TraceString);
246  $LogMsg = "Uncaught exception (".$Exception->getMessage().")"
247  ." at ".$Location."."
248  ." TRACE: ".$TraceString
249  ." URL: ".$this->FullUrl();
250  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
251  }
252  }
269  public static function AddObjectDirectory(
270  $Dir, $Prefix = "", $ClassPattern = NULL, $ClassReplacement = NULL)
271  {
272  # make sure directory has trailing slash
273  $Dir = $Dir.((substr($Dir, -1) != "/") ? "/" : "");
274 
275  # add directory to directory list
276  self::$ObjectDirectories[$Dir] = array(
277  "Prefix" => $Prefix,
278  "ClassPattern" => $ClassPattern,
279  "ClassReplacement" => $ClassReplacement,
280  );
281  }
282 
302  public function AddImageDirectories(
303  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
304  {
305  # add directories to existing image directory list
306  $this->ImageDirList = $this->AddToDirList(
307  $this->ImageDirList, $Dir, $SearchLast, $SkipSlashCheck);
308  }
309 
330  public function AddIncludeDirectories(
331  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
332  {
333  # add directories to existing image directory list
334  $this->IncludeDirList = $this->AddToDirList(
335  $this->IncludeDirList, $Dir, $SearchLast, $SkipSlashCheck);
336  }
337 
357  public function AddInterfaceDirectories(
358  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
359  {
360  # add directories to existing image directory list
361  $this->InterfaceDirList = $this->AddToDirList(
362  $this->InterfaceDirList, $Dir, $SearchLast, $SkipSlashCheck);
363  }
364 
384  public function AddFunctionDirectories(
385  $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
386  {
387  # add directories to existing image directory list
388  $this->FunctionDirList = $this->AddToDirList(
389  $this->FunctionDirList, $Dir, $SearchLast, $SkipSlashCheck);
390  }
391 
397  public function SetBrowserDetectionFunc($DetectionFunc)
398  {
399  $this->BrowserDetectFunc = $DetectionFunc;
400  }
401 
408  public function AddUnbufferedCallback($Callback, $Parameters=array())
409  {
410  if (is_callable($Callback))
411  {
412  $this->UnbufferedCallbacks[] = array($Callback, $Parameters);
413  }
414  }
415 
422  public function TemplateLocationCacheExpirationInterval($NewInterval = DB_NOVALUE)
423  {
424  return $this->UpdateSetting("TemplateLocationCacheInterval", $NewInterval);
425  }
426 
430  public function ClearTemplateLocationCache()
431  {
432  $this->TemplateLocationCache = array();
433  $this->SaveTemplateLocationCache = TRUE;
434  }
435 
442  public function ObjectLocationCacheExpirationInterval($NewInterval = DB_NOVALUE)
443  {
444  return $this->UpdateSetting("ObjectLocationCacheInterval", $NewInterval);
445  }
446 
450  public function ClearObjectLocationCache()
451  {
452  self::$ObjectLocationCache = array();
453  self::$SaveObjectLocationCache = TRUE;
454  }
455 
462  public function UrlFingerprintingEnabled($NewValue = DB_NOVALUE)
463  {
464  return $this->UpdateSetting("UrlFingerprintingEnabled", $NewValue);
465  }
466 
474  public function ScssSupportEnabled($NewValue = DB_NOVALUE)
475  {
476  return $this->UpdateSetting("ScssSupportEnabled", $NewValue);
477  }
478 
487  public function GenerateCompactCss($NewValue = DB_NOVALUE)
488  {
489  return $this->UpdateSetting("GenerateCompactCss", $NewValue);
490  }
491 
500  public function UseMinimizedJavascript($NewValue = DB_NOVALUE)
501  {
502  return $this->UpdateSetting("UseMinimizedJavascript", $NewValue);
503  }
504 
513  public function JavascriptMinimizationEnabled($NewValue = DB_NOVALUE)
514  {
515  return $this->UpdateSetting("JavascriptMinimizationEnabled", $NewValue);
516  }
517 
531  public function RecordContextInCaseOfCrash(
532  $BacktraceOptions = 0, $BacktraceLimit = 0)
533  {
534  if (version_compare(PHP_VERSION, "5.4.0", ">="))
535  {
536  $this->SavedContext = debug_backtrace(
537  $BacktraceOptions, $BacktraceLimit);
538  }
539  else
540  {
541  $this->SavedContext = debug_backtrace($BacktraceOptions);
542  }
543  array_shift($this->SavedContext);
544  }
545 
550  public function LoadPage($PageName)
551  {
552  # perform any clean URL rewriting
553  $PageName = $this->RewriteCleanUrls($PageName);
554 
555  # sanitize incoming page name and save local copy
556  $PageName = preg_replace("/[^a-zA-Z0-9_.-]/", "", $PageName);
557  $this->PageName = $PageName;
558 
559  # if page caching is turned on
560  if ($this->PageCacheEnabled())
561  {
562  # if we have a cached page
563  $CachedPage = $this->CheckForCachedPage($PageName);
564  if ($CachedPage !== NULL)
565  {
566  # set header to indicate cache hit was found
567  header("X-ScoutAF-Cache: HIT");
568 
569  # display cached page and exit
570  print $CachedPage;
571  return;
572  }
573  else
574  {
575  # set header to indicate no cache hit was found
576  header("X-ScoutAF-Cache: MISS");
577  }
578  }
579 
580  # buffer any output from includes or PHP file
581  ob_start();
582 
583  # include any files needed to set up execution environment
584  $IncludeFileContext = array();
585  foreach ($this->EnvIncludes as $IncludeFile)
586  {
587  $IncludeFileContext = $this->FilterContext(self::CONTEXT_ENV,
588  self::IncludeFile($IncludeFile, $IncludeFileContext));
589  }
590 
591  # signal page load
592  $this->SignalEvent("EVENT_PAGE_LOAD", array("PageName" => $PageName));
593 
594  # signal PHP file load
595  $SignalResult = $this->SignalEvent("EVENT_PHP_FILE_LOAD", array(
596  "PageName" => $PageName));
597 
598  # if signal handler returned new page name value
599  $NewPageName = $PageName;
600  if (($SignalResult["PageName"] != $PageName)
601  && strlen($SignalResult["PageName"]))
602  {
603  # if new page name value is page file
604  if (file_exists($SignalResult["PageName"]))
605  {
606  # use new value for PHP file name
607  $PageFile = $SignalResult["PageName"];
608  }
609  else
610  {
611  # use new value for page name
612  $NewPageName = $SignalResult["PageName"];
613  }
614 
615  # update local copy of page name
616  $this->PageName = $NewPageName;
617  }
618 
619  # if we do not already have a PHP file
620  if (!isset($PageFile))
621  {
622  # look for PHP file for page
623  $OurPageFile = "pages/".$NewPageName.".php";
624  $LocalPageFile = "local/pages/".$NewPageName.".php";
625  $PageFile = file_exists($LocalPageFile) ? $LocalPageFile
626  : (file_exists($OurPageFile) ? $OurPageFile
627  : "pages/".$this->DefaultPage.".php");
628  }
629 
630  # load PHP file
631  $IncludeFileContext = $this->FilterContext(self::CONTEXT_PAGE,
632  self::IncludeFile($PageFile, $IncludeFileContext));
633 
634  # save buffered output to be displayed later after HTML file loads
635  $PageOutput = ob_get_contents();
636  ob_end_clean();
637 
638  # signal PHP file load is complete
639  ob_start();
640  $Context["Variables"] = $IncludeFileContext;
641  $this->SignalEvent("EVENT_PHP_FILE_LOAD_COMPLETE",
642  array("PageName" => $PageName, "Context" => $Context));
643  $PageCompleteOutput = ob_get_contents();
644  ob_end_clean();
645 
646  # set up for possible TSR (Terminate and Stay Resident :))
647  $ShouldTSR = $this->PrepForTSR();
648 
649  # if PHP file indicated we should autorefresh to somewhere else
650  if (($this->JumpToPage) && ($this->JumpToPageDelay == 0))
651  {
652  if (!strlen(trim($PageOutput)))
653  {
654  # if client supports HTTP/1.1, use a 303 as it is most accurate
655  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.1")
656  {
657  header("HTTP/1.1 303 See Other");
658  header("Location: ".$this->JumpToPage);
659  }
660  else
661  {
662  # if the request was an HTTP/1.0 GET or HEAD, then
663  # use a 302 response code.
664 
665  # NB: both RFC 2616 (HTTP/1.1) and RFC1945 (HTTP/1.0)
666  # explicitly prohibit automatic redirection via a 302
667  # if the request was not GET or HEAD.
668  if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.0" &&
669  ($_SERVER["REQUEST_METHOD"] == "GET" ||
670  $_SERVER["REQUEST_METHOD"] == "HEAD") )
671  {
672  header("HTTP/1.0 302 Found");
673  header("Location: ".$this->JumpToPage);
674  }
675 
676  # otherwise, fall back to a meta refresh
677  else
678  {
679  print '<html><head><meta http-equiv="refresh" '
680  .'content="0; URL='.$this->JumpToPage.'">'
681  .'</head><body></body></html>';
682  }
683  }
684  }
685  }
686  # else if HTML loading is not suppressed
687  elseif (!$this->SuppressHTML)
688  {
689  # set content-type to get rid of diacritic errors
690  header("Content-Type: text/html; charset="
691  .$this->HtmlCharset, TRUE);
692 
693  # load common HTML file (defines common functions) if available
694  $CommonHtmlFile = $this->FindFile($this->IncludeDirList,
695  "Common", array("tpl", "html"));
696  if ($CommonHtmlFile)
697  {
698  $IncludeFileContext = $this->FilterContext(self::CONTEXT_COMMON,
699  self::IncludeFile($CommonHtmlFile, $IncludeFileContext));
700  }
701 
702  # load UI functions
703  $this->LoadUIFunctions();
704 
705  # begin buffering content
706  ob_start();
707 
708  # signal HTML file load
709  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD", array(
710  "PageName" => $PageName));
711 
712  # if signal handler returned new page name value
713  $NewPageName = $PageName;
714  $PageContentFile = NULL;
715  if (($SignalResult["PageName"] != $PageName)
716  && strlen($SignalResult["PageName"]))
717  {
718  # if new page name value is HTML file
719  if (file_exists($SignalResult["PageName"]))
720  {
721  # use new value for HTML file name
722  $PageContentFile = $SignalResult["PageName"];
723  }
724  else
725  {
726  # use new value for page name
727  $NewPageName = $SignalResult["PageName"];
728  }
729  }
730 
731  # load page content HTML file if available
732  if ($PageContentFile === NULL)
733  {
734  $PageContentFile = $this->FindFile(
735  $this->InterfaceDirList, $NewPageName,
736  array("tpl", "html"));
737  }
738  if ($PageContentFile)
739  {
740  $IncludeFileContext = $this->FilterContext(self::CONTEXT_INTERFACE,
741  self::IncludeFile($PageContentFile, $IncludeFileContext));
742  }
743  else
744  {
745  print "<h2>ERROR: No HTML/TPL template found"
746  ." for this page (".$NewPageName.").</h2>";
747  }
748 
749  # signal HTML file load complete
750  $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD_COMPLETE");
751 
752  # stop buffering and save output
753  $PageContentOutput = ob_get_contents();
754  ob_end_clean();
755 
756  # if standard page start/end have not been suppressed
757  $PageStartOutput = "";
758  $PageEndOutput = "";
759  if (!$this->SuppressStdPageStartAndEnd)
760  {
761  # load page start HTML file if available
762  $PageStartFile = $this->FindFile($this->IncludeDirList, "Start",
763  array("tpl", "html"), array("StdPage", "StandardPage"));
764  if ($PageStartFile)
765  {
766  ob_start();
767  $IncludeFileContext = self::IncludeFile(
768  $PageStartFile, $IncludeFileContext);
769  $PageStartOutput = ob_get_contents();
770  ob_end_clean();
771  }
772  $IncludeFileContext = $this->FilterContext(
773  self::CONTEXT_START, $IncludeFileContext);
774 
775  # load page end HTML file if available
776  $PageEndFile = $this->FindFile($this->IncludeDirList, "End",
777  array("tpl", "html"), array("StdPage", "StandardPage"));
778  if ($PageEndFile)
779  {
780  ob_start();
781  self::IncludeFile($PageEndFile, $IncludeFileContext);
782  $PageEndOutput = ob_get_contents();
783  ob_end_clean();
784  }
785  }
786 
787  # clear include file context because it may be large and is no longer needed
788  unset($IncludeFileContext);
789 
790  # if page auto-refresh requested
791  if ($this->JumpToPage)
792  {
793  # add auto-refresh tag to page
794  $this->AddMetaTag([
795  "http-equiv" => "refresh",
796  "content" => $this->JumpToPageDelay,
797  "url" => $this->JumpToPage,
798  ]);
799  }
800 
801  # assemble full page
802  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
803 
804  # get list of any required files not loaded
805  $RequiredFiles = $this->GetRequiredFilesNotYetLoaded($PageContentFile);
806 
807  # add file loading tags to page
808  $FullPageOutput = $this->AddFileTagsToPageOutput(
809  $FullPageOutput, $RequiredFiles);
810 
811  # add any requested meta tags to page
812  $FullPageOutput = $this->AddMetaTagsToPageOutput($FullPageOutput);
813 
814  # perform any regular expression replacements in output
815  $NewFullPageOutput = preg_replace($this->OutputModificationPatterns,
816  $this->OutputModificationReplacements, $FullPageOutput);
817 
818  # check to make sure replacements didn't fail
819  $FullPageOutput = $this->CheckOutputModification(
820  $FullPageOutput, $NewFullPageOutput,
821  "regular expression replacements");
822 
823  # for each registered output modification callback
824  foreach ($this->OutputModificationCallbacks as $Info)
825  {
826  # set up data for callback
827  $this->OutputModificationCallbackInfo = $Info;
828 
829  # perform output modification
830  $NewFullPageOutput = preg_replace_callback($Info["SearchPattern"],
831  array($this, "OutputModificationCallbackShell"),
832  $FullPageOutput);
833 
834  # check to make sure modification didn't fail
835  $ErrorInfo = "callback info: ".print_r($Info, TRUE);
836  $FullPageOutput = $this->CheckOutputModification(
837  $FullPageOutput, $NewFullPageOutput, $ErrorInfo);
838  }
839 
840  # provide the opportunity to modify full page output
841  $SignalResult = $this->SignalEvent("EVENT_PAGE_OUTPUT_FILTER", array(
842  "PageOutput" => $FullPageOutput));
843  if (isset($SignalResult["PageOutput"])
844  && strlen(trim($SignalResult["PageOutput"])))
845  {
846  $FullPageOutput = $SignalResult["PageOutput"];
847  }
848 
849  # if relative paths may not work because we were invoked via clean URL
850  if ($this->CleanUrlRewritePerformed || self::WasUrlRewritten())
851  {
852  # if using the <base> tag is okay
853  $BaseUrl = $this->BaseUrl();
854  if ($this->UseBaseTag)
855  {
856  # add <base> tag to header
857  $PageStartOutput = str_replace("<head>",
858  "<head><base href=\"".$BaseUrl."\" />",
859  $PageStartOutput);
860 
861  # re-assemble full page with new header
862  $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
863 
864  # the absolute URL to the current page
865  $FullUrl = $BaseUrl . $this->GetPageLocation();
866 
867  # make HREF attribute values with just a fragment ID
868  # absolute since they don't work with the <base> tag because
869  # they are relative to the current page/URL, not the site
870  # root
871  $NewFullPageOutput = preg_replace(
872  array("%href=\"(#[^:\" ]+)\"%i", "%href='(#[^:' ]+)'%i"),
873  array("href=\"".$FullUrl."$1\"", "href='".$FullUrl."$1'"),
874  $FullPageOutput);
875 
876  # check to make sure HREF cleanup didn't fail
877  $FullPageOutput = $this->CheckOutputModification(
878  $FullPageOutput, $NewFullPageOutput,
879  "HREF cleanup");
880  }
881  else
882  {
883  # try to fix any relative paths throughout code
884  $SrcFileExtensions = "(js|css|gif|png|jpg|svg|ico)";
885  $RelativePathPatterns = array(
886  "%src=\"/?([^?*:;{}\\\\\" ]+)\.".$SrcFileExtensions."\"%i",
887  "%src='/?([^?*:;{}\\\\' ]+)\.".$SrcFileExtensions."'%i",
888  # don't rewrite HREF attributes that are just
889  # fragment IDs because they are relative to the
890  # current page/URL, not the site root
891  "%href=\"/?([^#][^:\" ]*)\"%i",
892  "%href='/?([^#][^:' ]*)'%i",
893  "%action=\"/?([^#][^:\" ]*)\"%i",
894  "%action='/?([^#][^:' ]*)'%i",
895  "%@import\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
896  "%@import\s+url\('/?([^:\" ]+)'\s*\)%i",
897  "%src:\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
898  "%src:\s+url\('/?([^:\" ]+)'\s*\)%i",
899  "%@import\s+\"/?([^:\" ]+)\"\s*%i",
900  "%@import\s+'/?([^:\" ]+)'\s*%i",
901  );
902  $RelativePathReplacements = array(
903  "src=\"".$BaseUrl."$1.$2\"",
904  "src=\"".$BaseUrl."$1.$2\"",
905  "href=\"".$BaseUrl."$1\"",
906  "href=\"".$BaseUrl."$1\"",
907  "action=\"".$BaseUrl."$1\"",
908  "action=\"".$BaseUrl."$1\"",
909  "@import url(\"".$BaseUrl."$1\")",
910  "@import url('".$BaseUrl."$1')",
911  "src: url(\"".$BaseUrl."$1\")",
912  "src: url('".$BaseUrl."$1')",
913  "@import \"".$BaseUrl."$1\"",
914  "@import '".$BaseUrl."$1'",
915  );
916  $NewFullPageOutput = preg_replace($RelativePathPatterns,
917  $RelativePathReplacements, $FullPageOutput);
918 
919  # check to make sure relative path fixes didn't fail
920  $FullPageOutput = $this->CheckOutputModification(
921  $FullPageOutput, $NewFullPageOutput,
922  "relative path fixes");
923  }
924  }
925 
926  # handle any necessary alternate domain rewriting
927  $FullPageOutput = $this->RewriteAlternateDomainUrls($FullPageOutput);
928 
929  # update page cache for this page
930  $this->UpdatePageCache($PageName, $FullPageOutput);
931 
932  # write out full page
933  print $FullPageOutput;
934  }
935 
936  # run any post-processing routines
937  foreach ($this->PostProcessingFuncs as $Func)
938  {
939  call_user_func_array($Func["FunctionName"], $Func["Arguments"]);
940  }
941 
942  # write out any output buffered from page code execution
943  if (strlen($PageOutput))
944  {
945  if (!$this->SuppressHTML)
946  {
947  ?><table width="100%" cellpadding="5"
948  style="border: 2px solid #666666; background: #CCCCCC;
949  font-family: Courier New, Courier, monospace;
950  margin-top: 10px;"><tr><td><?PHP
951  }
952  if ($this->JumpToPage)
953  {
954  ?><div style="color: #666666;"><span style="font-size: 150%;">
955  <b>Page Jump Aborted</b></span>
956  (because of error or other unexpected output)<br />
957  <b>Jump Target:</b>
958  <i><?PHP print($this->JumpToPage); ?></i></div><?PHP
959  }
960  print $PageOutput;
961  if (!$this->SuppressHTML)
962  {
963  ?></td></tr></table><?PHP
964  }
965  }
966 
967  # write out any output buffered from the page code execution complete signal
968  if (!$this->JumpToPage && !$this->SuppressHTML && strlen($PageCompleteOutput))
969  {
970  print $PageCompleteOutput;
971  }
972 
973  # log slow page loads
974  if ($this->LogSlowPageLoads()
975  && !$this->DoNotLogSlowPageLoad
976  && ($this->GetElapsedExecutionTime()
977  >= ($this->SlowPageLoadThreshold())))
978  {
979  $RemoteHost = gethostbyaddr($_SERVER["REMOTE_ADDR"]);
980  if ($RemoteHost === FALSE)
981  {
982  $RemoteHost = $_SERVER["REMOTE_ADDR"];
983  }
984  elseif ($RemoteHost != $_SERVER["REMOTE_ADDR"])
985  {
986  $RemoteHost .= " (".$_SERVER["REMOTE_ADDR"].")";
987  }
988  $SlowPageLoadMsg = "Slow page load ("
989  .intval($this->GetElapsedExecutionTime())."s) for "
990  .$this->FullUrl()." from ".$RemoteHost;
991  $this->LogMessage(self::LOGLVL_INFO, $SlowPageLoadMsg);
992  }
993 
994  # execute callbacks that should not have their output buffered
995  foreach ($this->UnbufferedCallbacks as $Callback)
996  {
997  call_user_func_array($Callback[0], $Callback[1]);
998  }
999 
1000  # log high memory usage
1001  if (function_exists("memory_get_peak_usage"))
1002  {
1003  $MemoryThreshold = ($this->HighMemoryUsageThreshold()
1004  * $this->GetPhpMemoryLimit()) / 100;
1005  if ($this->LogHighMemoryUsage()
1006  && (memory_get_peak_usage(TRUE) >= $MemoryThreshold))
1007  {
1008  $HighMemUsageMsg = "High peak memory usage ("
1009  .number_format(memory_get_peak_usage(TRUE)).") for "
1010  .$this->FullUrl()." from "
1011  .$_SERVER["REMOTE_ADDR"];
1012  $this->LogMessage(self::LOGLVL_INFO, $HighMemUsageMsg);
1013  }
1014  }
1015 
1016  # terminate and stay resident (TSR!) if indicated and HTML has been output
1017  # (only TSR if HTML has been output because otherwise browsers will misbehave)
1018  if ($ShouldTSR) { $this->LaunchTSR(); }
1019  }
1020 
1026  public function GetPageName()
1027  {
1028  return $this->PageName;
1029  }
1030 
1036  public function GetPageLocation()
1037  {
1038  # retrieve current URL
1039  $Url = self::GetScriptUrl();
1040 
1041  # remove the base path if present
1042  $BasePath = $this->Settings["BasePath"];
1043  if (stripos($Url, $BasePath) === 0)
1044  {
1045  $Url = substr($Url, strlen($BasePath));
1046  }
1047 
1048  # if we're being accessed via an alternate domain,
1049  # add the appropriate prefix in
1050  if ($this->HtaccessSupport() &&
1051  self::$RootUrlOverride !== NULL)
1052  {
1053  $VHost = $_SERVER["SERVER_NAME"];
1054  if (isset($this->AlternateDomainPrefixes[$VHost]))
1055  {
1056  $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
1057  $Url = $ThisPrefix."/".$Url;
1058  }
1059  }
1060 
1061  return $Url;
1062  }
1063 
1069  public function GetPageUrl()
1070  {
1071  return self::BaseUrl() . $this->GetPageLocation();
1072  }
1073 
1085  public function SetJumpToPage($Page, $Delay = 0, $IsLiteral = FALSE)
1086  {
1087  if (!is_null($Page)
1088  && (!$IsLiteral)
1089  && (strpos($Page, "?") === FALSE)
1090  && ((strpos($Page, "=") !== FALSE)
1091  || ((stripos($Page, ".php") === FALSE)
1092  && (stripos($Page, ".htm") === FALSE)
1093  && (strpos($Page, "/") === FALSE)))
1094  && (stripos($Page, "http://") !== 0)
1095  && (stripos($Page, "https://") !== 0))
1096  {
1097  $this->JumpToPage = self::BaseUrl() . "index.php?P=".$Page;
1098  }
1099  else
1100  {
1101  $this->JumpToPage = $Page;
1102  }
1103  $this->JumpToPageDelay = $Delay;
1104  }
1105 
1110  public function JumpToPageIsSet()
1111  {
1112  return ($this->JumpToPage === NULL) ? FALSE : TRUE;
1113  }
1114 
1124  public function HtmlCharset($NewSetting = NULL)
1125  {
1126  if ($NewSetting !== NULL) { $this->HtmlCharset = $NewSetting; }
1127  return $this->HtmlCharset;
1128  }
1129 
1139  public function DoNotMinimizeFile($File)
1140  {
1141  if (!is_array($File)) { $File = array($File); }
1142  $this->DoNotMinimizeList = array_merge($this->DoNotMinimizeList, $File);
1143  }
1144 
1155  public function UseBaseTag($NewValue = NULL)
1156  {
1157  if ($NewValue !== NULL) { $this->UseBaseTag = $NewValue ? TRUE : FALSE; }
1158  return $this->UseBaseTag;
1159  }
1160 
1168  public function SuppressHTMLOutput($NewSetting = TRUE)
1169  {
1170  $this->SuppressHTML = $NewSetting;
1171  }
1172 
1180  public function SuppressStandardPageStartAndEnd($NewSetting = TRUE)
1181  {
1182  $this->SuppressStdPageStartAndEnd = $NewSetting;
1183  }
1184 
1190  public static function DefaultUserInterface($UIName = NULL)
1191  {
1192  if ($UIName !== NULL)
1193  {
1194  self::$DefaultUI = $UIName;
1195  }
1196  return self::$DefaultUI;
1197  }
1198 
1205  public static function ActiveUserInterface($UIName = NULL)
1206  {
1207  if ($UIName !== NULL)
1208  {
1209  self::$ActiveUI = preg_replace("/^SPTUI--/", "", $UIName);
1210  }
1211  return self::$ActiveUI;
1212  }
1213 
1224  public function GetUserInterfaces($FilterExp = NULL)
1225  {
1226  static $Interfaces;
1227 
1228  if (!isset($Interfaces[$FilterExp]))
1229  {
1230  # retrieve paths to user interface directories
1231  $Paths = $this->GetUserInterfacePaths($FilterExp);
1232 
1233  # start out with an empty list
1234  $Interfaces[$FilterExp] = array();
1235 
1236  # for each possible UI directory
1237  foreach ($Paths as $CanonicalName => $Path)
1238  {
1239  # if name file available
1240  $LabelFile = $Path."/NAME";
1241  if (is_readable($LabelFile))
1242  {
1243  # read the UI name
1244  $Label = file_get_contents($LabelFile);
1245 
1246  # if the UI name looks reasonable
1247  if (strlen(trim($Label)))
1248  {
1249  # use read name
1250  $Interfaces[$FilterExp][$CanonicalName] = $Label;
1251  }
1252  }
1253 
1254  # if we do not have a name yet
1255  if (!isset($Interfaces[$FilterExp][$CanonicalName]))
1256  {
1257  # use base directory for name
1258  $Interfaces[$FilterExp][$CanonicalName] = basename($Path);
1259  }
1260  }
1261  }
1262 
1263  # return list to caller
1264  return $Interfaces[$FilterExp];
1265  }
1266 
1275  public function GetUserInterfacePaths($FilterExp = NULL)
1276  {
1277  static $InterfacePaths;
1278 
1279  if (!isset($InterfacePaths[$FilterExp]))
1280  {
1281  # extract possible UI directories from interface directory list
1282  $InterfaceDirs = array();
1283  foreach ($this->ExpandDirectoryList($this->InterfaceDirList) as $Dir)
1284  {
1285  $Matches = array();
1286  if (preg_match("#([a-zA-Z0-9/]*interface)/[a-zA-Z0-9%/]*#",
1287  $Dir, $Matches))
1288  {
1289  $Dir = $Matches[1];
1290  if (!in_array($Dir, $InterfaceDirs))
1291  {
1292  $InterfaceDirs[] = $Dir;
1293  }
1294  }
1295  }
1296 
1297  # reverse order of interface directories so that the directory
1298  # returned is the base directory for the interface
1299  $InterfaceDirs = array_reverse($InterfaceDirs);
1300 
1301  # start out with an empty list
1302  $InterfacePaths[$FilterExp] = array();
1303  $InterfacesFound = array();
1304 
1305  # for each possible UI directory
1306  foreach ($InterfaceDirs as $InterfaceDir)
1307  {
1308  # check if the dir exists
1309  if (!is_dir($InterfaceDir))
1310  {
1311  continue;
1312  }
1313 
1314  $Dir = dir($InterfaceDir);
1315 
1316  # for each file in current directory
1317  while (($DirEntry = $Dir->read()) !== FALSE)
1318  {
1319  $InterfacePath = $InterfaceDir."/".$DirEntry;
1320 
1321  # skip anything we have already found
1322  # or that doesn't have a name in the required format
1323  # or that isn't a directory
1324  # or that doesn't match the filter regex (if supplied)
1325  if (in_array($DirEntry, $InterfacesFound)
1326  || !preg_match('/^[a-zA-Z0-9]+$/', $DirEntry)
1327  || !is_dir($InterfacePath)
1328  || (($FilterExp !== NULL)
1329  && !preg_match($FilterExp, $InterfacePath)))
1330  {
1331  continue;
1332  }
1333 
1334  # add interface to list
1335  $InterfacePaths[$FilterExp][$DirEntry] = $InterfacePath;
1336  $InterfacesFound[] = $DirEntry;
1337  }
1338 
1339  $Dir->close();
1340  }
1341  }
1342 
1343  # return list to caller
1344  return $InterfacePaths[$FilterExp];
1345  }
1346 
1371  public function AddPostProcessingCall($FunctionName,
1372  &$Arg1 = self::NOVALUE, &$Arg2 = self::NOVALUE, &$Arg3 = self::NOVALUE,
1373  &$Arg4 = self::NOVALUE, &$Arg5 = self::NOVALUE, &$Arg6 = self::NOVALUE,
1374  &$Arg7 = self::NOVALUE, &$Arg8 = self::NOVALUE, &$Arg9 = self::NOVALUE)
1375  {
1376  $FuncIndex = count($this->PostProcessingFuncs);
1377  $this->PostProcessingFuncs[$FuncIndex]["FunctionName"] = $FunctionName;
1378  $this->PostProcessingFuncs[$FuncIndex]["Arguments"] = array();
1379  $Index = 1;
1380  while (isset(${"Arg".$Index}) && (${"Arg".$Index} !== self::NOVALUE))
1381  {
1382  $this->PostProcessingFuncs[$FuncIndex]["Arguments"][$Index]
1383  =& ${"Arg".$Index};
1384  $Index++;
1385  }
1386  }
1387 
1393  public function AddEnvInclude($FileName)
1394  {
1395  $this->EnvIncludes[] = $FileName;
1396  }
1397 
1414  public function SetContextFilter($Context, $NewSetting)
1415  {
1416  if (($NewSetting === TRUE)
1417  || ($NewSetting === FALSE)
1418  || is_array($NewSetting))
1419  {
1420  $this->ContextFilters[$Context] = $NewSetting;
1421  }
1422  elseif (is_string($NewSetting))
1423  {
1424  $this->ContextFilters[$Context] = array($NewSetting);
1425  }
1426  else
1427  {
1428  throw new InvalidArgumentException(
1429  "Invalid setting (".$NewSetting.").");
1430  }
1431  }
1433  const CONTEXT_ENV = 1;
1435  const CONTEXT_PAGE = 2;
1437  const CONTEXT_COMMON = 3;
1439  const CONTEXT_INTERFACE = 4;
1441  const CONTEXT_START = 5;
1443  const CONTEXT_END = 6;
1444 
1451  public function GUIFile($FileName)
1452  {
1453  # determine which location to search based on file suffix
1454  $FileType = $this->GetFileType($FileName);
1455  $DirList = ($FileType == self::FT_IMAGE)
1456  ? $this->ImageDirList : $this->IncludeDirList;
1457 
1458  # if directed to use minimized JavaScript file
1459  if (($FileType == self::FT_JAVASCRIPT) && $this->UseMinimizedJavascript())
1460  {
1461  # look for minimized version of file
1462  $MinimizedFileName = substr_replace($FileName, ".min", -3, 0);
1463  $FoundFileName = $this->FindFile($DirList, $MinimizedFileName);
1464 
1465  # if minimized file was not found
1466  if (is_null($FoundFileName))
1467  {
1468  # look for unminimized file
1469  $FoundFileName = $this->FindFile($DirList, $FileName);
1470 
1471  # if unminimized file found
1472  if (!is_null($FoundFileName))
1473  {
1474  # if minimization enabled and supported
1475  if ($this->JavascriptMinimizationEnabled()
1476  && self::JsMinRewriteSupport())
1477  {
1478  # attempt to create minimized file
1479  $MinFileName = $this->MinimizeJavascriptFile(
1480  $FoundFileName);
1481 
1482  # if minimization succeeded
1483  if ($MinFileName !== NULL)
1484  {
1485  # use minimized version
1486  $FoundFileName = $MinFileName;
1487 
1488  # save file modification time if needed for fingerprinting
1489  if ($this->UrlFingerprintingEnabled())
1490  {
1491  $FileMTime = filemtime($FoundFileName);
1492  }
1493 
1494  # strip off the cache location, allowing .htaccess
1495  # to handle that for us
1496  $FoundFileName = str_replace(
1497  self::$JSMinCacheDir."/", "", $FoundFileName);
1498  }
1499  }
1500  }
1501  }
1502  }
1503  # else if directed to use SCSS files
1504  elseif (($FileType == self::FT_CSS) && $this->ScssSupportEnabled())
1505  {
1506  # look for SCSS version of file
1507  $SourceFileName = preg_replace("/.css$/", ".scss", $FileName);
1508  $FoundSourceFileName = $this->FindFile($DirList, $SourceFileName);
1509 
1510  # if SCSS file not found
1511  if ($FoundSourceFileName === NULL)
1512  {
1513  # look for CSS file
1514  $FoundFileName = $this->FindFile($DirList, $FileName);
1515  }
1516  else
1517  {
1518  # compile SCSS file (if updated) and return resulting CSS file
1519  $FoundFileName = $this->CompileScssFile($FoundSourceFileName);
1520 
1521  # save file modification time if needed for fingerprinting
1522  if ($this->UrlFingerprintingEnabled())
1523  {
1524  $FileMTime = filemtime($FoundFileName);
1525  }
1526 
1527  # strip off the cache location, allowing .htaccess to handle that for us
1528  if (self::ScssRewriteSupport())
1529  {
1530  $FoundFileName = str_replace(
1531  self::$ScssCacheDir."/", "", $FoundFileName);
1532  }
1533  }
1534  }
1535  # otherwise just search for the file
1536  else
1537  {
1538  $FoundFileName = $this->FindFile($DirList, $FileName);
1539  }
1540 
1541  # add non-image files to list of found files (used for required files loading)
1542  if ($FileType != self::FT_IMAGE)
1543  { $this->FoundUIFiles[] = basename($FoundFileName); }
1544 
1545  # if UI file fingerprinting is enabled and supported
1546  if ($this->UrlFingerprintingEnabled()
1547  && self::UrlFingerprintingRewriteSupport()
1548  && (isset($FileMTime) || file_exists($FoundFileName)))
1549  {
1550  # if file does not appear to be a server-side inclusion
1551  if (!preg_match('/\.(html|php)$/i', $FoundFileName))
1552  {
1553  # for each URL fingerprinting blacklist entry
1554  $OnBlacklist = FALSE;
1555  foreach ($this->UrlFingerprintBlacklist as $BlacklistEntry)
1556  {
1557  # if entry looks like a regular expression pattern
1558  if ($BlacklistEntry[0] == substr($BlacklistEntry, -1))
1559  {
1560  # check file name against regular expression
1561  if (preg_match($BlacklistEntry, $FoundFileName))
1562  {
1563  $OnBlacklist = TRUE;
1564  break;
1565  }
1566  }
1567  else
1568  {
1569  # check file name directly against entry
1570  if (basename($FoundFileName) == $BlacklistEntry)
1571  {
1572  $OnBlacklist = TRUE;
1573  break;
1574  }
1575  }
1576  }
1577 
1578  # if file was not on blacklist
1579  if (!$OnBlacklist)
1580  {
1581  # get file modification time if not already retrieved
1582  if (!isset($FileMTime))
1583  {
1584  $FileMTime = filemtime($FoundFileName);
1585  }
1586 
1587  # add timestamp fingerprint to file name
1588  $Fingerprint = sprintf("%06X",
1589  ($FileMTime % 0xFFFFFF));
1590  $FoundFileName = preg_replace("/^(.+)\.([a-z]+)$/",
1591  "$1.".$Fingerprint.".$2",
1592  $FoundFileName);
1593  }
1594  }
1595  }
1596 
1597  # return file name to caller
1598  return $FoundFileName;
1599  }
1600 
1609  public function PUIFile($FileName)
1610  {
1611  $FullFileName = $this->GUIFile($FileName);
1612  if ($FullFileName) { print($FullFileName); }
1613  }
1614 
1629  public function IncludeUIFile($FileNames, $AdditionalAttributes = NULL)
1630  {
1631  # convert file name to array if necessary
1632  if (!is_array($FileNames)) { $FileNames = array($FileNames); }
1633 
1634  # pad additional attributes if supplied
1635  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
1636 
1637  # for each file
1638  foreach ($FileNames as $BaseFileName)
1639  {
1640  # retrieve full file name
1641  $FileName = $this->GUIFile($BaseFileName);
1642 
1643  # if file was found
1644  if ($FileName)
1645  {
1646  # print appropriate tag
1647  print $this->GetUIFileLoadingTag($FileName, $AdditionalAttributes);
1648  }
1649 
1650  # if we are not already loading an override file
1651  if (!preg_match("/-Override.(css|scss|js)$/", $BaseFileName))
1652  {
1653  # attempt to load override file if available
1654  $FileType = $this->GetFileType($BaseFileName);
1655  switch ($FileType)
1656  {
1657  case self::FT_CSS:
1658  $OverrideFileName = preg_replace(
1659  "/\.(css|scss)$/", "-Override.$1",
1660  $BaseFileName);
1661  $this->IncludeUIFile($OverrideFileName,
1662  $AdditionalAttributes);
1663  break;
1664 
1665  case self::FT_JAVASCRIPT:
1666  $OverrideFileName = preg_replace(
1667  "/\.js$/", "-Override.js",
1668  $BaseFileName);
1669  $this->IncludeUIFile($OverrideFileName,
1670  $AdditionalAttributes);
1671  break;
1672  }
1673  }
1674  }
1675  }
1676 
1683  public function DoNotUrlFingerprint($Pattern)
1684  {
1685  $this->UrlFingerprintBlacklist[] = $Pattern;
1686  }
1687 
1698  public function RequireUIFile($FileName, $Order = self::ORDER_MIDDLE)
1699  {
1700  $this->AdditionalRequiredUIFiles[$FileName] = $Order;
1701  }
1702 
1708  public static function GetFileType($FileName)
1709  {
1710  static $FileTypeCache;
1711  if (isset($FileTypeCache[$FileName]))
1712  {
1713  return $FileTypeCache[$FileName];
1714  }
1715 
1716  $FileSuffix = strtolower(substr($FileName, -3));
1717  if ($FileSuffix == "css")
1718  {
1719  $FileTypeCache[$FileName] = self::FT_CSS;
1720  }
1721  elseif ($FileSuffix == ".js")
1722  {
1723  $FileTypeCache[$FileName] = self::FT_JAVASCRIPT;
1724  }
1725  elseif (($FileSuffix == "gif")
1726  || ($FileSuffix == "jpg")
1727  || ($FileSuffix == "png")
1728  || ($FileSuffix == "svg")
1729  || ($FileSuffix == "ico"))
1730  {
1731  $FileTypeCache[$FileName] = self::FT_IMAGE;
1732  }
1733  else
1734  {
1735  $FileTypeCache[$FileName] = self::FT_OTHER;
1736  }
1737 
1738  return $FileTypeCache[$FileName];
1739  }
1741  const FT_OTHER = 0;
1743  const FT_CSS = 1;
1745  const FT_IMAGE = 2;
1747  const FT_JAVASCRIPT = 3;
1748 
1757  public function LoadFunction($Callback)
1758  {
1759  # if specified function is not currently available
1760  if (!is_callable($Callback))
1761  {
1762  # if function info looks legal
1763  if (is_string($Callback) && strlen($Callback))
1764  {
1765  # start with function directory list
1766  $Locations = $this->FunctionDirList;
1767 
1768  # add object directories to list
1769  $Locations = array_merge(
1770  $Locations, array_keys(self::$ObjectDirectories));
1771 
1772  # look for function file
1773  $FunctionFileName = $this->FindFile($Locations, "F-".$Callback,
1774  array("php", "html"));
1775 
1776  # if function file was found
1777  if ($FunctionFileName)
1778  {
1779  # load function file
1780  include_once($FunctionFileName);
1781  }
1782  else
1783  {
1784  # log error indicating function load failed
1785  $this->LogError(self::LOGLVL_ERROR, "Unable to load function"
1786  ." for callback \"".$Callback."\".");
1787  }
1788  }
1789  else
1790  {
1791  # log error indicating specified function info was bad
1792  $this->LogError(self::LOGLVL_ERROR, "Unloadable callback value"
1793  ." (".$Callback.")"
1794  ." passed to AF::LoadFunction() by "
1795  .StdLib::GetMyCaller().".");
1796  }
1797  }
1798 
1799  # report to caller whether function load succeeded
1800  return is_callable($Callback);
1801  }
1802 
1807  public function GetElapsedExecutionTime()
1808  {
1809  return microtime(TRUE) - $this->ExecutionStartTime;
1810  }
1811 
1816  public function GetSecondsBeforeTimeout()
1817  {
1818  return $this->MaxExecutionTime() - $this->GetElapsedExecutionTime();
1819  }
1820 
1825  public function AddMetaTag($Attribs)
1826  {
1827  # add new meta tag to list
1828  $this->MetaTags[] = $Attribs;
1829  }
1830 
1831  /*@)*/ /* Application Framework */
1832 
1833 
1834  # ---- Page Caching ------------------------------------------------------
1835  /*@(*/
1837 
1844  public function PageCacheEnabled($NewValue = DB_NOVALUE)
1845  {
1846  return $this->UpdateSetting("PageCacheEnabled", $NewValue);
1847  }
1848 
1855  public function PageCacheExpirationPeriod($NewValue = DB_NOVALUE)
1856  {
1857  return $this->UpdateSetting("PageCacheExpirationPeriod", $NewValue);
1858  }
1859 
1864  public function DoNotCacheCurrentPage()
1865  {
1866  $this->CacheCurrentPage = FALSE;
1867  }
1868 
1875  public function AddPageCacheTag($Tag, $Pages = NULL)
1876  {
1877  # normalize tag
1878  $Tag = strtolower($Tag);
1879 
1880  # if pages were supplied
1881  if ($Pages !== NULL)
1882  {
1883  # add pages to list for this tag
1884  if (isset($this->PageCacheTags[$Tag]))
1885  {
1886  $this->PageCacheTags[$Tag] = array_merge(
1887  $this->PageCacheTags[$Tag], $Pages);
1888  }
1889  else
1890  {
1891  $this->PageCacheTags[$Tag] = $Pages;
1892  }
1893  }
1894  else
1895  {
1896  # add current page to list for this tag
1897  $this->PageCacheTags[$Tag][] = "CURRENT";
1898  }
1899  }
1900 
1906  public function ClearPageCacheForTag($Tag)
1907  {
1908  # get tag ID
1909  $TagId = $this->GetPageCacheTagId($Tag);
1910 
1911  # delete pages and tag/page connections for specified tag
1912  $this->DB->Query("DELETE CP, CPTI"
1913  ." FROM AF_CachedPages CP, AF_CachedPageTagInts CPTI"
1914  ." WHERE CPTI.TagId = ".intval($TagId)
1915  ." AND CP.CacheId = CPTI.CacheId");
1916  }
1917 
1921  public function ClearPageCache()
1922  {
1923  # clear all page cache tables
1924  $this->DB->Query("TRUNCATE TABLE AF_CachedPages");
1925  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTags");
1926  $this->DB->Query("TRUNCATE TABLE AF_CachedPageTagInts");
1927  }
1928 
1935  public function GetPageCacheInfo()
1936  {
1937  $Length = $this->DB->Query("SELECT COUNT(*) AS CacheLen"
1938  ." FROM AF_CachedPages", "CacheLen");
1939  $Oldest = $this->DB->Query("SELECT CachedAt FROM AF_CachedPages"
1940  ." ORDER BY CachedAt ASC LIMIT 1", "CachedAt");
1941  return array(
1942  "NumberOfEntries" => $Length,
1943  "OldestTimestamp" => strtotime($Oldest),
1944  );
1945  }
1946 
1947  /*@)*/ /* Page Caching */
1948 
1949 
1950  # ---- Logging -----------------------------------------------------------
1951  /*@(*/
1953 
1967  public function LogSlowPageLoads(
1968  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1969  {
1970  return $this->UpdateSetting(
1971  "LogSlowPageLoads", $NewValue, $Persistent);
1972  }
1973 
1984  public function SlowPageLoadThreshold(
1985  $NewValue = DB_NOVALUE, $Persistent = FALSE)
1986  {
1987  return $this->UpdateSetting(
1988  "SlowPageLoadThreshold", $NewValue, $Persistent);
1989  }
1990 
2004  public function LogHighMemoryUsage(
2005  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2006  {
2007  return $this->UpdateSetting(
2008  "LogHighMemoryUsage", $NewValue, $Persistent);
2009  }
2010 
2022  public function HighMemoryUsageThreshold(
2023  $NewValue = DB_NOVALUE, $Persistent = FALSE)
2024  {
2025  return $this->UpdateSetting(
2026  "HighMemoryUsageThreshold", $NewValue, $Persistent);
2027  }
2028 
2042  public function LogError($Level, $Msg)
2043  {
2044  # if error level is at or below current logging level
2045  if ($this->Settings["LoggingLevel"] >= $Level)
2046  {
2047  # attempt to log error message
2048  $Result = $this->LogMessage($Level, $Msg);
2049 
2050  # if logging attempt failed and level indicated significant error
2051  if (($Result === FALSE) && ($Level <= self::LOGLVL_ERROR))
2052  {
2053  # throw exception about inability to log error
2054  static $AlreadyThrewException = FALSE;
2055  if (!$AlreadyThrewException)
2056  {
2057  $AlreadyThrewException = TRUE;
2058  throw new Exception("Unable to log error (".$Level.": ".$Msg
2059  .") to ".$this->LogFileName);
2060  }
2061  }
2062 
2063  # report to caller whether message was logged
2064  return $Result;
2065  }
2066  else
2067  {
2068  # report to caller that message was not logged
2069  return FALSE;
2070  }
2071  }
2072 
2084  public function LogMessage($Level, $Msg)
2085  {
2086  # if message level is at or below current logging level
2087  if ($this->Settings["LoggingLevel"] >= $Level)
2088  {
2089  # attempt to open log file
2090  $FHndl = @fopen($this->LogFileName, "a");
2091 
2092  # if log file could not be open
2093  if ($FHndl === FALSE)
2094  {
2095  # report to caller that message was not logged
2096  return FALSE;
2097  }
2098  else
2099  {
2100  # format log entry
2101  $ErrorAbbrevs = array(
2102  self::LOGLVL_FATAL => "FTL",
2103  self::LOGLVL_ERROR => "ERR",
2104  self::LOGLVL_WARNING => "WRN",
2105  self::LOGLVL_INFO => "INF",
2106  self::LOGLVL_DEBUG => "DBG",
2107  self::LOGLVL_TRACE => "TRC",
2108  );
2109  $Msg = str_replace(array("\n", "\t", "\r"), " ", $Msg);
2110  $Msg = substr(trim($Msg), 0, self::LOGFILE_MAX_LINE_LENGTH);
2111  $LogEntry = date("Y-m-d H:i:s")
2112  ." ".($this->RunningInBackground ? "B" : "F")
2113  ." ".$ErrorAbbrevs[$Level]
2114  ." ".$Msg;
2115 
2116  # write entry to log
2117  $Success = fwrite($FHndl, $LogEntry."\n");
2118 
2119  # close log file
2120  fclose($FHndl);
2121 
2122  # report to caller whether message was logged
2123  return ($Success === FALSE) ? FALSE : TRUE;
2124  }
2125  }
2126  else
2127  {
2128  # report to caller that message was not logged
2129  return FALSE;
2130  }
2131  }
2132 
2154  public function LoggingLevel($NewValue = DB_NOVALUE)
2155  {
2156  # constrain new level (if supplied) to within legal bounds
2157  if ($NewValue !== DB_NOVALUE)
2158  {
2159  $NewValue = max(min($NewValue, 6), 1);
2160  }
2161 
2162  # set new logging level (if supplied) and return current level to caller
2163  return $this->UpdateSetting("LoggingLevel", $NewValue);
2164  }
2165 
2172  public function LogFile($NewValue = NULL)
2173  {
2174  if ($NewValue !== NULL) { $this->LogFileName = $NewValue; }
2175  return $this->LogFileName;
2176  }
2177 
2187  public function GetLogEntries($Limit = 0)
2188  {
2189  # return no entries if there isn't a log file
2190  # or we can't read it or it's empty
2191  $LogFile = $this->LogFile();
2192  if (!is_readable($LogFile) || !filesize($LogFile))
2193  {
2194  return array();
2195  }
2196 
2197  # if max number of entries specified
2198  if ($Limit > 0)
2199  {
2200  # load lines from file
2201  $FHandle = fopen($LogFile, "r");
2202  $FileSize = filesize($LogFile);
2203  $SeekPosition = max(0,
2204  ($FileSize - (self::LOGFILE_MAX_LINE_LENGTH
2205  * ($Limit + 1))));
2206  fseek($FHandle, $SeekPosition);
2207  $Block = fread($FHandle, ($FileSize - $SeekPosition));
2208  fclose($FHandle);
2209  $Lines = explode(PHP_EOL, $Block);
2210  array_pop($Lines);
2211 
2212  # prune array back to requested number of entries
2213  $Lines = array_slice($Lines, (0 - $Limit));
2214  }
2215  else
2216  {
2217  # load all lines from log file
2218  $Lines = file($LogFile, FILE_IGNORE_NEW_LINES);
2219  if ($Lines === FALSE)
2220  {
2221  return array();
2222  }
2223  }
2224 
2225  # reverse line order
2226  $Lines = array_reverse($Lines);
2227 
2228  # for each log file line
2229  $Entries = array();
2230  foreach ($Lines as $Line)
2231  {
2232  # attempt to parse line into component parts
2233  $Pieces = explode(" ", $Line, 5);
2234  $Date = isset($Pieces[0]) ? $Pieces[0] : "";
2235  $Time = isset($Pieces[1]) ? $Pieces[1] : "";
2236  $Back = isset($Pieces[2]) ? $Pieces[2] : "";
2237  $Level = isset($Pieces[3]) ? $Pieces[3] : "";
2238  $Msg = isset($Pieces[4]) ? $Pieces[4] : "";
2239 
2240  # skip line if it looks invalid
2241  $ErrorAbbrevs = array(
2242  "FTL" => self::LOGLVL_FATAL,
2243  "ERR" => self::LOGLVL_ERROR,
2244  "WRN" => self::LOGLVL_WARNING,
2245  "INF" => self::LOGLVL_INFO,
2246  "DBG" => self::LOGLVL_DEBUG,
2247  "TRC" => self::LOGLVL_TRACE,
2248  );
2249  if ((($Back != "F") && ($Back != "B"))
2250  || !array_key_exists($Level, $ErrorAbbrevs)
2251  || !strlen($Msg))
2252  {
2253  continue;
2254  }
2255 
2256  # convert parts into appropriate values and add to entries
2257  $Entries[] = array(
2258  "Time" => strtotime($Date." ".$Time),
2259  "Background" => ($Back == "B") ? TRUE : FALSE,
2260  "Level" => $ErrorAbbrevs[$Level],
2261  "Message" => $Msg,
2262  );
2263  }
2264 
2265  # return entries to caller
2266  return $Entries;
2267  }
2268 
2273  const LOGLVL_TRACE = 6;
2278  const LOGLVL_DEBUG = 5;
2284  const LOGLVL_INFO = 4;
2289  const LOGLVL_WARNING = 3;
2295  const LOGLVL_ERROR = 2;
2300  const LOGLVL_FATAL = 1;
2301 
2305  const LOGFILE_MAX_LINE_LENGTH = 2048;
2306 
2307  /*@)*/ /* Logging */
2308 
2309 
2310  # ---- Event Handling ----------------------------------------------------
2311  /*@(*/
2313 
2317  const EVENTTYPE_DEFAULT = 1;
2323  const EVENTTYPE_CHAIN = 2;
2329  const EVENTTYPE_FIRST = 3;
2337  const EVENTTYPE_NAMED = 4;
2338 
2340  const ORDER_FIRST = 1;
2342  const ORDER_MIDDLE = 2;
2344  const ORDER_LAST = 3;
2345 
2354  public function RegisterEvent($EventsOrEventName, $EventType = NULL)
2355  {
2356  # convert parameters to array if not already in that form
2357  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2358  : array($EventsOrEventName => $EventType);
2359 
2360  # for each event
2361  foreach ($Events as $Name => $Type)
2362  {
2363  # store event information
2364  $this->RegisteredEvents[$Name]["Type"] = $Type;
2365  $this->RegisteredEvents[$Name]["Hooks"] = array();
2366  }
2367  }
2368 
2375  public function IsRegisteredEvent($EventName)
2376  {
2377  return array_key_exists($EventName, $this->RegisteredEvents)
2378  ? TRUE : FALSE;
2379  }
2380 
2387  public function IsHookedEvent($EventName)
2388  {
2389  # the event isn't hooked to if it isn't even registered
2390  if (!$this->IsRegisteredEvent($EventName))
2391  {
2392  return FALSE;
2393  }
2394 
2395  # return TRUE if there is at least one callback hooked to the event
2396  return count($this->RegisteredEvents[$EventName]["Hooks"]) > 0;
2397  }
2398 
2412  public function HookEvent(
2413  $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2414  {
2415  # convert parameters to array if not already in that form
2416  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2417  : array($EventsOrEventName => $Callback);
2418 
2419  # for each event
2420  $Success = TRUE;
2421  foreach ($Events as $EventName => $EventCallback)
2422  {
2423  # if callback is valid
2424  if (is_callable($EventCallback))
2425  {
2426  # if this is a periodic event we process internally
2427  if (isset($this->PeriodicEvents[$EventName]))
2428  {
2429  # process event now
2430  $this->ProcessPeriodicEvent($EventName, $EventCallback);
2431  }
2432  # if specified event has been registered
2433  elseif (isset($this->RegisteredEvents[$EventName]))
2434  {
2435  # add callback for event
2436  $this->RegisteredEvents[$EventName]["Hooks"][]
2437  = array("Callback" => $EventCallback, "Order" => $Order);
2438 
2439  # sort callbacks by order
2440  if (count($this->RegisteredEvents[$EventName]["Hooks"]) > 1)
2441  {
2442  usort($this->RegisteredEvents[$EventName]["Hooks"],
2443  function ($A, $B) {
2444  return StdLib::SortCompare(
2445  $A["Order"], $B["Order"]);
2446  });
2447  }
2448  }
2449  else
2450  {
2451  $Success = FALSE;
2452  }
2453  }
2454  else
2455  {
2456  $Success = FALSE;
2457  }
2458  }
2459 
2460  # report to caller whether all callbacks were hooked
2461  return $Success;
2462  }
2463 
2477  public function UnhookEvent(
2478  $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2479  {
2480  # convert parameters to array if not already in that form
2481  $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2482  : array($EventsOrEventName => $Callback);
2483 
2484  # for each event
2485  $UnhookCount = 0;
2486  foreach ($Events as $EventName => $EventCallback)
2487  {
2488  # if this event has been registered and hooked
2489  if (isset($this->RegisteredEvents[$EventName])
2490  && count($this->RegisteredEvents[$EventName]))
2491  {
2492  # if this callback has been hooked for this event
2493  $CallbackData = array("Callback" => $EventCallback, "Order" => $Order);
2494  if (in_array($CallbackData,
2495  $this->RegisteredEvents[$EventName]["Hooks"]))
2496  {
2497  # unhook callback
2498  $HookIndex = array_search($CallbackData,
2499  $this->RegisteredEvents[$EventName]["Hooks"]);
2500  unset($this->RegisteredEvents[$EventName]["Hooks"][$HookIndex]);
2501  $UnhookCount++;
2502  }
2503  }
2504  }
2505 
2506  # report number of callbacks unhooked to caller
2507  return $UnhookCount;
2508  }
2509 
2520  public function SignalEvent($EventName, $Parameters = NULL)
2521  {
2522  $ReturnValue = NULL;
2523 
2524  # if event has been registered
2525  if (isset($this->RegisteredEvents[$EventName]))
2526  {
2527  # set up default return value (if not NULL)
2528  switch ($this->RegisteredEvents[$EventName]["Type"])
2529  {
2530  case self::EVENTTYPE_CHAIN:
2531  $ReturnValue = $Parameters;
2532  break;
2533 
2534  case self::EVENTTYPE_NAMED:
2535  $ReturnValue = array();
2536  break;
2537  }
2538 
2539  # for each callback for this event
2540  foreach ($this->RegisteredEvents[$EventName]["Hooks"] as $Hook)
2541  {
2542  # invoke callback
2543  $Callback = $Hook["Callback"];
2544  $Result = ($Parameters !== NULL)
2545  ? call_user_func_array($Callback, $Parameters)
2546  : call_user_func($Callback);
2547 
2548  # process return value based on event type
2549  switch ($this->RegisteredEvents[$EventName]["Type"])
2550  {
2551  case self::EVENTTYPE_CHAIN:
2552  if ($Result !== NULL)
2553  {
2554  foreach ($Parameters as $Index => $Value)
2555  {
2556  if (array_key_exists($Index, $Result))
2557  {
2558  $Parameters[$Index] = $Result[$Index];
2559  }
2560  }
2561  $ReturnValue = $Parameters;
2562  }
2563  break;
2564 
2565  case self::EVENTTYPE_FIRST:
2566  if ($Result !== NULL)
2567  {
2568  $ReturnValue = $Result;
2569  break 2;
2570  }
2571  break;
2572 
2573  case self::EVENTTYPE_NAMED:
2574  $CallbackName = is_array($Callback)
2575  ? (is_object($Callback[0])
2576  ? get_class($Callback[0])
2577  : $Callback[0])."::".$Callback[1]
2578  : $Callback;
2579  $ReturnValue[$CallbackName] = $Result;
2580  break;
2581 
2582  default:
2583  break;
2584  }
2585  }
2586  }
2587  else
2588  {
2589  $this->LogError(self::LOGLVL_WARNING,
2590  "Unregistered event (".$EventName.") signaled by "
2591  .StdLib::GetMyCaller().".");
2592  }
2593 
2594  # return value if any to caller
2595  return $ReturnValue;
2596  }
2597 
2603  public function IsStaticOnlyEvent($EventName)
2604  {
2605  return isset($this->PeriodicEvents[$EventName]) ? TRUE : FALSE;
2606  }
2607 
2618  public function EventWillNextRunAt($EventName, $Callback)
2619  {
2620  # if event is not a periodic event report failure to caller
2621  if (!array_key_exists($EventName, $this->EventPeriods)) { return FALSE; }
2622 
2623  # retrieve last execution time for event if available
2624  $Signature = self::GetCallbackSignature($Callback);
2625  $LastRunTime = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
2626  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
2627 
2628  # if event was not found report failure to caller
2629  if ($LastRunTime === NULL) { return FALSE; }
2630 
2631  # calculate next run time based on event period
2632  $NextRunTime = strtotime($LastRunTime) + $this->EventPeriods[$EventName];
2633 
2634  # report next run time to caller
2635  return $NextRunTime;
2636  }
2637 
2653  public function GetKnownPeriodicEvents()
2654  {
2655  # retrieve last execution times
2656  $this->DB->Query("SELECT * FROM PeriodicEvents");
2657  $LastRunTimes = $this->DB->FetchColumn("LastRunAt", "Signature");
2658 
2659  # for each known event
2660  $Events = array();
2661  foreach ($this->KnownPeriodicEvents as $Signature => $Info)
2662  {
2663  # if last run time for event is available
2664  if (array_key_exists($Signature, $LastRunTimes))
2665  {
2666  # calculate next run time for event
2667  $LastRun = strtotime($LastRunTimes[$Signature]);
2668  $NextRun = $LastRun + $this->EventPeriods[$Info["Period"]];
2669  if ($Info["Period"] == "EVENT_PERIODIC") { $LastRun = FALSE; }
2670  }
2671  else
2672  {
2673  # set info to indicate run times are not known
2674  $LastRun = FALSE;
2675  $NextRun = FALSE;
2676  }
2677 
2678  # add event info to list
2679  $Events[$Signature] = $Info;
2680  $Events[$Signature]["LastRun"] = $LastRun;
2681  $Events[$Signature]["NextRun"] = $NextRun;
2682  $Events[$Signature]["Parameters"] = NULL;
2683  }
2684 
2685  # return list of known events to caller
2686  return $Events;
2687  }
2688 
2695  public static function RunPeriodicEvent($EventName, $Callback, $Parameters)
2696  {
2697  static $DB;
2698  if (!isset($DB)) { $DB = new Database(); }
2699 
2700  # run event
2701  $ReturnVal = call_user_func_array($Callback, $Parameters);
2702 
2703  # if event is already in database
2704  $Signature = self::GetCallbackSignature($Callback);
2705  if ($DB->Query("SELECT COUNT(*) AS EventCount FROM PeriodicEvents"
2706  ." WHERE Signature = '".addslashes($Signature)."'", "EventCount"))
2707  {
2708  # update last run time for event
2709  $DB->Query("UPDATE PeriodicEvents SET LastRunAt = "
2710  .(($EventName == "EVENT_PERIODIC")
2711  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
2712  : "NOW()")
2713  ." WHERE Signature = '".addslashes($Signature)."'");
2714  }
2715  else
2716  {
2717  # add last run time for event to database
2718  $DB->Query("INSERT INTO PeriodicEvents (Signature, LastRunAt) VALUES "
2719  ."('".addslashes($Signature)."', "
2720  .(($EventName == "EVENT_PERIODIC")
2721  ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
2722  : "NOW()").")");
2723  }
2724  }
2725 
2726  /*@)*/ /* Event Handling */
2727 
2728 
2729  # ---- Task Management ---------------------------------------------------
2730  /*@(*/
2732 
2734  const PRIORITY_HIGH = 1;
2736  const PRIORITY_MEDIUM = 2;
2738  const PRIORITY_LOW = 3;
2740  const PRIORITY_BACKGROUND = 4;
2741 
2754  public function QueueTask($Callback, $Parameters = NULL,
2755  $Priority = self::PRIORITY_LOW, $Description = "")
2756  {
2757  # pack task info and write to database
2758  if ($Parameters === NULL) { $Parameters = array(); }
2759  $this->DB->Query("INSERT INTO TaskQueue"
2760  ." (Callback, Parameters, Priority, Description)"
2761  ." VALUES ('".addslashes(serialize($Callback))."', '"
2762  .addslashes(serialize($Parameters))."', ".intval($Priority).", '"
2763  .addslashes($Description)."')");
2764  }
2765 
2783  public function QueueUniqueTask($Callback, $Parameters = NULL,
2784  $Priority = self::PRIORITY_LOW, $Description = "")
2785  {
2786  if ($this->TaskIsInQueue($Callback, $Parameters))
2787  {
2788  $QueryResult = $this->DB->Query("SELECT TaskId,Priority FROM TaskQueue"
2789  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2790  .($Parameters ? " AND Parameters = '"
2791  .addslashes(serialize($Parameters))."'" : ""));
2792  if ($QueryResult !== FALSE)
2793  {
2794  $Record = $this->DB->FetchRow();
2795  if ($Record["Priority"] > $Priority)
2796  {
2797  $this->DB->Query("UPDATE TaskQueue"
2798  ." SET Priority = ".intval($Priority)
2799  ." WHERE TaskId = ".intval($Record["TaskId"]));
2800  }
2801  }
2802  return FALSE;
2803  }
2804  else
2805  {
2806  $this->QueueTask($Callback, $Parameters, $Priority, $Description);
2807  return TRUE;
2808  }
2809  }
2810 
2820  public function TaskIsInQueue($Callback, $Parameters = NULL)
2821  {
2822  $QueuedCount = $this->DB->Query(
2823  "SELECT COUNT(*) AS FoundCount FROM TaskQueue"
2824  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2825  .($Parameters ? " AND Parameters = '"
2826  .addslashes(serialize($Parameters))."'" : ""),
2827  "FoundCount");
2828  $RunningCount = $this->DB->Query(
2829  "SELECT COUNT(*) AS FoundCount FROM RunningTasks"
2830  ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2831  .($Parameters ? " AND Parameters = '"
2832  .addslashes(serialize($Parameters))."'" : ""),
2833  "FoundCount");
2834  $FoundCount = $QueuedCount + $RunningCount;
2835  return ($FoundCount ? TRUE : FALSE);
2836  }
2837 
2843  public function GetTaskQueueSize($Priority = NULL)
2844  {
2845  return $this->GetQueuedTaskCount(NULL, NULL, $Priority);
2846  }
2847 
2855  public function GetQueuedTaskList($Count = 100, $Offset = 0)
2856  {
2857  return $this->GetTaskList("SELECT * FROM TaskQueue"
2858  ." ORDER BY Priority, TaskId ", $Count, $Offset);
2859  }
2860 
2874  public function GetQueuedTaskCount($Callback = NULL,
2875  $Parameters = NULL, $Priority = NULL, $Description = NULL)
2876  {
2877  $Query = "SELECT COUNT(*) AS TaskCount FROM TaskQueue";
2878  $Sep = " WHERE";
2879  if ($Callback !== NULL)
2880  {
2881  $Query .= $Sep." Callback = '".addslashes(serialize($Callback))."'";
2882  $Sep = " AND";
2883  }
2884  if ($Parameters !== NULL)
2885  {
2886  $Query .= $Sep." Parameters = '".addslashes(serialize($Parameters))."'";
2887  $Sep = " AND";
2888  }
2889  if ($Priority !== NULL)
2890  {
2891  $Query .= $Sep." Priority = ".intval($Priority);
2892  $Sep = " AND";
2893  }
2894  if ($Description !== NULL)
2895  {
2896  $Query .= $Sep." Description = '".addslashes($Description)."'";
2897  }
2898  return $this->DB->Query($Query, "TaskCount");
2899  }
2900 
2908  public function GetRunningTaskList($Count = 100, $Offset = 0)
2909  {
2910  return $this->GetTaskList("SELECT * FROM RunningTasks"
2911  ." WHERE StartedAt >= '".date("Y-m-d H:i:s",
2912  (time() - $this->MaxExecutionTime()))."'"
2913  ." ORDER BY StartedAt", $Count, $Offset);
2914  }
2915 
2923  public function GetOrphanedTaskList($Count = 100, $Offset = 0)
2924  {
2925  return $this->GetTaskList("SELECT * FROM RunningTasks"
2926  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
2927  (time() - $this->MaxExecutionTime()))."'"
2928  ." ORDER BY StartedAt", $Count, $Offset);
2929  }
2930 
2935  public function GetOrphanedTaskCount()
2936  {
2937  return $this->DB->Query("SELECT COUNT(*) AS Count FROM RunningTasks"
2938  ." WHERE StartedAt < '".date("Y-m-d H:i:s",
2939  (time() - $this->MaxExecutionTime()))."'",
2940  "Count");
2941  }
2942 
2948  public function ReQueueOrphanedTask($TaskId, $NewPriority = NULL)
2949  {
2950  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
2951  $this->DB->Query("INSERT INTO TaskQueue"
2952  ." (Callback,Parameters,Priority,Description) "
2953  ."SELECT Callback, Parameters, Priority, Description"
2954  ." FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2955  if ($NewPriority !== NULL)
2956  {
2957  $NewTaskId = $this->DB->LastInsertId();
2958  $this->DB->Query("UPDATE TaskQueue SET Priority = "
2959  .intval($NewPriority)
2960  ." WHERE TaskId = ".intval($NewTaskId));
2961  }
2962  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2963  $this->DB->Query("UNLOCK TABLES");
2964  }
2965 
2972  public function RequeueCurrentTask($NewValue = TRUE)
2973  {
2974  $this->RequeueCurrentTask = $NewValue;
2975  }
2976 
2982  public function DeleteTask($TaskId)
2983  {
2984  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = ".intval($TaskId));
2985  $TasksRemoved = $this->DB->NumRowsAffected();
2986  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2987  $TasksRemoved += $this->DB->NumRowsAffected();
2988  return $TasksRemoved;
2989  }
2990 
2998  public function GetTask($TaskId)
2999  {
3000  # assume task will not be found
3001  $Task = NULL;
3002 
3003  # look for task in task queue
3004  $this->DB->Query("SELECT * FROM TaskQueue WHERE TaskId = ".intval($TaskId));
3005 
3006  # if task was not found in queue
3007  if (!$this->DB->NumRowsSelected())
3008  {
3009  # look for task in running task list
3010  $this->DB->Query("SELECT * FROM RunningTasks WHERE TaskId = "
3011  .intval($TaskId));
3012  }
3013 
3014  # if task was found
3015  if ($this->DB->NumRowsSelected())
3016  {
3017  # if task was periodic
3018  $Row = $this->DB->FetchRow();
3019  if ($Row["Callback"] ==
3020  serialize(array("ApplicationFramework", "RunPeriodicEvent")))
3021  {
3022  # unpack periodic task callback
3023  $WrappedCallback = unserialize($Row["Parameters"]);
3024  $Task["Callback"] = $WrappedCallback[1];
3025  $Task["Parameters"] = $WrappedCallback[2];
3026  }
3027  else
3028  {
3029  # unpack task callback and parameters
3030  $Task["Callback"] = unserialize($Row["Callback"]);
3031  $Task["Parameters"] = unserialize($Row["Parameters"]);
3032  }
3033  }
3034 
3035  # return task to caller
3036  return $Task;
3037  }
3038 
3046  public function TaskExecutionEnabled($NewValue = DB_NOVALUE)
3047  {
3048  return $this->UpdateSetting("TaskExecutionEnabled", $NewValue);
3049  }
3050 
3056  public function MaxTasks($NewValue = DB_NOVALUE)
3057  {
3058  return $this->UpdateSetting("MaxTasksRunning", $NewValue);
3059  }
3060 
3068  public static function GetTaskCallbackSynopsis($TaskInfo)
3069  {
3070  # if task callback is function use function name
3071  $Callback = $TaskInfo["Callback"];
3072  $Name = "";
3073  if (!is_array($Callback))
3074  {
3075  $Name = $Callback;
3076  }
3077  else
3078  {
3079  # if task callback is object
3080  if (is_object($Callback[0]))
3081  {
3082  # if task callback is encapsulated ask encapsulation for name
3083  if (method_exists($Callback[0], "GetCallbackAsText"))
3084  {
3085  $Name = $Callback[0]->GetCallbackAsText();
3086  }
3087  # else assemble name from object
3088  else
3089  {
3090  $Name = get_class($Callback[0]) . "::" . $Callback[1];
3091  }
3092  }
3093  # else assemble name from supplied info
3094  else
3095  {
3096  $Name= $Callback[0] . "::" . $Callback[1];
3097  }
3098  }
3099 
3100  # if parameter array was supplied
3101  $Parameters = $TaskInfo["Parameters"];
3102  $ParameterString = "";
3103  if (is_array($Parameters))
3104  {
3105  # assemble parameter string
3106  $Separator = "";
3107  foreach ($Parameters as $Parameter)
3108  {
3109  $ParameterString .= $Separator;
3110  if (is_int($Parameter) || is_float($Parameter))
3111  {
3112  $ParameterString .= $Parameter;
3113  }
3114  else if (is_string($Parameter))
3115  {
3116  $ParameterString .= "\"".htmlspecialchars($Parameter)."\"";
3117  }
3118  else if (is_array($Parameter))
3119  {
3120  $ParameterString .= "ARRAY";
3121  }
3122  else if (is_object($Parameter))
3123  {
3124  $ParameterString .= "OBJECT";
3125  }
3126  else if (is_null($Parameter))
3127  {
3128  $ParameterString .= "NULL";
3129  }
3130  else if (is_bool($Parameter))
3131  {
3132  $ParameterString .= $Parameter ? "TRUE" : "FALSE";
3133  }
3134  else if (is_resource($Parameter))
3135  {
3136  $ParameterString .= get_resource_type($Parameter);
3137  }
3138  else
3139  {
3140  $ParameterString .= "????";
3141  }
3142  $Separator = ", ";
3143  }
3144  }
3145 
3146  # assemble name and parameters and return result to caller
3147  return $Name."(".$ParameterString.")";
3148  }
3149 
3154  public function IsRunningInBackground()
3155  {
3156  return $this->RunningInBackground;
3157  }
3158 
3164  public function GetCurrentBackgroundPriority()
3165  {
3166  return isset($this->RunningTask)
3167  ? $this->RunningTask["Priority"] : NULL;
3168  }
3169 
3178  public function GetNextHigherBackgroundPriority($Priority = NULL)
3179  {
3180  if ($Priority === NULL)
3181  {
3182  $Priority = $this->GetCurrentBackgroundPriority();
3183  if ($Priority === NULL)
3184  {
3185  return NULL;
3186  }
3187  }
3188  return ($Priority > self::PRIORITY_HIGH)
3189  ? ($Priority - 1) : self::PRIORITY_HIGH;
3190  }
3191 
3200  public function GetNextLowerBackgroundPriority($Priority = NULL)
3201  {
3202  if ($Priority === NULL)
3203  {
3204  $Priority = $this->GetCurrentBackgroundPriority();
3205  if ($Priority === NULL)
3206  {
3207  return NULL;
3208  }
3209  }
3210  return ($Priority < self::PRIORITY_BACKGROUND)
3211  ? ($Priority + 1) : self::PRIORITY_BACKGROUND;
3212  }
3213 
3214  /*@)*/ /* Task Management */
3215 
3216 
3217  # ---- Clean URL Support -------------------------------------------------
3218  /*@(*/
3220 
3247  public function AddCleanUrl($Pattern, $Page, $GetVars = NULL, $Template = NULL)
3248  {
3249  # save clean URL mapping parameters
3250  $this->CleanUrlMappings[] = array(
3251  "Pattern" => $Pattern,
3252  "Page" => $Page,
3253  "GetVars" => $GetVars,
3254  "AddedBy" => StdLib::GetCallerInfo(),
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 
3484  public function GetCleanUrlList()
3485  {
3486  return $this->CleanUrlMappings;
3487  }
3488 
3501  public function AddPrefixForAlternateDomain($Domain, $Prefix)
3502  {
3503  $this->AlternateDomainPrefixes[$Domain] = $Prefix;
3504  }
3505 
3506 
3511  public function GetAlternateDomains()
3512  {
3513  return array_keys($this->AlternateDomainPrefixes);
3514  }
3515 
3522  public function GetPrefixForAlternateDomain($Domain)
3523  {
3524  return isset($this->AlternateDomainPrefixes[$Domain]) ?
3525  $this->AlternateDomainPrefixes[$Domain] : NULL;
3526  }
3527 
3528  /*@)*/ /* Clean URL Support */
3529 
3530  # ---- Server Environment ------------------------------------------------
3531  /*@(*/
3533 
3539  public static function SessionLifetime($NewValue = NULL)
3540  {
3541  if ($NewValue !== NULL)
3542  {
3543  self::$SessionLifetime = $NewValue;
3544  }
3545  return self::$SessionLifetime;
3546  }
3547 
3553  public static function HtaccessSupport()
3554  {
3555  return isset($_SERVER["HTACCESS_SUPPORT"])
3556  || isset($_SERVER["REDIRECT_HTACCESS_SUPPORT"]);
3557  }
3558 
3565  public static function UrlFingerprintingRewriteSupport()
3566  {
3567  return isset($_SERVER["URL_FINGERPRINTING_SUPPORT"])
3568  || isset($_SERVER["REDIRECT_URL_FINGERPRINTING_SUPPORT"]);
3569  }
3570 
3577  public static function ScssRewriteSupport()
3578  {
3579  return isset($_SERVER["SCSS_REWRITE_SUPPORT"])
3580  || isset($_SERVER["REDIRECT_SCSS_REWRITE_SUPPORT"]);
3581  }
3582 
3589  public static function JsMinRewriteSupport()
3590  {
3591  return isset($_SERVER["JSMIN_REWRITE_SUPPORT"])
3592  || isset($_SERVER["REDIRECT_JSMIN_REWRITE_SUPPORT"]);
3593  }
3594 
3602  public static function RootUrl()
3603  {
3604  # return override value if one is set
3605  if (self::$RootUrlOverride !== NULL)
3606  {
3607  return self::$RootUrlOverride;
3608  }
3609 
3610  # determine scheme name
3611  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
3612 
3613  # if HTTP_HOST is preferred or SERVER_NAME points to localhost
3614  # and HTTP_HOST is set
3615  if ((self::$PreferHttpHost || ($_SERVER["SERVER_NAME"] == "127.0.0.1"))
3616  && isset($_SERVER["HTTP_HOST"]))
3617  {
3618  # use HTTP_HOST for domain name
3619  $DomainName = $_SERVER["HTTP_HOST"];
3620  }
3621  else
3622  {
3623  # use SERVER_NAME for domain name
3624  $DomainName = $_SERVER["SERVER_NAME"];
3625  }
3626 
3627  # build URL root and return to caller
3628  return $Protocol."://".$DomainName;
3629  }
3630 
3645  public static function RootUrlOverride($NewValue = self::NOVALUE)
3646  {
3647  if ($NewValue !== self::NOVALUE)
3648  {
3649  self::$RootUrlOverride = strlen(trim($NewValue)) ? $NewValue : NULL;
3650  }
3651  return self::$RootUrlOverride;
3652  }
3653 
3663  public static function BaseUrl()
3664  {
3665  $BaseUrl = self::RootUrl().dirname($_SERVER["SCRIPT_NAME"]);
3666  if (substr($BaseUrl, -1) != "/") { $BaseUrl .= "/"; }
3667  return $BaseUrl;
3668  }
3669 
3677  public static function FullUrl()
3678  {
3679  return self::RootUrl().$_SERVER["REQUEST_URI"];
3680  }
3681 
3692  public static function PreferHttpHost($NewValue = NULL)
3693  {
3694  if ($NewValue !== NULL)
3695  {
3696  self::$PreferHttpHost = ($NewValue ? TRUE : FALSE);
3697  }
3698  return self::$PreferHttpHost;
3699  }
3700 
3705  public static function BasePath()
3706  {
3707  $BasePath = dirname($_SERVER["SCRIPT_NAME"]);
3708 
3709  if (substr($BasePath, -1) != "/")
3710  {
3711  $BasePath .= "/";
3712  }
3713 
3714  return $BasePath;
3715  }
3716 
3722  public static function GetScriptUrl()
3723  {
3724  if (array_key_exists("SCRIPT_URL", $_SERVER))
3725  {
3726  return $_SERVER["SCRIPT_URL"];
3727  }
3728  elseif (array_key_exists("REQUEST_URI", $_SERVER))
3729  {
3730  $Pieces = parse_url($_SERVER["REQUEST_URI"]);
3731  return isset($Pieces["path"]) ? $Pieces["path"] : NULL;
3732  }
3733  elseif (array_key_exists("REDIRECT_URL", $_SERVER))
3734  {
3735  return $_SERVER["REDIRECT_URL"];
3736  }
3737  else
3738  {
3739  return NULL;
3740  }
3741  }
3742 
3751  public static function WasUrlRewritten($ScriptName="index.php")
3752  {
3753  # needed to get the path of the URL minus the query and fragment pieces
3754  $Components = parse_url(self::GetScriptUrl());
3755 
3756  # if parsing was successful and a path is set
3757  if (is_array($Components) && isset($Components["path"]))
3758  {
3759  $BasePath = self::BasePath();
3760  $Path = $Components["path"];
3761 
3762  # the URL was rewritten if the path isn't the base path, i.e., the
3763  # home page, and the file in the URL isn't the script generating the
3764  # page
3765  if ($BasePath != $Path && basename($Path) != $ScriptName)
3766  {
3767  return TRUE;
3768  }
3769  }
3770 
3771  # the URL wasn't rewritten
3772  return FALSE;
3773  }
3774 
3784  public static function ReachedViaAjax($NewSetting = NULL)
3785  {
3786  if ($NewSetting !== NULL)
3787  {
3788  self::$IsAjaxPageLoad = $NewSetting;
3789  }
3790 
3791  if (isset(self::$IsAjaxPageLoad))
3792  {
3793  return self::$IsAjaxPageLoad;
3794  }
3795  elseif (isset($_SERVER["HTTP_X_REQUESTED_WITH"])
3796  && (strtolower($_SERVER["HTTP_X_REQUESTED_WITH"])
3797  == "xmlhttprequest"))
3798  {
3799  return TRUE;
3800  }
3801  else
3802  {
3803  return FALSE;
3804  }
3805  }
3806 
3812  public static function GetFreeMemory()
3813  {
3814  return self::GetPhpMemoryLimit() - memory_get_usage(TRUE);
3815  }
3816 
3822  public static function GetPhpMemoryLimit()
3823  {
3824  $Str = strtoupper(ini_get("memory_limit"));
3825  if (substr($Str, -1) == "B") { $Str = substr($Str, 0, strlen($Str) - 1); }
3826  switch (substr($Str, -1))
3827  {
3828  case "K":
3829  $MemoryLimit = (int)$Str * 1024;
3830  break;
3831 
3832  case "M":
3833  $MemoryLimit = (int)$Str * 1048576;
3834  break;
3835 
3836  case "G":
3837  $MemoryLimit = (int)$Str * 1073741824;
3838  break;
3839 
3840  default:
3841  $MemoryLimit = (int)$Str;
3842  break;
3843  }
3844  return $MemoryLimit;
3845  }
3846 
3859  public function MaxExecutionTime($NewValue = DB_NOVALUE, $Persistent = FALSE)
3860  {
3861  if ($NewValue !== DB_NOVALUE)
3862  {
3863  $NewValue = max($NewValue, 5);
3864  ini_set("max_execution_time", $NewValue);
3865  set_time_limit($NewValue - $this->GetElapsedExecutionTime());
3866  $this->UpdateSetting("MaxExecTime", $NewValue, $Persistent);
3867  }
3868  return ini_get("max_execution_time");
3869  }
3870 
3871  /*@)*/ /* Server Environment */
3872 
3873 
3874  # ---- Utility -----------------------------------------------------------
3875  /*@(*/
3877 
3889  public function DownloadFile($FilePath, $FileName = NULL, $MimeType = NULL)
3890  {
3891  # check that file is readable
3892  if (!is_readable($FilePath))
3893  {
3894  return FALSE;
3895  }
3896 
3897  # if file name was not supplied
3898  if ($FileName === NULL)
3899  {
3900  # extract file name from path
3901  $FileName = basename($FilePath);
3902  }
3903 
3904  # if MIME type was not supplied
3905  if ($MimeType === NULL)
3906  {
3907  # attempt to determine MIME type
3908  $FInfoHandle = finfo_open(FILEINFO_MIME);
3909  if ($FInfoHandle)
3910  {
3911  $FInfoMime = finfo_file($FInfoHandle, $FilePath);
3912  finfo_close($FInfoHandle);
3913  if ($FInfoMime)
3914  {
3915  $MimeType = $FInfoMime;
3916  }
3917  }
3918 
3919  # use default if unable to determine MIME type
3920  if ($MimeType === NULL)
3921  {
3922  $MimeType = "application/octet-stream";
3923  }
3924  }
3925 
3926  # set headers to download file
3927  header("Content-Type: ".$MimeType);
3928  header("Content-Length: ".filesize($FilePath));
3929  if ($this->CleanUrlRewritePerformed)
3930  {
3931  header('Content-Disposition: attachment; filename="'.$FileName.'"');
3932  }
3933 
3934  # make sure that apache does not attempt to compress file
3935  apache_setenv('no-gzip', '1');
3936 
3937  # send file to user, but unbuffered to avoid memory issues
3938  $this->AddUnbufferedCallback(function ($File)
3939  {
3940  $BlockSize = 512000;
3941 
3942  $Handle = @fopen($File, "rb");
3943  if ($Handle === FALSE)
3944  {
3945  return;
3946  }
3947 
3948  # (close out session, making it read-only, so that session file
3949  # lock is released and others are not potentially hanging
3950  # waiting for it while the download completes)
3951  session_write_close();
3952 
3953  while (!feof($Handle))
3954  {
3955  print fread($Handle, $BlockSize);
3956  flush();
3957  }
3958 
3959  fclose($Handle);
3960  }, array($FilePath));
3961 
3962  # prevent HTML output that might interfere with download
3963  $this->SuppressHTMLOutput();
3964 
3965  # set flag to indicate not to log a slow page load in case client
3966  # connection delays PHP execution because of header
3967  $this->DoNotLogSlowPageLoad = TRUE;
3968 
3969  # report no errors found to caller
3970  return TRUE;
3971  }
3972 
3985  public function GetLock($LockName, $Wait = TRUE)
3986  {
3987  # assume we will not get a lock
3988  $GotLock = FALSE;
3989 
3990  # clear out any stale locks
3991  static $CleanupHasBeenDone = FALSE;
3992  if (!$CleanupHasBeenDone)
3993  {
3994  # (margin for clearing stale locks is twice the known
3995  # maximum PHP execution time, because the max time
3996  # techinically does not include external operations
3997  # like database queries)
3998  $ClearLocksObtainedBefore = date(StdLib::SQL_DATE_FORMAT,
3999  (time() - ($this->MaxExecutionTime() * 2)));
4000  $this->DB->Query("DELETE FROM AF_Locks WHERE"
4001  ." ObtainedAt < '".$ClearLocksObtainedBefore."' AND"
4002  ." LockName = '".addslashes($LockName)."'");
4003  }
4004 
4005  do
4006  {
4007  # lock database table so nobody else can try to get a lock
4008  $this->DB->Query("LOCK TABLES AF_Locks WRITE");
4009 
4010  # look for lock with specified name
4011  $FoundCount = $this->DB->Query("SELECT COUNT(*) AS FoundCount"
4012  ." FROM AF_Locks WHERE LockName = '"
4013  .addslashes($LockName)."'", "FoundCount");
4014  $LockFound = ($FoundCount > 0) ? TRUE : FALSE;
4015 
4016  # if lock found
4017  if ($LockFound)
4018  {
4019  # unlock database tables
4020  $this->DB->Query("UNLOCK TABLES");
4021 
4022  # if blocking was requested
4023  if ($Wait)
4024  {
4025  # wait to give someone else a chance to release lock
4026  sleep(2);
4027  }
4028  }
4029  // @codingStandardsIgnoreStart
4030  // (because phpcs does not correctly handle do-while loops)
4031  # while lock was found and blocking was requested
4032  } while ($LockFound && $Wait);
4033  // @codingStandardsIgnoreEnd
4034 
4035  # if lock was not found
4036  if (!$LockFound)
4037  {
4038  # get our lock
4039  $this->DB->Query("INSERT INTO AF_Locks (LockName) VALUES ('"
4040  .addslashes($LockName)."')");
4041  $GotLock = TRUE;
4042 
4043  # unlock database tables
4044  $this->DB->Query("UNLOCK TABLES");
4045  }
4046 
4047  # report to caller whether lock was obtained
4048  return $GotLock;
4049  }
4050 
4058  public function ReleaseLock($LockName)
4059  {
4060  # release any existing locks
4061  $this->DB->Query("DELETE FROM AF_Locks WHERE LockName = '"
4062  .addslashes($LockName)."'");
4063 
4064  # report to caller whether existing lock was released
4065  return $this->DB->NumRowsAffected() ? TRUE : FALSE;
4066  }
4067 
4068  /*@)*/ /* Utility */
4069 
4070 
4071  # ---- Backward Compatibility --------------------------------------------
4072  /*@(*/
4074 
4081  public function FindCommonTemplate($BaseName)
4082  {
4083  return $this->FindFile(
4084  $this->IncludeDirList, $BaseName, array("tpl", "html"));
4085  }
4086 
4087  /*@)*/ /* Backward Compatibility */
4088 
4089 
4090  # ---- PRIVATE INTERFACE -------------------------------------------------
4091 
4092  private $AdditionalRequiredUIFiles = array();
4093  private $AlternateDomainPrefixes = array();
4094  private $BackgroundTaskMemLeakLogThreshold = 10; # percentage of max mem
4095  private $BackgroundTaskMinFreeMemPercent = 25;
4096  private $BrowserDetectFunc;
4097  private $CacheCurrentPage = TRUE;
4098  private $CleanUrlMappings = array();
4099  private $CleanUrlRewritePerformed = FALSE;
4100  private $ContextFilters = array(
4101  self::CONTEXT_START => TRUE,
4102  self::CONTEXT_PAGE => array("H_"),
4103  self::CONTEXT_COMMON => array("H_"),
4104  );
4105  private $CssUrlFingerprintPath;
4106  private $DB;
4107  private $DefaultPage = "Home";
4108  private $DoNotMinimizeList = array();
4109  private $DoNotLogSlowPageLoad = FALSE;
4110  private $EnvIncludes = array();
4111  private $ExecutionStartTime;
4112  private $FoundUIFiles = array();
4113  private $HtmlCharset = "UTF-8";
4114  private $InterfaceSettings = array();
4115  private $JSMinimizerJavaScriptPackerAvailable = FALSE;
4116  private $JSMinimizerJShrinkAvailable = TRUE;
4117  private $JumpToPage = NULL;
4118  private $JumpToPageDelay = 0;
4119  private $LogFileName = "local/logs/site.log";
4120  private $MaxRunningTasksToTrack = 250;
4121  private $MetaTags;
4122  private $OutputModificationCallbackInfo;
4123  private $OutputModificationCallbacks = array();
4124  private $OutputModificationPatterns = array();
4125  private $OutputModificationReplacements = array();
4126  private $PageCacheTags = array();
4127  private $PageName;
4128  private $PostProcessingFuncs = array();
4129  private $RequeueCurrentTask;
4130  private $RunningInBackground = FALSE;
4131  private $RunningTask;
4132  private $SavedContext;
4133  private $SaveTemplateLocationCache = FALSE;
4134  private $SessionStorage;
4135  private $SessionGcProbability;
4136  private $Settings;
4137  private $SuppressHTML = FALSE;
4138  private $SuppressStdPageStartAndEnd = FALSE;
4139  private $TemplateLocationCache;
4140  private $TemplateLocationCacheInterval = 60; # in minutes
4141  private $TemplateLocationCacheExpiration;
4142  private $UnbufferedCallbacks = array();
4143  private $UrlFingerprintBlacklist = array();
4144  private $UseBaseTag = FALSE;
4145 
4146  private static $ActiveUI = "default";
4147  private static $AppName = "ScoutAF";
4148  private static $DefaultUI = "default";
4149  private static $IsAjaxPageLoad;
4150  private static $JSMinCacheDir = "local/data/caches/JSMin";
4151  private static $ObjectDirectories = array();
4152  private static $ObjectLocationCache;
4153  private static $ObjectLocationCacheInterval = 60;
4154  private static $ObjectLocationCacheExpiration;
4155  private static $PreferHttpHost = FALSE;
4156  private static $RootUrlOverride = NULL;
4157  private static $SaveObjectLocationCache = FALSE;
4158  private static $ScssCacheDir = "local/data/caches/SCSS";
4159  private static $SessionLifetime = 1440; # in seconds
4160 
4161  # offset used to generate page cache tag IDs from numeric tags
4162  const PAGECACHETAGIDOFFSET = 100000;
4163 
4164  # minimum expired session garbage collection probability
4165  const MIN_GC_PROBABILITY = 0.01;
4166 
4171  private $NoTSR = FALSE;
4172 
4173  private $KnownPeriodicEvents = array();
4174  private $PeriodicEvents = array(
4175  "EVENT_HOURLY" => self::EVENTTYPE_DEFAULT,
4176  "EVENT_DAILY" => self::EVENTTYPE_DEFAULT,
4177  "EVENT_WEEKLY" => self::EVENTTYPE_DEFAULT,
4178  "EVENT_MONTHLY" => self::EVENTTYPE_DEFAULT,
4179  "EVENT_PERIODIC" => self::EVENTTYPE_NAMED,
4180  );
4181  private $EventPeriods = array(
4182  "EVENT_HOURLY" => 3600,
4183  "EVENT_DAILY" => 86400,
4184  "EVENT_WEEKLY" => 604800,
4185  "EVENT_MONTHLY" => 2592000,
4186  "EVENT_PERIODIC" => 0,
4187  );
4188  private $UIEvents = array(
4189  "EVENT_PAGE_LOAD" => self::EVENTTYPE_DEFAULT,
4190  "EVENT_PHP_FILE_LOAD" => self::EVENTTYPE_CHAIN,
4191  "EVENT_PHP_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
4192  "EVENT_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
4193  "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
4194  "EVENT_PAGE_OUTPUT_FILTER" => self::EVENTTYPE_CHAIN,
4195  );
4196 
4201  private function LoadSettings()
4202  {
4203  # read settings in from database
4204  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
4205  $this->Settings = $this->DB->FetchRow();
4206 
4207  # if settings were not previously initialized
4208  if ($this->Settings === FALSE)
4209  {
4210  # initialize settings in database
4211  $this->DB->Query("INSERT INTO ApplicationFrameworkSettings"
4212  ." (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");
4213 
4214  # read new settings in from database
4215  $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
4216  $this->Settings = $this->DB->FetchRow();
4217 
4218  # bail out if reloading new settings failed
4219  if ($this->Settings === FALSE)
4220  {
4221  throw new Exception(
4222  "Unable to load application framework settings.");
4223  }
4224  }
4225 
4226  # if base path was not previously set or we appear to have moved
4227  if (!array_key_exists("BasePath", $this->Settings)
4228  || (!strlen($this->Settings["BasePath"]))
4229  || (!array_key_exists("BasePathCheck", $this->Settings))
4230  || (__FILE__ != $this->Settings["BasePathCheck"]))
4231  {
4232  # attempt to extract base path from Apache .htaccess file
4233  if (is_readable(".htaccess"))
4234  {
4235  $Lines = file(".htaccess");
4236  foreach ($Lines as $Line)
4237  {
4238  if (preg_match("/\\s*RewriteBase\\s+/", $Line))
4239  {
4240  $Pieces = preg_split(
4241  "/\\s+/", $Line, NULL, PREG_SPLIT_NO_EMPTY);
4242  $BasePath = $Pieces[1];
4243  }
4244  }
4245  }
4246 
4247  # if base path was found
4248  if (isset($BasePath))
4249  {
4250  # save base path locally
4251  $this->Settings["BasePath"] = $BasePath;
4252 
4253  # save base path to database
4254  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
4255  ." SET BasePath = '".addslashes($BasePath)."'"
4256  .", BasePathCheck = '".addslashes(__FILE__)."'");
4257  }
4258  }
4259 
4260  # retrieve template location cache
4261  $this->TemplateLocationCache = unserialize(
4262  $this->Settings["TemplateLocationCache"]);
4263  $this->TemplateLocationCacheInterval =
4264  $this->Settings["TemplateLocationCacheInterval"];
4265  $this->TemplateLocationCacheExpiration =
4266  strtotime($this->Settings["TemplateLocationCacheExpiration"]);
4267 
4268  # if template location cache looks invalid or has expired
4269  $CurrentTime = time();
4270  if (!count($this->TemplateLocationCache)
4271  || ($CurrentTime >= $this->TemplateLocationCacheExpiration))
4272  {
4273  # clear cache and reset cache expiration
4274  $this->TemplateLocationCache = array();
4275  $this->TemplateLocationCacheExpiration =
4276  $CurrentTime + ($this->TemplateLocationCacheInterval * 60);
4277  $this->SaveTemplateLocationCache = TRUE;
4278  }
4279 
4280  # retrieve object location cache
4281  self::$ObjectLocationCache =
4282  unserialize($this->Settings["ObjectLocationCache"]);
4283  self::$ObjectLocationCacheInterval =
4284  $this->Settings["ObjectLocationCacheInterval"];
4285  self::$ObjectLocationCacheExpiration =
4286  strtotime($this->Settings["ObjectLocationCacheExpiration"]);
4287 
4288  # if object location cache looks invalid or has expired
4289  if (!count(self::$ObjectLocationCache)
4290  || ($CurrentTime >= self::$ObjectLocationCacheExpiration))
4291  {
4292  # clear cache and reset cache expiration
4293  self::$ObjectLocationCache = array();
4294  self::$ObjectLocationCacheExpiration =
4295  $CurrentTime + (self::$ObjectLocationCacheInterval * 60);
4296  self::$SaveObjectLocationCache = TRUE;
4297  }
4298  }
4299 
4306  private function RewriteCleanUrls($PageName)
4307  {
4308  # if URL rewriting is supported by the server
4309  if ($this->HtaccessSupport())
4310  {
4311  # retrieve current URL and remove base path if present
4312  $Url = $this->GetPageLocation();
4313 
4314  # for each clean URL mapping
4315  foreach ($this->CleanUrlMappings as $Info)
4316  {
4317  # if current URL matches clean URL pattern
4318  if (preg_match($Info["Pattern"], $Url, $Matches))
4319  {
4320  # set new page
4321  $PageName = $Info["Page"];
4322 
4323  # if $_GET variables specified for clean URL
4324  if ($Info["GetVars"] !== NULL)
4325  {
4326  # for each $_GET variable specified for clean URL
4327  foreach ($Info["GetVars"] as $VarName => $VarTemplate)
4328  {
4329  # start with template for variable value
4330  $Value = $VarTemplate;
4331 
4332  # for each subpattern matched in current URL
4333  foreach ($Matches as $Index => $Match)
4334  {
4335  # if not first (whole) match
4336  if ($Index > 0)
4337  {
4338  # make any substitutions in template
4339  $Value = str_replace("$".$Index, $Match, $Value);
4340  }
4341  }
4342 
4343  # set $_GET variable
4344  $_GET[$VarName] = $Value;
4345  }
4346  }
4347 
4348  # set flag indicating clean URL mapped
4349  $this->CleanUrlRewritePerformed = TRUE;
4350 
4351  # stop looking for a mapping
4352  break;
4353  }
4354  }
4355  }
4356 
4357  # return (possibly) updated page name to caller
4358  return $PageName;
4359  }
4360 
4373  private function RewriteAlternateDomainUrls($Html)
4374  {
4375  # if we were loaded via an alternate domain, and we have a
4376  # RootUrlOverride configured to tell us which domain is the
4377  # primary, and if rewriting support is enabled, then we can
4378  # handle URL Rewriting
4379  if ($this->LoadedViaAlternateDomain() &&
4380  self::$RootUrlOverride !== NULL &&
4381  $this->HtaccessSupport())
4382  {
4383  # pull out the configured prefix for this domain
4384  $VHost = $_SERVER["SERVER_NAME"];
4385  $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
4386 
4387  # get the URL for the primary domain, including the base path
4388  # (usually the part between the host name and the PHP file name)
4389  $RootUrl = $this->RootUrl().self::BasePath();
4390 
4391  # and figure out what protcol we were using
4392  $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
4393 
4394  # NB: preg_replace iterates through the configured
4395  # search/replacement pairs, such that the second one
4396  # runs after the first and so on
4397 
4398  # the first n-1 patterns below convert any relative
4399  # links in the generated HTML to absolute links using
4400  # our primary domain (e.g., for stylesheets, javascript,
4401  # images, etc)
4402 
4403  # the nth pattern looks for links that live within the
4404  # path subtree specified by our configured prefix on
4405  # our primary domain, then replaces them with equivalent
4406  # links on our secondary domain
4407 
4408  # for example, if our primary domain is
4409  # example.com/MySite and our secondary domain is
4410  # things.example.org/MySite with 'things' as the
4411  # configured prefix, then this last pattern will look
4412  # for example.com/MySite/things and replace it with
4413  # things.example.org/MySite
4414  $RelativePathPatterns = array(
4415  "%src=\"(?!http://|https://)%i",
4416  "%src='(?!http://|https://)%i",
4417  "%href=\"(?!http://|https://)%i",
4418  "%href='(?!http://|https://)%i",
4419  "%action=\"(?!http://|https://)%i",
4420  "%action='(?!http://|https://)%i",
4421  "%@import\s+url\(\"(?!http://|https://)%i",
4422  "%@import\s+url\('(?!http://|https://)%i",
4423  "%src:\s+url\(\"(?!http://|https://)%i",
4424  "%src:\s+url\('(?!http://|https://)%i",
4425  "%@import\s+\"(?!http://|https://)%i",
4426  "%@import\s+'(?!http://|https://)%i",
4427  "%".preg_quote($RootUrl.$ThisPrefix."/", "%")."%",
4428  );
4429  $RelativePathReplacements = array(
4430  "src=\"".$RootUrl,
4431  "src='".$RootUrl,
4432  "href=\"".$RootUrl,
4433  "href='".$RootUrl,
4434  "action=\"".$RootUrl,
4435  "action='".$RootUrl,
4436  "@import url(\"".$RootUrl,
4437  "@import url('".$RootUrl,
4438  "src: url(\"".$RootUrl,
4439  "src: url('".$RootUrl,
4440  "@import \"".$RootUrl,
4441  "@import '".$RootUrl,
4442  $Protocol."://".$VHost.self::BasePath(),
4443  );
4444 
4445  $NewHtml = preg_replace(
4446  $RelativePathPatterns,
4447  $RelativePathReplacements,
4448  $Html);
4449 
4450  # check to make sure relative path fixes didn't fail
4451  $Html = $this->CheckOutputModification(
4452  $Html, $NewHtml,
4453  "alternate domain substitutions");
4454  }
4455 
4456  return $Html;
4457  }
4458 
4463  private function LoadedViaAlternateDomain()
4464  {
4465  return (isset($_SERVER["SERVER_NAME"]) &&
4466  isset($this->AlternateDomainPrefixes[$_SERVER["SERVER_NAME"]])) ?
4467  TRUE : FALSE ;
4468  }
4469 
4488  private function FindFile($DirectoryList, $BaseName,
4489  $PossibleSuffixes = NULL, $PossiblePrefixes = NULL)
4490  {
4491  # generate template cache index for this page
4492  $CacheIndex = md5(serialize($DirectoryList))
4493  .self::$DefaultUI.self::$ActiveUI.$BaseName;
4494 
4495  # if caching is enabled and we have cached location
4496  if (($this->TemplateLocationCacheInterval > 0)
4497  && array_key_exists($CacheIndex,
4498  $this->TemplateLocationCache))
4499  {
4500  # use template location from cache
4501  $FoundFileName = $this->TemplateLocationCache[$CacheIndex];
4502  }
4503  else
4504  {
4505  # if suffixes specified and base name does not include suffix
4506  if (count($PossibleSuffixes)
4507  && !preg_match("/\.[a-zA-Z0-9]+$/", $BaseName))
4508  {
4509  # add versions of file names with suffixes to file name list
4510  $FileNames = array();
4511  foreach ($PossibleSuffixes as $Suffix)
4512  {
4513  $FileNames[] = $BaseName.".".$Suffix;
4514  }
4515  }
4516  else
4517  {
4518  # use base name as file name
4519  $FileNames = array($BaseName);
4520  }
4521 
4522  # if prefixes specified
4523  if (count($PossiblePrefixes))
4524  {
4525  # add versions of file names with prefixes to file name list
4526  $NewFileNames = array();
4527  foreach ($FileNames as $FileName)
4528  {
4529  foreach ($PossiblePrefixes as $Prefix)
4530  {
4531  $NewFileNames[] = $Prefix.$FileName;
4532  }
4533  }
4534  $FileNames = $NewFileNames;
4535  }
4536 
4537  # expand directory list to include variants
4538  $DirectoryList = $this->ExpandDirectoryList($DirectoryList);
4539 
4540  # for each possible location
4541  $FoundFileName = NULL;
4542  foreach ($DirectoryList as $Dir)
4543  {
4544  # for each possible file name
4545  foreach ($FileNames as $File)
4546  {
4547  # if template is found at location
4548  if (file_exists($Dir.$File))
4549  {
4550  # save full template file name and stop looking
4551  $FoundFileName = $Dir.$File;
4552  break 2;
4553  }
4554  }
4555  }
4556 
4557  # save location in cache
4558  $this->TemplateLocationCache[$CacheIndex]
4559  = $FoundFileName;
4560 
4561  # set flag indicating that cache should be saved
4562  $this->SaveTemplateLocationCache = TRUE;
4563  }
4564 
4565  # return full template file name to caller
4566  return $FoundFileName;
4567  }
4568 
4575  private function ExpandDirectoryList($DirList)
4576  {
4577  # generate lookup for supplied list
4578  $ExpandedListKey = md5(serialize($DirList)
4579  .self::$DefaultUI.self::$ActiveUI);
4580 
4581  # if we already have expanded version of supplied list
4582  if (isset($this->ExpandedDirectoryLists[$ExpandedListKey]))
4583  {
4584  # return expanded version to caller
4585  return $this->ExpandedDirectoryLists[$ExpandedListKey];
4586  }
4587 
4588  # for each directory in list
4589  $ExpDirList = array();
4590  foreach ($DirList as $Dir)
4591  {
4592  # if directory includes substitution keyword
4593  if ((strpos($Dir, "%DEFAULTUI%") !== FALSE)
4594  || (strpos($Dir, "%ACTIVEUI%") !== FALSE))
4595  {
4596  # start with empty new list segment
4597  $ExpDirListSegment = array();
4598 
4599  # use default values for initial parent
4600  $ParentInterface = array(self::$ActiveUI, self::$DefaultUI);
4601 
4602  do
4603  {
4604  # substitute in for keyword on parent
4605  $CurrDir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4606  $ParentInterface, $Dir);
4607 
4608  # add local version of parent directory to new list segment
4609  $ExpDirListSegment[] = "local/".$CurrDir;
4610 
4611  # add parent directory to new list segment
4612  $ExpDirListSegment[] = $CurrDir;
4613 
4614  # look for new parent interface
4615  $ParentInterface = $this->GetInterfaceSetting(
4616  $CurrDir, "ParentInterface");
4617 
4618  # repeat if parent is available
4619  } while (strlen($ParentInterface));
4620 
4621  # add new list segment to expanded list
4622  $ExpDirList = array_merge($ExpDirList, $ExpDirListSegment);
4623  }
4624  else
4625  {
4626  # add local version of directory to expanded list
4627  $ExpDirList[] = "local/".$Dir;
4628 
4629  # add directory to expanded list
4630  $ExpDirList[] = $Dir;
4631  }
4632  }
4633 
4634  # return expanded version to caller
4635  $this->ExpandedDirectoryLists[$ExpandedListKey] = $ExpDirList;
4636  return $this->ExpandedDirectoryLists[$ExpandedListKey];
4637  }
4638 
4647  private function GetInterfaceSetting($InterfaceDir, $SettingName = NULL)
4648  {
4649  # extract canonical interface name and base interface directory
4650  preg_match("%(.*interface/)([^/]+)%", $InterfaceDir, $Matches);
4651  $InterfaceDir = (count($Matches) > 2)
4652  ? $Matches[1].$Matches[2] : $InterfaceDir;
4653  $InterfaceName = (count($Matches) > 2)
4654  ? $Matches[2] : "UNKNOWN";
4655 
4656  # if we do not have settings for interface
4657  if (!isset($this->InterfaceSettings[$InterfaceName]))
4658  {
4659  # load default values for settings
4660  $this->InterfaceSettings[$InterfaceName] = array(
4661  "Source" => "",
4662  );
4663  }
4664 
4665  # if directory takes precedence over existing settings source
4666  # ("takes precendence" == is more local == longer directory length)
4667  if (strlen($InterfaceDir)
4668  > strlen($this->InterfaceSettings[$InterfaceName]["Source"]))
4669  {
4670  # if settings file exists in directory
4671  $SettingsFile = $InterfaceDir."/interface.ini";
4672  if (is_readable($SettingsFile))
4673  {
4674  # read in values from file
4675  $NewSettings = parse_ini_file($SettingsFile);
4676 
4677  # merge in values with existing settings
4678  $this->InterfaceSettings[$InterfaceName] = array_merge(
4679  $this->InterfaceSettings[$InterfaceName], $NewSettings);
4680 
4681  # save new source of settings
4682  $this->InterfaceSettings[$InterfaceName]["Source"] = $InterfaceDir;
4683  }
4684  }
4685 
4686  # return interface settings to caller
4687  return $SettingName
4688  ? (isset($this->InterfaceSettings[$InterfaceName][$SettingName])
4689  ? $this->InterfaceSettings[$InterfaceName][$SettingName]
4690  : NULL)
4691  : $this->InterfaceSettings[$InterfaceName];
4692  }
4693 
4702  private function CompileScssFile($SrcFile)
4703  {
4704  # build path to CSS file
4705  $DstFile = self::$ScssCacheDir."/".dirname($SrcFile)
4706  ."/".basename($SrcFile);
4707  $DstFile = substr_replace($DstFile, "css", -4);
4708 
4709  # if SCSS file is newer than CSS file
4710  if (!file_exists($DstFile)
4711  || (filemtime($SrcFile) > filemtime($DstFile)))
4712  {
4713  # attempt to create CSS cache subdirectory if not present
4714  if (!is_dir(dirname($DstFile)))
4715  {
4716  @mkdir(dirname($DstFile), 0777, TRUE);
4717  }
4718 
4719  # if CSS cache directory and CSS file path appear writable
4720  static $CacheDirIsWritable;
4721  if (!isset($CacheDirIsWritable))
4722  { $CacheDirIsWritable = is_writable(self::$ScssCacheDir); }
4723  if (is_writable($DstFile)
4724  || (!file_exists($DstFile) && $CacheDirIsWritable))
4725  {
4726  # load SCSS and compile to CSS
4727  $ScssCode = file_get_contents($SrcFile);
4728  $ScssCompiler = new scssc();
4729  $ScssCompiler->setFormatter($this->GenerateCompactCss()
4730  ? "scss_formatter_compressed" : "scss_formatter");
4731  try
4732  {
4733  $CssCode = $ScssCompiler->compile($ScssCode);
4734 
4735  # add fingerprinting for URLs in CSS
4736  $this->CssUrlFingerprintPath = dirname($SrcFile);
4737  $CssCode = preg_replace_callback(
4738  "/url\((['\"]*)(.+)\.([a-z]+)(['\"]*)\)/",
4739  array($this, "CssUrlFingerprintInsertion"),
4740  $CssCode);
4741 
4742  # strip out comments from CSS (if requested)
4743  if ($this->GenerateCompactCss())
4744  {
4745  $CssCode = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!',
4746  '', $CssCode);
4747  }
4748 
4749  # write out CSS file
4750  file_put_contents($DstFile, $CssCode);
4751  }
4752  catch (Exception $Ex)
4753  {
4754  $this->LogError(self::LOGLVL_ERROR,
4755  "Error compiling SCSS file ".$SrcFile.": "
4756  .$Ex->getMessage());
4757  $DstFile = NULL;
4758  }
4759  }
4760  else
4761  {
4762  # log error and set CSS file path to indicate failure
4763  $this->LogError(self::LOGLVL_ERROR,
4764  "Unable to write out CSS file (compiled from SCSS) to "
4765  .$DstFile);
4766  $DstFile = NULL;
4767  }
4768  }
4769 
4770  # return CSS file path to caller
4771  return $DstFile;
4772  }
4773 
4781  private function MinimizeJavascriptFile($SrcFile)
4782  {
4783  # bail out if file is on exclusion list
4784  foreach ($this->DoNotMinimizeList as $DNMFile)
4785  {
4786  if (($SrcFile == $DNMFile) || (basename($SrcFile) == $DNMFile))
4787  {
4788  return NULL;
4789  }
4790  }
4791 
4792  # build path to minimized file
4793  $DstFile = self::$JSMinCacheDir."/".dirname($SrcFile)
4794  ."/".basename($SrcFile);
4795  $DstFile = substr_replace($DstFile, ".min", -3, 0);
4796 
4797  # if original file is newer than minimized file
4798  if (!file_exists($DstFile)
4799  || (filemtime($SrcFile) > filemtime($DstFile)))
4800  {
4801  # attempt to create cache subdirectory if not present
4802  if (!is_dir(dirname($DstFile)))
4803  {
4804  @mkdir(dirname($DstFile), 0777, TRUE);
4805  }
4806 
4807  # if cache directory and minimized file path appear writable
4808  static $CacheDirIsWritable;
4809  if (!isset($CacheDirIsWritable))
4810  { $CacheDirIsWritable = is_writable(self::$JSMinCacheDir); }
4811  if (is_writable($DstFile)
4812  || (!file_exists($DstFile) && $CacheDirIsWritable))
4813  {
4814  # load JavaScript code
4815  $Code = file_get_contents($SrcFile);
4816 
4817  # decide which minimizer to use
4818  if ($this->JSMinimizerJavaScriptPackerAvailable
4819  && $this->JSMinimizerJShrinkAvailable)
4820  {
4821  $Minimizer = (strlen($Code) < 5000)
4822  ? "JShrink" : "JavaScriptPacker";
4823  }
4824  elseif ($this->JSMinimizerJShrinkAvailable)
4825  {
4826  $Minimizer = "JShrink";
4827  }
4828  else
4829  {
4830  $Minimizer = "NONE";
4831  }
4832 
4833  # minimize code
4834  switch ($Minimizer)
4835  {
4836  case "JavaScriptMinimizer":
4837  $Packer = new JavaScriptPacker($Code, "Normal");
4838  $MinimizedCode = $Packer->pack();
4839  break;
4840 
4841  case "JShrink":
4842  try
4843  {
4844  $MinimizedCode = \JShrink\Minifier::minify($Code);
4845  }
4846  catch (Exception $Exception)
4847  {
4848  unset($MinimizedCode);
4849  $MinimizeError = $Exception->getMessage();
4850  }
4851  break;
4852  }
4853 
4854  # if minimization succeeded
4855  if (isset($MinimizedCode))
4856  {
4857  # write out minimized file
4858  file_put_contents($DstFile, $MinimizedCode);
4859  }
4860  else
4861  {
4862  # log error and set destination file path to indicate failure
4863  $ErrMsg = "Unable to minimize JavaScript file ".$SrcFile;
4864  if (isset($MinimizeError))
4865  {
4866  $ErrMsg .= " (".$MinimizeError.")";
4867  }
4868  $this->LogError(self::LOGLVL_ERROR, $ErrMsg);
4869  $DstFile = NULL;
4870  }
4871  }
4872  else
4873  {
4874  # log error and set destination file path to indicate failure
4875  $this->LogError(self::LOGLVL_ERROR,
4876  "Unable to write out minimized JavaScript to file ".$DstFile);
4877  $DstFile = NULL;
4878  }
4879  }
4880 
4881  # return CSS file path to caller
4882  return $DstFile;
4883  }
4884 
4892  private function CssUrlFingerprintInsertion($Matches)
4893  {
4894  # generate fingerprint string from CSS file modification time
4895  $FileName = realpath($this->CssUrlFingerprintPath."/".
4896  $Matches[2].".".$Matches[3]);
4897  $MTime = filemtime($FileName);
4898  $Fingerprint = sprintf("%06X", ($MTime % 0xFFFFFF));
4899 
4900  # build URL string with fingerprint and return it to caller
4901  return "url(".$Matches[1].$Matches[2].".".$Fingerprint
4902  .".".$Matches[3].$Matches[4].")";
4903  }
4904 
4912  private function GetRequiredFilesNotYetLoaded($PageContentFile)
4913  {
4914  # start out assuming no files required
4915  $RequiredFiles = array();
4916 
4917  # if page content file supplied
4918  if ($PageContentFile)
4919  {
4920  # if file containing list of required files is available
4921  $Path = dirname($PageContentFile);
4922  $RequireListFile = $Path."/REQUIRES";
4923  if (file_exists($RequireListFile))
4924  {
4925  # read in list of required files
4926  $RequestedFiles = file($RequireListFile);
4927 
4928  # for each line in required file list
4929  foreach ($RequestedFiles as $Line)
4930  {
4931  # if line is not a comment
4932  $Line = trim($Line);
4933  if (!preg_match("/^#/", $Line))
4934  {
4935  # if file has not already been loaded
4936  if (!in_array($Line, $this->FoundUIFiles))
4937  {
4938  # add to list of required files
4939  $RequiredFiles[$Line] = self::ORDER_MIDDLE;
4940  }
4941  }
4942  }
4943  }
4944  }
4945 
4946  # add in additional required files if any
4947  if (count($this->AdditionalRequiredUIFiles))
4948  {
4949  # make sure there are no duplicates
4950  $AdditionalRequiredUIFiles = array_unique(
4951  $this->AdditionalRequiredUIFiles);
4952 
4953  $RequiredFiles = array_merge(
4954  $RequiredFiles, $this->AdditionalRequiredUIFiles);
4955  }
4956 
4957  # return list of required files to caller
4958  return $RequiredFiles;
4959  }
4960 
4969  private function SubBrowserIntoFileNames($FileNames)
4970  {
4971  # if a browser detection function has been made available
4972  $UpdatedFileNames = array();
4973  if (is_callable($this->BrowserDetectFunc))
4974  {
4975  # call function to get browser list
4976  $Browsers = call_user_func($this->BrowserDetectFunc);
4977 
4978  # for each required file
4979  foreach ($FileNames as $FileName => $Value)
4980  {
4981  # if file name includes browser keyword
4982  if (preg_match("/%BROWSER%/", $FileName))
4983  {
4984  # for each browser
4985  foreach ($Browsers as $Browser)
4986  {
4987  # substitute in browser name and add to new file list
4988  $NewFileName = preg_replace(
4989  "/%BROWSER%/", $Browser, $FileName);
4990  $UpdatedFileNames[$NewFileName] = $Value;
4991  }
4992  }
4993  else
4994  {
4995  # add to new file list
4996  $UpdatedFileNames[$FileName] = $Value;
4997  }
4998  }
4999  }
5000  else
5001  {
5002  # filter out any files with browser keyword in their name
5003  foreach ($FileNames as $FileName => $Value)
5004  {
5005  if (!preg_match("/%BROWSER%/", $FileName))
5006  {
5007  $UpdatedFileNames[$FileName] = $Value;
5008  }
5009  }
5010  }
5011 
5012  return $UpdatedFileNames;
5013  }
5014 
5020  private function AddMetaTagsToPageOutput($PageOutput)
5021  {
5022  if (isset($this->MetaTags))
5023  {
5024  $MetaTagSection = "";
5025  foreach ($this->MetaTags as $MetaTagAttribs)
5026  {
5027  $MetaTagSection .= "<meta";
5028  foreach ($MetaTagAttribs as
5029  $MetaTagAttribName => $MetaTagAttribValue)
5030  {
5031  $MetaTagSection .= " ".$MetaTagAttribName."=\""
5032  .htmlspecialchars(trim($MetaTagAttribValue))."\"";
5033  }
5034  $MetaTagSection .= " />\n";
5035  }
5036 
5037  if ($this->SuppressStdPageStartAndEnd)
5038  {
5039  $PageOutput = $MetaTagSection.$PageOutput;
5040  }
5041  else
5042  {
5043  $PageOutput = preg_replace("#<head>#i",
5044  "<head>\n".$MetaTagSection, $PageOutput, 1);
5045  }
5046  }
5047 
5048  return $PageOutput;
5049  }
5050 
5058  private function AddFileTagsToPageOutput($PageOutput, $Files)
5059  {
5060  # substitute browser name into names of required files as appropriate
5061  $Files = $this->SubBrowserIntoFileNames($Files);
5062 
5063  # initialize content sections
5064  $HeadContent = [
5065  self::ORDER_FIRST => "",
5066  self::ORDER_MIDDLE => "",
5067  self::ORDER_LAST => "",
5068  ];
5069  $BodyContent = [
5070  self::ORDER_FIRST => "",
5071  self::ORDER_MIDDLE => "",
5072  self::ORDER_LAST => "",
5073  ];
5074 
5075  # for each required file
5076  foreach ($Files as $File => $Order)
5077  {
5078  # locate specific file to use
5079  $FilePath = $this->GUIFile($File);
5080 
5081  # if file was found
5082  if ($FilePath)
5083  {
5084  # generate tag for file
5085  $Tag = $this->GetUIFileLoadingTag($FilePath);
5086 
5087  # add file to HTML output based on file type
5088  $FileType = $this->GetFileType($FilePath);
5089  switch ($FileType)
5090  {
5091  case self::FT_CSS:
5092  $HeadContent[$Order] .= $Tag."\n";
5093  break;
5094 
5095  case self::FT_JAVASCRIPT:
5096  $BodyContent[$Order] .= $Tag."\n";
5097  break;
5098  }
5099  }
5100  }
5101 
5102  # add content to head
5103  $Replacement = $HeadContent[self::ORDER_MIDDLE]
5104  .$HeadContent[self::ORDER_LAST];
5105  $UpdatedPageOutput = str_ireplace("</head>",
5106  $Replacement."</head>",
5107  $PageOutput, $ReplacementCount);
5108  # (if no </head> tag was found, just prepend tags to page content)
5109  if ($ReplacementCount == 0)
5110  {
5111  $PageOutput = $Replacement.$PageOutput;
5112  }
5113  # (else if multiple </head> tags found, only prepend tags to the first)
5114  elseif ($ReplacementCount > 1)
5115  {
5116  $PageOutput = preg_replace("#</head>#i",
5117  $Replacement."</head>",
5118  $PageOutput, 1);
5119  }
5120  else
5121  {
5122  $PageOutput = $UpdatedPageOutput;
5123  }
5124  $Replacement = $HeadContent[self::ORDER_FIRST];
5125  $UpdatedPageOutput = str_ireplace("<head>",
5126  "<head>\n".$Replacement,
5127  $PageOutput, $ReplacementCount);
5128  # (if no <head> tag was found, just prepend tags to page content)
5129  if ($ReplacementCount == 0)
5130  {
5131  $PageOutput = $Replacement.$PageOutput;
5132  }
5133  # (else if multiple <head> tags found, only append tags to the first)
5134  elseif ($ReplacementCount > 1)
5135  {
5136  $PageOutput = preg_replace("#<head>#i",
5137  "<head>\n".$Replacement,
5138  $PageOutput, 1);
5139  }
5140  else
5141  {
5142  $PageOutput = $UpdatedPageOutput;
5143  }
5144 
5145  # add content to body
5146  $Replacement = $BodyContent[self::ORDER_FIRST];
5147  $PageOutput = preg_replace("#<body([^>]*)>#i",
5148  "<body\\1>\n".$Replacement,
5149  $PageOutput, 1, $ReplacementCount);
5150  # (if no <body> tag was found, just append tags to page content)
5151  if ($ReplacementCount == 0)
5152  {
5153  $PageOutput = $PageOutput.$Replacement;
5154  }
5155  $Replacement = $BodyContent[self::ORDER_MIDDLE]
5156  .$BodyContent[self::ORDER_LAST];
5157  $UpdatedPageOutput = str_ireplace("</body>",
5158  $Replacement."\n</body>",
5159  $PageOutput, $ReplacementCount);
5160  # (if no </body> tag was found, just append tags to page content)
5161  if ($ReplacementCount == 0)
5162  {
5163  $PageOutput = $PageOutput.$Replacement;
5164  }
5165  # (else if multiple </body> tags found, only prepend tag to the first)
5166  elseif ($ReplacementCount > 1)
5167  {
5168  $PageOutput = preg_replace("#</body>#i",
5169  $Replacement."\n</body>",
5170  $PageOutput, 1);
5171  }
5172  else
5173  {
5174  $PageOutput = $UpdatedPageOutput;
5175  }
5176 
5177  return $PageOutput;
5178  }
5179 
5190  private function GetUIFileLoadingTag($FileName, $AdditionalAttributes = NULL)
5191  {
5192  # pad additional attributes if supplied
5193  $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
5194 
5195  # retrieve type of UI file
5196  $FileType = $this->GetFileType($FileName);
5197 
5198  # construct tag based on file type
5199  switch ($FileType)
5200  {
5201  case self::FT_CSS:
5202  $Tag = " <link rel=\"stylesheet\" type=\"text/css\""
5203  ." media=\"all\" href=\"".$FileName."\""
5204  .$AddAttribs." />\n";
5205  break;
5206 
5207  case self::FT_JAVASCRIPT:
5208  $Tag = " <script type=\"text/javascript\""
5209  ." src=\"".$FileName."\""
5210  .$AddAttribs."></script>\n";
5211  break;
5212 
5213  case self::FT_IMAGE:
5214  $Tag = "<img src=\"".$FileName."\"".$AddAttribs.">";
5215  break;
5216 
5217  default:
5218  $Tag = "";
5219  break;
5220  }
5221 
5222  # return constructed tag to caller
5223  return $Tag;
5224  }
5225 
5230  private function AutoloadObjects($ClassName)
5231  {
5232  # if caching is not turned off
5233  # and we have a cached location for class
5234  # and file at cached location is readable
5235  if ((self::$ObjectLocationCacheInterval > 0)
5236  && array_key_exists($ClassName,
5237  self::$ObjectLocationCache)
5238  && is_readable(self::$ObjectLocationCache[$ClassName]))
5239  {
5240  # use object location from cache
5241  require_once(self::$ObjectLocationCache[$ClassName]);
5242  }
5243  else
5244  {
5245  # convert any namespace separators in class name
5246  $ClassName = str_replace("\\", "-", $ClassName);
5247 
5248  # for each possible object file directory
5249  static $FileLists;
5250  foreach (self::$ObjectDirectories as $Location => $Info)
5251  {
5252  # make any needed replacements in directory path
5253  $Location = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
5254  array(self::$ActiveUI, self::$DefaultUI), $Location);
5255 
5256  # if directory looks valid
5257  if (is_dir($Location))
5258  {
5259  # build class file name
5260  $NewClassName = ($Info["ClassPattern"] && $Info["ClassReplacement"])
5261  ? preg_replace($Info["ClassPattern"],
5262  $Info["ClassReplacement"], $ClassName)
5263  : $ClassName;
5264 
5265  # read in directory contents if not already retrieved
5266  if (!isset($FileLists[$Location]))
5267  {
5268  $FileLists[$Location] = self::ReadDirectoryTree(
5269  $Location, '/^.+\.php$/i');
5270  }
5271 
5272  # for each file in target directory
5273  $FileNames = $FileLists[$Location];
5274  $TargetName = strtolower($Info["Prefix"].$NewClassName.".php");
5275  foreach ($FileNames as $FileName)
5276  {
5277  # if file matches our target object file name
5278  if (strtolower($FileName) == $TargetName)
5279  {
5280  # include object file
5281  require_once($Location.$FileName);
5282 
5283  # save location to cache
5284  self::$ObjectLocationCache[$ClassName]
5285  = $Location.$FileName;
5286 
5287  # set flag indicating that cache should be saved
5288  self::$SaveObjectLocationCache = TRUE;
5289 
5290  # stop looking
5291  break 2;
5292  }
5293  }
5294  }
5295  }
5296  }
5297  }
5298 
5306  private static function ReadDirectoryTree($Directory, $Pattern)
5307  {
5308  $CurrentDir = getcwd();
5309  chdir($Directory);
5310  $DirIter = new RecursiveDirectoryIterator(".");
5311  $IterIter = new RecursiveIteratorIterator($DirIter);
5312  $RegexResults = new RegexIterator($IterIter, $Pattern,
5313  RecursiveRegexIterator::GET_MATCH);
5314  $FileList = array();
5315  foreach ($RegexResults as $Result)
5316  {
5317  $FileList[] = substr($Result[0], 2);
5318  }
5319  chdir($CurrentDir);
5320  return $FileList;
5321  }
5322 
5327  private function LoadUIFunctions()
5328  {
5329  $Dirs = array(
5330  "local/interface/%ACTIVEUI%/include",
5331  "interface/%ACTIVEUI%/include",
5332  "local/interface/%DEFAULTUI%/include",
5333  "interface/%DEFAULTUI%/include",
5334  );
5335  foreach ($Dirs as $Dir)
5336  {
5337  $Dir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
5338  array(self::$ActiveUI, self::$DefaultUI), $Dir);
5339  if (is_dir($Dir))
5340  {
5341  $FileNames = scandir($Dir);
5342  foreach ($FileNames as $FileName)
5343  {
5344  if (preg_match("/^F-([A-Za-z0-9_]+)\.php/",
5345  $FileName, $Matches)
5346  || preg_match("/^F-([A-Za-z0-9_]+)\.html/",
5347  $FileName, $Matches))
5348  {
5349  if (!function_exists($Matches[1]))
5350  {
5351  include_once($Dir."/".$FileName);
5352  }
5353  }
5354  }
5355  }
5356  }
5357  }
5358 
5364  private function ProcessPeriodicEvent($EventName, $Callback)
5365  {
5366  # retrieve last execution time for event if available
5367  $Signature = self::GetCallbackSignature($Callback);
5368  $LastRun = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
5369  ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
5370 
5371  # determine whether enough time has passed for event to execute
5372  $ShouldExecute = (($LastRun === NULL)
5373  || (time() > (strtotime($LastRun) + $this->EventPeriods[$EventName])))
5374  ? TRUE : FALSE;
5375 
5376  # if event should run
5377  if ($ShouldExecute)
5378  {
5379  # add event to task queue
5380  $WrapperCallback = array("ApplicationFramework", "RunPeriodicEvent");
5381  $WrapperParameters = array(
5382  $EventName, $Callback, array("LastRunAt" => $LastRun));
5383  $this->QueueUniqueTask($WrapperCallback, $WrapperParameters);
5384  }
5385 
5386  # add event to list of periodic events
5387  $this->KnownPeriodicEvents[$Signature] = array(
5388  "Period" => $EventName,
5389  "Callback" => $Callback,
5390  "Queued" => $ShouldExecute);
5391  }
5392 
5398  private static function GetCallbackSignature($Callback)
5399  {
5400  return !is_array($Callback) ? $Callback
5401  : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
5402  ."::".$Callback[1];
5403  }
5404 
5409  private function PrepForTSR()
5410  {
5411  # if HTML has been output and it's time to launch another task
5412  # (only TSR if HTML has been output because otherwise browsers
5413  # may misbehave after connection is closed)
5414  if ((PHP_SAPI != "cli")
5415  && ($this->JumpToPage || !$this->SuppressHTML)
5416  && !$this->LoadedViaAlternateDomain()
5417  && (time() > (strtotime($this->Settings["LastTaskRunAt"])
5418  + ($this->MaxExecutionTime()
5419  / $this->Settings["MaxTasksRunning"]) + 5))
5420  && $this->GetTaskQueueSize()
5421  && $this->Settings["TaskExecutionEnabled"])
5422  {
5423  # begin buffering output for TSR
5424  ob_start();
5425 
5426  # let caller know it is time to launch another task
5427  return TRUE;
5428  }
5429  else
5430  {
5431  # let caller know it is not time to launch another task
5432  return FALSE;
5433  }
5434  }
5435 
5440  private function LaunchTSR()
5441  {
5442  # set headers to close out connection to browser
5443  if (!$this->NoTSR)
5444  {
5445  ignore_user_abort(TRUE);
5446  header("Connection: close");
5447  header("Content-Length: ".ob_get_length());
5448  }
5449 
5450  # output buffered content
5451  while (ob_get_level()) { ob_end_flush(); }
5452  flush();
5453 
5454  # write out any outstanding data and end HTTP session
5455  session_write_close();
5456 
5457  # set flag indicating that we are now running in background
5458  $this->RunningInBackground = TRUE;
5459 
5460  # handle garbage collection for session data
5461  if (isset($this->SessionStorage) &&
5462  (rand()/getrandmax()) <= $this->SessionGcProbability)
5463  {
5464  # determine when sessions will expire
5465  $ExpiredTime = strtotime("-". self::$SessionLifetime." seconds");
5466 
5467  # iterate over files in the session directory with a DirectoryIterator
5468  # NB: we cannot use scandir() here because it reads the
5469  # entire list of files into memory and may exceed the memory
5470  # limit for directories with very many files
5471  $DI = new DirectoryIterator($this->SessionStorage);
5472  while ($DI->valid())
5473  {
5474  if ((strpos($DI->getFilename(), "sess_") === 0) &&
5475  $DI->isFile() &&
5476  $DI->getCTime() < $ExpiredTime)
5477  {
5478  unlink($DI->getPathname());
5479  }
5480  $DI->next();
5481  }
5482  unset($DI);
5483  }
5484 
5485  # if there is still a task in the queue
5486  if ($this->GetTaskQueueSize())
5487  {
5488  # garbage collect to give as much memory as possible for tasks
5489  if (function_exists("gc_collect_cycles")) { gc_collect_cycles(); }
5490 
5491  # turn on output buffering to (hopefully) record any crash output
5492  ob_start();
5493 
5494  # lock tables and grab last task run time to double check
5495  $this->DB->Query("LOCK TABLES ApplicationFrameworkSettings WRITE");
5496  $this->LoadSettings();
5497 
5498  # if still time to launch another task
5499  if (time() > (strtotime($this->Settings["LastTaskRunAt"])
5500  + ($this->MaxExecutionTime()
5501  / $this->Settings["MaxTasksRunning"]) + 5))
5502  {
5503  # update the "last run" time and release tables
5504  $this->DB->Query("UPDATE ApplicationFrameworkSettings"
5505  ." SET LastTaskRunAt = '".date("Y-m-d H:i:s")."'");
5506  $this->DB->Query("UNLOCK TABLES");
5507 
5508  # run tasks while there is a task in the queue
5509  # and enough time and memory left
5510  do
5511  {
5512  # run the next task
5513  $this->RunNextTask();
5514 
5515  # calculate percentage of memory still available
5516  $PercentFreeMem = (self::GetFreeMemory()
5517  / self::GetPhpMemoryLimit()) * 100;
5518  }
5519  while ($this->GetTaskQueueSize()
5520  && ($this->GetSecondsBeforeTimeout() > 65)
5521  && ($PercentFreeMem > $this->BackgroundTaskMinFreeMemPercent));
5522  }
5523  else
5524  {
5525  # release tables
5526  $this->DB->Query("UNLOCK TABLES");
5527  }
5528  }
5529  }
5530 
5540  private function GetTaskList($DBQuery, $Count, $Offset)
5541  {
5542  $this->DB->Query($DBQuery." LIMIT ".intval($Offset).",".intval($Count));
5543  $Tasks = array();
5544  while ($Row = $this->DB->FetchRow())
5545  {
5546  $Tasks[$Row["TaskId"]] = $Row;
5547  if ($Row["Callback"] ==
5548  serialize(array("ApplicationFramework", "RunPeriodicEvent")))
5549  {
5550  $WrappedCallback = unserialize($Row["Parameters"]);
5551  $Tasks[$Row["TaskId"]]["Callback"] = $WrappedCallback[1];
5552  $Tasks[$Row["TaskId"]]["Parameters"] = NULL;
5553  }
5554  else
5555  {
5556  $Tasks[$Row["TaskId"]]["Callback"] = unserialize($Row["Callback"]);
5557  $Tasks[$Row["TaskId"]]["Parameters"] = unserialize($Row["Parameters"]);
5558  }
5559  }
5560  return $Tasks;
5561  }
5562 
5566  private function RunNextTask()
5567  {
5568  # lock tables to prevent same task from being run by multiple sessions
5569  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
5570 
5571  # look for task at head of queue
5572  $this->DB->Query("SELECT * FROM TaskQueue ORDER BY Priority, TaskId LIMIT 1");
5573  $Task = $this->DB->FetchRow();
5574 
5575  # if there was a task available
5576  if ($Task)
5577  {
5578  # move task from queue to running tasks list
5579  $this->DB->Query("INSERT INTO RunningTasks "
5580  ."(TaskId,Callback,Parameters,Priority,Description) "
5581  ."SELECT * FROM TaskQueue WHERE TaskId = "
5582  .intval($Task["TaskId"]));
5583  $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = "
5584  .intval($Task["TaskId"]));
5585 
5586  # release table locks to again allow other sessions to run tasks
5587  $this->DB->Query("UNLOCK TABLES");
5588 
5589  # unpack stored task info
5590  $Callback = unserialize($Task["Callback"]);
5591  $Parameters = unserialize($Task["Parameters"]);
5592 
5593  # attempt to load task callback if not already available
5594  $this->LoadFunction($Callback);
5595 
5596  # clear task requeue flag
5597  $this->RequeueCurrentTask = FALSE;
5598 
5599  # save amount of free memory for later comparison
5600  $BeforeFreeMem = self::GetFreeMemory();
5601 
5602  # run task
5603  $this->RunningTask = $Task;
5604  if ($Parameters)
5605  {
5606  call_user_func_array($Callback, $Parameters);
5607  }
5608  else
5609  {
5610  call_user_func($Callback);
5611  }
5612  unset($this->RunningTask);
5613 
5614  # log if task leaked significant memory
5615  if (function_exists("gc_collect_cycles")) { gc_collect_cycles(); }
5616  $AfterFreeMem = self::GetFreeMemory();
5617  $LeakThreshold = self::GetPhpMemoryLimit()
5618  * ($this->BackgroundTaskMemLeakLogThreshold / 100);
5619  if (($BeforeFreeMem - $AfterFreeMem) > $LeakThreshold)
5620  {
5621  $this->LogError(self::LOGLVL_DEBUG, "Task "
5622  .self::GetTaskCallbackSynopsis(
5623  $this->GetTask($Task["TaskId"]))." leaked "
5624  .number_format($BeforeFreeMem - $AfterFreeMem)." bytes.");
5625  }
5626 
5627  # if task requeue requested
5628  if ($this->RequeueCurrentTask)
5629  {
5630  # move task from running tasks list to queue
5631  $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
5632  $this->DB->Query("INSERT INTO TaskQueue"
5633  ." (Callback,Parameters,Priority,Description)"
5634  ." SELECT Callback,Parameters,Priority,Description"
5635  ." FROM RunningTasks WHERE TaskId = "
5636  .intval($Task["TaskId"]));
5637  $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = "
5638  .intval($Task["TaskId"]));
5639  $this->DB->Query("UNLOCK TABLES");
5640  }
5641  else
5642  {
5643  # remove task from running tasks list
5644  $this->DB->Query("DELETE FROM RunningTasks"
5645  ." WHERE TaskId = ".intval($Task["TaskId"]));
5646  }
5647 
5648  # prune running tasks list if necessary
5649  $RunningTasksCount = $this->DB->Query(
5650  "SELECT COUNT(*) AS TaskCount FROM RunningTasks", "TaskCount");
5651  if ($RunningTasksCount > $this->MaxRunningTasksToTrack)
5652  {
5653  $this->DB->Query("DELETE FROM RunningTasks ORDER BY StartedAt"
5654  ." LIMIT ".($RunningTasksCount - $this->MaxRunningTasksToTrack));
5655  }
5656  }
5657  else
5658  {
5659  # release table locks to again allow other sessions to run tasks
5660  $this->DB->Query("UNLOCK TABLES");
5661  }
5662  }
5663 
5669  public function OnCrash()
5670  {
5671  # attempt to remove any memory limits
5672  $FreeMemory = $this->GetFreeMemory();
5673  ini_set("memory_limit", -1);
5674 
5675  # if there is a background task currently running
5676  if (isset($this->RunningTask))
5677  {
5678  # add info about current page load
5679  $CrashInfo["ElapsedTime"] = $this->GetElapsedExecutionTime();
5680  $CrashInfo["FreeMemory"] = $FreeMemory;
5681  $CrashInfo["REMOTE_ADDR"] = $_SERVER["REMOTE_ADDR"];
5682  $CrashInfo["REQUEST_URI"] = $_SERVER["REQUEST_URI"];
5683  if (isset($_SERVER["REQUEST_TIME"]))
5684  {
5685  $CrashInfo["REQUEST_TIME"] = $_SERVER["REQUEST_TIME"];
5686  }
5687  if (isset($_SERVER["REMOTE_HOST"]))
5688  {
5689  $CrashInfo["REMOTE_HOST"] = $_SERVER["REMOTE_HOST"];
5690  }
5691 
5692  # add info about error that caused crash (if available)
5693  if (function_exists("error_get_last"))
5694  {
5695  $CrashInfo["LastError"] = error_get_last();
5696  }
5697 
5698  # add info about current output buffer contents (if available)
5699  if (ob_get_length() !== FALSE)
5700  {
5701  $CrashInfo["OutputBuffer"] = ob_get_contents();
5702  }
5703 
5704  # if backtrace info is available for the crash
5705  $Backtrace = debug_backtrace();
5706  if (count($Backtrace) > 1)
5707  {
5708  # discard the current context from the backtrace
5709  array_shift($Backtrace);
5710 
5711  # add the backtrace to the crash info
5712  $CrashInfo["Backtrace"] = $Backtrace;
5713  }
5714  # else if saved backtrace info is available
5715  elseif (isset($this->SavedContext))
5716  {
5717  # add the saved backtrace to the crash info
5718  $CrashInfo["Backtrace"] = $this->SavedContext;
5719  }
5720 
5721  # save crash info for currently running task
5722  $DB = new Database();
5723  $DB->Query("UPDATE RunningTasks SET CrashInfo = '"
5724  .addslashes(serialize($CrashInfo))
5725  ."' WHERE TaskId = ".intval($this->RunningTask["TaskId"]));
5726  }
5727 
5728  print("\n");
5729  return;
5730  }
5731 
5748  private function AddToDirList($DirList, $Dir, $SearchLast, $SkipSlashCheck)
5749  {
5750  # convert incoming directory to array of directories (if needed)
5751  $Dirs = is_array($Dir) ? $Dir : array($Dir);
5752 
5753  # reverse array so directories are searched in specified order
5754  $Dirs = array_reverse($Dirs);
5755 
5756  # for each directory
5757  foreach ($Dirs as $Location)
5758  {
5759  # make sure directory includes trailing slash
5760  if (!$SkipSlashCheck)
5761  {
5762  $Location = $Location
5763  .((substr($Location, -1) != "/") ? "/" : "");
5764  }
5765 
5766  # remove directory from list if already present
5767  if (in_array($Location, $DirList))
5768  {
5769  $DirList = array_diff(
5770  $DirList, array($Location));
5771  }
5772 
5773  # add directory to list of directories
5774  if ($SearchLast)
5775  {
5776  array_push($DirList, $Location);
5777  }
5778  else
5779  {
5780  array_unshift($DirList, $Location);
5781  }
5782  }
5783 
5784  # return updated directory list to caller
5785  return $DirList;
5786  }
5787 
5794  private function OutputModificationCallbackShell($Matches)
5795  {
5796  # call previously-stored external function
5797  return call_user_func($this->OutputModificationCallbackInfo["Callback"],
5798  $Matches,
5799  $this->OutputModificationCallbackInfo["Pattern"],
5800  $this->OutputModificationCallbackInfo["Page"],
5801  $this->OutputModificationCallbackInfo["SearchPattern"]);
5802  }
5803 
5812  private function CheckOutputModification($Original, $Modified, $ErrorInfo)
5813  {
5814  # if error was reported by regex engine
5815  if (preg_last_error() !== PREG_NO_ERROR)
5816  {
5817  # log error
5818  $this->LogError(self::LOGLVL_ERROR,
5819  "Error reported by regex engine when modifying output."
5820  ." (".$ErrorInfo.")");
5821 
5822  # use unmodified version of output
5823  $OutputToUse = $Original;
5824  }
5825  # else if modification reduced output by more than threshold
5826  elseif ((strlen(trim($Modified)) / strlen(trim($Original)))
5827  < self::OUTPUT_MODIFICATION_THRESHOLD)
5828  {
5829  # log error
5830  $this->LogError(self::LOGLVL_WARNING,
5831  "Content reduced below acceptable threshold while modifying output."
5832  ." (".$ErrorInfo.")");
5833 
5834  # use unmodified version of output
5835  $OutputToUse = $Original;
5836  }
5837  else
5838  {
5839  # use modified version of output
5840  $OutputToUse = $Modified;
5841  }
5842 
5843  # return output to use to caller
5844  return $OutputToUse;
5845  }
5846 
5848  const OUTPUT_MODIFICATION_THRESHOLD = 0.10;
5849 
5859  private function UpdateSetting(
5860  $FieldName, $NewValue = DB_NOVALUE, $Persistent = TRUE)
5861  {
5862  static $LocalSettings;
5863  if ($NewValue !== DB_NOVALUE)
5864  {
5865  if ($Persistent)
5866  {
5867  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5868  "ApplicationFrameworkSettings",
5869  $FieldName, $NewValue, NULL, $this->Settings);
5870  }
5871  else
5872  {
5873  $LocalSettings[$FieldName] = $NewValue;
5874  }
5875  }
5876  elseif (!isset($LocalSettings[$FieldName]))
5877  {
5878  $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5879  "ApplicationFrameworkSettings",
5880  $FieldName, $NewValue, NULL, $this->Settings);
5881  }
5882  return $LocalSettings[$FieldName];
5883  }
5884 
5894  private static function IncludeFile($_AF_File, $_AF_ContextVars = array())
5895  {
5896  # set up context
5897  foreach ($_AF_ContextVars as $_AF_VarName => $_AF_VarValue)
5898  {
5899  $$_AF_VarName = $_AF_VarValue;
5900  }
5901  unset($_AF_VarName);
5902  unset($_AF_VarValue);
5903  unset($_AF_ContextVars);
5904 
5905  # add variables to context that we assume are always available
5906  $AF = $GLOBALS["AF"];
5907 
5908  # load file
5909  include($_AF_File);
5910 
5911  # return updated context
5912  $ContextVars = get_defined_vars();
5913  unset($ContextVars["_AF_File"]);
5914  return $ContextVars;
5915  }
5916 
5923  private function FilterContext($Context, $ContextVars)
5924  {
5925  # clear all variables if no setting for context is available
5926  # or setting is FALSE
5927  if (!isset($this->ContextFilters[$Context])
5928  || ($this->ContextFilters[$Context] == FALSE))
5929  {
5930  return array();
5931  }
5932  # keep all variables if setting for context is TRUE
5933  elseif ($this->ContextFilters[$Context] == TRUE)
5934  {
5935  return $ContextVars;
5936  }
5937  else
5938  {
5939  $Prefixes = $this->ContextFilters[$Context];
5940  $FilterFunc = function($VarName) use ($Prefixes) {
5941  foreach ($Prefixes as $Prefix)
5942  {
5943  if (substr($VarName, $Prefix) === 0)
5944  {
5945  return TRUE;
5946  }
5947  }
5948  return FALSE;
5949  };
5950  return array_filter(
5951  $ContextVars, $FilterFunc, ARRAY_FILTER_USE_KEY);
5952  }
5953  }
5954 
5956  private $InterfaceDirList = array(
5957  "interface/%ACTIVEUI%/",
5958  "interface/%DEFAULTUI%/",
5959  );
5964  private $IncludeDirList = array(
5965  "interface/%ACTIVEUI%/include/",
5966  "interface/%ACTIVEUI%/objects/",
5967  "interface/%DEFAULTUI%/include/",
5968  "interface/%DEFAULTUI%/objects/",
5969  );
5971  private $ImageDirList = array(
5972  "interface/%ACTIVEUI%/images/",
5973  "interface/%DEFAULTUI%/images/",
5974  );
5976  private $FunctionDirList = array(
5977  "interface/%ACTIVEUI%/include/",
5978  "interface/%DEFAULTUI%/include/",
5979  "include/",
5980  );
5981 
5982  const NOVALUE = ".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";
5983 
5984 
5985  # ---- Page Caching (Internal Methods) -----------------------------------
5986 
5992  private function CheckForCachedPage($PageName)
5993  {
5994  # assume no cached page will be found
5995  $CachedPage = NULL;
5996 
5997  # if returning a cached page is allowed
5998  if ($this->CacheCurrentPage)
5999  {
6000  # get fingerprint for requested page
6001  $PageFingerprint = $this->GetPageFingerprint($PageName);
6002 
6003  # look for matching page in cache in database
6004  $this->DB->Query("SELECT * FROM AF_CachedPages"
6005  ." WHERE Fingerprint = '".addslashes($PageFingerprint)."'");
6006 
6007  # if matching page found
6008  if ($this->DB->NumRowsSelected())
6009  {
6010  # if cached page has expired
6011  $Row = $this->DB->FetchRow();
6012  $ExpirationTime = strtotime(
6013  "-".$this->PageCacheExpirationPeriod()." seconds");
6014  if (strtotime($Row["CachedAt"]) < $ExpirationTime)
6015  {
6016  # clear expired pages from cache
6017  $ExpirationTimestamp = date("Y-m-d H:i:s", $ExpirationTime);
6018  $this->DB->Query("DELETE CP, CPTI FROM AF_CachedPages CP,"
6019  ." AF_CachedPageTagInts CPTI"
6020  ." WHERE CP.CachedAt < '".$ExpirationTimestamp."'"
6021  ." AND CPTI.CacheId = CP.CacheId");
6022  $this->DB->Query("DELETE FROM AF_CachedPages "
6023  ." WHERE CachedAt < '".$ExpirationTimestamp."'");
6024  }
6025  else
6026  {
6027  # display cached page and exit
6028  $CachedPage = $Row["PageContent"];
6029  }
6030  }
6031  }
6032 
6033  # return any cached page found to caller
6034  return $CachedPage;
6035  }
6036 
6042  private function UpdatePageCache($PageName, $PageContent)
6043  {
6044  # if page caching is enabled and current page should be cached
6045  if ($this->PageCacheEnabled()
6046  && $this->CacheCurrentPage
6047  && ($PageName != "404"))
6048  {
6049  # if page content looks invalid
6050  if (strlen(trim(strip_tags($PageContent))) == 0)
6051  {
6052  # log error
6053  $LogMsg = "Page not cached because content was empty."
6054  ." (PAGE: ".$PageName.", URL: ".$this->FullUrl().")";
6055  $this->LogError(self::LOGLVL_ERROR, $LogMsg);
6056  }
6057  else
6058  {
6059  # save page to cache
6060  $PageFingerprint = $this->GetPageFingerprint($PageName);
6061  $this->DB->Query("INSERT INTO AF_CachedPages"
6062  ." (Fingerprint, PageContent) VALUES"
6063  ." ('".$this->DB->EscapeString($PageFingerprint)."', '"
6064  .$this->DB->EscapeString($PageContent)."')");
6065  $CacheId = $this->DB->LastInsertId();
6066 
6067  # for each page cache tag that was added
6068  foreach ($this->PageCacheTags as $Tag => $Pages)
6069  {
6070  # if current page is in list for tag
6071  if (in_array("CURRENT", $Pages) || in_array($PageName, $Pages))
6072  {
6073  # look up tag ID
6074  $TagId = $this->GetPageCacheTagId($Tag);
6075 
6076  # mark current page as associated with tag
6077  $this->DB->Query("INSERT INTO AF_CachedPageTagInts"
6078  ." (TagId, CacheId) VALUES "
6079  ." (".intval($TagId).", ".intval($CacheId).")");
6080  }
6081  }
6082  }
6083  }
6084  }
6085 
6091  private function GetPageCacheTagId($Tag)
6092  {
6093  # if tag is a non-negative integer
6094  if (is_numeric($Tag) && ($Tag > 0) && (intval($Tag) == $Tag))
6095  {
6096  # generate ID
6097  $Id = self::PAGECACHETAGIDOFFSET + $Tag;
6098  }
6099  else
6100  {
6101  # look up ID in database
6102  $Id = $this->DB->Query("SELECT TagId FROM AF_CachedPageTags"
6103  ." WHERE Tag = '".addslashes($Tag)."'", "TagId");
6104 
6105  # if ID was not found
6106  if ($Id === NULL)
6107  {
6108  # add tag to database
6109  $this->DB->Query("INSERT INTO AF_CachedPageTags"
6110  ." SET Tag = '".addslashes($Tag)."'");
6111  $Id = $this->DB->LastInsertId();
6112  }
6113  }
6114 
6115  # return tag ID to caller
6116  return $Id;
6117  }
6118 
6124  private function GetPageFingerprint($PageName)
6125  {
6126  # only get the environmental fingerprint once so that it is consistent
6127  # between page construction start and end
6128  static $EnvFingerprint;
6129  if (!isset($EnvFingerprint))
6130  {
6131  $EnvData = json_encode($_GET).json_encode($_POST);
6132 
6133  # if alternate domain support is enabled
6134  if ($this->HtaccessSupport() && self::$RootUrlOverride !== NULL)
6135  {
6136  # and if we were accessed via an alternate domain
6137  $VHost = $_SERVER["SERVER_NAME"];
6138  if (isset($this->AlternateDomainPrefixes[$VHost]))
6139  {
6140  # then add the alternate domain that was used to our
6141  # environment data
6142  $EnvData .= $VHost;
6143  }
6144  }
6145 
6146  $EnvFingerprint = md5($EnvData);
6147 
6148  }
6149 
6150  # build page fingerprint and return it to caller
6151  return $PageName."-".$EnvFingerprint;
6152  }
6153 }
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:559
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:708
const DB_NOVALUE
Definition: Database.php:1706
const SQL_DATE_FORMAT
Format to feed to date() to get SQL-compatible date/time string.
Definition: StdLib.php:794
static GetCallerInfo($Element=NULL)
Get info about call to current function.
Definition: StdLib.php:26