diff --git a/xtrn/DDUploadProcessor/DDUP.cfg b/xtrn/DDUploadProcessor/DDUP.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..f027306af23dfa843bdfacd4958eb260b11b4a19
--- /dev/null
+++ b/xtrn/DDUploadProcessor/DDUP.cfg
@@ -0,0 +1,24 @@
+; This is the general configuration file for Digital Distortion
+; Upload Processor.  This contains options for the processor
+; itself.
+
+; In the scan command, the following specifiers can be used:
+; Specifier                 Replaced with
+; %FILESPEC%                The name of the file/directory to scan
+;
+; The virus scanner should be able to take just a directory name (to
+; scan that entire directory) so that archive files can be unpacked
+; into a directory and scanned.
+;
+; This scan command is for AVG 9; you may need to change the path for your computer.
+scanCmd="C:\Program Files\AVG\AVG9\avgscanx.exe" /SCAN=%FILESPEC%
+
+; The following option sets whether or not to pause for the user to
+; enter a key after a file is scanned.  Valid values are yes and
+; no.
+pauseAtEnd=no
+
+; If the following setting is set to yes, then all uploads by
+; sysops will be automatically approved.  Valid values are yes
+; and no.
+skipScanIfSysop=no
\ No newline at end of file
diff --git a/xtrn/DDUploadProcessor/DDUP.js b/xtrn/DDUploadProcessor/DDUP.js
new file mode 100644
index 0000000000000000000000000000000000000000..96d1fd63371e2b1a2f6615820536441e27f401d2
--- /dev/null
+++ b/xtrn/DDUploadProcessor/DDUP.js
@@ -0,0 +1,1015 @@
+/* Name: Digital Distortion Upload Processor
+ *
+ * Description: This is a script for Synchronet that scans
+ * uploaded files with a virus scanner.  Compressed archives are
+ * unpacked so that the files inside can be scanned by the virus
+ * scanner.
+ *
+ * Author: Eric Oulashin (AKA Nightfox)
+ * BBS: Digital Distortion
+ * BBS address: digdist.bbsindex.com
+ *
+ * Date       User              Description
+ * 2009-12-25-
+ * 2009-12-28 Eric Oulashin     Initial development
+ * 2009-12-29 Eric Oulashin     Version 1.00
+ *                              Initial public release
+ */
+
+/* Command-line arguments:
+ 1 (argv[0]): The name of the file to scan
+*/
+
+load("sbbsdefs.js");
+
+// Determine the script's execution directory.
+// This code is a trick that was created by Deuce, suggested by Rob
+// Swindell as a way to detect which directory the script was executed
+// in.  I've shortened the code a little.
+// Note: gStartupPath will include the trailing slash.
+var gStartupPath = '.';
+try { throw dig.dist(dist); } catch(e) { gStartupPath = e.fileName; }
+gStartupPath = backslash(gStartupPath.replace(/[\/\\][^\/\\]*$/,''));
+
+load(gStartupPath + "DDUP_Cleanup.js");
+
+// Version information
+var gDDUPVersion = "1.00";
+var gDDUPVerDate = "2009-12-29";
+
+
+// If the filename was specified on the command line, then use that
+// for the filename.  Otherwise, read the name of the file to view
+// from DDArcViewerFilename.txt in the node directory.
+var gFileToScan = "";
+if (argv.length > 0)
+{
+   if (typeof(argv[0]) == "string")
+   {
+      // Make sure the arguments are correct (in case they have spaces),
+      // then use the first one.
+      var fixedArgs = fixArgs(argv);
+      if ((typeof(fixedArgs[0]) == "string") && (fixedArgs[0].length > 0))
+         gFileToScan = fixedArgs[0];
+      else
+      {
+         console.print("nyhError: ncBlank filename argument given.\r\np");
+         exit(-2);
+      }
+   }
+   else
+   {
+      console.print("nyhError: ncUnknown command-line argument specified.\r\np");
+      exit(-1);
+   }
+}
+else
+{
+   // Read the filename from DDArcViewerFilename.txt in the node directory.
+   // This is a workaround for file/directory names with spaces in
+   // them, which would get separated into separate command-line
+   // arguments for JavaScript scripts.
+   var filenameFileFilename = system.node_dir + "DDArcViewerFilename.txt";
+   var filenameFile = new File(filenameFileFilename);
+   if (filenameFile.open("r"))
+   {
+      if (!filenameFile.eof)
+         gFileToScan = filenameFile.readln(2048);
+      filenameFile.close();
+   }
+}
+
+// Make sure the slashes in the filename are correct for the platform.
+if (gFileToScan.length > 0)
+   gFileToScan = fixPathSlashes(gFileToScan);
+
+// A filename must be specified as the first argument, so give an error and return
+// if not.
+if (gFileToScan.length == 0)
+{
+   console.print("nyhError: ncNo filename specified to process.\r\np");
+   exit(1);
+}
+
+// Create the global configuration objects.
+var gGenCfg = new Object();
+gGenCfg.scanCmd = "";
+gGenCfg.skipScanIfSysop = false;
+gGenCfg.pauseAtEnd = false;
+var gFileTypeCfg = new Object();
+
+// Read the configuration files to populate the global configuration object.
+var configFileRead = ReadConfigFile(gStartupPath);
+// If the configuration files weren't read, then output an error and exit.
+if (!configFileRead)
+{
+   console.print("nyhError: ncUpload processor is unable to read its\r\n");
+   console.print("configuration files.\r\np");
+   exit(2);
+}
+// Exit if there is no scan command.
+if (gGenCfg.scanCmd.length == 0)
+{
+   console.print("nyhWarning: ncNo scan command configured for the upload processor.\r\n");
+   exit(0);
+}
+
+// Global variables
+// Strings for the OK and failure symbols
+var gOKStr = "nkh[ng�kh]n";
+var gOKStrWithNewline = gOKStr + "\r\n";
+var gFailStr = "nkh[rXk]n";
+var gFailStrWithNewline = gFailStr + "\r\n";
+// Stuff for the printf formatting string for the status messages
+var gStatusTextLen = 79 - console.strlen(gOKStr); // gOKStr and gFailStr should have the same length
+var gStatusPrintfStr = "n%s%-" + gStatusTextLen + "sn"; // For a color and the status text
+
+// Now, scan the file and return the appropriate return code.
+exit(main());
+// End of script execution.
+
+
+// This is the "main" function that contains the main code
+// for the script.
+//
+// Return value: An integer to return upon script exit.
+function main()
+{
+   // Output the program name & version information
+   console.print("n\r\nchDncigital hDncistortion hUncpload hPncrocessor whvng" +
+                 gDDUPVersion);
+   // Originally I had this script output the version date, but now I'm not sure
+   // if I want to do that..
+   //console.print(" wh(b" + gDDUPVerDate + "w)");
+   console.print("n");
+   console.crlf();
+
+   // Process the file
+   var exitCode = processFile(gFileToScan);
+   // Depending on the exit code, display a success or failure message.
+   console.crlf();
+   if (exitCode == 0)
+      console.print(gOKStr + " nbhScan successful - The file passed.\r\n");
+   else
+      console.print(gFailStr + " nyhScan failed!\r\n");
+
+   // If the option to pause at the end is enabled, then prompt the user for
+   // a keypress.
+   if (gGenCfg.pauseAtEnd)
+   {
+      console.print("nwhPress any key to continue:n");
+      console.getkey(K_NOECHO);
+   }
+
+   return exitCode;
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////
+// Object stuff
+
+// Constructor for the ScannableFile object, which contains information
+// about a viewable file.
+//
+// Parameters:
+//  pExtension: The filename extension
+//  pViewCmd: The OS command to view it
+//
+// The ScannableFile object contains the following properties:
+//  extension: The filename extension
+//  pExtractCmd: The OS command to extract it (if applicable)
+//  pScanOption: A string containing a scan option.  The following are valid:
+//               "scan": Always scan the file using the scan commdn
+//               "always pass": Don't scan the file, and assume it's good
+//               "always fail": Don't scan the file, and assume it's bad
+function ScannableFile(pExtension, pExtractCmd, pScanOption)
+{ 
+   this.extension = "";      // The archive filename extension
+   this.extractCmd = "";     // The command to extract the archive (if applicable)
+   this.scanOption = "scan"; // The scan option ("scan", "always pass", "always fail")
+
+   // If the parameters are valid, then use them to set the object properties.
+   if ((pExtension != null) && (pExtension != undefined) && (typeof(pExtension) == "string"))
+      this.extension = pExtension;
+   if ((pExtractCmd != null) && (pExtractCmd != undefined) && (typeof(pExtractCmd) == "string"))
+      this.extractCmd = pExtractCmd;
+   if ((pScanOption != null) && (pScanOption != undefined) && (typeof(pScanOption) == "string"))
+      this.scanOption = pScanOption;
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////////
+// Functions
+
+// This function fixes an array of command-line arguments so that
+// arguments with spaces in them are a single argument.  This function
+// was written by Tracker1 of The Roughnecks BBS - He posted the code
+// in the DOVE-Net Synchronet Discussion sub-board on December 20, 2009.
+function fixArgs(input)
+{
+   var patt1 = /\"[^\"]*\"|\S+/g;
+   var patt2 = /^\"?([^\"]*)\"?$/;
+   return input.join(' ').match(patt1).map(function(item)
+   {
+     return item.replace(patt2, "$1")
+   });
+}
+
+// Scans a file.
+//
+// Parameters:
+//  pFilename: The name of the file to scan
+//
+// Return value: A return code from scanning the file.  0 means success;
+//               non-zero means failure.
+function processFile(pFilename)
+{
+   // Display the program header stuff - The name of the file being scanned
+   // and the status header line
+   var justFilename = getFilenameFromPath(pFilename);
+   console.print("nwhScanning b" + justFilename.substr(0, 70));
+   console.print("n\r\nb7                             File Scan Status                                  n\r\n");
+
+   // If the skipScanIfSysop option is enabled and the user is a sysop,
+   // then assume the file is good.
+   if (gGenCfg.skipScanIfSysop && user.compare_ars("SYSOP"))
+   {
+      printf(gStatusPrintfStr, "gh", "Auto-approving the file (you're a sysop)");
+      console.print(gOKStrWithNewline);
+      return 0;
+   }
+
+   var retval = 0;
+
+   // Look for the file extension in gFileTypeCfg to get the file scan settings.
+   // If the file extension is not there, then go ahead and scan it (to be on the
+   // safe side).
+   var filenameExtension = getFilenameExtension(pFilename);
+   if (typeof(gFileTypeCfg[filenameExtension]) != "undefined")
+   {
+      if (gFileTypeCfg[filenameExtension].scanOption == "scan")
+      {
+         // - If the file has an extract command, then:
+         //   Extract the file to a temporary directory in the node dir
+         //   For each file in the directory:
+         //     If it's a subdir
+         //        Recurse into it
+         //     else
+         //        Scan it for viruses
+         //        If non-zero retval
+         //           Return with error code
+         var filespec = pFilename;
+         if (gFileTypeCfg[filenameExtension].extractCmd.length > 0)
+         {
+            // Create the base work directory for this script in the node dir.
+            // And just in case that dir already exists, remove it before
+            // creating it.
+            var baseWorkDir = system.node_dir + "DDUploadProcessor_Temp";
+            deltree(baseWorkDir + "/");
+            if (!mkdir(baseWorkDir))
+            {
+               console.print("nyhWarning: nwh Unable to create the work dir.n\r\n");
+               retval = -1;
+            }
+            
+            // If all is okay, then create the directory in the temporary work dir.
+            var workDir = baseWorkDir + "/" + justFilename + "_temp";
+            if (retval == 0)
+            {
+               deltree(workDir + "/");
+               if (!mkdir(workDir))
+               {
+                  console.print("nyhWarning: nwh Unable to create a dir in the temporary work dir.n\r\n");
+                  retval = -1;
+               }
+            }
+
+            // If all is okay, we can now process the file.
+            if (retval == 0)
+            {
+               // Extract the file to the work directory
+               printf(gStatusPrintfStr, "mh", "Extracting the file...");
+               var errorStr = extractFileToDir(pFilename, workDir);
+               if (errorStr.length == 0)
+               {
+                  console.print(gOKStrWithNewline);
+                  // Scan the files in the work directory.
+                  printf(gStatusPrintfStr, "r", "Scanning files inside the archive for viruses...");
+                  var retObj = scanFilesInDir(workDir);
+                  retval = retObj.returnCode;
+                  if (retObj.returnCode == 0)
+                     console.print(gOKStrWithNewline);
+                  else
+                  {
+                     console.print(gFailStrWithNewline);
+                     console.print("nyhVirus scan failed.  Scan output:n\r\n");
+                     for (var index = 0; index < retObj.cmdOutput.length; ++index)
+                     {
+                        console.print(retObj.cmdOutput[index]);
+                        console.crlf();
+                     }
+                  }
+               }
+               else
+               {
+                  console.print(gFailStrWithNewline);
+                  // Scan the files in the work directory.
+                  console.print("nyhWarning: nwh Unable to extract to work dir.n\r\n");
+                  retval = -2;
+               }
+            }
+            // Remove the work directory.
+            deltree(baseWorkDir + "/");
+         }
+         else
+         {
+            // The file has no extract command, so just scan it.
+            printf(gStatusPrintfStr, "bh", "Scanning...");
+            var scanCmd = gGenCfg.scanCmd.replace("%FILESPEC%", "\"" + fixPathSlashes(pFilename) + "\"");
+            // Run the scan command and capture its output, in case the scan fails.
+            var retObj = runExternalCmdWithOutput(scanCmd);
+            retval = retObj.returnCode;
+            if (retObj.returnCode == 0)
+               console.print(gOKStrWithNewline);
+            else
+            {
+               console.print(gFailStrWithNewline);
+               console.print("nyhVirus scan failed.  Scan output:n\r\n");
+               for (var index = 0; index < retObj.cmdOutput.length; ++index)
+               {
+                  console.print(retObj.cmdOutput[index]);
+                  console.crlf();
+               }
+            }
+         }
+      }
+      else if (gFileTypeCfg[filenameExtension].scanOption == "always fail")
+         exitCode = 10;
+   }
+   else
+   {
+      // There's nothing configured for the file's extension, so just scan it.
+      printf(gStatusPrintfStr, "r", "Scanning...");
+      var scanCmd = gGenCfg.scanCmd.replace("%FILESPEC%", "\"" + fixPathSlashes(pFilename) + "\"");
+      var retObj = runExternalCmdWithOutput(scanCmd);
+      retval = retObj.returnCode;
+      if (retObj.returnCode == 0)
+         console.print(gOKStrWithNewline);
+      else
+      {
+         console.print(gFailStrWithNewline);
+         console.print("nyhVirus scan failed.  Scan output:n\r\n");
+         for (var index = 0; index < retObj.cmdOutput.length; ++index)
+         {
+            console.print(retObj.cmdOutput[index]);
+            console.crlf();
+         }
+      }
+   }
+
+   return retval;
+}
+
+// Recursively scans the files in a directory using the scan command in
+// gGencfg.
+//
+// Parameters:
+//  pDir: The directory to scan
+//
+// Return value: 0 on success, or non-zero on error.
+// Return value: An object containing the following properties:
+//               returnCode: The return code of the last scan command called in the
+//                           OS (0 is good, non-zero is failure).
+//               cmdOutput: An array of strings containing the output from the last
+//                          file scan.
+function scanFilesInDir(pDir)
+{
+   // If pDir is unspecified, then just return.
+   if (typeof(pDir) != "string")
+   {
+      var retObj = new Object();
+      retObj.cmdOutput = new Array();
+      retObj.returnCode = -1;
+      return retObj;
+   }
+   if (pDir.length == 0)
+   {
+      var retObj = new Object();
+      retObj.cmdOutput = new Array();
+      retObj.returnCode = -2;
+      return retObj;
+   }
+   // Also, just return if gGenCfg.scanCmd is blank.
+   if (gGenCfg.scanCmd.length == 0)
+   {
+      var retObj = new Object();
+      retObj.cmdOutput = new Array();
+      retObj.returnCode = -3;
+      return retObj;
+   }
+
+   // If the filename has a trailing slash, remove it.
+   if ((/\/$/.test(pDir)) || (/\\$/.test(pDir)))
+      pDir = pDir.substr(0, pDir.length-1);
+
+   var retObj = null;  // Will be used to capture the return from the scan commands
+
+   // If the virus scan command contains %FILESPEC%, then
+   // replace %FILESPEC% with pDir and run the scan command.
+   if (gGenCfg.scanCmd.indexOf("%FILESPEC%") > -1)
+   {
+      var scanCmd = gGenCfg.scanCmd.replace("%FILESPEC%", "\"" + fixPathSlashes(pDir) + "\"");
+      retObj = runExternalCmdWithOutput(scanCmd);
+
+      // This is old code, for scanning each file individually (slow):
+      /*
+      // Get a list of the files, and scan them.
+      var files = directory(pDir + "/*");
+      if (files.length > 0)
+      {
+         var scanCmd = null; // Will be used for the scan commands (string)
+         var counter = 0;    // Loop variable
+         for (var i in files)
+         {
+            // If the file is a directory, then recurse into it.  Otherwise,
+            // scan the file using the configured scan command.
+            if (file_isdir(files[i]))
+               retObj = scanFilesInDir(files[i]);
+            else
+            {
+               scanCmd = gGenCfg.scanCmd.replace("%FILESPEC%", "\"" + fixPathSlashes(files[i]) + "\"");
+               // Run the scan command and capture its output, in case the scan fails.
+               retObj = runExternalCmdWithOutput(scanCmd);
+            }
+   
+            // If there's a problem, then stop going through the list of files.
+            if (retObj.returnCode != 0)
+               break;
+         }
+      }
+      else
+      {
+         // There are no files.  So create retObj with default settings
+         // for a good result.
+         retObj = new Object();
+         retObj.returnCode = 0;
+         retObj.cmdOutput = new Array();
+      }
+      */
+   }
+   else
+   {
+      // gGenCfg.scanCmd doesn't contain %FILESPEC%, so set up
+      // retObj with a non-zero return code (for failure)
+      retObj = new Object();
+      retObj.returnCode = -4;
+      retObj.cmdOutput = new Array();
+      retObj.cmdOutput.push("The virus scanner is not set up correctly.");
+   }
+
+   return retObj;
+}
+
+// Reads the configuration file and returns an object containing the
+// configuration settings.
+//
+// Parameters:
+//  pCfgFilePath: The path from which to load the configuration file.
+//
+// Return value: Boolean - Whether or not the configuration was read.
+function ReadConfigFile(pCfgFilePath)
+{
+   // Read the file type settings.
+   var fileTypeSettingsRead = false;
+   var fileTypeCfgFile = new File(pCfgFilePath + "DDUPFileTypes.cfg");
+   if (fileTypeCfgFile.open("r"))
+   {
+      if (fileTypeCfgFile.length > 0)
+      {
+         fileTypeSettingsRead = true;
+         // Read each line from the config file and set the
+         // various options.
+         var pos = 0;               // Index of = in the file lines
+         var fileLine = "";
+         var filenameExt = "";      // Archive filename extension
+         var option = "";           // Configuration option
+         var optionValue = "";      // Configuration option value
+         var optionValueUpper;      // Upper-cased configuration option value
+         var scannableFile = null;  // Will be used to create & store scannable file options
+         while (!fileTypeCfgFile.eof)
+         {
+            // Read the line from the config file, look for a =, and
+            // if found, read the option & value and set them
+            // in cfgObj.
+            fileLine = fileTypeCfgFile.readln(1024);
+
+            // fileLine should be a string, but I've seen some cases
+            // where it isn't, so check its type.
+            if (typeof(fileLine) != "string")
+               continue;
+
+            // If the line is blank or starts with with a semicolon
+            // (the comment character), then skip it.
+            if ((fileLine.length == 0) || (fileLine.substr(0, 1) == ";"))
+               continue;
+
+            // Look for a file extension in square brackets ([ and ]).
+            // If found, then set filenameExt and continue onto the next line.
+            // Note: This regular expression allows whitespace around the [...].
+            if (/^\s*\[.*\]\s*$/.test(fileLine))
+            {
+               var startIndex = fileLine.indexOf("[") + 1;
+               var endIndex = fileLine.lastIndexOf("]");
+               var ext = fileLine.substr(startIndex, endIndex-startIndex).toUpperCase();
+               // If the filename extension is different than the last one
+               // we've seen, then:
+               // 1. If scannableFile is not null, then add it to gScannableFileTypes.
+               // 2. Create a new one (referenced as scannableFile).
+               if (ext != filenameExt)
+               {
+                  if ((scannableFile != null) && (scannableFile != undefined) &&
+                      (filenameExt.length > 0))
+                  {
+                     gFileTypeCfg[filenameExt] = scannableFile;
+                  }
+                  filenameExt = ext;
+                  scannableFile = new ScannableFile(ext, "", "scan");
+               }
+               continue;
+            }
+
+            // If filenameExt is blank, then continue onto the next line.
+            if (filenameExt.length == 0)
+               continue;
+
+            // If we're here, then filenameExt is set, and this is a valid
+            // line to process.
+            // Look for an = in the line, and if found, split into
+            // option & value.
+            pos = fileLine.indexOf("=");
+            if (pos > -1)
+            {
+               // Extract the option & value, trimming leading & trailing spaces.
+               option = trimSpaces(fileLine.substr(0, pos), true, false, true).toUpperCase();
+               optionValue = trimSpaces(fileLine.substr(pos+1), true, false, true);
+
+               if (option == "EXTRACT")
+                  scannableFile.extractCmd = optionValue;
+               else if (option == "SCANOPTION")
+                  scannableFile.scanOption = optionValue;
+            }
+         }
+      }
+
+      fileTypeCfgFile.close();
+   }
+
+   // Read the general program configuration
+   var genSettingsRead = false;
+   var genCfgFile = new File(pCfgFilePath + "DDUP.cfg");
+   if (genCfgFile.open("r"))
+   {
+      if (genCfgFile.length > 0)
+      {
+         genSettingsRead = true;
+         var fileLine = null;     // A line read from the file
+         var equalsPos = 0;       // Position of a = in the line
+         var commentPos = 0;      // Position of the start of a comment
+         var setting = null;      // A setting name (string)
+         var settingUpper = null; // Upper-case setting name
+         var value = null;        // A value for a setting (string)
+         while (!genCfgFile.eof)
+         {
+            // Read the next line from the config file.
+            fileLine = genCfgFile.readln(1024);
+
+            // fileLine should be a string, but I've seen some cases
+            // where it isn't, so check its type.
+            if (typeof(fileLine) != "string")
+               continue;
+
+            // If the line starts with with a semicolon (the comment
+            // character) or is blank, then skip it.
+            if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
+               continue;
+
+            // If the line has a semicolon anywhere in it, then remove
+            // everything from the semicolon onward.
+            commentPos = fileLine.indexOf(";");
+            if (commentPos > -1)
+               fileLine = fileLine.substr(0, commentPos);
+
+            // Look for an equals sign, and if found, separate the line
+            // into the setting name (before the =) and the value (after the
+            // equals sign).
+            equalsPos = fileLine.indexOf("=");
+            if (equalsPos > 0)
+            {
+               // Read the setting & value, and trim leading & trailing spaces.
+               setting = trimSpaces(fileLine.substr(0, equalsPos), true, false, true);
+               settingUpper = setting.toUpperCase();
+               value = trimSpaces(fileLine.substr(equalsPos+1), true, false, true);
+
+               // Skip this one if the value is blank.
+               if (value.length == 0)
+                  continue;
+
+               // Set the appropriate value in the settings object.
+               if (settingUpper == "SCANCMD")
+                  gGenCfg.scanCmd = value;
+               else if (settingUpper == "SKIPSCANIFSYSOP")
+                  gGenCfg.skipScanIfSysop = (value.toUpperCase() == "YES");
+               else if (settingUpper == "PAUSEATEND")
+                  gGenCfg.pauseAtEnd = (value.toUpperCase() == "YES");
+            }
+         }
+
+         genCfgFile.close();
+      }
+   }
+
+   return (fileTypeSettingsRead && genSettingsRead);
+}
+
+// Removes multiple, leading, and/or trailing spaces
+// The search & replace regular expressions used in this
+// function came from the following URL:
+//  http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces
+//
+// Parameters:
+//  pString: The string to trim
+//  pLeading: Whether or not to trim leading spaces (optional, defaults to true)
+//  pMultiple: Whether or not to trim multiple spaces (optional, defaults to true)
+//  pTrailing: Whether or not to trim trailing spaces (optional, defaults to true)
+function trimSpaces(pString, pLeading, pMultiple, pTrailing)
+{
+	var leading = true;
+	var multiple = true;
+	var trailing = true;
+	if (typeof(pLeading) != "undefined")
+		leading = pLeading;
+	if (typeof(pMultiple) != "undefined")
+		multiple = pMultiple;
+	if (typeof(pTrailing) != "undefined")
+		trailing = pTrailing;
+		
+	// To remove both leading & trailing spaces:
+	//pString = pString.replace(/(^\s*)|(\s*$)/gi,"");
+
+	if (leading)
+		pString = pString.replace(/(^\s*)/gi,"");
+	if (multiple)
+		pString = pString.replace(/[ ]{2,}/gi," ");
+	if (trailing)
+		pString = pString.replace(/(\s*$)/gi,"");
+
+	return pString;
+}
+
+// Returns a filename's extension.  Always returns a string.
+//
+// Parameters:
+//  pFilename: The name of a file
+//
+// Return value: The filename's extension, or blank if there is none.
+function getFilenameExtension(pFilename)
+{
+   const filenameUpper = pFilename.toUpperCase();
+   var filenameExt = "";
+   // Special case for .tar.gz - Report tar.gz as an extension
+   // rather than just gz
+   if (/.TAR.GZ$/.test(filenameUpper))
+      filenameExt = "TAR.GZ";
+   else
+   {
+      // Look for the last period in filenameUpper
+      var dotIndex = filenameUpper.lastIndexOf(".");
+      if (dotIndex > -1)
+         filenameExt = filenameUpper.substr(dotIndex+1);
+   }
+   return filenameExt;
+}
+
+// This function returns just the filename from the end of a full path, regardless
+// of whether it has a trailing slash.
+//
+// Parameters:
+//  pFilename: The full path & filename
+//
+// Return value: Just the filename from the end of the path
+function getFilenameFromPath(pFilename)
+{
+   var filename = pFilename;
+   if (filename.length > 0)
+   {
+      // If the filename has a trailing slash, remove it.  Then,
+      // use file_getname() to get the filename from the end.
+      if ((/\/$/.test(filename)) || (/\\$/.test(filename)))
+         filename = filename.substr(0, filename.length-1);
+      filename = file_getname(filename);
+   }
+   return filename;
+}
+
+// Given a full path & filename, this function returns just the path portion,
+// with a trailing slash.
+//
+// Parameters:
+//  pFilename: A filename with the full path
+//
+// Return value: Just the path portion of the filename.
+function getPathFromFilename(pFilename)
+{
+   // Make sure pFilename is valid
+   if ((pFilename == null) || (pFilename == undefined))
+      return "";
+   if (typeof(pFilename) != "string")
+      return "";
+   if (pFilename.length == 0)
+      return "";
+
+   // Determine which slash character to use for paths, depending
+   // on the OS.
+   if (getPathFromFilename.inWin == undefined)
+      getPathFromFilename.inWin = /^WIN/.test(system.platform.toUpperCase());
+   var pathSlash = (getPathFromFilename.inWin ? "\\" : "/");
+
+   // Make sure the filename has the correct slashes for
+   // the platform.
+   var filename = fixPathSlashes(pFilename);
+
+   // If pFilename is actually a directory, then just return it.
+   if (file_isdir(filename))
+   {
+      // Make sure it has a trailing slash that's appropriate
+      // for the OS.
+      var lastChar = filename.charAt(filename.length-1);
+      if ((lastChar != "/") || (lastChar == "\\"))
+         filename += pathSlash;
+      return filename;
+   }
+
+   // Find the index of the last slash and use that to extract the path.
+   var path = "";
+   var lastSlashIndex = filename.lastIndexOf(pathSlash);
+   if (lastSlashIndex > 0)
+      path = filename.substr(0, lastSlashIndex);
+
+   // If we extracted the path, make sure it ends with a slash.
+   if (path.length > 0)
+   {
+      var lastChar = path.charAt(path.length-1);
+      if (lastChar != pathSlash)
+         path += pathSlash;
+   }
+
+   return path;
+}
+
+// Fixes all slashes in a given path to be the appropriate slash
+// character for the OS.  Returns a new string with the fixed version.
+//
+// Parameters:
+//  pPath: A path to fix
+//
+// Return value: The fixed version of pPath
+function fixPathSlashes(pPath)
+{
+   // Make sure pPath is valid.
+   if ((pPath == null) || (pPath == undefined))
+      return "";
+   if (typeof(pPath) != "string")
+      return "";
+   if (pPath.length == 0)
+      return "";
+
+   // Create a variable to store whether or not we're in Windows,
+   // but only once (for speed).
+   if (fixPathSlashes.inWin == undefined)
+      fixPathSlashes.inWin = /^WIN/.test(system.platform.toUpperCase());
+
+   // Fix the slashes and return the fixed version.
+   //return(fixPathSlashes.inWin ? pPath.replace("/", "\\") : pPath.replace("\\", "/"));
+   var path = pPath;
+   if (fixPathSlashes.inWin) // Windows
+   {
+      while (path.indexOf("/") > -1)
+         path = path.replace("/", "\\");
+   }
+   else // *nix
+   {
+      while (path.indexOf("\\") > -1)
+         path = path.replace("\\", "/");
+   }
+   return path;
+}
+
+// This function extracts a file to a directory.
+//
+// Parameters:
+//  pFilename: The name of the file to extract
+//  pWorkDir: The directory to extract the file into.  This directory must
+//            exist before calling this function.
+//
+// Return value: A blank string on success, or an error message on failure.
+function extractFileToDir(pFilename, pWorkDir)
+{
+   // If pFilename doesn't exist, then return with an error.
+   if (typeof(pFilename) != "string")
+      return ("Invalid filename specified.");
+   if (pFilename.length == 0)
+      return ("No filename specified.");
+   if (!file_exists(pFilename))
+      return ("The specified file does not exist.");
+
+   // If pWorkDir is blank, then return with an error.
+   if (typeof(pWorkDir) != "string")
+      return ("Unknown argument specified for the work directory.");
+   if (pWorkDir.length == 0)
+      return ("No work directory specified.");
+
+   // If pWorkDir ends with a slash, remove it.
+   if ((/\/$/.test(pWorkDir)) || (/\\$/.test(pWorkDir)))
+      pWorkDir = pWorkDir.substr(0, pWorkDir.length-1);
+
+   // If the work directory doesn't exist, then return with
+   // an error.
+   // Note: file_exists() doesn't seem to work properly with directories.
+   //if (!file_exists(pWorkDir))
+   //   return ("The work directory doesn't exist.");
+
+   var filenameExt = getFilenameExtension(pFilename);
+   // Return with errors if there are problems.
+   if (filenameExt.length == 0)
+      return ("Can't extract (no file extension).");
+   if (typeof(gFileTypeCfg[filenameExt]) == "undefined")
+      return ("Can't extract " + getFilenameFromPath(pFilename) + " (I don't know how).");
+   if (gFileTypeCfg[filenameExt].extractCmd == "")
+      return ("Can't extract " + getFilenameFromPath(pFilename) + " (I don't know how).");
+
+   var retval = "";
+
+   // Extract the file to the work directory.
+   var extractCmd = gFileTypeCfg[filenameExt].extractCmd.replace("%FILENAME%", "\"" + fixPathSlashes(pFilename) + "\"");
+   extractCmd = extractCmd.replace("%FILESPEC% ", "");
+   extractCmd = extractCmd.replace("%TO_DIR%", "\"" + fixPathSlashes(pWorkDir) + "\"");
+   var retCode = system.exec(extractCmd);
+   if (retCode != 0)
+      return ("Extract failed with exit code " + retCode);
+   //   For each file in the work directory:
+   //     If the file has an extract command
+   //        Extract it to a subdir in the temp dir
+   //        Delete the archive
+   var files = directory(pWorkDir + "/*");
+   for (var i in files)
+   {
+      // If the file has an extract command, then extract it to a
+      // temp directory in the work directory.
+      filenameExt = getFilenameExtension(files[i]);
+      if ((typeof(gFileTypeCfg[filenameExt]) != "undefined") &&
+          ((gFileTypeCfg[filenameExt].extractCmd != "")))
+      {
+         // Create the temp directory and extract the file there.
+         var workDir = pWorkDir + "/" + getFilenameFromPath(files[i] + "_temp");
+         if (mkdir(workDir))
+            retval = extractFileToDir(files[i], workDir);
+         else
+            retval = "Unable to create a temporary directory.";
+
+         // If there was no problem, then delete the archive file.  Otherwise,
+         // stop going through the list of files.
+         if (retval.length == 0)
+            file_remove(files[i]);
+         else
+            break;
+      }
+   }
+
+   return retval;
+}
+
+// This function executes an OS command and returns its output as an
+// array of strings.  The reason this function was written is that
+// system.popen() is only functional in UNIX.
+//
+// Parameters:
+//  pCommand: The command to execute
+//
+// Return value: An object containing the following properties:
+//               returnCode: The return code of the OS command
+//               cmdOutput: An array of strings containing the program's output.
+function execCmdWithOutput(pCommand)
+{
+   var retObj = new Object();
+   retObj.returnCode = 0;
+   retObj.cmdOutput = new Array();
+
+   if ((pCommand == undefined) || (pCommand == null) || (typeof(pCommand) != "string"))
+      return retObj;
+
+   // Execute the command and redirect the output to a file in the
+   // node's directory.  system.exec() returns the return code that the
+   // command returns; generally, 0 means success and non-zero means
+   // failure (or an error of some sort).
+   const tempFilename = system.node_dir + "DDUPCommandOutput_temp.txt";
+   retObj.returnCode = system.exec(pCommand + " >" + tempFilename + " 2>&1");
+   // Read the temporary file and populate retObj.cmdOutput with its
+   // contents.
+   var tempFile = new File(tempFilename);
+   if (tempFile.open("r"))
+   {
+      if (tempFile.length > 0)
+      {
+         var fileLine = null;
+         while (!tempFile.eof)
+         {
+            fileLine = tempFile.readln(2048);
+
+            // fileLine should be a string, but I've seen some cases
+            // where it isn't, so check its type.
+            if (typeof(fileLine) != "string")
+               continue;
+
+            retObj.cmdOutput.push(fileLine);
+         }
+      }
+
+      tempFile.close();
+   }
+
+   // Remove the temporary file, if it exists.
+   if (file_exists(tempFilename))
+      file_remove(tempFilename);
+
+   return retObj;
+}
+
+// Runs an external command.  This function was written because
+// I want to be able to handle executable files with spaces in
+// their name/path (system.exec() doesn't handle said spaces).
+//
+// Parameters:
+//  pCommand: The command to execute
+//
+// Return value: An object containing the following properties:
+//               returnCode: The return code of the OS command
+//               cmdOutput: An array of strings containing the program's output.
+function runExternalCmdWithOutput(pCommand)
+{
+   // Determine whether or not we're in Windows.
+   if (runExternalCmdWithOutput.inWin == undefined)
+      runExternalCmdWithOutput.inWin = /^WIN/.test(system.platform.toUpperCase());
+
+   var retObj = null; // The return object
+   var wroteScriptFile = false; // Whether or not we were able to write the script file
+
+   // In the node directory, write a batch file (if in Windows) or a *nix shell
+   // script (if not in Windows) containing the command to run.
+   var scriptFilename = "";
+   if (runExternalCmdWithOutput.inWin)
+   {
+      // Write a Windows batch file to run the command
+      scriptFilename = fixPathSlashes(system.node_dir + "DDUP_ScanCmd.bat");
+      //console.print(":" + scriptFilename + ":\r\n\1p"); // Temporary (for debugging)
+      var scriptFile = new File(scriptFilename);
+      if (scriptFile.open("w"))
+      {
+         scriptFile.writeln("@echo off");
+         scriptFile.writeln(pCommand);
+         scriptFile.close();
+         wroteScriptFile = true;
+         retObj = execCmdWithOutput(scriptFilename);
+      }
+   }
+   else
+   {
+      // Write a *nix shell script to run the command
+      scriptFilename = system.node_dir + "DDUP_ScanCmd.sh";
+      var scriptFile = new File(scriptFilename);
+      if (scriptFile.open("w"))
+      {
+         scriptFile.writeln("#!/bin/bash"); // Hopefully /bin/bash is valid on the system!
+         scriptFile.writeln(pCommand);
+         scriptFile.close();
+         wroteScriptFile = true;
+         system.exec("chmod ugo+x " + scriptFilename);
+         retObj = execCmdWithOutput("bash " + scriptFilename);
+      }
+   }
+
+   // Remove the script file, if it exists
+   if (file_exists(scriptFilename))
+      file_remove(scriptFilename);
+
+   // If we were unable to write the script file, then create retObj with
+   // a returnCode indicating failure.
+   if (!wroteScriptFile)
+   {
+      // Could not open the script file for writing
+      retObj = new Object();
+      retObj.cmdOutput = new Array();
+      retObj.returnCode = -1;
+   }
+
+   return retObj;
+}
\ No newline at end of file
diff --git a/xtrn/DDUploadProcessor/DDUPFileTypes.cfg b/xtrn/DDUploadProcessor/DDUPFileTypes.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..bb73a127f6f93757b5a5ecc82b343d928eb13997
--- /dev/null
+++ b/xtrn/DDUploadProcessor/DDUPFileTypes.cfg
@@ -0,0 +1,201 @@
+; This is the file type configuration for Digital Distortion Upload Processor.
+
+; Compressed archive file extensions
+[ZIP]
+scanOption=scan
+; PKZip for Win32
+;EXTRACT=\BBS\COMPRESS\pkzip25.exe -extract -NoZipExtension -Directories %FILENAME% %FILESPEC% %TO_DIR%
+; Info-ZIP for Win32 console (comes with Synchronet)
+EXTRACT=\BBS\sbbs\exec\unzip.exe -qq -o %FILENAME% %FILESPEC% -d %TO_DIR%
+; Info-ZIP, *nix
+;EXTRACT=unzip -qq -o %FILENAME% %FILESPEC% -d %TO_DIR%
+; 7-Zip for Win32 console
+;EXTRACT=\BBS\COMPRESS\7za.exe x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+; 7-Zip for *nix
+;EXTRACT=7za x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+
+[7Z]
+scanOption=scan
+; 7-Zip for Win32 console
+EXTRACT=\BBS\COMPRESS\7za.exe x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+; 7-Zip for *nix
+;EXTRACT=7za x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+
+[RAR]
+scanOption=scan
+; RAR for Win32 console
+EXTRACT=\BBS\COMPRESS\Rar.exe x -p- -y %FILENAME% %FILESPEC% %TO_DIR%
+; Alexander Roshal's unrar for *nix
+;EXTRACT=unrar x %FILENAME% %FILESPEC% %TO_DIR%
+
+[ARJ]
+scanOption=scan
+; ARJ for Win32 console
+EXTRACT=\BBS\COMPRESS\ARJ32.EXE x -y %FILENAME% %FILESPEC% -ht%TO_DIR%
+; Open-Source ARJ for *nix
+;EXTRACT=arj x %FILENAME% %FILESPEC% -ht%TO_DIR%
+
+[ISO]
+scanOption=scan
+; 7-Zip for Win32 console
+EXTRACT=\BBS\COMPRESS\7za.exe x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+; 7-Zip for *nix
+;EXTRACT=7za x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+
+[TAR]
+scanOption=scan
+; 7-Zip for Win32 console
+EXTRACT=\BBS\COMPRESS\7za.exe x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+; 7-Zip for *nix
+;EXTRACT=7za x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+
+[GZ]
+scanOption=scan
+; 7-Zip for Win32 console
+EXTRACT=\BBS\COMPRESS\7za.exe x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+; 7-Zip for *nix
+;EXTRACT=7za x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+
+[TGZ]
+scanOption=scan
+; 7-Zip for Win32 console
+EXTRACT=\BBS\COMPRESS\7za.exe x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+; 7-Zip for *nix
+;EXTRACT=7za x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+
+[TAR.GZ]
+scanOption=scan
+; 7-Zip for Win32 console
+EXTRACT=\BBS\COMPRESS\7za.exe x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+; tar for *nix
+;EXTRACT=cd %TO_DIR% && tar zxvf %FILENAME%
+
+; Microsoft Installer file
+[MSI]
+scanOption=scan
+; 7-Zip for Win32 console
+EXTRACT=\BBS\COMPRESS\7za.exe x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+; 7-Zip for *nix
+;EXTRACT=7za x -y -pxyz %FILENAME% %FILESPEC% -o%TO_DIR%
+
+; Text file extensions
+[TXT]
+scanOption=always pass
+
+[DIZ]
+scanOption=always pass
+
+[DOC]
+scanOption=scan
+
+[ANS]
+scanOption=always pass
+
+[ASC]
+scanOption=always pass
+
+[ASCII]
+scanOption=always pass
+
+[RIP]
+scanOption=always pass
+
+[NFO]
+scanOption=always pass
+
+; Chinese text file
+[ZW]
+scanOption=always pass
+
+[NEW]
+scanOption=always pass
+
+[BBS]
+scanOption=always pass
+
+[ICE]
+scanOption=always pass
+
+[LOG]
+scanOption=always pass
+
+; Guitar tab file
+[TAB]
+scanOption=always pass
+
+; Guitar tab file
+[CRD]
+scanOption=always pass
+
+; Guitar tab file
+[CHORD]
+scanOption=always pass
+
+; Readme file (i.e., READ.ME)
+[ME]
+scanOption=always pass
+
+[FAQ]
+scanOption=always pass
+
+[NOW]
+scanOption=always pass
+
+[HTM]
+scanOption=always pass
+
+[HTML]
+scanOption=always pass
+
+[REG]
+scanOption=scan
+
+; A readme file, as in README.1ST
+[1ST]
+scanOption=always pass
+
+[CFG]
+scanOption=always pass
+
+[INI]
+scanOption=always pass
+
+; Batch file
+[BAT]
+scanOption=always pass
+
+; Command list file (similar to a DOS batch file)
+[CMD]
+scanOption=always pass
+
+; REXX script (a programming language developed by IBM)
+[REXX]
+scanOption=always pass
+
+; *nix shell script
+[SH]
+scanOption=always pass
+
+; Outlook Express email message
+[EMAIL]
+scanOption=scan
+
+; Mail message
+[EMLX]
+scanOption=scan
+
+; EditPad Pro
+[EPP]
+scanOption=always pass
+
+; Error log
+[ERR]
+scanOption=always pass
+
+; Outlook Express mailbox index file
+[IDX]
+scanOption=scan
+
+; Data list
+[LST]
+scanOption=always pass
\ No newline at end of file
diff --git a/xtrn/DDUploadProcessor/DDUP_Cleanup.js b/xtrn/DDUploadProcessor/DDUP_Cleanup.js
new file mode 100644
index 0000000000000000000000000000000000000000..7a8bf12b24febdc01e6a95ede7a5801da7424dea
--- /dev/null
+++ b/xtrn/DDUploadProcessor/DDUP_Cleanup.js
@@ -0,0 +1,119 @@
+/* This is a cleanup script for Digital Distortion Upload Processor.
+ * This script cleans up temporary files & directories from the
+ * node directory used by Digital Distortion Upload Processor.
+ *
+ * Author: Eric Oulashin (AKA Nightfox)
+ * BBS: Digital Distortion
+ * BBS address: digdist.bbsindex.com
+ *
+ * Date       User              Description
+ * 2009-12-26 Eric Oulashin     Created
+ * 2009-12-28 Eric Oulashin     Added removal of DDUPCommandOutput_temp.txt
+ *                              and DDUP_ScanCmd.*
+ */
+
+load("sbbsdefs.js");
+
+// Remove the temporary work directory used by Digital Distortion
+// Upload Processor.
+deltree(system.node_dir + "DDUploadProcessor_Temp");
+// Remove DDUPCommandOutput_temp.txt from the node directory
+file_remove(system.node_dir + "DDUPCommandOutput_temp.txt");
+// Remove the command script from the node directory
+file_remove(system.node_dir + "DDUP_ScanCmd.*");
+
+
+/////////////////////////////////////////////////////////////////////////////////
+// Functions
+
+// This function recursively removes a directory and all of its contents.  Returns
+// whether or not the directory was removed.
+//
+// Parameters:
+//  pDir: The directory to remove (with trailing slash).
+//
+// Return value: Boolean - Whether or not the directory was removed.
+function deltree(pDir)
+{
+   if ((pDir == null) || (pDir == undefined))
+      return false;
+   if (typeof(pDir) != "string")
+      return false;
+   if (pDir.length == 0)
+      return false;
+   // Make sure pDir actually specifies a directory.
+   if (!file_isdir(pDir))
+      return false;
+   // Don't wipe out a root directory.
+   if ((pDir == "/") || (pDir == "\\") || (/:\\$/.test(pDir)) || (/:\/$/.test(pDir)) || (/:$/.test(pDir)))
+      return false;
+
+   // If we're on Windows, then use the "RD /S /Q" command to delete
+   // the directory.  Otherwise, assume *nix and use "rm -rf" to
+   // delete the directory.
+   if (deltree.inWindows == undefined)
+      deltree.inWindows = (/^WIN/.test(system.platform.toUpperCase()));
+   if (deltree.inWindows)
+      system.exec("RD " + withoutTrailingSlash(pDir) + " /s /q");
+   else
+      system.exec("rm -rf " + withoutTrailingSlash(pDir));
+   // The directory should be gone, so we should return true.  I'd like to verify that the
+   // directory really is gone, but file_exists() seems to return false for directories,
+   // even if the directory does exist.  So I test to make sure no files are seen in the dir.
+   return (directory(pDir + "*").length == 0);
+
+   /*
+   // Recursively deleting each file & dir using JavaScript:
+   var retval = true;
+
+   // Open the directory and delete each entry.
+   var files = directory(pDir + "*");
+   for (var i = 0; i < files.length; ++i)
+   {
+      // If the entry is a directory, then deltree it (Note: The entry
+      // should have a trailing slash).  Otherwise, delete the file.
+      // If the directory/file couldn't be removed, then break out
+      // of the loop.
+      if (file_isdir(files[i]))
+      {
+         retval = deltree(files[i]);
+         if (!retval)
+            break;
+      }
+      else
+      {
+         retval = file_remove(files[i]);
+         if (!retval)
+            break;
+      }
+   }
+
+   // Delete the directory specified by pDir.
+   if (retval)
+      retval = rmdir(pDir);
+
+   return retval;
+   */
+}
+
+// Removes a trailing (back)slash from a path.
+//
+// Parameters:
+//  pPath: A directory path
+//
+// Return value: The path without a trailing (back)slash.
+function withoutTrailingSlash(pPath)
+{
+   if ((pPath == null) || (pPath == undefined))
+      return "";
+
+   var retval = pPath;
+   if (retval.length > 0)
+   {
+      var lastIndex = retval.length - 1;
+      var lastChar = retval.charAt(lastIndex);
+      if ((lastChar == "\\") || (lastChar == "/"))
+         retval = retval.substr(0, lastIndex);
+   }
+   return retval;
+}
\ No newline at end of file
diff --git a/xtrn/DDUploadProcessor/FILE_ID.DIZ b/xtrn/DDUploadProcessor/FILE_ID.DIZ
new file mode 100644
index 0000000000000000000000000000000000000000..3f8d0408fd33b8e1a0189362687be72d8227f801
--- /dev/null
+++ b/xtrn/DDUploadProcessor/FILE_ID.DIZ
@@ -0,0 +1,9 @@
+nhc  Digital Distortion Upload Processor yvw1.00
+hy             For Synchronet 3.14+
+k���������������������������������������������
+ncThis is an upload processor for Synchronet
+that allows for performing a virus scan on
+files inside of archives.  File extraction
+commands and the virus scan command are
+configurable.
+bhRelease date: 2009-12-29
\ No newline at end of file
diff --git a/xtrn/DDUploadProcessor/Read Me.txt b/xtrn/DDUploadProcessor/Read Me.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7ae3b2cce93b2e4d466068f75b8795ec1a15590c
--- /dev/null
+++ b/xtrn/DDUploadProcessor/Read Me.txt	
@@ -0,0 +1,343 @@
+                   Digital Distortion Upload Processor
+                              Version 1.00
+                        Release date: 2009-12-29
+
+                                  by
+
+                             Eric Oulashin
+                     Sysop of Digital Distortion BBS
+               BBS internet address: digitaldistortionbbs.com
+                                     digdist.bbsindex.com
+                     Email: eric.oulashin@gmail.com
+
+
+
+This file describes the Digital Distortion Upload Processor.
+
+Contents
+========
+1. Disclaimer
+2. Introduction
+3. Archive File Software
+4. Installation and Setup
+5. Main configuration file
+6. Archive file type configuration file
+
+
+1. Disclaimer
+=============
+The only guarantee that I can make about Digital Distortion Upload Processor
+is that it will take up space on your computer.  I have tested this with
+the Windows verison of Synchronet and with the Windows version of AVG Free
+(virus scanner) on my BBS, running in Windows 2000; this script has not
+been tested with Linux Synchronet platforms.  I created this script because
+I felt that it would be useful and am providing it to the Synchronet BBS
+community in case other Synchronet sysops might find it useful.
+
+
+2. Introduction
+===============
+Digital Distortion Upload Processor is a script makes use of a virus scanner
+to scan uploaded files, with the added ability to extract compressed files
+in order to scan the files inside the compressed file.
+
+File formats can be specified and configured via a configuration file,
+including extraction commands for compressed files.  In addition, the
+virus scan command can be configured in the main configuration file,
+which should allow for the use of any virus scanner, as long as it is a
+command-line scanner (no GUI) and is able to take a subdirectory as a
+command-line parameter.
+
+Compressed (archive) files will be extracted to a temporary directory in the
+node directory after they are uploaded.  Furthermore, compressed files
+within the compressed file will be extracted to subdirectories within that
+subdirectory, and any compressed files inside those compressed files will
+be extracted, etc..  This way, all files inside of the archive can be
+scanned by the virus scanner.
+
+The temporary directory created in the node directory has the following
+name:
+DDUploadProcessor_Temp
+Additionally, the following temporary files are created in the node
+directory:
+DDUPCommandOutput_temp.txt
+DDUP_ScanCmd.bat (for Win32 systems) or DDUP_ScanCmd.sh (for *nix systems)
+
+The temporary files and temporary directory will be removed when the
+script finishes; however, in cases where they aren't removed (i.e., if the
+user disconnects during the scan), a cleanup script is also included, which
+can be executed in your logon and logoff scripts to ensure  that the node
+directory does not contain the temporary files.
+
+Detection of viruses will reject the uploaded file.  Also, failure to
+extract an archive (and thus, inability to scan for viruses) will cause the
+uploaded file to be rejected.
+
+
+3. Archive File Software
+========================
+Digital Distortion Upload Processor comes with configuration settings to
+handle extraction of ZIP, 7Z (7-Zip), RAR, ARJ, MSI, TAR, GZ, TGZ, and
+TAR.GZ archives.
+
+The file format configuration file included with this script includes
+extraction command lines (specified by an EXTRACT setting) for various
+archivers for both Windows and Linux.  In order for this script to work
+properly, you will need to make sure you have the appropriate archiver
+software installed on your system, and you will need to edit the
+DDUPFileTypes.cfg file (using a text editor) and make sure you have
+EXTRACT command lines set properly for your system.  For information on
+that configuration file, see section 6: Archive file type configuration
+file.
+
+The following archive contains Win32 command-line archivers for popular
+archive file formats:
+http://digdist.bbsindex.com/miscFilesForDL/Win32CmdLineCompressionTools.zip
+The archivers included in that archive handle the most popular file formats
+(ZIP, 7Z (7-Zip), RAR, ARJ, TAR, GZ, TGZ, and TAR.GZ), and they are set up in
+DDUPFileTypes.cfg to extract popular file formats (ZIP, 7Z (7-Zip), RAR, ARJ,
+MSI, TAR, GZ, TGZ, and TAR.GZ).  Note that you will need to edit that .cfg
+file and change the path to the .exe file according to where you copied them
+on your system.  If your BBS is running in Windows, the included configuration
+file should work for you (although it does also have the Linux command lines
+as comments).  If you copy the archivers to a directory that is not in your
+system path, you will need to edit the DDUPFileTypes.cfg file to include the
+full paths with the archive executables.
+
+Extractor notes:
+DDUPFileTypes.cfg includes a setup for using 7-Zip to extract ISO (CD/DVD
+image) files; however, in testing, it seemed that 7-Zip can only extract
+or see one file in an ISO image.
+DDUPFileTypes.cfg also includes a setup for extracting MSI (Microsoft
+Installer) files.
+
+For Linux, the following is a list of Linux archivers that this
+script is configured for and how you can acquire them if you don't
+have them already:
+------------------
+ZIP, 7Z, GZ, TGZ, TAR:
+- Install p7zip using your distro's package manager, or download it via
+the web.  A download page is available here:
+http://www.7-zip.org/download.html
+
+RAR:
+- Install unrar using your distro's package manager, or download it via
+the web.  Or, download RARLab's version:
+http://www.rarlab.com/download.htm
+
+ARJ:
+- Source code for open-Source ARJ (and instructions) are available here:
+http://linux.softpedia.com/get/System/Archiving/Arj-12097.shtml
+This is the download link from that page:
+http://linux.softpedia.com/progDownload/Arj-Download-12097.html
+Download the source, and follow the page's instructions to build that on
+your Linux system.  After compiling, the executable file will be located
+in linux-gnu/en/rs/arj .  Place the executable file (arj) in a directory
+in your path (i.e., /usr/local/bin or /usr/bin).
+Instructions for building the ARJ source code, from the above web page,
+are as follows:
+cd gnu;autoconf;./configure;cd ..;make prepare;make
+
+Notes:
+
+1. GNU make must be used (on FreeBSD systems it's called "gmake").
+2. You have to run autoconf prior to running configure - as there will be different configure scripts for UNIX-like systems and OS/2 EMX.
+3. On OS/2 EMX, autoconf v 2.57 is confirmed to work.
+4. You can finalize the build process with "make install" (to perform a local installation) or "make package" (to create a self-extracting distribution kit).
+
+ARJ is a CPU-intensive program. If you wish to configure ARJ for a higher performance on a specific CPU, try the following, assuming that you have a newer version of GCC, e.g. 3.2.1:
+
+./configure CFLAGS="-march=i386 -mcpu=athlon-xp"
+
+where "-mcpu" designates the type of your CPU, run "man gcc" for a list of CPU types.
+
+
+4. Installation and Setup
+=========================
+Step 1: Install a virus scanner
+-------------------------------
+You will need to download and install a virus scanner on your BBS system.
+The one that I set up this script for is AVG Free version 9 for Windows,
+which is available at the following web page:
+http://free.avg.com/us-en/download?prd=afg
+
+Step 2: Copy the script files, configuration files, & archivers to your system
+------------------------------------------------------------------------------
+Digital Distortion Upload Processor consists of the following files, which
+you will need to place in your sbbs/exec directory (or another directory of
+your choice):
+ 1. DDUP.js
+ 2. DDUP.cfg
+ 3. DDUP_Cleanup.js
+ 4. DDUPFileTypes.cfg
+
+For sysops running their BBS in Windows, the following archiver programs
+in the Win32Archivers directory will need to be placed in a directory
+(preferably a directory that's included in the system's path):
+ 1. 7za.exe
+ 2. ARJ32.EXE
+ 3. Rar.exe
+ 4. unzip.exe
+
+For sysops running the Linux version of Synchronet, you will need to acquire
+the appropriate archivers as described in the previous section.
+
+Step 3: Edit the configuration files
+------------------------------------
+You will need to edit DDUPFileTypes.cfg to make sure that the EXTRACT
+command lines are correct for your system.  If you're running your BBS
+in Linux, you will first need to comment the Windows command lines and
+uncomment the Linux command lines.  See section 6: Archive file type
+configuration file.
+
+You will also need to edit DDUP.cfg and change the scanCmd option, which
+specifies the command line to use to scan files for viruses.  In this
+command line, the text string %FILESPEC% will be replaced with the name
+of the file or directory to be scanned; thus, the virus scanner you use
+should be able to take the name of an individual file or a directory as
+a parameter, and it should also be a command-line scanner (capable of
+taking command-line parameters).
+
+Special note about the scanner command line
+-------------------------------------------
+It should be noted that the scanner command line specified in DDUP.cfg
+will be written to a temporary batch file (on Win32 systems) or a shell
+script (on *nix systems), which is then run in order to scan the file(s).
+The reason for this is that if there are any spaces in the file or
+directory names used in the scanner command, the command line doesn't seem
+to be passed to the operating system correctly by Synchronet's JavaScript
+object model.
+
+
+Step 4: Set up Digital Distortion Upload Processor for Testable Files
+in Synchronet's configuration program (SCFG)
+----------------------------------------------------------------------
+1. Run Synchronet's configuration program (SCFG)
+2. From the main menu, choose "File Options".
+3. From the menu that appears, choose "Testable Files..."
+4. For each file type that you want to be able to test, you will need
+an entry in this table.  Some archive file types (i.e., ZIP) might
+already be in there.  In that case, simply scroll down to it and press
+Enter to select it.  If the desired file type is not in the list, then
+press the INS key to insert an entry or scroll down to the first blank
+line and press Enter to insert an entry there, then press Enter to
+select and edit it.
+The Command Line setting for the file type should look similar to
+this (assuming the upload processor files were copied to your
+sbbs\exec directory):
+?DDUP.js %f
+If you copied the upload processor files to another directory, you
+will need to provide the path to DDUP.js; for example:
+?/sbbs/UploadProcessor/DDUP.js %f
+As an example, your Testable File Type window should look similar
+to the following:
++[�][?]--------------------------------------------------------+
+�                      Testable File Type                      �
+�--------------------------------------------------------------�
+� �File Extension        ZIP                                   �
+� �Command Line          ?DDUP.js %f                           �
+� �Working String        Scanning arrchive file for viruses... �
+� �Access Requirements                                         �
++--------------------------------------------------------------+
+
+
+Step 5: Create/update your logout and scripts to handle temporary file cleanup
+------------------------------------------------------------------------------
+If you have not already done so, you will need to create logout and login
+scripts for your BBS; there, you will want to load DDUP_Cleanup.js - That will
+help to ensure that temporary files created by the upload processor will be
+removed to prevent extra space on your hard drive being wasted.
+
+If you are not already using a logout script, follow these steps:
+1. Create a file in your sbbs\exec directory called logout.js (or another name
+of your choosing)
+2. Add this line to it (assuming that the upload processor scripts are in
+sbbs\exec):
+load("DDUP_Cleanup.js");
+3. Add your logout script to Synchronet's configuration:
+   A. Run Synchronet's configuration program (SCFG)
+   B. From the main menu, choose "System".
+   C. From that menu, choose "Loadable Modules..."
+   D. Arrow down to highlight "Logout Event", and press Enter.  When it
+      prompts you, type in "logout" (without the quotes) and press Enter.
+   E. Escape back to the main menu, saving changes when it asks you to do
+      so.  Then, exit out of SCFG.
+If you already have a logout script (using JavaScript), you just need to add the
+following line to it:
+load("DDUP_Cleanup.js");
+
+It is recommended that you do the same thing with your logout script.
+
+
+5. Main configuration file
+==========================
+The file DDUP.cfg contains general settings for the upload processor.  This
+file can be edited with a text editor.  The syntax for each setting is as
+folows:
+setting=value
+
+where "setting" is the setting name, "value" is the corresponding value for
+the setting.
+Also, comments are allowed in the configuration file.  Comments begin with a
+semicolon (;).
+
+The following are the settings used in this configuration file:
+Setting                               Description
+-------                               -----------
+scanCmd                               The command line to use for the virus scanner.
+                                      In this command line, the text string
+                                      %FILESPEC% will be replaced with the
+                                      file/directory to be scanned.
+
+pauseAtEnd                            Specifies whether or not to pause for user
+                                      input when the scan is done.  Valid values
+                                      are yes and no.
+
+skipScanIfSysop                       Specifies whether or not to skip scanning
+                                      for the sysop(s).  Valid values are yes
+                                      and no.
+
+
+
+6. Archive file type configuration file
+=======================================
+The configuration file DDUPFileTypes.cfg defines options for various file
+types, including the extract command (for archive files) and whether or not
+you want the upload processor to scan it.  File types are specified by their
+filename extension in square brackets.  Extractable files must have an
+EXTRACT option, which specifies the command line for extracting the file.
+Another option that can be specified in this file is scanOption, which
+specifies whether or not you want the upload processor to scan the file
+(or files in the archive) with the virus scanner.
+The general format for each file type is as follows:
+
+[EXTENSION]
+EXTRACT=command
+scanOption=scan (or always pass, or always fail)
+
+By default, the scanOption setting will be "scan", and by default, the extract
+command is blank (not set).
+
+The valid values for the scanOption setting are as follows:
+scan: Scan the file using the virus scanner
+always pass: Always assume the file is good
+always fail: Always assume the file is bad
+
+As an example, the following settings can be used for zip files in Windows:
+
+[ZIP]
+scanOption=scan
+EXTRACT=\BBS\sbbs\exec\unzip.exe -qq -o %FILENAME% %FILESPEC% -d %TO_DIR%
+
+Note that for the extract command, the following pseudonyms are used:
+%FILENAME% : The name of the archive file or text file.
+%FILESPEC% : This would specify the file(s) to be extracted from the archive. 
+             The script actually will totally remove this from the command; it
+             is not used.  It's currently here for possible future use.
+%TO_DIR%   : The directory to which the archive file will be extracted.
+
+Using the above example configuration for zip files, if the user (on node 1)
+uploads D:\Files\someArchive.zip and your Synchronet installation is located
+in D:\sbbs, the temp directory is D:\sbbs\node1\DDUploadProcessor_Temp, and
+the extract command will be translated to the following:
+unzip.exe -qq -o D:\Files\someArchive.zip -d D:\sbbs\node1\DDUploadProcessor_Temp
\ No newline at end of file
diff --git a/xtrn/DDUploadProcessor/Revision history.txt b/xtrn/DDUploadProcessor/Revision history.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6bb5abf43dceb394e57abb1c5bceb17fcdc0c2fd
--- /dev/null
+++ b/xtrn/DDUploadProcessor/Revision history.txt	
@@ -0,0 +1,5 @@
+Revision History for Digital Distortion Upload Processor
+========================================================
+Version  Date         Description
+-------  ----         -----------
+1.00     2009-12-29   First general public release
\ No newline at end of file