diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 9241f0517..9b0185add 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -514,17 +514,17 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): :param display: Which display to use :param service_item: The ServiceItem containing the details to be played. """ - used_players = get_media_players() - default_player = used_players[0] + used_players = get_media_players()[0] + # If no player, we can't play + if not used_players: + return False + default_player = [used_players[0]] if service_item.processor and service_item.processor != UiStrings().Automatic: # check to see if the player is usable else use the default one. if not service_item.processor.lower() in used_players: used_players = default_player else: used_players = [service_item.processor.lower()] - # If no player, we can't play - if not used_players: - return False if controller.media_info.file_info.isFile(): suffix = '*.%s' % controller.media_info.file_info.suffix().lower() for title in used_players: diff --git a/openlp/plugins/remotes/lib/httprouter.py b/openlp/plugins/remotes/lib/httprouter.py index dedefdc90..9f6e68506 100644 --- a/openlp/plugins/remotes/lib/httprouter.py +++ b/openlp/plugins/remotes/lib/httprouter.py @@ -149,6 +149,7 @@ class HttpRouter(RegistryProperties): self.routes = [ ('^/$', {'function': self.serve_file, 'secure': False}), ('^/(stage)$', {'function': self.serve_file, 'secure': False}), + ('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}), ('^/(main)$', {'function': self.serve_file, 'secure': False}), (r'^/files/(.*)$', {'function': self.serve_file, 'secure': False}), (r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}), @@ -169,6 +170,7 @@ class HttpRouter(RegistryProperties): self.settings_section = 'remotes' self.translate() self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'remotes', 'html') + self.config_dir = os.path.join(AppLocation.get_data_path(), 'stages') def do_post_processor(self): """ @@ -339,24 +341,32 @@ class HttpRouter(RegistryProperties): 'settings': translate('RemotePlugin.Mobile', 'Settings'), } - def serve_file(self, file_name=None): + def stages(self, url_path, file_name): """ - Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder. - If subfolders requested return 404, easier for security for the present. + Allow Stage view to be delivered with custom views. - Ultimately for i18n, this could first look for xx/file.html before falling back to file.html. - where xx is the language, e.g. 'en' + :param url_path: base path of the URL. Not used but passed by caller + :param file_name: file name with path + :return: """ log.debug('serve file request %s' % file_name) - if not file_name: - file_name = 'index.html' - elif file_name == 'stage': - file_name = 'stage.html' - elif file_name == 'main': - file_name = 'main.html' - path = os.path.normpath(os.path.join(self.html_dir, file_name)) - if not path.startswith(self.html_dir): + parts = file_name.split('/') + if len(parts) == 1: + file_name = os.path.join(parts[0], 'stage.html') + elif len(parts) == 3: + file_name = os.path.join(parts[1], parts[2]) + path = os.path.normpath(os.path.join(self.config_dir, file_name)) + if not path.startswith(self.config_dir): return self.do_not_found() + return self._process_file(path) + + def _process_file(self, path): + """ + Common file processing code + + :param path: path to file to be loaded + :return: web resource to be loaded + """ content = None ext, content_type = self.get_content_type(path) file_handle = None @@ -379,10 +389,32 @@ class HttpRouter(RegistryProperties): self.end_headers() return content + def serve_file(self, file_name=None): + """ + Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder. + If subfolders requested return 404, easier for security for the present. + + Ultimately for i18n, this could first look for xx/file.html before falling back to file.html. + where xx is the language, e.g. 'en' + """ + log.debug('serve file request %s' % file_name) + if not file_name: + file_name = 'index.html' + elif file_name == 'stage': + file_name = 'stage.html' + elif file_name == 'main': + file_name = 'main.html' + path = os.path.normpath(os.path.join(self.html_dir, file_name)) + if not path.startswith(self.html_dir): + return self.do_not_found() + return self._process_file(path) + def get_content_type(self, file_name): """ Examines the extension of the file and determines what the content_type should be, defaults to text/plain Returns the extension and the content_type + + :param file_name: name of file """ ext = os.path.splitext(file_name)[1] content_type = FILE_TYPES.get(ext, 'text/plain') @@ -391,6 +423,10 @@ class HttpRouter(RegistryProperties): def serve_thumbnail(self, controller_name=None, dimensions=None, file_name=None): """ Serve an image file. If not found return 404. + + :param file_name: file name to be served + :param dimensions: image size + :param controller_name: controller to be called """ log.debug('serve thumbnail %s/thumbnails%s/%s' % (controller_name, dimensions, file_name)) supported_controllers = ['presentations', 'images'] @@ -495,6 +531,8 @@ class HttpRouter(RegistryProperties): def controller_text(self, var): """ Perform an action on the slide controller. + + :param var: variable - not used """ log.debug("controller_text var = %s" % var) current_item = self.live_controller.service_item @@ -628,6 +666,8 @@ class HttpRouter(RegistryProperties): def go_live(self, plugin_name): """ Go live on an item of type ``plugin``. + + :param plugin_name: name of plugin """ try: request_id = json.loads(self.request_data)['request']['id'] @@ -641,6 +681,8 @@ class HttpRouter(RegistryProperties): def add_to_service(self, plugin_name): """ Add item of type ``plugin_name`` to the end of the service. + + :param plugin_name: name of plugin to be called """ try: request_id = json.loads(self.request_data)['request']['id'] diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index d890bb773..2b21c2d83 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -167,7 +167,7 @@ class HTTPSServer(HTTPServer): local_data = AppLocation.get_directory(AppLocation.DataDir) self.socket = ssl.SSLSocket( sock=socket.socket(self.address_family, self.socket_type), - ssl_version=ssl.PROTOCOL_TLSv1, + ssl_version=ssl.PROTOCOL_TLSv1_2, certfile=os.path.join(local_data, 'remotes', 'openlp.crt'), keyfile=os.path.join(local_data, 'remotes', 'openlp.key'), server_side=True) diff --git a/tests/functional/openlp_core_ui_media/test_mediacontroller.py b/tests/functional/openlp_core_ui_media/test_mediacontroller.py index d78f9a06b..c207d1d99 100644 --- a/tests/functional/openlp_core_ui_media/test_mediacontroller.py +++ b/tests/functional/openlp_core_ui_media/test_mediacontroller.py @@ -120,6 +120,66 @@ class TestMediaController(TestCase, TestMixin): # THEN: it should return False self.assertFalse(ret, '_check_file_type should return False when the processor for service_item is None.') + @patch('openlp.core.ui.media.mediacontroller.get_media_players') + @patch('openlp.core.ui.media.mediacontroller.UiStrings') + def check_file_type_automatic_processor_test(self, mocked_uistrings, mocked_get_media_players): + """ + Test that we can play media when players are available and we have a automatic processor from the service item + """ + # GIVEN: A mocked UiStrings, get_media_players, controller, display and service_item + mocked_get_media_players.return_value = (['vlc', 'webkit'], '') + mocked_ret_uistrings = MagicMock() + mocked_ret_uistrings.Automatic = 1 + mocked_uistrings.return_value = mocked_ret_uistrings + media_controller = MediaController() + mocked_vlc = MagicMock() + mocked_vlc.video_extensions_list = ['*.mp4'] + media_controller.media_players = {'vlc': mocked_vlc, 'webkit': MagicMock()} + mocked_controller = MagicMock() + mocked_suffix = MagicMock() + mocked_suffix.return_value = 'mp4' + mocked_controller.media_info.file_info.suffix = mocked_suffix + mocked_display = MagicMock() + mocked_service_item = MagicMock() + mocked_service_item.processor = 1 + + # WHEN: calling _check_file_type when the processor for the service item is None + ret = media_controller._check_file_type(mocked_controller, mocked_display, mocked_service_item) + + # THEN: it should return True + self.assertTrue(ret, '_check_file_type should return True when mediaplayers are available and ' + 'the service item has an automatic processor.') + + @patch('openlp.core.ui.media.mediacontroller.get_media_players') + @patch('openlp.core.ui.media.mediacontroller.UiStrings') + def check_file_type_processor_different_from_available_test(self, mocked_uistrings, mocked_get_media_players): + """ + Test that we can play media when players available are different from the processor from the service item + """ + # GIVEN: A mocked UiStrings, get_media_players, controller, display and service_item + mocked_get_media_players.return_value = (['phonon'], '') + mocked_ret_uistrings = MagicMock() + mocked_ret_uistrings.Automatic = 'automatic' + mocked_uistrings.return_value = mocked_ret_uistrings + media_controller = MediaController() + mocked_phonon = MagicMock() + mocked_phonon.video_extensions_list = ['*.mp4'] + media_controller.media_players = {'phonon': mocked_phonon} + mocked_controller = MagicMock() + mocked_suffix = MagicMock() + mocked_suffix.return_value = 'mp4' + mocked_controller.media_info.file_info.suffix = mocked_suffix + mocked_display = MagicMock() + mocked_service_item = MagicMock() + mocked_service_item.processor = 'vlc' + + # WHEN: calling _check_file_type when the processor for the service item is None + ret = media_controller._check_file_type(mocked_controller, mocked_display, mocked_service_item) + + # THEN: it should return True + self.assertTrue(ret, '_check_file_type should return True when the players available are different' + 'from the processor from the service item.') + def media_play_msg_test(self): """ Test that the media controller responds to the request to play a loaded video diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py index 3789cbe06..99feb2153 100644 --- a/tests/functional/openlp_plugins/remotes/test_router.py +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -343,3 +343,39 @@ class TestRouter(TestCase, TestMixin): # THEN: service_manager.next_item() should have been called self.assertTrue(mocked_previous_item.called, 'previous_item() should have been called in service_manager') + + def remote_stage_personal_html_test(self): + """ + Test the stage url with a parameter after loaded a url/stage.html file + """ + # GIVEN: initial route + self.router.config_dir = '' + self.router.send_response = MagicMock() + self.router.send_header = MagicMock() + self.router.end_headers = MagicMock() + self.router.wfile = MagicMock() + self.router._process_file = MagicMock() + + # WHEN: I call stage with a suffix + self.router.stages('stages', 'trb') + + # THEN: we should use the specific stage file instance + self.router._process_file.assert_called_with(os.path.join('trb', 'stage.html')) + + def remote_stage_personal_css_test(self): + """ + Test the html with reference stages/trb/trb.css then loaded a stages/trb/trb.css file + """ + # GIVEN: initial route + self.router.config_dir = '' + self.router.send_response = MagicMock() + self.router.send_header = MagicMock() + self.router.end_headers = MagicMock() + self.router.wfile = MagicMock() + self.router._process_file = MagicMock() + + # WHEN: I call stage with a suffix + self.router.stages('stages', 'stages/trb/trb.css') + + # THEN: we should use the specific stage file instance + self.router._process_file.assert_called_with(os.path.join('trb', 'trb.css'))