diff --git a/.drone.yml b/.drone.yml index 5b201da353..b204a2870c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -88,7 +88,15 @@ steps: services: - name: server - image: nextcloudci/server:server-3 + image: nextcloudci/server:server-13 + commands: + - /initnc.sh + - su www-data -c "OC_PASS=user1 php /var/www/html/occ user:add --password-from-env --display-name='User One' user1" + - su www-data -c "OC_PASS=user2 php /var/www/html/occ user:add --password-from-env --display-name='User Two' user2" + - su www-data -c "php /var/www/html/occ group:add users" + - su www-data -c "php /var/www/html/occ group:adduser users user1" + - su www-data -c "php /var/www/html/occ group:adduser users user2" + - /run.sh trigger: branch: diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt index a5b5e0f805..725a5ba2a7 100644 --- a/scripts/analysis/findbugs-results.txt +++ b/scripts/analysis/findbugs-results.txt @@ -1 +1 @@ -181 \ No newline at end of file +185 diff --git a/src/androidTest/java/com/owncloud/android/AbstractIT.java b/src/androidTest/java/com/owncloud/android/AbstractIT.java index 6d01493fc4..4e84bd5fa6 100644 --- a/src/androidTest/java/com/owncloud/android/AbstractIT.java +++ b/src/androidTest/java/com/owncloud/android/AbstractIT.java @@ -10,6 +10,7 @@ import com.owncloud.android.lib.common.OwnCloudBasicCredentials; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.OwnCloudClientFactory; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation; import com.owncloud.android.lib.resources.files.RemoveFileRemoteOperation; import com.owncloud.android.lib.resources.files.model.RemoteFile; @@ -22,7 +23,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import static junit.framework.TestCase.assertTrue; @@ -101,13 +101,15 @@ public static File extractAsset(String fileName, Context context) throws IOExcep @After public void after() { - ArrayList list = new ReadFolderRemoteOperation("/").execute(client).getData(); + RemoteOperationResult result = new ReadFolderRemoteOperation("/").execute(client); + assertTrue(result.getLogMessage(), result.isSuccess()); - for (Object object : list) { + for (Object object : result.getData()) { RemoteFile remoteFile = (RemoteFile) object; if (!remoteFile.getRemotePath().equals("/")) { - new RemoveFileRemoteOperation(remoteFile.getRemotePath()).execute(client); + assertTrue(new RemoveFileRemoteOperation(remoteFile.getRemotePath()) + .execute(client).isSuccess()); } } } diff --git a/src/androidTest/java/com/owncloud/android/FileIT.java b/src/androidTest/java/com/owncloud/android/FileIT.java index 22937b886c..42573a9472 100644 --- a/src/androidTest/java/com/owncloud/android/FileIT.java +++ b/src/androidTest/java/com/owncloud/android/FileIT.java @@ -2,12 +2,22 @@ import android.net.Uri; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation; import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation; import com.owncloud.android.lib.resources.files.RemoveFileRemoteOperation; +import com.owncloud.android.lib.resources.files.model.RemoteFile; +import com.owncloud.android.lib.resources.shares.CreateShareRemoteOperation; +import com.owncloud.android.lib.resources.shares.OCShare; +import com.owncloud.android.lib.resources.shares.ShareType; +import com.owncloud.android.lib.resources.shares.ShareeUser; import org.junit.Test; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -68,4 +78,218 @@ public void testCreateFolderWithWrongURL() { client.setBaseUri(uri); } + + @Test + public void testZeroSharees() { + // create & verify folder + String path = "/testFolder/"; + assertTrue(new CreateFolderRemoteOperation(path, true).execute(client).isSuccess()); + assertTrue(new ReadFolderRemoteOperation(path).execute(client).isSuccess()); + + // verify + RemoteOperationResult result = new ReadFolderRemoteOperation("/").execute(client); + assertTrue(result.isSuccess()); + + RemoteFile parentFolder = (RemoteFile) result.getData().get(0); + assertEquals("/", parentFolder.getRemotePath()); + + for (int i = 1; i < result.getData().size(); i++) { + RemoteFile child = (RemoteFile) result.getData().get(i); + + if (path.equals(child.getRemotePath())) { + assertEquals(0, child.getSharees().length); + } + } + } + + @Test + public void testShareViaLinkSharees() { + // create & verify folder + String path = "/testFolder/"; + assertTrue(new CreateFolderRemoteOperation(path, true).execute(client).isSuccess()); + assertTrue(new ReadFolderRemoteOperation(path).execute(client).isSuccess()); + + // share folder + assertTrue(new CreateShareRemoteOperation(path, + ShareType.PUBLIC_LINK, + "", + false, + "", + OCShare.DEFAULT_PERMISSION) + .execute(client).isSuccess()); + + // verify + RemoteOperationResult result = new ReadFolderRemoteOperation("/").execute(client); + assertTrue(result.isSuccess()); + + RemoteFile parentFolder = (RemoteFile) result.getData().get(0); + assertEquals("/", parentFolder.getRemotePath()); + + for (int i = 1; i < result.getData().size(); i++) { + RemoteFile child = (RemoteFile) result.getData().get(i); + + if (path.equals(child.getRemotePath())) { + assertEquals(0, child.getSharees().length); + } + } + } + + @Test + public void testShareToGroupSharees() { + // create & verify folder + String path = "/testFolder/"; + assertTrue(new CreateFolderRemoteOperation(path, true).execute(client).isSuccess()); + assertTrue(new ReadFolderRemoteOperation(path).execute(client).isSuccess()); + + ShareeUser sharee = new ShareeUser("users", "", ShareType.GROUP); + + // share folder + assertTrue(new CreateShareRemoteOperation(path, + ShareType.GROUP, + "users", + false, + "", + OCShare.DEFAULT_PERMISSION) + .execute(client).isSuccess()); + + // verify + RemoteOperationResult result = new ReadFolderRemoteOperation("/").execute(client); + assertTrue(result.isSuccess()); + + RemoteFile parentFolder = (RemoteFile) result.getData().get(0); + assertEquals("/", parentFolder.getRemotePath()); + + for (int i = 1; i < result.getData().size(); i++) { + RemoteFile child = (RemoteFile) result.getData().get(i); + + if (path.equals(child.getRemotePath())) { + assertEquals(1, child.getSharees().length); + assertEquals(sharee, child.getSharees()[0]); + } + } + } + + @Test + public void testOneSharees() { + // create & verify folder + String path = "/testFolder/"; + assertTrue(new CreateFolderRemoteOperation(path, true).execute(client).isSuccess()); + assertTrue(new ReadFolderRemoteOperation(path).execute(client).isSuccess()); + + ShareeUser sharee = new ShareeUser("user1", "User One", ShareType.USER); + + // share folder + assertTrue(new CreateShareRemoteOperation(path, + ShareType.USER, + "user1", + false, + "", + OCShare.DEFAULT_PERMISSION) + .execute(client).isSuccess()); + + // verify + RemoteOperationResult result = new ReadFolderRemoteOperation("/").execute(client); + assertTrue(result.isSuccess()); + + RemoteFile parentFolder = (RemoteFile) result.getData().get(0); + assertEquals("/", parentFolder.getRemotePath()); + + for (int i = 1; i < result.getData().size(); i++) { + RemoteFile child = (RemoteFile) result.getData().get(i); + + if (path.equals(child.getRemotePath())) { + assertEquals(1, child.getSharees().length); + assertEquals(sharee, child.getSharees()[0]); + } + } + } + + @Test + public void testTwoShareesOnParent() { + // create & verify folder + String path = "/testFolder/"; + assertTrue(new CreateFolderRemoteOperation(path, true).execute(client).isSuccess()); + assertTrue(new ReadFolderRemoteOperation(path).execute(client).isSuccess()); + + List sharees = new ArrayList<>(); + sharees.add(new ShareeUser("user1", "User One", ShareType.USER)); + sharees.add(new ShareeUser("user2", "User Two", ShareType.USER)); + + // share folder + assertTrue(new CreateShareRemoteOperation(path, + ShareType.USER, + "user1", + false, + "", + OCShare.DEFAULT_PERMISSION) + .execute(client).isSuccess()); + + assertTrue(new CreateShareRemoteOperation(path, + ShareType.USER, + "user2", + false, + "", + OCShare.DEFAULT_PERMISSION) + .execute(client).isSuccess()); + + // verify + RemoteOperationResult result = new ReadFolderRemoteOperation("/").execute(client); + assertTrue(result.isSuccess()); + + RemoteFile parentFolder = (RemoteFile) result.getData().get(0); + assertEquals("/", parentFolder.getRemotePath()); + + for (int i = 1; i < result.getData().size(); i++) { + RemoteFile child = (RemoteFile) result.getData().get(i); + + if (path.equals(child.getRemotePath())) { + assertEquals(2, child.getSharees().length); + + for (ShareeUser user : child.getSharees()) { + assertTrue(sharees.contains(user)); + } + } + } + } + + @Test + public void testTwoSharees() { + // create & verify folder + String path = "/testFolder/"; + assertTrue(new CreateFolderRemoteOperation(path, true).execute(client).isSuccess()); + assertTrue(new ReadFolderRemoteOperation(path).execute(client).isSuccess()); + + List sharees = new ArrayList<>(); + sharees.add(new ShareeUser("user1", "User One", ShareType.USER)); + sharees.add(new ShareeUser("user2", "User Two", ShareType.USER)); + + // share folder + assertTrue(new CreateShareRemoteOperation(path, + ShareType.USER, + "user1", + false, + "", + OCShare.DEFAULT_PERMISSION) + .execute(client).isSuccess()); + + assertTrue(new CreateShareRemoteOperation(path, + ShareType.USER, + "user2", + false, + "", + OCShare.DEFAULT_PERMISSION) + .execute(client).isSuccess()); + + // verify + RemoteOperationResult result = new ReadFolderRemoteOperation(path).execute(client); + assertTrue(result.isSuccess()); + + RemoteFile folder = (RemoteFile) result.getData().get(0); + assertEquals(path, folder.getRemotePath()); + assertEquals(2, folder.getSharees().length); + + for (ShareeUser user : folder.getSharees()) { + assertTrue(sharees.contains(user)); + } + } } diff --git a/src/main/java/com/owncloud/android/lib/common/network/WebdavEntry.java b/src/main/java/com/owncloud/android/lib/common/network/WebdavEntry.java index fb1b32d9e4..5c8fca29a3 100644 --- a/src/main/java/com/owncloud/android/lib/common/network/WebdavEntry.java +++ b/src/main/java/com/owncloud/android/lib/common/network/WebdavEntry.java @@ -27,19 +27,24 @@ import android.net.Uri; import com.owncloud.android.lib.common.utils.Log_OC; +import com.owncloud.android.lib.resources.shares.ShareType; +import com.owncloud.android.lib.resources.shares.ShareeUser; import org.apache.jackrabbit.webdav.MultiStatusResponse; import org.apache.jackrabbit.webdav.property.DavProperty; import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.property.DavPropertySet; import org.apache.jackrabbit.webdav.xml.Namespace; +import org.w3c.dom.Element; +import org.w3c.dom.Node; import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Collections; import java.util.Date; import java.util.List; +import javax.annotation.Nullable; + import lombok.Getter; import lombok.Setter; @@ -64,6 +69,9 @@ public class WebdavEntry { public static final String TRASHBIN_FILENAME = "trashbin-filename"; public static final String TRASHBIN_ORIGINAL_LOCATION = "trashbin-original-location"; public static final String TRASHBIN_DELETION_TIME = "trashbin-deletion-time"; + public static final String SHAREES_DISPLAY_NAME = "display-name"; + public static final String SHAREES_ID = "id"; + public static final String SHAREES_SHARE_TYPE = "type"; public static final String PROPERTY_QUOTA_USED_BYTES = "quota-used-bytes"; public static final String PROPERTY_QUOTA_AVAILABLE_BYTES = "quota-available-bytes"; @@ -96,7 +104,7 @@ public class WebdavEntry { @Getter private int unreadCommentsCount; @Getter @Setter private boolean hasPreview; @Getter private String note = ""; - @Getter private List sharees = new ArrayList<>(); + @Getter private ShareeUser[] sharees = new ShareeUser[0]; public enum MountType {INTERNAL, EXTERNAL, GROUP} @@ -321,12 +329,80 @@ public WebdavEntry(MultiStatusResponse ms, String splitElement) { // NC sharees property prop = propSet.get(EXTENDED_PROPERTY_SHAREES, ncNamespace); if (prop != null && prop.getValue() != null) { - Collections.addAll(sharees, prop.getValue().toString().split(", ")); + if (prop.getValue() instanceof ArrayList) { + ArrayList list = (ArrayList) prop.getValue(); + + List tempList = new ArrayList<>(); + + for (int i = 0; i < list.size(); i++) { + Element element = (Element) list.get(i); + + ShareeUser user = createShareeUser(element); + + if (user != null) { + tempList.add(user); + } + } + + sharees = tempList.toArray(new ShareeUser[0]); + + } else { + // single item or empty + Element element = (Element) prop.getValue(); + + ShareeUser user = createShareeUser(element); + + if (user != null) { + sharees = new ShareeUser[]{user}; + } + } } } else { Log_OC.e("WebdavEntry", "General fuckup, no status for webdav response"); } } + + private @Nullable + ShareeUser createShareeUser(Element element) { + String displayName = extractDisplayName(element); + String userId = extractUserId(element); + ShareType shareType = extractShareType(element); + + if ((ShareType.GROUP == shareType || !displayName.isEmpty()) && !userId.isEmpty()) { + return new ShareeUser(userId, displayName, shareType); + } else { + return null; + } + } + + private String extractDisplayName(Element element) { + Node displayName = element.getElementsByTagNameNS(NAMESPACE_NC, SHAREES_DISPLAY_NAME).item(0); + if (displayName != null && displayName.getFirstChild() != null) { + return displayName.getFirstChild().getNodeValue(); + } + + return ""; + } + + private String extractUserId(Element element) { + Node userId = element.getElementsByTagNameNS(NAMESPACE_NC, SHAREES_ID).item(0); + if (userId != null && userId.getFirstChild() != null) { + return userId.getFirstChild().getNodeValue(); + } + + return ""; + } + + private ShareType extractShareType(Element element) { + Node shareType = element.getElementsByTagNameNS(NAMESPACE_NC, SHAREES_SHARE_TYPE).item(0); + if (shareType != null && shareType.getFirstChild() != null) { + int value = Integer.parseInt(shareType.getFirstChild().getNodeValue()); + + return ShareType.fromValue(value); + } + + return ShareType.NO_SHARED; + } public String decodedPath() { return Uri.decode(path); diff --git a/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java b/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java index d86d4d933a..6acba4ad51 100644 --- a/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java +++ b/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java @@ -303,7 +303,7 @@ public final void run() { * to trigger authentication update */ if (mCallerActivity != null && mAccount != null && mContext != null && !result.isSuccess() && - ResultCode.UNAUTHORIZED.equals(result.getCode()) + ResultCode.UNAUTHORIZED == result.getCode() ) { /// possible fail due to lack of authorization // in an operation performed in foreground diff --git a/src/main/java/com/owncloud/android/lib/resources/files/model/RemoteFile.java b/src/main/java/com/owncloud/android/lib/resources/files/model/RemoteFile.java index f736e15d7d..2111129b72 100644 --- a/src/main/java/com/owncloud/android/lib/resources/files/model/RemoteFile.java +++ b/src/main/java/com/owncloud/android/lib/resources/files/model/RemoteFile.java @@ -29,9 +29,9 @@ import com.owncloud.android.lib.common.network.WebdavEntry; import com.owncloud.android.lib.resources.files.FileUtils; +import com.owncloud.android.lib.resources.shares.ShareeUser; import java.io.Serializable; -import java.util.List; import lombok.Getter; import lombok.Setter; @@ -66,7 +66,7 @@ public class RemoteFile implements Parcelable, Serializable { private int unreadCommentsCount; private boolean hasPreview; private String note; - private List sharees; + private ShareeUser[] sharees; public RemoteFile() { resetData(); @@ -171,7 +171,7 @@ public void readFromParcel(Parcel source) { ownerDisplayName = source.readString(); hasPreview = Boolean.parseBoolean(source.readString()); note = source.readString(); - source.readStringList(sharees); + source.readParcelableArray(ShareeUser.class.getClassLoader()); } @Override @@ -197,6 +197,6 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(ownerDisplayName); dest.writeString(Boolean.toString(hasPreview)); dest.writeString(note); - dest.writeStringList(sharees); + dest.writeParcelableArray(sharees, 0); } } diff --git a/src/main/java/com/owncloud/android/lib/resources/shares/OCShare.java b/src/main/java/com/owncloud/android/lib/resources/shares/OCShare.java index 77548d31c9..53171feba3 100644 --- a/src/main/java/com/owncloud/android/lib/resources/shares/OCShare.java +++ b/src/main/java/com/owncloud/android/lib/resources/shares/OCShare.java @@ -26,14 +26,16 @@ import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; + import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.FileUtils; -import lombok.Getter; -import lombok.Setter; import java.io.Serializable; +import androidx.annotation.NonNull; +import lombok.Getter; +import lombok.Setter; + /** * Contains the data of a Share from the Share API @@ -161,10 +163,10 @@ public void setShareLink(String shareLink) { } public boolean isPasswordProtected() { - if (!ShareType.PUBLIC_LINK.equals(shareType)) { - return isPasswordProtected; - } else { + if (ShareType.PUBLIC_LINK == shareType) { return shareWith.length() > 0; + } else { + return isPasswordProtected; } } diff --git a/src/main/java/com/owncloud/android/lib/resources/shares/ShareType.java b/src/main/java/com/owncloud/android/lib/resources/shares/ShareType.java index 5d16673d71..493cb48c38 100644 --- a/src/main/java/com/owncloud/android/lib/resources/shares/ShareType.java +++ b/src/main/java/com/owncloud/android/lib/resources/shares/ShareType.java @@ -58,29 +58,27 @@ public enum ShareType { public int getValue() { return value; } - - public static ShareType fromValue(int value) - { - switch (value) - { - case -1: - return NO_SHARED; - case 0: - return USER; - case 1: - return GROUP; - case 3: - return PUBLIC_LINK; - case 4: - return EMAIL; - case 5: - return CONTACT; - case 6: - return FEDERATED; + + public static ShareType fromValue(int value) { + switch (value) { + case -1: + return NO_SHARED; + case 0: + return USER; + case 1: + return GROUP; + case 3: + return PUBLIC_LINK; + case 4: + return EMAIL; + case 5: + return CONTACT; + case 6: + return FEDERATED; case 10: - return ROOM; - default: - return NO_SHARED; + return ROOM; + default: + return NO_SHARED; } } } diff --git a/src/main/java/com/owncloud/android/lib/resources/shares/ShareeUser.java b/src/main/java/com/owncloud/android/lib/resources/shares/ShareeUser.java new file mode 100644 index 0000000000..0d22cd1aef --- /dev/null +++ b/src/main/java/com/owncloud/android/lib/resources/shares/ShareeUser.java @@ -0,0 +1,85 @@ +/* Nextcloud Android Library is available under MIT license + * + * @author Tobias Kaminsky + * Copyright (C) 2019 Tobias Kaminsky + * Copyright (C) 2019 Nextcloud 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.shares; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.Nullable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +public class ShareeUser implements Parcelable { + @Getter private String userId; + @Getter private String displayName; + @Getter private ShareType shareType; + + + public static final Creator CREATOR = new Creator() { + @Override + public ShareeUser createFromParcel(Parcel in) { + return new ShareeUser(in); + } + + @Override + public ShareeUser[] newArray(int size) { + return new ShareeUser[size]; + } + }; + + protected ShareeUser(Parcel in) { + userId = in.readString(); + displayName = in.readString(); + shareType = ShareType.fromValue(in.readInt()); + } + + @Override + public int describeContents() { + return this.hashCode(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(userId); + dest.writeString(displayName); + dest.writeInt(shareType.getValue()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof ShareeUser)) { + return false; + } + ShareeUser that = (ShareeUser) obj; + + return this.userId.equals(that.userId) && + this.displayName.equals(that.displayName) && + this.shareType == that.shareType; + } +}