diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 0da1899b65..a5746a5f15 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -213,7 +213,7 @@ dependencies { testImplementation Libs.JUNIT testImplementation Libs.ROBOLECTRIC testImplementation Libs.ANDROIDX_TEST_JUNIT - testImplementation "io.mockk:mockk:1.9.3" + testImplementation Libs.MOCKK androidTestImplementation Libs.ANDROIDX_TEST_JUNIT testImplementation Libs.KOTLIN_COROUTINES_TEST diff --git a/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt b/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt index e167ca585f..2889424497 100644 --- a/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt +++ b/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt @@ -39,6 +39,7 @@ import com.instructure.pandautils.activities.BaseViewMediaActivity import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.LoaderUtils +import com.instructure.pandautils.utils.RouteUtils import com.instructure.pandautils.utils.nonNullArgs import com.instructure.student.R import com.instructure.student.activity.InternalWebViewActivity @@ -256,12 +257,11 @@ object RouteMatcher : BaseRouteMatcher() { handleFullscreenRoute(context, route) } else { val isGroupRoute = "groups" == route.uri?.pathSegments?.get(0) - if (route.queryParamsHash.containsKey(RouterParams.PREVIEW)) { - // This is a link for a file preview, so we need to get the file id from the preview query param - handleSpecificFile(context as FragmentActivity, route.queryParamsHash[RouterParams.PREVIEW] ?: "", isGroupRoute) - } else { - handleSpecificFile(context as FragmentActivity, route.paramsHash[RouterParams.FILE_ID] ?: "", isGroupRoute) - } + handleSpecificFile( + context as FragmentActivity, + (if (route.queryParamsHash.containsKey(RouterParams.PREVIEW)) route.queryParamsHash[RouterParams.PREVIEW] else route.paramsHash[RouterParams.FILE_ID]) ?: "", + route, + isGroupRoute) } } else if (route.routeContext == RouteContext.MEDIA) { handleMediaRoute(context, route) @@ -354,15 +354,26 @@ object RouteMatcher : BaseRouteMatcher() { } } - private fun openMedia(activity: FragmentActivity?, mime: String, url: String, filename: String) { - if (activity != null) { + private fun openMedia(activity: FragmentActivity?, mime: String, url: String, filename: String, route: Route, fileId: String?) { + if (activity == null) { + return + } + + // If we're trying to open an HTML file, don't download it. It could be referencing other files + // through a relative URL which we won't be able to access. Instead, just showing the file in + // a webview will load the file the user is trying to view and will resolve all relative paths + if (filename.toLowerCase().endsWith(".htm") || filename.toLowerCase().endsWith(".html")) { + RouteUtils.retrieveFileUrl(route, fileId) { fileUrl, context, needsAuth -> + InternalWebviewFragment.loadInternalWebView(activity, InternalWebviewFragment.makeRoute(context, fileUrl, needsAuth, true)) + } + } else { openMediaCallbacks = null openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(mime, url, filename) LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(activity), openMediaBundle, getLoaderCallbacks(activity), R.id.openMediaLoaderID) } } - private fun handleSpecificFile(activity: FragmentActivity, fileID: String?, isGroupFile: Boolean) { + private fun handleSpecificFile(activity: FragmentActivity, fileID: String?, route: Route, isGroupFile: Boolean) { val fileFolderStatusCallback = object : StatusCallback() { override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { @@ -372,7 +383,7 @@ object RouteMatcher : BaseRouteMatcher() { Toast.makeText(activity, String.format(activity.getString(R.string.fileLocked), if (fileFolder.displayName == null) activity.getString(R.string.file) else fileFolder.displayName), Toast.LENGTH_LONG).show() } else { // This is either a group file (which have no permissions), or a file that is accessible by the user - openMedia(activity, fileFolder.contentType!!, fileFolder.url!!, fileFolder.displayName!!) + openMedia(activity, fileFolder.contentType!!, fileFolder.url!!, fileFolder.displayName!!, route, fileID) } } ?: Toast.makeText(activity, activity.getString(R.string.errorOccurred), Toast.LENGTH_LONG).show() } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt index 30f653c409..890849967e 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt @@ -44,6 +44,7 @@ import com.instructure.pandautils.activities.BaseViewMediaActivity import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.LoaderUtils +import com.instructure.pandautils.utils.RouteUtils import com.instructure.pandautils.utils.nonNullArgs import com.instructure.teacher.PSPDFKit.AnnotationComments.AnnotationCommentListFragment import com.instructure.teacher.R @@ -170,13 +171,10 @@ object RouteMatcher : BaseRouteMatcher() { openMedia(context as FragmentActivity, route.uri.toString()) } } else { - if (route.queryParamsHash.containsKey(RouterParams.PREVIEW)) { - // This is a link for a file preview, so we need to get the file id from the preview query param - handleSpecificFile(context as FragmentActivity, route.queryParamsHash[RouterParams.PREVIEW] - ?: "") - } else { - handleSpecificFile(context as FragmentActivity, route.paramsHash[RouterParams.FILE_ID] ?: "") - } + handleSpecificFile( + context as FragmentActivity, + (if (route.queryParamsHash.containsKey(RouterParams.PREVIEW)) route.queryParamsHash[RouterParams.PREVIEW] else route.paramsHash[RouterParams.FILE_ID]) ?: "", + route) } } else if (route.routeContext === RouteContext.MEDIA) { @@ -514,8 +512,20 @@ object RouteMatcher : BaseRouteMatcher() { } } - private fun openMedia(activity: FragmentActivity?, mime: String, url: String, filename: String) { - if (activity != null) { + private fun openMedia(activity: FragmentActivity?, mime: String, url: String, filename: String, route: Route, fileId: String?) { + if (activity == null) { + return + } + + // If we're trying to open an HTML file, don't download it. It could be referencing other files + // through a relative URL which we won't be able to access. Instead, just showing the file in + // a webview will load the file the user is trying to view and will resolve all relative paths + if (filename.toLowerCase().endsWith(".htm") || filename.toLowerCase().endsWith(".html")) { + RouteUtils.retrieveFileUrl(route, fileId) { fileUrl, context, needsAuth -> + val bundle = InternalWebViewFragment.makeBundle(url = fileUrl, title = filename, shouldAuthenticate = needsAuth) + RouteMatcher.route(activity, Route(FullscreenInternalWebViewFragment::class.java, context, bundle)) + } + } else { openMediaCallbacks = null openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(mime, url, filename) LoaderUtils.restartLoaderWithBundle>( @@ -523,7 +533,7 @@ object RouteMatcher : BaseRouteMatcher() { } } - private fun handleSpecificFile(activity: FragmentActivity, fileID: String?) { + private fun handleSpecificFile(activity: FragmentActivity, fileID: String?, route: Route) { val fileFolderStatusCallback = object : StatusCallback() { override fun onResponse(response: retrofit2.Response, linkHeaders: com.instructure.canvasapi2.utils.LinkHeaders, type: ApiType) { super.onResponse(response, linkHeaders, type) @@ -531,7 +541,7 @@ object RouteMatcher : BaseRouteMatcher() { if (fileFolder!!.isLocked || fileFolder.isLockedForUser) { Toast.makeText(activity, String.format(activity.getString(R.string.fileLocked), if (fileFolder.displayName == null) activity.getString(R.string.file) else fileFolder.displayName), Toast.LENGTH_LONG).show() } else { - openMedia(activity, fileFolder.contentType!!, fileFolder.url!!, fileFolder.displayName!!) + openMedia(activity, fileFolder.contentType!!, fileFolder.url!!, fileFolder.displayName!!, route, fileID) } } } diff --git a/buildSrc/src/main/java/GlobalDependencies.kt b/buildSrc/src/main/java/GlobalDependencies.kt index b147b4489e..a98ed9c166 100644 --- a/buildSrc/src/main/java/GlobalDependencies.kt +++ b/buildSrc/src/main/java/GlobalDependencies.kt @@ -85,6 +85,7 @@ object Libs { const val JUNIT = "junit:junit:${Versions.JUNIT}" const val ROBOLECTRIC = "org.robolectric:robolectric:${Versions.ROBOLECTRIC}" const val ANDROIDX_TEST_JUNIT = "androidx.test.ext:junit:1.1.0" + const val MOCKK = "io.mockk:mockk:1.9.3" /* Other */ const val CRASHLYTICS = "com.crashlytics.sdk.android:crashlytics:${Versions.CRASHLYTICS}" diff --git a/libs/canvas-api-2/build.gradle b/libs/canvas-api-2/build.gradle index 90a0d2499e..541d828421 100644 --- a/libs/canvas-api-2/build.gradle +++ b/libs/canvas-api-2/build.gradle @@ -124,7 +124,7 @@ dependencies { /* Test Dependencies */ testImplementation Libs.JUNIT - testImplementation "io.mockk:mockk:1.9.3" + testImplementation Libs.MOCKK testImplementation "org.mockito:mockito-inline:2.25.0" testImplementation "au.com.dius:pact-jvm-consumer-junit_2.11:3.5.14" testImplementation group: 'org.slf4j', name: 'slf4j-nop', version: '1.7.26' diff --git a/libs/pandautils/build.gradle b/libs/pandautils/build.gradle index 2ccb03f822..3aafe4ca1d 100644 --- a/libs/pandautils/build.gradle +++ b/libs/pandautils/build.gradle @@ -97,6 +97,7 @@ dependencies { /* Test Dependencies */ testImplementation Libs.JUNIT + testImplementation Libs.MOCKK /* Media handling */ api("com.github.bumptech.glide:glide:$glideVersion") { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/RouteUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/RouteUtils.kt new file mode 100644 index 0000000000..7dda369fd1 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/RouteUtils.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.instructure.pandautils.utils + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.interactions.router.Route +import com.instructure.interactions.router.RouterParams + +object RouteUtils { + fun retrieveFileUrl(route: Route, fileId: String?, block: (url: String, canvasContext: CanvasContext, needsAuth: Boolean) -> Unit) { + var needsAuth = true + var fileUrl = ApiPrefs.fullDomain + var context = CanvasContext.currentUserContext(ApiPrefs.user!!) + route.paramsHash[RouterParams.COURSE_ID]?.let { + context = CanvasContext.getGenericContext(CanvasContext.Type.COURSE, it.toLong()) + fileUrl += "/courses/$it" + } + fileUrl += "/files/$fileId/preview" + route.queryParamsHash[RouterParams.VERIFIER]?.let { + needsAuth = false + fileUrl += "?verifier=$it" + } + + block.invoke(fileUrl, context, needsAuth) + } +} diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/unit/RouteUtilsTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/unit/RouteUtilsTest.kt new file mode 100644 index 0000000000..b1c0e68c0f --- /dev/null +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/unit/RouteUtilsTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2017 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.unit + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.interactions.router.Route +import com.instructure.interactions.router.RouterParams +import com.instructure.pandautils.utils.RouteUtils +import io.mockk.* +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +class RouteUtilsTest : Assert() { + + lateinit var route: Route + lateinit var user: User + + @Before + fun setup() { + user = User() + route = Route() + + mockkStatic(ApiPrefs::class) + every { ApiPrefs.user } returns user + every { ApiPrefs.fullDomain } returns "https://domain.instructure.com" + } + + @Test + @Throws(Exception::class) + fun `retrieveFileUrl returns a url with a file id`() { + val fileId = "6" + + RouteUtils.retrieveFileUrl(route, fileId) { url, _, _ -> + assertEquals("https://domain.instructure.com/files/$fileId/preview", url) + } + } + + @Test + @Throws(Exception::class) + fun `retrieveFileUrl returns a url with a course and a file id`() { + val fileId = "6" + val courseId = "12" + route.paramsHash = hashMapOf(Pair(RouterParams.COURSE_ID, courseId)) + + RouteUtils.retrieveFileUrl(route, fileId) { url, _, _ -> + assertEquals("https://domain.instructure.com/courses/$courseId/files/$fileId/preview", url) + } + } + + @Test + @Throws(Exception::class) + fun `retrieveFileUrl returns a url with a verifier`() { + val fileId = "6" + val verifier = "thisisaverifier" + route.queryParamsHash = hashMapOf(Pair(RouterParams.VERIFIER, verifier)) + + RouteUtils.retrieveFileUrl(route, fileId) { url, _, _ -> + assertEquals("https://domain.instructure.com/files/$fileId/preview?verifier=$verifier", url) + } + } + + @Test + @Throws(Exception::class) + fun `retrieveFileUrl returns that auth is NOT needed with a verifier`() { + val fileId = "6" + val verifier = "thisisaverifier" + route.queryParamsHash = hashMapOf(Pair(RouterParams.VERIFIER, verifier)) + + RouteUtils.retrieveFileUrl(route, fileId) { _, _, needsAuth -> + assertFalse(needsAuth) + } + } + + @Test + @Throws(Exception::class) + fun `retrieveFileUrl returns that auth is needed without a verifier`() { + val fileId = "6" + + RouteUtils.retrieveFileUrl(route, fileId) { _, _, needsAuth -> + assertTrue(needsAuth) + } + } + + @Test + @Throws(Exception::class) + fun `retrieveFileUrl returns a course context when a course id is provided`() { + val fileId = "6" + val courseId = "12" + route.paramsHash = hashMapOf(Pair(RouterParams.COURSE_ID, courseId)) + + RouteUtils.retrieveFileUrl(route, fileId) { _, context, _ -> + assertEquals(CanvasContext.getGenericContext(CanvasContext.Type.COURSE, 12), context) + } + } + + @Test + @Throws(Exception::class) + fun `retrieveFileUrl returns a user context when no course id is provided`() { + val fileId = "6" + + RouteUtils.retrieveFileUrl(route, fileId) { _, context, _ -> + assertEquals(CanvasContext.currentUserContext(user), context) + } + } + +}