From c5e769deabeb80d7257b85c5c3d802cf46e6b191 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Fri, 13 Apr 2018 00:13:28 -0400 Subject: Fix view full image (#882) * Test including full photo viewer * Test cookie in glide * Fix parser and add redirects to view full image * Update changelog --- .../pitchedapps/frost/activities/ImageActivity.kt | 21 +++++++++++--- .../com/pitchedapps/frost/facebook/FbCookie.kt | 4 +++ .../com/pitchedapps/frost/facebook/FbRegex.kt | 1 + .../pitchedapps/frost/facebook/FbUrlFormatter.kt | 5 +--- .../frost/facebook/requests/FbRequest.kt | 19 +++++++++---- .../pitchedapps/frost/facebook/requests/Images.kt | 17 ++++++++++-- .../com/pitchedapps/frost/glide/GlideUtils.kt | 21 ++++++++++++++ .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 20 +++++++++++--- .../frost/web/FrostUrlOverlayValidator.kt | 8 +++++- .../pitchedapps/frost/web/FrostWebViewClients.kt | 13 +++++---- app/src/main/res/xml/frost_changelog.xml | 2 +- .../pitchedapps/frost/facebook/FbFullImageTest.kt | 32 ++++++++++++++++++++++ .../com/pitchedapps/frost/facebook/FbUrlTest.kt | 17 ++++++++++++ .../com/pitchedapps/frost/internal/Internal.kt | 8 ++++-- 14 files changed, 157 insertions(+), 31 deletions(-) create mode 100644 app/src/test/kotlin/com/pitchedapps/frost/facebook/FbFullImageTest.kt diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt index e9426beb..399e86b0 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt @@ -24,9 +24,11 @@ import com.pitchedapps.frost.R import com.pitchedapps.frost.facebook.FB_IMAGE_ID_MATCHER import com.pitchedapps.frost.facebook.get import com.pitchedapps.frost.facebook.requests.call +import com.pitchedapps.frost.facebook.requests.getFullSizedImageUrl +import com.pitchedapps.frost.facebook.requests.requestBuilder import com.pitchedapps.frost.utils.* import com.sothree.slidinguppanel.SlidingUpPanelLayout -import okhttp3.Request +import okhttp3.Response import org.jetbrains.anko.activityUiThreadWithContext import org.jetbrains.anko.doAsync import org.jetbrains.anko.uiThread @@ -86,8 +88,18 @@ class ImageActivity : KauBaseActivity() { private val L = KauLoggerExtension("Image", com.pitchedapps.frost.utils.L) } + private val cookie: String? by lazy { intent.getStringExtra(ARG_COOKIE) } + val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') } + private val trueImageUrl: String by lazy { + val result = if (!imageUrl.isIndirectImageUrl) imageUrl + else cookie?.getFullSizedImageUrl(imageUrl)?.blockingGet() ?: imageUrl + if (result != imageUrl) + L.v { "Launching with true url $result" } + result + } + private val imageText: String? by lazy { intent.getStringExtra(ARG_TEXT) } // a unique image identifier based on the id (if it exists), and its hash @@ -205,12 +217,13 @@ class ImageActivity : KauBaseActivity() { return File.createTempFile(imageFileName, IMG_EXTENSION, frostDir) } - private fun getImageResponse() = Request.Builder() - .url(imageUrl) + private fun getImageResponse(): Response = cookie.requestBuilder() + .url(trueImageUrl) .get() .call() .execute() + @Throws(IOException::class) private fun downloadImageTo(file: File) { val body = getImageResponse().body() @@ -263,7 +276,7 @@ class ImageActivity : KauBaseActivity() { override fun onDestroy() { tempFile = null val purge = System.currentTimeMillis() - PURGE_TIME - tempDir.listFiles(FileFilter { it.isFile && it.lastModified() < purge }).forEach { + tempDir.listFiles(FileFilter { it.isFile && it.lastModified() < purge })?.forEach { it.delete() } super.onDestroy() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt index e5f0b8fe..38de6150 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt @@ -20,6 +20,10 @@ import io.reactivex.subjects.SingleSubject */ object FbCookie { + /** + * Retrieves the facebook cookie if it exists + * Note that this is a synchronized call + */ inline val webCookie: String? get() = CookieManager.getInstance().getCookie(FB_URL_BASE) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt index 4ba3c80d..cfa2796c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt @@ -26,6 +26,7 @@ val FB_MESSAGE_NOTIF_ID_MATCHER: Regex by lazy { Regex("[thread|user]_fbid_([0-9 val FB_CSS_URL_MATCHER: Regex by lazy { Regex("url\\([\"|']?(.*?)[\"|']?\\)") } val FB_JSON_URL_MATCHER: Regex by lazy { Regex("\"(http.*?)\"") } val FB_IMAGE_ID_MATCHER: Regex by lazy { Regex("fbcdn.*?/[0-9]+_([0-9]+)_") } +val FB_REDIRECT_URL_MATCHER: Regex by lazy { Regex("url=(.*?fbcdn.*?)\"") } operator fun MatchResult?.get(groupIndex: Int) = this?.groupValues?.get(groupIndex) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt index f21c03e9..add35154 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt @@ -43,6 +43,7 @@ class FbUrlFormatter(url: String) { L.e(e) { "Failed url formatting" } return url } + cleanedUrl = cleanedUrl.replace("&", "&") if (changed && !cleanedUrl.contains("?")) //ensure we aren't missing '?' cleanedUrl = cleanedUrl.replaceFirst("&", "?") val qm = cleanedUrl.indexOf("?") @@ -54,8 +55,6 @@ class FbUrlFormatter(url: String) { cleanedUrl = cleanedUrl.substring(0, qm) } discardableQueries.forEach { queries.remove(it) } - //final cleanup - misc.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) } 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" } @@ -101,8 +100,6 @@ class FbUrlFormatter(url: String) { VIDEO_REDIRECT ) - val misc = arrayOf("&" to "&") - val discardableQueries = arrayOf("ref", "refid", "acontext", "SharedWith") val converter = listOf( diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt index b5c2e4e9..692312a1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt @@ -6,7 +6,10 @@ import com.pitchedapps.frost.rx.RxFlyweight import com.pitchedapps.frost.utils.L import io.reactivex.Single import io.reactivex.schedulers.Schedulers -import okhttp3.* +import okhttp3.Call +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request import okhttp3.logging.HttpLoggingInterceptor import org.apache.commons.text.StringEscapeUtils @@ -92,12 +95,16 @@ internal fun List>.withEmptyData(vararg key: String): List InputStream? = { null } + +/** + * Attempts to get the fbcdn url of the supplied image redirect url + */ +fun String.getFullSizedImageUrl(url: String): Maybe = Maybe.fromCallable { + val redirect = requestBuilder().url(url).get().call() + .execute().body()?.string() ?: return@fromCallable null + return@fromCallable FB_REDIRECT_URL_MATCHER.find(redirect)[1]?.formattedFbUrl + ?: return@fromCallable null +}.onErrorComplete() + /** * Request loader for a potentially hd version of a url * In this case, each url may potentially return an id, diff --git a/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt index 651e57d8..50d4d7b4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt @@ -10,6 +10,11 @@ import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.CircleCrop import com.bumptech.glide.module.AppGlideModule import com.bumptech.glide.request.RequestOptions +import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.utils.L +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Response /** * Created by Allan Wang on 28/12/17. @@ -35,6 +40,22 @@ fun RequestBuilder.transform(vararg transformation: BitmapTransformation) class FrostGlideModule : AppGlideModule() { override fun registerComponents(context: Context, glide: Glide, registry: Registry) { +// registry.replace(GlideUrl::class.java, +// InputStream::class.java, +// OkHttpUrlLoader.Factory(getFrostHttpClient())) // registry.prepend(HdImageMaybe::class.java, InputStream::class.java, HdImageLoadingFactory()) } +} + +private fun getFrostHttpClient(): OkHttpClient = + OkHttpClient.Builder().addInterceptor(FrostCookieInterceptor()).build() + +class FrostCookieInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val origRequest = chain.request() + val cookie = FbCookie.webCookie ?: return chain.proceed(origRequest) + L.v { "Add cookie to req $cookie" } + val request = origRequest.newBuilder().addHeader("Cookie", cookie).build() + return chain.proceed(request) + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt index b1b129fb..b4b78bf2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -44,6 +44,7 @@ const val ARG_URL = "arg_url" const val ARG_USER_ID = "arg_user_id" const val ARG_IMAGE_URL = "arg_image_url" const val ARG_TEXT = "arg_text" +const val ARG_COOKIE = "arg_cookie" inline fun Context.launchNewTask(cookieList: ArrayList = arrayListOf(), clearStack: Boolean = false) { startActivity(clearStack, intentBuilder = { @@ -83,11 +84,12 @@ fun Context.launchWebOverlayBasic(url: String) = launchWebOverlayImpl(intentBuilder = { putExtras(fadeBundle()) putExtra(ARG_IMAGE_URL, imageUrl) putExtra(ARG_TEXT, text) + putExtra(ARG_COOKIE, cookie) }) } @@ -241,10 +243,20 @@ inline val String.isVideoUrl (startsWith("https://video-") && contains(FBCDN_NET)) /** - * [true] if url is or redirects to an explicit facebook image + * [true] if url directly leads to a usable image */ -inline val String.isImageUrl - get() = contains(FBCDN_NET) && (contains(".png") || contains(".jpg")) +inline val String.isImageUrl: Boolean + get() { + return contains(FBCDN_NET) && (contains(".png") || contains(".jpg")) + } + +/** + * [true] if url can be retrieved to get a direct image url + */ +inline val String.isIndirectImageUrl: Boolean + get() { + return contains("/photo/view_full_size/") && contains("fbid=") + } /** * [true] if url can be displayed in a different webview diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt index 7e5f1632..b58f1a16 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt @@ -4,6 +4,7 @@ import com.pitchedapps.frost.activities.WebOverlayActivity import com.pitchedapps.frost.activities.WebOverlayActivityBase import com.pitchedapps.frost.activities.WebOverlayBasicActivity import com.pitchedapps.frost.contracts.VideoViewHolder +import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.USER_AGENT_BASIC import com.pitchedapps.frost.facebook.formattedFbUrl @@ -37,7 +38,12 @@ fun FrostWebView.requestWebOverlay(url: String): Boolean { } if (url.isImageUrl) { L.d { "Found fb image" } - context.launchImageActivity(url.formattedFbUrl, null) + context.launchImageActivity(url.formattedFbUrl) + return true + } + if (url.isIndirectImageUrl) { + L.d { "Found indirect fb image" } + context.launchImageActivity(url.formattedFbUrl, cookie = FbCookie.webCookie) return true } if (!url.isIndependent) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt index f1d03f35..a1fd594f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt @@ -7,7 +7,9 @@ import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient import com.pitchedapps.frost.facebook.FB_URL_BASE +import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.facebook.formattedFbUrl import com.pitchedapps.frost.injectors.* import com.pitchedapps.frost.utils.* import com.pitchedapps.frost.views.FrostWebView @@ -57,7 +59,6 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() { ) } - override fun onPageCommitVisible(view: WebView, url: String?) { super.onPageCommitVisible(view, url) injectBackgroundColor() @@ -91,7 +92,7 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() { onPageFinishedActions(url) } - open internal fun onPageFinishedActions(url: String) { + internal open fun onPageFinishedActions(url: String) { if (url.startsWith("${FbItem.MESSAGES.url}/read/") && Prefs.messageScrollToBottom) web.pageDown(true) injectAndFinish() @@ -125,9 +126,9 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() { return web.requestWebOverlay(request.url.toString()) } - private fun launchImage(url: String, text: String? = null): Boolean { + private fun launchImage(url: String, text: String? = null, cookie: String? = null): Boolean { v { "Launching image: $url" } - web.context.launchImageActivity(url, text) + web.context.launchImageActivity(url, text, cookie) if (web.canGoBack()) web.goBack() return true } @@ -143,7 +144,9 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() { } if (path.startsWith("/composer/")) return launchRequest(request) if (url.isImageUrl) - return launchImage(url) + return launchImage(url.formattedFbUrl) + if (url.isIndirectImageUrl) + return launchImage(url.formattedFbUrl, cookie = FbCookie.webCookie) if (Prefs.linksInDefaultApp && view.context.resolveActivityForUri(request.url)) return true return super.shouldOverrideUrlLoading(view, request) } diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml index f7a02179..9a3463a1 100644 --- a/app/src/main/res/xml/frost_changelog.xml +++ b/app/src/main/res/xml/frost_changelog.xml @@ -11,7 +11,7 @@ - + diff --git a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbFullImageTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbFullImageTest.kt new file mode 100644 index 00000000..98dc7dda --- /dev/null +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbFullImageTest.kt @@ -0,0 +1,32 @@ +package com.pitchedapps.frost.facebook + +import com.pitchedapps.frost.facebook.requests.getFullSizedImage +import com.pitchedapps.frost.facebook.requests.getFullSizedImageUrl +import com.pitchedapps.frost.internal.COOKIE +import com.pitchedapps.frost.internal.authDependent +import org.junit.BeforeClass +import org.junit.Test +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Created by Allan Wang on 12/04/18. + */ +class FbFullImageTest { + + companion object { + @BeforeClass + @JvmStatic + fun before() { + authDependent() + } + } + + @Test + fun getFullImage() { + val url = "https://touch.facebook.com/photo/view_full_size/?fbid=107368839645039" + val result = COOKIE.getFullSizedImageUrl(url).blockingGet() + assertNotNull(result) + println(result) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt index 0e45d2f2..beda26ef 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt @@ -1,6 +1,7 @@ package com.pitchedapps.frost.facebook import com.pitchedapps.frost.utils.isImageUrl +import com.pitchedapps.frost.utils.isIndirectImageUrl import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -47,6 +48,13 @@ class FbUrlTest { assertFbFormat(expected, url) } + @Test + fun ampersand() { + val url = "https://scontent-yyz1-1.xx.fbcdn.net/v/t31.0-8/fr/cp0/e15/q65/123.jpg?_nc_cat=0&efg=asdf" + val formattedUrl = "https://scontent-yyz1-1.xx.fbcdn.net/v/t31.0-8/fr/cp0/e15/q65/123.jpg?_nc_cat=0&efg=asdf" + assertFbFormat(formattedUrl, url) + } + @Test fun doubleDash() { assertFbFormat("${FB_URL_BASE}relative", "$FB_URL_BASE/relative") @@ -72,6 +80,15 @@ class FbUrlTest { } } + @Test + fun indirectImage() { + arrayOf( + "#!/photo/view_full_size/?fbid=107368839645039" + ).forEach { + assertTrue(it.isIndirectImageUrl, "Failed to match indirect image for $it") + } + } + @Test fun antiImageRegex() { arrayOf( diff --git a/app/src/test/kotlin/com/pitchedapps/frost/internal/Internal.kt b/app/src/test/kotlin/com/pitchedapps/frost/internal/Internal.kt index 2af98eda..e3beabd2 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/internal/Internal.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/internal/Internal.kt @@ -24,11 +24,13 @@ import kotlin.test.fail private const val FILE = "priv.properties" +private val propPaths = arrayOf(FILE, "../$FILE") + val PROPS: Properties by lazy { val props = Properties() - val file = File(FILE) - if (!file.exists()) { - println("$FILE not found") + val file = propPaths.map(::File).firstOrNull { it.isFile } + if (file == null) { + println("$FILE not found at ${File(".").absolutePath}") return@lazy props } println("Found properties at ${file.absolutePath}") -- cgit v1.2.3