Repository: oppia/oppia

Issue:
Logging out from exploration editor page redirects to 404.
**Steps to reproduce:**

- As a logged in user click on "Create" Button. (it will redirect you to editor page providing you with an unique exploration id )
- Try to log out (Through top-rightmost drop-down toggle).
- You'll be redirected to editor page with the same exploration id and as expected it will show 404 Error.

Expected behaviour:
After logging out from anywhere it should redirect us to /library page.


Discussion:
@shubha1593 please assign me this.
Hi @sroy8091 -- GitHub doesn't yet have functionality for assigning folks who are outside the organization, but in the meantime, please feel free to go ahead and work on this issue, and submit a PR when you're ready. I think it's clear from this conversation thread that you're taking it up, so as long as you submit a PR reasonably soon, there shouldn't be a risk of collision. Thanks!
Okay.
Note to contributors: this issue is again open to tackle, since @sroy8091 is planning to start with an easier issue. Please note the existing work in #4077.

Also, with regards to an implementation strategy, we decided that we'd probably want the logout URL to have a target that looks something like /exp_editor_logout_page. The corresponding backend handler would then retrieve the exploration, check its status, and redirect to the correct destination, similar to https://github.com/oppia/oppia/blob/develop/core/controllers/library.py#L254.
@seanlip i want to work on this issue.
as you mentioned above, i think this is what it means:
* create a url handler '/exp_editor_logout_page' and a corresponding controller in editor.py.

* pass 'redirect_url_on_logout' when rendering a response for '/create' to '/exp_editor_logout_page?return_url = {{ self.request.url }}'

* handle this request in newly created class. if exploration is not published, redirect to /library else redirect to return_url.


That seems reasonable, yes. Remember to write backend tests, too!

Resolving patch:
diff --git a/core/controllers/base.py b/core/controllers/base.py
--- a/core/controllers/base.py
+++ b/core/controllers/base.py
@@ -275,6 +275,18 @@ def render_json(self, values):
         json_output = json.dumps(values, cls=utils.JSONEncoderForHTML)
         self.response.write('%s%s' % (feconf.XSSI_PREFIX, json_output))
 
+    def _get_logout_url(self, redirect_url_on_logout):
+        """Prepares and returns logout url which will be handled
+        by LogoutPage handler.
+
+        Args:
+            redirect_url_on_logout: str. URL to redirect to on logout.
+
+        Returns:
+            str. Logout URL to be handled by LogoutPage handler.
+        """
+        return current_user_services.create_logout_url(redirect_url_on_logout)
+
     def render_template(
             self, filepath, iframe_restriction='DENY',
             redirect_url_on_logout=None):
@@ -356,9 +368,7 @@ def render_template(
 
         if self.user_id:
             values['login_url'] = None
-            values['logout_url'] = (
-                current_user_services.create_logout_url(
-                    redirect_url_on_logout))
+            values['logout_url'] = self._get_logout_url(redirect_url_on_logout)
         else:
             target_url = (
                 '/' if self.request.uri.endswith(feconf.SPLASH_URL)
diff --git a/core/controllers/editor.py b/core/controllers/editor.py
--- a/core/controllers/editor.py
+++ b/core/controllers/editor.py
@@ -19,6 +19,7 @@
 import datetime
 import imghdr
 import logging
+import re
 import StringIO
 
 import jinja2
@@ -101,9 +102,49 @@ def _require_valid_version(version_from_payload, exploration_version):
             % (exploration_version, version_from_payload))
 
 
+class EditorLogoutHandler(base.BaseHandler):
+    """Handles logout from editor page."""
+
+    @acl_decorators.open_access
+    def get(self):
+        """Checks if exploration is published and redirects accordingly."""
+
+        url_to_redirect_to = str(self.request.get('return_url'))
+        url_to_redirect_to_regex = (
+            r'%s/(?P<exploration_id>[\w-]+)$' % feconf.EDITOR_URL_PREFIX)
+        is_valid_path = re.match(url_to_redirect_to_regex, url_to_redirect_to)
+
+        if is_valid_path:
+            exploration_id = is_valid_path.group(1)
+            exploration_rights = rights_manager.get_exploration_rights(
+                exploration_id, strict=False)
+
+            if exploration_rights is None or exploration_rights.is_private():
+                url_to_redirect_to = feconf.LIBRARY_INDEX_URL
+        else:
+            url_to_redirect_to = feconf.LIBRARY_INDEX_URL
+
+        self.redirect(super(EditorLogoutHandler, self)._get_logout_url(
+            url_to_redirect_to))
+
+
 class EditorHandler(base.BaseHandler):
     """Base class for all handlers for the editor page."""
-    pass
+
+    def _get_logout_url(self, redirect_url_on_logout):
+        """This overrides the method in base.BaseHandler.
+        Returns logout url which will be handled by
+        EditorLogoutHandler.
+
+        Args:
+            redirect_url_on_logout: str. URL to redirect to on logout.
+
+        Returns:
+            str. logout url.
+        """
+        logout_url = utils.set_url_query_parameter(
+            '/exploration_editor_logout', 'return_url', redirect_url_on_logout)
+        return logout_url
 
 
 class ExplorationPage(EditorHandler):
@@ -179,7 +220,9 @@ def get(self, exploration_id):
         })
 
         self.render_template(
-            'pages/exploration_editor/exploration_editor.html')
+            'pages/exploration_editor/exploration_editor.html',
+            redirect_url_on_logout=(
+                '%s/%s' % (feconf.EDITOR_URL_PREFIX, exploration_id)))
 
 
 class ExplorationHandler(EditorHandler):
