Skip to content

Commit e21ed79

Browse files
Merge pull request nextcloud#15388 from nextcloud/fix/left-to-right-filename-sanitization-v2
fix: left to right filename sanitization
2 parents db19387 + 013caa1 commit e21ed79

File tree

16 files changed

+382
-44
lines changed

16 files changed

+382
-44
lines changed

app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,13 @@ class OCFileListFragmentStaticServerIT : AbstractIT() {
231231
sut.storageManager.saveFile(this)
232232
}
233233

234+
OCFile("/Foo%e2%80%aedm.exe").apply {
235+
remoteId = "000000011"
236+
parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId
237+
modificationTimestamp = 1000
238+
sut.storageManager.saveFile(this)
239+
}
240+
234241
sut.addFragment(fragment)
235242
val root = sut.storageManager.getFileByEncryptedRemotePath("/")
236243
fragment.listDirectory(root, false, false)

app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole
3333
import com.nextcloud.client.account.CurrentAccountProvider
3434
import com.nextcloud.client.di.Injectable
3535
import com.nextcloud.client.di.ViewModelFactory
36+
import com.nextcloud.utils.extensions.setVisibleIf
3637
import com.owncloud.android.R
3738
import com.owncloud.android.databinding.FileActionsBottomSheetBinding
3839
import com.owncloud.android.databinding.FileActionsBottomSheetItemBinding
@@ -44,6 +45,7 @@ import com.owncloud.android.lib.resources.files.model.FileLockType
4445
import com.owncloud.android.ui.activity.ComponentsGetter
4546
import com.owncloud.android.utils.DisplayUtils
4647
import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener
48+
import com.owncloud.android.utils.FileStorageUtils
4749
import com.owncloud.android.utils.theme.ViewThemeUtils
4850
import javax.inject.Inject
4951

