4 # FILE: ApplicationFramework.php
6 # Part of the ScoutLib application support library
7 # Copyright 2009-2012 Edward Almasy and Internet Scout
8 # http://scout.wisc.edu
17 # ---- PUBLIC INTERFACE --------------------------------------------------
25 function __construct()
27 # save execution start time
28 $this->ExecutionStartTime = microtime(TRUE);
30 # begin/restore PHP session
31 $SessionPath = isset($_SERVER[
"REQUEST_URI"])
32 ? dirname($_SERVER[
"REQUEST_URI"])
33 : isset($_SERVER[
"SCRIPT_NAME"])
34 ? dirname($_SERVER[
"SCRIPT_NAME"])
35 : isset($_SERVER[
"PHP_SELF"])
36 ? dirname($_SERVER[
"PHP_SELF"])
38 $SessionDomain = isset($_SERVER[
"SERVER_NAME"]) ? $_SERVER[
"SERVER_NAME"]
39 : isset($_SERVER[
"HTTP_HOST"]) ? $_SERVER[
"HTTP_HOST"]
41 $SessionStorage = session_save_path()
42 .
"/".self::$AppName.
"_".md5($SessionDomain.$SessionPath);
43 if (!is_dir($SessionStorage)) { mkdir($SessionStorage, 0700 ); }
44 session_save_path($SessionStorage);
45 ini_set(
"session.gc_maxlifetime", self::$SessionLifetime);
46 session_set_cookie_params(
47 self::$SessionLifetime, $SessionPath, $SessionDomain);
50 # set up object file autoloader
51 $this->SetUpObjectAutoloading();
53 # set up function to output any buffered text in case of crash
54 register_shutdown_function(array($this,
"OnCrash"));
56 # set up our internal environment
59 # set up our exception handler
60 set_exception_handler(array($this,
"GlobalExceptionHandler"));
62 # load our settings from database
63 $this->LoadSettings();
65 # set PHP maximum execution time
68 # register events we handle internally
80 # if template location cache is flagged to be saved
81 if ($this->SaveTemplateLocationCache)
83 # write template location cache out and update cache expiration
84 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings"
85 .
" SET TemplateLocationCache = '"
86 .addslashes(serialize(
87 $this->Settings[
"TemplateLocationCache"])).
"',"
88 .
" TemplateLocationCacheExpiration = "
90 .$this->Settings[
"TemplateLocationCacheInterval"]
100 function GlobalExceptionHandler($Exception)
102 # display exception info
103 $Location = $Exception->getFile().
"[".$Exception->getLine().
"]";
104 ?><table width=
"100%" cellpadding=
"5"
105 style=
"border: 2px solid #666666; background: #CCCCCC;
106 font-family: Courier New, Courier, monospace;
107 margin-top: 10px;"><tr><td>
108 <div style=
"color: #666666;">
109 <span style=
"font-size: 150%;">
110 <b>Uncaught Exception</b></span><br />
111 <b>
Message:</b> <i><?
PHP print $Exception->getMessage(); ?></i><br />
112 <b>Location:</b> <i><?
PHP print $Location; ?></i><br />
114 <blockquote><pre><?
PHP print $Exception->getTraceAsString();
115 ?></pre></blockquote>
117 </td></tr></table><?
PHP
119 # log exception if possible
120 $LogMsg =
"Uncaught exception (".$Exception->getMessage().
").";
121 $this->
LogError(self::LOGLVL_ERROR, $LogMsg);
137 $Dir, $Prefix =
"", $ClassPattern = NULL, $ClassReplacement = NULL)
139 # make sure directory has trailing slash
140 $Dir = $Dir.((substr($Dir, -1) !=
"/") ?
"/" :
"");
142 # add directory to directory list
143 self::$ObjectDirectories = array_merge(
146 "ClassPattern" => $ClassPattern,
147 "ClassReplacement" => $ClassReplacement,
149 self::$ObjectDirectories);
173 # add directories to existing image directory list
174 $this->ImageDirList = $this->AddToDirList(
175 $this->ImageDirList, $Dir, $SearchLast, $SkipSlashCheck);
200 # add directories to existing image directory list
201 $this->IncludeDirList = $this->AddToDirList(
202 $this->IncludeDirList, $Dir, $SearchLast, $SkipSlashCheck);
226 # add directories to existing image directory list
227 $this->InterfaceDirList = $this->AddToDirList(
228 $this->InterfaceDirList, $Dir, $SearchLast, $SkipSlashCheck);
252 # add directories to existing image directory list
253 $this->FunctionDirList = $this->AddToDirList(
254 $this->FunctionDirList, $Dir, $SearchLast, $SkipSlashCheck);
264 $this->BrowserDetectFunc = $DetectionFunc;
275 if (is_callable($Callback))
277 $this->UnbufferedCallbacks[] = array($Callback, $Parameters);
289 if ($NewInterval >= 0)
291 $this->Settings[
"TemplateLocationCacheInterval"] = $NewInterval;
292 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings"
293 .
" SET TemplateLocationCacheInterval = '"
294 .intval($NewInterval).
"'");
296 return $this->Settings[
"TemplateLocationCacheInterval"];
306 if ($NewValue !== NULL)
308 self::$SessionLifetime = $NewValue;
310 return self::$SessionLifetime;
319 # sanitize incoming page name and save local copy
320 $PageName = preg_replace(
"/[^a-zA-Z0-9_.-]/",
"", $PageName);
321 $this->PageName = $PageName;
323 # buffer any output from includes or PHP file
326 # include any files needed to set up execution environment
327 foreach ($this->EnvIncludes as $IncludeFile)
329 include($IncludeFile);
333 $this->
SignalEvent(
"EVENT_PAGE_LOAD", array(
"PageName" => $PageName));
335 # signal PHP file load
336 $SignalResult = $this->
SignalEvent(
"EVENT_PHP_FILE_LOAD", array(
337 "PageName" => $PageName));
339 # if signal handler returned new page name value
340 $NewPageName = $PageName;
341 if (($SignalResult[
"PageName"] != $PageName)
342 && strlen($SignalResult[
"PageName"]))
344 # if new page name value is page file
345 if (file_exists($SignalResult[
"PageName"]))
347 # use new value for PHP file name
348 $PageFile = $SignalResult[
"PageName"];
352 # use new value for page name
353 $NewPageName = $SignalResult[
"PageName"];
357 # if we do not already have a PHP file
358 if (!isset($PageFile))
360 # look for PHP file for page
361 $OurPageFile =
"pages/".$NewPageName.
".php";
362 $LocalPageFile =
"local/pages/".$NewPageName.
".php";
363 $PageFile = file_exists($LocalPageFile) ? $LocalPageFile
364 : (file_exists($OurPageFile) ? $OurPageFile
365 :
"pages/".$this->DefaultPage.
".php");
371 # save buffered output to be displayed later after HTML file loads
372 $PageOutput = ob_get_contents();
375 # signal PHP file load is complete
377 $Context[
"Variables"] = get_defined_vars();
379 array(
"PageName" => $PageName,
"Context" => $Context));
380 $PageCompleteOutput = ob_get_contents();
383 # set up for possible TSR (Terminate and Stay Resident :))
384 $ShouldTSR = $this->PrepForTSR();
386 # if PHP file indicated we should autorefresh to somewhere else
387 if ($this->JumpToPage)
389 if (!strlen(trim($PageOutput)))
393 <meta http-equiv=
"refresh" content=
"0; URL=<?PHP
394 print($this->JumpToPage); ?>">
396 <body bgcolor=
"white">
401 # else if HTML loading is not suppressed
402 elseif (!$this->SuppressHTML)
404 # set content-type to get rid of diacritic errors
405 header(
"Content-Type: text/html; charset="
408 # load common HTML file (defines common functions) if available
409 $CommonHtmlFile = $this->FindFile($this->IncludeDirList,
410 "Common", array(
"tpl",
"html"));
411 if ($CommonHtmlFile) { include($CommonHtmlFile); }
414 $this->LoadUIFunctions();
416 # begin buffering content
419 # signal HTML file load
420 $SignalResult = $this->
SignalEvent(
"EVENT_HTML_FILE_LOAD", array(
421 "PageName" => $PageName));
423 # if signal handler returned new page name value
424 $NewPageName = $PageName;
425 $PageContentFile = NULL;
426 if (($SignalResult[
"PageName"] != $PageName)
427 && strlen($SignalResult[
"PageName"]))
429 # if new page name value is HTML file
430 if (file_exists($SignalResult[
"PageName"]))
432 # use new value for HTML file name
433 $PageContentFile = $SignalResult[
"PageName"];
437 # use new value for page name
438 $NewPageName = $SignalResult[
"PageName"];
442 # load page content HTML file if available
443 if ($PageContentFile === NULL)
445 $PageContentFile = $this->FindFile(
446 $this->InterfaceDirList, $NewPageName,
447 array(
"tpl",
"html"));
449 if ($PageContentFile)
451 include($PageContentFile);
455 print
"<h2>ERROR: No HTML/TPL template found"
456 .
" for this page.</h2>";
459 # signal HTML file load complete
460 $SignalResult = $this->
SignalEvent(
"EVENT_HTML_FILE_LOAD_COMPLETE");
462 # stop buffering and save output
463 $PageContentOutput = ob_get_contents();
466 # load page start HTML file if available
468 $PageStartFile = $this->FindFile($this->IncludeDirList,
"Start",
469 array(
"tpl",
"html"), array(
"StdPage",
"StandardPage"));
470 if ($PageStartFile) { include($PageStartFile); }
471 $PageStartOutput = ob_get_contents();
474 # load page end HTML file if available
476 $PageEndFile = $this->FindFile($this->IncludeDirList,
"End",
477 array(
"tpl",
"html"), array(
"StdPage",
"StandardPage"));
478 if ($PageEndFile) { include($PageEndFile); }
479 $PageEndOutput = ob_get_contents();
482 # get list of any required files not loaded
483 $RequiredFiles = $this->GetRequiredFilesNotYetLoaded($PageContentFile);
485 # if a browser detection function has been made available
486 if (is_callable($this->BrowserDetectFunc))
488 # call function to get browser list
489 $Browsers = call_user_func($this->BrowserDetectFunc);
491 # for each required file
492 $NewRequiredFiles = array();
493 foreach ($RequiredFiles as $File)
495 # if file name includes browser keyword
496 if (preg_match(
"/%BROWSER%/", $File))
499 foreach ($Browsers as $Browser)
501 # substitute in browser name and add to new file list
502 $NewRequiredFiles[] = preg_replace(
503 "/%BROWSER%/", $Browser, $File);
508 # add to new file list
509 $NewRequiredFiles[] = $File;
512 $RequiredFiles = $NewRequiredFiles;
515 # for each required file
516 foreach ($RequiredFiles as $File)
518 # locate specific file to use
519 $FilePath = $this->
GUIFile($File);
524 # determine file type
525 $NamePieces = explode(
".", $File);
526 $FileSuffix = strtolower(array_pop($NamePieces));
528 # add file to HTML output based on file type
529 $FilePath = htmlspecialchars($FilePath);
533 $Tag =
'<script type="text/javascript" src="'
534 .$FilePath.
'"></script>';
535 $PageEndOutput = preg_replace(
536 "#</body>#i", $Tag.
"\n</body>", $PageEndOutput, 1);
540 $Tag =
'<link rel="stylesheet" type="text/css"'
541 .
' media="all" href="'.$FilePath.
'">';
542 $PageStartOutput = preg_replace(
543 "#</head>#i", $Tag.
"\n</head>", $PageStartOutput, 1);
550 print $PageStartOutput.$PageContentOutput.$PageEndOutput;
553 # run any post-processing routines
554 foreach ($this->PostProcessingFuncs as $Func)
556 call_user_func_array($Func[
"FunctionName"], $Func[
"Arguments"]);
559 # write out any output buffered from page code execution
560 if (strlen($PageOutput))
562 if (!$this->SuppressHTML)
564 ?><table width=
"100%" cellpadding=
"5"
565 style=
"border: 2px solid #666666; background: #CCCCCC;
566 font-family: Courier New, Courier, monospace;
567 margin-top: 10px;"><tr><td><?
PHP
569 if ($this->JumpToPage)
571 ?><div style=
"color: #666666;"><span style=
"font-size: 150%;">
572 <b>Page Jump Aborted</b></span>
573 (because of error or other unexpected output)<br />
575 <i><?
PHP print($this->JumpToPage); ?></i></div><?
PHP
578 if (!$this->SuppressHTML)
580 ?></td></tr></table><?
PHP
584 # write out any output buffered from the page code execution complete signal
585 if (!$this->JumpToPage && !$this->SuppressHTML && strlen($PageCompleteOutput))
587 print $PageCompleteOutput;
590 # execute callbacks that should not have their output buffered
591 foreach ($this->UnbufferedCallbacks as $Callback)
593 call_user_func_array($Callback[0], $Callback[1]);
596 # terminate and stay resident (TSR!) if indicated and HTML has been output
597 # (only TSR if HTML has been output because otherwise browsers will misbehave)
598 if ($ShouldTSR) { $this->LaunchTSR(); }
608 return $this->PageName;
619 if (!is_null($Page) && (strpos($Page,
"?") === FALSE)
620 && ((strpos($Page,
"=") !== FALSE)
621 || ((stripos($Page,
".php") === FALSE)
622 && (stripos($Page,
".htm") === FALSE)
623 && (strpos($Page,
"/") === FALSE)))
624 && (stripos($Page,
"http://") !== 0)
625 && (stripos($Page,
"https://") !== 0))
627 $this->JumpToPage =
"index.php?P=".$Page;
631 $this->JumpToPage = $Page;
641 return ($this->JumpToPage === NULL) ? FALSE : TRUE;
655 if ($NewSetting !== NULL) { $this->
HtmlCharset = $NewSetting; }
656 return $this->HtmlCharset;
667 $this->SuppressHTML = $NewSetting;
678 if ($UIName !== NULL)
680 $this->ActiveUI = preg_replace(
"/^SPTUI--/",
"", $UIName);
682 return $this->ActiveUI;
692 # possible UI directories
693 $InterfaceDirs = array(
697 # start out with an empty list
698 $Interfaces = array();
700 # for each possible UI directory
701 foreach ($InterfaceDirs as $InterfaceDir)
703 $Dir = dir($InterfaceDir);
705 # for each file in current directory
706 while (($DirEntry = $Dir->read()) !== FALSE)
708 $InterfacePath = $InterfaceDir.
"/".$DirEntry;
710 # skip anything that doesn't have a name in the required format
711 if (!preg_match(
'/^[a-zA-Z]+$/', $DirEntry))
716 # skip anything that isn't a directory
717 if (!is_dir($InterfacePath))
722 # read the UI name (if available)
723 $UIName = @file_get_contents($InterfacePath.
"/NAME");
725 # use the directory name if the UI name isn't available
726 if ($UIName === FALSE || !strlen($UIName))
731 $Interfaces[$InterfacePath] = $UIName;
737 # return list to caller
757 &$Arg1 = self::NOVALUE, &$Arg2 = self::NOVALUE, &$Arg3 = self::NOVALUE,
758 &$Arg4 = self::NOVALUE, &$Arg5 = self::NOVALUE, &$Arg6 = self::NOVALUE,
759 &$Arg7 = self::NOVALUE, &$Arg8 = self::NOVALUE, &$Arg9 = self::NOVALUE)
761 $FuncIndex = count($this->PostProcessingFuncs);
762 $this->PostProcessingFuncs[$FuncIndex][
"FunctionName"] = $FunctionName;
763 $this->PostProcessingFuncs[$FuncIndex][
"Arguments"] = array();
765 while (isset(${
"Arg".$Index}) && (${
"Arg".$Index} !== self::NOVALUE))
767 $this->PostProcessingFuncs[$FuncIndex][
"Arguments"][$Index]
780 $this->EnvIncludes[] = $FileName;
791 # determine which location to search based on file suffix
792 $FileIsImage = preg_match(
"/\.(gif|jpg|png)$/", $FileName);
793 $DirList = $FileIsImage ? $this->ImageDirList : $this->IncludeDirList;
796 $FoundFileName = $this->FindFile($DirList, $FileName);
798 # add non-image files to list of found files (used for required files loading)
799 if (!$FileIsImage) { $this->FoundUIFiles[] = basename($FoundFileName); }
801 # return file name to caller
802 return $FoundFileName;
816 $FullFileName = $this->
GUIFile($FileName);
817 if ($FullFileName) { print($FullFileName); }
829 $this->AdditionalRequiredUIFiles[] = $FileName;
842 # if specified function is not currently available
843 if (!is_callable($Callback))
845 # if function info looks legal
846 if (is_string($Callback) && strlen($Callback))
848 # start with function directory list
849 $Locations = $this->FunctionDirList;
851 # add object directories to list
852 $Locations = array_merge(
853 $Locations, array_keys(self::$ObjectDirectories));
855 # look for function file
856 $FunctionFileName = $this->FindFile($Locations,
"F-".$Callback,
857 array(
"php",
"html"));
859 # if function file was found
860 if ($FunctionFileName)
863 include_once($FunctionFileName);
867 # log error indicating function load failed
868 $this->
LogError(self::LOGLVL_ERROR,
"Unable to load function"
869 .
" for callback \"".$Callback.
"\".");
874 # log error indicating specified function info was bad
875 $this->
LogError(self::LOGLVL_ERROR,
"Unloadable callback value"
877 .
" passed to AF::LoadFunction().");
881 # report to caller whether function load succeeded
882 return is_callable($Callback);
891 return microtime(TRUE) - $this->ExecutionStartTime;
909 # HTACCESS_SUPPORT is set in the .htaccess file
910 return isset($_SERVER[
"HTACCESS_SUPPORT"]);
926 # if error level is at or below current logging level
927 if ($this->Settings[
"LoggingLevel"] >= $Level)
929 # attempt to log error message
932 # if logging attempt failed and level indicated significant error
933 if (($Result === FALSE) && ($Level <= self::LOGLVL_ERROR))
935 # throw exception about inability to log error
936 static $AlreadyThrewException = FALSE;
937 if (!$AlreadyThrewException)
939 $AlreadyThrewException = TRUE;
940 throw new Exception(
"Unable to log error (".$Level.
": ".$Msg.
").");
944 # report to caller whether message was logged
949 # report to caller that message was not logged
967 # if message level is at or below current logging level
968 if ($this->Settings[
"LoggingLevel"] >= $Level)
970 # attempt to open log file
971 $FHndl = @fopen(
"local/logs/cwis.log",
"a");
973 # if log file could not be open
974 if ($FHndl === FALSE)
976 # report to caller that message was not logged
982 $ErrorAbbrevs = array(
983 self::LOGLVL_FATAL =>
"FTL",
984 self::LOGLVL_ERROR =>
"ERR",
985 self::LOGLVL_WARNING =>
"WRN",
986 self::LOGLVL_INFO =>
"INF",
987 self::LOGLVL_DEBUG =>
"DBG",
988 self::LOGLVL_TRACE =>
"TRC",
990 $LogEntry = date(
"Y-m-d H:i:s")
991 .
" ".($this->RunningInBackground ?
"B" :
"F")
992 .
" ".$ErrorAbbrevs[$Level]
996 $Success = fputs($FHndl, $LogEntry.
"\n");
1001 # report to caller whether message was logged
1002 return ($Success === FALSE) ? FALSE : TRUE;
1007 # report to caller that message was not logged
1035 # if new logging level was specified
1036 if ($NewValue !== NULL)
1038 # constrain new level to within legal bounds and store locally
1039 $this->Settings[
"LoggingLevel"] = max(min($NewValue, 6), 1);
1041 # save new logging level in database
1042 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings"
1043 .
" SET LoggingLevel = "
1044 .intval($this->Settings[
"LoggingLevel"]));
1047 # report current logging level to caller
1048 return $this->Settings[
"LoggingLevel"];
1086 # ---- Event Handling ----------------------------------------------------
1132 # convert parameters to array if not already in that form
1133 $Events = is_array($EventsOrEventName) ? $EventsOrEventName
1134 : array($EventsOrEventName => $Type);
1137 foreach ($Events as $Name => $Type)
1139 # store event information
1140 $this->RegisteredEvents[$Name][
"Type"] = $Type;
1141 $this->RegisteredEvents[$Name][
"Hooks"] = array();
1152 return array_key_exists($EventName, $this->RegisteredEvents)
1169 function HookEvent($EventsOrEventName, $Callback = NULL, $Order = self::ORDER_MIDDLE)
1171 # convert parameters to array if not already in that form
1172 $Events = is_array($EventsOrEventName) ? $EventsOrEventName
1173 : array($EventsOrEventName => $Callback);
1177 foreach ($Events as $EventName => $EventCallback)
1179 # if callback is valid
1180 if (is_callable($EventCallback))
1182 # if this is a periodic event we process internally
1183 if (isset($this->PeriodicEvents[$EventName]))
1186 $this->ProcessPeriodicEvent($EventName, $EventCallback);
1188 # if specified event has been registered
1189 elseif (isset($this->RegisteredEvents[$EventName]))
1191 # add callback for event
1192 $this->RegisteredEvents[$EventName][
"Hooks"][]
1193 = array(
"Callback" => $EventCallback,
"Order" => $Order);
1195 # sort callbacks by order
1196 if (count($this->RegisteredEvents[$EventName][
"Hooks"]) > 1)
1198 usort($this->RegisteredEvents[$EventName][
"Hooks"],
1199 array(
"ApplicationFramework",
"HookEvent_OrderCompare"));
1213 # report to caller whether all callbacks were hooked
1216 private static function HookEvent_OrderCompare($A, $B)
1218 if ($A[
"Order"] == $B[
"Order"]) {
return 0; }
1219 return ($A[
"Order"] < $B[
"Order"]) ? -1 : 1;
1232 $ReturnValue = NULL;
1234 # if event has been registered
1235 if (isset($this->RegisteredEvents[$EventName]))
1237 # set up default return value (if not NULL)
1238 switch ($this->RegisteredEvents[$EventName][
"Type"])
1240 case self::EVENTTYPE_CHAIN:
1241 $ReturnValue = $Parameters;
1244 case self::EVENTTYPE_NAMED:
1245 $ReturnValue = array();
1249 # for each callback for this event
1250 foreach ($this->RegisteredEvents[$EventName][
"Hooks"] as $Hook)
1253 $Callback = $Hook[
"Callback"];
1254 $Result = ($Parameters !== NULL)
1255 ? call_user_func_array($Callback, $Parameters)
1256 : call_user_func($Callback);
1258 # process return value based on event type
1259 switch ($this->RegisteredEvents[$EventName][
"Type"])
1261 case self::EVENTTYPE_CHAIN:
1262 if ($Result !== NULL)
1264 foreach ($Parameters as $Index => $Value)
1266 if (array_key_exists($Index, $Result))
1268 $Parameters[$Index] = $Result[$Index];
1271 $ReturnValue = $Parameters;
1275 case self::EVENTTYPE_FIRST:
1276 if ($Result !== NULL)
1278 $ReturnValue = $Result;
1283 case self::EVENTTYPE_NAMED:
1284 $CallbackName = is_array($Callback)
1285 ? (is_object($Callback[0])
1286 ? get_class($Callback[0])
1287 : $Callback[0]).
"::".$Callback[1]
1289 $ReturnValue[$CallbackName] = $Result;
1299 $this->
LogError(self::LOGLVL_WARNING,
1300 "Unregistered event signaled (".$EventName.
").");
1303 # return value if any to caller
1304 return $ReturnValue;
1314 return isset($this->PeriodicEvents[$EventName]) ? TRUE : FALSE;
1319 # ---- Task Management ---------------------------------------------------
1345 $Priority = self::PRIORITY_MEDIUM, $Description =
"")
1347 # pack task info and write to database
1348 if ($Parameters === NULL) { $Parameters = array(); }
1349 $this->DB->Query(
"INSERT INTO TaskQueue"
1350 .
" (Callback, Parameters, Priority, Description)"
1351 .
" VALUES ('".addslashes(serialize($Callback)).
"', '"
1352 .addslashes(serialize($Parameters)).
"', ".intval($Priority).
", '"
1353 .addslashes($Description).
"')");
1374 $Priority = self::PRIORITY_MEDIUM, $Description =
"")
1378 $QueryResult = $this->DB->Query(
"SELECT TaskId,Priority FROM TaskQueue"
1379 .
" WHERE Callback = '".addslashes(serialize($Callback)).
"'"
1380 .($Parameters ?
" AND Parameters = '"
1381 .addslashes(serialize($Parameters)).
"'" :
""));
1382 if ($QueryResult !== FALSE)
1384 $Record = $this->DB->FetchRow();
1385 if ($Record[
"Priority"] > $Priority)
1387 $this->DB->Query(
"UPDATE TaskQueue"
1388 .
" SET Priority = ".intval($Priority)
1389 .
" WHERE TaskId = ".intval($Record[
"TaskId"]));
1396 $this->
QueueTask($Callback, $Parameters, $Priority, $Description);
1412 $FoundCount = $this->DB->Query(
"SELECT COUNT(*) AS FoundCount FROM TaskQueue"
1413 .
" WHERE Callback = '".addslashes(serialize($Callback)).
"'"
1414 .($Parameters ?
" AND Parameters = '"
1415 .addslashes(serialize($Parameters)).
"'" :
""),
1417 + $this->DB->Query(
"SELECT COUNT(*) AS FoundCount FROM RunningTasks"
1418 .
" WHERE Callback = '".addslashes(serialize($Callback)).
"'"
1419 .($Parameters ?
" AND Parameters = '"
1420 .addslashes(serialize($Parameters)).
"'" :
""),
1422 return ($FoundCount ? TRUE : FALSE);
1432 return $this->DB->Query(
"SELECT COUNT(*) AS QueueSize FROM TaskQueue"
1433 .($Priority ?
" WHERE Priority = ".intval($Priority) :
""),
1446 return $this->GetTaskList(
"SELECT * FROM TaskQueue"
1447 .
" ORDER BY Priority, TaskId ", $Count, $Offset);
1459 return $this->GetTaskList(
"SELECT * FROM RunningTasks"
1460 .
" WHERE StartedAt >= '".date(
"Y-m-d H:i:s",
1461 (time() - ini_get(
"max_execution_time"))).
"'"
1462 .
" ORDER BY StartedAt", $Count, $Offset);
1474 return $this->GetTaskList(
"SELECT * FROM RunningTasks"
1475 .
" WHERE StartedAt < '".date(
"Y-m-d H:i:s",
1476 (time() - ini_get(
"max_execution_time"))).
"'"
1477 .
" ORDER BY StartedAt", $Count, $Offset);
1486 return $this->DB->Query(
"SELECT COUNT(*) AS Count FROM RunningTasks"
1487 .
" WHERE StartedAt < '".date(
"Y-m-d H:i:s",
1488 (time() - ini_get(
"max_execution_time"))).
"'",
1499 $this->DB->Query(
"LOCK TABLES TaskQueue WRITE, RunningTasks WRITE");
1500 $this->DB->Query(
"INSERT INTO TaskQueue"
1501 .
" (Callback,Parameters,Priority,Description) "
1502 .
"SELECT Callback, Parameters, Priority, Description"
1503 .
" FROM RunningTasks WHERE TaskId = ".intval($TaskId));
1504 if ($NewPriority !== NULL)
1506 $NewTaskId = $this->DB->LastInsertId(
"TaskQueue");
1507 $this->DB->Query(
"UPDATE TaskQueue SET Priority = "
1508 .intval($NewPriority)
1509 .
" WHERE TaskId = ".intval($NewTaskId));
1511 $this->DB->Query(
"DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
1512 $this->DB->Query(
"UNLOCK TABLES");
1521 $this->DB->Query(
"DELETE FROM TaskQueue WHERE TaskId = ".intval($TaskId));
1522 $this->DB->Query(
"DELETE FROM RunningTasks WHERE TaskId = ".intval($TaskId));
1534 # assume task will not be found
1537 # look for task in task queue
1538 $this->DB->Query(
"SELECT * FROM TaskQueue WHERE TaskId = ".intval($TaskId));
1540 # if task was not found in queue
1541 if (!$this->DB->NumRowsSelected())
1543 # look for task in running task list
1544 $this->DB->Query(
"SELECT * FROM RunningTasks WHERE TaskId = "
1549 if ($this->DB->NumRowsSelected())
1551 # if task was periodic
1552 $Row = $this->DB->FetchRow();
1553 if ($Row[
"Callback"] ==
1554 serialize(array(
"ApplicationFramework",
"PeriodicEventWrapper")))
1556 # unpack periodic task callback
1557 $WrappedCallback = unserialize($Row[
"Parameters"]);
1558 $Task[
"Callback"] = $WrappedCallback[1];
1559 $Task[
"Parameters"] = $WrappedCallback[2];
1563 # unpack task callback and parameters
1564 $Task[
"Callback"] = unserialize($Row[
"Callback"]);
1565 $Task[
"Parameters"] = unserialize($Row[
"Parameters"]);
1569 # return task to caller
1582 if ($NewValue !== NULL)
1584 $this->Settings[
"TaskExecutionEnabled"] = $NewValue ? 1 : 0;
1585 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings"
1586 .
" SET TaskExecutionEnabled = "
1587 .$this->Settings[
"TaskExecutionEnabled"]);
1589 return $this->Settings[
"TaskExecutionEnabled"];
1599 if (func_num_args() && ($NewValue >= 1))
1601 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings"
1602 .
" SET MaxTasksRunning = ".intval($NewValue));
1603 $this->Settings[
"MaxTasksRunning"] = intval($NewValue);
1605 return $this->Settings[
"MaxTasksRunning"];
1617 if (func_num_args() && !ini_get(
"safe_mode"))
1619 if ($NewValue != $this->Settings[
"MaxExecTime"])
1621 $this->Settings[
"MaxExecTime"] = max($NewValue, 5);
1622 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings"
1623 .
" SET MaxExecTime = '"
1624 .intval($this->Settings[
"MaxExecTime"]).
"'");
1626 ini_set(
"max_execution_time", $this->Settings[
"MaxExecTime"]);
1627 set_time_limit($this->Settings[
"MaxExecTime"]);
1629 return ini_get(
"max_execution_time");
1634 # ---- Backward Compatibility --------------------------------------------
1642 return $this->FindFile(
1643 $this->IncludeDirList, $BaseName, array(
"tpl",
"html"));
1649 # ---- PRIVATE INTERFACE -------------------------------------------------
1651 private $ActiveUI =
"default";
1652 private $BrowserDetectFunc;
1654 private $DefaultPage =
"Home";
1655 private $EnvIncludes = array();
1656 private $ExecutionStartTime;
1657 private $FoundUIFiles = array();
1658 private $AdditionalRequiredUIFiles = array();
1659 private $HtmlCharset =
"UTF-8";
1660 private $JumpToPage = NULL;
1662 private $MaxRunningTasksToTrack = 250;
1663 private $PostProcessingFuncs = array();
1664 private $RunningInBackground = FALSE;
1665 private $RunningTask;
1667 private $SuppressHTML = FALSE;
1668 private $SaveTemplateLocationCache = FALSE;
1669 private $UnbufferedCallbacks = array();
1671 private static $AppName =
"ScoutAF";
1672 private static $ObjectDirectories = array();
1673 private static $SessionLifetime = 1440;
1679 private $NoTSR = FALSE;
1681 private $PeriodicEvents = array(
1682 "EVENT_HOURLY" => self::EVENTTYPE_DEFAULT,
1683 "EVENT_DAILY" => self::EVENTTYPE_DEFAULT,
1684 "EVENT_WEEKLY" => self::EVENTTYPE_DEFAULT,
1685 "EVENT_MONTHLY" => self::EVENTTYPE_DEFAULT,
1686 "EVENT_PERIODIC" => self::EVENTTYPE_NAMED,
1688 private $UIEvents = array(
1689 "EVENT_PAGE_LOAD" => self::EVENTTYPE_DEFAULT,
1690 "EVENT_PHP_FILE_LOAD" => self::EVENTTYPE_CHAIN,
1691 "EVENT_PHP_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
1692 "EVENT_HTML_FILE_LOAD" => self::EVENTTYPE_CHAIN,
1693 "EVENT_HTML_FILE_LOAD_COMPLETE" => self::EVENTTYPE_DEFAULT,
1699 private function LoadSettings()
1701 # read settings in from database
1702 $this->DB->Query(
"SELECT * FROM ApplicationFrameworkSettings");
1703 $this->Settings = $this->DB->FetchRow();
1705 # if settings were not previously initialized
1706 if (!$this->Settings)
1708 # initialize settings in database
1709 $this->DB->Query(
"INSERT INTO ApplicationFrameworkSettings"
1710 .
" (LastTaskRunAt) VALUES ('2000-01-02 03:04:05')");
1712 # read new settings in from database
1713 $this->DB->Query(
"SELECT * FROM ApplicationFrameworkSettings");
1714 $this->Settings = $this->DB->FetchRow();
1717 # if template location cache has been saved to database
1718 if (isset($this->Settings[
"TemplateLocationCache"]))
1720 # unserialize cache values into array and use if valid
1721 $Cache = unserialize($this->Settings[
"TemplateLocationCache"]);
1722 $this->Settings[
"TemplateLocationCache"] =
1723 count($Cache) ? $Cache : array();
1727 # start with empty cache
1728 $this->Settings[
"TemplateLocationCache"] = array();
1748 private function FindFile($DirectoryList, $BaseName,
1749 $PossibleSuffixes = NULL, $PossiblePrefixes = NULL)
1751 # generate template cache index for this page
1752 $CacheIndex = md5(serialize($DirectoryList))
1753 .
":".$this->ActiveUI.
":".$BaseName;
1755 # if we have cached location and cache expiration time has not elapsed
1756 if (($this->Settings[
"TemplateLocationCacheInterval"] > 0)
1757 && count($this->Settings[
"TemplateLocationCache"])
1758 && array_key_exists($CacheIndex,
1759 $this->Settings[
"TemplateLocationCache"])
1760 && (time() < strtotime(
1761 $this->Settings[
"TemplateLocationCacheExpiration"])))
1763 # use template location from cache
1764 $FoundFileName = $this->Settings[
1765 "TemplateLocationCache"][$CacheIndex];
1769 # if suffixes specified and base name does not include suffix
1770 if (count($PossibleSuffixes)
1771 && !preg_match(
"/\.[a-zA-Z0-9]+$/", $BaseName))
1773 # add versions of file names with suffixes to file name list
1774 $FileNames = array();
1775 foreach ($PossibleSuffixes as $Suffix)
1777 $FileNames[] = $BaseName.
".".$Suffix;
1782 # use base name as file name
1783 $FileNames = array($BaseName);
1786 # if prefixes specified
1787 if (count($PossiblePrefixes))
1789 # add versions of file names with prefixes to file name list
1790 $NewFileNames = array();
1791 foreach ($FileNames as $FileName)
1793 foreach ($PossiblePrefixes as $Prefix)
1795 $NewFileNames[] = $Prefix.$FileName;
1798 $FileNames = $NewFileNames;
1801 # for each possible location
1802 $FoundFileName = NULL;
1803 foreach ($DirectoryList as $Dir)
1805 # substitute active UI name into path
1806 $Dir = str_replace(
"%ACTIVEUI%", $this->ActiveUI, $Dir);
1808 # for each possible file name
1809 foreach ($FileNames as $File)
1811 # if template is found at location
1812 if (file_exists($Dir.$File))
1814 # save full template file name and stop looking
1815 $FoundFileName = $Dir.$File;
1821 # save location in cache
1822 $this->Settings[
"TemplateLocationCache"][$CacheIndex]
1825 # set flag indicating that cache should be saved
1826 $this->SaveTemplateLocationCache = TRUE;
1829 # return full template file name to caller
1830 return $FoundFileName;
1839 private function GetRequiredFilesNotYetLoaded($PageContentFile)
1841 # start out assuming no files required
1842 $RequiredFiles = array();
1844 # if page content file supplied
1845 if ($PageContentFile)
1847 # if file containing list of required files is available
1848 $Path = dirname($PageContentFile);
1849 $RequireListFile = $Path.
"/REQUIRES";
1850 if (file_exists($RequireListFile))
1852 # read in list of required files
1853 $RequestedFiles = file($RequireListFile);
1855 # for each line in required file list
1856 foreach ($RequestedFiles as $Line)
1858 # if line is not a comment
1859 $Line = trim($Line);
1860 if (!preg_match(
"/^#/", $Line))
1862 # if file has not already been loaded
1863 if (!in_array($Line, $this->FoundUIFiles))
1865 # add to list of required files
1866 $RequiredFiles[] = $Line;
1873 # add in additional required files if any
1874 if (count($this->AdditionalRequiredUIFiles))
1876 # make sure there are no duplicates
1877 $AdditionalRequiredUIFiles = array_unique(
1878 $this->AdditionalRequiredUIFiles);
1880 $RequiredFiles = array_merge(
1881 $RequiredFiles, $AdditionalRequiredUIFiles);
1884 # return list of required files to caller
1885 return $RequiredFiles;
1891 private function SetUpObjectAutoloading()
1893 function __autoload($ClassName)
1895 ApplicationFramework::AutoloadObjects($ClassName);
1904 static function AutoloadObjects($ClassName)
1907 foreach (self::$ObjectDirectories as $Location => $Info)
1909 if (is_dir($Location))
1911 $NewClassName = ($Info[
"ClassPattern"] && $Info[
"ClassReplacement"])
1912 ? preg_replace($Info[
"ClassPattern"],
1913 $Info[
"ClassReplacement"], $ClassName)
1915 if (!isset($FileLists[$Location]))
1917 $FileLists[$Location] = self::ReadDirectoryTree(
1918 $Location,
'/^.+\.php$/i');
1920 $FileNames = $FileLists[$Location];
1921 $TargetName = strtolower($Info[
"Prefix"].$NewClassName.
".php");
1922 foreach ($FileNames as $FileName)
1924 if (strtolower($FileName) == $TargetName)
1926 require_once($Location.$FileName);
1941 private static function ReadDirectoryTree($Directory, $Pattern)
1943 $CurrentDir = getcwd();
1945 $DirIter =
new RecursiveDirectoryIterator(
".");
1946 $IterIter =
new RecursiveIteratorIterator($DirIter);
1947 $RegexResults =
new RegexIterator($IterIter, $Pattern,
1948 RecursiveRegexIterator::GET_MATCH);
1949 $FileList = array();
1950 foreach ($RegexResults as $Result)
1952 $FileList[] = substr($Result[0], 2);
1963 private function LoadUIFunctions()
1966 "local/interface/%ACTIVEUI%/include",
1967 "interface/%ACTIVEUI%/include",
1968 "local/interface/default/include",
1969 "interface/default/include",
1971 foreach ($Dirs as $Dir)
1973 $Dir = str_replace(
"%ACTIVEUI%", $this->ActiveUI, $Dir);
1976 $FileNames = scandir($Dir);
1977 foreach ($FileNames as $FileName)
1979 if (preg_match(
"/^F-([A-Za-z_]+)\.php/", $FileName, $Matches)
1980 || preg_match(
"/^F-([A-Za-z_]+)\.html/", $FileName, $Matches))
1982 if (!function_exists($Matches[1]))
1984 include_once($Dir.
"/".$FileName);
1997 private function ProcessPeriodicEvent($EventName, $Callback)
1999 # retrieve last execution time for event if available
2000 $Signature = self::GetCallbackSignature($Callback);
2001 $LastRun = $this->DB->Query(
"SELECT LastRunAt FROM PeriodicEvents"
2002 .
" WHERE Signature = '".addslashes($Signature).
"'",
"LastRunAt");
2004 # determine whether enough time has passed for event to execute
2005 $EventPeriods = array(
2006 "EVENT_HOURLY" => 60*60,
2007 "EVENT_DAILY" => 60*60*24,
2008 "EVENT_WEEKLY" => 60*60*24*7,
2009 "EVENT_MONTHLY" => 60*60*24*30,
2010 "EVENT_PERIODIC" => 0,
2012 $ShouldExecute = (($LastRun === NULL)
2013 || (time() > (strtotime($LastRun) + $EventPeriods[$EventName])))
2016 # if event should run
2019 # add event to task queue
2020 $WrapperCallback = array(
"ApplicationFramework",
"PeriodicEventWrapper");
2021 $WrapperParameters = array(
2022 $EventName, $Callback, array(
"LastRunAt" => $LastRun));
2034 private static function PeriodicEventWrapper($EventName, $Callback, $Parameters)
2037 if (!isset($DB)) { $DB =
new Database(); }
2040 $ReturnVal = call_user_func_array($Callback, $Parameters);
2042 # if event is already in database
2043 $Signature = self::GetCallbackSignature($Callback);
2044 if ($DB->Query(
"SELECT COUNT(*) AS EventCount FROM PeriodicEvents"
2045 .
" WHERE Signature = '".addslashes($Signature).
"'",
"EventCount"))
2047 # update last run time for event
2048 $DB->Query(
"UPDATE PeriodicEvents SET LastRunAt = "
2049 .(($EventName ==
"EVENT_PERIODIC")
2050 ?
"'".date(
"Y-m-d H:i:s", time() + ($ReturnVal * 60)).
"'"
2052 .
" WHERE Signature = '".addslashes($Signature).
"'");
2056 # add last run time for event to database
2057 $DB->Query(
"INSERT INTO PeriodicEvents (Signature, LastRunAt) VALUES "
2058 .
"('".addslashes($Signature).
"', "
2059 .(($EventName ==
"EVENT_PERIODIC")
2060 ?
"'".date(
"Y-m-d H:i:s", time() + ($ReturnVal * 60)).
"'"
2070 private static function GetCallbackSignature($Callback)
2072 return !is_array($Callback) ? $Callback
2073 : (is_object($Callback[0]) ? md5(serialize($Callback[0])) : $Callback[0])
2081 private function PrepForTSR()
2083 # if HTML has been output and it's time to launch another task
2084 # (only TSR if HTML has been output because otherwise browsers
2085 # may misbehave after connection is closed)
2086 if (($this->JumpToPage || !$this->SuppressHTML)
2087 && (time() > (strtotime($this->Settings[
"LastTaskRunAt"])
2088 + (ini_get(
"max_execution_time")
2089 / $this->Settings[
"MaxTasksRunning"]) + 5))
2091 && $this->Settings[
"TaskExecutionEnabled"])
2093 # begin buffering output for TSR
2096 # let caller know it is time to launch another task
2101 # let caller know it is not time to launch another task
2110 private function LaunchTSR()
2112 # set headers to close out connection to browser
2115 ignore_user_abort(TRUE);
2116 header(
"Connection: close");
2117 header(
"Content-Length: ".ob_get_length());
2120 # output buffered content
2124 # write out any outstanding data and end HTTP session
2125 session_write_close();
2127 # set flag indicating that we are now running in background
2128 $this->RunningInBackground = TRUE;
2130 # if there is still a task in the queue
2133 # turn on output buffering to (hopefully) record any crash output
2136 # lock tables and grab last task run time to double check
2137 $this->DB->Query(
"LOCK TABLES ApplicationFrameworkSettings WRITE");
2138 $this->LoadSettings();
2140 # if still time to launch another task
2141 if (time() > (strtotime($this->Settings[
"LastTaskRunAt"])
2142 + (ini_get(
"max_execution_time")
2143 / $this->Settings[
"MaxTasksRunning"]) + 5))
2145 # update the "last run" time and release tables
2146 $this->DB->Query(
"UPDATE ApplicationFrameworkSettings"
2147 .
" SET LastTaskRunAt = '".date(
"Y-m-d H:i:s").
"'");
2148 $this->DB->Query(
"UNLOCK TABLES");
2150 # run tasks while there is a task in the queue and enough time left
2154 $this->RunNextTask();
2162 $this->DB->Query(
"UNLOCK TABLES");
2174 private function GetTaskList($DBQuery, $Count, $Offset)
2176 $this->DB->Query($DBQuery.
" LIMIT ".intval($Offset).
",".intval($Count));
2178 while ($Row = $this->DB->FetchRow())
2180 $Tasks[$Row[
"TaskId"]] = $Row;
2181 if ($Row[
"Callback"] ==
2182 serialize(array(
"ApplicationFramework",
"PeriodicEventWrapper")))
2184 $WrappedCallback = unserialize($Row[
"Parameters"]);
2185 $Tasks[$Row[
"TaskId"]][
"Callback"] = $WrappedCallback[1];
2186 $Tasks[$Row[
"TaskId"]][
"Parameters"] = NULL;
2190 $Tasks[$Row[
"TaskId"]][
"Callback"] = unserialize($Row[
"Callback"]);
2191 $Tasks[$Row[
"TaskId"]][
"Parameters"] = unserialize($Row[
"Parameters"]);
2200 private function RunNextTask()
2202 # look for task at head of queue
2203 $this->DB->Query(
"SELECT * FROM TaskQueue ORDER BY Priority, TaskId LIMIT 1");
2204 $Task = $this->DB->FetchRow();
2206 # if there was a task available
2209 # move task from queue to running tasks list
2210 $this->DB->Query(
"INSERT INTO RunningTasks "
2211 .
"(TaskId,Callback,Parameters,Priority,Description) "
2212 .
"SELECT * FROM TaskQueue WHERE TaskId = "
2213 .intval($Task[
"TaskId"]));
2214 $this->DB->Query(
"DELETE FROM TaskQueue WHERE TaskId = "
2215 .intval($Task[
"TaskId"]));
2217 # unpack stored task info
2218 $Callback = unserialize($Task[
"Callback"]);
2219 $Parameters = unserialize($Task[
"Parameters"]);
2221 # attempt to load task callback if not already available
2225 $this->RunningTask = $Task;
2228 call_user_func_array($Callback, $Parameters);
2232 call_user_func($Callback);
2234 unset($this->RunningTask);
2236 # remove task from running tasks list
2237 $this->DB->Query(
"DELETE FROM RunningTasks"
2238 .
" WHERE TaskId = ".intval($Task[
"TaskId"]));
2240 # prune running tasks list if necessary
2241 $RunningTasksCount = $this->DB->Query(
2242 "SELECT COUNT(*) AS TaskCount FROM RunningTasks",
"TaskCount");
2243 if ($RunningTasksCount > $this->MaxRunningTasksToTrack)
2245 $this->DB->Query(
"DELETE FROM RunningTasks ORDER BY StartedAt"
2246 .
" LIMIT ".($RunningTasksCount - $this->MaxRunningTasksToTrack));
2258 if (isset($this->RunningTask))
2260 if (function_exists(
"error_get_last"))
2262 $CrashInfo[
"LastError"] = error_get_last();
2264 if (ob_get_length() !== FALSE)
2266 $CrashInfo[
"OutputBuffer"] = ob_get_contents();
2268 if (isset($CrashInfo))
2271 $DB->Query(
"UPDATE RunningTasks SET CrashInfo = '"
2272 .addslashes(serialize($CrashInfo))
2273 .
"' WHERE TaskId = ".intval($this->RunningTask[
"TaskId"]));
2280 if (ob_get_length() !== FALSE)
2283 <table width=
"100%" cellpadding=
"5" style=
"border: 2px solid #666666; background: #FFCCCC; font-family: Courier New, Courier, monospace; margin-top: 10px; font-weight: bold;"><tr><td>
2284 <div style=
"font-size: 200%;">CRASH OUTPUT</div><?
PHP
2286 ?></td></tr></table><?
PHP
2306 private function AddToDirList($DirList, $Dir, $SearchLast, $SkipSlashCheck)
2308 # convert incoming directory to array of directories (if needed)
2309 $Dirs = is_array($Dir) ? $Dir : array($Dir);
2311 # reverse array so directories are searched in specified order
2312 $Dirs = array_reverse($Dirs);
2314 # for each directory
2315 foreach ($Dirs as $Location)
2317 # make sure directory includes trailing slash
2318 if (!$SkipSlashCheck)
2320 $Location = $Location
2321 .((substr($Location, -1) !=
"/") ?
"/" :
"");
2324 # remove directory from list if already present
2325 if (in_array($Location, $DirList))
2327 $DirList = array_diff(
2328 $DirList, array($Location));
2331 # add directory to list of directories
2334 array_push($DirList, $Location);
2338 array_unshift($DirList, $Location);
2342 # return updated directory list to caller
2347 private $InterfaceDirList = array(
2348 "local/interface/%ACTIVEUI%/",
2349 "interface/%ACTIVEUI%/",
2350 "local/interface/default/",
2351 "interface/default/",
2357 private $IncludeDirList = array(
2358 "local/interface/%ACTIVEUI%/include/",
2359 "interface/%ACTIVEUI%/include/",
2360 "local/interface/default/include/",
2361 "interface/default/include/",
2364 private $ImageDirList = array(
2365 "local/interface/%ACTIVEUI%/images/",
2366 "interface/%ACTIVEUI%/images/",
2367 "local/interface/default/images/",
2368 "interface/default/images/",
2371 private $FunctionDirList = array(
2372 "local/interface/%ACTIVEUI%/include/",
2373 "interface/%ACTIVEUI%/include/",
2374 "local/interface/default/include/",
2375 "interface/default/include/",
2380 const NOVALUE =
".-+-.NO VALUE PASSED IN FOR ARGUMENT.-+-.";