diff --git a/main.py b/main.py
--- a/main.py
+++ b/main.py
@@ -481,6 +481,8 @@ def ui_access_wrapper(self, *args, **kwargs):
         r'/explorationdataextractionhandler', admin.DataExtractionQueryHandler),
     get_redirect_route(r'/frontend_errors', FrontendErrorHandler),
     get_redirect_route(r'/logout', base.LogoutPage),
+    get_redirect_route(
+        r'/exploration_editor_logout', editor.EditorLogoutHandler),
 
     get_redirect_route(
         r'/ml/trainedclassifierhandler', classifier.TrainedClassifierHandler),


Test patch:
diff --git a/core/controllers/editor_test.py b/core/controllers/editor_test.py
--- a/core/controllers/editor_test.py
+++ b/core/controllers/editor_test.py
@@ -412,6 +412,164 @@ def _submit_answer(
             self.logout()
 
 
+class ExplorationEditorLogoutTest(BaseEditorControllerTest):
+    """Test handler for logout from exploration editor page."""
+
+    def test_logout_from_invalid_url(self):
+        """Logour from invalid url should redirect to library.
+        To be caught by regex.
+        """
+
+        published_exp_id = '_published_exp_id-1200'
+        exploration = exp_domain.Exploration.create_default_exploration(
+            published_exp_id)
+        exp_services.save_new_exploration(self.owner_id, exploration)
+        rights_manager.publish_exploration(self.owner, published_exp_id)
+
+        invalid_current_page = '/doesnotexit/%s' % published_exp_id
+        invalid_logout_url = ('/exploration_editor_logout?return_url=%s' % (
+            invalid_current_page))
+
+        self.login(self.OWNER_EMAIL)
+        response = self.testapp.get(invalid_logout_url, expect_errors=False)
+        self.assertEqual(response.status_int, 302)
+        response.follow()
+        self.assertEqual(response.status_int, 302)
+        self.assertIn('library', response.headers['location'])
+        self.logout()
+
+    def test_logout_from_invalid_extra_url(self):
+        """Logour from invalid url should redirect to library.
+        To be caught by regex.
+        """
+
+        published_exp_id = '123-published_exp_id'
+        exploration = exp_domain.Exploration.create_default_exploration(
+            published_exp_id)
+        exp_services.save_new_exploration(self.owner_id, exploration)
+        rights_manager.publish_exploration(self.owner, published_exp_id)
+
+        invalid_current_page = '%s/%s/extra' % (
+            feconf.EDITOR_URL_PREFIX, published_exp_id)
+        invalid_logout_url = ('/exploration_editor_logout?return_url=%s' % (
+            invalid_current_page))
+
+        self.login(self.OWNER_EMAIL)
+        response = self.testapp.get(invalid_logout_url, expect_errors=False)
+        self.assertEqual(response.status_int, 302)
+        response.follow()
+        self.assertEqual(response.status_int, 302)
+        self.assertIn('library', response.headers['location'])
+        self.logout()
+
+    def test_logout_from_invalid_regex_exp_id(self):
+        """Logour from invalid url should redirect to library.
+        To be caught by regex.
+        """
+
+        invalid_regex_exp_id = '1?23-inv@alid_ex#p_id'
+        exploration = exp_domain.Exploration.create_default_exploration(
+            invalid_regex_exp_id)
+        exp_services.save_new_exploration(self.owner_id, exploration)
+        rights_manager.publish_exploration(self.owner, invalid_regex_exp_id)
+
+        invalid_current_page = '%s/%s' % (
+            feconf.EDITOR_URL_PREFIX, invalid_regex_exp_id)
+        invalid_logout_url = ('/exploration_editor_logout?return_url=%s' % (
+            invalid_current_page))
+
+        self.login(self.OWNER_EMAIL)
+        response = self.testapp.get(invalid_logout_url, expect_errors=False)
+        self.assertEqual(response.status_int, 302)
+        response.follow()
+        self.assertEqual(response.status_int, 302)
+        self.assertIn('library', response.headers['location'])
+        self.logout()
+
+    def test_logout_from_empty_url(self):
+        """Logout from empty exploration id should redirect
+        to library page.
+        """
+        empty_redirect_logout_url = '/exploration_editor_logout?return_url='
+
+        self.login(self.OWNER_EMAIL)
+        response = self.testapp.get(empty_redirect_logout_url)
+        self.assertEqual(response.status_int, 302)
+
+        response.follow()
+        self.assertEqual(response.status_int, 302)
+        self.assertIn('library', response.headers['location'])
+        self.logout()
+
+    def test_logout_from_invalid_exploration_id(self):
+        """Logout from invalid exploration id should redirect
+        to library page.
+        """
+
+        invalid_current_page = '%s/%s' % (
+            feconf.EDITOR_URL_PREFIX, 'invalid_eid')
+        invalid_logout_url = (
+            '/exploration_editor_logout?return_url=%s' % invalid_current_page)
+
+        self.login(self.OWNER_EMAIL)
+        response = self.testapp.get(invalid_logout_url, expect_errors=False)
+        self.assertEqual(response.status_int, 302)
+        response.follow()
+        self.assertEqual(response.status_int, 302)
+        self.assertIn('library', response.headers['location'])
+        self.logout()
+
+    def test_logout_from_unpublished_exploration_editor(self):
+        """Logout from unpublished exploration should redirect
+        to library page.
+        """
+
+        unpublished_exp_id = '_unpublished_eid123'
+        exploration = exp_domain.Exploration.create_default_exploration(
+            unpublished_exp_id)
+        exp_services.save_new_exploration(self.owner_id, exploration)
+
+        current_page_url = '%s/%s' % (
+            feconf.EDITOR_URL_PREFIX, unpublished_exp_id)
+        self.login(self.OWNER_EMAIL)
+        response = self.testapp.get(current_page_url, expect_errors=False)
+        self.assertEqual(response.status_int, 200)
+
+        response = self.testapp.get(
+            '/exploration_editor_logout?return_url=%s' % current_page_url)
+        self.assertEqual(response.status_int, 302)
+        response = response.follow()
+        self.assertEqual(response.status_int, 302)
+        self.assertIn('library', response.headers['location'])
+        self.logout()
+
+    def test_logout_from_published_exploration_editor(self):
+        """Logout from published exploration should redirect
+        to same page.
+        """
+
+        published_exp_id = 'published_eid-123'
+        exploration = exp_domain.Exploration.create_default_exploration(
+            published_exp_id)
+        exp_services.save_new_exploration(self.owner_id, exploration)
+
+        current_page_url = '%s/%s' % (
+            feconf.EDITOR_URL_PREFIX, published_exp_id)
+        self.login(self.OWNER_EMAIL)
+        response = self.testapp.get(current_page_url, expect_errors=False)
+        self.assertEqual(response.status_int, 200)
+
+        rights_manager.publish_exploration(self.owner, published_exp_id)
+
+        response = self.testapp.get(
+            '/exploration_editor_logout?return_url=%s' % current_page_url)
+        self.assertEqual(response.status_int, 302)
+        response = response.follow()
+        self.assertEqual(response.status_int, 302)
+        self.assertIn(current_page_url, response.headers['location'])
+        self.logout()
+
+
 class DownloadIntegrationTest(BaseEditorControllerTest):
     """Test handler for exploration and state download."""