fix(Spotify - Sanitize sharing links): Bring back original logic

Close https://github.com/anddea/revanced-patches/issues/1244
This commit is contained in:
Aaron Veil 2025-10-01 19:36:32 +03:00
parent fd42fc54b1
commit 40a14f46aa
4 changed files with 72 additions and 139 deletions

View file

@ -1,38 +1,36 @@
package app.revanced.extension.spotify.misc.privacy;
import android.net.Uri;
import app.revanced.extension.shared.utils.Logger;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.List;
@SuppressWarnings("unused")
public final class SanitizeSharingLinksPatch {
private static final List<String> PARAMETERS_TO_REMOVE = Arrays.asList("context", "pi", "si", "utm_source");
/**
* Injection point.
* Removes specified tracking parameters and anything after them from a URL string.
*
* @param url The original URL string.
* @return The URL string with the tracking parameters removed, or the original URL if none found or if input is null.
*/
public static String sanitizeUrl(String url, String parameters) {
try {
Uri uri = Uri.parse(url);
Uri.Builder builder = uri.buildUpon().clearQuery();
Set<String> paramsToRemove = new HashSet<>(Arrays.asList(parameters.split(",\\s*")));
for (String paramName : uri.getQueryParameterNames()) {
if (!paramsToRemove.contains(paramName)) {
for (String value : uri.getQueryParameters(paramName)) {
builder.appendQueryParameter(paramName, value);
}
}
}
String sanitizedUrl = builder.build().toString();
Logger.printInfo(() -> "Sanitized url " + url + " to " + sanitizedUrl);
return sanitizedUrl;
} catch (Exception ex) {
Logger.printException(() -> "sanitizeUrl failure with " + url, ex);
return url;
public static String sanitizeUrl(String url) {
if (url == null) {
return null;
}
for (String param : PARAMETERS_TO_REMOVE) {
// Consider everything after "?" as a tracking parameter,
// since nothing is actually needed after the artist/album/song id
// to work properly.
// It helps us avoid unknown tracking parameters left in the URL.
String paramPattern = "?" + param + "=";
int paramIndex = url.indexOf(paramPattern);
if (paramIndex >= 0) {
// Found the parameter, return the substring before it
return url.substring(0, paramIndex);
}
}
// No parameters found, return the original URL
return url;
}
}

View file

@ -756,6 +756,10 @@ public final class app/revanced/patches/spotify/extended/LyricsSearchPatchKt {
public static final fun getMediaSessionMetadataConstructorFingerprint ()Lapp/revanced/patcher/Fingerprint;
}
public final class app/revanced/patches/spotify/extended/SanitizeSharingLinksPatchKt {
public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/spotify/extended/SetActivityContextPatchKt {
public static final fun getMainActivityOnCreateFingerprint ()Lapp/revanced/patcher/Fingerprint;
public static final fun getSetActivityContextPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
@ -789,8 +793,8 @@ public final class app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProvider
public static final fun getChangeLyricsProviderPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt {
public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
public final class app/revanced/patches/spotify/misc/privacy/FingerprintsKt {
public static final fun getShareLinkFingerprint ()Lapp/revanced/patcher/Fingerprint;
}
public final class app/revanced/patches/spotify/misc/widgets/FixThirdPartyLaunchersWidgetsKt {

View file

@ -1,48 +1,33 @@
package app.revanced.patches.spotify.misc.privacy
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.fingerprint
import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.StringReference
internal val shareCopyUrlFingerprint = fingerprint {
returns("Ljava/lang/Object;")
parameters("Ljava/lang/Object;")
strings("clipboard", "Spotify Link")
custom { method, _ ->
method.name == "invokeSuspend"
}
}
internal val oldShareCopyUrlFingerprint = fingerprint {
returns("Ljava/lang/Object;")
parameters("Ljava/lang/Object;")
strings("clipboard", "createNewSession failed")
custom { method, _ ->
method.name == "apply"
}
}
internal val formatAndroidShareSheetUrlFingerprint = fingerprint {
returns("Ljava/lang/String;")
parameters("L", "Ljava/lang/String;")
opcodes(
Opcode.GOTO,
Opcode.IF_EQZ,
Opcode.INVOKE_STATIC,
Opcode.MOVE_RESULT_OBJECT,
Opcode.RETURN_OBJECT
val shareLinkFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
parameters(
"Ljava/lang/String;",
"Ljava/lang/String;",
"Ljava/lang/String;",
"Ljava/lang/String;"
)
literal {
'\n'.code.toLong()
}
}
returns("V")
internal val oldFormatAndroidShareSheetUrlFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC)
returns("Ljava/lang/String;")
parameters("Lcom/spotify/share/social/sharedata/ShareData;", "Ljava/lang/String;")
literal {
'\n'.code.toLong()
custom { _, classDef ->
val toStringMethod = classDef.methods.firstOrNull {
it.name == "toString" && it.parameters.isEmpty() && it.returnType == "Ljava/lang/String;"
} ?: return@custom false
val toStringInstructions = toStringMethod.instructionsOrNull ?: return@custom false
toStringInstructions.any { instruction ->
instruction.opcode == Opcode.CONST_STRING &&
(instruction as? ReferenceInstruction)?.reference?.let { ref ->
(ref as? StringReference)?.string?.startsWith("ShareUrl(url=") == true
} == true
}
}
}

View file

@ -1,22 +1,21 @@
package app.revanced.patches.spotify.misc.privacy
package app.revanced.patches.spotify.extended
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.fingerprint
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
import app.revanced.patches.spotify.misc.privacy.shareLinkFingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch;"
private val SHARE_PARAMETERS = setOf("context", "pi", "si", "utm_source")
@Suppress("unused")
val sanitizeSharingLinksPatch = bytecodePatch(
name = "Sanitize sharing links",
@ -24,73 +23,20 @@ val sanitizeSharingLinksPatch = bytecodePatch(
) {
compatibleWith("com.spotify.music")
dependsOn(sharedExtensionPatch)
val shareParameters by stringOption(
key = "shareParameters",
default = SHARE_PARAMETERS.joinToString(", "),
title = "Parameters to remove",
description = "A list of parameters to be removed from sharing links, separated by commas.",
required = true,
)
execute {
val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" +
"sanitizeUrl(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"
val copyFingerprint = if (shareCopyUrlFingerprint.originalMethodOrNull != null) {
shareCopyUrlFingerprint
} else {
oldShareCopyUrlFingerprint
val originalMethod = shareLinkFingerprint.method
val invokeDirectIndex = originalMethod.indexOfFirstInstructionOrThrow {
opcode == Opcode.INVOKE_DIRECT &&
(this as ReferenceInstruction).reference is MethodReference &&
(getReference<MethodReference>()?.name == "<init>") &&
(getReference<MethodReference>()?.definingClass == "Ljava/lang/Object;")
}
copyFingerprint.method.apply {
val newPlainTextInvokeIndex = indexOfFirstInstructionOrThrow {
getReference<MethodReference>()?.name == "newPlainText"
}
val register = getInstruction<FiveRegisterInstruction>(newPlainTextInvokeIndex).registerD
val freeRegister = getInstruction<OneRegisterInstruction>(newPlainTextInvokeIndex - 1).registerA
val smaliCodeToInsert = """
invoke-static {p1}, $EXTENSION_CLASS_DESCRIPTOR->sanitizeUrl(Ljava/lang/String;)Ljava/lang/String;
move-result-object p1
""".trimIndent()
addInstructions(
newPlainTextInvokeIndex,
"""
const-string v$freeRegister, "$shareParameters"
invoke-static { v$register, v$freeRegister }, $extensionMethodDescriptor
move-result-object v$register
"""
)
}
// Android native share sheet is used for all other quick share types (X, WhatsApp, etc).
val shareUrlParameter: String
val shareSheetFingerprint = if (formatAndroidShareSheetUrlFingerprint.originalMethodOrNull != null) {
val methodAccessFlags = formatAndroidShareSheetUrlFingerprint.originalMethod
shareUrlParameter = if (AccessFlags.STATIC.isSet(methodAccessFlags.accessFlags)) {
// In newer implementations the method is static, so p0 is not `this`.
"p1"
} else {
// In older implementations the method is not static, making it so p0 is `this`.
// For that reason, add one to the parameter register.
"p2"
}
formatAndroidShareSheetUrlFingerprint
} else {
shareUrlParameter = "p2"
oldFormatAndroidShareSheetUrlFingerprint
}
shareSheetFingerprint.method.apply {
val freeRegister = getInstruction<OneRegisterInstruction>(0).registerA
addInstructions(
0,
"""
const-string v$freeRegister, "$shareParameters"
invoke-static { $shareUrlParameter, v$freeRegister }, $extensionMethodDescriptor
move-result-object $shareUrlParameter
"""
)
}
originalMethod.addInstructions(invokeDirectIndex + 1, smaliCodeToInsert)
}
}