aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt
blob: 8ee857520ea4a634449f86f89279bf816100c240 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/*
 * Copyright 2018 Allan Wang
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.pitchedapps.frost.facebook

import android.net.Uri
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.urlEncode
import java.net.URLDecoder
import java.nio.charset.StandardCharsets

/**
 * Created by Allan Wang on 2017-07-07.
 *
 * Custom url builder so we can easily test it without the Android framework
 */
inline val String.formattedFbUrl: String
    get() = FbUrlFormatter(this).toString()

inline val Uri.formattedFbUri: Uri
    get() {
        val url = toString()
        return if (url.startsWith("http")) Uri.parse(url.formattedFbUrl) else this
    }

class FbUrlFormatter(url: String) {
    private val queries = mutableMapOf<String, String>()
    private val cleaned: String

    /**
     * Formats all facebook urls
     *
     * The order is very important:
     * 1. Wrapper links (discardables) are stripped away, resulting in the actual link
     * 2. CSS encoding is converted to normal encoding
     * 3. Url is completely decoded
     * 4. Url is split into sections
     */
    init {
        cleaned = clean(url)
    }

    fun clean(url: String): String {
        if (url.isBlank()) return ""
        var cleanedUrl = url
        if (cleanedUrl.startsWith("#!")) cleanedUrl = cleanedUrl.substring(2)
        val urlRef = cleanedUrl
        discardable.forEach { cleanedUrl = cleanedUrl.replace(it, "", true) }
        val changed = cleanedUrl != urlRef
        converter.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) }
        try {
            cleanedUrl = URLDecoder.decode(cleanedUrl, StandardCharsets.UTF_8.name())
        } catch (e: Exception) {
            L.e(e) { "Failed url formatting" }
            return url
        }
        cleanedUrl = cleanedUrl.replace("&amp;", "&")
        if (changed && !cleanedUrl.contains("?")) // ensure we aren't missing '?'
            cleanedUrl = cleanedUrl.replaceFirst("&", "?")
        val qm = cleanedUrl.indexOf("?")
        if (qm > -1) {
            cleanedUrl.substring(qm + 1).split("&").forEach {
                val p = it.split("=")
                queries[p[0]] = p.elementAtOrNull(1) ?: ""
            }
            cleanedUrl = cleanedUrl.substring(0, qm)
        }
        discardableQueries.forEach { queries.remove(it) }
        // Convert desktop urls to mobile ones
        cleanedUrl = cleanedUrl.replace(WWW_FACEBOOK_COM, FACEBOOK_BASE_COM)
        if (cleanedUrl.startsWith("/")) cleanedUrl = FB_URL_BASE + cleanedUrl.substring(1)
        cleanedUrl = cleanedUrl.replaceFirst(
            ".facebook.com//",
            ".facebook.com/"
        ) // sometimes we are given a bad url
        L.v { "Formatted url from $url to $cleanedUrl" }
        return cleanedUrl
    }

    override fun toString(): String = buildString {
        append(cleaned)
        if (queries.isNotEmpty()) {
            append("?")
            queries.forEach { (k, v) ->
                if (v.isEmpty()) {
                    append("${k.urlEncode()}&")
                } else {
                    append("${k.urlEncode()}=${v.urlEncode()}&")
                }
            }
        }
    }.removeSuffix("&")

    fun toLogList(): List<String> {
        val list = mutableListOf(cleaned)
        queries.forEach { (k, v) -> list.add("\n- $k\t=\t$v") }
        list.add("\n\n${toString()}")
        return list
    }

    companion object {

        const val VIDEO_REDIRECT = "/video_redirect/?src="

        /**
         * Items here are explicitly removed from the url
         * Taken from FaceSlim
         * https://github.com/indywidualny/FaceSlim/blob/master/app/src/main/java/org/indywidualni/fblite/util/Miscellany.java
         *
         * Note: Typically, in this case, the redirect url should have all the necessary queries
         * I am unsure how Facebook reacts in all cases, so the ones after the redirect are appended on afterwards
         * That shouldn't break anything
         */
        val discardable = arrayOf(
            "http://lm.facebook.com/l.php?u=",
            "https://lm.facebook.com/l.php?u=",
            "http://m.facebook.com/l.php?u=",
            "https://m.facebook.com/l.php?u=",
            "http://touch.facebook.com/l.php?u=",
            "https://touch.facebook.com/l.php?u=",
            VIDEO_REDIRECT
        )

        /**
         * Queries that are not necessary for independent links
         *
         * acontext is not required for "friends interested in" notifications
         */
        val discardableQueries = arrayOf(
            "ref",
            "refid",
            "SharedWith",
            "fbclid",
            "h",
            "_ft_",
            "_tn_",
            "_xt_",
            "bacr",
            "frefs",
            "hc_ref",
            "loc_ref",
            "pn_ref"
        )

        val converter = listOf(
            "\\3C " to "%3C", "\\3E " to "%3E", "\\23 " to "%23", "\\25 " to "%25",
            "\\7B " to "%7B", "\\7D " to "%7D", "\\7C " to "%7C", "\\5C " to "%5C",
            "\\5E " to "%5E", "\\7E " to "%7E", "\\5B " to "%5B", "\\5D " to "%5D",
            "\\60 " to "%60", "\\3B " to "%3B", "\\2F " to "%2F", "\\3F " to "%3F",
            "\\3A " to "%3A", "\\40 " to "%40", "\\3D " to "%3D", "\\26 " to "%26",
            "\\24 " to "%24", "\\2B " to "%2B", "\\22 " to "%22", "\\2C " to "%2C",
            "\\20 " to "%20"
        )
    }
}