From c7d5efb79692f7a82be67354095ba97226844ca1 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Mon, 13 Jan 2020 18:05:30 -0800 Subject: [PATCH] Add way to get YouTube metadata that doesn't require a session cookie The Video Player XBlock will sometimes make API calls to /couses/yt_video_metadata, a REST API endpoint that in turn loads video metadata from YouTube using the configured settings.YOUTUBE_API_KEY. However, in the Blockstore-based XBlock runtime, we are running XBlocks in a secure sandbox, and the user's browser cannot pass session cookies when calling REST API endpoints. So currently, the video XBlock tries to request YouTube metadata from that API endpoint, but it fails if run within such a sandbox. The existing API also doesn't work for anonymous users (users who are allowed to see video XBlocks but who have not logged in to an LMS user account). This commit updates the Video XBlock so that it can use a handler to load the data from YouTube instead of a generic REST API. This works well in the new runtime, because it has code to support calling handlers within the sandbox, including by anonymous users. I also fixed a bug where on a default devstack, the endpoint will try calling YouTube using PUT_YOUR_API_KEY_HERE as an API key, and get a "bad request" error from YouTube. The code could be re-organized by moving things around, but I've left everything as-is for now to keep the diff as small as possible. --- .../xmodule/js/src/video/01_initialize.js | 10 ++++++++- .../xmodule/video_module/video_handlers.py | 13 ++++++++++++ .../xmodule/video_module/video_module.py | 7 +++++++ .../courseware/tests/test_video_mongo.py | 5 +++++ lms/djangoapps/courseware/views/views.py | 21 ++++++++++++++----- 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js index 4165a1b04d..6051cc4286 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -723,12 +723,20 @@ function(VideoPlayer, i18n, moment, _) { } function getVideoMetadata(url, callback) { + var youTubeEndpoint; if (!(_.isString(url))) { url = this.videos['1.0'] || ''; } // Will hit the API URL to get the youtube video metadata. + youTubeEndpoint = this.config.ytMetadataEndpoint; // The new runtime supports anonymous users + // and uses an XBlock handler to get YouTube metadata + if (!youTubeEndpoint) { + // The old runtime has a full/separate LMS API for getting YouTube metadata, but it doesn't + // support anonymous users nor videos that play in a sandboxed iframe. + youTubeEndpoint = [this.config.lmsRootURL, '/courses/yt_video_metadata', '?id=', url].join(''); + } return $.ajax({ - url: [this.config.lmsRootURL, '/courses/yt_video_metadata', '?id=', url].join(''), + url: youTubeEndpoint, success: _.isFunction(callback) ? callback : null, error: function() { console.warn( diff --git a/common/lib/xmodule/xmodule/video_module/video_handlers.py b/common/lib/xmodule/xmodule/video_module/video_handlers.py index 8056305f3f..0be32e3081 100644 --- a/common/lib/xmodule/xmodule/video_module/video_handlers.py +++ b/common/lib/xmodule/xmodule/video_module/video_handlers.py @@ -377,6 +377,19 @@ class VideoStudentViewHandlers(object): return response + @XBlock.handler + def yt_video_metadata(self, request, suffix=''): + """ + Endpoint to get YouTube metadata. + This handler is only used in the Blockstore-based runtime. The old + runtime uses a similar REST API that's not an XBlock handler. + """ + from lms.djangoapps.courseware.views.views import load_metadata_from_youtube + metadata, status_code = load_metadata_from_youtube(video_id=self.youtube_id_1_0) + response = Response(json.dumps(metadata), status=status_code) + response.content_type = 'application/json' + return response + class VideoStudioViewHandlers(object): """ diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index 9ca7624555..7ac946e19b 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -415,6 +415,13 @@ class VideoBlock( 'ytTestTimeout': settings.YOUTUBE['TEST_TIMEOUT'], 'ytApiUrl': settings.YOUTUBE['API'], 'lmsRootURL': settings.LMS_ROOT_URL, + 'ytMetadataEndpoint': ( + # In the new runtime, get YouTube metadata via a handler. The handler supports anonymous users and + # can work in sandboxed iframes. In the old runtime, the JS will call the LMS's yt_video_metadata + # API endpoint directly (not an XBlock handler). + self.runtime.handler_url(self, 'yt_video_metadata') + if getattr(self.runtime, 'suppports_state_for_anonymous_users', False) else '' + ), 'transcriptTranslationUrl': self.runtime.handler_url( self, 'transcript', 'translation/__lang__' diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index e2f6b41634..2c8b9b47fa 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -111,6 +111,7 @@ class TestVideoYouTube(TestVideo): # pylint: disable=test-inherits-tests 'end': 3610.0, 'transcriptLanguage': 'en', 'transcriptLanguages': OrderedDict({'en': 'English', 'uk': u'Українська'}), + 'ytMetadataEndpoint': '', 'ytTestTimeout': 1500, 'ytApiUrl': 'https://www.youtube.com/iframe_api', 'lmsRootURL': settings.LMS_ROOT_URL, @@ -194,6 +195,7 @@ class TestVideoNonYouTube(TestVideo): # pylint: disable=test-inherits-tests 'end': 3610.0, 'transcriptLanguage': 'en', 'transcriptLanguages': OrderedDict({'en': 'English'}), + 'ytMetadataEndpoint': '', 'ytTestTimeout': 1500, 'ytApiUrl': 'https://www.youtube.com/iframe_api', 'lmsRootURL': settings.LMS_ROOT_URL, @@ -257,6 +259,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock): 'end': 3610.0, 'transcriptLanguage': 'en', 'transcriptLanguages': OrderedDict({'en': 'English'}), + 'ytMetadataEndpoint': '', 'ytTestTimeout': 1500, 'ytApiUrl': 'https://www.youtube.com/iframe_api', 'lmsRootURL': settings.LMS_ROOT_URL, @@ -2236,6 +2239,7 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests 'end': 3610.0, 'transcriptLanguage': 'en', 'transcriptLanguages': OrderedDict({'en': 'English', 'uk': u'Українська'}), + 'ytMetadataEndpoint': '', 'ytTestTimeout': 1500, 'ytApiUrl': 'https://www.youtube.com/iframe_api', 'lmsRootURL': settings.LMS_ROOT_URL, @@ -2311,6 +2315,7 @@ class TestAutoAdvanceVideo(TestVideo): 'end': 3610.0, 'transcriptLanguage': 'en', 'transcriptLanguages': OrderedDict({'en': 'English', 'uk': u'Українська'}), + 'ytMetadataEndpoint': '', 'ytTestTimeout': 1500, 'ytApiUrl': 'https://www.youtube.com/iframe_api', 'lmsRootURL': settings.LMS_ROOT_URL, diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index bf801c917b..e2c17c73a8 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -280,10 +280,21 @@ def yt_video_metadata(request): Will hit the youtube API if the key is available in settings :return: youtube video metadata """ - response = {} - status_code = 500 video_id = request.GET.get('id', None) - if settings.YOUTUBE_API_KEY and video_id: + metadata, status_code = load_metadata_from_youtube(video_id) + return Response(metadata, status=status_code, content_type='application/json') + + +def load_metadata_from_youtube(video_id): + """ + Get metadata about a YouTube video. + + This method is used via the standalone /courses/yt_video_metadata REST API + endpoint, or via the video XBlock as a its 'yt_video_metadata' handler. + """ + metadata = {} + status_code = 500 + if video_id and settings.YOUTUBE_API_KEY and settings.YOUTUBE_API_KEY != 'PUT_YOUR_API_KEY_HERE': yt_api_key = settings.YOUTUBE_API_KEY yt_metadata_url = settings.YOUTUBE['METADATA_URL'] yt_timeout = settings.YOUTUBE.get('TEST_TIMEOUT', 1500) / 1000 # converting milli seconds to seconds @@ -295,7 +306,7 @@ def yt_video_metadata(request): try: res_json = res.json() if res_json.get('items', []): - response = res_json + metadata = res_json else: logging.warning(u'Unable to find the items in response. Following response ' u'was received: {res}'.format(res=res.text)) @@ -310,7 +321,7 @@ def yt_video_metadata(request): else: logging.warning(u'YouTube API key or video id is None. Please make sure API key and video id is not None') - return Response(response, status=status_code, content_type='application/json') + return metadata, status_code @ensure_csrf_cookie