diff --git a/config/alfresco/desktop/Alfresco.exe b/config/alfresco/desktop/Alfresco.exe index 1b2dfaf205..df18004bab 100644 Binary files a/config/alfresco/desktop/Alfresco.exe and b/config/alfresco/desktop/Alfresco.exe differ diff --git a/config/alfresco/domain/hibernate-cfg.properties b/config/alfresco/domain/hibernate-cfg.properties index 5da910a9cd..275d4fedf4 100644 --- a/config/alfresco/domain/hibernate-cfg.properties +++ b/config/alfresco/domain/hibernate-cfg.properties @@ -12,4 +12,4 @@ hibernate.cache.use_second_level_cache=true hibernate.default_batch_fetch_size=1 hibernate.jdbc.batch_size=32 hibernate.connection.release_mode=auto -hibernate.connection.isolation=4 \ No newline at end of file +hibernate.connection.isolation=2 \ No newline at end of file diff --git a/config/alfresco/scheduled-jobs-context.xml b/config/alfresco/scheduled-jobs-context.xml index 294215a324..612be30dc9 100644 --- a/config/alfresco/scheduled-jobs-context.xml +++ b/config/alfresco/scheduled-jobs-context.xml @@ -40,7 +40,6 @@ - 1 @@ -49,38 +48,6 @@ - - - - - - - org.alfresco.repo.node.index.IndexRecoveryJob - - - - - - - - - - - - - - - - 5 - - - 0 - - - diff --git a/config/alfresco/workflow/parallelreview_processdefinition.xml b/config/alfresco/workflow/parallelreview_processdefinition.xml index 2ccbae432b..13b80f9955 100644 --- a/config/alfresco/workflow/parallelreview_processdefinition.xml +++ b/config/alfresco/workflow/parallelreview_processdefinition.xml @@ -6,14 +6,7 @@ - - - + @@ -21,6 +14,14 @@ #{bpm_assignees} reviewer + + + diff --git a/config/alfresco/workflow/workflow-messages.properties b/config/alfresco/workflow/workflow-messages.properties index 32aba2a288..297cebe529 100644 --- a/config/alfresco/workflow/workflow-messages.properties +++ b/config/alfresco/workflow/workflow-messages.properties @@ -13,6 +13,10 @@ wf_workflowmodel.type.wf_submitReviewTask.title=Start Review wf_workflowmodel.type.wf_submitReviewTask.description=Submit documents for review & approval wf_workflowmodel.type.wf_reviewTask.title=Review wf_workflowmodel.type.wf_reviewTask.description=Review Documents to Approve or Reject them +wf_workflowmodel.type.wf_rejectedTask.title=Rejected +wf_workflowmodel.type.wf_rejectedTask.description=Rejected +wf_workflowmodel.type.wf_approvedTask.title=Approved +wf_workflowmodel.type.wf_approvedTask.description=Approved # Review & Approve Process Definitions diff --git a/source/cpp/CAlfrescoApp/CAlfrescoApp.cpp b/source/cpp/CAlfrescoApp/CAlfrescoApp.cpp index 94aa66b959..276faa7484 100644 --- a/source/cpp/CAlfrescoApp/CAlfrescoApp.cpp +++ b/source/cpp/CAlfrescoApp/CAlfrescoApp.cpp @@ -25,6 +25,7 @@ #include "util\DataBuffer.h" #include "util\FileName.h" #include "util\Integer.h" +#include "util\Debug.h" #include @@ -68,6 +69,22 @@ BOOL CAlfrescoApp::InitInstance() CWinApp::InitInstance(); AfxEnableControlContainer(); + // Check if debug logging is enabled + + char dbgLogName[MAX_PATH]; + size_t dbgLogSize; + + if ( getenv_s( &dbgLogSize, dbgLogName, sizeof( dbgLogName), "ALFDEBUG") == 0) { + + // Enable debug output + + Debug::openLog( dbgLogName); + + // Log the application startup + + DBGOUT_TS << "---------- Desktop client app started ----------" << endl; + } + // Get the application path String appPath = __wargv[0]; @@ -76,6 +93,7 @@ BOOL CAlfrescoApp::InitInstance() if ( pos < 0) { AfxMessageBox( L"Invalid application path", MB_OK | MB_ICONSTOP); + DBGOUT_TS << "Error, bad application path, " << appPath << endl; return 1; } @@ -91,10 +109,29 @@ BOOL CAlfrescoApp::InitInstance() try { + // DEBUG + + DBGOUT_TS << "Using folder " << folderPath << endl; + // Get the action information AlfrescoActionInfo actionInfo = alfresco.getActionInformation(exeName); + // DEBUG + + if ( HAS_DEBUG) { + DBGOUT_TS << "Action " << actionInfo.getName() << endl; + DBGOUT_TS << " PreProcess: "; + + if ( actionInfo.hasPreProcessAction( PreConfirmAction)) + DBGOUT << "Confirm "; + if ( actionInfo.hasPreProcessAction( PreCopyToTarget)) + DBGOUT << "CopyToTarget "; + if ( actionInfo.hasPreProcessAction( PreLocalToWorkingCopy)) + DBGOUT << "LocalToWorkingCopy"; + DBGOUT << endl; + } + // Check if the action should be confirmed if ( actionInfo.hasPreProcessAction(PreConfirmAction)) { @@ -105,10 +142,16 @@ BOOL CAlfrescoApp::InitInstance() if ( confirmMsg.length() == 0) confirmMsg = L"Run action ?"; + // DEBUG + + DBGOUT_TS << "Confirm action, message = " << confirmMsg << endl; + // Display a confirmation dialog - if ( AfxMessageBox( confirmMsg, MB_OKCANCEL | MB_ICONQUESTION) == IDCANCEL) + if ( AfxMessageBox( confirmMsg, MB_OKCANCEL | MB_ICONQUESTION) == IDCANCEL) { + DBGOUT_TS << "User cancelled action" << endl; return FALSE; + } } // Check if the action supports multiple paths, if not then call the action once for each supplied path @@ -164,6 +207,7 @@ BOOL CAlfrescoApp::InitInstance() } else { AfxMessageBox( L"Not a valid Alfresco CIFS folder", MB_OK | MB_ICONSTOP); + DBGOUT_TS << "Error, not a valid Alfresco CIFS folder, " << folderPath << endl; return 1; } @@ -198,6 +242,10 @@ bool CAlfrescoApp::buildDesktopParameters( AlfrescoInterface& alfresco, StringLi String curFile = paths.getStringAt( i); + // DEBUG + + DBGOUT_TS << "Parameter: " << curFile << endl; + // Check if the path is on an Alfresco mapped drive if ( alfresco.isMappedDrive() && curFile.startsWithIgnoreCase( alfresco.getDrivePath())) { @@ -223,10 +271,12 @@ bool CAlfrescoApp::buildDesktopParameters( AlfrescoInterface& alfresco, StringLi if ( isDir && actionInfo.supportsFolders() == false) { AfxMessageBox(L"Action does not support folders", MB_OK | MB_ICONSTOP); + DBGOUT_TS << "Error, action does not support folders" << endl; return false; } else if ( actionInfo.supportsFiles() == false) { AfxMessageBox(L"Action does not support files", MB_OK | MB_ICONSTOP); + DBGOUT_TS << "Error, action does not support files" << endl; return false; } @@ -244,10 +294,12 @@ bool CAlfrescoApp::buildDesktopParameters( AlfrescoInterface& alfresco, StringLi if ( isDir == false && actionInfo.hasAttribute(AttrClientFiles) == false) { AfxMessageBox(L"Action does not support local files", MB_OK | MB_ICONSTOP); + DBGOUT_TS << "Error, action does not support local files" << endl; return false; } else if ( isDir == true && actionInfo.hasAttribute(AttrClientFolders) == false) { AfxMessageBox(L"Action does not support local folders", MB_OK | MB_ICONSTOP); + DBGOUT_TS << "Error, action does not support local folders" << endl; return false; } @@ -260,10 +312,12 @@ bool CAlfrescoApp::buildDesktopParameters( AlfrescoInterface& alfresco, StringLi if ( fInfo->getLockType() != LockNone) { AfxMessageBox( L"Cannot copy file to Alfresco folder, destination file is locked", MB_OK | MB_ICONEXCLAMATION); + DBGOUT_TS << "Error, cannot copy to Alfresco folder, destination file is locked" << endl; return false; } else if ( actionInfo.hasPreProcessAction(PreLocalToWorkingCopy) == true && fInfo->isWorkingCopy() == false) { AfxMessageBox( L"Cannot copy to Alfresco folder, destination must overwrite a working copy", MB_OK | MB_ICONEXCLAMATION); + DBGOUT_TS << "Error, cannot copy to Alfresco folder, destination must overwrite a working copy" << endl; return false; } } @@ -274,6 +328,7 @@ bool CAlfrescoApp::buildDesktopParameters( AlfrescoInterface& alfresco, StringLi CString msg; msg.FormatMessage( L"No matching working copy for %1", curName.data()); AfxMessageBox( msg, MB_OK | MB_ICONEXCLAMATION); + DBGOUT_TS << "Error, no matching working copy for " << curName << endl; return false; } @@ -293,6 +348,7 @@ bool CAlfrescoApp::buildDesktopParameters( AlfrescoInterface& alfresco, StringLi msg.FormatMessage( isDir ? L"Failed to copy folder %1" : L"Failed to copy file %1", curFile.data()); AfxMessageBox( msg, MB_OK | MB_ICONSTOP); + DBGOUT_TS << "Error, copy failed for " << curName << endl; return false; } else { @@ -302,10 +358,15 @@ bool CAlfrescoApp::buildDesktopParameters( AlfrescoInterface& alfresco, StringLi CString msg; msg.FormatMessage( L"Copy aborted for %1", curFile.data()); AfxMessageBox( msg, MB_OK | MB_ICONSTOP); + DBGOUT_TS << "Error, copy aborted by user, " << curName << endl; return false; } } + // DEBUG + + DBGOUT_TS << "Added target " << curName << endl; + // Add a desktop target for the copied file params.addTarget( new DesktopTarget(isDir ? TargetCopiedFolder : TargetCopiedFile, curName)); @@ -355,6 +416,7 @@ bool CAlfrescoApp::buildDesktopParameters( AlfrescoInterface& alfresco, StringLi msg.FormatMessage( isDir ? L"Failed to copy folder %1" : L"Failed to copy file %1", curFile.data()); AfxMessageBox( msg, MB_OK | MB_ICONSTOP); + DBGOUT_TS << "Error, copy failed for " << curName << endl; return false; } else { @@ -364,6 +426,7 @@ bool CAlfrescoApp::buildDesktopParameters( AlfrescoInterface& alfresco, StringLi CString msg; msg.FormatMessage( L"Copy aborted for %1", curFile.data()); AfxMessageBox( msg, MB_OK | MB_ICONSTOP); + DBGOUT_TS << "Error, copy aborted for " << curName << endl; return false; } } @@ -381,6 +444,10 @@ bool CAlfrescoApp::buildDesktopParameters( AlfrescoInterface& alfresco, StringLi } } + // DEBUG + + DBGOUT_TS << "Added target " << pTarget->getTarget() << endl; + // Add the desktop target params.addTarget( pTarget); @@ -403,6 +470,10 @@ bool CAlfrescoApp::buildDesktopParameters( AlfrescoInterface& alfresco, StringLi */ bool CAlfrescoApp::copyFilesUsingShell(const String& fromFileFolder, const String& toFolder, bool& aborted) { + // DEBUG + + DBGOUT_TS << "Copy from " << fromFileFolder << " to " << toFolder << endl; + // Build the from/to paths, must be double null terminated wchar_t fromPath[MAX_PATH + 1]; @@ -469,6 +540,7 @@ bool CAlfrescoApp::runAction( AlfrescoInterface& alfresco, StringList& pathList, if ( actionInfo.allowsNoParameters() == false && desktopParams.numberOfTargets() == 0) { AfxMessageBox( L"No parameters for action", MB_OK | MB_ICONEXCLAMATION); + DBGOUT_TS << "Error, no parameters for action" << endl; return false; } @@ -484,6 +556,10 @@ bool CAlfrescoApp::runAction( AlfrescoInterface& alfresco, StringList& pathList, if ( response.getStatus() == StsCommandLine) { + // DEBUG + + DBGOUT_TS << "Action returned command line, " << response.getStatusMessage() << endl; + // Launch a process using the command line sts = doCommandLine( alfresco, response.getStatusMessage()); @@ -493,6 +569,10 @@ bool CAlfrescoApp::runAction( AlfrescoInterface& alfresco, StringList& pathList, else if ( response.getStatus() == StsLaunchURL) { + // DEBUG + + DBGOUT_TS << "Action returned URL, " << response.getStatusMessage() << endl; + // Use the Windows shell to open the URL sts = doURL( alfresco, response.getStatusMessage()); @@ -534,6 +614,8 @@ bool CAlfrescoApp::runAction( AlfrescoInterface& alfresco, StringList& pathList, msg = errMsg.data(); AfxMessageBox( msg, MB_OK | MB_ICONERROR); + + DBGOUT_TS << "Action returned error status, " << msg << endl; } } else if ( response.hasStatusMessage()) { @@ -543,6 +625,8 @@ bool CAlfrescoApp::runAction( AlfrescoInterface& alfresco, StringList& pathList, CString msg; msg.FormatMessage( L"Action returned message\n\n%1", response.getStatusMessage().data()); AfxMessageBox( msg, MB_OK | MB_ICONINFORMATION); + + DBGOUT_TS << "Action returned error message, " << msg << endl; } } @@ -684,6 +768,10 @@ bool CAlfrescoApp::doCommandLine( AlfrescoInterface& alfresco, const String& cmd CString msg; msg.FormatMessage( L"Failed to launch command line\n\n%1\n\nError %2!d!", cmdLine.data(), GetLastError()); AfxMessageBox( msg, MB_OK | MB_ICONERROR); + + // DEBUG + + DBGOUT_TS << "Error, failed to launch command line, status " << GetLastError() << endl; } else sts = true; diff --git a/source/cpp/CAlfrescoApp/includes/util/Debug.h b/source/cpp/CAlfrescoApp/includes/util/Debug.h new file mode 100755 index 0000000000..b2990e5b7b --- /dev/null +++ b/source/cpp/CAlfrescoApp/includes/util/Debug.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2005-2006 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ + +#ifndef _Debug_H +#define _Debug_H + +// Includes + +#include "util\String.h" +#include +#include + +// Classes defined in this header file + +namespace Alfresco { + class Debug; + + // Macro to access to the debug output stream + + #define HAS_DEBUG Debug::hasOutputStream() == true + #define DBGOUT if ( Debug::hasOutputStream()) Debug::getOutputStream() + #define TIMESTAMP Debug::timeStamp(); + #define DBGOUT_TS TIMESTAMP DBGOUT +} + +/** + * Debug Logging Class + * + * Outputs debugging information to a file on the local filesystem. + */ +class Alfresco::Debug { +public: + + // Open/close the debug log + + static void openLog( const char* logName, bool append = true); + static void closeLog( void); + + // Check if the output stream is valid, return the output stream + + static bool hasOutputStream( void) { return _debugOut.is_open() ? true : false; } + static std::ofstream& getOutputStream( void) { return _debugOut; } + + // Output a timestamp to the debug log + + static void timeStamp( void); + +private: + // Debug output log file + + static std::ofstream _debugOut; + +private: + // Hide constructors, static only class + + Debug( void) {}; + Debug( const Debug& dbg) {}; +}; + +#endif \ No newline at end of file diff --git a/source/cpp/CAlfrescoApp/source/util/Debug.cpp b/source/cpp/CAlfrescoApp/source/util/Debug.cpp new file mode 100755 index 0000000000..2b793b6088 --- /dev/null +++ b/source/cpp/CAlfrescoApp/source/util/Debug.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2005-2006 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ + +#include +#include "util\Debug.h" + +using namespace std; +using namespace Alfresco; + +// Global debug output stream + +ofstream Debug::_debugOut; + +/** + * Open the debug output file + * + * @param logName const char* + * @param append bool + */ +void Debug::openLog(const char *logName, bool append) { + + // Check if the log is already open + + if ( Debug::hasOutputStream()) + Debug::closeLog(); + + // Open the debug log file + + unsigned int openMode = append ? ios::app : ios::out; + _debugOut.open( logName, openMode); +} + +/** + * Close the debug output file + */ +void Debug::closeLog( void) { + + // Close the debug log + + if ( Debug::hasOutputStream()) { + + // Close the debug log file + + _debugOut.close(); + } +} + +/** + * Output the current date/time to the debug log + */ +void Debug::timeStamp( void) { + + if ( Debug::hasOutputStream()) { + + // Get the time in seconds and convert to a structure + + char timeBuf[32]; + __time32_t timeNow; + struct tm timeTm; + + _time32( &timeNow); + _localtime32_s( &timeTm, &timeNow); + + sprintf_s( timeBuf, sizeof( timeBuf), "%02d/%02d/%04d %02d:%02d:%02d ", + timeTm.tm_mday, timeTm.tm_mon, timeTm.tm_year + 1900, timeTm.tm_hour, timeTm.tm_min, timeTm.tm_sec); + Debug::getOutputStream() << timeBuf; + } +} + diff --git a/source/java/org/alfresco/filesys/ftp/FTPCommand.java b/source/java/org/alfresco/filesys/ftp/FTPCommand.java index ab39eea915..8eeb859e32 100644 --- a/source/java/org/alfresco/filesys/ftp/FTPCommand.java +++ b/source/java/org/alfresco/filesys/ftp/FTPCommand.java @@ -68,8 +68,10 @@ public class FTPCommand public final static int XRmd = 39; public final static int XCup = 40; public final static int XCwd = 41; + public final static int MLst = 42; + public final static int MLsd = 43; - public final static int MaxId = 41; + public final static int MaxId = 43; public final static int InvalidCmd = -1; @@ -78,7 +80,7 @@ public class FTPCommand private static final String[] _cmds = { "USER", "PASS", "ACCT", "CWD", "CDUP", "SMNT", "REIN", "QUIT", "PORT", "PASV", "TYPE", "STRU", "MODE", "RETR", "STOR", "STOU", "APPE", "ALLO", "REST", "RNFR", "RNTO", "ABOR", "DELE", "RMD", "MKD", "PWD", "LIST", "NLST", "SITE", "SYST", "STAT", "HELP", "NOOP", "MDTM", "SIZE", - "OPTS", "FEAT", "XPWD", "XMKD", "XRMD", "XCUP", "XCWD" }; + "OPTS", "FEAT", "XPWD", "XMKD", "XRMD", "XCUP", "XCWD", "MLST", "MLSD" }; /** * Convert an FTP command to an id diff --git a/source/java/org/alfresco/filesys/ftp/FTPDate.java b/source/java/org/alfresco/filesys/ftp/FTPDate.java index c43a561e67..7e0f4edead 100644 --- a/source/java/org/alfresco/filesys/ftp/FTPDate.java +++ b/source/java/org/alfresco/filesys/ftp/FTPDate.java @@ -16,6 +16,7 @@ */ package org.alfresco.filesys.ftp; +import java.text.SimpleDateFormat; import java.util.*; /** @@ -37,6 +38,11 @@ public class FTPDate protected final static String[] _months = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + // Machine listing date/time formatters + + protected final static SimpleDateFormat _mlstFormat = new SimpleDateFormat( "yyyyMMddHHmmss"); + protected final static SimpleDateFormat _mlstFormatLong = new SimpleDateFormat( "yyyyMMddHHmmss.SSS"); + /** * Pack a date string in Unix format The format is 'Mmm dd hh:mm' if the file is less than six * months old, else the format is 'Mmm dd yyyy'. @@ -104,4 +110,26 @@ public class FTPDate buf.append(min); } } + + /** + * Return a machine listing date/time, in the format 'YYYYMMDDHHSS'. + * + * @param dateTime long + * @return String + */ + public final static String packMlstDateTime( long dateTime) + { + return _mlstFormat.format( new Date( dateTime)); + } + + /** + * Return a machine listing date/time, in the format 'YYYYMMDDHHSS.sss'. + * + * @param dateTime long + * @return String + */ + public final static String packMlstDateTimeLong( long dateTime) + { + return _mlstFormatLong.format( new Date( dateTime)); + } } diff --git a/source/java/org/alfresco/filesys/ftp/FTPNetworkServer.java b/source/java/org/alfresco/filesys/ftp/FTPNetworkServer.java index d13bd4bb71..3e9865fbb0 100644 --- a/source/java/org/alfresco/filesys/ftp/FTPNetworkServer.java +++ b/source/java/org/alfresco/filesys/ftp/FTPNetworkServer.java @@ -21,6 +21,7 @@ import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; +import java.nio.charset.Charset; import java.util.Enumeration; import org.alfresco.filesys.server.ServerListener; @@ -46,8 +47,8 @@ public class FTPNetworkServer extends NetworkFileServer implements Runnable // Constants // - // Session Thread group + private static final ThreadGroup THREAD_GROUP_SESSION = new ThreadGroup("FTP_SESSION_GROUP"); // Listen backlog for the server socket @@ -86,6 +87,14 @@ public class FTPNetworkServer extends NetworkFileServer implements Runnable private String m_localFTPaddress; + // SITE command interface + + private FTPSiteInterface m_siteInterface; + + // Default character encoding to use for file names + + private String m_charSet; + /** * Class constructor * @@ -122,6 +131,12 @@ public class FTPNetworkServer extends NetworkFileServer implements Runnable logger.error(ex); } } + + // Set the default character set + + m_charSet = config.getFTPCharacterSet(); + if ( m_charSet == null) + m_charSet = Charset.defaultCharset().name(); } /** @@ -333,6 +348,16 @@ public class FTPNetworkServer extends NetworkFileServer implements Runnable return m_rootPath; } + /** + * Get the character set to use for file name encoding/decoding + * + * @return String + */ + public final String getCharacterSet() + { + return m_charSet; + } + /** * Notify the server that a user has logged on. * @@ -357,6 +382,8 @@ public class FTPNetworkServer extends NetworkFileServer implements Runnable if (logger.isDebugEnabled() && hasDebug()) { logger.debug("FTP Server starting on port " + getPort()); + if ( getCharacterSet() != null) + logger.debug( "Using character set " + getCharacterSet()); } // Create a server socket to listen for incoming FTP session requests @@ -582,4 +609,34 @@ public class FTPNetworkServer extends NetworkFileServer implements Runnable fireServerEvent(ServerListener.ServerStartup); } + + /** + * Check if the site interface is valid + * + * @return boolean + */ + public final boolean hasSiteInterface() + { + return m_siteInterface != null ? true : false; + } + + /** + * Return the site interface + * + * @return FTPSiteInterface + */ + public final FTPSiteInterface getSiteInterface() + { + return m_siteInterface; + } + + /** + * Set the site specific commands interface + * + * @param siteInterface FTPSiteInterface + */ + public final void setSiteInterface( FTPSiteInterface siteInterface) + { + m_siteInterface = siteInterface; + } } diff --git a/source/java/org/alfresco/filesys/ftp/FTPSiteInterface.java b/source/java/org/alfresco/filesys/ftp/FTPSiteInterface.java new file mode 100644 index 0000000000..c121cb99c8 --- /dev/null +++ b/source/java/org/alfresco/filesys/ftp/FTPSiteInterface.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ + +package org.alfresco.filesys.ftp; + +/** + * FTP SITE Command Interface + * + *

