CWIS Developer Documentation

- Home
- Main Page
- Namespaces
- Classes
- Files

- File List
- File Members

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