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 # save execution start time
28 $this->ExecutionStartTime = microtime(TRUE);
29
30 # begin/restore PHP session
31 $SessionDomain = isset($_SERVER["SERVER_NAME"]) ? $_SERVER["SERVER_NAME"]
32 : isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"]
33 : php_uname("n");
34 if (is_writable(session_save_path()))
35 {
36 $SessionStorage = session_save_path()
37 ."/".self::$AppName."_".md5($SessionDomain.dirname(__FILE__));
38 if (!is_dir($SessionStorage)) { mkdir($SessionStorage, 0700 ); }
39 if (is_writable($SessionStorage))
40 {
41 # save params of our session storage as instance variables for later use
42 $this->SessionGcProbability =
43 ini_get("session.gc_probability") / ini_get("session.gc_divisor");
44 $this->SessionStorage = $SessionStorage;
45
46 # store our session files in a subdir to avoid
47 # accidentally sharing sessions with other CWIS installs
48 # on the same domain
49 session_save_path($SessionStorage);
50
51 # set PHP's gc not to run, as it doesn't handle subdirectories anyway
52 # instead, we'll do the cleanup as we run background tasks
53 ini_set("session.gc_probability", 0);
54 }
55 }
56 ini_set("session.gc_maxlifetime", self::$SessionLifetime);
57 session_set_cookie_params(
58 self::$SessionLifetime, "/", $SessionDomain);
59 if (php_sapi_name() !== "cli")
60 {
61 session_start();
62 }
63
64 # set up default object file search locations
65 self::AddObjectDirectory("local/interface/%ACTIVEUI%/objects");
66 self::AddObjectDirectory("interface/%ACTIVEUI%/objects");
67 self::AddObjectDirectory("local/interface/%DEFAULTUI%/objects");
68 self::AddObjectDirectory("interface/%DEFAULTUI%/objects");
69 self::AddObjectDirectory("local/objects");
70 self::AddObjectDirectory("objects");
71
72 # set up object file autoloader
73 spl_autoload_register("ApplicationFramework::AutoloadObjects");
74
75 # set up function to output any buffered text in case of crash
76 register_shutdown_function(array($this, "OnCrash"));
77
78 # set up our internal environment
79 $this->DB = new Database();
80
81 # set up our exception handler
82 set_exception_handler(array($this, "GlobalExceptionHandler"));
83
84 # perform any work needed to undo PHP magic quotes
85 $this->UndoMagicQuotes();
86
87 # load our settings from database
88 $this->LoadSettings();
89
90 # set PHP maximum execution time
91 $this->MaxExecutionTime($this->Settings["MaxExecTime"]);
92
93 # register events we handle internally
94 $this->RegisterEvent($this->PeriodicEvents);
95 $this->RegisterEvent($this->UIEvents);
96
97 # attempt to create SCSS cache directory if needed and it does not exist
98 if ($this->ScssSupportEnabled() && !is_dir(self::$ScssCacheDir))
99 { @mkdir(self::$ScssCacheDir, 0777, TRUE); }
100
101 # attempt to create minimized JS cache directory if needed and it does not exist
102 if ($this->UseMinimizedJavascript()
103 && $this->JavascriptMinimizationEnabled()
104 && !is_dir(self::$JSMinCacheDir))
105 {
106 @mkdir(self::$JSMinCacheDir, 0777, TRUE);
107 }
108 }
115 public function __destruct()
116 {
117 # if template location cache is flagged to be saved
118 if ($this->SaveTemplateLocationCache)
119 {
120 # write template location cache out and update cache expiration
121 $this->DB->Query("UPDATE ApplicationFrameworkSettings"
122 ." SET TemplateLocationCache = '"
123 .addslashes(serialize(
124 $this->TemplateLocationCache))."',"
125 ." TemplateLocationCacheExpiration = '"
126 .date("Y-m-d H:i:s",
127 $this->TemplateLocationCacheExpiration)."'");
128 }
129
130 # if object location cache is flagged to be saved
131 if (self::$SaveObjectLocationCache)
132 {
133 # write object location cache out and update cache expiration
134 $this->DB->Query("UPDATE ApplicationFrameworkSettings"
135 ." SET ObjectLocationCache = '"
136 .addslashes(serialize(
137 self::$ObjectLocationCache))."',"
138 ." ObjectLocationCacheExpiration = '"
139 .date("Y-m-d H:i:s",
140 self::$ObjectLocationCacheExpiration)."'");
141 }
142 }
150 public function GlobalExceptionHandler($Exception)
151 {
152 # display exception info
153 $Location = str_replace(getcwd()."/", "",
154 $Exception->getFile()."[".$Exception->getLine()."]");
155 ?><table width="100%" cellpadding="5"
156 style="border: 2px solid #666666; background: #CCCCCC;
157 font-family: Courier New, Courier, monospace;
158 margin-top: 10px;"><tr><td>
159 <div style="color: #666666;">
160 <span style="font-size: 150%;">
161 <b>Uncaught Exception</b></span><br />
162 <b>Message:</b> <i><?PHP print $Exception->getMessage(); ?></i><br />
163 <b>Location:</b> <i><?PHP print $Location; ?></i><br />
164 <b>Trace:</b>
165 <blockquote><pre><?PHP print preg_replace(
166 ":(#[0-9]+) ".getcwd()."/".":", "$1 ",
167 $Exception->getTraceAsString());
168 ?></pre></blockquote>
169 </div>
170 </td></tr></table><?PHP
171
172 # log exception if possible
173 $TraceString = $Exception->getTraceAsString();
174 $TraceString = str_replace("\n", ", ", $TraceString);
175 $TraceString = preg_replace(":(#[0-9]+) ".getcwd()."/".":", "$1 ", $TraceString);
176 $LogMsg = "Uncaught exception (".$Exception->getMessage().") at "
177 .$Location.". TRACE: ".$TraceString." URL: ".$this->FullUrl();
178 $this->LogError(self::LOGLVL_ERROR, $LogMsg);
179 }
196 public static function AddObjectDirectory(
197 $Dir, $Prefix = "", $ClassPattern = NULL, $ClassReplacement = NULL)
198 {
199 # make sure directory has trailing slash
200 $Dir = $Dir.((substr($Dir, -1) != "/") ? "/" : "");
201
202 # add directory to directory list
203 self::$ObjectDirectories[$Dir] = array(
204 "Prefix" => $Prefix,
205 "ClassPattern" => $ClassPattern,
206 "ClassReplacement" => $ClassReplacement,
207 );
208 }
209
229 public function AddImageDirectories(
230 $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
231 {
232 # add directories to existing image directory list
233 $this->ImageDirList = $this->AddToDirList(
234 $this->ImageDirList, $Dir, $SearchLast, $SkipSlashCheck);
235 }
236
257 public function AddIncludeDirectories(
258 $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
259 {
260 # add directories to existing image directory list
261 $this->IncludeDirList = $this->AddToDirList(
262 $this->IncludeDirList, $Dir, $SearchLast, $SkipSlashCheck);
263 }
264
284 public function AddInterfaceDirectories(
285 $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
286 {
287 # add directories to existing image directory list
288 $this->InterfaceDirList = $this->AddToDirList(
289 $this->InterfaceDirList, $Dir, $SearchLast, $SkipSlashCheck);
290 }
291
311 public function AddFunctionDirectories(
312 $Dir, $SearchLast = FALSE, $SkipSlashCheck = FALSE)
313 {
314 # add directories to existing image directory list
315 $this->FunctionDirList = $this->AddToDirList(
316 $this->FunctionDirList, $Dir, $SearchLast, $SkipSlashCheck);
317 }
318
324 public function SetBrowserDetectionFunc($DetectionFunc)
325 {
326 $this->BrowserDetectFunc = $DetectionFunc;
327 }
328
335 public function AddUnbufferedCallback($Callback, $Parameters=array())
336 {
337 if (is_callable($Callback))
338 {
339 $this->UnbufferedCallbacks[] = array($Callback, $Parameters);
340 }
341 }
342
349 public function TemplateLocationCacheExpirationInterval($NewInterval = DB_NOVALUE)
350 {
351 return $this->UpdateSetting("TemplateLocationCacheInterval", $NewInterval);
352 }
353
357 public function ClearTemplateLocationCache()
358 {
359 $this->TemplateLocationCache = array();
360 $this->SaveTemplateLocationCache = TRUE;
361 }
362
369 public function ObjectLocationCacheExpirationInterval($NewInterval = DB_NOVALUE)
370 {
371 return $this->UpdateSetting("ObjectLocationCacheInterval", $NewInterval);
372 }
373
377 public function ClearObjectLocationCache()
378 {
379 self::$ObjectLocationCache = array();
380 self::$SaveObjectLocationCache = TRUE;
381 }
382
389 public function UrlFingerprintingEnabled($NewValue = DB_NOVALUE)
390 {
391 return $this->UpdateSetting("UrlFingerprintingEnabled", $NewValue);
392 }
393
401 public function ScssSupportEnabled($NewValue = DB_NOVALUE)
402 {
403 return $this->UpdateSetting("ScssSupportEnabled", $NewValue);
404 }
405
414 public function GenerateCompactCss($NewValue = DB_NOVALUE)
415 {
416 return $this->UpdateSetting("GenerateCompactCss", $NewValue);
417 }
418
427 public function UseMinimizedJavascript($NewValue = DB_NOVALUE)
428 {
429 return $this->UpdateSetting("UseMinimizedJavascript", $NewValue);
430 }
431
440 public function JavascriptMinimizationEnabled($NewValue = DB_NOVALUE)
441 {
442 return $this->UpdateSetting("JavascriptMinimizationEnabled", $NewValue);
443 }
444
458 public function RecordContextInCaseOfCrash(
459 $BacktraceOptions = 0, $BacktraceLimit = 0)
460 {
461 if (version_compare(PHP_VERSION, "5.4.0", ">="))
462 {
463 $this->SavedContext = debug_backtrace(
464 $BacktraceOptions, $BacktraceLimit);
465 }
466 else
467 {
468 $this->SavedContext = debug_backtrace($BacktraceOptions);
469 }
470 array_shift($this->SavedContext);
471 }
472
477 public function LoadPage($PageName)
478 {
479 # perform any clean URL rewriting
480 $PageName = $this->RewriteCleanUrls($PageName);
481
482 # sanitize incoming page name and save local copy
483 $PageName = preg_replace("/[^a-zA-Z0-9_.-]/", "", $PageName);
484 $this->PageName = $PageName;
485
486 # if page caching is turned on
487 if ($this->PageCacheEnabled())
488 {
489 # if we have a cached page
490 $CachedPage = $this->CheckForCachedPage($PageName);
491 if ($CachedPage !== NULL)
492 {
493 # set header to indicate cache hit was found
494 header("X-ScoutAF-Cache: HIT");
495
496 # display cached page and exit
497 print $CachedPage;
498 return;
499 }
500 else
501 {
502 # set header to indicate no cache hit was found
503 header("X-ScoutAF-Cache: MISS");
504 }
505 }
506
507 # buffer any output from includes or PHP file
508 ob_start();
509
510 # include any files needed to set up execution environment
511 foreach ($this->EnvIncludes as $IncludeFile)
512 {
513 include($IncludeFile);
514 }
515
516 # signal page load
517 $this->SignalEvent("EVENT_PAGE_LOAD", array("PageName" => $PageName));
518
519 # signal PHP file load
520 $SignalResult = $this->SignalEvent("EVENT_PHP_FILE_LOAD", array(
521 "PageName" => $PageName));
522
523 # if signal handler returned new page name value
524 $NewPageName = $PageName;
525 if (($SignalResult["PageName"] != $PageName)
526 && strlen($SignalResult["PageName"]))
527 {
528 # if new page name value is page file
529 if (file_exists($SignalResult["PageName"]))
530 {
531 # use new value for PHP file name
532 $PageFile = $SignalResult["PageName"];
533 }
534 else
535 {
536 # use new value for page name
537 $NewPageName = $SignalResult["PageName"];
538 }
539
540 # update local copy of page name
541 $this->PageName = $NewPageName;
542 }
543
544 # if we do not already have a PHP file
545 if (!isset($PageFile))
546 {
547 # look for PHP file for page
548 $OurPageFile = "pages/".$NewPageName.".php";
549 $LocalPageFile = "local/pages/".$NewPageName.".php";
550 $PageFile = file_exists($LocalPageFile) ? $LocalPageFile
551 : (file_exists($OurPageFile) ? $OurPageFile
552 : "pages/".$this->DefaultPage.".php");
553 }
554
555 # load PHP file
556 include($PageFile);
557
558 # save buffered output to be displayed later after HTML file loads
559 $PageOutput = ob_get_contents();
560 ob_end_clean();
561
562 # signal PHP file load is complete
563 ob_start();
564 $Context["Variables"] = get_defined_vars();
565 $this->SignalEvent("EVENT_PHP_FILE_LOAD_COMPLETE",
566 array("PageName" => $PageName, "Context" => $Context));
567 $PageCompleteOutput = ob_get_contents();
568 ob_end_clean();
569
570 # set up for possible TSR (Terminate and Stay Resident :))
571 $ShouldTSR = $this->PrepForTSR();
572
573 # if PHP file indicated we should autorefresh to somewhere else
574 if (($this->JumpToPage) && ($this->JumpToPageDelay == 0))
575 {
576 if (!strlen(trim($PageOutput)))
577 {
578 # if client supports HTTP/1.1, use a 303 as it is most accurate
579 if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.1")
580 {
581 header("HTTP/1.1 303 See Other");
582 header("Location: ".$this->JumpToPage);
583 }
584 else
585 {
586 # if the request was an HTTP/1.0 GET or HEAD, then
587 # use a 302 response code.
588
589 # NB: both RFC 2616 (HTTP/1.1) and RFC1945 (HTTP/1.0)
590 # explicitly prohibit automatic redirection via a 302
591 # if the request was not GET or HEAD.
592 if ($_SERVER["SERVER_PROTOCOL"] == "HTTP/1.0" &&
593 ($_SERVER["REQUEST_METHOD"] == "GET" ||
594 $_SERVER["REQUEST_METHOD"] == "HEAD") )
595 {
596 header("HTTP/1.0 302 Found");
597 header("Location: ".$this->JumpToPage);
598 }
599
600 # otherwise, fall back to a meta refresh
601 else
602 {
603 print '<html><head><meta http-equiv="refresh" '
604 .'content="0; URL='.$this->JumpToPage.'">'
605 .'</head><body></body></html>';
606 }
607 }
608 }
609 }
610 # else if HTML loading is not suppressed
611 elseif (!$this->SuppressHTML)
612 {
613 # set content-type to get rid of diacritic errors
614 header("Content-Type: text/html; charset="
615 .$this->HtmlCharset, TRUE);
616
617 # load common HTML file (defines common functions) if available
618 $CommonHtmlFile = $this->FindFile($this->IncludeDirList,
619 "Common", array("tpl", "html"));
620 if ($CommonHtmlFile) { include($CommonHtmlFile); }
621
622 # load UI functions
623 $this->LoadUIFunctions();
624
625 # begin buffering content
626 ob_start();
627
628 # signal HTML file load
629 $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD", array(
630 "PageName" => $PageName));
631
632 # if signal handler returned new page name value
633 $NewPageName = $PageName;
634 $PageContentFile = NULL;
635 if (($SignalResult["PageName"] != $PageName)
636 && strlen($SignalResult["PageName"]))
637 {
638 # if new page name value is HTML file
639 if (file_exists($SignalResult["PageName"]))
640 {
641 # use new value for HTML file name
642 $PageContentFile = $SignalResult["PageName"];
643 }
644 else
645 {
646 # use new value for page name
647 $NewPageName = $SignalResult["PageName"];
648 }
649 }
650
651 # load page content HTML file if available
652 if ($PageContentFile === NULL)
653 {
654 $PageContentFile = $this->FindFile(
655 $this->InterfaceDirList, $NewPageName,
656 array("tpl", "html"));
657 }
658 if ($PageContentFile)
659 {
660 include($PageContentFile);
661 }
662 else
663 {
664 print "<h2>ERROR: No HTML/TPL template found"
665 ." for this page (".$NewPageName.").</h2>";
666 }
667
668 # signal HTML file load complete
669 $SignalResult = $this->SignalEvent("EVENT_HTML_FILE_LOAD_COMPLETE");
670
671 # stop buffering and save output
672 $PageContentOutput = ob_get_contents();
673 ob_end_clean();
674
675 # load page start HTML file if available
676 ob_start();
677 $PageStartFile = $this->FindFile($this->IncludeDirList, "Start",
678 array("tpl", "html"), array("StdPage", "StandardPage"));
679 if ($PageStartFile) { include($PageStartFile); }
680 $PageStartOutput = ob_get_contents();
681 ob_end_clean();
682
683 # if page auto-refresh requested
684 if ($this->JumpToPage)
685 {
686 $RefreshLine = '<meta http-equiv="refresh" content="'
687 .$this->JumpToPageDelay.'; url='.$this->JumpToPage.'">';
688 $PageStartOutput = str_replace("<head>", "<head>\n".$RefreshLine,
689 $PageStartOutput);
690 }
691
692 # load page end HTML file if available
693 ob_start();
694 $PageEndFile = $this->FindFile($this->IncludeDirList, "End",
695 array("tpl", "html"), array("StdPage", "StandardPage"));
696 if ($PageEndFile) { include($PageEndFile); }
697 $PageEndOutput = ob_get_contents();
698 ob_end_clean();
699
700 # get list of any required files not loaded
701 $RequiredFiles = $this->GetRequiredFilesNotYetLoaded($PageContentFile);
702
703 # if a browser detection function has been made available
704 if (is_callable($this->BrowserDetectFunc))
705 {
706 # call function to get browser list
707 $Browsers = call_user_func($this->BrowserDetectFunc);
708
709 # for each required file
710 $NewRequiredFiles = array();
711 foreach ($RequiredFiles as $File)
712 {
713 # if file name includes browser keyword
714 if (preg_match("/%BROWSER%/", $File))
715 {
716 # for each browser
717 foreach ($Browsers as $Browser)
718 {
719 # substitute in browser name and add to new file list
720 $NewRequiredFiles[] = preg_replace(
721 "/%BROWSER%/", $Browser, $File);
722 }
723 }
724 else
725 {
726 # add to new file list
727 $NewRequiredFiles[] = $File;
728 }
729 }
730 $RequiredFiles = $NewRequiredFiles;
731 }
732 else
733 {
734 # filter out any files with browser keyword in their name
735 $NewRequiredFiles = array();
736 foreach ($RequiredFiles as $File)
737 {
738 if (!preg_match("/%BROWSER%/", $File))
739 {
740 $NewRequiredFiles[] = $File;
741 }
742 }
743 $RequiredFiles = $NewRequiredFiles;
744 }
745
746 # for each required file
747 foreach ($RequiredFiles as $File)
748 {
749 # locate specific file to use
750 $FilePath = $this->GUIFile($File);
751
752 # if file was found
753 if ($FilePath)
754 {
755 # generate tag for file
756 $Tag = $this->GetUIFileLoadingTag($FilePath);
757
758 # add file to HTML output based on file type
759 $FileType = $this->GetFileType($FilePath);
760 switch ($FileType)
761 {
762 case self::FT_CSS:
763 $PageStartOutput = preg_replace(
764 "#</head>#i", $Tag."\n</head>", $PageStartOutput, 1);
765 break;
766
767 case self::FT_JAVASCRIPT:
768 $PageEndOutput = preg_replace(
769 "#</body>#i", $Tag."\n</body>", $PageEndOutput, 1);
770 break;
771 }
772 }
773 }
774
775 # assemble full page
776 $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
777
778 # perform any regular expression replacements in output
779 $NewFullPageOutput = preg_replace($this->OutputModificationPatterns,
780 $this->OutputModificationReplacements, $FullPageOutput);
781
782 # check to make sure replacements didn't fail
783 $FullPageOutput = $this->CheckOutputModification(
784 $FullPageOutput, $NewFullPageOutput,
785 "regular expression replacements");
786
787 # for each registered output modification callback
788 foreach ($this->OutputModificationCallbacks as $Info)
789 {
790 # set up data for callback
791 $this->OutputModificationCallbackInfo = $Info;
792
793 # perform output modification
794 $NewFullPageOutput = preg_replace_callback($Info["SearchPattern"],
795 array($this, "OutputModificationCallbackShell"),
796 $FullPageOutput);
797
798 # check to make sure modification didn't fail
799 $ErrorInfo = "callback info: ".print_r($Info, TRUE);
800 $FullPageOutput = $this->CheckOutputModification(
801 $FullPageOutput, $NewFullPageOutput, $ErrorInfo);
802 }
803
804 # provide the opportunity to modify full page output
805 $SignalResult = $this->SignalEvent("EVENT_PAGE_OUTPUT_FILTER", array(
806 "PageOutput" => $FullPageOutput));
807 if (isset($SignalResult["PageOutput"])
808 && strlen(trim($SignalResult["PageOutput"])))
809 {
810 $FullPageOutput = $SignalResult["PageOutput"];
811 }
812
813 # if relative paths may not work because we were invoked via clean URL
814 if ($this->CleanUrlRewritePerformed || self::WasUrlRewritten())
815 {
816 # if using the <base> tag is okay
817 $BaseUrl = $this->BaseUrl();
818 if ($this->UseBaseTag)
819 {
820 # add <base> tag to header
821 $PageStartOutput = str_replace("<head>",
822 "<head><base href=\"".$BaseUrl."\" />",
823 $PageStartOutput);
824
825 # re-assemble full page with new header
826 $FullPageOutput = $PageStartOutput.$PageContentOutput.$PageEndOutput;
827
828 # the absolute URL to the current page
829 $FullUrl = $BaseUrl . $this->GetPageLocation();
830
831 # make HREF attribute values with just a fragment ID
832 # absolute since they don't work with the <base> tag because
833 # they are relative to the current page/URL, not the site
834 # root
835 $NewFullPageOutput = preg_replace(
836 array("%href=\"(#[^:\" ]+)\"%i", "%href='(#[^:' ]+)'%i"),
837 array("href=\"".$FullUrl."$1\"", "href='".$FullUrl."$1'"),
838 $FullPageOutput);
839
840 # check to make sure HREF cleanup didn't fail
841 $FullPageOutput = $this->CheckOutputModification(
842 $FullPageOutput, $NewFullPageOutput,
843 "HREF cleanup");
844 }
845 else
846 {
847 # try to fix any relative paths throughout code
848 $RelativePathPatterns = array(
849 "%src=\"/?([^?*:;{}\\\\\" ]+)\.(js|css|gif|png|jpg)\"%i",
850 "%src='/?([^?*:;{}\\\\' ]+)\.(js|css|gif|png|jpg)'%i",
851 # don't rewrite HREF attributes that are just
852 # fragment IDs because they are relative to the
853 # current page/URL, not the site root
854 "%href=\"/?([^#][^:\" ]*)\"%i",
855 "%href='/?([^#][^:' ]*)'%i",
856 "%action=\"/?([^#][^:\" ]*)\"%i",
857 "%action='/?([^#][^:' ]*)'%i",
858 "%@import\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
859 "%@import\s+url\('/?([^:\" ]+)'\s*\)%i",
860 "%src:\s+url\(\"/?([^:\" ]+)\"\s*\)%i",
861 "%src:\s+url\('/?([^:\" ]+)'\s*\)%i",
862 "%@import\s+\"/?([^:\" ]+)\"\s*%i",
863 "%@import\s+'/?([^:\" ]+)'\s*%i",
864 );
865 $RelativePathReplacements = array(
866 "src=\"".$BaseUrl."$1.$2\"",
867 "src=\"".$BaseUrl."$1.$2\"",
868 "href=\"".$BaseUrl."$1\"",
869 "href=\"".$BaseUrl."$1\"",
870 "action=\"".$BaseUrl."$1\"",
871 "action=\"".$BaseUrl."$1\"",
872 "@import url(\"".$BaseUrl."$1\")",
873 "@import url('".$BaseUrl."$1')",
874 "src: url(\"".$BaseUrl."$1\")",
875 "src: url('".$BaseUrl."$1')",
876 "@import \"".$BaseUrl."$1\"",
877 "@import '".$BaseUrl."$1'",
878 );
879 $NewFullPageOutput = preg_replace($RelativePathPatterns,
880 $RelativePathReplacements, $FullPageOutput);
881
882 # check to make sure relative path fixes didn't fail
883 $FullPageOutput = $this->CheckOutputModification(
884 $FullPageOutput, $NewFullPageOutput,
885 "relative path fixes");
886 }
887 }
888
889 # handle any necessary alternate domain rewriting
890 $FullPageOutput = $this->RewriteAlternateDomainUrls($FullPageOutput);
891
892 # update page cache for this page
893 $this->UpdatePageCache($PageName, $FullPageOutput);
894
895 # write out full page
896 print $FullPageOutput;
897 }
898
899 # run any post-processing routines
900 foreach ($this->PostProcessingFuncs as $Func)
901 {
902 call_user_func_array($Func["FunctionName"], $Func["Arguments"]);
903 }
904
905 # write out any output buffered from page code execution
906 if (strlen($PageOutput))
907 {
908 if (!$this->SuppressHTML)
909 {
910 ?><table width="100%" cellpadding="5"
911 style="border: 2px solid #666666; background: #CCCCCC;
912 font-family: Courier New, Courier, monospace;
913 margin-top: 10px;"><tr><td><?PHP
914 }
915 if ($this->JumpToPage)
916 {
917 ?><div style="color: #666666;"><span style="font-size: 150%;">
918 <b>Page Jump Aborted</b></span>
919 (because of error or other unexpected output)<br />
920 <b>Jump Target:</b>
921 <i><?PHP print($this->JumpToPage); ?></i></div><?PHP
922 }
923 print $PageOutput;
924 if (!$this->SuppressHTML)
925 {
926 ?></td></tr></table><?PHP
927 }
928 }
929
930 # write out any output buffered from the page code execution complete signal
931 if (!$this->JumpToPage && !$this->SuppressHTML && strlen($PageCompleteOutput))
932 {
933 print $PageCompleteOutput;
934 }
935
936 # log slow page loads
937 if ($this->LogSlowPageLoads()
938 && !$this->DoNotLogSlowPageLoad
939 && ($this->GetElapsedExecutionTime()
940 >= ($this->SlowPageLoadThreshold())))
941 {
942 $RemoteHost = gethostbyaddr($_SERVER["REMOTE_ADDR"]);
943 if ($RemoteHost === FALSE)
944 {
945 $RemoteHost = $_SERVER["REMOTE_ADDR"];
946 }
947 elseif ($RemoteHost != $_SERVER["REMOTE_ADDR"])
948 {
949 $RemoteHost .= " (".$_SERVER["REMOTE_ADDR"].")";
950 }
951 $SlowPageLoadMsg = "Slow page load ("
952 .intval($this->GetElapsedExecutionTime())."s) for "
953 .$this->FullUrl()." from ".$RemoteHost;
954 $this->LogMessage(self::LOGLVL_INFO, $SlowPageLoadMsg);
955 }
956
957 # execute callbacks that should not have their output buffered
958 foreach ($this->UnbufferedCallbacks as $Callback)
959 {
960 call_user_func_array($Callback[0], $Callback[1]);
961 }
962
963 # log high memory usage
964 if (function_exists("memory_get_peak_usage"))
965 {
966 $MemoryThreshold = ($this->HighMemoryUsageThreshold()
967 * $this->GetPhpMemoryLimit()) / 100;
968 if ($this->LogHighMemoryUsage()
969 && (memory_get_peak_usage() >= $MemoryThreshold))
970 {
971 $HighMemUsageMsg = "High peak memory usage ("
972 .intval(memory_get_peak_usage()).") for "
973 .$this->FullUrl()." from "
974 .$_SERVER["REMOTE_ADDR"];
975 $this->LogMessage(self::LOGLVL_INFO, $HighMemUsageMsg);
976 }
977 }
978
979 # terminate and stay resident (TSR!) if indicated and HTML has been output
980 # (only TSR if HTML has been output because otherwise browsers will misbehave)
981 if ($ShouldTSR) { $this->LaunchTSR(); }
982 }
983
989 public function GetPageName()
990 {
991 return $this->PageName;
992 }
993
999 public function GetPageLocation()
1000 {
1001 # retrieve current URL
1002 $Url = $this->GetScriptUrl();
1003
1004 # remove the base path if present
1005 $BasePath = $this->Settings["BasePath"];
1006 if (stripos($Url, $BasePath) === 0)
1007 {
1008 $Url = substr($Url, strlen($BasePath));
1009 }
1010
1011 # if we're being accessed via an alternate domain,
1012 # add the appropriate prefix in
1013 if ($this->HtaccessSupport() &&
1014 self::$RootUrlOverride !== NULL)
1015 {
1016 $VHost = $_SERVER["SERVER_NAME"];
1017 if (isset($this->AlternateDomainPrefixes[$VHost]))
1018 {
1019 $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
1020 $Url = $ThisPrefix."/".$Url;
1021 }
1022 }
1023
1024 return $Url;
1025 }
1026
1032 public function GetPageUrl()
1033 {
1034 return self::BaseUrl() . $this->GetPageLocation();
1035 }
1036
1048 public function SetJumpToPage($Page, $Delay = 0, $IsLiteral = FALSE)
1049 {
1050 if (!is_null($Page)
1051 && (!$IsLiteral)
1052 && (strpos($Page, "?") === FALSE)
1053 && ((strpos($Page, "=") !== FALSE)
1054 || ((stripos($Page, ".php") === FALSE)
1055 && (stripos($Page, ".htm") === FALSE)
1056 && (strpos($Page, "/") === FALSE)))
1057 && (stripos($Page, "http://") !== 0)
1058 && (stripos($Page, "https://") !== 0))
1059 {
1060 $this->JumpToPage = self::BaseUrl() . "index.php?P=".$Page;
1061 }
1062 else
1063 {
1064 $this->JumpToPage = $Page;
1065 }
1066 $this->JumpToPageDelay = $Delay;
1067 }
1068
1073 public function JumpToPageIsSet()
1074 {
1075 return ($this->JumpToPage === NULL) ? FALSE : TRUE;
1076 }
1077
1087 public function HtmlCharset($NewSetting = NULL)
1088 {
1089 if ($NewSetting !== NULL) { $this->HtmlCharset = $NewSetting; }
1090 return $this->HtmlCharset;
1091 }
1092
1102 public function DoNotMinimizeFile($File)
1103 {
1104 if (!is_array($File)) { $File = array($File); }
1105 $this->DoNotMinimizeList = array_merge($this->DoNotMinimizeList, $File);
1106 }
1107
1118 public function UseBaseTag($NewValue = NULL)
1119 {
1120 if ($NewValue !== NULL) { $this->UseBaseTag = $NewValue ? TRUE : FALSE; }
1121 return $this->UseBaseTag;
1122 }
1123
1130 public function SuppressHTMLOutput($NewSetting = TRUE)
1131 {
1132 $this->SuppressHTML = $NewSetting;
1133 }
1134
1140 public static function DefaultUserInterface($UIName = NULL)
1141 {
1142 if ($UIName !== NULL)
1143 {
1144 self::$DefaultUI = $UIName;
1145 }
1146 return self::$DefaultUI;
1147 }
1148
1155 public static function ActiveUserInterface($UIName = NULL)
1156 {
1157 if ($UIName !== NULL)
1158 {
1159 self::$ActiveUI = preg_replace("/^SPTUI--/", "", $UIName);
1160 }
1161 return self::$ActiveUI;
1162 }
1163
1174 public function GetUserInterfaces($FilterExp = NULL)
1175 {
1176 static $Interfaces;
1177
1178 if (!isset($Interfaces[$FilterExp]))
1179 {
1180 # retrieve paths to user interface directories
1181 $Paths = $this->GetUserInterfacePaths($FilterExp);
1182
1183 # start out with an empty list
1184 $Interfaces[$FilterExp] = array();
1185
1186 # for each possible UI directory
1187 foreach ($Paths as $CanonicalName => $Path)
1188 {
1189 # if name file available
1190 $LabelFile = $Path."/NAME";
1191 if (is_readable($LabelFile))
1192 {
1193 # read the UI name
1194 $Label = file_get_contents($LabelFile);
1195
1196 # if the UI name looks reasonable
1197 if (strlen(trim($Label)))
1198 {
1199 # use read name
1200 $Interfaces[$FilterExp][$CanonicalName] = $Label;
1201 }
1202 }
1203
1204 # if we do not have a name yet
1205 if (!isset($Interfaces[$FilterExp][$CanonicalName]))
1206 {
1207 # use base directory for name
1208 $Interfaces[$FilterExp][$CanonicalName] = basename($Path);
1209 }
1210 }
1211 }
1212
1213 # return list to caller
1214 return $Interfaces[$FilterExp];
1215 }
1216
1225 public function GetUserInterfacePaths($FilterExp = NULL)
1226 {
1227 static $InterfacePaths;
1228
1229 if (!isset($InterfacePaths[$FilterExp]))
1230 {
1231 # extract possible UI directories from interface directory list
1232 $InterfaceDirs = array();
1233 foreach ($this->InterfaceDirList as $Dir)
1234 {
1235 $Matches = array();
1236 if (preg_match("#([a-zA-Z0-9/]*interface)/[a-zA-Z0-9%/]*#",
1237 $Dir, $Matches))
1238 {
1239 $Dir = $Matches[1];
1240 if (!in_array($Dir, $InterfaceDirs))
1241 {
1242 $InterfaceDirs[] = $Dir;
1243 }
1244 }
1245 }
1246
1247 # reverse order of interface directories so that the directory
1248 # returned is the base directory for the interface
1249 $InterfaceDirs = array_reverse($InterfaceDirs);
1250
1251 # start out with an empty list
1252 $InterfacePaths[$FilterExp] = array();
1253 $InterfacesFound = array();
1254
1255 # for each possible UI directory
1256 foreach ($InterfaceDirs as $InterfaceDir)
1257 {
1258 $Dir = dir($InterfaceDir);
1259
1260 # for each file in current directory
1261 while (($DirEntry = $Dir->read()) !== FALSE)
1262 {
1263 $InterfacePath = $InterfaceDir."/".$DirEntry;
1264
1265 # skip anything we have already found
1266 # or that doesn't have a name in the required format
1267 # or that isn't a directory
1268 # or that doesn't match the filter regex (if supplied)
1269 if (in_array($DirEntry, $InterfacesFound)
1270 || !preg_match('/^[a-zA-Z0-9]+$/', $DirEntry)
1271 || !is_dir($InterfacePath)
1272 || (($FilterExp !== NULL)
1273 && !preg_match($FilterExp, $InterfacePath)))
1274 {
1275 continue;
1276 }
1277
1278 # add interface to list
1279 $InterfacePaths[$FilterExp][$DirEntry] = $InterfacePath;
1280 $InterfacesFound[] = $DirEntry;
1281 }
1282
1283 $Dir->close();
1284 }
1285 }
1286
1287 # return list to caller
1288 return $InterfacePaths[$FilterExp];
1289 }
1290
1315 public function AddPostProcessingCall($FunctionName,
1316 &$Arg1 = self::NOVALUE, &$Arg2 = self::NOVALUE, &$Arg3 = self::NOVALUE,
1317 &$Arg4 = self::NOVALUE, &$Arg5 = self::NOVALUE, &$Arg6 = self::NOVALUE,
1318 &$Arg7 = self::NOVALUE, &$Arg8 = self::NOVALUE, &$Arg9 = self::NOVALUE)
1319 {
1320 $FuncIndex = count($this->PostProcessingFuncs);
1321 $this->PostProcessingFuncs[$FuncIndex]["FunctionName"] = $FunctionName;
1322 $this->PostProcessingFuncs[$FuncIndex]["Arguments"] = array();
1323 $Index = 1;
1324 while (isset(${"Arg".$Index}) && (${"Arg".$Index} !== self::NOVALUE))
1325 {
1326 $this->PostProcessingFuncs[$FuncIndex]["Arguments"][$Index]
1327 =& ${"Arg".$Index};
1328 $Index++;
1329 }
1330 }
1331
1337 public function AddEnvInclude($FileName)
1338 {
1339 $this->EnvIncludes[] = $FileName;
1340 }
1341
1348 public function GUIFile($FileName)
1349 {
1350 # determine which location to search based on file suffix
1351 $FileType = $this->GetFileType($FileName);
1352 $DirList = ($FileType == self::FT_IMAGE)
1353 ? $this->ImageDirList : $this->IncludeDirList;
1354
1355 # if directed to use minimized JavaScript file
1356 if (($FileType == self::FT_JAVASCRIPT) && $this->UseMinimizedJavascript())
1357 {
1358 # look for minimized version of file
1359 $MinimizedFileName = substr_replace($FileName, ".min", -3, 0);
1360 $FoundFileName = $this->FindFile($DirList, $MinimizedFileName);
1361
1362 # if minimized file was not found
1363 if (is_null($FoundFileName))
1364 {
1365 # look for unminimized file
1366 $FoundFileName = $this->FindFile($DirList, $FileName);
1367
1368 # if unminimized file found
1369 if (!is_null($FoundFileName))
1370 {
1371 # if minimization enabled and supported
1372 if ($this->JavascriptMinimizationEnabled()
1373 && self::JsMinRewriteSupport())
1374 {
1375 # attempt to create minimized file
1376 $MinFileName = $this->MinimizeJavascriptFile(
1377 $FoundFileName);
1378
1379 # if minimization succeeded
1380 if ($MinFileName !== NULL)
1381 {
1382 # use minimized version
1383 $FoundFileName = $MinFileName;
1384
1385 # save file modification time if needed for fingerprinting
1386 if ($this->UrlFingerprintingEnabled())
1387 {
1388 $FileMTime = filemtime($FoundFileName);
1389 }
1390
1391 # strip off the cache location, allowing .htaccess
1392 # to handle that for us
1393 $FoundFileName = str_replace(
1394 self::$JSMinCacheDir."/", "", $FoundFileName);
1395 }
1396 }
1397 }
1398 }
1399 }
1400 # else if directed to use SCSS files
1401 elseif (($FileType == self::FT_CSS) && $this->ScssSupportEnabled())
1402 {
1403 # look for SCSS version of file
1404 $SourceFileName = preg_replace("/.css$/", ".scss", $FileName);
1405 $FoundSourceFileName = $this->FindFile($DirList, $SourceFileName);
1406
1407 # if SCSS file not found
1408 if ($FoundSourceFileName === NULL)
1409 {
1410 # look for CSS file
1411 $FoundFileName = $this->FindFile($DirList, $FileName);
1412 }
1413 else
1414 {
1415 # compile SCSS file (if updated) and return resulting CSS file
1416 $FoundFileName = $this->CompileScssFile($FoundSourceFileName);
1417
1418 # save file modification time if needed for fingerprinting
1419 if ($this->UrlFingerprintingEnabled())
1420 {
1421 $FileMTime = filemtime($FoundFileName);
1422 }
1423
1424 # strip off the cache location, allowing .htaccess to handle that for us
1425 if (self::ScssRewriteSupport())
1426 {
1427 $FoundFileName = str_replace(
1428 self::$ScssCacheDir."/", "", $FoundFileName);
1429 }
1430 }
1431 }
1432 # otherwise just search for the file
1433 else
1434 {
1435 $FoundFileName = $this->FindFile($DirList, $FileName);
1436 }
1437
1438 # add non-image files to list of found files (used for required files loading)
1439 if ($FileType != self::FT_IMAGE)
1440 { $this->FoundUIFiles[] = basename($FoundFileName); }
1441
1442 # if UI file fingerprinting is enabled and supported
1443 if ($this->UrlFingerprintingEnabled()
1444 && self::UrlFingerprintingRewriteSupport()
1445 && (isset($FileMTime) || file_exists($FoundFileName)))
1446 {
1447 # if file does not appear to be a server-side inclusion
1448 if (!preg_match('/\.(html|php)$/i', $FoundFileName))
1449 {
1450 # for each URL fingerprinting blacklist entry
1451 $OnBlacklist = FALSE;
1452 foreach ($this->UrlFingerprintBlacklist as $BlacklistEntry)
1453 {
1454 # if entry looks like a regular expression pattern
1455 if ($BlacklistEntry[0] == substr($BlacklistEntry, -1))
1456 {
1457 # check file name against regular expression
1458 if (preg_match($BlacklistEntry, $FoundFileName))
1459 {
1460 $OnBlacklist = TRUE;
1461 break;
1462 }
1463 }
1464 else
1465 {
1466 # check file name directly against entry
1467 if (basename($FoundFileName) == $BlacklistEntry)
1468 {
1469 $OnBlacklist = TRUE;
1470 break;
1471 }
1472 }
1473 }
1474
1475 # if file was not on blacklist
1476 if (!$OnBlacklist)
1477 {
1478 # get file modification time if not already retrieved
1479 if (!isset($FileMTime))
1480 {
1481 $FileMTime = filemtime($FoundFileName);
1482 }
1483
1484 # add timestamp fingerprint to file name
1485 $Fingerprint = sprintf("%06X",
1486 ($FileMTime % 0xFFFFFF));
1487 $FoundFileName = preg_replace("/^(.+)\.([a-z]+)$/",
1488 "$1.".$Fingerprint.".$2",
1489 $FoundFileName);
1490 }
1491 }
1492 }
1493
1494 # return file name to caller
1495 return $FoundFileName;
1496 }
1497
1506 public function PUIFile($FileName)
1507 {
1508 $FullFileName = $this->GUIFile($FileName);
1509 if ($FullFileName) { print($FullFileName); }
1510 }
1511
1526 public function IncludeUIFile($FileNames, $AdditionalAttributes = NULL)
1527 {
1528 # convert file name to array if necessary
1529 if (!is_array($FileNames)) { $FileNames = array($FileNames); }
1530
1531 # pad additional attributes if supplied
1532 $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
1533
1534 # for each file
1535 foreach ($FileNames as $BaseFileName)
1536 {
1537 # retrieve full file name
1538 $FileName = $this->GUIFile($BaseFileName);
1539
1540 # if file was found
1541 if ($FileName)
1542 {
1543 # print appropriate tag
1544 print $this->GetUIFileLoadingTag(
1545 $FileName, $AdditionalAttributes);
1546 }
1547
1548 # if we are not already loading an override file
1549 if (!preg_match("/-Override.(css|scss|js)$/", $BaseFileName))
1550 {
1551 # attempt to load override file if available
1552 $FileType = $this->GetFileType($BaseFileName);
1553 switch ($FileType)
1554 {
1555 case self::FT_CSS:
1556 $OverrideFileName = preg_replace(
1557 "/\.(css|scss)$/", "-Override.$1",
1558 $BaseFileName);
1559 $this->IncludeUIFile($OverrideFileName,
1560 $AdditionalAttributes);
1561 break;
1562
1563 case self::FT_JAVASCRIPT:
1564 $OverrideFileName = preg_replace(
1565 "/\.js$/", "-Override.js",
1566 $BaseFileName);
1567 $this->IncludeUIFile($OverrideFileName,
1568 $AdditionalAttributes);
1569 break;
1570 }
1571 }
1572 }
1573 }
1574
1581 public function DoNotUrlFingerprint($Pattern)
1582 {
1583 $this->UrlFingerprintBlacklist[] = $Pattern;
1584 }
1585
1593 public function RequireUIFile($FileName)
1594 {
1595 $this->AdditionalRequiredUIFiles[] = $FileName;
1596 }
1597
1603 public static function GetFileType($FileName)
1604 {
1605 static $FileTypeCache;
1606 if (isset($FileTypeCache[$FileName]))
1607 {
1608 return $FileTypeCache[$FileName];
1609 }
1610
1611 $FileSuffix = strtolower(substr($FileName, -3));
1612 if ($FileSuffix == "css")
1613 {
1614 $FileTypeCache[$FileName] = self::FT_CSS;
1615 }
1616 elseif ($FileSuffix == ".js")
1617 {
1618 $FileTypeCache[$FileName] = self::FT_JAVASCRIPT;
1619 }
1620 elseif (($FileSuffix == "gif")
1621 || ($FileSuffix == "jpg")
1622 || ($FileSuffix == "png"))
1623 {
1624 $FileTypeCache[$FileName] = self::FT_IMAGE;
1625 }
1626 else
1627 {
1628 $FileTypeCache[$FileName] = self::FT_OTHER;
1629 }
1630
1631 return $FileTypeCache[$FileName];
1632 }
1634 const FT_OTHER = 0;
1636 const FT_CSS = 1;
1638 const FT_IMAGE = 2;
1640 const FT_JAVASCRIPT = 3;
1641
1650 public function LoadFunction($Callback)
1651 {
1652 # if specified function is not currently available
1653 if (!is_callable($Callback))
1654 {
1655 # if function info looks legal
1656 if (is_string($Callback) && strlen($Callback))
1657 {
1658 # start with function directory list
1659 $Locations = $this->FunctionDirList;
1660
1661 # add object directories to list
1662 $Locations = array_merge(
1663 $Locations, array_keys(self::$ObjectDirectories));
1664
1665 # look for function file
1666 $FunctionFileName = $this->FindFile($Locations, "F-".$Callback,
1667 array("php", "html"));
1668
1669 # if function file was found
1670 if ($FunctionFileName)
1671 {
1672 # load function file
1673 include_once($FunctionFileName);
1674 }
1675 else
1676 {
1677 # log error indicating function load failed
1678 $this->LogError(self::LOGLVL_ERROR, "Unable to load function"
1679 ." for callback \"".$Callback."\".");
1680 }
1681 }
1682 else
1683 {
1684 # log error indicating specified function info was bad
1685 $this->LogError(self::LOGLVL_ERROR, "Unloadable callback value"
1686 ." (".$Callback.")"
1687 ." passed to AF::LoadFunction() by "
1688 .StdLib::GetMyCaller().".");
1689 }
1690 }
1691
1692 # report to caller whether function load succeeded
1693 return is_callable($Callback);
1694 }
1695
1700 public function GetElapsedExecutionTime()
1701 {
1702 return microtime(TRUE) - $this->ExecutionStartTime;
1703 }
1704
1709 public function GetSecondsBeforeTimeout()
1710 {
1711 return ini_get("max_execution_time") - $this->GetElapsedExecutionTime();
1712 }
1713
1714 /*@)*/ /* Application Framework */
1715
1716
1717 # ---- Page Caching ------------------------------------------------------
1718 /*@(*/
1720
1727 public function PageCacheEnabled($NewValue = DB_NOVALUE)
1728 {
1729 return $this->UpdateSetting("PageCacheEnabled", $NewValue);
1730 }
1731
1738 public function PageCacheExpirationPeriod($NewValue = DB_NOVALUE)
1739 {
1740 return $this->UpdateSetting("PageCacheExpirationPeriod", $NewValue);
1741 }
1742
1747 public function DoNotCacheCurrentPage()
1748 {
1749 $this->CacheCurrentPage = FALSE;
1750 }
1751
1758 public function AddPageCacheTag($Tag, $Pages = NULL)
1759 {
1760 # normalize tag
1761 $Tag = strtolower($Tag);
1762
1763 # if pages were supplied
1764 if ($Pages !== NULL)
1765 {
1766 # add pages to list for this tag
1767 if (isset($this->PageCacheTags[$Tag]))
1768 {
1769 $this->PageCacheTags[$Tag] = array_merge(
1770 $this->PageCacheTags[$Tag], $Pages);
1771 }
1772 else
1773 {
1774 $this->PageCacheTags[$Tag] = $Pages;
1775 }
1776 }
1777 else
1778 {
1779 # add current page to list for this tag
1780 $this->PageCacheTags[$Tag][] = "CURRENT";
1781 }
1782 }
1783
1789 public function ClearPageCacheForTag($Tag)
1790 {
1791 # get tag ID
1792 $TagId = $this->GetPageCacheTagId($Tag);
1793
1794 # delete pages and tag/page connections for specified tag
1795 $this->DB->Query("DELETE CP, CPTI"
1796 ." FROM AF_CachedPages CP, AF_CachedPageTagInts CPTI"
1797 ." WHERE CPTI.TagId = ".intval($TagId)
1798 ." AND CP.CacheId = CPTI.CacheId");
1799 }
1800
1804 public function ClearPageCache()
1805 {
1806 # clear all page cache tables
1807 $this->DB->Query("TRUNCATE TABLE AF_CachedPages");
1808 $this->DB->Query("TRUNCATE TABLE AF_CachedPageTags");
1809 $this->DB->Query("TRUNCATE TABLE AF_CachedPageTagInts");
1810 }
1811
1818 public function GetPageCacheInfo()
1819 {
1820 $Length = $this->DB->Query("SELECT COUNT(*) AS CacheLen"
1821 ." FROM AF_CachedPages", "CacheLen");
1822 $Oldest = $this->DB->Query("SELECT CachedAt FROM AF_CachedPages"
1823 ." ORDER BY CachedAt ASC LIMIT 1", "CachedAt");
1824 return array(
1825 "NumberOfEntries" => $Length,
1826 "OldestTimestamp" => strtotime($Oldest),
1827 );
1828 }
1829
1830 /*@)*/ /* Page Caching */
1831
1832
1833 # ---- Logging -----------------------------------------------------------
1834 /*@(*/
1836
1850 public function LogSlowPageLoads(
1851 $NewValue = DB_NOVALUE, $Persistent = FALSE)
1852 {
1853 return $this->UpdateSetting(
1854 "LogSlowPageLoads", $NewValue, $Persistent);
1855 }
1856
1867 public function SlowPageLoadThreshold(
1868 $NewValue = DB_NOVALUE, $Persistent = FALSE)
1869 {
1870 return $this->UpdateSetting(
1871 "SlowPageLoadThreshold", $NewValue, $Persistent);
1872 }
1873
1887 public function LogHighMemoryUsage(
1888 $NewValue = DB_NOVALUE, $Persistent = FALSE)
1889 {
1890 return $this->UpdateSetting(
1891 "LogHighMemoryUsage", $NewValue, $Persistent);
1892 }
1893
1905 public function HighMemoryUsageThreshold(
1906 $NewValue = DB_NOVALUE, $Persistent = FALSE)
1907 {
1908 return $this->UpdateSetting(
1909 "HighMemoryUsageThreshold", $NewValue, $Persistent);
1910 }
1911
1925 public function LogError($Level, $Msg)
1926 {
1927 # if error level is at or below current logging level
1928 if ($this->Settings["LoggingLevel"] >= $Level)
1929 {
1930 # attempt to log error message
1931 $Result = $this->LogMessage($Level, $Msg);
1932
1933 # if logging attempt failed and level indicated significant error
1934 if (($Result === FALSE) && ($Level <= self::LOGLVL_ERROR))
1935 {
1936 # throw exception about inability to log error
1937 static $AlreadyThrewException = FALSE;
1938 if (!$AlreadyThrewException)
1939 {
1940 $AlreadyThrewException = TRUE;
1941 throw new Exception("Unable to log error (".$Level.": ".$Msg
1942 .") to ".$this->LogFileName);
1943 }
1944 }
1945
1946 # report to caller whether message was logged
1947 return $Result;
1948 }
1949 else
1950 {
1951 # report to caller that message was not logged
1952 return FALSE;
1953 }
1954 }
1955
1967 public function LogMessage($Level, $Msg)
1968 {
1969 # if message level is at or below current logging level
1970 if ($this->Settings["LoggingLevel"] >= $Level)
1971 {
1972 # attempt to open log file
1973 $FHndl = @fopen($this->LogFileName, "a");
1974
1975 # if log file could not be open
1976 if ($FHndl === FALSE)
1977 {
1978 # report to caller that message was not logged
1979 return FALSE;
1980 }
1981 else
1982 {
1983 # format log entry
1984 $ErrorAbbrevs = array(
1985 self::LOGLVL_FATAL => "FTL",
1986 self::LOGLVL_ERROR => "ERR",
1987 self::LOGLVL_WARNING => "WRN",
1988 self::LOGLVL_INFO => "INF",
1989 self::LOGLVL_DEBUG => "DBG",
1990 self::LOGLVL_TRACE => "TRC",
1991 );
1992 $Msg = str_replace(array("\n", "\t", "\r"), " ", $Msg);
1993 $Msg = substr(trim($Msg), 0, self::LOGFILE_MAX_LINE_LENGTH);
1994 $LogEntry = date("Y-m-d H:i:s")
1995 ." ".($this->RunningInBackground ? "B" : "F")
1996 ." ".$ErrorAbbrevs[$Level]
1997 ." ".$Msg;
1998
1999 # write entry to log
2000 $Success = fwrite($FHndl, $LogEntry."\n");
2001
2002 # close log file
2003 fclose($FHndl);
2004
2005 # report to caller whether message was logged
2006 return ($Success === FALSE) ? FALSE : TRUE;
2007 }
2008 }
2009 else
2010 {
2011 # report to caller that message was not logged
2012 return FALSE;
2013 }
2014 }
2015
2037 public function LoggingLevel($NewValue = DB_NOVALUE)
2038 {
2039 # constrain new level (if supplied) to within legal bounds
2040 if ($NewValue !== DB_NOVALUE)
2041 {
2042 $NewValue = max(min($NewValue, 6), 1);
2043 }
2044
2045 # set new logging level (if supplied) and return current level to caller
2046 return $this->UpdateSetting("LoggingLevel", $NewValue);
2047 }
2048
2055 public function LogFile($NewValue = NULL)
2056 {
2057 if ($NewValue !== NULL) { $this->LogFileName = $NewValue; }
2058 return $this->LogFileName;
2059 }
2060
2070 public function GetLogEntries($Limit = 0)
2071 {
2072 # return no entries if there isn't a log file or we can't read it
2073 if (!is_readable($this->LogFile()))
2074 {
2075 return array();
2076 }
2077
2078 # if max number of entries specified
2079 if ($Limit > 0)
2080 {
2081 # load lines from file
2082 $FHandle = fopen($this->LogFile(), "r");
2083 $FileSize = filesize($this->LogFile());
2084 $SeekPosition = max(0,
2085 ($FileSize - (self::LOGFILE_MAX_LINE_LENGTH * ($Limit + 1))));
2086 fseek($FHandle, $SeekPosition);
2087 $Block = fread($FHandle, ($FileSize - $SeekPosition));
2088 fclose($FHandle);
2089 $Lines = explode(PHP_EOL, $Block);
2090 array_shift($Lines);
2091
2092 # prune array back to requested number of entries
2093 $Lines = array_slice($Lines, (0 - $Limit));
2094 }
2095 else
2096 {
2097 # load all lines from log file
2098 $Lines = file($this->LogFile(), FILE_IGNORE_NEW_LINES);
2099 if ($Lines === FALSE)
2100 {
2101 return array();
2102 }
2103 }
2104
2105 # reverse line order
2106 $Lines = array_reverse($Lines);
2107
2108 # for each log file line
2109 $Entries = array();
2110 foreach ($Lines as $Line)
2111 {
2112 # attempt to parse line into component parts
2113 $Pieces = explode(" ", $Line, 5);
2114 $Date = isset($Pieces[0]) ? $Pieces[0] : "";
2115 $Time = isset($Pieces[1]) ? $Pieces[1] : "";
2116 $Back = isset($Pieces[2]) ? $Pieces[2] : "";
2117 $Level = isset($Pieces[3]) ? $Pieces[3] : "";
2118 $Msg = isset($Pieces[4]) ? $Pieces[4] : "";
2119
2120 # skip line if it looks invalid
2121 $ErrorAbbrevs = array(
2122 "FTL" => self::LOGLVL_FATAL,
2123 "ERR" => self::LOGLVL_ERROR,
2124 "WRN" => self::LOGLVL_WARNING,
2125 "INF" => self::LOGLVL_INFO,
2126 "DBG" => self::LOGLVL_DEBUG,
2127 "TRC" => self::LOGLVL_TRACE,
2128 );
2129 if ((($Back != "F") && ($Back != "B"))
2130 || !array_key_exists($Level, $ErrorAbbrevs)
2131 || !strlen($Msg))
2132 {
2133 continue;
2134 }
2135
2136 # convert parts into appropriate values and add to entries
2137 $Entries[] = array(
2138 "Time" => strtotime($Date." ".$Time),
2139 "Background" => ($Back == "B") ? TRUE : FALSE,
2140 "Level" => $ErrorAbbrevs[$Level],
2141 "Message" => $Msg,
2142 );
2143 }
2144
2145 # return entries to caller
2146 return $Entries;
2147 }
2148
2153 const LOGLVL_TRACE = 6;
2158 const LOGLVL_DEBUG = 5;
2164 const LOGLVL_INFO = 4;
2169 const LOGLVL_WARNING = 3;
2175 const LOGLVL_ERROR = 2;
2180 const LOGLVL_FATAL = 1;
2181
2185 const LOGFILE_MAX_LINE_LENGTH = 2048;
2186
2187 /*@)*/ /* Logging */
2188
2189
2190 # ---- Event Handling ----------------------------------------------------
2191 /*@(*/
2193
2197 const EVENTTYPE_DEFAULT = 1;
2203 const EVENTTYPE_CHAIN = 2;
2209 const EVENTTYPE_FIRST = 3;
2217 const EVENTTYPE_NAMED = 4;
2218
2220 const ORDER_FIRST = 1;
2222 const ORDER_MIDDLE = 2;
2224 const ORDER_LAST = 3;
2225
2234 public function RegisterEvent($EventsOrEventName, $EventType = NULL)
2235 {
2236 # convert parameters to array if not already in that form
2237 $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2238 : array($EventsOrEventName => $EventType);
2239
2240 # for each event
2241 foreach ($Events as $Name => $Type)
2242 {
2243 # store event information
2244 $this->RegisteredEvents[$Name]["Type"] = $Type;
2245 $this->RegisteredEvents[$Name]["Hooks"] = array();
2246 }
2247 }
2248
2255 public function IsRegisteredEvent($EventName)
2256 {
2257 return array_key_exists($EventName, $this->RegisteredEvents)
2258 ? TRUE : FALSE;
2259 }
2260
2267 public function IsHookedEvent($EventName)
2268 {
2269 # the event isn't hooked to if it isn't even registered
2270 if (!$this->IsRegisteredEvent($EventName))
2271 {
2272 return FALSE;
2273 }
2274
2275 # return TRUE if there is at least one callback hooked to the event
2276 return count($this->RegisteredEvents[$EventName]["Hooks"]) > 0;
2277 }
2278
2292 public function HookEvent(
2293 $EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
2294 {
2295 # convert parameters to array if not already in that form
2296 $Events = is_array($EventsOrEventName) ? $EventsOrEventName
2297 : array($EventsOrEventName => $Callback);
2298
2299 # for each event
2300 $Success = TRUE;
2301 foreach ($Events as $EventName => $EventCallback)
2302 {
2303 # if callback is valid
2304 if (is_callable($EventCallback))
2305 {
2306 # if this is a periodic event we process internally
2307 if (isset($this->PeriodicEvents[$EventName]))
2308 {
2309 # process event now
2310 $this->ProcessPeriodicEvent($EventName, $EventCallback);
2311 }
2312 # if specified event has been registered
2313 elseif (isset($this->RegisteredEvents[$EventName]))
2314 {
2315 # add callback for event
2316 $this->RegisteredEvents[$EventName]["Hooks"][]
2317 = array("Callback" => $EventCallback, "Order" => $Order);
2318
2319 # sort callbacks by order
2320 if (count($this->RegisteredEvents[$EventName]["Hooks"]) > 1)
2321 {
2322 usort($this->RegisteredEvents[$EventName]["Hooks"],
2323 array("ApplicationFramework", "HookEvent_OrderCompare"));
2324 }
2325 }
2326 else
2327 {
2328 $Success = FALSE;
2329 }
2330 }
2331 else
2332 {
2333 $Success = FALSE;
2334 }
2335 }
2336
2337 # report to caller whether all callbacks were hooked
2338 return $Success;
2339 }
2346 private static function HookEvent_OrderCompare($A, $B)
2347 {
2348 if ($A["Order"] == $B["Order"]) { return 0; }
2349 return ($A["Order"] < $B["Order"]) ? -1 : 1;
2350 }
2351
2362 public function SignalEvent($EventName, $Parameters = NULL)
2363 {
2364 $ReturnValue = NULL;
2365
2366 # if event has been registered
2367 if (isset($this->RegisteredEvents[$EventName]))
2368 {
2369 # set up default return value (if not NULL)
2370 switch ($this->RegisteredEvents[$EventName]["Type"])
2371 {
2372 case self::EVENTTYPE_CHAIN:
2373 $ReturnValue = $Parameters;
2374 break;
2375
2376 case self::EVENTTYPE_NAMED:
2377 $ReturnValue = array();
2378 break;
2379 }
2380
2381 # for each callback for this event
2382 foreach ($this->RegisteredEvents[$EventName]["Hooks"] as $Hook)
2383 {
2384 # invoke callback
2385 $Callback = $Hook["Callback"];
2386 $Result = ($Parameters !== NULL)
2387 ? call_user_func_array($Callback, $Parameters)
2388 : call_user_func($Callback);
2389
2390 # process return value based on event type
2391 switch ($this->RegisteredEvents[$EventName]["Type"])
2392 {
2393 case self::EVENTTYPE_CHAIN:
2394 if ($Result !== NULL)
2395 {
2396 foreach ($Parameters as $Index => $Value)
2397 {
2398 if (array_key_exists($Index, $Result))
2399 {
2400 $Parameters[$Index] = $Result[$Index];
2401 }
2402 }
2403 $ReturnValue = $Parameters;
2404 }
2405 break;
2406
2407 case self::EVENTTYPE_FIRST:
2408 if ($Result !== NULL)
2409 {
2410 $ReturnValue = $Result;
2411 break 2;
2412 }
2413 break;
2414
2415 case self::EVENTTYPE_NAMED:
2416 $CallbackName = is_array($Callback)
2417 ? (is_object($Callback[0])
2418 ? get_class($Callback[0])
2419 : $Callback[0])."::".$Callback[1]
2420 : $Callback;
2421 $ReturnValue[$CallbackName] = $Result;
2422 break;
2423
2424 default:
2425 break;
2426 }
2427 }
2428 }
2429 else
2430 {
2431 $this->LogError(self::LOGLVL_WARNING,
2432 "Unregistered event (".$EventName.") signaled by "
2433 .StdLib::GetMyCaller().".");
2434 }
2435
2436 # return value if any to caller
2437 return $ReturnValue;
2438 }
2439
2445 public function IsStaticOnlyEvent($EventName)
2446 {
2447 return isset($this->PeriodicEvents[$EventName]) ? TRUE : FALSE;
2448 }
2449
2460 public function EventWillNextRunAt($EventName, $Callback)
2461 {
2462 # if event is not a periodic event report failure to caller
2463 if (!array_key_exists($EventName, $this->EventPeriods)) { return FALSE; }
2464
2465 # retrieve last execution time for event if available
2466 $Signature = self::GetCallbackSignature($Callback);
2467 $LastRunTime = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
2468 ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
2469
2470 # if event was not found report failure to caller
2471 if ($LastRunTime === NULL) { return FALSE; }
2472
2473 # calculate next run time based on event period
2474 $NextRunTime = strtotime($LastRunTime) + $this->EventPeriods[$EventName];
2475
2476 # report next run time to caller
2477 return $NextRunTime;
2478 }
2479
2495 public function GetKnownPeriodicEvents()
2496 {
2497 # retrieve last execution times
2498 $this->DB->Query("SELECT * FROM PeriodicEvents");
2499 $LastRunTimes = $this->DB->FetchColumn("LastRunAt", "Signature");
2500
2501 # for each known event
2502 $Events = array();
2503 foreach ($this->KnownPeriodicEvents as $Signature => $Info)
2504 {
2505 # if last run time for event is available
2506 if (array_key_exists($Signature, $LastRunTimes))
2507 {
2508 # calculate next run time for event
2509 $LastRun = strtotime($LastRunTimes[$Signature]);
2510 $NextRun = $LastRun + $this->EventPeriods[$Info["Period"]];
2511 if ($Info["Period"] == "EVENT_PERIODIC") { $LastRun = FALSE; }
2512 }
2513 else
2514 {
2515 # set info to indicate run times are not known
2516 $LastRun = FALSE;
2517 $NextRun = FALSE;
2518 }
2519
2520 # add event info to list
2521 $Events[$Signature] = $Info;
2522 $Events[$Signature]["LastRun"] = $LastRun;
2523 $Events[$Signature]["NextRun"] = $NextRun;
2524 $Events[$Signature]["Parameters"] = NULL;
2525 }
2526
2527 # return list of known events to caller
2528 return $Events;
2529 }
2530
2531 /*@)*/ /* Event Handling */
2532
2533
2534 # ---- Task Management ---------------------------------------------------
2535 /*@(*/
2537
2539 const PRIORITY_HIGH = 1;
2541 const PRIORITY_MEDIUM = 2;
2543 const PRIORITY_LOW = 3;
2545 const PRIORITY_BACKGROUND = 4;
2546
2559 public function QueueTask($Callback, $Parameters = NULL,
2560 $Priority = self::PRIORITY_LOW, $Description = "")
2561 {
2562 # pack task info and write to database
2563 if ($Parameters === NULL) { $Parameters = array(); }
2564 $this->DB->Query("INSERT INTO TaskQueue"
2565 ." (Callback, Parameters, Priority, Description)"
2566 ." VALUES ('".addslashes(serialize($Callback))."', '"
2567 .addslashes(serialize($Parameters))."', ".intval($Priority).", '"
2568 .addslashes($Description)."')");
2569 }
2570
2588 public function QueueUniqueTask($Callback, $Parameters = NULL,
2589 $Priority = self::PRIORITY_LOW, $Description = "")
2590 {
2591 if ($this->TaskIsInQueue($Callback, $Parameters))
2592 {
2593 $QueryResult = $this->DB->Query("SELECT TaskId,Priority FROM TaskQueue"
2594 ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2595 .($Parameters ? " AND Parameters = '"
2596 .addslashes(serialize($Parameters))."'" : ""));
2597 if ($QueryResult !== FALSE)
2598 {
2599 $Record = $this->DB->FetchRow();
2600 if ($Record["Priority"] > $Priority)
2601 {
2602 $this->DB->Query("UPDATE TaskQueue"
2603 ." SET Priority = ".intval($Priority)
2604 ." WHERE TaskId = ".intval($Record["TaskId"]));
2605 }
2606 }
2607 return FALSE;
2608 }
2609 else
2610 {
2611 $this->QueueTask($Callback, $Parameters, $Priority, $Description);
2612 return TRUE;
2613 }
2614 }
2615
2625 public function TaskIsInQueue($Callback, $Parameters = NULL)
2626 {
2627 $QueuedCount = $this->DB->Query(
2628 "SELECT COUNT(*) AS FoundCount FROM TaskQueue"
2629 ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2630 .($Parameters ? " AND Parameters = '"
2631 .addslashes(serialize($Parameters))."'" : ""),
2632 "FoundCount");
2633 $RunningCount = $this->DB->Query(
2634 "SELECT COUNT(*) AS FoundCount FROM RunningTasks"
2635 ." WHERE Callback = '".addslashes(serialize($Callback))."'"
2636 .($Parameters ? " AND Parameters = '"
2637 .addslashes(serialize($Parameters))."'" : ""),
2638 "FoundCount");
2639 $FoundCount = $QueuedCount + $RunningCount;
2640 return ($FoundCount ? TRUE : FALSE);
2641 }
2642
2648 public function GetTaskQueueSize($Priority = NULL)
2649 {
2650 return $this->GetQueuedTaskCount(NULL, NULL, $Priority);
2651 }
2652
2660 public function GetQueuedTaskList($Count = 100, $Offset = 0)
2661 {
2662 return $this->GetTaskList("SELECT * FROM TaskQueue"
2663 ." ORDER BY Priority, TaskId ", $Count, $Offset);
2664 }
2665
2679 public function GetQueuedTaskCount($Callback = NULL,
2680 $Parameters = NULL, $Priority = NULL, $Description = NULL)
2681 {
2682 $Query = "SELECT COUNT(*) AS TaskCount FROM TaskQueue";
2683 $Sep = " WHERE";
2684 if ($Callback !== NULL)
2685 {
2686 $Query .= $Sep." Callback = '".addslashes(serialize($Callback))."'";
2687 $Sep = " AND";
2688 }
2689 if ($Parameters !== NULL)
2690 {
2691 $Query .= $Sep." Parameters = '".addslashes(serialize($Parameters))."'";
2692 $Sep = " AND";
2693 }
2694 if ($Priority !== NULL)
2695 {
2696 $Query .= $Sep." Priority = ".intval($Priority);
2697 $Sep = " AND";
2698 }
2699 if ($Description !== NULL)
2700 {
2701 $Query .= $Sep." Description = '".addslashes($Description)."'";
2702 }
2703 return $this->DB->Query($Query, "TaskCount");
2704 }
2705
2713 public function GetRunningTaskList($Count = 100, $Offset = 0)
2714 {
2715 return $this->GetTaskList("SELECT * FROM RunningTasks"
2716 ." WHERE StartedAt >= '".date("Y-m-d H:i:s",
2717 (time() - ini_get("max_execution_time")))."'"
2718 ." ORDER BY StartedAt", $Count, $Offset);
2719 }
2720
2728 public function GetOrphanedTaskList($Count = 100, $Offset = 0)
2729 {
2730 return $this->GetTaskList("SELECT * FROM RunningTasks"
2731 ." WHERE StartedAt < '".date("Y-m-d H:i:s",
2732 (time() - ini_get("max_execution_time")))."'"
2733 ." ORDER BY StartedAt", $Count, $Offset);
2734 }
2735
2740 public function GetOrphanedTaskCount()
2741 {
2742 return $this->DB->Query("SELECT COUNT(*) AS Count FROM RunningTasks"
2743 ." WHERE StartedAt < '".date("Y-m-d H:i:s",
2744 (time() - ini_get("max_execution_time")))."'",
2745 "Count");
2746 }
2747
2753 public function ReQueueOrphanedTask($TaskId, $NewPriority = NULL)
2754 {
2755 $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
2756 $this->DB->Query("INSERT INTO TaskQueue"
2757 ." (Callback,Parameters,Priority,Description) "
2758 ."SELECT Callback, Parameters, Priority, Description"
2759 ." FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2760 if ($NewPriority !== NULL)
2761 {
2762 $NewTaskId = $this->DB->LastInsertId();
2763 $this->DB->Query("UPDATE TaskQueue SET Priority = "
2764 .intval($NewPriority)
2765 ." WHERE TaskId = ".intval($NewTaskId));
2766 }
2767 $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2768 $this->DB->Query("UNLOCK TABLES");
2769 }
2770
2775 public function DeleteTask($TaskId)
2776 {
2777 $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = ".intval($TaskId));
2778 $this->DB->Query("DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
2779 }
2780
2788 public function GetTask($TaskId)
2789 {
2790 # assume task will not be found
2791 $Task = NULL;
2792
2793 # look for task in task queue
2794 $this->DB->Query("SELECT * FROM TaskQueue WHERE TaskId = ".intval($TaskId));
2795
2796 # if task was not found in queue
2797 if (!$this->DB->NumRowsSelected())
2798 {
2799 # look for task in running task list
2800 $this->DB->Query("SELECT * FROM RunningTasks WHERE TaskId = "
2801 .intval($TaskId));
2802 }
2803
2804 # if task was found
2805 if ($this->DB->NumRowsSelected())
2806 {
2807 # if task was periodic
2808 $Row = $this->DB->FetchRow();
2809 if ($Row["Callback"] ==
2810 serialize(array("ApplicationFramework", "PeriodicEventWrapper")))
2811 {
2812 # unpack periodic task callback
2813 $WrappedCallback = unserialize($Row["Parameters"]);
2814 $Task["Callback"] = $WrappedCallback[1];
2815 $Task["Parameters"] = $WrappedCallback[2];
2816 }
2817 else
2818 {
2819 # unpack task callback and parameters
2820 $Task["Callback"] = unserialize($Row["Callback"]);
2821 $Task["Parameters"] = unserialize($Row["Parameters"]);
2822 }
2823 }
2824
2825 # return task to caller
2826 return $Task;
2827 }
2828
2836 public function TaskExecutionEnabled($NewValue = DB_NOVALUE)
2837 {
2838 return $this->UpdateSetting("TaskExecutionEnabled", $NewValue);
2839 }
2840
2846 public function MaxTasks($NewValue = DB_NOVALUE)
2847 {
2848 return $this->UpdateSetting("MaxTasksRunning", $NewValue);
2849 }
2850
2858 public static function GetTaskCallbackSynopsis($TaskInfo)
2859 {
2860 # if task callback is function use function name
2861 $Callback = $TaskInfo["Callback"];
2862 $Name = "";
2863 if (!is_array($Callback))
2864 {
2865 $Name = $Callback;
2866 }
2867 else
2868 {
2869 # if task callback is object
2870 if (is_object($Callback[0]))
2871 {
2872 # if task callback is encapsulated ask encapsulation for name
2873 if (method_exists($Callback[0], "GetCallbackAsText"))
2874 {
2875 $Name = $Callback[0]->GetCallbackAsText();
2876 }
2877 # else assemble name from object
2878 else
2879 {
2880 $Name = get_class($Callback[0]) . "::" . $Callback[1];
2881 }
2882 }
2883 # else assemble name from supplied info
2884 else
2885 {
2886 $Name= $Callback[0] . "::" . $Callback[1];
2887 }
2888 }
2889
2890 # if parameter array was supplied
2891 $Parameters = $TaskInfo["Parameters"];
2892 $ParameterString = "";
2893 if (is_array($Parameters))
2894 {
2895 # assemble parameter string
2896 $Separator = "";
2897 foreach ($Parameters as $Parameter)
2898 {
2899 $ParameterString .= $Separator;
2900 if (is_int($Parameter) || is_float($Parameter))
2901 {
2902 $ParameterString .= $Parameter;
2903 }
2904 else if (is_string($Parameter))
2905 {
2906 $ParameterString .= "\"".htmlspecialchars($Parameter)."\"";
2907 }
2908 else if (is_array($Parameter))
2909 {
2910 $ParameterString .= "ARRAY";
2911 }
2912 else if (is_object($Parameter))
2913 {
2914 $ParameterString .= "OBJECT";
2915 }
2916 else if (is_null($Parameter))
2917 {
2918 $ParameterString .= "NULL";
2919 }
2920 else if (is_bool($Parameter))
2921 {
2922 $ParameterString .= $Parameter ? "TRUE" : "FALSE";
2923 }
2924 else if (is_resource($Parameter))
2925 {
2926 $ParameterString .= get_resource_type($Parameter);
2927 }
2928 else
2929 {
2930 $ParameterString .= "????";
2931 }
2932 $Separator = ", ";
2933 }
2934 }
2935
2936 # assemble name and parameters and return result to caller
2937 return $Name."(".$ParameterString.")";
2938 }
2939
2944 public function IsRunningInBackground()
2945 {
2946 return $this->RunningInBackground;
2947 }
2948
2954 public function GetCurrentBackgroundPriority()
2955 {
2956 return isset($this->RunningTask)
2957 ? $this->RunningTask["Priority"] : NULL;
2958 }
2959
2968 public function GetNextHigherBackgroundPriority($Priority = NULL)
2969 {
2970 if ($Priority === NULL)
2971 {
2972 $Priority = $this->GetCurrentBackgroundPriority();
2973 if ($Priority === NULL)
2974 {
2975 return NULL;
2976 }
2977 }
2978 return ($Priority > self::PRIORITY_HIGH)
2979 ? ($Priority - 1) : self::PRIORITY_HIGH;
2980 }
2981
2990 public function GetNextLowerBackgroundPriority($Priority = NULL)
2991 {
2992 if ($Priority === NULL)
2993 {
2994 $Priority = $this->GetCurrentBackgroundPriority();
2995 if ($Priority === NULL)
2996 {
2997 return NULL;
2998 }
2999 }
3000 return ($Priority < self::PRIORITY_BACKGROUND)
3001 ? ($Priority + 1) : self::PRIORITY_BACKGROUND;
3002 }
3003
3004 /*@)*/ /* Task Management */
3005
3006
3007 # ---- Clean URL Support -------------------------------------------------
3008 /*@(*/
3010
3037 public function AddCleanUrl($Pattern, $Page, $GetVars = NULL, $Template = NULL)
3038 {
3039 # save clean URL mapping parameters
3040 $this->CleanUrlMappings[] = array(
3041 "Pattern" => $Pattern,
3042 "Page" => $Page,
3043 "GetVars" => $GetVars,
3044 );
3045
3046 # if replacement template specified
3047 if ($Template !== NULL)
3048 {
3049 # if GET parameters specified
3050 if (count($GetVars))
3051 {
3052 # retrieve all possible permutations of GET parameters
3053 $GetPerms = $this->ArrayPermutations(array_keys($GetVars));
3054
3055 # for each permutation of GET parameters
3056 foreach ($GetPerms as $VarPermutation)
3057 {
3058 # construct search pattern for permutation
3059 $SearchPattern = "/href=([\"'])index\\.php\\?P=".$Page;
3060 $GetVarSegment = "";
3061 foreach ($VarPermutation as $GetVar)
3062 {
3063 if (preg_match("%\\\$[0-9]+%", $GetVars[$GetVar]))
3064 {
3065 $GetVarSegment .= "&amp;".$GetVar."=((?:(?!\\1)[^&])+)";
3066 }
3067 else
3068 {
3069 $GetVarSegment .= "&amp;".$GetVar."=".$GetVars[$GetVar];
3070 }
3071 }
3072 $SearchPattern .= $GetVarSegment."\\1/i";
3073
3074 # if template is actually a callback
3075 if (is_callable($Template))
3076 {
3077 # add pattern to HTML output mod callbacks list
3078 $this->OutputModificationCallbacks[] = array(
3079 "Pattern" => $Pattern,
3080 "Page" => $Page,
3081 "SearchPattern" => $SearchPattern,
3082 "Callback" => $Template,
3083 );
3084 }
3085 else
3086 {
3087 # construct replacement string for permutation
3088 $Replacement = $Template;
3089 $Index = 2;
3090 foreach ($VarPermutation as $GetVar)
3091 {
3092 $Replacement = str_replace(
3093 "\$".$GetVar, "\$".$Index, $Replacement);
3094 $Index++;
3095 }
3096 $Replacement = "href=\"".$Replacement."\"";
3097
3098 # add pattern to HTML output modifications list
3099 $this->OutputModificationPatterns[] = $SearchPattern;
3100 $this->OutputModificationReplacements[] = $Replacement;
3101 }
3102 }
3103 }
3104 else
3105 {
3106 # construct search pattern
3107 $SearchPattern = "/href=\"index\\.php\\?P=".$Page."\"/i";
3108
3109 # if template is actually a callback
3110 if (is_callable($Template))
3111 {
3112 # add pattern to HTML output mod callbacks list
3113 $this->OutputModificationCallbacks[] = array(
3114 "Pattern" => $Pattern,
3115 "Page" => $Page,
3116 "SearchPattern" => $SearchPattern,
3117 "Callback" => $Template,
3118 );
3119 }
3120 else
3121 {
3122 # add simple pattern to HTML output modifications list
3123 $this->OutputModificationPatterns[] = $SearchPattern;
3124 $this->OutputModificationReplacements[] = "href=\"".$Template."\"";
3125 }
3126 }
3127 }
3128 }
3129
3135 public function CleanUrlIsMapped($Path)
3136 {
3137 foreach ($this->CleanUrlMappings as $Info)
3138 {
3139 if (preg_match($Info["Pattern"], $Path))
3140 {
3141 return TRUE;
3142 }
3143 }
3144 return FALSE;
3145 }
3146
3156 public function GetCleanUrlForPath($Path)
3157 {
3158 # the search patterns and callbacks require a specific format
3159 $Format = "href=\"".str_replace("&", "&amp;", $Path)."\"";
3160 $Search = $Format;
3161
3162 # perform any regular expression replacements on the search string
3163 $Search = preg_replace($this->OutputModificationPatterns,
3164 $this->OutputModificationReplacements, $Search);
3165
3166 # only run the callbacks if a replacement hasn't already been performed
3167 if ($Search == $Format)
3168 {
3169 # perform any callback replacements on the search string
3170 foreach ($this->OutputModificationCallbacks as $Info)
3171 {
3172 # make the information available to the callback
3173 $this->OutputModificationCallbackInfo = $Info;
3174
3175 # execute the callback
3176 $Search = preg_replace_callback($Info["SearchPattern"],
3177 array($this, "OutputModificationCallbackShell"),
3178 $Search);
3179 }
3180 }
3181
3182 # return the path untouched if no replacements were performed
3183 if ($Search == $Format)
3184 {
3185 return $Path;
3186 }
3187
3188 # remove the bits added to the search string to get it recognized by
3189 # the replacement expressions and callbacks
3190 $Result = substr($Search, 6, -1);
3191
3192 return $Result;
3193 }
3194
3201 public function GetUncleanUrlForPath($Path)
3202 {
3203 # for each clean URL mapping
3204 foreach ($this->CleanUrlMappings as $Info)
3205 {
3206 # if current path matches the clean URL pattern
3207 if (preg_match($Info["Pattern"], $Path, $Matches))
3208 {
3209 # the GET parameters for the URL, starting with the page name
3210 $GetVars = array("P" => $Info["Page"]);
3211
3212 # if additional $_GET variables specified for clean URL
3213 if ($Info["GetVars"] !== NULL)
3214 {
3215 # for each $_GET variable specified for clean URL
3216 foreach ($Info["GetVars"] as $VarName => $VarTemplate)
3217 {
3218 # start with template for variable value
3219 $Value = $VarTemplate;
3220
3221 # for each subpattern matched in current URL
3222 foreach ($Matches as $Index => $Match)
3223 {
3224 # if not first (whole) match
3225 if ($Index > 0)
3226 {
3227 # make any substitutions in template
3228 $Value = str_replace("$".$Index, $Match, $Value);
3229 }
3230 }
3231
3232 # add the GET variable
3233 $GetVars[$VarName] = $Value;
3234 }
3235 }
3236
3237 # return the unclean URL
3238 return "index.php?" . http_build_query($GetVars);
3239 }
3240 }
3241
3242 # return the path unchanged
3243 return $Path;
3244 }
3245
3251 public function GetCleanUrl()
3252 {
3253 return $this->GetCleanUrlForPath($this->GetUncleanUrl());
3254 }
3255
3260 public function GetUncleanUrl()
3261 {
3262 $GetVars = array("P" => $this->GetPageName()) + $_GET;
3263 return "index.php?" . http_build_query($GetVars);
3264 }
3265
3277 public function AddPrefixForAlternateDomain($Domain, $Prefix)
3278 {
3279 $this->AlternateDomainPrefixes[$Domain] = $Prefix;
3280 }
3281
3282 /*@)*/ /* Clean URL Support */
3283
3284 # ---- Server Environment ------------------------------------------------
3285 /*@(*/
3287
3293 public static function SessionLifetime($NewValue = NULL)
3294 {
3295 if ($NewValue !== NULL)
3296 {
3297 self::$SessionLifetime = $NewValue;
3298 }
3299 return self::$SessionLifetime;
3300 }
3301
3307 public static function HtaccessSupport()
3308 {
3309 return isset($_SERVER["HTACCESS_SUPPORT"])
3310 || isset($_SERVER["REDIRECT_HTACCESS_SUPPORT"]);
3311 }
3312
3319 public static function UrlFingerprintingRewriteSupport()
3320 {
3321 return isset($_SERVER["URL_FINGERPRINTING_SUPPORT"])
3322 || isset($_SERVER["REDIRECT_URL_FINGERPRINTING_SUPPORT"]);
3323 }
3324
3331 public static function ScssRewriteSupport()
3332 {
3333 return isset($_SERVER["SCSS_REWRITE_SUPPORT"])
3334 || isset($_SERVER["REDIRECT_SCSS_REWRITE_SUPPORT"]);
3335 }
3336
3343 public static function JsMinRewriteSupport()
3344 {
3345 return isset($_SERVER["JSMIN_REWRITE_SUPPORT"])
3346 || isset($_SERVER["REDIRECT_JSMIN_REWRITE_SUPPORT"]);
3347 }
3348
3356 public static function RootUrl()
3357 {
3358 # return override value if one is set
3359 if (self::$RootUrlOverride !== NULL)
3360 {
3361 return self::$RootUrlOverride;
3362 }
3363
3364 # determine scheme name
3365 $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
3366
3367 # if HTTP_HOST is preferred or SERVER_NAME points to localhost
3368 # and HTTP_HOST is set
3369 if ((self::$PreferHttpHost || ($_SERVER["SERVER_NAME"] == "127.0.0.1"))
3370 && isset($_SERVER["HTTP_HOST"]))
3371 {
3372 # use HTTP_HOST for domain name
3373 $DomainName = $_SERVER["HTTP_HOST"];
3374 }
3375 else
3376 {
3377 # use SERVER_NAME for domain name
3378 $DomainName = $_SERVER["HTTP_HOST"];
3379 }
3380
3381 # build URL root and return to caller
3382 return $Protocol."://".$DomainName;
3383 }
3384
3399 public static function RootUrlOverride($NewValue = self::NOVALUE)
3400 {
3401 if ($NewValue !== self::NOVALUE)
3402 {
3403 self::$RootUrlOverride = strlen(trim($NewValue)) ? $NewValue : NULL;
3404 }
3405 return self::$RootUrlOverride;
3406 }
3407
3417 public static function BaseUrl()
3418 {
3419 $BaseUrl = self::RootUrl().dirname($_SERVER["SCRIPT_NAME"]);
3420 if (substr($BaseUrl, -1) != "/") { $BaseUrl .= "/"; }
3421 return $BaseUrl;
3422 }
3423
3431 public static function FullUrl()
3432 {
3433 return self::RootUrl().$_SERVER["REQUEST_URI"];
3434 }
3435
3446 public static function PreferHttpHost($NewValue = NULL)
3447 {
3448 if ($NewValue !== NULL)
3449 {
3450 self::$PreferHttpHost = ($NewValue ? TRUE : FALSE);
3451 }
3452 return self::$PreferHttpHost;
3453 }
3454
3459 public static function BasePath()
3460 {
3461 $BasePath = dirname($_SERVER["SCRIPT_NAME"]);
3462
3463 if (substr($BasePath, -1) != "/")
3464 {
3465 $BasePath .= "/";
3466 }
3467
3468 return $BasePath;
3469 }
3470
3476 public static function GetScriptUrl()
3477 {
3478 if (array_key_exists("SCRIPT_URL", $_SERVER))
3479 {
3480 return $_SERVER["SCRIPT_URL"];
3481 }
3482 elseif (array_key_exists("REDIRECT_URL", $_SERVER))
3483 {
3484 return $_SERVER["REDIRECT_URL"];
3485 }
3486 elseif (array_key_exists("REQUEST_URI", $_SERVER))
3487 {
3488 $Pieces = parse_url($_SERVER["REQUEST_URI"]);
3489 return isset($Pieces["path"]) ? $Pieces["path"] : NULL;
3490 }
3491 else
3492 {
3493 return NULL;
3494 }
3495 }
3496
3505 public static function WasUrlRewritten($ScriptName="index.php")
3506 {
3507 # needed to get the path of the URL minus the query and fragment pieces
3508 $Components = parse_url(self::GetScriptUrl());
3509
3510 # if parsing was successful and a path is set
3511 if (is_array($Components) && isset($Components["path"]))
3512 {
3513 $BasePath = self::BasePath();
3514 $Path = $Components["path"];
3515
3516 # the URL was rewritten if the path isn't the base path, i.e., the
3517 # home page, and the file in the URL isn't the script generating the
3518 # page
3519 if ($BasePath != $Path && basename($Path) != $ScriptName)
3520 {
3521 return TRUE;
3522 }
3523 }
3524
3525 # the URL wasn't rewritten
3526 return FALSE;
3527 }
3528
3534 public static function GetFreeMemory()
3535 {
3536 return self::GetPhpMemoryLimit() - memory_get_usage();
3537 }
3538
3544 public static function GetPhpMemoryLimit()
3545 {
3546 $Str = strtoupper(ini_get("memory_limit"));
3547 if (substr($Str, -1) == "B") { $Str = substr($Str, 0, strlen($Str) - 1); }
3548 switch (substr($Str, -1))
3549 {
3550 case "K":
3551 $MemoryLimit = (int)$Str * 1024;
3552 break;
3553
3554 case "M":
3555 $MemoryLimit = (int)$Str * 1048576;
3556 break;
3557
3558 case "G":
3559 $MemoryLimit = (int)$Str * 1073741824;
3560 break;
3561
3562 default:
3563 $MemoryLimit = (int)$Str;
3564 break;
3565 }
3566 return $MemoryLimit;
3567 }
3568
3576 public function MaxExecutionTime($NewValue = NULL)
3577 {
3578 if (func_num_args() && !ini_get("safe_mode"))
3579 {
3580 if ($NewValue != $this->Settings["MaxExecTime"])
3581 {
3582 $this->Settings["MaxExecTime"] = max($NewValue, 5);
3583 $this->DB->Query("UPDATE ApplicationFrameworkSettings"
3584 ." SET MaxExecTime = '"
3585 .intval($this->Settings["MaxExecTime"])."'");
3586 }
3587 ini_set("max_execution_time", $this->Settings["MaxExecTime"]);
3588 set_time_limit($this->Settings["MaxExecTime"]);
3589 }
3590 return ini_get("max_execution_time");
3591 }
3592
3593 /*@)*/ /* Server Environment */
3594
3595
3596 # ---- Utility -----------------------------------------------------------
3597 /*@(*/
3599
3611 public function DownloadFile($FilePath, $FileName = NULL, $MimeType = NULL)
3612 {
3613 # check that file is readable
3614 if (!is_readable($FilePath))
3615 {
3616 return FALSE;
3617 }
3618
3619 # if file name was not supplied
3620 if ($FileName === NULL)
3621 {
3622 # extract file name from path
3623 $FileName = basename($FilePath);
3624 }
3625
3626 # if MIME type was not supplied
3627 if ($MimeType === NULL)
3628 {
3629 # attempt to determine MIME type
3630 $FInfoHandle = finfo_open(FILEINFO_MIME);
3631 if ($FInfoHandle)
3632 {
3633 $FInfoMime = finfo_file($FInfoHandle, $FilePath);
3634 finfo_close($FInfoHandle);
3635 if ($FInfoMime)
3636 {
3637 $MimeType = $FInfoMime;
3638 }
3639 }
3640
3641 # use default if unable to determine MIME type
3642 if ($MimeType === NULL)
3643 {
3644 $MimeType = "application/octet-stream";
3645 }
3646 }
3647
3648 # set headers to download file
3649 header("Content-Type: ".$MimeType);
3650 header("Content-Length: ".filesize($FilePath));
3651 if ($this->CleanUrlRewritePerformed)
3652 {
3653 header('Content-Disposition: attachment; filename="'.$FileName.'"');
3654 }
3655
3656 # make sure that apache does not attempt to compress file
3657 apache_setenv('no-gzip', '1');
3658
3659 # send file to user, but unbuffered to avoid memory issues
3660 $this->AddUnbufferedCallback(function ($File)
3661 {
3662 $BlockSize = 512000;
3663 $Handle = @fopen($File, "rb");
3664 if ($Handle === FALSE)
3665 {
3666 return;
3667 }
3668 while (!feof($Handle))
3669 {
3670 print fread($Handle, $BlockSize);
3671 flush();
3672 }
3673 fclose($Handle);
3674 }, array($FilePath));
3675
3676 # prevent HTML output that might interfere with download
3677 $this->SuppressHTMLOutput();
3678
3679 # set flag to indicate not to log a slow page load in case client
3680 # connection delays PHP execution because of header
3681 $this->DoNotLogSlowPageLoad = TRUE;
3682
3683 # report no errors found to caller
3684 return TRUE;
3685 }
3686
3687 /*@)*/ /* Utility */
3688
3689
3690 # ---- Backward Compatibility --------------------------------------------
3691 /*@(*/
3693
3700 public function FindCommonTemplate($BaseName)
3701 {
3702 return $this->FindFile(
3703 $this->IncludeDirList, $BaseName, array("tpl", "html"));
3704 }
3705
3706 /*@)*/ /* Backward Compatibility */
3707
3708
3709 # ---- PRIVATE INTERFACE -------------------------------------------------
3710
3711 private $AdditionalRequiredUIFiles = array();
3712 private $BackgroundTaskMemLeakLogThreshold = 10; # percentage of max mem
3713 private $BackgroundTaskMinFreeMemPercent = 25;
3714 private $BrowserDetectFunc;
3715 private $CacheCurrentPage = TRUE;
3716 private $AlternateDomainPrefixes = array();
3717 private $CleanUrlMappings = array();
3718 private $CleanUrlRewritePerformed = FALSE;
3719 private $CssUrlFingerprintPath;
3720 private $DB;
3721 private $DefaultPage = "Home";
3722 private $DoNotMinimizeList = array();
3723 private $DoNotLogSlowPageLoad = FALSE;
3724 private $EnvIncludes = array();
3725 private $ExecutionStartTime;
3726 private $FoundUIFiles = array();
3727 private $HtmlCharset = "UTF-8";
3728 private $JSMinimizerJavaScriptPackerAvailable = FALSE;
3729 private $JSMinimizerJShrinkAvailable = TRUE;
3730 private $JumpToPage = NULL;
3731 private $JumpToPageDelay = 0;
3732 private $LogFileName = "local/logs/site.log";
3733 private $MaxRunningTasksToTrack = 250;
3734 private $OutputModificationCallbackInfo;
3735 private $OutputModificationCallbacks = array();
3736 private $OutputModificationPatterns = array();
3737 private $OutputModificationReplacements = array();
3738 private $PageCacheTags = array();
3739 private $PageName;
3740 private $PostProcessingFuncs = array();
3741 private $RunningInBackground = FALSE;
3742 private $RunningTask;
3743 private $SavedContext;
3744 private $SaveTemplateLocationCache = FALSE;
3745 private $SessionStorage;
3746 private $SessionGcProbability;
3747 private $Settings;
3748 private $SuppressHTML = FALSE;
3749 private $TemplateLocationCache;
3750 private $TemplateLocationCacheInterval = 60; # in minutes
3751 private $TemplateLocationCacheExpiration;
3752 private $UnbufferedCallbacks = array();
3753 private $UrlFingerprintBlacklist = array();
3754 private $UseBaseTag = FALSE;
3755
3756 private static $ActiveUI = "default";
3757 private static $AppName = "ScoutAF";
3758 private static $DefaultUI = "default";
3759 private static $JSMinCacheDir = "local/data/caches/JSMin";
3760 private static $ObjectDirectories = array();
3761 private static $ObjectLocationCache;
3762 private static $ObjectLocationCacheInterval = 60;
3763 private static $ObjectLocationCacheExpiration;
3764 private static $PreferHttpHost = FALSE;
3765 private static $RootUrlOverride = NULL;
3766 private static $SaveObjectLocationCache = FALSE;
3767 private static $ScssCacheDir = "local/data/caches/SCSS";
3768 private static $SessionLifetime = 1440; # in seconds
3769
3770 # offset used to generate page cache tag IDs from numeric tags
3771 const PAGECACHETAGIDOFFSET = 100000;
3772
3777 private $NoTSR = FALSE;
3778
3779 private $KnownPeriodicEvents = array();
3780 private $PeriodicEvents = array(
3781 "EVENT_HOURLY" => self::EVENTTYPE_DEFAULT,
3782 "EVENT_DAILY" => self::EVENTTYPE_DEFAULT,
3783 "EVENT_WEEKLY" => self::EVENTTYPE_DEFAULT,
3784 "EVENT_MONTHLY" => self::EVENTTYPE_DEFAULT,
3785 "EVENT_PERIODIC" => self::EVENTTYPE_NAMED,
3786 );
3787 private $EventPeriods = array(
3788 "EVENT_HOURLY" => 3600,
3789 "EVENT_DAILY" => 86400,
3790 "EVENT_WEEKLY" => 604800,
3791 "EVENT_MONTHLY" => 2592000,
3792 "EVENT_PERIODIC" => 0,
3793 );
3794 private $UIEvents = array(
3795 "EVENT_PAGE_LOAD" => self::EVENTTYPE_DEFAULT,
3796 "EVENT_PHP_FILE_LOAD" => self::EVENTTYPE_CHAIN,
3797 "EVENT_PHP_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
3798 "EVENT_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
3799 "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
3800 "EVENT_PAGE_OUTPUT_FILTER" => self::EVENTTYPE_CHAIN,
3801 );
3802
3807 private function LoadSettings()
3808 {
3809 # read settings in from database
3810 $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
3811 $this->Settings = $this->DB->FetchRow();
3812
3813 # if settings were not previously initialized
3814 if ($this->Settings === FALSE)
3815 {
3816 # initialize settings in database
3817 $this->DB->Query("INSERT INTO ApplicationFrameworkSettings"
3818 ." (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");
3819
3820 # read new settings in from database
3821 $this->DB->Query("SELECT * FROM ApplicationFrameworkSettings");
3822 $this->Settings = $this->DB->FetchRow();
3823
3824 # bail out if reloading new settings failed
3825 if ($this->Settings === FALSE)
3826 {
3827 throw new Exception(
3828 "Unable to load application framework settings.");
3829 }
3830 }
3831
3832 # if base path was not previously set or we appear to have moved
3833 if (!array_key_exists("BasePath", $this->Settings)
3834 || (!strlen($this->Settings["BasePath"]))
3835 || (!array_key_exists("BasePathCheck", $this->Settings))
3836 || (__FILE__ != $this->Settings["BasePathCheck"]))
3837 {
3838 # attempt to extract base path from Apache .htaccess file
3839 if (is_readable(".htaccess"))
3840 {
3841 $Lines = file(".htaccess");
3842 foreach ($Lines as $Line)
3843 {
3844 if (preg_match("/\\s*RewriteBase\\s+/", $Line))
3845 {
3846 $Pieces = preg_split(
3847 "/\\s+/", $Line, NULL, PREG_SPLIT_NO_EMPTY);
3848 $BasePath = $Pieces[1];
3849 }
3850 }
3851 }
3852
3853 # if base path was found
3854 if (isset($BasePath))
3855 {
3856 # save base path locally
3857 $this->Settings["BasePath"] = $BasePath;
3858
3859 # save base path to database
3860 $this->DB->Query("UPDATE ApplicationFrameworkSettings"
3861 ." SET BasePath = '".addslashes($BasePath)."'"
3862 .", BasePathCheck = '".addslashes(__FILE__)."'");
3863 }
3864 }
3865
3866 # retrieve template location cache
3867 $this->TemplateLocationCache = unserialize(
3868 $this->Settings["TemplateLocationCache"]);
3869 $this->TemplateLocationCacheInterval =
3870 $this->Settings["TemplateLocationCacheInterval"];
3871 $this->TemplateLocationCacheExpiration =
3872 strtotime($this->Settings["TemplateLocationCacheExpiration"]);
3873
3874 # if template location cache looks invalid or has expired
3875 $CurrentTime = time();
3876 if (!count($this->TemplateLocationCache)
3877 || ($CurrentTime >= $this->TemplateLocationCacheExpiration))
3878 {
3879 # clear cache and reset cache expiration
3880 $this->TemplateLocationCache = array();
3881 $this->TemplateLocationCacheExpiration =
3882 $CurrentTime + ($this->TemplateLocationCacheInterval * 60);
3883 $this->SaveTemplateLocationCache = TRUE;
3884 }
3885
3886 # retrieve object location cache
3887 self::$ObjectLocationCache =
3888 unserialize($this->Settings["ObjectLocationCache"]);
3889 self::$ObjectLocationCacheInterval =
3890 $this->Settings["ObjectLocationCacheInterval"];
3891 self::$ObjectLocationCacheExpiration =
3892 strtotime($this->Settings["ObjectLocationCacheExpiration"]);
3893
3894 # if object location cache looks invalid or has expired
3895 if (!count(self::$ObjectLocationCache)
3896 || ($CurrentTime >= self::$ObjectLocationCacheExpiration))
3897 {
3898 # clear cache and reset cache expiration
3899 self::$ObjectLocationCache = array();
3900 self::$ObjectLocationCacheExpiration =
3901 $CurrentTime + (self::$ObjectLocationCacheInterval * 60);
3902 self::$SaveObjectLocationCache = TRUE;
3903 }
3904 }
3905
3912 private function RewriteCleanUrls($PageName)
3913 {
3914 # if URL rewriting is supported by the server
3915 if ($this->HtaccessSupport())
3916 {
3917 # retrieve current URL and remove base path if present
3918 $Url = $this->GetPageLocation();
3919
3920 # for each clean URL mapping
3921 foreach ($this->CleanUrlMappings as $Info)
3922 {
3923 # if current URL matches clean URL pattern
3924 if (preg_match($Info["Pattern"], $Url, $Matches))
3925 {
3926 # set new page
3927 $PageName = $Info["Page"];
3928
3929 # if $_GET variables specified for clean URL
3930 if ($Info["GetVars"] !== NULL)
3931 {
3932 # for each $_GET variable specified for clean URL
3933 foreach ($Info["GetVars"] as $VarName => $VarTemplate)
3934 {
3935 # start with template for variable value
3936 $Value = $VarTemplate;
3937
3938 # for each subpattern matched in current URL
3939 foreach ($Matches as $Index => $Match)
3940 {
3941 # if not first (whole) match
3942 if ($Index > 0)
3943 {
3944 # make any substitutions in template
3945 $Value = str_replace("$".$Index, $Match, $Value);
3946 }
3947 }
3948
3949 # set $_GET variable
3950 $_GET[$VarName] = $Value;
3951 }
3952 }
3953
3954 # set flag indicating clean URL mapped
3955 $this->CleanUrlRewritePerformed = TRUE;
3956
3957 # stop looking for a mapping
3958 break;
3959 }
3960 }
3961 }
3962
3963 # return (possibly) updated page name to caller
3964 return $PageName;
3965 }
3966
3979 private function RewriteAlternateDomainUrls($Html)
3980 {
3981 if ($this->HtaccessSupport() &&
3982 self::$RootUrlOverride !== NULL)
3983 {
3984 $VHost = $_SERVER["SERVER_NAME"];
3985 if (isset($this->AlternateDomainPrefixes[$VHost]))
3986 {
3987 $ThisPrefix = $this->AlternateDomainPrefixes[$VHost];
3988
3989 # get the URL for the primary domain
3990 $RootUrl = $this->RootUrl()."/";
3991
3992 # and figure out what protcol we were using
3993 $Protocol = (isset($_SERVER["HTTPS"]) ? "https" : "http");
3994
3995 # convert all relative URLs absolute URLs within our
3996 # primary domain, then substitute in the alternate domain
3997 # for paths inside our configured prefix
3998 $RelativePathPatterns = array(
3999 "%src=\"(?!http://|https://)%i",
4000 "%src='(?!http://|https://)%i",
4001 "%href=\"(?!http://|https://)%i",
4002 "%href='(?!http://|https://)%i",
4003 "%action=\"(?!http://|https://)%i",
4004 "%action='(?!http://|https://)%i",
4005 "%@import\s+url\(\"(?!http://|https://)%i",
4006 "%@import\s+url\('(?!http://|https://)%i",
4007 "%src:\s+url\(\"(?!http://|https://)%i",
4008 "%src:\s+url\('(?!http://|https://)%i",
4009 "%@import\s+\"(?!http://|https://)%i",
4010 "%@import\s+'(?!http://|https://)%i",
4011 "%".preg_quote($RootUrl.$ThisPrefix."/", "%")."%",
4012 );
4013 $RelativePathReplacements = array(
4014 "src=\"".$RootUrl,
4015 "src='".$RootUrl,
4016 "href=\"".$RootUrl,
4017 "href='".$RootUrl,
4018 "action=\"".$RootUrl,
4019 "action='".$RootUrl,
4020 "@import url(\"".$RootUrl,
4021 "@import url('".$RootUrl,
4022 "src: url(\"".$RootUrl,
4023 "src: url('".$RootUrl,
4024 "@import \"".$RootUrl,
4025 "@import '".$RootUrl,
4026 $Protocol."://".$VHost."/",
4027 );
4028
4029 $NewHtml = preg_replace(
4030 $RelativePathPatterns,
4031 $RelativePathReplacements,
4032 $Html);
4033
4034 # check to make sure relative path fixes didn't fail
4035 $Html = $this->CheckOutputModification(
4036 $Html, $NewHtml,
4037 "alternate domain substitutions");
4038 }
4039 }
4040
4041 return $Html;
4042 }
4043
4062 private function FindFile($DirectoryList, $BaseName,
4063 $PossibleSuffixes = NULL, $PossiblePrefixes = NULL)
4064 {
4065 # generate template cache index for this page
4066 $CacheIndex = md5(serialize($DirectoryList))
4067 .":".self::$ActiveUI.":".$BaseName;
4068
4069 # if caching is enabled and we have cached location
4070 if (($this->TemplateLocationCacheInterval > 0)
4071 && array_key_exists($CacheIndex,
4072 $this->TemplateLocationCache))
4073 {
4074 # use template location from cache
4075 $FoundFileName = $this->TemplateLocationCache[$CacheIndex];
4076 }
4077 else
4078 {
4079 # if suffixes specified and base name does not include suffix
4080 if (count($PossibleSuffixes)
4081 && !preg_match("/\.[a-zA-Z0-9]+$/", $BaseName))
4082 {
4083 # add versions of file names with suffixes to file name list
4084 $FileNames = array();
4085 foreach ($PossibleSuffixes as $Suffix)
4086 {
4087 $FileNames[] = $BaseName.".".$Suffix;
4088 }
4089 }
4090 else
4091 {
4092 # use base name as file name
4093 $FileNames = array($BaseName);
4094 }
4095
4096 # if prefixes specified
4097 if (count($PossiblePrefixes))
4098 {
4099 # add versions of file names with prefixes to file name list
4100 $NewFileNames = array();
4101 foreach ($FileNames as $FileName)
4102 {
4103 foreach ($PossiblePrefixes as $Prefix)
4104 {
4105 $NewFileNames[] = $Prefix.$FileName;
4106 }
4107 }
4108 $FileNames = $NewFileNames;
4109 }
4110
4111 # for each possible location
4112 $FoundFileName = NULL;
4113 foreach ($DirectoryList as $Dir)
4114 {
4115 # substitute active or default UI name into path
4116 $Dir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4117 array(self::$ActiveUI, self::$DefaultUI), $Dir);
4118
4119 # for each possible file name
4120 foreach ($FileNames as $File)
4121 {
4122 # if template is found at location
4123 if (file_exists($Dir.$File))
4124 {
4125 # save full template file name and stop looking
4126 $FoundFileName = $Dir.$File;
4127 break 2;
4128 }
4129 }
4130 }
4131
4132 # save location in cache
4133 $this->TemplateLocationCache[$CacheIndex]
4134 = $FoundFileName;
4135
4136 # set flag indicating that cache should be saved
4137 $this->SaveTemplateLocationCache = TRUE;
4138 }
4139
4140 # return full template file name to caller
4141 return $FoundFileName;
4142 }
4143
4152 private function CompileScssFile($SrcFile)
4153 {
4154 # build path to CSS file
4155 $DstFile = self::$ScssCacheDir."/".dirname($SrcFile)
4156 ."/".basename($SrcFile);
4157 $DstFile = substr_replace($DstFile, "css", -4);
4158
4159 # if SCSS file is newer than CSS file
4160 if (!file_exists($DstFile)
4161 || (filemtime($SrcFile) > filemtime($DstFile)))
4162 {
4163 # attempt to create CSS cache subdirectory if not present
4164 if (!is_dir(dirname($DstFile)))
4165 {
4166 @mkdir(dirname($DstFile), 0777, TRUE);
4167 }
4168
4169 # if CSS cache directory and CSS file path appear writable
4170 static $CacheDirIsWritable;
4171 if (!isset($CacheDirIsWritable))
4172 { $CacheDirIsWritable = is_writable(self::$ScssCacheDir); }
4173 if (is_writable($DstFile)
4174 || (!file_exists($DstFile) && $CacheDirIsWritable))
4175 {
4176 # load SCSS and compile to CSS
4177 $ScssCode = file_get_contents($SrcFile);
4178 $ScssCompiler = new scssc();
4179 $ScssCompiler->setFormatter($this->GenerateCompactCss()
4180 ? "scss_formatter_compressed" : "scss_formatter");
4181 try
4182 {
4183 $CssCode = $ScssCompiler->compile($ScssCode);
4184
4185 # add fingerprinting for URLs in CSS
4186 $this->CssUrlFingerprintPath = dirname($SrcFile);
4187 $CssCode = preg_replace_callback(
4188 "/url\((['\"]*)(.+)\.([a-z]+)(['\"]*)\)/",
4189 array($this, "CssUrlFingerprintInsertion"),
4190 $CssCode);
4191
4192 # strip out comments from CSS (if requested)
4193 if ($this->GenerateCompactCss())
4194 {
4195 $CssCode = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!',
4196 '', $CssCode);
4197 }
4198
4199 # write out CSS file
4200 file_put_contents($DstFile, $CssCode);
4201 }
4202 catch (Exception $Ex)
4203 {
4204 $this->LogError(self::LOGLVL_ERROR,
4205 "Error compiling SCSS file ".$SrcFile.": "
4206 .$Ex->getMessage());
4207 $DstFile = NULL;
4208 }
4209 }
4210 else
4211 {
4212 # log error and set CSS file path to indicate failure
4213 $this->LogError(self::LOGLVL_ERROR,
4214 "Unable to write out CSS file (compiled from SCSS) to "
4215 .$DstFile);
4216 $DstFile = NULL;
4217 }
4218 }
4219
4220 # return CSS file path to caller
4221 return $DstFile;
4222 }
4223
4231 private function MinimizeJavascriptFile($SrcFile)
4232 {
4233 # bail out if file is on exclusion list
4234 foreach ($this->DoNotMinimizeList as $DNMFile)
4235 {
4236 if (($SrcFile == $DNMFile) || (basename($SrcFile) == $DNMFile))
4237 {
4238 return NULL;
4239 }
4240 }
4241
4242 # build path to minimized file
4243 $DstFile = self::$JSMinCacheDir."/".dirname($SrcFile)
4244 ."/".basename($SrcFile);
4245 $DstFile = substr_replace($DstFile, ".min", -3, 0);
4246
4247 # if original file is newer than minimized file
4248 if (!file_exists($DstFile)
4249 || (filemtime($SrcFile) > filemtime($DstFile)))
4250 {
4251 # attempt to create cache subdirectory if not present
4252 if (!is_dir(dirname($DstFile)))
4253 {
4254 @mkdir(dirname($DstFile), 0777, TRUE);
4255 }
4256
4257 # if cache directory and minimized file path appear writable
4258 static $CacheDirIsWritable;
4259 if (!isset($CacheDirIsWritable))
4260 { $CacheDirIsWritable = is_writable(self::$JSMinCacheDir); }
4261 if (is_writable($DstFile)
4262 || (!file_exists($DstFile) && $CacheDirIsWritable))
4263 {
4264 # load JavaScript code
4265 $Code = file_get_contents($SrcFile);
4266
4267 # decide which minimizer to use
4268 if ($this->JSMinimizerJavaScriptPackerAvailable
4269 && $this->JSMinimizerJShrinkAvailable)
4270 {
4271 $Minimizer = (strlen($Code) < 5000)
4272 ? "JShrink" : "JavaScriptPacker";
4273 }
4274 elseif ($this->JSMinimizerJShrinkAvailable)
4275 {
4276 $Minimizer = "JShrink";
4277 }
4278 else
4279 {
4280 $Minimizer = "NONE";
4281 }
4282
4283 # minimize code
4284 switch ($Minimizer)
4285 {
4286 case "JavaScriptMinimizer":
4287 $Packer = new JavaScriptPacker($Code, "Normal");
4288 $MinimizedCode = $Packer->pack();
4289 break;
4290
4291 case "JShrink":
4292 try
4293 {
4294 $MinimizedCode = \JShrink\Minifier::minify($Code);
4295 }
4296 catch (Exception $Exception)
4297 {
4298 unset($MinimizedCode);
4299 $MinimizeError = $Exception->getMessage();
4300 }
4301 break;
4302 }
4303
4304 # if minimization succeeded
4305 if (isset($MinimizedCode))
4306 {
4307 # write out minimized file
4308 file_put_contents($DstFile, $MinimizedCode);
4309 }
4310 else
4311 {
4312 # log error and set destination file path to indicate failure
4313 $ErrMsg = "Unable to minimize JavaScript file ".$SrcFile;
4314 if (isset($MinimizeError))
4315 {
4316 $ErrMsg .= " (".$MinimizeError.")";
4317 }
4318 $this->LogError(self::LOGLVL_ERROR, $ErrMsg);
4319 $DstFile = NULL;
4320 }
4321 }
4322 else
4323 {
4324 # log error and set destination file path to indicate failure
4325 $this->LogError(self::LOGLVL_ERROR,
4326 "Unable to write out minimized JavaScript to file ".$DstFile);
4327 $DstFile = NULL;
4328 }
4329 }
4330
4331 # return CSS file path to caller
4332 return $DstFile;
4333 }
4334
4342 private function CssUrlFingerprintInsertion($Matches)
4343 {
4344 # generate fingerprint string from CSS file modification time
4345 $FileName = realpath($this->CssUrlFingerprintPath."/".
4346 $Matches[2].".".$Matches[3]);
4347 $MTime = filemtime($FileName);
4348 $Fingerprint = sprintf("%06X", ($MTime % 0xFFFFFF));
4349
4350 # build URL string with fingerprint and return it to caller
4351 return "url(".$Matches[1].$Matches[2].".".$Fingerprint
4352 .".".$Matches[3].$Matches[4].")";
4353 }
4354
4361 private function GetRequiredFilesNotYetLoaded($PageContentFile)
4362 {
4363 # start out assuming no files required
4364 $RequiredFiles = array();
4365
4366 # if page content file supplied
4367 if ($PageContentFile)
4368 {
4369 # if file containing list of required files is available
4370 $Path = dirname($PageContentFile);
4371 $RequireListFile = $Path."/REQUIRES";
4372 if (file_exists($RequireListFile))
4373 {
4374 # read in list of required files
4375 $RequestedFiles = file($RequireListFile);
4376
4377 # for each line in required file list
4378 foreach ($RequestedFiles as $Line)
4379 {
4380 # if line is not a comment
4381 $Line = trim($Line);
4382 if (!preg_match("/^#/", $Line))
4383 {
4384 # if file has not already been loaded
4385 if (!in_array($Line, $this->FoundUIFiles))
4386 {
4387 # add to list of required files
4388 $RequiredFiles[] = $Line;
4389 }
4390 }
4391 }
4392 }
4393 }
4394
4395 # add in additional required files if any
4396 if (count($this->AdditionalRequiredUIFiles))
4397 {
4398 # make sure there are no duplicates
4399 $AdditionalRequiredUIFiles = array_unique(
4400 $this->AdditionalRequiredUIFiles);
4401
4402 $RequiredFiles = array_merge(
4403 $RequiredFiles, $AdditionalRequiredUIFiles);
4404 }
4405
4406 # return list of required files to caller
4407 return $RequiredFiles;
4408 }
4409
4420 private function GetUIFileLoadingTag($FileName, $AdditionalAttributes = NULL)
4421 {
4422 # pad additional attributes if supplied
4423 $AddAttribs = $AdditionalAttributes ? " ".$AdditionalAttributes : "";
4424
4425 # retrieve type of UI file
4426 $FileType = $this->GetFileType($FileName);
4427
4428 # construct tag based on file type
4429 switch ($FileType)
4430 {
4431 case self::FT_CSS:
4432 $Tag = "<link rel=\"stylesheet\" type=\"text/css\""
4433 ." media=\"all\" href=\"".$FileName."\""
4434 .$AddAttribs." />";
4435 break;
4436
4437 case self::FT_JAVASCRIPT:
4438 $Tag = "<script type=\"text/javascript\""
4439 ." src=\"".$FileName."\""
4440 .$AddAttribs."></script>";
4441 break;
4442
4443 default:
4444 $Tag = "";
4445 break;
4446 }
4447
4448 # return constructed tag to caller
4449 return $Tag;
4450 }
4451
4457 public static function AutoloadObjects($ClassName)
4458 {
4459 # if caching is not turned off
4460 # and we have a cached location for class
4461 # and file at cached location is readable
4462 if ((self::$ObjectLocationCacheInterval > 0)
4463 && array_key_exists($ClassName,
4464 self::$ObjectLocationCache)
4465 && is_readable(self::$ObjectLocationCache[$ClassName]))
4466 {
4467 # use object location from cache
4468 require_once(self::$ObjectLocationCache[$ClassName]);
4469 }
4470 else
4471 {
4472 # convert any namespace separators in class name
4473 $ClassName = str_replace("\\", "-", $ClassName);
4474
4475 # for each possible object file directory
4476 static $FileLists;
4477 foreach (self::$ObjectDirectories as $Location => $Info)
4478 {
4479 # make any needed replacements in directory path
4480 $Location = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4481 array(self::$ActiveUI, self::$DefaultUI), $Location);
4482
4483 # if directory looks valid
4484 if (is_dir($Location))
4485 {
4486 # build class file name
4487 $NewClassName = ($Info["ClassPattern"] && $Info["ClassReplacement"])
4488 ? preg_replace($Info["ClassPattern"],
4489 $Info["ClassReplacement"], $ClassName)
4490 : $ClassName;
4491
4492 # read in directory contents if not already retrieved
4493 if (!isset($FileLists[$Location]))
4494 {
4495 $FileLists[$Location] = self::ReadDirectoryTree(
4496 $Location, '/^.+\.php$/i');
4497 }
4498
4499 # for each file in target directory
4500 $FileNames = $FileLists[$Location];
4501 $TargetName = strtolower($Info["Prefix"].$NewClassName.".php");
4502 foreach ($FileNames as $FileName)
4503 {
4504 # if file matches our target object file name
4505 if (strtolower($FileName) == $TargetName)
4506 {
4507 # include object file
4508 require_once($Location.$FileName);
4509
4510 # save location to cache
4511 self::$ObjectLocationCache[$ClassName]
4512 = $Location.$FileName;
4513
4514 # set flag indicating that cache should be saved
4515 self::$SaveObjectLocationCache = TRUE;
4516
4517 # stop looking
4518 break 2;
4519 }
4520 }
4521 }
4522 }
4523 }
4524 }
4534 private static function ReadDirectoryTree($Directory, $Pattern)
4535 {
4536 $CurrentDir = getcwd();
4537 chdir($Directory);
4538 $DirIter = new RecursiveDirectoryIterator(".");
4539 $IterIter = new RecursiveIteratorIterator($DirIter);
4540 $RegexResults = new RegexIterator($IterIter, $Pattern,
4541 RecursiveRegexIterator::GET_MATCH);
4542 $FileList = array();
4543 foreach ($RegexResults as $Result)
4544 {
4545 $FileList[] = substr($Result[0], 2);
4546 }
4547 chdir($CurrentDir);
4548 return $FileList;
4549 }
4550
4554 private function UndoMagicQuotes()
4555 {
4556 # if this PHP version has magic quotes support
4557 if (version_compare(PHP_VERSION, "5.4.0", "<"))
4558 {
4559 # turn off runtime magic quotes if on
4560 if (get_magic_quotes_runtime())
4561 {
4562 // @codingStandardsIgnoreStart
4563 set_magic_quotes_runtime(FALSE);
4564 // @codingStandardsIgnoreEnd
4565 }
4566
4567 # if magic quotes GPC is on
4568 if (get_magic_quotes_gpc())
4569 {
4570 # strip added slashes from incoming variables
4571 $GPC = array(&$_GET, &$_POST, &$_COOKIE, &$_REQUEST);
4572 array_walk_recursive($GPC,
4573 array($this, "UndoMagicQuotes_StripCallback"));
4574 }
4575 }
4576 }
4581 private function UndoMagicQuotes_StripCallback(&$Value)
4582 {
4583 $Value = stripslashes($Value);
4584 }
4585
4590 private function LoadUIFunctions()
4591 {
4592 $Dirs = array(
4593 "local/interface/%ACTIVEUI%/include",
4594 "interface/%ACTIVEUI%/include",
4595 "local/interface/%DEFAULTUI%/include",
4596 "interface/%DEFAULTUI%/include",
4597 );
4598 foreach ($Dirs as $Dir)
4599 {
4600 $Dir = str_replace(array("%ACTIVEUI%", "%DEFAULTUI%"),
4601 array(self::$ActiveUI, self::$DefaultUI), $Dir);
4602 if (is_dir($Dir))
4603 {
4604 $FileNames = scandir($Dir);
4605 foreach ($FileNames as $FileName)
4606 {
4607 if (preg_match("/^F-([A-Za-z0-9_]+)\.php/",
4608 $FileName, $Matches)
4609 || preg_match("/^F-([A-Za-z0-9_]+)\.html/",
4610 $FileName, $Matches))
4611 {
4612 if (!function_exists($Matches[1]))
4613 {
4614 include_once($Dir."/".$FileName);
4615 }
4616 }
4617 }
4618 }
4619 }
4620 }
4621
4627 private function ProcessPeriodicEvent($EventName, $Callback)
4628 {
4629 # retrieve last execution time for event if available
4630 $Signature = self::GetCallbackSignature($Callback);
4631 $LastRun = $this->DB->Query("SELECT LastRunAt FROM PeriodicEvents"
4632 ." WHERE Signature = '".addslashes($Signature)."'", "LastRunAt");
4633
4634 # determine whether enough time has passed for event to execute
4635 $ShouldExecute = (($LastRun === NULL)
4636 || (time() > (strtotime($LastRun) + $this->EventPeriods[$EventName])))
4637 ? TRUE : FALSE;
4638
4639 # if event should run
4640 if ($ShouldExecute)
4641 {
4642 # add event to task queue
4643 $WrapperCallback = array("ApplicationFramework", "PeriodicEventWrapper");
4644 $WrapperParameters = array(
4645 $EventName, $Callback, array("LastRunAt" => $LastRun));
4646 $this->QueueUniqueTask($WrapperCallback, $WrapperParameters);
4647 }
4648
4649 # add event to list of periodic events
4650 $this->KnownPeriodicEvents[$Signature] = array(
4651 "Period" => $EventName,
4652 "Callback" => $Callback,
4653 "Queued" => $ShouldExecute);
4654 }
4655
4663 private static function PeriodicEventWrapper($EventName, $Callback, $Parameters)
4664 {
4665 static $DB;
4666 if (!isset($DB)) { $DB = new Database(); }
4667
4668 # run event
4669 $ReturnVal = call_user_func_array($Callback, $Parameters);
4670
4671 # if event is already in database
4672 $Signature = self::GetCallbackSignature($Callback);
4673 if ($DB->Query("SELECT COUNT(*) AS EventCount FROM PeriodicEvents"
4674 ." WHERE Signature = '".addslashes($Signature)."'", "EventCount"))
4675 {
4676 # update last run time for event
4677 $DB->Query("UPDATE PeriodicEvents SET LastRunAt = "
4678 .(($EventName == "EVENT_PERIODIC")
4679 ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
4680 : "NOW()")
4681 ." WHERE Signature = '".addslashes($Signature)."'");
4682 }
4683 else
4684 {
4685 # add last run time for event to database
4686 $DB->Query("INSERT INTO PeriodicEvents (Signature, LastRunAt) VALUES "
4687 ."('".addslashes($Signature)."', "
4688 .(($EventName == "EVENT_PERIODIC")
4689 ? "'".date("Y-m-d H:i:s", time() + ($ReturnVal * 60))."'"
4690 : "NOW()").")");
4691 }
4692 }
4693
4699 private static function GetCallbackSignature($Callback)
4700 {
4701 return !is_array($Callback) ? $Callback
4702 : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
4703 ."::".$Callback[1];
4704 }
4705
4710 private function PrepForTSR()
4711 {
4712 # if HTML has been output and it's time to launch another task
4713 # (only TSR if HTML has been output because otherwise browsers
4714 # may misbehave after connection is closed)
4715 if ((PHP_SAPI != "cli")
4716 && ($this->JumpToPage || !$this->SuppressHTML)
4717 && (time() > (strtotime($this->Settings["LastTaskRunAt"])
4718 + (ini_get("max_execution_time")
4719 / $this->Settings["MaxTasksRunning"]) + 5))
4720 && $this->GetTaskQueueSize()
4721 && $this->Settings["TaskExecutionEnabled"])
4722 {
4723 # begin buffering output for TSR
4724 ob_start();
4725
4726 # let caller know it is time to launch another task
4727 return TRUE;
4728 }
4729 else
4730 {
4731 # let caller know it is not time to launch another task
4732 return FALSE;
4733 }
4734 }
4735
4740 private function LaunchTSR()
4741 {
4742 # set headers to close out connection to browser
4743 if (!$this->NoTSR)
4744 {
4745 ignore_user_abort(TRUE);
4746 header("Connection: close");
4747 header("Content-Length: ".ob_get_length());
4748 }
4749
4750 # output buffered content
4751 while (ob_get_level()) { ob_end_flush(); }
4752 flush();
4753
4754 # write out any outstanding data and end HTTP session
4755 session_write_close();
4756
4757 # set flag indicating that we are now running in background
4758 $this->RunningInBackground = TRUE;
4759
4760 # handle garbage collection for session data
4761 if (isset($this->SessionStorage) &&
4762 (rand()/getrandmax()) <= $this->SessionGcProbability)
4763 {
4764 # determine when sessions will expire
4765 $ExpiredTime = strtotime("-". self::$SessionLifetime." seconds");
4766
4767 # iterate over the files in our session storage, deleting the expired
4768 # ones (sess_ is php's session prefix)
4769 foreach (scandir($this->SessionStorage) as $TgtFile)
4770 {
4771 $FullPath = $this->SessionStorage."/".$TgtFile;
4772 if ( is_file($FullPath) &&
4773 preg_match("/^sess_/", $TgtFile) &&
4774 filectime($FullPath) < $ExpiredTime)
4775 {
4776 unlink($FullPath);
4777 }
4778 }
4779 }
4780
4781 # if there is still a task in the queue
4782 if ($this->GetTaskQueueSize())
4783 {
4784 # garbage collect to give as much memory as possible for tasks
4785 if (function_exists("gc_collect_cycles")) { gc_collect_cycles(); }
4786
4787 # turn on output buffering to (hopefully) record any crash output
4788 ob_start();
4789
4790 # lock tables and grab last task run time to double check
4791 $this->DB->Query("LOCK TABLES ApplicationFrameworkSettings WRITE");
4792 $this->LoadSettings();
4793
4794 # if still time to launch another task
4795 if (time() > (strtotime($this->Settings["LastTaskRunAt"])
4796 + (ini_get("max_execution_time")
4797 / $this->Settings["MaxTasksRunning"]) + 5))
4798 {
4799 # update the "last run" time and release tables
4800 $this->DB->Query("UPDATE ApplicationFrameworkSettings"
4801 ." SET LastTaskRunAt = '".date("Y-m-d H:i:s")."'");
4802 $this->DB->Query("UNLOCK TABLES");
4803
4804 # run tasks while there is a task in the queue
4805 # and enough time and memory left
4806 do
4807 {
4808 # run the next task
4809 $this->RunNextTask();
4810
4811 # calculate percentage of memory still available
4812 $PercentFreeMem = (self::GetFreeMemory()
4813 / self::GetPhpMemoryLimit()) * 100;
4814 }
4815 while ($this->GetTaskQueueSize()
4816 && ($this->GetSecondsBeforeTimeout() > 65)
4817 && ($PercentFreeMem > $this->BackgroundTaskMinFreeMemPercent));
4818 }
4819 else
4820 {
4821 # release tables
4822 $this->DB->Query("UNLOCK TABLES");
4823 }
4824 }
4825 }
4826
4836 private function GetTaskList($DBQuery, $Count, $Offset)
4837 {
4838 $this->DB->Query($DBQuery." LIMIT ".intval($Offset).",".intval($Count));
4839 $Tasks = array();
4840 while ($Row = $this->DB->FetchRow())
4841 {
4842 $Tasks[$Row["TaskId"]] = $Row;
4843 if ($Row["Callback"] ==
4844 serialize(array("ApplicationFramework", "PeriodicEventWrapper")))
4845 {
4846 $WrappedCallback = unserialize($Row["Parameters"]);
4847 $Tasks[$Row["TaskId"]]["Callback"] = $WrappedCallback[1];
4848 $Tasks[$Row["TaskId"]]["Parameters"] = NULL;
4849 }
4850 else
4851 {
4852 $Tasks[$Row["TaskId"]]["Callback"] = unserialize($Row["Callback"]);
4853 $Tasks[$Row["TaskId"]]["Parameters"] = unserialize($Row["Parameters"]);
4854 }
4855 }
4856 return $Tasks;
4857 }
4858
4862 private function RunNextTask()
4863 {
4864 # lock tables to prevent same task from being run by multiple sessions
4865 $this->DB->Query("LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
4866
4867 # look for task at head of queue
4868 $this->DB->Query("SELECT * FROM TaskQueue ORDER BY Priority, TaskId LIMIT 1");
4869 $Task = $this->DB->FetchRow();
4870
4871 # if there was a task available
4872 if ($Task)
4873 {
4874 # move task from queue to running tasks list
4875 $this->DB->Query("INSERT INTO RunningTasks "
4876 ."(TaskId,Callback,Parameters,Priority,Description) "
4877 ."SELECT * FROM TaskQueue WHERE TaskId = "
4878 .intval($Task["TaskId"]));
4879 $this->DB->Query("DELETE FROM TaskQueue WHERE TaskId = "
4880 .intval($Task["TaskId"]));
4881
4882 # release table locks to again allow other sessions to run tasks
4883 $this->DB->Query("UNLOCK TABLES");
4884
4885 # unpack stored task info
4886 $Callback = unserialize($Task["Callback"]);
4887 $Parameters = unserialize($Task["Parameters"]);
4888
4889 # attempt to load task callback if not already available
4890 $this->LoadFunction($Callback);
4891
4892 # save amount of free memory for later comparison
4893 $BeforeFreeMem = self::GetFreeMemory();
4894
4895 # run task
4896 $this->RunningTask = $Task;
4897 if ($Parameters)
4898 {
4899 call_user_func_array($Callback, $Parameters);
4900 }
4901 else
4902 {
4903 call_user_func($Callback);
4904 }
4905 unset($this->RunningTask);
4906
4907 # log if task leaked significant memory
4908 if (function_exists("gc_collect_cycles")) { gc_collect_cycles(); }
4909 $AfterFreeMem = self::GetFreeMemory();
4910 $LeakThreshold = self::GetPhpMemoryLimit()
4911 * ($this->BackgroundTaskMemLeakLogThreshold / 100);
4912 if (($BeforeFreeMem - $AfterFreeMem) > $LeakThreshold)
4913 {
4914 $this->LogError(self::LOGLVL_DEBUG, "Task "
4915 .self::GetTaskCallbackSynopsis(
4916 $this->GetTask($Task["TaskId"]))." leaked "
4917 .number_format($BeforeFreeMem - $AfterFreeMem)." bytes.");
4918 }
4919
4920 # remove task from running tasks list
4921 $this->DB->Query("DELETE FROM RunningTasks"
4922 ." WHERE TaskId = ".intval($Task["TaskId"]));
4923
4924 # prune running tasks list if necessary
4925 $RunningTasksCount = $this->DB->Query(
4926 "SELECT COUNT(*) AS TaskCount FROM RunningTasks", "TaskCount");
4927 if ($RunningTasksCount > $this->MaxRunningTasksToTrack)
4928 {
4929 $this->DB->Query("DELETE FROM RunningTasks ORDER BY StartedAt"
4930 ." LIMIT ".($RunningTasksCount - $this->MaxRunningTasksToTrack));
4931 }
4932 }
4933 else
4934 {
4935 # release table locks to again allow other sessions to run tasks
4936 $this->DB->Query("UNLOCK TABLES");
4937 }
4938 }
4939
4945 public function OnCrash()
4946 {
4947 # attempt to remove any memory limits
4948 $FreeMemory = $this->GetFreeMemory();
4949 ini_set("memory_limit", -1);
4950
4951 # if there is a background task currently running
4952 if (isset($this->RunningTask))
4953 {
4954 # add info about current page load
4955 $CrashInfo["ElapsedTime"] = $this->GetElapsedExecutionTime();
4956 $CrashInfo["FreeMemory"] = $FreeMemory;
4957 $CrashInfo["REMOTE_ADDR"] = $_SERVER["REMOTE_ADDR"];
4958 $CrashInfo["REQUEST_URI"] = $_SERVER["REQUEST_URI"];
4959 if (isset($_SERVER["REQUEST_TIME"]))
4960 {
4961 $CrashInfo["REQUEST_TIME"] = $_SERVER["REQUEST_TIME"];
4962 }
4963 if (isset($_SERVER["REMOTE_HOST"]))
4964 {
4965 $CrashInfo["REMOTE_HOST"] = $_SERVER["REMOTE_HOST"];
4966 }
4967
4968 # add info about error that caused crash (if available)
4969 if (function_exists("error_get_last"))
4970 {
4971 $CrashInfo["LastError"] = error_get_last();
4972 }
4973
4974 # add info about current output buffer contents (if available)
4975 if (ob_get_length() !== FALSE)
4976 {
4977 $CrashInfo["OutputBuffer"] = ob_get_contents();
4978 }
4979
4980 # if backtrace info is available for the crash
4981 $Backtrace = debug_backtrace();
4982 if (count($Backtrace) > 1)
4983 {
4984 # discard the current context from the backtrace
4985 array_shift($Backtrace);
4986
4987 # add the backtrace to the crash info
4988 $CrashInfo["Backtrace"] = $Backtrace;
4989 }
4990 # else if saved backtrace info is available
4991 elseif (isset($this->SavedContext))
4992 {
4993 # add the saved backtrace to the crash info
4994 $CrashInfo["Backtrace"] = $this->SavedContext;
4995 }
4996
4997 # save crash info for currently running task
4998 $DB = new Database();
4999 $DB->Query("UPDATE RunningTasks SET CrashInfo = '"
5000 .addslashes(serialize($CrashInfo))
5001 ."' WHERE TaskId = ".intval($this->RunningTask["TaskId"]));
5002 }
5003
5004 print("\n");
5005 return;
5006 }
5007
5024 private function AddToDirList($DirList, $Dir, $SearchLast, $SkipSlashCheck)
5025 {
5026 # convert incoming directory to array of directories (if needed)
5027 $Dirs = is_array($Dir) ? $Dir : array($Dir);
5028
5029 # reverse array so directories are searched in specified order
5030 $Dirs = array_reverse($Dirs);
5031
5032 # for each directory
5033 foreach ($Dirs as $Location)
5034 {
5035 # make sure directory includes trailing slash
5036 if (!$SkipSlashCheck)
5037 {
5038 $Location = $Location
5039 .((substr($Location, -1) != "/") ? "/" : "");
5040 }
5041
5042 # remove directory from list if already present
5043 if (in_array($Location, $DirList))
5044 {
5045 $DirList = array_diff(
5046 $DirList, array($Location));
5047 }
5048
5049 # add directory to list of directories
5050 if ($SearchLast)
5051 {
5052 array_push($DirList, $Location);
5053 }
5054 else
5055 {
5056 array_unshift($DirList, $Location);
5057 }
5058 }
5059
5060 # return updated directory list to caller
5061 return $DirList;
5062 }
5063
5071 private function ArrayPermutations($Items, $Perms = array())
5072 {
5073 if (empty($Items))
5074 {
5075 $Result = array($Perms);
5076 }
5077 else
5078 {
5079 $Result = array();
5080 for ($Index = count($Items) - 1; $Index >= 0; --$Index)
5081 {
5082 $NewItems = $Items;
5083 $NewPerms = $Perms;
5084 list($Segment) = array_splice($NewItems, $Index, 1);
5085 array_unshift($NewPerms, $Segment);
5086 $Result = array_merge($Result,
5087 $this->ArrayPermutations($NewItems, $NewPerms));
5088 }
5089 }
5090 return $Result;
5091 }
5092
5099 private function OutputModificationCallbackShell($Matches)
5100 {
5101 # call previously-stored external function
5102 return call_user_func($this->OutputModificationCallbackInfo["Callback"],
5103 $Matches,
5104 $this->OutputModificationCallbackInfo["Pattern"],
5105 $this->OutputModificationCallbackInfo["Page"],
5106 $this->OutputModificationCallbackInfo["SearchPattern"]);
5107 }
5108
5117 private function CheckOutputModification($Original, $Modified, $ErrorInfo)
5118 {
5119 # if error was reported by regex engine
5120 if (preg_last_error() !== PREG_NO_ERROR)
5121 {
5122 # log error
5123 $this->LogError(
5124 "Error reported by regex engine when modifying output."
5125 ." (".$ErrorInfo.")");
5126
5127 # use unmodified version of output
5128 $OutputToUse = $Original;
5129 }
5130 # else if modification reduced output by more than threshold
5131 elseif ((strlen(trim($Modified)) / strlen(trim($Original)))
5132 < self::OUTPUT_MODIFICATION_THRESHOLD)
5133 {
5134 # log error
5135 $this->LogError(
5136 "Content reduced below acceptable threshold while modifying output."
5137 ." (".$ErrorInfo.")");
5138
5139 # use unmodified version of output
5140 $OutputToUse = $Original;
5141 }
5142 else
5143 {
5144 # use modified version of output
5145 $OutputToUse = $Modified;
5146 }
5147
5148 # return output to use to caller
5149 return $OutputToUse;
5150 }
5151
5153 const OUTPUT_MODIFICATION_THRESHOLD = 0.10;
5154
5164 private function UpdateSetting(
5165 $FieldName, $NewValue = DB_NOVALUE, $Persistent = TRUE)
5166 {
5167 static $LocalSettings;
5168 if ($NewValue !== DB_NOVALUE)
5169 {
5170 if ($Persistent)
5171 {
5172 $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5173 "ApplicationFrameworkSettings",
5174 $FieldName, $NewValue, NULL, $this->Settings);
5175 }
5176 else
5177 {
5178 $LocalSettings[$FieldName] = $NewValue;
5179 }
5180 }
5181 elseif (!isset($LocalSettings[$FieldName]))
5182 {
5183 $LocalSettings[$FieldName] = $this->DB->UpdateValue(
5184 "ApplicationFrameworkSettings",
5185 $FieldName, $NewValue, NULL, $this->Settings);
5186 }
5187 return $LocalSettings[$FieldName];
5188 }
5189
5191 private $InterfaceDirList = array(
5192 "local/interface/%ACTIVEUI%/",
5193 "interface/%ACTIVEUI%/",
5194 "local/interface/%DEFAULTUI%/",
5195 "interface/%DEFAULTUI%/",
5196 );
5201 private $IncludeDirList = array(
5202 "local/interface/%ACTIVEUI%/include/",
5203 "interface/%ACTIVEUI%/include/",
5204 "interface/%ACTIVEUI%/objects/",
5205 "local/interface/%DEFAULTUI%/include/",
5206 "interface/%DEFAULTUI%/include/",
5207 "interface/%DEFAULTUI%/objects/",
5208 );
5210 private $ImageDirList = array(
5211 "local/interface/%ACTIVEUI%/images/",
5212 "interface/%ACTIVEUI%/images/",
5213 "local/interface/%DEFAULTUI%/images/",
5214 "interface/%DEFAULTUI%/images/",
5215 );
5217 private $FunctionDirList = array(
5218 "local/interface/%ACTIVEUI%/include/",
5219 "interface/%ACTIVEUI%/include/",
5220 "local/interface/%DEFAULTUI%/include/",
5221 "interface/%DEFAULTUI%/include/",
5222 "local/include/",
5223 "include/",
5224 );
5225
5226 const NOVALUE = ".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";
5227
5228
5229 # ---- Page Caching (Internal Methods) -----------------------------------
5230
5236 private function CheckForCachedPage($PageName)
5237 {
5238 # assume no cached page will be found
5239 $CachedPage = NULL;
5240
5241 # if returning a cached page is allowed
5242 if ($this->CacheCurrentPage)
5243 {
5244 # get fingerprint for requested page
5245 $PageFingerprint = $this->GetPageFingerprint($PageName);
5246
5247 # look for matching page in cache in database
5248 $this->DB->Query("SELECT * FROM AF_CachedPages"
5249 ." WHERE Fingerprint = '".addslashes($PageFingerprint)."'");
5250
5251 # if matching page found
5252 if ($this->DB->NumRowsSelected())
5253 {
5254 # if cached page has expired
5255 $Row = $this->DB->FetchRow();
5256 $ExpirationTime = strtotime(
5257 "-".$this->PageCacheExpirationPeriod()." seconds");
5258 if (strtotime($Row["CachedAt"]) < $ExpirationTime)
5259 {
5260 # clear expired pages from cache
5261 $ExpirationTimestamp = date("Y-m-d H:i:s", $ExpirationTime);
5262 $this->DB->Query("DELETE CP, CPTI FROM AF_CachedPages CP,"
5263 ." AF_CachedPageTagInts CPTI"
5264 ." WHERE CP.CachedAt < '".$ExpirationTimestamp."'"
5265 ." AND CPTI.CacheId = CP.CacheId");
5266 $this->DB->Query("DELETE FROM AF_CachedPages "
5267 ." WHERE CachedAt < '".$ExpirationTimestamp."'");
5268 }
5269 else
5270 {
5271 # display cached page and exit
5272 $CachedPage = $Row["PageContent"];
5273 }
5274 }
5275 }
5276
5277 # return any cached page found to caller
5278 return $CachedPage;
5279 }
5280
5286 private function UpdatePageCache($PageName, $PageContent)
5287 {
5288 # if page caching is enabled and current page should be cached
5289 if ($this->PageCacheEnabled()
5290 && $this->CacheCurrentPage
5291 && ($PageName != "404"))
5292 {
5293 # if page content looks invalid
5294 if (strlen(trim(strip_tags($PageContent))) == 0)
5295 {
5296 # log error
5297 $LogMsg = "Page not cached because content was empty."
5298 ." (PAGE: ".$PageName.", URL: ".$this->FullUrl().")";
5299 $this->LogError(self::LOGLVL_ERROR, $LogMsg);
5300 }
5301 else
5302 {
5303 # save page to cache
5304 $PageFingerprint = $this->GetPageFingerprint($PageName);
5305 $this->DB->Query("INSERT INTO AF_CachedPages"
5306 ." (Fingerprint, PageContent) VALUES"
5307 ." ('".$this->DB->EscapeString($PageFingerprint)."', '"
5308 .$this->DB->EscapeString($PageContent)."')");
5309 $CacheId = $this->DB->LastInsertId();
5310
5311 # for each page cache tag that was added
5312 foreach ($this->PageCacheTags as $Tag => $Pages)
5313 {
5314 # if current page is in list for tag
5315 if (in_array("CURRENT", $Pages) || in_array($PageName, $Pages))
5316 {
5317 # look up tag ID
5318 $TagId = $this->GetPageCacheTagId($Tag);
5319
5320 # mark current page as associated with tag
5321 $this->DB->Query("INSERT INTO AF_CachedPageTagInts"
5322 ." (TagId, CacheId) VALUES "
5323 ." (".intval($TagId).", ".intval($CacheId).")");
5324 }
5325 }
5326 }
5327 }
5328 }
5329
5335 private function GetPageCacheTagId($Tag)
5336 {
5337 # if tag is a non-negative integer
5338 if (is_numeric($Tag) && ($Tag > 0) && (intval($Tag) == $Tag))
5339 {
5340 # generate ID
5341 $Id = self::PAGECACHETAGIDOFFSET + $Tag;
5342 }
5343 else
5344 {
5345 # look up ID in database
5346 $Id = $this->DB->Query("SELECT TagId FROM AF_CachedPageTags"
5347 ." WHERE Tag = '".addslashes($Tag)."'", "TagId");
5348
5349 # if ID was not found
5350 if ($Id === NULL)
5351 {
5352 # add tag to database
5353 $this->DB->Query("INSERT INTO AF_CachedPageTags"
5354 ." SET Tag = '".addslashes($Tag)."'");
5355 $Id = $this->DB->LastInsertId();
5356 }
5357 }
5358
5359 # return tag ID to caller
5360 return $Id;
5361 }
5362
5368 private function GetPageFingerprint($PageName)
5369 {
5370 # only get the environmental fingerprint once so that it is consistent
5371 # between page construction start and end
5372 static $EnvFingerprint;
5373 if (!isset($EnvFingerprint))
5374 {
5375 $EnvData = json_encode($_GET).json_encode($_POST);
5376
5377 # if alternate domain support is enabled
5378 if ($this->HtaccessSupport() && self::$RootUrlOverride !== NULL)
5379 {
5380 # and if we were accessed via an alternate domain
5381 $VHost = $_SERVER["SERVER_NAME"];
5382 if (isset($this->AlternateDomainPrefixes[$VHost]))
5383 {
5384 # then add the alternate domain that was used to our
5385 # environment data
5386 $EnvData .= $VHost;
5387 }
5388 }
5389
5390 $EnvFingerprint = md5($EnvData);
5391
5392 }
5393
5394 # build page fingerprint and return it to caller
5395 return $PageName."-".$EnvFingerprint;
5396 }
5397 }
Message
Abstraction for forum messages and resource comments.
Definition: Message.php:15
Database
SQL database abstraction object with smart query caching.
Definition: Database.php:22
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
PHP
PHP
Definition: RSSClient--Test.php:12
DB_NOVALUE
const DB_NOVALUE
Definition: Database.php:1396
JavaScriptPacker
Definition: JavaScriptPacker.php:69
$Items
$Items
Definition: RSSClient--Test.php:26