mirror of
				https://github.com/owncloud/android-library.git
				synced 2025-10-31 02:17:41 +00:00 
			
		
		
		
	Merge pull request #450 from owncloud/connection_validator
Connection validator
This commit is contained in:
		
						commit
						d8ac22d57e
					
				| @ -12,6 +12,7 @@ dependencies { | |||||||
|     implementation("com.squareup.moshi:moshi-kotlin:$moshiVersion") { |     implementation("com.squareup.moshi:moshi-kotlin:$moshiVersion") { | ||||||
|         exclude module: "kotlin-reflect" |         exclude module: "kotlin-reflect" | ||||||
|     } |     } | ||||||
|  |     implementation 'org.apache.commons:commons-lang3:3.12.0' | ||||||
|     kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion" |     kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion" | ||||||
| 
 | 
 | ||||||
|     testImplementation 'junit:junit:4.13.2' |     testImplementation 'junit:junit:4.13.2' | ||||||
|  | |||||||
| @ -0,0 +1,186 @@ | |||||||
|  | package com.owncloud.android.lib.common | ||||||
|  | 
 | ||||||
|  | import android.accounts.AccountManager | ||||||
|  | import android.accounts.AccountsException | ||||||
|  | import android.content.Context | ||||||
|  | import com.owncloud.android.lib.common.authentication.OwnCloudCredentials | ||||||
|  | import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory.OwnCloudAnonymousCredentials | ||||||
|  | import com.owncloud.android.lib.common.http.HttpConstants | ||||||
|  | import com.owncloud.android.lib.common.operations.RemoteOperationResult | ||||||
|  | import com.owncloud.android.lib.resources.files.CheckPathExistenceRemoteOperation | ||||||
|  | import com.owncloud.android.lib.resources.status.GetRemoteStatusOperation | ||||||
|  | import com.owncloud.android.lib.resources.status.RemoteServerInfo | ||||||
|  | import org.apache.commons.lang3.exception.ExceptionUtils | ||||||
|  | import timber.log.Timber | ||||||
|  | import java.io.IOException | ||||||
|  | import java.lang.Exception | ||||||
|  | 
 | ||||||
|  | class ConnectionValidator( | ||||||
|  |     val context: Context, | ||||||
|  |     val clearCookiesOnValidation: Boolean | ||||||
|  | ) { | ||||||
|  |     fun validate(baseClient: OwnCloudClient, singleSessionManager: SingleSessionManager): Boolean { | ||||||
|  |         try { | ||||||
|  |             var validationRetryCount = 0 | ||||||
|  |             val client = OwnCloudClient(baseClient.baseUri, null, false, singleSessionManager) | ||||||
|  |             if (clearCookiesOnValidation) { | ||||||
|  |                 client.clearCookies() | ||||||
|  |             } else { | ||||||
|  |                 client.cookiesForBaseUri = baseClient.cookiesForBaseUri | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             client.account = baseClient.account | ||||||
|  |             client.credentials = baseClient.credentials | ||||||
|  |             while (validationRetryCount < VALIDATION_RETRY_COUNT) { | ||||||
|  |                 Timber.d("validationRetryCout %d", validationRetryCount) | ||||||
|  |                 var successCounter = 0 | ||||||
|  |                 var failCounter = 0 | ||||||
|  | 
 | ||||||
|  |                 client.setFollowRedirects(true) | ||||||
|  |                 if (isOnwCloudStatusOk(client)) { | ||||||
|  |                     successCounter++ | ||||||
|  |                 } else { | ||||||
|  |                     failCounter++ | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Skip the part where we try to check if we can access the parts where we have to be logged in... if we are not logged in | ||||||
|  |                 if (baseClient.credentials !is OwnCloudAnonymousCredentials) { | ||||||
|  |                     client.setFollowRedirects(false) | ||||||
|  |                     val contentReply = canAccessRootFolder(client) | ||||||
|  |                     if (contentReply.httpCode == HttpConstants.HTTP_OK) { | ||||||
|  |                         if (contentReply.data == true) { //if data is true it means that the content reply was ok | ||||||
|  |                             successCounter++ | ||||||
|  |                         } else { | ||||||
|  |                             failCounter++ | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         failCounter++ | ||||||
|  |                         if (contentReply.httpCode == HttpConstants.HTTP_UNAUTHORIZED) { | ||||||
|  |                             checkUnauthorizedAccess(client, singleSessionManager, contentReply.httpCode) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 if (successCounter >= failCounter) { | ||||||
|  |                     baseClient.credentials = client.credentials | ||||||
|  |                     baseClient.cookiesForBaseUri = client.cookiesForBaseUri | ||||||
|  |                     return true | ||||||
|  |                 } | ||||||
|  |                 validationRetryCount++ | ||||||
|  |             } | ||||||
|  |             Timber.d("Could not authenticate or get valid data from owncloud") | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             Timber.d(ExceptionUtils.getStackTrace(e)) | ||||||
|  |         } | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun isOnwCloudStatusOk(client: OwnCloudClient): Boolean { | ||||||
|  |         val reply = getOwnCloudStatus(client) | ||||||
|  |         // dont check status code. It currently relais on the broken redirect code of the owncloud client | ||||||
|  |         // TODO: Use okhttp redirect and add this check again | ||||||
|  |         // return reply.httpCode == HttpConstants.HTTP_OK && | ||||||
|  |         return !reply.isException && | ||||||
|  |                 reply.data != null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun getOwnCloudStatus(client: OwnCloudClient): RemoteOperationResult<RemoteServerInfo> { | ||||||
|  |         val remoteStatusOperation = GetRemoteStatusOperation() | ||||||
|  |         return remoteStatusOperation.execute(client) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun canAccessRootFolder(client: OwnCloudClient): RemoteOperationResult<Boolean> { | ||||||
|  |         val checkPathExistenceRemoteOperation = CheckPathExistenceRemoteOperation("/", true) | ||||||
|  |         return checkPathExistenceRemoteOperation.execute(client) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Determines if credentials should be invalidated according the to the HTTPS status | ||||||
|  |      * of a network request just performed. | ||||||
|  |      * | ||||||
|  |      * @param httpStatusCode Result of the last request ran with the 'credentials' belows. | ||||||
|  |      * @return 'True' if credentials should and might be invalidated, 'false' if shouldn't or | ||||||
|  |      * cannot be invalidated with the given arguments. | ||||||
|  |      */ | ||||||
|  |     private fun shouldInvalidateAccountCredentials(credentials: OwnCloudCredentials, account: OwnCloudAccount, httpStatusCode: Int): Boolean { | ||||||
|  |         var shouldInvalidateAccountCredentials = httpStatusCode == HttpConstants.HTTP_UNAUTHORIZED | ||||||
|  |         shouldInvalidateAccountCredentials = shouldInvalidateAccountCredentials and  // real credentials | ||||||
|  |                 (credentials !is OwnCloudAnonymousCredentials) | ||||||
|  | 
 | ||||||
|  |         // test if have all the needed to effectively invalidate ... | ||||||
|  |         shouldInvalidateAccountCredentials = | ||||||
|  |             shouldInvalidateAccountCredentials and (account.savedAccount != null) | ||||||
|  |         return shouldInvalidateAccountCredentials | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Invalidates credentials stored for the given account in the system  [AccountManager] and in | ||||||
|  |      * current [SingleSessionManager.getDefaultSingleton] instance. | ||||||
|  |      * | ||||||
|  |      * | ||||||
|  |      * [.shouldInvalidateAccountCredentials] should be called first. | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |     private fun invalidateAccountCredentials(account: OwnCloudAccount, credentials: OwnCloudCredentials) { | ||||||
|  |         val am = AccountManager.get(context) | ||||||
|  |         am.invalidateAuthToken( | ||||||
|  |             account.savedAccount.type, | ||||||
|  |             credentials.authToken | ||||||
|  |         ) | ||||||
|  |         am.clearPassword(account.savedAccount) // being strict, only needed for Basic Auth credentials | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Checks the status code of an execution and decides if should be repeated with fresh credentials. | ||||||
|  |      * | ||||||
|  |      * | ||||||
|  |      * Invalidates current credentials if the request failed as anauthorized. | ||||||
|  |      * | ||||||
|  |      * | ||||||
|  |      * Refresh current credentials if possible, and marks a retry. | ||||||
|  |      * | ||||||
|  |      * @param status | ||||||
|  |      * @param repeatCounter | ||||||
|  |      * @return | ||||||
|  |      */ | ||||||
|  |     private fun checkUnauthorizedAccess(client: OwnCloudClient, singleSessionManager: SingleSessionManager, status: Int): Boolean { | ||||||
|  |         var credentialsWereRefreshed = false | ||||||
|  |         val account = client.account | ||||||
|  |         val credentials = account.credentials | ||||||
|  |         if (shouldInvalidateAccountCredentials(credentials, account, status)) { | ||||||
|  |             invalidateAccountCredentials(account, credentials) | ||||||
|  | 
 | ||||||
|  |             if (credentials.authTokenCanBeRefreshed()) { | ||||||
|  |                 try { | ||||||
|  |                     // This command does the actual refresh | ||||||
|  |                     account.loadCredentials(context) | ||||||
|  |                     // if mAccount.getCredentials().length() == 0 --> refresh failed | ||||||
|  |                     client.credentials = account.credentials | ||||||
|  |                     credentialsWereRefreshed = true | ||||||
|  |                 } catch (e: AccountsException) { | ||||||
|  |                     Timber.e( | ||||||
|  |                         e, "Error while trying to refresh auth token for %s\ntrace: %s", | ||||||
|  |                         account.savedAccount.name, | ||||||
|  |                         ExceptionUtils.getStackTrace(e) | ||||||
|  |                     ) | ||||||
|  |                 } catch (e: IOException) { | ||||||
|  |                     Timber.e( | ||||||
|  |                         e, "Error while trying to refresh auth token for %s\ntrace: %s", | ||||||
|  |                         account.savedAccount.name, | ||||||
|  |                         ExceptionUtils.getStackTrace(e) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |                 if (!credentialsWereRefreshed) { | ||||||
|  |                     // if credentials are not refreshed, client must be removed | ||||||
|  |                     // from the OwnCloudClientManager to prevent it is reused once and again | ||||||
|  |                     singleSessionManager.removeClientFor(account) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             // else: onExecute will finish with status 401 | ||||||
|  |         } | ||||||
|  |         return credentialsWereRefreshed | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         private val VALIDATION_RETRY_COUNT = 3 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -25,11 +25,8 @@ | |||||||
| 
 | 
 | ||||||
| package com.owncloud.android.lib.common; | package com.owncloud.android.lib.common; | ||||||
| 
 | 
 | ||||||
| import android.accounts.AccountManager; |  | ||||||
| import android.accounts.AccountsException; |  | ||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| 
 | 
 | ||||||
| import at.bitfire.dav4jvm.exception.HttpException; |  | ||||||
| import com.owncloud.android.lib.common.accounts.AccountUtils; | import com.owncloud.android.lib.common.accounts.AccountUtils; | ||||||
| import com.owncloud.android.lib.common.authentication.OwnCloudCredentials; | import com.owncloud.android.lib.common.authentication.OwnCloudCredentials; | ||||||
| import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory; | import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory; | ||||||
| @ -37,7 +34,6 @@ import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory | |||||||
| import com.owncloud.android.lib.common.http.HttpClient; | import com.owncloud.android.lib.common.http.HttpClient; | ||||||
| import com.owncloud.android.lib.common.http.HttpConstants; | import com.owncloud.android.lib.common.http.HttpConstants; | ||||||
| import com.owncloud.android.lib.common.http.methods.HttpBaseMethod; | import com.owncloud.android.lib.common.http.methods.HttpBaseMethod; | ||||||
| import com.owncloud.android.lib.common.network.RedirectionPath; |  | ||||||
| import com.owncloud.android.lib.common.utils.RandomUtils; | import com.owncloud.android.lib.common.utils.RandomUtils; | ||||||
| import com.owncloud.android.lib.resources.status.OwnCloudVersion; | import com.owncloud.android.lib.resources.status.OwnCloudVersion; | ||||||
| import okhttp3.Cookie; | import okhttp3.Cookie; | ||||||
| @ -49,40 +45,51 @@ import java.io.InputStream; | |||||||
| import java.util.List; | import java.util.List; | ||||||
| 
 | 
 | ||||||
| import static com.owncloud.android.lib.common.http.HttpConstants.AUTHORIZATION_HEADER; | import static com.owncloud.android.lib.common.http.HttpConstants.AUTHORIZATION_HEADER; | ||||||
| import static com.owncloud.android.lib.common.http.HttpConstants.OC_X_REQUEST_ID; | import static com.owncloud.android.lib.common.http.HttpConstants.HTTP_MOVED_PERMANENTLY; | ||||||
| 
 | 
 | ||||||
| public class OwnCloudClient extends HttpClient { | public class OwnCloudClient extends HttpClient { | ||||||
| 
 | 
 | ||||||
|     public static final String WEBDAV_FILES_PATH_4_0 = "/remote.php/dav/files/"; |     public static final String WEBDAV_FILES_PATH_4_0 = "/remote.php/dav/files/"; | ||||||
|     public static final String WEBDAV_PATH_4_0_AND_LATER = "/remote.php/dav"; |  | ||||||
|     public static final String STATUS_PATH = "/status.php"; |     public static final String STATUS_PATH = "/status.php"; | ||||||
|     private static final String WEBDAV_UPLOADS_PATH_4_0 = "/remote.php/dav/uploads/"; |     private static final String WEBDAV_UPLOADS_PATH_4_0 = "/remote.php/dav/uploads/"; | ||||||
|     private static final int MAX_REDIRECTIONS_COUNT = 3; |     private static final int MAX_RETRY_COUNT = 2; | ||||||
|     private static final int MAX_REPEAT_COUNT_WITH_FRESH_CREDENTIALS = 1; |  | ||||||
| 
 | 
 | ||||||
|     private static byte[] sExhaustBuffer = new byte[1024]; |  | ||||||
|     private static int sIntanceCounter = 0; |     private static int sIntanceCounter = 0; | ||||||
|     private OwnCloudCredentials mCredentials = null; |     private OwnCloudCredentials mCredentials = null; | ||||||
|     private int mInstanceNumber; |     private int mInstanceNumber; | ||||||
|     private Uri mBaseUri; |     private Uri mBaseUri; | ||||||
|     private OwnCloudVersion mVersion = null; |     private OwnCloudVersion mVersion = null; | ||||||
|     private OwnCloudAccount mAccount; |     private OwnCloudAccount mAccount; | ||||||
|  |     private final ConnectionValidator mConnectionValidator; | ||||||
|  |     private Object mRequestMutex = new Object(); | ||||||
|  | 
 | ||||||
|  |     // If set to true a mutex will be used to prevent parallel execution of the execute() method | ||||||
|  |     // if false the execute() method can be called even though the mutex is already aquired. | ||||||
|  |     // This is used for the ConnectionValidator, which has to be able to execute OperationsWhile all "normal" operations net | ||||||
|  |     // to be set on hold. | ||||||
|  |     private final Boolean mSynchronizeRequests; | ||||||
| 
 | 
 | ||||||
|     private SingleSessionManager mSingleSessionManager = null; |     private SingleSessionManager mSingleSessionManager = null; | ||||||
| 
 | 
 | ||||||
|     private boolean mFollowRedirects; |     private boolean mFollowRedirects = false; | ||||||
| 
 | 
 | ||||||
|     public OwnCloudClient(Uri baseUri) { |     public OwnCloudClient(Uri baseUri, | ||||||
|  |                           ConnectionValidator connectionValidator, | ||||||
|  |                           boolean synchronizeRequests, | ||||||
|  |                           SingleSessionManager singleSessionManager) { | ||||||
|         if (baseUri == null) { |         if (baseUri == null) { | ||||||
|             throw new IllegalArgumentException("Parameter 'baseUri' cannot be NULL"); |             throw new IllegalArgumentException("Parameter 'baseUri' cannot be NULL"); | ||||||
|         } |         } | ||||||
|         mBaseUri = baseUri; |         mBaseUri = baseUri; | ||||||
|  |         mSynchronizeRequests = synchronizeRequests; | ||||||
|  |         mSingleSessionManager = singleSessionManager; | ||||||
| 
 | 
 | ||||||
|         mInstanceNumber = sIntanceCounter++; |         mInstanceNumber = sIntanceCounter++; | ||||||
|         Timber.d("#" + mInstanceNumber + "Creating OwnCloudClient"); |         Timber.d("#" + mInstanceNumber + "Creating OwnCloudClient"); | ||||||
| 
 | 
 | ||||||
|         clearCredentials(); |         clearCredentials(); | ||||||
|         clearCookies(); |         clearCookies(); | ||||||
|  |         mConnectionValidator = connectionValidator; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void clearCredentials() { |     public void clearCredentials() { | ||||||
| @ -92,11 +99,27 @@ public class OwnCloudClient extends HttpClient { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public int executeHttpMethod(HttpBaseMethod method) throws Exception { |     public int executeHttpMethod(HttpBaseMethod method) throws Exception { | ||||||
|         boolean repeatWithFreshCredentials; |         if(mSynchronizeRequests) { | ||||||
|  |             synchronized (mRequestMutex) { | ||||||
|  |                 return saveExecuteHttpMethod(method); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             return saveExecuteHttpMethod(method); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private int saveExecuteHttpMethod(HttpBaseMethod method) throws Exception { | ||||||
|         int repeatCounter = 0; |         int repeatCounter = 0; | ||||||
|         int status; |         int status; | ||||||
| 
 | 
 | ||||||
|  |         if(mFollowRedirects) { | ||||||
|  |             method.setFollowRedirects(true); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         boolean retry; | ||||||
|         do { |         do { | ||||||
|  |             repeatCounter++; | ||||||
|  |             retry = false; | ||||||
|             String requestId = RandomUtils.generateRandomUUID(); |             String requestId = RandomUtils.generateRandomUUID(); | ||||||
| 
 | 
 | ||||||
|             // Header to allow tracing requests in apache and ownCloud logs |             // Header to allow tracing requests in apache and ownCloud logs | ||||||
| @ -104,105 +127,31 @@ public class OwnCloudClient extends HttpClient { | |||||||
|             method.setRequestHeader(HttpConstants.OC_X_REQUEST_ID, requestId); |             method.setRequestHeader(HttpConstants.OC_X_REQUEST_ID, requestId); | ||||||
|             method.setRequestHeader(HttpConstants.USER_AGENT_HEADER, SingleSessionManager.getUserAgent()); |             method.setRequestHeader(HttpConstants.USER_AGENT_HEADER, SingleSessionManager.getUserAgent()); | ||||||
|             method.setRequestHeader(HttpConstants.ACCEPT_ENCODING_HEADER, HttpConstants.ACCEPT_ENCODING_IDENTITY); |             method.setRequestHeader(HttpConstants.ACCEPT_ENCODING_HEADER, HttpConstants.ACCEPT_ENCODING_IDENTITY); | ||||||
|             if (mCredentials.getHeaderAuth() != null && method.getRequestHeader(AUTHORIZATION_HEADER) == null) { |             if (mCredentials.getHeaderAuth() != null && !mCredentials.getHeaderAuth().isEmpty()) { | ||||||
|                 method.setRequestHeader(AUTHORIZATION_HEADER, mCredentials.getHeaderAuth()); |                 method.setRequestHeader(AUTHORIZATION_HEADER, mCredentials.getHeaderAuth()); | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|             status = method.execute(); |             status = method.execute(); | ||||||
| 
 | 
 | ||||||
|             if (mFollowRedirects) { |             if (shouldConnectionValidatorBeCalled(method, status)) { | ||||||
|                 status = followRedirection(method).getLastStatus(); |                 retry = mConnectionValidator.validate(this, mSingleSessionManager); // retry on success fail on no success | ||||||
|  |             } else if(method.getFollowPermanentRedirects() && status == HTTP_MOVED_PERMANENTLY) { | ||||||
|  |                 retry = true; | ||||||
|  |                 method.setFollowRedirects(true); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             repeatWithFreshCredentials = checkUnauthorizedAccess(status, repeatCounter); |         } while (retry && repeatCounter < MAX_RETRY_COUNT); | ||||||
|             if (repeatWithFreshCredentials) { |  | ||||||
|                 repeatCounter++; |  | ||||||
|             } |  | ||||||
|         } while (repeatWithFreshCredentials); |  | ||||||
| 
 | 
 | ||||||
|         return status; |         return status; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private int executeRedirectedHttpMethod(HttpBaseMethod method) throws Exception { |     private boolean shouldConnectionValidatorBeCalled(HttpBaseMethod method, int status) { | ||||||
|         boolean repeatWithFreshCredentials; |         return !mFollowRedirects && | ||||||
|         int repeatCounter = 0; |                 !method.getFollowRedirects() && | ||||||
|         int status; |                 mConnectionValidator != null && | ||||||
| 
 |                 (status == HttpConstants.HTTP_MOVED_TEMPORARILY || | ||||||
|         do { |                         (!(mCredentials instanceof OwnCloudAnonymousCredentials) && | ||||||
|             String requestId = RandomUtils.generateRandomUUID(); |                                 status == HttpConstants.HTTP_UNAUTHORIZED)); | ||||||
| 
 |  | ||||||
|             // Header to allow tracing requests in apache and ownCloud logs |  | ||||||
|             Timber.d("Executing in request with id %s", requestId); |  | ||||||
|             method.setRequestHeader(OC_X_REQUEST_ID, requestId); |  | ||||||
|             method.setRequestHeader(HttpConstants.USER_AGENT_HEADER, SingleSessionManager.getUserAgent()); |  | ||||||
|             method.setRequestHeader(HttpConstants.ACCEPT_ENCODING_HEADER, HttpConstants.ACCEPT_ENCODING_IDENTITY); |  | ||||||
|             if (mCredentials.getHeaderAuth() != null) { |  | ||||||
|                 method.setRequestHeader(AUTHORIZATION_HEADER, mCredentials.getHeaderAuth()); |  | ||||||
|             } |  | ||||||
|             status = method.execute(); |  | ||||||
| 
 |  | ||||||
|             repeatWithFreshCredentials = checkUnauthorizedAccess(status, repeatCounter); |  | ||||||
|             if (repeatWithFreshCredentials) { |  | ||||||
|                 repeatCounter++; |  | ||||||
|             } |  | ||||||
|         } while (repeatWithFreshCredentials); |  | ||||||
| 
 |  | ||||||
|         return status; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public RedirectionPath followRedirection(HttpBaseMethod method) throws Exception { |  | ||||||
|         int redirectionsCount = 0; |  | ||||||
|         int status = method.getStatusCode(); |  | ||||||
|         RedirectionPath redirectionPath = new RedirectionPath(status, MAX_REDIRECTIONS_COUNT); |  | ||||||
| 
 |  | ||||||
|         while (redirectionsCount < MAX_REDIRECTIONS_COUNT && |  | ||||||
|                 (status == HttpConstants.HTTP_MOVED_PERMANENTLY || |  | ||||||
|                         status == HttpConstants.HTTP_MOVED_TEMPORARILY || |  | ||||||
|                         status == HttpConstants.HTTP_TEMPORARY_REDIRECT) |  | ||||||
|         ) { |  | ||||||
| 
 |  | ||||||
|             final String location = method.getResponseHeader(HttpConstants.LOCATION_HEADER) != null |  | ||||||
|                     ? method.getResponseHeader(HttpConstants.LOCATION_HEADER) |  | ||||||
|                     : method.getResponseHeader(HttpConstants.LOCATION_HEADER_LOWER); |  | ||||||
| 
 |  | ||||||
|             if (location != null) { |  | ||||||
|                 Timber.d("#" + mInstanceNumber + "Location to redirect: " + location); |  | ||||||
| 
 |  | ||||||
|                 redirectionPath.addLocation(location); |  | ||||||
| 
 |  | ||||||
|                 // Release the connection to avoid reach the max number of connections per host |  | ||||||
|                 // due to it will be set a different url |  | ||||||
|                 exhaustResponse(method.getResponseBodyAsStream()); |  | ||||||
| 
 |  | ||||||
|                 method.setUrl(HttpUrl.parse(location)); |  | ||||||
|                 final String destination = method.getRequestHeader("Destination") != null |  | ||||||
|                         ? method.getRequestHeader("Destination") |  | ||||||
|                         : method.getRequestHeader("destination"); |  | ||||||
| 
 |  | ||||||
|                 if (destination != null) { |  | ||||||
|                     final int suffixIndex = location.lastIndexOf(getUserFilesWebDavUri().toString()); |  | ||||||
|                     final String redirectionBase = location.substring(0, suffixIndex); |  | ||||||
|                     final String destinationPath = destination.substring(mBaseUri.toString().length()); |  | ||||||
| 
 |  | ||||||
|                     method.setRequestHeader("destination", redirectionBase + destinationPath); |  | ||||||
|                 } |  | ||||||
|                 try { |  | ||||||
|                     status = executeRedirectedHttpMethod(method); |  | ||||||
|                 } catch (HttpException e) { |  | ||||||
|                     if (e.getMessage().contains(Integer.toString(HttpConstants.HTTP_MOVED_TEMPORARILY))) { |  | ||||||
|                         status = HttpConstants.HTTP_MOVED_TEMPORARILY; |  | ||||||
|                     } else { |  | ||||||
|                         throw e; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 redirectionPath.addStatus(status); |  | ||||||
|                 redirectionsCount++; |  | ||||||
| 
 |  | ||||||
|             } else { |  | ||||||
|                 Timber.d(" #" + mInstanceNumber + "No location to redirect!"); |  | ||||||
|                 status = HttpConstants.HTTP_NOT_FOUND; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return redirectionPath; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -273,26 +222,18 @@ public class OwnCloudClient extends HttpClient { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public String getCookiesString() { |     public void setCookiesForBaseUri(List<Cookie> cookies) { | ||||||
|         StringBuilder cookiesString = new StringBuilder(); |  | ||||||
|         List<Cookie> cookieList = getCookiesFromUrl(HttpUrl.parse(mBaseUri.toString())); |  | ||||||
| 
 |  | ||||||
|         if (cookieList != null) { |  | ||||||
|             for (Cookie cookie : cookieList) { |  | ||||||
|                 cookiesString.append(cookie.toString()).append(";"); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return cookiesString.toString(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setCookiesForCurrentAccount(List<Cookie> cookies) { |  | ||||||
|         getOkHttpClient().cookieJar().saveFromResponse( |         getOkHttpClient().cookieJar().saveFromResponse( | ||||||
|                 HttpUrl.parse(getAccount().getBaseUri().toString()), |                 HttpUrl.parse(mBaseUri.toString()), | ||||||
|                 cookies |                 cookies | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public List<Cookie> getCookiesForBaseUri() { | ||||||
|  |         return getOkHttpClient().cookieJar().loadForRequest( | ||||||
|  |                 HttpUrl.parse(mBaseUri.toString())); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public OwnCloudVersion getOwnCloudVersion() { |     public OwnCloudVersion getOwnCloudVersion() { | ||||||
|         return mVersion; |         return mVersion; | ||||||
|     } |     } | ||||||
| @ -309,94 +250,6 @@ public class OwnCloudClient extends HttpClient { | |||||||
|         this.mAccount = account; |         this.mAccount = account; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * Checks the status code of an execution and decides if should be repeated with fresh credentials. |  | ||||||
|      * <p> |  | ||||||
|      * Invalidates current credentials if the request failed as anauthorized. |  | ||||||
|      * <p> |  | ||||||
|      * Refresh current credentials if possible, and marks a retry. |  | ||||||
|      * |  | ||||||
|      * @param status |  | ||||||
|      * @param repeatCounter |  | ||||||
|      * @return |  | ||||||
|      */ |  | ||||||
|     private boolean checkUnauthorizedAccess(int status, int repeatCounter) { |  | ||||||
|         boolean credentialsWereRefreshed = false; |  | ||||||
| 
 |  | ||||||
|         if (shouldInvalidateAccountCredentials(status)) { |  | ||||||
|             boolean invalidated = invalidateAccountCredentials(); |  | ||||||
| 
 |  | ||||||
|             if (invalidated) { |  | ||||||
|                 if (getCredentials().authTokenCanBeRefreshed() && |  | ||||||
|                         repeatCounter < MAX_REPEAT_COUNT_WITH_FRESH_CREDENTIALS) { |  | ||||||
|                     try { |  | ||||||
|                         mAccount.loadCredentials(getContext()); |  | ||||||
|                         // if mAccount.getCredentials().length() == 0 --> refresh failed |  | ||||||
|                         setCredentials(mAccount.getCredentials()); |  | ||||||
|                         credentialsWereRefreshed = true; |  | ||||||
| 
 |  | ||||||
|                     } catch (AccountsException | IOException e) { |  | ||||||
|                         Timber.e(e, "Error while trying to refresh auth token for %s", |  | ||||||
|                                 mAccount.getSavedAccount().name |  | ||||||
|                         ); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (!credentialsWereRefreshed && mSingleSessionManager != null) { |  | ||||||
|                     // if credentials are not refreshed, client must be removed |  | ||||||
|                     // from the OwnCloudClientManager to prevent it is reused once and again |  | ||||||
|                     mSingleSessionManager.removeClientFor(mAccount); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             // else: onExecute will finish with status 401 |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return credentialsWereRefreshed; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Determines if credentials should be invalidated according the to the HTTPS status |  | ||||||
|      * of a network request just performed. |  | ||||||
|      * |  | ||||||
|      * @param httpStatusCode Result of the last request ran with the 'credentials' belows. |  | ||||||
|      * @return 'True' if credentials should and might be invalidated, 'false' if shouldn't or |  | ||||||
|      * cannot be invalidated with the given arguments. |  | ||||||
|      */ |  | ||||||
|     private boolean shouldInvalidateAccountCredentials(int httpStatusCode) { |  | ||||||
|         boolean shouldInvalidateAccountCredentials = |  | ||||||
|                 (httpStatusCode == HttpConstants.HTTP_UNAUTHORIZED); |  | ||||||
| 
 |  | ||||||
|         shouldInvalidateAccountCredentials &= (mCredentials != null &&         // real credentials |  | ||||||
|                 !(mCredentials instanceof OwnCloudCredentialsFactory.OwnCloudAnonymousCredentials)); |  | ||||||
| 
 |  | ||||||
|         // test if have all the needed to effectively invalidate ... |  | ||||||
|         shouldInvalidateAccountCredentials &= (mAccount != null && mAccount.getSavedAccount() != null && getContext() != null); |  | ||||||
| 
 |  | ||||||
|         return shouldInvalidateAccountCredentials; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Invalidates credentials stored for the given account in the system  {@link AccountManager} and in |  | ||||||
|      * current {@link SingleSessionManager#getDefaultSingleton()} instance. |  | ||||||
|      * <p> |  | ||||||
|      * {@link #shouldInvalidateAccountCredentials(int)} should be called first. |  | ||||||
|      * |  | ||||||
|      * @return 'True' if invalidation was successful, 'false' otherwise. |  | ||||||
|      */ |  | ||||||
|     private boolean invalidateAccountCredentials() { |  | ||||||
|         AccountManager am = AccountManager.get(getContext()); |  | ||||||
|         am.invalidateAuthToken( |  | ||||||
|                 mAccount.getSavedAccount().type, |  | ||||||
|                 mCredentials.getAuthToken() |  | ||||||
|         ); |  | ||||||
|         am.clearPassword(mAccount.getSavedAccount()); // being strict, only needed for Basic Auth credentials |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public boolean followRedirects() { |  | ||||||
|         return mFollowRedirects; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setFollowRedirects(boolean followRedirects) { |     public void setFollowRedirects(boolean followRedirects) { | ||||||
|         this.mFollowRedirects = followRedirects; |         this.mFollowRedirects = followRedirects; | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,58 +0,0 @@ | |||||||
| /* ownCloud Android Library is available under MIT license |  | ||||||
|  *   Copyright (C) 2020 ownCloud GmbH. |  | ||||||
|  * |  | ||||||
|  *   Permission is hereby granted, free of charge, to any person obtaining a copy |  | ||||||
|  *   of this software and associated documentation files (the "Software"), to deal |  | ||||||
|  *   in the Software without restriction, including without limitation the rights |  | ||||||
|  *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |  | ||||||
|  *   copies of the Software, and to permit persons to whom the Software is |  | ||||||
|  *   furnished to do so, subject to the following conditions: |  | ||||||
|  * |  | ||||||
|  *   The above copyright notice and this permission notice shall be included in |  | ||||||
|  *   all copies or substantial portions of the Software. |  | ||||||
|  * |  | ||||||
|  *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |  | ||||||
|  *   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |  | ||||||
|  *   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |  | ||||||
|  *   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |  | ||||||
|  *   BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |  | ||||||
|  *   ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |  | ||||||
|  *   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |  | ||||||
|  *   THE SOFTWARE. |  | ||||||
|  * |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| package com.owncloud.android.lib.common; |  | ||||||
| 
 |  | ||||||
| import android.content.Context; |  | ||||||
| import android.net.Uri; |  | ||||||
| 
 |  | ||||||
| import com.owncloud.android.lib.common.http.HttpClient; |  | ||||||
| import com.owncloud.android.lib.resources.status.GetRemoteStatusOperation; |  | ||||||
| 
 |  | ||||||
| public class OwnCloudClientFactory { |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Creates a OwnCloudClient to access a URL and sets the desired parameters for ownCloud |  | ||||||
|      * client connections. |  | ||||||
|      * |  | ||||||
|      * @param uri     URL to the ownCloud server; BASE ENTRY POINT, not WebDavPATH |  | ||||||
|      * @param context Android context where the OwnCloudClient is being created. |  | ||||||
|      * @return A OwnCloudClient object ready to be used |  | ||||||
|      */ |  | ||||||
|     public static OwnCloudClient createOwnCloudClient(Uri uri, Context context, boolean followRedirects) { |  | ||||||
|         OwnCloudClient client = new OwnCloudClient(uri); |  | ||||||
| 
 |  | ||||||
|         client.setFollowRedirects(followRedirects); |  | ||||||
| 
 |  | ||||||
|         HttpClient.setContext(context); |  | ||||||
|         retrieveCookiesFromMiddleware(client); |  | ||||||
| 
 |  | ||||||
|         return client; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static void retrieveCookiesFromMiddleware(OwnCloudClient client) { |  | ||||||
|         final GetRemoteStatusOperation statusOperation = new GetRemoteStatusOperation(); |  | ||||||
|         statusOperation.run(client); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -49,6 +49,7 @@ public class SingleSessionManager { | |||||||
| 
 | 
 | ||||||
|     private static SingleSessionManager sDefaultSingleton; |     private static SingleSessionManager sDefaultSingleton; | ||||||
|     private static String sUserAgent; |     private static String sUserAgent; | ||||||
|  |     private static ConnectionValidator sConnectionValidator; | ||||||
| 
 | 
 | ||||||
|     private ConcurrentMap<String, OwnCloudClient> mClientsWithKnownUsername = new ConcurrentHashMap<>(); |     private ConcurrentMap<String, OwnCloudClient> mClientsWithKnownUsername = new ConcurrentHashMap<>(); | ||||||
|     private ConcurrentMap<String, OwnCloudClient> mClientsWithUnknownUsername = new ConcurrentHashMap<>(); |     private ConcurrentMap<String, OwnCloudClient> mClientsWithUnknownUsername = new ConcurrentHashMap<>(); | ||||||
| @ -60,6 +61,14 @@ public class SingleSessionManager { | |||||||
|         return sDefaultSingleton; |         return sDefaultSingleton; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static void setConnectionValidator(ConnectionValidator connectionValidator) { | ||||||
|  |         sConnectionValidator = connectionValidator; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static ConnectionValidator getConnectionValidator() { | ||||||
|  |         return sConnectionValidator; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public static String getUserAgent() { |     public static String getUserAgent() { | ||||||
|         return sUserAgent; |         return sUserAgent; | ||||||
|     } |     } | ||||||
| @ -68,7 +77,22 @@ public class SingleSessionManager { | |||||||
|         sUserAgent = userAgent; |         sUserAgent = userAgent; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public OwnCloudClient getClientFor(OwnCloudAccount account, Context context) throws OperationCanceledException, |     private static OwnCloudClient createOwnCloudClient(Uri uri, Context context, ConnectionValidator connectionValidator, SingleSessionManager singleSessionManager) { | ||||||
|  |         OwnCloudClient client = new OwnCloudClient(uri, connectionValidator, true, singleSessionManager); | ||||||
|  |         HttpClient.setContext(context); | ||||||
|  | 
 | ||||||
|  |         return client; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public OwnCloudClient getClientFor(OwnCloudAccount account, | ||||||
|  |                                        Context context) throws OperationCanceledException, | ||||||
|  |             AuthenticatorException, IOException { | ||||||
|  |         return getClientFor(account, context, getConnectionValidator()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public OwnCloudClient getClientFor(OwnCloudAccount account, | ||||||
|  |                                        Context context, | ||||||
|  |                                        ConnectionValidator connectionValidator) throws OperationCanceledException, | ||||||
|             AuthenticatorException, IOException { |             AuthenticatorException, IOException { | ||||||
| 
 | 
 | ||||||
|         Timber.d("getClientFor starting "); |         Timber.d("getClientFor starting "); | ||||||
| @ -104,10 +128,11 @@ public class SingleSessionManager { | |||||||
| 
 | 
 | ||||||
|         if (client == null) { |         if (client == null) { | ||||||
|             // no client to reuse - create a new one |             // no client to reuse - create a new one | ||||||
|             client = OwnCloudClientFactory.createOwnCloudClient( |             client = createOwnCloudClient( | ||||||
|                     account.getBaseUri(), |                     account.getBaseUri(), | ||||||
|                     context.getApplicationContext(), |                     context.getApplicationContext(), | ||||||
|                     true);    // TODO remove dependency on OwnCloudClientFactory |                     connectionValidator, | ||||||
|  |                     this); | ||||||
| 
 | 
 | ||||||
|             //the next two lines are a hack because okHttpclient is used as a singleton instead of being an |             //the next two lines are a hack because okHttpclient is used as a singleton instead of being an | ||||||
|             //injected instance that can be deleted when required |             //injected instance that can be deleted when required | ||||||
|  | |||||||
| @ -28,7 +28,7 @@ import okhttp3.CookieJar | |||||||
| import okhttp3.HttpUrl | import okhttp3.HttpUrl | ||||||
| 
 | 
 | ||||||
| class CookieJarImpl( | class CookieJarImpl( | ||||||
|     private val sCookieStore: HashMap<String, List<Cookie>> |     private val cookieStore: HashMap<String, List<Cookie>> | ||||||
| ) : CookieJar { | ) : CookieJar { | ||||||
| 
 | 
 | ||||||
|     fun containsCookieWithName(cookies: List<Cookie>, name: String): Boolean { |     fun containsCookieWithName(cookies: List<Cookie>, name: String): Boolean { | ||||||
| @ -52,12 +52,11 @@ class CookieJarImpl( | |||||||
| 
 | 
 | ||||||
|     override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) { |     override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) { | ||||||
|         // Avoid duplicated cookies but update |         // Avoid duplicated cookies but update | ||||||
|         val currentCookies: List<Cookie> = sCookieStore[url.host] ?: ArrayList() |         val currentCookies: List<Cookie> = cookieStore[url.host] ?: ArrayList() | ||||||
|         val updatedCookies: List<Cookie> = getUpdatedCookies(currentCookies, cookies) |         val updatedCookies: List<Cookie> = getUpdatedCookies(currentCookies, cookies) | ||||||
|         sCookieStore[url.host] = updatedCookies |         cookieStore[url.host] = updatedCookies | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun loadForRequest(url: HttpUrl) = |     override fun loadForRequest(url: HttpUrl) = | ||||||
|         sCookieStore[url.host] ?: ArrayList() |         cookieStore[url.host] ?: ArrayList() | ||||||
| 
 |  | ||||||
| } | } | ||||||
| @ -132,10 +132,6 @@ public class HttpClient { | |||||||
|         return sLogInterceptor; |         return sLogInterceptor; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static List<Cookie> getCookiesFromUrl(HttpUrl httpUrl) { |  | ||||||
|         return sCookieStore.get(httpUrl.host()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public Context getContext() { |     public Context getContext() { | ||||||
|         return sContext; |         return sContext; | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -41,6 +41,7 @@ abstract class HttpBaseMethod constructor(url: URL) { | |||||||
|     var okHttpClient: OkHttpClient |     var okHttpClient: OkHttpClient | ||||||
|     var httpUrl: HttpUrl = url.toHttpUrlOrNull() ?: throw MalformedURLException() |     var httpUrl: HttpUrl = url.toHttpUrlOrNull() ?: throw MalformedURLException() | ||||||
|     var request: Request |     var request: Request | ||||||
|  |     private var _followPermanentRedirects = false | ||||||
|     abstract var response: Response |     abstract var response: Response | ||||||
| 
 | 
 | ||||||
|     var call: Call? = null |     var call: Call? = null | ||||||
| @ -123,6 +124,11 @@ abstract class HttpBaseMethod constructor(url: URL) { | |||||||
|         return response.body?.byteStream() |         return response.body?.byteStream() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * returns the final url after following the last redirect. | ||||||
|  |      */ | ||||||
|  |     open fun getFinalUrl() = response.request.url | ||||||
|  | 
 | ||||||
|     /************************* |     /************************* | ||||||
|      *** Connection Params *** |      *** Connection Params *** | ||||||
|      *************************/ |      *************************/ | ||||||
| @ -158,6 +164,15 @@ abstract class HttpBaseMethod constructor(url: URL) { | |||||||
|             .build() |             .build() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     open fun getFollowRedirects() = okHttpClient.followRedirects | ||||||
|  | 
 | ||||||
|  |     open fun setFollowPermanentRedirects(followRedirects: Boolean) { | ||||||
|  |         _followPermanentRedirects = followRedirects | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     open fun getFollowPermanentRedirects() = _followPermanentRedirects | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     /************ |     /************ | ||||||
|      *** Call *** |      *** Call *** | ||||||
|      ************/ |      ************/ | ||||||
|  | |||||||
| @ -50,7 +50,7 @@ class PropfindMethod( | |||||||
|     public override fun onExecute(): Int { |     public override fun onExecute(): Int { | ||||||
|         davResource.propfind( |         davResource.propfind( | ||||||
|             depth = depth, |             depth = depth, | ||||||
|             reqProp = *propertiesToRequest, |             reqProp = propertiesToRequest, | ||||||
|             listOfHeaders = super.getRequestHeadersAsHashMap(), |             listOfHeaders = super.getRequestHeadersAsHashMap(), | ||||||
|             callback = { response: Response, hrefRelation: HrefRelation? -> |             callback = { response: Response, hrefRelation: HrefRelation? -> | ||||||
|                 when (hrefRelation) { |                 when (hrefRelation) { | ||||||
|  | |||||||
| @ -159,7 +159,7 @@ public abstract class RemoteOperation<T> implements Runnable { | |||||||
|             if (mAccount != null && mContext != null) { |             if (mAccount != null && mContext != null) { | ||||||
|                 OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, mContext); |                 OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, mContext); | ||||||
|                 mClient = SingleSessionManager.getDefaultSingleton(). |                 mClient = SingleSessionManager.getDefaultSingleton(). | ||||||
|                         getClientFor(ocAccount, mContext); |                         getClientFor(ocAccount, mContext, SingleSessionManager.getConnectionValidator()); | ||||||
|             } else { |             } else { | ||||||
|                 throw new IllegalStateException("Trying to run a remote operation " + |                 throw new IllegalStateException("Trying to run a remote operation " + | ||||||
|                         "asynchronously with no client and no chance to create one (no account)"); |                         "asynchronously with no client and no chance to create one (no account)"); | ||||||
|  | |||||||
| @ -34,6 +34,7 @@ import com.owncloud.android.lib.common.http.HttpConstants; | |||||||
| import com.owncloud.android.lib.common.http.methods.HttpBaseMethod; | import com.owncloud.android.lib.common.http.methods.HttpBaseMethod; | ||||||
| import com.owncloud.android.lib.common.network.CertificateCombinedException; | import com.owncloud.android.lib.common.network.CertificateCombinedException; | ||||||
| import okhttp3.Headers; | import okhttp3.Headers; | ||||||
|  | import org.apache.commons.lang3.exception.ExceptionUtils; | ||||||
| import org.json.JSONException; | import org.json.JSONException; | ||||||
| import timber.log.Timber; | import timber.log.Timber; | ||||||
| 
 | 
 | ||||||
| @ -59,13 +60,15 @@ public class RemoteOperationResult<T> | |||||||
|      * Generated - should be refreshed every time the class changes!! |      * Generated - should be refreshed every time the class changes!! | ||||||
|      */ |      */ | ||||||
|     private static final long serialVersionUID = 4968939884332372230L; |     private static final long serialVersionUID = 4968939884332372230L; | ||||||
|  |     private static final String LOCATION = "location"; | ||||||
|  |     private static final String WWW_AUTHENTICATE = "www-authenticate"; | ||||||
| 
 | 
 | ||||||
|     private boolean mSuccess = false; |     private boolean mSuccess = false; | ||||||
|     private int mHttpCode = -1; |     private int mHttpCode = -1; | ||||||
|     private String mHttpPhrase = null; |     private String mHttpPhrase = null; | ||||||
|     private Exception mException = null; |     private Exception mException = null; | ||||||
|     private ResultCode mCode = ResultCode.UNKNOWN_ERROR; |     private ResultCode mCode = ResultCode.UNKNOWN_ERROR; | ||||||
|     private String mRedirectedLocation; |     private String mRedirectedLocation = ""; | ||||||
|     private List<String> mAuthenticate = new ArrayList<>(); |     private List<String> mAuthenticate = new ArrayList<>(); | ||||||
|     private String mLastPermanentLocation = null; |     private String mLastPermanentLocation = null; | ||||||
|     private T mData = null; |     private T mData = null; | ||||||
| @ -112,6 +115,14 @@ public class RemoteOperationResult<T> | |||||||
|      */ |      */ | ||||||
|     public RemoteOperationResult(Exception e) { |     public RemoteOperationResult(Exception e) { | ||||||
|         mException = e; |         mException = e; | ||||||
|  |         //TODO: Do propper exception handling and remove this | ||||||
|  |         Timber.e("---------------------------------" + | ||||||
|  |                         "\nCreate RemoteOperationResult from exception." + | ||||||
|  |                         "\n Message: %s" + | ||||||
|  |                         "\n Stacktrace: %s" + | ||||||
|  |                         "\n---------------------------------", | ||||||
|  |                 ExceptionUtils.getMessage(e), | ||||||
|  |                 ExceptionUtils.getStackTrace(e)); | ||||||
| 
 | 
 | ||||||
|         if (e instanceof OperationCancelledException) { |         if (e instanceof OperationCancelledException) { | ||||||
|             mCode = ResultCode.CANCELLED; |             mCode = ResultCode.CANCELLED; | ||||||
| @ -248,11 +259,11 @@ public class RemoteOperationResult<T> | |||||||
|         this(httpCode, httpPhrase); |         this(httpCode, httpPhrase); | ||||||
|         if (headers != null) { |         if (headers != null) { | ||||||
|             for (Map.Entry<String, List<String>> header : headers.toMultimap().entrySet()) { |             for (Map.Entry<String, List<String>> header : headers.toMultimap().entrySet()) { | ||||||
|                 if ("location".equals(header.getKey().toLowerCase())) { |                 if (LOCATION.equalsIgnoreCase(header.getKey())) { | ||||||
|                     mRedirectedLocation = header.getValue().get(0); |                     mRedirectedLocation = header.getValue().get(0); | ||||||
|                     continue; |                     continue; | ||||||
|                 } |                 } | ||||||
|                 if ("www-authenticate".equals(header.getKey().toLowerCase())) { |                 if (WWW_AUTHENTICATE.equalsIgnoreCase(header.getKey())) { | ||||||
|                     for (String value: header.getValue()) { |                     for (String value: header.getValue()) { | ||||||
|                         mAuthenticate.add(value.toLowerCase()); |                         mAuthenticate.add(value.toLowerCase()); | ||||||
|                     } |                     } | ||||||
| @ -321,7 +332,7 @@ public class RemoteOperationResult<T> | |||||||
|                     mHttpPhrase = errorMessage; |                     mHttpPhrase = errorMessage; | ||||||
|                 } |                 } | ||||||
|             } catch (Exception e) { |             } catch (Exception e) { | ||||||
|                 Timber.w("Error reading exception from server: %s", e.getMessage()); |                 Timber.w("Error reading exception from server: %s\nTrace: %s", e.getMessage(), ExceptionUtils.getStackTrace(e)); | ||||||
|                 // mCode stays as set in this(success, httpCode, headers) |                 // mCode stays as set in this(success, httpCode, headers) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -27,7 +27,6 @@ import com.owncloud.android.lib.common.OwnCloudClient | |||||||
| import com.owncloud.android.lib.common.http.HttpConstants | import com.owncloud.android.lib.common.http.HttpConstants | ||||||
| import com.owncloud.android.lib.common.http.methods.webdav.DavUtils.allPropset | import com.owncloud.android.lib.common.http.methods.webdav.DavUtils.allPropset | ||||||
| import com.owncloud.android.lib.common.http.methods.webdav.PropfindMethod | import com.owncloud.android.lib.common.http.methods.webdav.PropfindMethod | ||||||
| import com.owncloud.android.lib.common.network.RedirectionPath |  | ||||||
| import com.owncloud.android.lib.common.network.WebdavUtils | import com.owncloud.android.lib.common.network.WebdavUtils | ||||||
| import com.owncloud.android.lib.common.operations.RemoteOperation | import com.owncloud.android.lib.common.operations.RemoteOperation | ||||||
| import com.owncloud.android.lib.common.operations.RemoteOperationResult | import com.owncloud.android.lib.common.operations.RemoteOperationResult | ||||||
| @ -44,26 +43,18 @@ import java.util.concurrent.TimeUnit | |||||||
|  * @author Abel García de Prada |  * @author Abel García de Prada | ||||||
|  * |  * | ||||||
|  * @param remotePath      Path to append to the URL owned by the client instance. |  * @param remotePath      Path to append to the URL owned by the client instance. | ||||||
|  * @param isUserLogged    When `true`, the username won't be added at the end of the PROPFIND url since is not |  * @param isUserLoggedIn    When `true`, the username won't be added at the end of the PROPFIND url since is not | ||||||
|  *                        needed to check user credentials |  *                        needed to check user credentials | ||||||
|  */ |  */ | ||||||
| class CheckPathExistenceRemoteOperation( | class CheckPathExistenceRemoteOperation( | ||||||
|     val remotePath: String? = "", |     val remotePath: String? = "", | ||||||
|     val isUserLogged: Boolean |     val isUserLoggedIn: Boolean | ||||||
| ) : RemoteOperation<Boolean>() { | ) : RemoteOperation<Boolean>() { | ||||||
|     /** |  | ||||||
|      * Gets the sequence of redirections followed during the execution of the operation. |  | ||||||
|      * |  | ||||||
|      * @return Sequence of redirections followed, if any, or NULL if the operation was not executed. |  | ||||||
|      */ |  | ||||||
|     var redirectionPath: RedirectionPath? = null |  | ||||||
|         private set |  | ||||||
| 
 | 
 | ||||||
|     override fun run(client: OwnCloudClient): RemoteOperationResult<Boolean> { |     override fun run(client: OwnCloudClient): RemoteOperationResult<Boolean> { | ||||||
|         val previousFollowRedirects = client.followRedirects() |  | ||||||
|         return try { |         return try { | ||||||
|             val stringUrl = |             val stringUrl = | ||||||
|                 if (isUserLogged) client.baseFilesWebDavUri.toString() |                 if (isUserLoggedIn) client.baseFilesWebDavUri.toString() | ||||||
|                 else client.userFilesWebDavUri.toString() + WebdavUtils.encodePath(remotePath) |                 else client.userFilesWebDavUri.toString() + WebdavUtils.encodePath(remotePath) | ||||||
| 
 | 
 | ||||||
|             val propFindMethod = PropfindMethod(URL(stringUrl), 0, allPropset).apply { |             val propFindMethod = PropfindMethod(URL(stringUrl), 0, allPropset).apply { | ||||||
| @ -71,12 +62,7 @@ class CheckPathExistenceRemoteOperation( | |||||||
|                 setConnectionTimeout(TIMEOUT.toLong(), TimeUnit.SECONDS) |                 setConnectionTimeout(TIMEOUT.toLong(), TimeUnit.SECONDS) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             client.setFollowRedirects(false) |  | ||||||
|             var status = client.executeHttpMethod(propFindMethod) |             var status = client.executeHttpMethod(propFindMethod) | ||||||
|             if (previousFollowRedirects) { |  | ||||||
|                 redirectionPath = client.followRedirection(propFindMethod) |  | ||||||
|                 status = redirectionPath?.lastStatus!! |  | ||||||
|             } |  | ||||||
|             /* PROPFIND method |             /* PROPFIND method | ||||||
|              * 404 NOT FOUND: path doesn't exist, |              * 404 NOT FOUND: path doesn't exist, | ||||||
|              * 207 MULTI_STATUS: path exists. |              * 207 MULTI_STATUS: path exists. | ||||||
| @ -94,16 +80,9 @@ class CheckPathExistenceRemoteOperation( | |||||||
|                 "Existence check for ${client.userFilesWebDavUri}${WebdavUtils.encodePath(remotePath)} : ${result.logMessage}" |                 "Existence check for ${client.userFilesWebDavUri}${WebdavUtils.encodePath(remotePath)} : ${result.logMessage}" | ||||||
|             ) |             ) | ||||||
|             result |             result | ||||||
|         } finally { |  | ||||||
|             client.setFollowRedirects(previousFollowRedirects) |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * @return 'True' if the operation was executed and at least one redirection was followed. |  | ||||||
|      */ |  | ||||||
|     fun wasRedirected() = redirectionPath?.redirectionsCount ?: 0 > 0 |  | ||||||
| 
 |  | ||||||
|     private fun isSuccess(status: Int) = status == HttpConstants.HTTP_OK || status == HttpConstants.HTTP_MULTI_STATUS |     private fun isSuccess(status: Int) = status == HttpConstants.HTTP_OK || status == HttpConstants.HTTP_MULTI_STATUS | ||||||
| 
 | 
 | ||||||
|     companion object { |     companion object { | ||||||
|  | |||||||
| @ -0,0 +1,77 @@ | |||||||
|  | /* ownCloud Android Library is available under MIT license | ||||||
|  | *   Copyright (C) 2022 ownCloud GmbH. | ||||||
|  | * | ||||||
|  | *   Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  | *   of this software and associated documentation files (the "Software"), to deal | ||||||
|  | *   in the Software without restriction, including without limitation the rights | ||||||
|  | *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
|  | *   copies of the Software, and to permit persons to whom the Software is | ||||||
|  | *   furnished to do so, subject to the following conditions: | ||||||
|  | * | ||||||
|  | *   The above copyright notice and this permission notice shall be included in | ||||||
|  | *   all copies or substantial portions of the Software. | ||||||
|  | * | ||||||
|  | *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||||
|  | *   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||||||
|  | *   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||||||
|  | *   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS | ||||||
|  | *   BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN | ||||||
|  | *   ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | ||||||
|  | *   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||||
|  | *   THE SOFTWARE. | ||||||
|  | * | ||||||
|  | */ | ||||||
|  | package com.owncloud.android.lib.resources.files | ||||||
|  | 
 | ||||||
|  | import com.owncloud.android.lib.common.OwnCloudClient | ||||||
|  | import com.owncloud.android.lib.common.http.HttpConstants | ||||||
|  | import com.owncloud.android.lib.common.http.methods.webdav.DavUtils | ||||||
|  | import com.owncloud.android.lib.common.http.methods.webdav.PropfindMethod | ||||||
|  | import com.owncloud.android.lib.common.operations.RemoteOperation | ||||||
|  | import com.owncloud.android.lib.common.operations.RemoteOperationResult | ||||||
|  | import timber.log.Timber | ||||||
|  | import java.net.URL | ||||||
|  | import java.util.concurrent.TimeUnit | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Operation to get the base url, which might differ in case of a redirect. | ||||||
|  |  * | ||||||
|  |  * @author Christian Schabesberger | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | class GetBaseUrlRemoteOperation : RemoteOperation<String?>() { | ||||||
|  |     override fun run(client: OwnCloudClient): RemoteOperationResult<String?> { | ||||||
|  |         return try { | ||||||
|  |             val stringUrl = client.baseFilesWebDavUri.toString() | ||||||
|  | 
 | ||||||
|  |             val propFindMethod = PropfindMethod(URL(stringUrl), 0, DavUtils.allPropset).apply { | ||||||
|  |                 setReadTimeout(TIMEOUT, TimeUnit.SECONDS) | ||||||
|  |                 setConnectionTimeout(TIMEOUT, TimeUnit.SECONDS) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             val status = client.executeHttpMethod(propFindMethod) | ||||||
|  | 
 | ||||||
|  |             if (isSuccess(status)) { | ||||||
|  |                 RemoteOperationResult<String?>(RemoteOperationResult.ResultCode.OK).apply { | ||||||
|  |                     data = propFindMethod.getFinalUrl().toString() | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 RemoteOperationResult<String?>(propFindMethod).apply { | ||||||
|  |                     data = null | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             Timber.e(e, "Could not get actuall (or redirected) base URL from base url (/).") | ||||||
|  |             RemoteOperationResult<String?>(e) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun isSuccess(status: Int) = status == HttpConstants.HTTP_OK || status == HttpConstants.HTTP_MULTI_STATUS | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         /** | ||||||
|  |          * Maximum time to wait for a response from the server in milliseconds. | ||||||
|  |          */ | ||||||
|  |         private const val TIMEOUT = 10_000L | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -77,8 +77,6 @@ public class ReadRemoteFolderOperation extends RemoteOperation<ArrayList<RemoteF | |||||||
|                     DavConstants.DEPTH_1, |                     DavConstants.DEPTH_1, | ||||||
|                     DavUtils.getAllPropset()); |                     DavUtils.getAllPropset()); | ||||||
| 
 | 
 | ||||||
|             client.setFollowRedirects(true); |  | ||||||
| 
 |  | ||||||
|             int status = client.executeHttpMethod(propfindMethod); |             int status = client.executeHttpMethod(propfindMethod); | ||||||
| 
 | 
 | ||||||
|             if (isSuccess(status)) { |             if (isSuccess(status)) { | ||||||
|  | |||||||
| @ -33,6 +33,6 @@ class OCFileService(override val client: OwnCloudClient) : | |||||||
|     override fun checkPathExistence(path: String, isUserLogged: Boolean): RemoteOperationResult<Boolean> = |     override fun checkPathExistence(path: String, isUserLogged: Boolean): RemoteOperationResult<Boolean> = | ||||||
|         CheckPathExistenceRemoteOperation( |         CheckPathExistenceRemoteOperation( | ||||||
|             remotePath = path, |             remotePath = path, | ||||||
|             isUserLogged = isUserLogged |             isUserLoggedIn = isUserLogged | ||||||
|         ).execute(client) |         ).execute(client) | ||||||
| } | } | ||||||
|  | |||||||
| @ -55,6 +55,7 @@ class GetOIDCDiscoveryRemoteOperation : RemoteOperation<OIDCDiscoveryResponse>() | |||||||
|                 addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE) |                 addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             getMethod.setFollowRedirects(true) | ||||||
|             val status = client.executeHttpMethod(getMethod) |             val status = client.executeHttpMethod(getMethod) | ||||||
| 
 | 
 | ||||||
|             val responseBody = getMethod.getResponseBodyAsString() |             val responseBody = getMethod.getResponseBodyAsString() | ||||||
|  | |||||||
| @ -30,9 +30,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult | |||||||
| import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode | import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode | ||||||
| import com.owncloud.android.lib.resources.status.HttpScheme.HTTPS_PREFIX | import com.owncloud.android.lib.resources.status.HttpScheme.HTTPS_PREFIX | ||||||
| import com.owncloud.android.lib.resources.status.HttpScheme.HTTP_PREFIX | import com.owncloud.android.lib.resources.status.HttpScheme.HTTP_PREFIX | ||||||
| import com.owncloud.android.lib.resources.status.HttpScheme.HTTP_SCHEME |  | ||||||
| import org.json.JSONException | import org.json.JSONException | ||||||
| import timber.log.Timber |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Checks if the server is valid |  * Checks if the server is valid | ||||||
| @ -45,27 +43,24 @@ import timber.log.Timber | |||||||
| class GetRemoteStatusOperation : RemoteOperation<RemoteServerInfo>() { | class GetRemoteStatusOperation : RemoteOperation<RemoteServerInfo>() { | ||||||
| 
 | 
 | ||||||
|     public override fun run(client: OwnCloudClient): RemoteOperationResult<RemoteServerInfo> { |     public override fun run(client: OwnCloudClient): RemoteOperationResult<RemoteServerInfo> { | ||||||
|  |         if (!usesHttpOrHttps(client.baseUri)) { | ||||||
|             client.baseUri = buildFullHttpsUrl(client.baseUri) |             client.baseUri = buildFullHttpsUrl(client.baseUri) | ||||||
| 
 |         } | ||||||
|         var result = tryToConnect(client) |         return tryToConnect(client) | ||||||
|         if (!(result.code == ResultCode.OK || result.code == ResultCode.OK_SSL) && !result.isSslRecoverableException) { |  | ||||||
|             Timber.d("Establishing secure connection failed, trying non secure connection") |  | ||||||
|             client.baseUri = client.baseUri.buildUpon().scheme(HTTP_SCHEME).build() |  | ||||||
|             result = tryToConnect(client) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|         return result |     private fun updateClientBaseUrl(client: OwnCloudClient, newBaseUrl: String) { | ||||||
|  |         client.baseUri = Uri.parse(newBaseUrl) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun tryToConnect(client: OwnCloudClient): RemoteOperationResult<RemoteServerInfo> { |     private fun tryToConnect(client: OwnCloudClient): RemoteOperationResult<RemoteServerInfo> { | ||||||
|         val baseUrl = client.baseUri.toString() |         val baseUrl = client.baseUri.toString() | ||||||
|         client.setFollowRedirects(false) |  | ||||||
|         return try { |         return try { | ||||||
|             val requester = StatusRequester() |             val requester = StatusRequester() | ||||||
|             val requestResult = requester.requestAndFollowRedirects(baseUrl, client) |             val requestResult = requester.request(baseUrl, client) | ||||||
|             requester.handleRequestResult(requestResult, baseUrl).also { |             val result = requester.handleRequestResult(requestResult, baseUrl) | ||||||
|                 client.baseUri = Uri.parse(it.data.baseUrl) |             updateClientBaseUrl(client, result.data.baseUrl) | ||||||
|             } |             return result | ||||||
|         } catch (e: JSONException) { |         } catch (e: JSONException) { | ||||||
|             RemoteOperationResult(ResultCode.INSTANCE_NOT_CONFIGURED) |             RemoteOperationResult(ResultCode.INSTANCE_NOT_CONFIGURED) | ||||||
|         } catch (e: Exception) { |         } catch (e: Exception) { | ||||||
|  | |||||||
| @ -73,36 +73,18 @@ internal class StatusRequester { | |||||||
|     data class RequestResult( |     data class RequestResult( | ||||||
|         val getMethod: GetMethod, |         val getMethod: GetMethod, | ||||||
|         val status: Int, |         val status: Int, | ||||||
|         val redirectedToUnsecureLocation: Boolean, |  | ||||||
|         val lastLocation: String |         val lastLocation: String | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     fun requestAndFollowRedirects(baseLocation: String, client: OwnCloudClient): RequestResult { |     fun request(baseLocation: String, client: OwnCloudClient): RequestResult { | ||||||
|         var currentLocation = baseLocation + OwnCloudClient.STATUS_PATH |         val currentLocation = baseLocation + OwnCloudClient.STATUS_PATH | ||||||
|         var redirectedToUnsecureLocation = false |  | ||||||
|         var status: Int |         var status: Int | ||||||
| 
 | 
 | ||||||
|         while (true) { |  | ||||||
|         val getMethod = getGetMethod(currentLocation) |         val getMethod = getGetMethod(currentLocation) | ||||||
| 
 |         getMethod.setFollowPermanentRedirects(true) | ||||||
|         status = client.executeHttpMethod(getMethod) |         status = client.executeHttpMethod(getMethod) | ||||||
|             val result = |  | ||||||
|                 if (status.isSuccess()) RemoteOperationResult<OwnCloudVersion>(RemoteOperationResult.ResultCode.OK) |  | ||||||
|                 else RemoteOperationResult(getMethod) |  | ||||||
| 
 | 
 | ||||||
|             if (result.redirectedLocation.isNullOrEmpty() || result.isSuccess) { |         return RequestResult(getMethod, status, getMethod.getFinalUrl().toString()) | ||||||
|                 return RequestResult(getMethod, status, redirectedToUnsecureLocation, currentLocation) |  | ||||||
|             } else { |  | ||||||
|                 val nextLocation = updateLocationWithRedirectPath(currentLocation, result.redirectedLocation) |  | ||||||
|                 redirectedToUnsecureLocation = |  | ||||||
|                     isRedirectedToNonSecureConnection( |  | ||||||
|                         redirectedToUnsecureLocation, |  | ||||||
|                         currentLocation, |  | ||||||
|                         nextLocation |  | ||||||
|                     ) |  | ||||||
|                 currentLocation = nextLocation |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun Int.isSuccess() = this == HttpConstants.HTTP_OK |     private fun Int.isSuccess() = this == HttpConstants.HTTP_OK | ||||||
| @ -122,12 +104,8 @@ internal class StatusRequester { | |||||||
|         // the version object will be returned even if the version is invalid, no error code; |         // the version object will be returned even if the version is invalid, no error code; | ||||||
|         // every app will decide how to act if (ocVersion.isVersionValid() == false) |         // every app will decide how to act if (ocVersion.isVersionValid() == false) | ||||||
|         val result: RemoteOperationResult<RemoteServerInfo> = |         val result: RemoteOperationResult<RemoteServerInfo> = | ||||||
|             if (requestResult.redirectedToUnsecureLocation) { |  | ||||||
|                 RemoteOperationResult(RemoteOperationResult.ResultCode.OK_REDIRECT_TO_NON_SECURE_CONNECTION) |  | ||||||
|             } else { |  | ||||||
|             if (baseUrl.startsWith(HTTPS_SCHEME)) RemoteOperationResult(RemoteOperationResult.ResultCode.OK_SSL) |             if (baseUrl.startsWith(HTTPS_SCHEME)) RemoteOperationResult(RemoteOperationResult.ResultCode.OK_SSL) | ||||||
|             else RemoteOperationResult(RemoteOperationResult.ResultCode.OK_NO_SSL) |             else RemoteOperationResult(RemoteOperationResult.ResultCode.OK_NO_SSL) | ||||||
|             } |  | ||||||
|         val finalUrl = URL(requestResult.lastLocation) |         val finalUrl = URL(requestResult.lastLocation) | ||||||
|         val finalBaseUrl = URL( |         val finalBaseUrl = URL( | ||||||
|             finalUrl.protocol, |             finalUrl.protocol, | ||||||
|  | |||||||
| @ -42,7 +42,7 @@ class OCServerInfoService : ServerInfoService { | |||||||
|     ): RemoteOperationResult<Boolean> = |     ): RemoteOperationResult<Boolean> = | ||||||
|         CheckPathExistenceRemoteOperation( |         CheckPathExistenceRemoteOperation( | ||||||
|             remotePath = path, |             remotePath = path, | ||||||
|             isUserLogged = true |             isUserLoggedIn = true | ||||||
|         ).execute(client) |         ).execute(client) | ||||||
| 
 | 
 | ||||||
|     override fun getRemoteStatus( |     override fun getRemoteStatus( | ||||||
|  | |||||||
| @ -0,0 +1,302 @@ | |||||||
|  | /* ownCloud Android Library is available under MIT license | ||||||
|  |  *   Copyright (C) 2020 ownCloud GmbH. | ||||||
|  |  * | ||||||
|  |  *   Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  |  *   of this software and associated documentation files (the "Software"), to deal | ||||||
|  |  *   in the Software without restriction, including without limitation the rights | ||||||
|  |  *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
|  |  *   copies of the Software, and to permit persons to whom the Software is | ||||||
|  |  *   furnished to do so, subject to the following conditions: | ||||||
|  |  * | ||||||
|  |  *   The above copyright notice and this permission notice shall be included in | ||||||
|  |  *   all copies or substantial portions of the Software. | ||||||
|  |  * | ||||||
|  |  *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||||
|  |  *   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||||||
|  |  *   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||||||
|  |  *   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS | ||||||
|  |  *   BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN | ||||||
|  |  *   ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | ||||||
|  |  *   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||||
|  |  *   THE SOFTWARE. | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package com.owncloud.android.lib.sampleclient; | ||||||
|  | 
 | ||||||
|  | import android.annotation.SuppressLint; | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.content.pm.PackageInfo; | ||||||
|  | import android.content.pm.PackageManager; | ||||||
|  | import android.content.res.AssetManager; | ||||||
|  | import android.graphics.drawable.BitmapDrawable; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.os.Handler; | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.ListView; | ||||||
|  | import android.widget.TextView; | ||||||
|  | import android.widget.Toast; | ||||||
|  | 
 | ||||||
|  | import com.owncloud.android.lib.common.ConnectionValidator; | ||||||
|  | import com.owncloud.android.lib.common.OwnCloudClient; | ||||||
|  | import com.owncloud.android.lib.common.SingleSessionManager; | ||||||
|  | import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory; | ||||||
|  | import com.owncloud.android.lib.common.network.OnDatatransferProgressListener; | ||||||
|  | import com.owncloud.android.lib.common.operations.OnRemoteOperationListener; | ||||||
|  | import com.owncloud.android.lib.common.operations.RemoteOperation; | ||||||
|  | import com.owncloud.android.lib.common.operations.RemoteOperationResult; | ||||||
|  | import com.owncloud.android.lib.resources.files.DownloadRemoteFileOperation; | ||||||
|  | import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation; | ||||||
|  | import com.owncloud.android.lib.resources.files.RemoteFile; | ||||||
|  | import com.owncloud.android.lib.resources.files.RemoveRemoteFileOperation; | ||||||
|  | import com.owncloud.android.lib.resources.files.UploadRemoteFileOperation; | ||||||
|  | import timber.log.Timber; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.FileOutputStream; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.InputStream; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | public class MainActivity extends Activity implements OnRemoteOperationListener, OnDatatransferProgressListener { | ||||||
|  | 
 | ||||||
|  |     private Handler mHandler; | ||||||
|  |     private OwnCloudClient mClient; | ||||||
|  |     private FilesArrayAdapter mFilesAdapter; | ||||||
|  |     private View mFrame; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called when the activity is first created. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |         setContentView(R.layout.main); | ||||||
|  | 
 | ||||||
|  |         Timber.plant(); | ||||||
|  |         mHandler = new Handler(); | ||||||
|  | 
 | ||||||
|  |         final Uri serverUri = Uri.parse(getString(R.string.server_base_url)); | ||||||
|  | 
 | ||||||
|  |         SingleSessionManager.setUserAgent(getUserAgent()); | ||||||
|  |         SingleSessionManager.setConnectionValidator(new ConnectionValidator(this, false)); | ||||||
|  |         mClient = new OwnCloudClient(serverUri, | ||||||
|  |                SingleSessionManager.getConnectionValidator(), | ||||||
|  |                 true, | ||||||
|  |                 SingleSessionManager.getDefaultSingleton()); | ||||||
|  |         mClient.setCredentials( | ||||||
|  |                 OwnCloudCredentialsFactory.newBasicCredentials( | ||||||
|  |                         getString(R.string.username), | ||||||
|  |                         getString(R.string.password) | ||||||
|  |                 ) | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         mFilesAdapter = new FilesArrayAdapter(this, R.layout.file_in_list); | ||||||
|  |         ((ListView) findViewById(R.id.list_view)).setAdapter(mFilesAdapter); | ||||||
|  | 
 | ||||||
|  |         // TODO move to background thread or task | ||||||
|  |         AssetManager assets = getAssets(); | ||||||
|  |         try { | ||||||
|  |             String sampleFileName = getString(R.string.sample_file_name); | ||||||
|  |             File upFolder = new File(getCacheDir(), getString(R.string.upload_folder_path)); | ||||||
|  |             upFolder.mkdir(); | ||||||
|  |             File upFile = new File(upFolder, sampleFileName); | ||||||
|  |             FileOutputStream fos = new FileOutputStream(upFile); | ||||||
|  |             InputStream is = assets.open(sampleFileName); | ||||||
|  |             int count; | ||||||
|  |             byte[] buffer = new byte[1024]; | ||||||
|  |             while ((count = is.read(buffer, 0, buffer.length)) >= 0) { | ||||||
|  |                 fos.write(buffer, 0, count); | ||||||
|  |             } | ||||||
|  |             is.close(); | ||||||
|  |             fos.close(); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             Toast.makeText(this, R.string.error_copying_sample_file, Toast.LENGTH_SHORT).show(); | ||||||
|  |             Timber.e(e, getString(R.string.error_copying_sample_file)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         mFrame = findViewById(R.id.frame); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onDestroy() { | ||||||
|  |         File upFolder = new File(getCacheDir(), getString(R.string.upload_folder_path)); | ||||||
|  |         File upFile = upFolder.listFiles()[0]; | ||||||
|  |         upFile.delete(); | ||||||
|  |         upFolder.delete(); | ||||||
|  |         super.onDestroy(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onClickHandler(View button) { | ||||||
|  |         switch (button.getId()) { | ||||||
|  |             case R.id.button_refresh: | ||||||
|  |                 startRefresh(); | ||||||
|  |                 break; | ||||||
|  |             case R.id.button_upload: | ||||||
|  |                 startUpload(); | ||||||
|  |                 break; | ||||||
|  |             case R.id.button_delete_remote: | ||||||
|  |                 startRemoteDeletion(); | ||||||
|  |                 break; | ||||||
|  |             case R.id.button_download: | ||||||
|  |                 startDownload(); | ||||||
|  |                 break; | ||||||
|  |             case R.id.button_delete_local: | ||||||
|  |                 startLocalDeletion(); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 Toast.makeText(this, R.string.youre_doing_it_wrong, Toast.LENGTH_SHORT).show(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void startRefresh() { | ||||||
|  |         ReadRemoteFolderOperation refreshOperation = new ReadRemoteFolderOperation(File.separator); | ||||||
|  |         refreshOperation.execute(mClient, this, mHandler); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void startUpload() { | ||||||
|  |         File upFolder = new File(getCacheDir(), getString(R.string.upload_folder_path)); | ||||||
|  |         File fileToUpload = upFolder.listFiles()[0]; | ||||||
|  |         String remotePath = File.separator + fileToUpload.getName(); | ||||||
|  |         String mimeType = getString(R.string.sample_file_mimetype); | ||||||
|  | 
 | ||||||
|  |         // Get the last modification date of the file from the file system | ||||||
|  |         long timeStampLong = fileToUpload.lastModified() / 1000; | ||||||
|  |         String timeStamp = Long.toString(timeStampLong); | ||||||
|  | 
 | ||||||
|  |         UploadRemoteFileOperation uploadOperation = new UploadRemoteFileOperation(fileToUpload.getAbsolutePath(), | ||||||
|  |                 remotePath, mimeType, timeStamp); | ||||||
|  |         uploadOperation.addDatatransferProgressListener(this); | ||||||
|  |         uploadOperation.execute(mClient, this, mHandler); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void startRemoteDeletion() { | ||||||
|  |         File upFolder = new File(getCacheDir(), getString(R.string.upload_folder_path)); | ||||||
|  |         File fileToUpload = upFolder.listFiles()[0]; | ||||||
|  |         String remotePath = File.separator + fileToUpload.getName(); | ||||||
|  | 
 | ||||||
|  |         RemoveRemoteFileOperation removeOperation = new RemoveRemoteFileOperation(remotePath); | ||||||
|  |         removeOperation.execute(mClient, this, mHandler); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void startDownload() { | ||||||
|  |         File downFolder = new File(getCacheDir(), getString(R.string.download_folder_path)); | ||||||
|  |         downFolder.mkdir(); | ||||||
|  |         File upFolder = new File(getCacheDir(), getString(R.string.upload_folder_path)); | ||||||
|  |         File fileToUpload = upFolder.listFiles()[0]; | ||||||
|  |         String remotePath = File.separator + fileToUpload.getName(); | ||||||
|  | 
 | ||||||
|  |         DownloadRemoteFileOperation downloadOperation = new DownloadRemoteFileOperation(remotePath, | ||||||
|  |                 downFolder.getAbsolutePath()); | ||||||
|  |         downloadOperation.addDatatransferProgressListener(this); | ||||||
|  |         downloadOperation.execute(mClient, this, mHandler); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void startLocalDeletion() { | ||||||
|  |         File downFolder = new File(getCacheDir(), getString(R.string.download_folder_path)); | ||||||
|  |         File downloadedFile = downFolder.listFiles()[0]; | ||||||
|  |         if (!downloadedFile.delete() && downloadedFile.exists()) { | ||||||
|  |             Toast.makeText(this, R.string.error_deleting_local_file, Toast.LENGTH_SHORT).show(); | ||||||
|  |         } else { | ||||||
|  |             ((TextView) findViewById(R.id.download_progress)).setText("0%"); | ||||||
|  |             findViewById(R.id.frame).setBackgroundDrawable(null); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) { | ||||||
|  |         if (!result.isSuccess()) { | ||||||
|  |             Toast.makeText(this, R.string.todo_operation_finished_in_fail, Toast.LENGTH_SHORT).show(); | ||||||
|  |             Timber.e(result.getException(), result.getLogMessage()); | ||||||
|  | 
 | ||||||
|  |         } else if (operation instanceof ReadRemoteFolderOperation) { | ||||||
|  |             onSuccessfulRefresh(result); | ||||||
|  | 
 | ||||||
|  |         } else if (operation instanceof com.owncloud.android.lib.resources.files.UploadRemoteFileOperation) { | ||||||
|  |             onSuccessfulUpload(); | ||||||
|  | 
 | ||||||
|  |         } else if (operation instanceof RemoveRemoteFileOperation) { | ||||||
|  |             onSuccessfulRemoteDeletion(); | ||||||
|  | 
 | ||||||
|  |         } else if (operation instanceof DownloadRemoteFileOperation) { | ||||||
|  |             onSuccessfulDownload(); | ||||||
|  | 
 | ||||||
|  |         } else { | ||||||
|  |             Toast.makeText(this, R.string.todo_operation_finished_in_success, Toast.LENGTH_SHORT).show(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void onSuccessfulRefresh(RemoteOperationResult result) { | ||||||
|  |         mFilesAdapter.clear(); | ||||||
|  |         List<RemoteFile> files = new ArrayList<>(); | ||||||
|  |         for (RemoteFile remoteFile : (List<RemoteFile>) result.getData()) { | ||||||
|  |             files.add(remoteFile); | ||||||
|  |         } | ||||||
|  |         for (RemoteFile file : files) { | ||||||
|  |             mFilesAdapter.add(file); | ||||||
|  |         } | ||||||
|  |         mFilesAdapter.remove(mFilesAdapter.getItem(0)); | ||||||
|  |         mFilesAdapter.notifyDataSetChanged(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void onSuccessfulUpload() { | ||||||
|  |         startRefresh(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void onSuccessfulRemoteDeletion() { | ||||||
|  |         startRefresh(); | ||||||
|  |         TextView progressView = findViewById(R.id.upload_progress); | ||||||
|  |         if (progressView != null) { | ||||||
|  |             progressView.setText("0%"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void onSuccessfulDownload() { | ||||||
|  |         File downFolder = new File(getCacheDir(), getString(R.string.download_folder_path)); | ||||||
|  |         File downloadedFile = downFolder.listFiles()[0]; | ||||||
|  |         BitmapDrawable bDraw = new BitmapDrawable(getResources(), downloadedFile.getAbsolutePath()); | ||||||
|  |         mFrame.setBackgroundDrawable(bDraw); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @SuppressLint("SetTextI18n") | ||||||
|  |     @Override | ||||||
|  |     public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName) { | ||||||
|  |         final long percentage = (totalToTransfer > 0 ? totalTransferredSoFar * 100 / totalToTransfer : 0); | ||||||
|  |         final boolean upload = fileName.contains(getString(R.string.upload_folder_path)); | ||||||
|  |         Timber.d("progressRate %s", percentage); | ||||||
|  |         mHandler.post(() -> { | ||||||
|  |             TextView progressView; | ||||||
|  |             if (upload) { | ||||||
|  |                 progressView = findViewById(R.id.upload_progress); | ||||||
|  |             } else { | ||||||
|  |                 progressView = findViewById(R.id.download_progress); | ||||||
|  |             } | ||||||
|  |             if (progressView != null) { | ||||||
|  |                 progressView.setText(percentage + "%"); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // user agent | ||||||
|  |     @SuppressLint("StringFormatInvalid") | ||||||
|  |     private String getUserAgent() { | ||||||
|  |         String appString = getResources().getString(R.string.user_agent); | ||||||
|  |         String packageName = getPackageName(); | ||||||
|  |         String version = ""; | ||||||
|  | 
 | ||||||
|  |         PackageInfo pInfo; | ||||||
|  |         try { | ||||||
|  |             pInfo = getPackageManager().getPackageInfo(packageName, 0); | ||||||
|  |             if (pInfo != null) { | ||||||
|  |                 version = pInfo.versionName; | ||||||
|  |             } | ||||||
|  |         } catch (PackageManager.NameNotFoundException e) { | ||||||
|  |             Timber.e(e); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Mozilla/5.0 (Android) ownCloud-android/1.7.0 | ||||||
|  |         return String.format(appString, version); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user