@@ -204,11 +206,23 @@ class FileActionsBottomSheet :
204206
private fun displayTitle(titleFile: OCFile?) {
205207
val decryptedFileName = titleFile?.decryptedFileName
206208
if (decryptedFileName != null) {
207-
decryptedFileName.let {
208-
binding.title.text = it
209+
val isFolder = titleFile.isFolder
210+
val isRTL = DisplayUtils.isRTL()
211+
val (base, ext) = FileStorageUtils.getFilenameAndExtension(decryptedFileName, isFolder, isRTL)
212+
val titleMaxWidth = DisplayUtils.convertDpToPixel(
213+
requireContext().resources.configuration.screenWidthDp.times(FILENAME_MAX_WIDTH_PERCENTAGE).toFloat(),
214+
context
215+
)
216+
217+
binding.title.maxWidth = titleMaxWidth
218+
binding.title.text = base
219+
binding.extension.setVisibleIf(!isFolder)
220+
if (!isFolder) {
221+
binding.extension.text = ext
209222
}
210223
} else {
211224
binding.title.isVisible = false
225+
binding.extension.isVisible = false
212226
}
213227
}
214228

@@ -300,6 +314,7 @@ class FileActionsBottomSheet :
300314
companion object {
301315
private const val REQUEST_KEY = "REQUEST_KEY_ACTION"
302316
private const val RESULT_KEY_ACTION_ID = "RESULT_KEY_ACTION_ID"
317+
private const val FILENAME_MAX_WIDTH_PERCENTAGE = 0.6
303318

304319
@JvmStatic
305320
@JvmOverloads

app/src/main/java/com/owncloud/android/ui/adapter/GroupfolderListAdapter.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class GroupfolderListAdapter(
5353
listHolder.apply {
5454
fileName.text = file.name
5555
fileSize.text = file.parentFile?.path ?: "/"
56+
extension.visibility = View.GONE
5657
fileSizeSeparator.visibility = View.GONE
5758
lastModification.visibility = View.GONE
5859
checkbox.visibility = View.GONE

app/src/main/java/com/owncloud/android/ui/adapter/ListGridItemViewHolder.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ import android.widget.TextView
1111

1212
internal interface ListGridItemViewHolder : ListViewHolder {
1313
val fileName: TextView
14+
val extension: TextView?
1415
}

app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
import androidx.recyclerview.widget.LinearLayoutManager;
103103
import androidx.recyclerview.widget.RecyclerView;
104104
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
105+
import kotlin.Pair;
105106
import me.zhanghai.android.fastscroll.PopupTextProvider;
106107

107108
/**
@@ -123,6 +124,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
123124
private FileDataStorageManager mStorageManager;
124125
private User user;
125126
private final OCFileListFragmentInterface ocFileListFragmentInterface;
127+
private final boolean isRTL;
126128

127129
private OCFile currentDirectory;
128130
private static final String TAG = OCFileListAdapter.class.getSimpleName();
@@ -195,6 +197,7 @@ public OCFileListAdapter(
195197

196198
// initialise thumbnails cache on background thread
197199
ThumbnailsCacheManager.initDiskCacheAsync();
200+
isRTL = DisplayUtils.isRTL();
198201
}
199202

200203
public boolean isMultiSelect() {
@@ -476,7 +479,7 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi
476479
}
477480

478481
if (holder instanceof ListGridItemViewHolder gridItemViewHolder) {
479-
bindListGridItemViewHolder(gridItemViewHolder, file);
482+
setFilenameAndExtension(gridItemViewHolder, file);
480483
checkVisibilityOfFileFeaturesLayout(gridItemViewHolder);
481484
}
482485

@@ -556,8 +559,53 @@ private void updateLivePhotoIndicators(ListViewHolder holder, OCFile file) {
556559
}
557560
}
558561

559-
private void bindListGridItemViewHolder(ListGridItemViewHolder holder, OCFile file) {
560-
holder.getFileName().setText(mStorageManager.getFilenameConsideringOfflineOperation(file));
562+
private void setFilenameAndExtension(ListGridItemViewHolder holder, OCFile file) {
563+
final String filename = mStorageManager.getFilenameConsideringOfflineOperation(file);
564+
final var pair = FileStorageUtils.getFilenameAndExtension(filename, file.isFolder(), isRTL);
565+
final boolean isFolder = file.isFolder();
566+
567+
if (holder instanceof OCFileListGridItemViewHolder gridItemViewHolder) {
568+
handleGridMode(filename, gridItemViewHolder, pair, file);
569+
} else {
570+
handleListMode(holder, pair, isFolder);
571+
}
572+
}
573+
574+
private void handleGridMode(String filename, OCFileListGridItemViewHolder holder, Pair<String, String> filenamePair, OCFile file) {
575+
boolean containsBidiControlCharacters = FileStorageUtils.containsBidiControlCharacters(filename);
576+
ViewExtensionsKt.setVisibleIf(holder.getFileName(),!containsBidiControlCharacters);
577+
ViewExtensionsKt.setVisibleIf(holder.getBinding().bidiFilenameContainer, containsBidiControlCharacters);
578+
final var extension = holder.getExtension();
579+
580+
if (containsBidiControlCharacters) {
581+
holder.getBidiFilename().setText(filenamePair.getFirst());
582+
if (extension != null) {
583+
extension.setText(filenamePair.getSecond());
584+
}
585+
holder.getBinding().more.setVisibility(View.GONE);
586+
holder.getBinding().bidiMore.setOnClickListener(v -> ocFileListFragmentInterface.onOverflowIconClicked(file, v));
587+
} else {
588+
holder.getFileName().setText(filename);
589+
if (extension != null) {
590+
extension.setVisibility(View.GONE);
591+
}
592+
}
593+
}
594+
595+
private void handleListMode(ListGridItemViewHolder holder,
596+
Pair<String, String> filenamePair,
597+
boolean isFolder) {
598+
holder.getFileName().setText(filenamePair.getFirst());
599+
600+
final var extension = holder.getExtension();
601+
if (extension != null) {
602+
if (isFolder) {
603+
extension.setVisibility(View.GONE);
604+
} else {
605+
extension.setVisibility(View.VISIBLE);
606+
extension.setText(filenamePair.getSecond());
607+
}
608+
}
561609
}
562610

563611
private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) {
@@ -1145,6 +1193,10 @@ public void cancelAllPendingTasks() {
11451193
ocFileListDelegate.cancelAllPendingTasks();
11461194
}
11471195

1196+
public boolean isGridView() {
1197+
return gridView;
1198+
}
1199+
11481200
public void setGridView(boolean bool) {
11491201
gridView = bool;
11501202
}

app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,26 @@ import android.widget.ImageButton
1212
import android.widget.ImageView
1313
import android.widget.LinearLayout
1414
import android.widget.TextView
15+
import androidx.core.view.isVisible
1516
import androidx.recyclerview.widget.RecyclerView
1617
import com.elyeproj.loaderviewlibrary.LoaderImageView
1718
import com.owncloud.android.databinding.GridItemBinding
1819

19-
internal class OCFileListGridItemViewHolder(var binding: GridItemBinding) :
20+
class OCFileListGridItemViewHolder(var binding: GridItemBinding) :
2021
RecyclerView.ViewHolder(
2122
binding.root
2223
),
2324
ListGridItemViewHolder {
25+
val bidiFilename: TextView
26+
get() = binding.bidiFilename
2427
override val fileName: TextView
2528
get() = binding.Filename
29+
override val extension: TextView?
30+
get() = if (binding.bidiFilenameContainer.isVisible) {
31+
binding.bidiExtension
32+
} else {
33+
null
34+
}
2635
override val thumbnail: ImageView
2736
get() = binding.thumbnail
2837

@@ -56,8 +65,11 @@ internal class OCFileListGridItemViewHolder(var binding: GridItemBinding) :
5665
override val fileFeaturesLayout: LinearLayout
5766
get() = binding.fileFeaturesLayout
5867
override val more: ImageButton
59-
get() = binding.more
60-
68+
get() = if (binding.bidiFilenameContainer.isVisible) {
69+
binding.bidiMore
70+
} else {
71+
binding.more
72+
}
6173
init {
6274
binding.favoriteAction.drawable.mutate()
6375
}

app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ internal class OCFileListItemViewHolder(private var binding: ListItemBinding) :
4343
get() = binding.sharedAvatars
4444
override val fileName: TextView
4545
get() = binding.Filename
46+
override val extension: TextView
47+
get() = binding.extension
4648
override val thumbnail: ImageView
4749
get() = binding.thumbnailLayout.thumbnail
4850
override val tagsGroup: ChipGroup

app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import android.widget.ImageButton
1212
import android.widget.ImageView
1313
import android.widget.LinearLayout
1414
import android.widget.TextView
15+
import androidx.core.view.isVisible
1516
import androidx.recyclerview.widget.RecyclerView
1617
import com.elyeproj.loaderviewlibrary.LoaderImageView
1718
import com.owncloud.android.databinding.GridItemBinding
@@ -26,7 +27,11 @@ internal class OCFileListViewHolder(var binding: GridItemBinding) :
2627
get() = binding.thumbnail
2728

2829
override val imageFileName: TextView
29-
get() = binding.Filename
30+
get() = if (binding.bidiFilenameContainer.isVisible) {
31+
binding.bidiFilename
32+
} else {
33+
binding.Filename
34+
}
3035

3136
override fun showVideoOverlay() {
3237
// noop
@@ -47,7 +52,11 @@ internal class OCFileListViewHolder(var binding: GridItemBinding) :
4752
override val unreadComments: ImageView
4853
get() = binding.unreadComments
4954
override val more: ImageButton
50-
get() = binding.more
55+
get() = if (binding.bidiFilenameContainer.isVisible) {
56+
binding.bidiMore
57+
} else {
58+
binding.more
59+
}
5160
override val fileFeaturesLayout: LinearLayout
5261
get() = binding.fileFeaturesLayout
5362
override val gridLivePhotoIndicator: ImageView

app/src/main/java/com/owncloud/android/utils/DisplayUtils.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import android.app.Activity;
2727
import android.content.Context;
2828
import android.content.Intent;
29+
import android.content.res.Configuration;
2930
import android.content.res.Resources;
3031
import android.graphics.Bitmap;
3132
import android.graphics.Color;
@@ -685,6 +686,14 @@ public static float convertPixelToDp(int px, Context context) {
685686
return px * (DisplayMetrics.DENSITY_DEFAULT / (float) metrics.densityDpi);
686687
}
687688

689+
public static boolean isRTL() {
690+
return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
691+
}
692+
693+
public static boolean isOrientationLandscape() {
694+
return MainApp.getAppContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
695+
}
696+
688697
static public void showServerOutdatedSnackbar(Activity activity, int length) {
689698
Snackbar.make(activity.findViewById(android.R.id.content),
690699
R.string.outdated_server, length)

app/src/main/java/com/owncloud/android/utils/FileStorageUtils.java

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,19 @@
2727
import com.owncloud.android.datamodel.OCFile;
2828
import com.owncloud.android.lib.common.utils.Log_OC;
2929
import com.owncloud.android.lib.resources.files.model.RemoteFile;
30-
import com.owncloud.android.lib.resources.shares.ShareeUser;
3130
import com.owncloud.android.ui.helpers.FileOperationsHelper;
3231

32+
import org.apache.commons.io.FilenameUtils;
33+
3334
import java.io.File;
3435
import java.io.FileInputStream;
3536
import java.io.FileOutputStream;
3637
import java.io.IOException;
3738
import java.io.InputStream;
3839
import java.io.OutputStream;
40+
import java.io.UnsupportedEncodingException;
41+
import java.net.URLDecoder;
42+
import java.nio.charset.StandardCharsets;
3943
import java.text.DateFormat;
4044
import java.text.SimpleDateFormat;
4145
import java.util.ArrayList;
@@ -53,6 +57,7 @@
5357
import androidx.annotation.VisibleForTesting;
5458
import androidx.core.app.ActivityCompat;
5559
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
60+
import kotlin.Pair;
5661

5762
/**
5863
* Static methods to help in access to local file system.
@@ -69,6 +74,56 @@ private FileStorageUtils() {
6974
// utility class -> private constructor
7075
}
7176

77+
public static boolean containsBidiControlCharacters(String filename) {
78+
if (filename == null) return false;
79+
80+
String decoded;
81+
try {
82+
decoded = URLDecoder.decode(filename, StandardCharsets.UTF_8.toString());
83+
} catch (UnsupportedEncodingException e) {
84+
return false;
85+
}
86+
87+
int[] bidiControlCharacters = {
88+
0x202A, 0x202B, 0x202C, 0x202D, 0x202E,
89+
0x200E, 0x200F, 0x2066, 0x2067, 0x2068,
90+
0x2069, 0x061C
91+
};
92+
93+
for (int i = 0; i < decoded.length(); i++) {
94+
int codePoint = decoded.codePointAt(i);
95+
for (int chars : bidiControlCharacters) {
96+
if (codePoint == chars) {
97+
return true;
98+
}
99+
}
100+
}
101+
102+
for (char c : decoded.toCharArray()) {
103+
if (c < 32) return true;
104+
}
105+
106+
return false;
107+
}
108+
109+
public static Pair<String,String> getFilenameAndExtension(String filename, boolean isFolder, boolean isRTL) {
110+
if (isFolder) {
111+
return new Pair<>(filename, "");
112+
}
113+
114+
final String base = FilenameUtils.getBaseName(filename);
115+
String extension = FilenameUtils.getExtension(filename);
116+
if (!extension.isEmpty()) {
117+
extension = StringConstants.DOT + extension;
118+
}
119+
120+
if (isRTL) {
121+
return new Pair<>(extension, base);
122+
} else {
123+
return new Pair<>(base, extension);
124+
}
125+
}
126+
72127
public static boolean isValidExtFilename(String name) {
73128
for (int i = 0; i < name.length(); i++) {
74129
char c = name.charAt(i);

0 commit comments

Comments
 (0)