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