ma1 pushed to branch mullvad-browser-153.4.0esr-16.0-1 at The Tor Project / Applications / Mullvad Browser
Commits:
6 changed files:
- mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt
- mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt
- mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
- mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt
- toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html
- toolkit/components/extensions/webrequest/WebRequestUpload.sys.mjs
Changes:
| ... | ... | @@ -28,6 +28,7 @@ import mozilla.components.feature.downloads.AbstractFetchDownloadService.Compani |
| 28 | 28 | import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_RESUME
|
| 29 | 29 | import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_TRY_AGAIN
|
| 30 | 30 | import mozilla.components.feature.downloads.AbstractFetchDownloadService.DownloadJobState
|
| 31 | +import mozilla.components.support.ktx.kotlin.truncateMiddle
|
|
| 31 | 32 | import mozilla.components.support.utils.DownloadFileUtils
|
| 32 | 33 | import kotlin.random.Random
|
| 33 | 34 | import kotlin.time.Duration.Companion.seconds
|
| ... | ... | @@ -40,6 +41,7 @@ internal object DownloadNotification { |
| 40 | 41 | internal const val NOTIFICATION_DOWNLOAD_GROUP_ID = 100
|
| 41 | 42 | private const val LEGACY_NOTIFICATION_CHANNEL_ID = "Downloads"
|
| 42 | 43 | internal const val PERCENTAGE_MULTIPLIER = 100
|
| 44 | + private const val MAX_FILENAME_LENGTH = 25
|
|
| 43 | 45 | |
| 44 | 46 | @VisibleForTesting
|
| 45 | 47 | internal fun createDownloadGroupNotification(
|
| ... | ... | @@ -89,11 +91,12 @@ internal object DownloadNotification { |
| 89 | 91 | val channelId = ensureChannelExists(context)
|
| 90 | 92 | val isIndeterminate = downloadState.isIndeterminate()
|
| 91 | 93 | val percentCopied = downloadState.getPercent() ?: -1
|
| 94 | + val fileName = downloadState.fileName?.truncateMiddle(MAX_FILENAME_LENGTH).orEmpty()
|
|
| 92 | 95 | |
| 93 | 96 | return NotificationCompat.Builder(context, channelId)
|
| 94 | 97 | .setStyle(
|
| 95 | 98 | NotificationCompat.BigTextStyle()
|
| 96 | - .setBigContentTitle(downloadState.fileName.orEmpty())
|
|
| 99 | + .setBigContentTitle(fileName)
|
|
| 97 | 100 | .setSummaryText(
|
| 98 | 101 | formatDownloadTimeRemaining(
|
| 99 | 102 | context = context,
|
| ... | ... | @@ -105,10 +108,8 @@ internal object DownloadNotification { |
| 105 | 108 | ),
|
| 106 | 109 | )
|
| 107 | 110 | .setSmallIcon(R.drawable.mozac_feature_download_ic_ongoing_download)
|
| 108 | - .setContentTitle(downloadState.fileName.orEmpty())
|
|
| 109 | - .setContentText(
|
|
| 110 | - downloadState.getProgress(fileSizeFormatter = fileSizeFormatter),
|
|
| 111 | - )
|
|
| 111 | + .setContentTitle(fileName)
|
|
| 112 | + .setContentText(downloadState.getProgress(fileSizeFormatter = fileSizeFormatter))
|
|
| 112 | 113 | .setColor(ContextCompat.getColor(context, notificationAccentColor))
|
| 113 | 114 | .setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
| 114 | 115 | .setProgress(PERCENTAGE_MULTIPLIER, percentCopied, isIndeterminate)
|
| ... | ... | @@ -132,10 +133,11 @@ internal object DownloadNotification { |
| 132 | 133 | notificationAccentColor: Int,
|
| 133 | 134 | ): Notification {
|
| 134 | 135 | val channelId = ensureChannelExists(context)
|
| 136 | + val fileName = downloadState.fileName?.truncateMiddle(MAX_FILENAME_LENGTH).orEmpty()
|
|
| 135 | 137 | |
| 136 | 138 | return NotificationCompat.Builder(context, channelId)
|
| 137 | 139 | .setSmallIcon(R.drawable.mozac_feature_download_ic_download)
|
| 138 | - .setContentTitle(downloadState.fileName)
|
|
| 140 | + .setContentTitle(fileName)
|
|
| 139 | 141 | .setContentText(
|
| 140 | 142 | context.applicationContext.getString(R.string.mozac_feature_downloads_paused_notification_text),
|
| 141 | 143 | )
|
| ... | ... | @@ -163,10 +165,11 @@ internal object DownloadNotification { |
| 163 | 165 | contentIntent: PendingIntent = createOpenFilePendingIntent(context, downloadState, downloadFileUtils),
|
| 164 | 166 | ): Notification {
|
| 165 | 167 | val channelId = ensureChannelExists(context)
|
| 168 | + val fileName = downloadState.fileName?.truncateMiddle(MAX_FILENAME_LENGTH).orEmpty()
|
|
| 166 | 169 | |
| 167 | 170 | return NotificationCompat.Builder(context, channelId)
|
| 168 | 171 | .setSmallIcon(R.drawable.mozac_feature_download_ic_download_complete)
|
| 169 | - .setContentTitle(downloadState.fileName)
|
|
| 172 | + .setContentTitle(fileName)
|
|
| 170 | 173 | .setWhen(createdTime)
|
| 171 | 174 | .setOnlyAlertOnce(true)
|
| 172 | 175 | .setContentText(
|
| ... | ... | @@ -191,10 +194,11 @@ internal object DownloadNotification { |
| 191 | 194 | notificationAccentColor: Int,
|
| 192 | 195 | ): Notification {
|
| 193 | 196 | val channelId = ensureChannelExists(context)
|
| 197 | + val fileName = downloadState.fileName?.truncateMiddle(MAX_FILENAME_LENGTH).orEmpty()
|
|
| 194 | 198 | |
| 195 | 199 | return NotificationCompat.Builder(context, channelId)
|
| 196 | 200 | .setSmallIcon(R.drawable.mozac_feature_download_ic_download_failed)
|
| 197 | - .setContentTitle(downloadState.fileName)
|
|
| 201 | + .setContentTitle(fileName)
|
|
| 198 | 202 | .setContentText(
|
| 199 | 203 | context.applicationContext.getString(R.string.mozac_feature_downloads_failed_notification_text2),
|
| 200 | 204 | )
|
| ... | ... | @@ -217,7 +221,8 @@ internal object DownloadNotification { |
| 217 | 221 | notifications: List<DownloadJobState>,
|
| 218 | 222 | ): List<String> {
|
| 219 | 223 | return notifications.take(2).map { downloadState ->
|
| 220 | - "${downloadState.state.fileName} ${downloadState.state.getStatusDescription(
|
|
| 224 | + val fileName = downloadState.state.fileName.orEmpty().truncateMiddle(MAX_FILENAME_LENGTH)
|
|
| 225 | + "$fileName ${downloadState.state.getStatusDescription(
|
|
| 221 | 226 | context = context,
|
| 222 | 227 | fileSizeFormatter = fileSizeFormatter,
|
| 223 | 228 | )}"
|
| ... | ... | @@ -4,6 +4,7 @@ |
| 4 | 4 | |
| 5 | 5 | package mozilla.components.feature.downloads
|
| 6 | 6 | |
| 7 | +import android.app.Notification
|
|
| 7 | 8 | import android.app.PendingIntent
|
| 8 | 9 | import androidx.core.app.NotificationCompat
|
| 9 | 10 | import androidx.core.app.NotificationCompat.EXTRA_PROGRESS
|
| ... | ... | @@ -307,6 +308,32 @@ class DownloadNotificationTest { |
| 307 | 308 | assertEquals(true, notificationDownloadWithNoSize.extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE))
|
| 308 | 309 | }
|
| 309 | 310 | |
| 311 | + @Test
|
|
| 312 | + fun `createOngoingDownloadNotification with long filename truncates from middle`() {
|
|
| 313 | + val longFileName = "this_is_a_very_long_filename_that_exceeds_forty_characters_to_test_truncation.pdf"
|
|
| 314 | + val downloadState =
|
|
| 315 | + DownloadState(
|
|
| 316 | + fileName = longFileName,
|
|
| 317 | + url = "mozilla.org/file.pdf",
|
|
| 318 | + contentLength = 100L,
|
|
| 319 | + currentBytesCopied = 10,
|
|
| 320 | + status = DownloadState.Status.DOWNLOADING,
|
|
| 321 | + )
|
|
| 322 | + val style = AbstractFetchDownloadService.Style()
|
|
| 323 | + |
|
| 324 | + val notification =
|
|
| 325 | + DownloadNotification.createOngoingDownloadNotification(
|
|
| 326 | + context = testContext,
|
|
| 327 | + downloadState = downloadState,
|
|
| 328 | + fileSizeFormatter = fakeFileSizeFormatter,
|
|
| 329 | + notificationAccentColor = style.notificationAccentColor,
|
|
| 330 | + downloadEstimator = DownloadEstimator(dateTimeProvider = FakeDateTimeProvider()),
|
|
| 331 | + )
|
|
| 332 | + |
|
| 333 | + val expectedTitle = "this_is_a_ve…uncation.pdf"
|
|
| 334 | + assertEquals(expectedTitle, notification.extras.getCharSequence(Notification.EXTRA_TITLE).toString())
|
|
| 335 | + }
|
|
| 336 | + |
|
| 310 | 337 | @Test
|
| 311 | 338 | fun getOngoingNotificationAccentColor() {
|
| 312 | 339 | val download = DownloadJobState(
|
| ... | ... | @@ -498,3 +498,24 @@ internal fun String.extractBase6RawString(): String? { |
| 498 | 498 | contentString
|
| 499 | 499 | }
|
| 500 | 500 | }
|
| 501 | + |
|
| 502 | +/**
|
|
| 503 | + * Truncates the string from the middle if it exceeds [maxLength].
|
|
| 504 | + *
|
|
| 505 | + * @param maxLength The maximum length of the string.
|
|
| 506 | + * @param ellipsis The string to use as ellipsis, defaults to "…".
|
|
| 507 | + * @return The truncated string.
|
|
| 508 | + */
|
|
| 509 | +fun String.truncateMiddle(maxLength: Int, ellipsis: String = "…"): String {
|
|
| 510 | + if (length <= maxLength) {
|
|
| 511 | + return this
|
|
| 512 | + }
|
|
| 513 | + if (maxLength <= ellipsis.length) {
|
|
| 514 | + return take(maxLength)
|
|
| 515 | + }
|
|
| 516 | + |
|
| 517 | + val half = (maxLength - ellipsis.length) / 2
|
|
| 518 | + val start = take(half)
|
|
| 519 | + val end = takeLast(maxLength - ellipsis.length - half)
|
|
| 520 | + return "$start$ellipsis$end"
|
|
| 521 | +} |
| ... | ... | @@ -306,6 +306,16 @@ class StringTest { |
| 306 | 306 | }
|
| 307 | 307 | }
|
| 308 | 308 | |
| 309 | + @Test
|
|
| 310 | + fun truncateMiddle() {
|
|
| 311 | + assertEquals("123…890", "1234567890".truncateMiddle(7))
|
|
| 312 | + assertEquals("1234567890", "1234567890".truncateMiddle(10))
|
|
| 313 | + assertEquals("1234567890", "1234567890".truncateMiddle(20))
|
|
| 314 | + assertEquals("1…0", "1234567890".truncateMiddle(3))
|
|
| 315 | + assertEquals("1", "1234567890".truncateMiddle(1))
|
|
| 316 | + assertEquals("123...890", "1234567890".truncateMiddle(9, "..."))
|
|
| 317 | + }
|
|
| 318 | + |
|
| 309 | 319 | @Test
|
| 310 | 320 | fun `WHEN a string contains utf 8 encoded characters or illegal filename characters decode decodes it`() {
|
| 311 | 321 | // List of pairs of encoded strings or illegal filename characters and their expected decoded results
|
| ... | ... | @@ -27,6 +27,7 @@ |
| 27 | 27 | enctype="multipart/form-data"
|
| 28 | 28 | >
|
| 29 | 29 | <input type="text" name="textInput2" value="value2">
|
| 30 | +<input type="text" name="__proto__" value="regression test for bug 2061470">
|
|
| 30 | 31 | <input type="file" name="testFile">
|
| 31 | 32 | <input type="file" name="emptyFile">
|
| 32 | 33 | </form>
|
| ... | ... | @@ -161,11 +162,19 @@ add_task(async function test_xhr_forms() { |
| 161 | 162 | }
|
| 162 | 163 | let action = new URL(form.action);
|
| 163 | 164 | let formData = new FormData(form);
|
| 164 | - let webRequestFD = {};
|
|
| 165 | 165 | |
| 166 | 166 | let updateActionURL = () => {
|
| 167 | + let webRequestFD = {};
|
|
| 167 | 168 | for (let name of formData.keys()) {
|
| 168 | - webRequestFD[name] = name in uploads ? [uploads[name].fileName] : formData.getAll(name);
|
|
| 169 | + if (Object.hasOwn(webRequestFD, name)) {
|
|
| 170 | + // Ignore duplicate keys; formData.getAll already read all values.
|
|
| 171 | + continue;
|
|
| 172 | + }
|
|
| 173 | + const value = Object.hasOwn(uploads, name) ? [uploads[name].fileName] : formData.getAll(name);
|
|
| 174 | + // Cannot use webRequestFD[name] = value, because for "__proto__" as
|
|
| 175 | + // name, that would trigger the Object.prototype.__proto__ setter
|
|
| 176 | + // instead of defining a data property.
|
|
| 177 | + Object.defineProperty(webRequestFD, name, { value, enumerable: true });
|
|
| 169 | 178 | }
|
| 170 | 179 | action.searchParams.set("upload", JSON.stringify(webRequestFD));
|
| 171 | 180 | action.searchParams.set("enctype", form.enctype);
|
| ... | ... | @@ -117,22 +117,6 @@ class Headers extends Map { |
| 117 | 117 | }
|
| 118 | 118 | }
|
| 119 | 119 | |
| 120 | -/**
|
|
| 121 | - * Creates a new Object with a corresponding property for every
|
|
| 122 | - * key-value pair in the given Map.
|
|
| 123 | - *
|
|
| 124 | - * @param {Map} map
|
|
| 125 | - * The map to convert.
|
|
| 126 | - * @returns {object}
|
|
| 127 | - */
|
|
| 128 | -function mapToObject(map) {
|
|
| 129 | - let result = {};
|
|
| 130 | - for (let [key, value] of map) {
|
|
| 131 | - result[key] = value;
|
|
| 132 | - }
|
|
| 133 | - return result;
|
|
| 134 | -}
|
|
| 135 | - |
|
| 136 | 120 | /**
|
| 137 | 121 | * Rewinds the given seekable input stream to its beginning, and catches
|
| 138 | 122 | * any resulting errors.
|
| ... | ... | @@ -446,7 +430,7 @@ function createFormData(stream, channel, lenient) { |
| 446 | 430 | try {
|
| 447 | 431 | let formData = parseFormData(stream, channel, lenient);
|
| 448 | 432 | if (formData) {
|
| 449 | - return mapToObject(formData);
|
|
| 433 | + return Object.fromEntries(formData);
|
|
| 450 | 434 | }
|
| 451 | 435 | } catch (e) {
|
| 452 | 436 | Cu.reportError(e);
|