Optional interface that is used to provide processing for the FTP SITE command. + */ +public interface FTPSiteInterface { + + /** + * Process an FTP SITE specific command + * + * @param sess FTPSrvSession + * @param req FTPRequest + */ + void processFTPSiteCommand( FTPSrvSession sess, FTPRequest req); +} diff --git a/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java b/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java index db56f00801..540afa1106 100644 --- a/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java +++ b/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java @@ -26,9 +26,11 @@ import java.net.InetAddress; import java.net.Socket; import java.net.SocketException; import java.net.UnknownHostException; +import java.util.Calendar; import java.util.Date; import java.util.Enumeration; import java.util.StringTokenizer; +import java.util.TimeZone; import java.util.Vector; import javax.transaction.UserTransaction; @@ -57,6 +59,7 @@ import org.alfresco.filesys.server.filesys.SrvDiskInfo; import org.alfresco.filesys.server.filesys.TreeConnection; import org.alfresco.filesys.server.filesys.TreeConnectionHash; import org.alfresco.filesys.smb.server.repo.ContentContext; +import org.alfresco.filesys.util.HexDump; import org.alfresco.model.ContentModel; import org.alfresco.repo.security.authentication.AuthenticationComponent; import org.alfresco.service.cmr.repository.NodeRef; @@ -105,6 +108,13 @@ public class FTPSrvSession extends SrvSession implements Runnable public static final int DBG_DIRECTORY = 0x00000200; // Directory commands + // Enabled features + + protected static boolean FeatureUTF8 = false; + protected static boolean FeatureMDTM = true; + protected static boolean FeatureSIZE = true; + protected static boolean FeatureMLST = true; + // Anonymous user name private static final String USER_ANONYMOUS = "anonymous"; @@ -133,8 +143,36 @@ public class FTPSrvSession extends SrvSession implements Runnable // LIST command options - protected final static String LIST_OPTION_HIDDEN = "-a"; + protected final static String LIST_OPTION_PREFIX = "-"; + protected final static char LIST_OPTION_HIDDEN = 'a'; + + // Machine listing fact ids + + protected static final int MLST_SIZE = 0x0001; + protected static final int MLST_MODIFY = 0x0002; + protected static final int MLST_CREATE = 0x0004; + protected static final int MLST_TYPE = 0x0008; + protected static final int MLST_UNIQUE = 0x0010; + protected static final int MLST_PERM = 0x0020; + protected static final int MLST_MEDIATYPE = 0x0040; + + // Default fact list to use for machine listing commands + + protected static final int MLST_DEFAULT = MLST_SIZE + MLST_MODIFY + MLST_CREATE + MLST_TYPE + MLST_UNIQUE + MLST_PERM + MLST_MEDIATYPE; + + // Machine listing fact names + + protected static final String _factNames[] = { "size", "modify", "create", "type", "unique", "perm", "media-type" }; + + // MLSD buffer size to allocate + + protected static final int MLSD_BUFFER_SIZE = 4096; + + // Modify date/time minimum date/time argument length + + protected static final int MDTM_DATETIME_MINLEN = 14; // YYYYMMDDHHMMSS + // Flag to control whether data transfers use a seperate thread private static boolean UseThreadedDataTransfer = false; @@ -145,12 +183,12 @@ public class FTPSrvSession extends SrvSession implements Runnable // Input/output streams to remote client - private InputStreamReader m_in; - - private char[] m_inbuf; + private InputStream m_in; + private byte[] m_inbuf; +// private InputStreamReader m_in; +// private char[] m_inbuf; private OutputStreamWriter m_out; - private StringBuffer m_outbuf; // Data connection @@ -171,6 +209,14 @@ public class FTPSrvSession extends SrvSession implements Runnable private long m_restartPos = 0; + // Flag to indicate if UTF-8 paths are enabled + + private boolean m_utf8Paths = false; + + // Machine listing fact list + + private int m_mlstFacts = MLST_DEFAULT; + // Rename from path details private FTPPath m_renameFrom; @@ -183,6 +229,26 @@ public class FTPSrvSession extends SrvSession implements Runnable private TreeConnectionHash m_connections; + /** + * Static initializer + */ + static + { + try + { + // Check if the sun.text classes are available for UTF-8 conversion + + Class.forName( "sun.text.Normalizer"); + + // Enable UTF-8 support + + FeatureUTF8 = true; + } + catch ( Exception ex) + { + } + } + /** * Class constructor * @@ -338,6 +404,16 @@ public class FTPSrvSession extends SrvSession implements Runnable return m_cwd != null ? true : false; } + /** + * Check if UTF-8 filenames are enabled + * + * @return boolean + */ + public final boolean isUTF8Enabled() + { + return m_utf8Paths; + } + /** * Set the default path for the session * @@ -388,7 +464,7 @@ public class FTPSrvSession extends SrvSession implements Runnable // Convert the path to an FTP format path - String path = convertToFTPSeperators(req.getArgument()); + String path = convertToFTPSeperators( req.getArgument()); // Check if the path is the root directory and there is a default root // path configured @@ -1205,23 +1281,28 @@ public class FTPSrvSession extends SrvSession implements Runnable boolean hidden = false; - if (req.hasArgument() && req.getArgument().startsWith(LIST_OPTION_HIDDEN)) + if (req.hasArgument() && req.getArgument().startsWith(LIST_OPTION_PREFIX)) { - // Indicate that we want hidden files in the listing - - hidden = true; - - // Remove the option from the command argument, and update the - // request - - String arg = req.getArgument(); - int pos = arg.indexOf(" "); - if (pos > 0) - arg = arg.substring(pos + 1); - else - arg = null; - - req.updateArgument(arg); + // We only support the hidden files option + + String arg = req.getArgument(); + if ( arg.indexOf( LIST_OPTION_HIDDEN) != -1) + { + // Indicate that we want hidden files in the listing + + hidden = true; + } + + // Remove the option from the command argument, and update the + // request + + int pos = arg.indexOf(" "); + if (pos > 0) + arg = arg.substring(pos + 1); + else + arg = null; + + req.updateArgument(arg); } // Create the path for the file listing @@ -1294,7 +1375,10 @@ public class FTPSrvSession extends SrvSession implements Runnable // Open an output stream to the client - dataWrt = new OutputStreamWriter(dataSock.getOutputStream()); + if ( isUTF8Enabled()) + dataWrt = new OutputStreamWriter(dataSock.getOutputStream(), "UTF-8"); + else + dataWrt = new OutputStreamWriter(dataSock.getOutputStream()); // Check if a path has been specified to list @@ -1472,7 +1556,10 @@ public class FTPSrvSession extends SrvSession implements Runnable // Open an output stream to the client - dataWrt = new OutputStreamWriter(dataSock.getOutputStream()); + if ( isUTF8Enabled()) + dataWrt = new OutputStreamWriter(dataSock.getOutputStream(), "UTF-8"); + else + dataWrt = new OutputStreamWriter(dataSock.getOutputStream()); // Check if a path has been specified to list @@ -1593,11 +1680,177 @@ public class FTPSrvSession extends SrvSession implements Runnable } /** - * Process a quit command - * - * @param req FTPRequest - * @exception IOException - */ + * Process an options request + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procOptions(FTPRequest req) throws IOException { + + // Check if the user is logged in + + if (isLoggedOn() == false) + { + sendFTPResponse(500, ""); + return; + } + + // Check if the parameter has been specified + + if (req.hasArgument() == false) + { + sendFTPResponse(501, "Required argument missing"); + return; + } + + // Parse the argument to get the sub-command and arguments + + StringTokenizer token = new StringTokenizer(req.getArgument(), " "); + if (token.hasMoreTokens() == false) + { + sendFTPResponse(501, "Invalid argument"); + return; + } + + // Get the sub-command + + String optsCmd = token.nextToken(); + + // UTF8 enable/disable command + + if (FeatureUTF8 && optsCmd.equalsIgnoreCase("UTF8")) + { + + // Get the next argument + + if (token.hasMoreTokens()) + { + String optsArg = token.nextToken(); + if (optsArg.equalsIgnoreCase("ON")) + { + + // Enable UTF-8 file names + + m_utf8Paths = true; + } + else if (optsArg.equalsIgnoreCase("OFF")) + { + + // Disable UTF-8 file names + + m_utf8Paths = false; + } + else + { + + // Invalid argument + + sendFTPResponse(501, "OPTS UTF8 Invalid argument"); + return; + } + + // Report the new setting back to the client + + sendFTPResponse(200, "OPTS UTF8 " + (isUTF8Enabled() ? "ON" : "OFF")); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILE)) + logger.debug("UTF8 options utf8=" + (isUTF8Enabled() ? "ON" : "OFF")); + } + } + + // MLST/MLSD fact list command + + else if (FeatureMLST && optsCmd.equalsIgnoreCase("MLST")) + { + + // Check if the fact list argument is valid + + if (token.hasMoreTokens() == false) + { + + // Invalid fact list argument + + sendFTPResponse(501, "OPTS MLST Invalid argument"); + return; + } + + // Parse the supplied fact names + + int mlstFacts = 0; + StringTokenizer factTokens = new StringTokenizer(token.nextToken(), + ";"); + StringBuffer factStr = new StringBuffer(); + + while (factTokens.hasMoreTokens()) + { + // Get the current fact name and validate + + String factName = factTokens.nextToken(); + int factIdx = -1; + int idx = 0; + + while (idx < _factNames.length && factIdx == -1) + { + if (_factNames[idx].equalsIgnoreCase(factName)) + factIdx = idx; + else + idx++; + } + + // Check if the fact name is valid, ignore invalid names + + if (factIdx != -1) + { + // Add the fact name to the reply tring + + factStr.append(_factNames[factIdx]); + factStr.append(";"); + + // Add the fact to the fact bit mask + + mlstFacts += (1 << factIdx); + } + } + + // check if any valid fact names were found + + if (mlstFacts == 0) + { + sendFTPResponse(501, "OPTS MLST Invalid Argument"); + return; + } + + // Update the MLST enabled fact list for this session + + m_mlstFacts = mlstFacts; + + // Send the response + + sendFTPResponse(200, "MLST OPTS " + factStr.toString()); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_SEARCH)) + logger.debug("MLst options facts=" + factStr.toString()); + } + else + { + // Invalid options command, or feature not enabled + + sendFTPResponse(501, "Invalid options commands"); + } + } + + /** + * Process a quit command + * + * @param req + * FTPRequest + * @exception IOException + */ protected final void procQuit(FTPRequest req) throws IOException { @@ -1616,11 +1869,12 @@ public class FTPSrvSession extends SrvSession implements Runnable } /** - * Process a type command - * - * @param req FTPRequest - * @exception IOException - */ + * Process a type command + * + * @param req + * FTPRequest + * @exception IOException + */ protected final void procType(FTPRequest req) throws IOException { @@ -1984,9 +2238,10 @@ public class FTPSrvSession extends SrvSession implements Runnable * Process a store file command * * @param req FTPRequest + * @param append boolean * @exception IOException */ - protected final void procStoreFile(FTPRequest req) throws IOException + protected final void procStoreFile(FTPRequest req, boolean append) throws IOException { // Check if the user is logged in @@ -2072,9 +2327,11 @@ public class FTPSrvSession extends SrvSession implements Runnable // Create the file open parameters - FileOpenParams params = new FileOpenParams(ftpPath.getSharePath(), - sts == FileStatus.FileExists ? FileAction.TruncateExisting : FileAction.CreateNotExist, - AccessMode.ReadWrite, 0); + int openAction = FileAction.CreateNotExist; + if ( sts == FileStatus.FileExists) + openAction = append == false ? FileAction.TruncateExisting : FileAction.OpenIfExists; + + FileOpenParams params = new FileOpenParams(ftpPath.getSharePath(), openAction, AccessMode.ReadWrite, 0); // Create a new file to receive the data @@ -2163,6 +2420,11 @@ public class FTPSrvSession extends SrvSession implements Runnable long filePos = 0; int len = is.read(buf, 0, buf.length); + // If the data is to be appended then set the starting file position to the end of the file + + if ( append == true) + filePos = netFile.getFileSize(); + while (len > 0) { @@ -2775,9 +3037,7 @@ public class FTPSrvSession extends SrvSession implements Runnable } // Check if the path is the root directory, cannot delete directories - // from the root - // directory - // as it maps to the list of available disk shares. + // from the root directory as it maps to the list of available disk shares. if (ftpPath.isRootPath() || ftpPath.isRootSharePath()) { @@ -2820,8 +3080,7 @@ public class FTPSrvSession extends SrvSession implements Runnable disk.deleteDirectory(this, tree, ftpPath.getSharePath()); - // Check if there are any file/directory change notify requests - // active + // Check if there are any file/directory change notify requests active DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext(); if (diskCtx.hasChangeHandler()) @@ -2857,48 +3116,474 @@ public class FTPSrvSession extends SrvSession implements Runnable } /** - * Process a modify date/time command - * - * @param req FTPRequest - * @exception IOException - */ - protected final void procModifyDateTime(FTPRequest req) throws IOException - { + * Process a machine listing request, single folder + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procMachineListing(FTPRequest req) throws IOException { - // Return a success response + // Check if the user is logged in - sendFTPResponse(550, "Not implemented yet"); - } + if (isLoggedOn() == false) { + sendFTPResponse(500, "Not logged in"); + return; + } - /** - * Process a features command - * - * @param req FTPRequest - * @exception IOException - */ - protected final void procFeatures(FTPRequest req) throws IOException - { - // Check if the user is logged in + // Check if an argument has been specified - if (isLoggedOn() == false) - { - sendFTPResponse(500, ""); - return; - } + if (req.hasArgument() == false) { + sendFTPResponse(501, "Syntax error, parameter required"); + return; + } - // Send back the list of features supported by this FTP server - - sendFTPResponse( 211, "Features"); - sendFTPResponse( "SIZE"); - sendFTPResponse( 211, "End"); - } + // Create the path to be listed + + FTPPath ftpPath = generatePathForRequest(req, false, true); + if (ftpPath == null) { + sendFTPResponse(500, "Invalid path"); + return; + } + + // Get the file information + + DiskInterface disk = null; + TreeConnection tree = null; + + try { + + // Create a temporary tree connection + + tree = getTreeConnection(ftpPath.getSharedDevice()); + + // Access the virtual filesystem driver + + disk = (DiskInterface) ftpPath.getSharedDevice().getInterface(); + + // Get the file information + + FileInfo finfo = disk.getFileInformation(this, tree, ftpPath + .getSharePath()); + + if (finfo == null) { + sendFTPResponse(550, "Path " + req.getArgument() + " not available"); + return; + } else if (finfo.isDirectory() == false) { + sendFTPResponse(501, "Path " + req.getArgument() + " is not a directory"); + return; + } + + // Return the folder details + + sendFTPResponse("250- Listing " + req.getArgument()); + + StringBuffer mlstStr = new StringBuffer(80); + mlstStr.append(" "); + + generateMlstString(finfo, m_mlstFacts, mlstStr, true); + mlstStr.append(CRLF); + + sendFTPResponse(mlstStr.toString()); + sendFTPResponse("250 End"); + + // DEBUG + + if ( logger.isDebugEnabled() && hasDebug(DBG_FILE)) + logger.debug("Mlst ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", info=" + finfo); + } catch (Exception ex) { + sendFTPResponse(550, "Error retrieving file information"); + } + } + + /** + * Process a machine listing request, folder contents + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procMachineListingContents(FTPRequest req) + throws IOException { + + // Check if the user is logged in + + if (isLoggedOn() == false) { + sendFTPResponse(500, ""); + return; + } + + // Check if the request has an argument, if not then use the current + // working directory + + if (req.hasArgument() == false) + req.updateArgument("."); + + // Create the path for the file listing + + FTPPath ftpPath = m_cwd; + if (req.hasArgument()) + ftpPath = generatePathForRequest(req, true); + + if (ftpPath == null) { + sendFTPResponse(500, "Invalid path"); + return; + } + + // Check if the session has the required access + + if (ftpPath.isRootPath() == false) { + + // Check if the session has access to the filesystem + + TreeConnection tree = getTreeConnection(ftpPath.getSharedDevice()); + if (tree == null || tree.hasReadAccess() == false) { + + // Session does not have access to the filesystem + + sendFTPResponse(550, "Access denied"); + return; + } + } + + // Send the intermediate response + + sendFTPResponse(150, "File status okay, about to open data connection"); + + // Check if there is an active data session + + if (m_dataSess == null) { + sendFTPResponse(425, "Can't open data connection"); + return; + } + + // Get the data connection socket + + Socket dataSock = null; + + try { + dataSock = m_dataSess.getSocket(); + } catch (Exception ex) { + logger.error(ex); + } + + if (dataSock == null) { + sendFTPResponse(426, "Connection closed; transfer aborted"); + return; + } + + // Output the directory listing to the client + + Writer dataWrt = null; + + try { + + // Open an output stream to the client + + if ( isUTF8Enabled()) + dataWrt = new OutputStreamWriter(dataSock.getOutputStream(), "UTF-8"); + else + dataWrt = new OutputStreamWriter(dataSock.getOutputStream()); + + // Get a list of file information objects for the current directory + + Vector files = null; + + files = listFilesForPath(ftpPath, false, false); + + // Output the file list to the client + + if (files != null) { + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_SEARCH)) + logger.debug("MLsd found " + files.size() + " files in " + ftpPath.getFTPPath()); + + // Output the file information to the client + + StringBuffer str = new StringBuffer(MLSD_BUFFER_SIZE); + + for (int i = 0; i < files.size(); i++) { + + // Get the current file information + + FileInfo finfo = (FileInfo) files.elementAt(i); + + generateMlstString(finfo, m_mlstFacts, str, false); + str.append(CRLF); + + // Output the file information record when the buffer is + // full + + if (str.length() >= MLSD_BUFFER_SIZE) { + + // Output the file data records + + dataWrt.write(str.toString()); + + // Reset the buffer + + str.setLength(0); + } + } + + // Flush any remaining file record data + + if (str.length() > 0) + dataWrt.write(str.toString()); + } + + // End of file list transmission + + sendFTPResponse(226, "Closing data connection"); + } catch (Exception ex) { + + // Failed to send file listing + + sendFTPResponse(451, "Error reading file list"); + } finally { + + // Close the data stream to the client + + if (dataWrt != null) + dataWrt.close(); + + // Close the data connection to the client + + if (m_dataSess != null) { + getFTPServer().releaseDataSession(m_dataSess); + m_dataSess = null; + } + } + } + + /** + * Process a modify date/time command + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procModifyDateTime(FTPRequest req) throws IOException { + + // Check if the user is logged in + + if (isLoggedOn() == false) { + sendFTPResponse(500, ""); + return; + } + + // Check if an argument has been specified + + if (req.hasArgument() == false) { + sendFTPResponse(501, "Syntax error, parameter required"); + return; + } + + // Check the format of the argument to detemine if this is a get or set + // modify date/time request + // + // Get format is just the filename/path + // Set format is YYYYMMDDHHMMSS + + String path = req.getArgument(); + long modifyDateTime = 0L; + + if (path.length() > MDTM_DATETIME_MINLEN && path.indexOf(' ') != -1) { + + // Check if the first argument looks like a date/time value + + boolean settime = true; + for (int i = 0; i < MDTM_DATETIME_MINLEN; i++) { + if (Character.isDigit(path.charAt(i)) == false) + settime = false; + } + + // Looks like a date/time value + + if (settime == true) { + + try { + + // Parse the various fields + + int year = Integer.valueOf(path.substring(0, 4)).intValue(); + int month = Integer.valueOf(path.substring(4, 6)).intValue(); + int day = Integer.valueOf(path.substring(6, 8)).intValue(); + + int hours = Integer.valueOf(path.substring(8, 10)).intValue(); + int mins = Integer.valueOf(path.substring(10, 12)).intValue(); + int secs = Integer.valueOf(path.substring(12, 14)).intValue(); + + // Check if the date/time includes milliseconds + + int millis = 0; + int sep = path.indexOf(' ', MDTM_DATETIME_MINLEN); + + if (path.charAt(MDTM_DATETIME_MINLEN) == '.') { + + // Find the seperator between the date/time and path + + millis = Integer.valueOf(path.substring(MDTM_DATETIME_MINLEN + 1, sep)) + .intValue(); + } + + // Create the modify date/time + + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + + cal.set(year, month, day, hours, mins, secs); + if (millis != 0) + cal.set(Calendar.MILLISECOND, millis); + + // Get the modify date/time + + modifyDateTime = cal.getTimeInMillis(); + + // Remove the date/time from the request argument + + path = path.substring(sep + 1); + req.updateArgument(path); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILE)) + logger.debug("Modify date/time arg=" + path + ", utcTime=" + modifyDateTime); + } catch (NumberFormatException ex) { + } + } + } + + // Create the path for the file listing + + FTPPath ftpPath = generatePathForRequest(req, true); + if (ftpPath == null) { + sendFTPResponse(550, "Invalid path"); + return; + } + + // Get the file information + + DiskInterface disk = null; + TreeConnection tree = null; + + try { + + // Create a temporary tree connection + + tree = getTreeConnection(ftpPath.getSharedDevice()); + + // Access the virtual filesystem driver + + disk = (DiskInterface) ftpPath.getSharedDevice().getInterface(); + + // Check if the modify date/time should be set + + if (modifyDateTime != 0L) { + + // Set the file/folder modification date/time + + FileInfo finfo = new FileInfo(); + finfo.setModifyDateTime(modifyDateTime); + finfo.setFileInformationFlags(FileInfo.SetModifyDate); + + disk.setFileInformation(this, tree, ftpPath.getSharePath(), + finfo); + } + + // Get the file information + + FileInfo finfo = disk.getFileInformation(this, tree, ftpPath + .getSharePath()); + + if (finfo == null) { + sendFTPResponse(550, "File " + req.getArgument() + + " not available"); + return; + } + + // Return the file modification date/time + + if (finfo.hasModifyDateTime()) + sendFTPResponse(213, FTPDate.packMlstDateTime(finfo + .getModifyDateTime())); + else + sendFTPResponse(550, + "Modification date/time not available for " + + finfo.getFileName()); + + // DEBUG + + if (logger.isDebugEnabled() && hasDebug(DBG_FILE)) + logger.debug("File modify date/time ftp=" + + ftpPath.getFTPPath() + ", share=" + + ftpPath.getShareName() + ", modified=" + + finfo.getModifyDateTime()); + } catch (Exception ex) { + sendFTPResponse(550, "Error retrieving file modification date/time"); + } + } + + /** + * Process a server features request + * + * @param req + * FTPRequest + * @exception IOException + */ + protected final void procFeatures(FTPRequest req) throws IOException { + + // Return the list of supported server features + + sendFTPResponse("211-Features supported"); + + // MOdify date/time and size commands supported + + if ( FeatureMDTM) + sendFTPResponse(" MDTM"); + + if ( FeatureSIZE) + sendFTPResponse(" SIZE"); + + if ( FeatureUTF8) + sendFTPResponse(" UTF8"); + + // Machine listing supported, build the fact list + + if ( FeatureMLST) + { + StringBuffer mlstStr = new StringBuffer(); + + mlstStr.append(" MLST "); + + for (int i = 0; i < _factNames.length; i++) { + + // Output the fact name + + mlstStr.append(_factNames[i]); + + // Check if the fact is enabled by default + + if ((MLST_DEFAULT & (1 << i)) != 0) + mlstStr.append("*"); + mlstStr.append(";"); + } + + sendFTPResponse(mlstStr.toString()); + sendFTPResponse(" MLSD"); + } + + sendFTPResponse(211, "END"); + } /** - * Process a file size command - * - * @param req FTPRequest - * @exception IOException - */ + * Process a file size command + * + * @param req + * FTPRequest + * @exception IOException + */ protected final void procFileSize(FTPRequest req) throws IOException { @@ -2969,6 +3654,40 @@ public class FTPSrvSession extends SrvSession implements Runnable } } + /** + * Process a site specific command + * + * @param req FTPRequest + * @exception IOException + */ + protected final void procSite(FTPRequest req) + throws IOException + { + // Check if the user is logged in + + if (isLoggedOn() == false) { + sendFTPResponse(500, ""); + return; + } + + // Check if the FTP server has a site interface + + if ( getFTPServer().hasSiteInterface()) { + + // Pass the request to the site interface + + FTPSiteInterface siteInterface = getFTPServer().getSiteInterface(); + + siteInterface.processFTPSiteCommand( this, req); + } + else { + + // SITE command not implemented + + sendFTPResponse( 501, "SITE commands not implemented"); + } + } + /** * Process a structure command. This command is obsolete. * @@ -3379,10 +4098,10 @@ public class FTPSrvSession extends SrvSession implements Runnable // Create the input/output streams - m_in = new InputStreamReader(m_sock.getInputStream()); + m_in = m_sock.getInputStream(); m_out = new OutputStreamWriter(m_sock.getOutputStream()); - m_inbuf = new char[512]; + m_inbuf = new byte[512]; m_outbuf = new StringBuffer(256); // Return the initial response @@ -3427,10 +4146,21 @@ public class FTPSrvSession extends SrvSession implements Runnable rdlen--; } - // Get the command string + // Get the command string, decode as UTF-8 if enabled - cmd = new String(m_inbuf, 0, rdlen); + if ( isUTF8Enabled()) + { + // Convert the string from UTF-8 + + cmd = sun.text.Normalizer.compose( new String(m_inbuf, 0, rdlen, "UTF-8"), false, 0); + } + else + { + // Convert the request using the configured character set + cmd = new String(m_inbuf, 0, rdlen, getFTPServer().getCharacterSet()); + } + // Debug if (logger.isDebugEnabled() && hasDebug(DBG_TIMING)) @@ -3501,9 +4231,15 @@ public class FTPSrvSession extends SrvSession implements Runnable // Store file command case FTPCommand.Stor: - procStoreFile(ftpReq); + procStoreFile(ftpReq, false); break; + // Append file command + + case FTPCommand.Appe: + procStoreFile(ftpReq, true); + break; + // Print working directory command case FTPCommand.Pwd: @@ -3635,6 +4371,30 @@ public class FTPSrvSession extends SrvSession implements Runnable procFeatures(ftpReq); break; + // Options command + + case FTPCommand.Opts: + procOptions(ftpReq); + break; + + // Machine listing, single folder + + case FTPCommand.MLst: + procMachineListing(ftpReq); + break; + + // Machine listing, folder contents + + case FTPCommand.MLsd: + procMachineListingContents(ftpReq); + break; + + // Site specific commands + + case FTPCommand.Site: + procSite( ftpReq); + break; + // Unknown/unimplemented command default: @@ -3748,4 +4508,111 @@ public class FTPSrvSession extends SrvSession implements Runnable authService.authenticate( cInfo.getUserName(), cInfo.getPasswordAsCharArray()); } } + + /** + * Generate a machine listing string for the specified file/folder information + * + * @param finfo FileInfo + * @param mlstFlags int + * @param buf StringBuffer + * @param isMlsd boolean + */ + protected final void generateMlstString(FileInfo finfo, int mlstFlags, StringBuffer buf, boolean isMlsd) + { + // Create the machine listing record + + for (int i = 0; i < _factNames.length; i++) { + + // Check if the current fact is enabled + + int curFact = 1 << i; + + if ((mlstFlags & curFact) != 0) { + + // Output the fact value + + switch (curFact) { + + // File size + + case MLST_SIZE: + buf.append(_factNames[i]); + buf.append("="); + buf.append(finfo.getSize()); + buf.append(";"); + break; + + // Modify date/time + + case MLST_MODIFY: + if (finfo.hasModifyDateTime()) { + buf.append(_factNames[i]); + buf.append("="); + buf.append(FTPDate.packMlstDateTime(finfo + .getModifyDateTime())); + buf.append(";"); + } + break; + + // Creation date/time + + case MLST_CREATE: + if (finfo.hasCreationDateTime()) { + buf.append(_factNames[i]); + buf.append("="); + buf.append(FTPDate.packMlstDateTime(finfo + .getCreationDateTime())); + buf.append(";"); + } + break; + + // Type + + case MLST_TYPE: + buf.append(_factNames[i]); + + if (finfo.isDirectory() == false) { + buf.append("=file;"); + } else { + buf.append("=dir;"); + } + break; + + // Unique identifier + + case MLST_UNIQUE: + if (finfo.getFileId() != -1) { + buf.append(_factNames[i]); + buf.append("="); + buf.append(finfo.getFileId()); + buf.append(";"); + } + break; + + // Permissions + + case MLST_PERM: + buf.append(_factNames[i]); + buf.append("="); + if (finfo.isDirectory()) { + buf.append(finfo.isReadOnly() ? "el" : "ceflmp"); + } else { + buf.append(finfo.isReadOnly() ? "r" : "rwadf"); + } + buf.append(";"); + break; + + // Media-type + + case MLST_MEDIATYPE: + break; + } + } + } + + // Add the file name + + buf.append(" "); + buf.append(finfo.getFileName()); + } } \ No newline at end of file diff --git a/source/java/org/alfresco/filesys/server/config/ServerConfiguration.java b/source/java/org/alfresco/filesys/server/config/ServerConfiguration.java index 912f24acef..e78e98cb3c 100644 --- a/source/java/org/alfresco/filesys/server/config/ServerConfiguration.java +++ b/source/java/org/alfresco/filesys/server/config/ServerConfiguration.java @@ -23,6 +23,9 @@ import java.net.NetworkInterface; import java.net.Socket; import java.net.SocketException; import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.UnsupportedCharsetException; import java.security.Provider; import java.security.Security; import java.util.ArrayList; @@ -315,6 +318,10 @@ public class ServerConfiguration extends AbstractLifecycleBean private int m_ftpDebug; + // FTP character set + + private String m_ftpCharSet; + //-------------------------------------------------------------------------------- // NFS specific configuration parameters // @@ -1660,6 +1667,29 @@ public class ServerConfiguration extends AbstractLifecycleBean setFTPDebug(ftpDbg); } + + // Check if a character set has been specified + + elem = config.getConfigElement( "charSet"); + if ( elem != null) { + + try { + + // Validate the character set name + + Charset.forName( elem.getValue()); + + // Set the FTP character set + + setFTPCharacterSet( elem.getValue()); + } + catch ( IllegalCharsetNameException ex) { + throw new AlfrescoRuntimeException("Illegal character set name, " + elem.getValue()); + } + catch ( UnsupportedCharsetException ex) { + throw new AlfrescoRuntimeException("Unsupported character set name, " + elem.getValue()); + } + } } /** @@ -3639,6 +3669,16 @@ public class ServerConfiguration extends AbstractLifecycleBean m_ftpBindAddress = addr; } + /** + * Return the FTP character set + * + * @return String + */ + public final String getFTPCharacterSet() + { + return m_ftpCharSet; + } + /** * Set the FTP server port to use for incoming connections, -1 indicates disable the FTP server * @@ -3688,6 +3728,16 @@ public class ServerConfiguration extends AbstractLifecycleBean { m_ftpDebug = dbg; } + + /** + * Set the FTP character set + * + * @param charSet String + */ + public final void setFTPCharacterSet( String charSet) + { + m_ftpCharSet = charSet; + } /** * Check if the NFS server is enabled diff --git a/source/java/org/alfresco/filesys/smb/server/repo/desk/JavaScriptDesktopAction.java b/source/java/org/alfresco/filesys/smb/server/repo/desk/JavaScriptDesktopAction.java index c3d2bc20ec..b3036e4645 100644 --- a/source/java/org/alfresco/filesys/smb/server/repo/desk/JavaScriptDesktopAction.java +++ b/source/java/org/alfresco/filesys/smb/server/repo/desk/JavaScriptDesktopAction.java @@ -69,7 +69,7 @@ public class JavaScriptDesktopAction extends DesktopAction { */ public JavaScriptDesktopAction() { - super( DesktopAction.AttrAnyFiles, DesktopAction.PreConfirmAction + DesktopAction.PreCopyToTarget); + super( 0, 0); } /** @@ -209,30 +209,31 @@ public class JavaScriptDesktopAction extends DesktopAction { { // Check if the pre-process string is empty - if ( elem.getValue().length() == 0) - throw new DesktopActionException("Empty desktop action pre-processing flags"); - - // Parse the pre-process string - int pre = 0; - StringTokenizer tokens = new StringTokenizer( elem.getValue(), ","); - while ( tokens.hasMoreTokens()) + if ( elem.getValue() != null && elem.getValue().length() > 0) { - // Get the current pre-process token and validate + // Parse the pre-process string - String token = tokens.nextToken().trim(); + StringTokenizer tokens = new StringTokenizer( elem.getValue(), ","); - if ( token.equalsIgnoreCase( "copyToTarget")) - pre |= PreCopyToTarget; - else if ( token.equalsIgnoreCase( "confirm")) - pre |= PreConfirmAction; - else if ( token.equalsIgnoreCase( "localToWorkingCopy")) - pre |= PreLocalToWorkingCopy; - else - throw new DesktopActionException("Unknown pre-processing flag, " + token); + while ( tokens.hasMoreTokens()) + { + // Get the current pre-process token and validate + + String token = tokens.nextToken().trim(); + + if ( token.equalsIgnoreCase( "copyToTarget")) + pre |= PreCopyToTarget; + else if ( token.equalsIgnoreCase( "confirm")) + pre |= PreConfirmAction; + else if ( token.equalsIgnoreCase( "localToWorkingCopy")) + pre |= PreLocalToWorkingCopy; + else + throw new DesktopActionException("Unknown pre-processing flag, " + token); + } } - + // Set the action pre-processing flags setPreProcessActions( pre); diff --git a/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java b/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java index 646921d296..7c616bba95 100644 --- a/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java +++ b/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java @@ -645,12 +645,17 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest // control checks assertEquals("n6 not present", 1, countNodesByReference(n6Ref)); assertEquals("n8 not present", 1, countNodesByReference(n8Ref)); + assertTrue("n8 exists failure", nodeService.exists(n8Ref)); assertEquals("n6 primary parent association not present on n3", 1, countChildrenOfNode(n3Ref)); assertEquals("n6 secondary parent association not present on n4", 1, countChildrenOfNode(n4Ref)); assertEquals("n8 secondary parent association not present on n7", 1, countChildrenOfNode(n7Ref)); // delete n6 nodeService.deleteNode(n6Ref); + + // ensure that we can't see cascaded nodes in-transaction + assertFalse("n8 not cascade deleted in-transaction", nodeService.exists(n8Ref)); + // commit to check setComplete(); endTransaction(); diff --git a/source/java/org/alfresco/repo/template/NodeSearchResultsMap.java b/source/java/org/alfresco/repo/template/NodeSearchResultsMap.java index 2ae5213683..30b642f7e7 100644 --- a/source/java/org/alfresco/repo/template/NodeSearchResultsMap.java +++ b/source/java/org/alfresco/repo/template/NodeSearchResultsMap.java @@ -48,7 +48,7 @@ public class NodeSearchResultsMap extends BaseSearchResultsMap if (key != null) { String ref = key.toString().replace(":", "\\:"); - ref = ref.replace("/", "\\/"); + ref = "ID:" + ref.replace("/", "\\/"); List results = query(ref); diff --git a/source/java/org/alfresco/repo/version/common/counter/VersionCounterServiceTest.java b/source/java/org/alfresco/repo/version/common/counter/VersionCounterServiceTest.java index a9c34e9d63..7cd3dca989 100644 --- a/source/java/org/alfresco/repo/version/common/counter/VersionCounterServiceTest.java +++ b/source/java/org/alfresco/repo/version/common/counter/VersionCounterServiceTest.java @@ -112,6 +112,7 @@ public class VersionCounterServiceTest extends TestCase public void testConcurrentVersionNumber() throws Throwable { + counter.currentVersionNumber(storeRef1); VersionCounterThread[] threads = new VersionCounterThread[threadCount]; for (int i = 0; i < threadCount; i++) {