From fd380a5239edb8b076dceb9a118b5471d9422b31 Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Tue, 11 Oct 2022 16:24:37 +0200 Subject: [PATCH 01/70] WIP: Adds context to 404 page --- readthedocs/core/views/__init__.py | 18 ++++++++++++++++++ readthedocs/urls.py | 2 ++ 2 files changed, 20 insertions(+) diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py index 7caa3b18cb6..4d2e34ea114 100644 --- a/readthedocs/core/views/__init__.py +++ b/readthedocs/core/views/__init__.py @@ -48,6 +48,24 @@ def get_context_data(self, **kwargs): return context +def server_error_404(request, template_name="404.html"): + """A simple 404 handler.""" + project_slug = getattr(request, "host_project_slug", None) + log.debug( + "404 page detected a project slug in request.", + project_slug=project_slug, + ) + project = None + if project_slug: + try: + project = Project.objects.get(slug=project_slug) + except Project.DoesNotExist: + pass + r = render(request, template_name, context={"project": project}) + r.status_code = 404 + return r + + def server_error_500(request, template_name='500.html'): """A simple 500 handler so we get media.""" r = render(request, template_name) diff --git a/readthedocs/urls.py b/readthedocs/urls.py index 579c059ca96..1df4981624e 100644 --- a/readthedocs/urls.py +++ b/readthedocs/urls.py @@ -13,12 +13,14 @@ HomepageView, SupportView, do_not_track, + server_error_404, server_error_500, ) from readthedocs.search.views import GlobalSearchView admin.autodiscover() +handler404 = server_error_404 handler500 = server_error_500 basic_urls = [ From aca278d7141a0af4c1f8d950cdd49b545b043302 Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Wed, 12 Oct 2022 16:08:59 +0200 Subject: [PATCH 02/70] WIP: Adds a test 404 page --- readthedocs/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/readthedocs/urls.py b/readthedocs/urls.py index 1df4981624e..63198333aa3 100644 --- a/readthedocs/urls.py +++ b/readthedocs/urls.py @@ -25,6 +25,7 @@ basic_urls = [ path("", HomepageView.as_view(), name="homepage"), + path("404/", server_error_404), re_path(r"^security/", TemplateView.as_view(template_name="security.html")), re_path( r'^\.well-known/security.txt$', From 7f86767c843164432818d5be6a7a5c605dbe1249 Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Wed, 12 Oct 2022 17:31:26 +0200 Subject: [PATCH 03/70] Contextualization gives us 3 interesting scenarios for the 404 page --- readthedocs/templates/404.html | 12 ++++++++++++ readthedocs/urls.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/readthedocs/templates/404.html b/readthedocs/templates/404.html index a364ec864c5..9393e9b0ba1 100644 --- a/readthedocs/templates/404.html +++ b/readthedocs/templates/404.html @@ -16,6 +16,18 @@ {% block language-select-form %}{% endblock %} {% block content %} + +{% if project %} +{# project exists #} +Project {{ project }} was found but the page from the project wasn't +{% elif "_proxito_404_" in request.path %} +{# we are looking at a 404 generated by proxito, we then know that the project itself doesn't exist or hasn't been built #} +Project does not exist +{% else %} +{# we are on readthedocs.org itself #} +readthedocs.org should not have 404s, so you must have followed a broken link. +{% endif %} +
 
         \          SORRY            /
diff --git a/readthedocs/urls.py b/readthedocs/urls.py
index 63198333aa3..6bc61c4ca20 100644
--- a/readthedocs/urls.py
+++ b/readthedocs/urls.py
@@ -25,7 +25,6 @@
 
 basic_urls = [
     path("", HomepageView.as_view(), name="homepage"),
-    path("404/", server_error_404),
     re_path(r"^security/", TemplateView.as_view(template_name="security.html")),
     re_path(
         r'^\.well-known/security.txt$',
@@ -131,6 +130,7 @@
         document_root=os.path.join(settings.MEDIA_ROOT, build_format),
     )
 debug_urls += [
+    path("404/", server_error_404),
     path(
         "style-catalog/",
         TemplateView.as_view(template_name="style_catalog.html"),

From 31ec5f555fa137b70432079677de65443ed11684 Mon Sep 17 00:00:00 2001
From: Benjamin Bach 
Date: Wed, 12 Oct 2022 17:51:02 +0200
Subject: [PATCH 04/70] 404 handler: Receive exception keyword

---
 readthedocs/core/views/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py
index 4d2e34ea114..bfdde2bc216 100644
--- a/readthedocs/core/views/__init__.py
+++ b/readthedocs/core/views/__init__.py
@@ -48,7 +48,7 @@ def get_context_data(self, **kwargs):
         return context
 
 
-def server_error_404(request, template_name="404.html"):
+def server_error_404(request, template_name="404.html", exception=exception):
     """A simple 404 handler."""
     project_slug = getattr(request, "host_project_slug", None)
     log.debug(

From 8cbff084845a9a7ad0fd660f96be8bab245ec171 Mon Sep 17 00:00:00 2001
From: Benjamin Bach 
Date: Thu, 27 Oct 2022 19:33:34 +0200
Subject: [PATCH 05/70] Improve contextualized messages, not ready for review,
 needs further testing, and search for isn't safe to assume

---
 readthedocs/core/views/__init__.py |  8 +++--
 readthedocs/settings/base.py       |  4 +--
 readthedocs/templates/404.html     | 47 +++++++++++++++++++++++++-----
 3 files changed, 48 insertions(+), 11 deletions(-)

diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py
index bfdde2bc216..398c5812ece 100644
--- a/readthedocs/core/views/__init__.py
+++ b/readthedocs/core/views/__init__.py
@@ -48,7 +48,7 @@ def get_context_data(self, **kwargs):
         return context
 
 
-def server_error_404(request, template_name="404.html", exception=exception):
+def server_error_404(request, template_name="404.html", exception=None):
     """A simple 404 handler."""
     project_slug = getattr(request, "host_project_slug", None)
     log.debug(
@@ -61,7 +61,11 @@ def server_error_404(request, template_name="404.html", exception=exception):
             project = Project.objects.get(slug=project_slug)
         except Project.DoesNotExist:
             pass
-    r = render(request, template_name, context={"project": project})
+    r = render(
+        request,
+        template_name,
+        context={"project": project, "project_slug": project_slug},
+    )
     r.status_code = 404
     return r
 
diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py
index b1366a3003e..752ee342137 100644
--- a/readthedocs/settings/base.py
+++ b/readthedocs/settings/base.py
@@ -44,7 +44,7 @@ class CommunityBaseSettings(Settings):
     DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
 
     # Debug settings
-    DEBUG = True
+    DEBUG = False
     RTD_FORCE_SHOW_DEBUG_TOOLBAR = False
 
     @property
@@ -73,7 +73,7 @@ def SHOW_DEBUG_TOOLBAR(self):
     PRODUCTION_DOMAIN = 'readthedocs.org'
     PUBLIC_DOMAIN = None
     PUBLIC_DOMAIN_USES_HTTPS = False
-    USE_SUBDOMAIN = False
+    USE_SUBDOMAIN = True
     PUBLIC_API_URL = 'https://{}'.format(PRODUCTION_DOMAIN)
     RTD_INTERSPHINX_URL = 'https://{}'.format(PRODUCTION_DOMAIN)
     RTD_EXTERNAL_VERSION_DOMAIN = 'external-builds.readthedocs.io'
diff --git a/readthedocs/templates/404.html b/readthedocs/templates/404.html
index 9393e9b0ba1..082e74210fa 100644
--- a/readthedocs/templates/404.html
+++ b/readthedocs/templates/404.html
@@ -3,7 +3,7 @@
 {% load i18n %}
 
 {% block title %}
-  {% trans "Maze Found" %}
+  {% trans "404 - Maze Found" %}
 {% endblock %}
 
 {% block header-wrapper %}
@@ -17,15 +17,48 @@
 
 {% block content %}
 
+

{% trans "404 - page not found" %}

+ {% if project %} -{# project exists #} -Project {{ project }} was found but the page from the project wasn't + + {# project exists #} + +

You are browsing the documentation of "{{ project.slug }}". The page you are looking for at {{ request.path }} was not found.

+ +

{% trans "Documentation changes over time. Pages are moved around. You can try to navigate to the front page of the project and finding the page through its navigation." %}

+ +

{% trans "You can try searching for a keyword:" %}

+ +
+ + + +
+ +

{% trans "Are you the project owner?" %}

+ +

{% trans "Here are some tips to address 404 errors:" %}

+ + + {% elif "_proxito_404_" in request.path %} -{# we are looking at a 404 generated by proxito, we then know that the project itself doesn't exist or hasn't been built #} -Project does not exist + + {# we are looking at a 404 generated by proxito, we then know that the project itself doesn't exist or hasn't been built #} + +

+ {% blocktrans %}The project "{{ project_slug }}" that you are looking for either does not exist, has been deleted or does not yet have any valid builds.{% endblocktrans %} +

+ {% else %} -{# we are on readthedocs.org itself #} -readthedocs.org should not have 404s, so you must have followed a broken link. + +

{% trans "You have encountered a 404 on Read the Docs. You are either looking for a page that does not exist or a project that has been removed." %}

+ +

{% trans "If you believe this is a technical error, please contact our support:" %} {% trans "Go to support" %}»

+ {% endif %}

From bc73398ce8e62f1a01fd34c8249f38edeb008a40 Mon Sep 17 00:00:00 2001
From: Benjamin Bach 
Date: Tue, 1 Nov 2022 15:01:22 +0100
Subject: [PATCH 06/70] Remove search form, that was a bad idea, since it will
 only work on standard Sphinx projects.

---
 readthedocs/templates/404.html | 17 ++++++-----------
 1 file changed, 6 insertions(+), 11 deletions(-)

diff --git a/readthedocs/templates/404.html b/readthedocs/templates/404.html
index 082e74210fa..5f57d22c304 100644
--- a/readthedocs/templates/404.html
+++ b/readthedocs/templates/404.html
@@ -23,17 +23,12 @@ 

{% trans "404 - page not found" %}

{# project exists #} -

You are browsing the documentation of "{{ project.slug }}". The page you are looking for at {{ request.path }} was not found.

- -

{% trans "Documentation changes over time. Pages are moved around. You can try to navigate to the front page of the project and finding the page through its navigation." %}

- -

{% trans "You can try searching for a keyword:" %}

- -
- +

+ {% blocktrans with project_slug=project.slug request_path=request.path %}You are browsing the documentation of "{{ project_slug }}". The page you are looking for at {{ request.path }} was not found.{% endblocktrans %} +

- -
+

+ {% blocktrans %}Documentation changes over time. Pages are moved around. You can try to navigate to the index page of the project and use its navigation or search function to find a similar page.{% endblocktrans %}

{% trans "Are you the project owner?" %}

@@ -50,7 +45,7 @@

{% trans "Are you the project owner?" %}

{# we are looking at a 404 generated by proxito, we then know that the project itself doesn't exist or hasn't been built #}

- {% blocktrans %}The project "{{ project_slug }}" that you are looking for either does not exist, has been deleted or does not yet have any valid builds.{% endblocktrans %} + {% blocktrans with project_slug=project_slug %}The project "{{ project_slug }}" that you are looking for either does not exist, has been deleted or does not yet have any valid builds.{% endblocktrans %}

{% else %} From a8573f9bf922fd16a6d467856e35261c2f164419 Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Wed, 2 Nov 2022 18:39:44 +0100 Subject: [PATCH 07/70] Early work on an approach that uses a custom Http404 exception to save relevant 404 context --- readthedocs/core/views/__init__.py | 12 ++++---- readthedocs/proxito/exceptions.py | 9 ++++++ readthedocs/proxito/views/decorators.py | 16 +++++++--- readthedocs/proxito/views/serve.py | 29 +++++++++++++----- readthedocs/proxito/views/utils.py | 40 ++++++++++++++++++++----- 5 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 readthedocs/proxito/exceptions.py diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py index 398c5812ece..16592ed7474 100644 --- a/readthedocs/core/views/__init__.py +++ b/readthedocs/core/views/__init__.py @@ -50,17 +50,15 @@ def get_context_data(self, **kwargs): def server_error_404(request, template_name="404.html", exception=None): """A simple 404 handler.""" - project_slug = getattr(request, "host_project_slug", None) + # This property is set by ProxitoHttp404. We could also have a look at the + # subproject_slug + project_slug = getattr(exception, "project_slug", None) + log.debug(exception) + project = getattr(exception, "project", None) log.debug( "404 page detected a project slug in request.", project_slug=project_slug, ) - project = None - if project_slug: - try: - project = Project.objects.get(slug=project_slug) - except Project.DoesNotExist: - pass r = render( request, template_name, diff --git a/readthedocs/proxito/exceptions.py b/readthedocs/proxito/exceptions.py new file mode 100644 index 00000000000..233385f1f1b --- /dev/null +++ b/readthedocs/proxito/exceptions.py @@ -0,0 +1,9 @@ +from django.http.response import Http404 + + +class ProxitoHttp404(Http404): + def __init__(self, message, project_slug=None, project=None, subproject_slug=None): + self.project_slug = project_slug + self.project = project + self.subproject_slug = subproject_slug + super().__init__(message) diff --git a/readthedocs/proxito/views/decorators.py b/readthedocs/proxito/views/decorators.py index 11a744e72b6..1e422523e9e 100644 --- a/readthedocs/proxito/views/decorators.py +++ b/readthedocs/proxito/views/decorators.py @@ -1,9 +1,9 @@ -import structlog from functools import wraps -from django.http import Http404 +import structlog from readthedocs.projects.models import Project, ProjectRelationship +from readthedocs.proxito.exceptions import ProxitoHttp404 log = structlog.get_logger(__name__) # noqa @@ -44,7 +44,9 @@ def inner_view( # noqa subproject_slug=subproject_slug, project_slug=kwargs['project'].slug, ) - raise Http404('Invalid subproject slug') + raise ProxitoHttp404( + "Invalid subproject slug", subproject_slug=subproject_slug + ) return view_func(request, subproject=subproject, *args, **kwargs) return inner_view @@ -74,7 +76,13 @@ def inner_view( # noqa try: project = Project.objects.get(slug=project_slug) except Project.DoesNotExist: - raise Http404('Project does not exist.') + log.debug( + "Project not found.", + project_slug=project_slug, + ) + raise ProxitoHttp404( + "Project does not exist.", project_slug=project_slug + ) return view_func(request, project=project, *args, **kwargs) return inner_view diff --git a/readthedocs/proxito/views/serve.py b/readthedocs/proxito/views/serve.py index 1e08401cb3f..a98dc2beda6 100644 --- a/readthedocs/proxito/views/serve.py +++ b/readthedocs/proxito/views/serve.py @@ -4,7 +4,7 @@ import structlog from django.conf import settings -from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.urls import resolve as url_resolve from django.utils.decorators import method_decorator @@ -22,6 +22,7 @@ from readthedocs.projects.constants import SPHINX_HTMLDIR from readthedocs.projects.models import Feature from readthedocs.projects.templatetags.projects_tags import sort_version_aware +from readthedocs.proxito.exceptions import ProxitoHttp404 from readthedocs.redirects.exceptions import InfiniteRedirectException from readthedocs.storage import build_media_storage, staticfiles_storage @@ -109,7 +110,7 @@ def get(self, # version on the database we want to return 404. if (version and not version.active) or (version_slug and not version): log.warning("Version does not exist or is not active.") - raise Http404("Version does not exist or is not active.") + raise ProxitoHttp404("Version does not exist or is not active.") if self._is_cache_enabled(final_project) and version and not version.is_private: # All public versions can be cached. @@ -174,7 +175,7 @@ def get(self, 'Invalid URL for project with versions.', filename=filename, ) - raise Http404('Invalid URL for project with versions') + raise ProxitoHttp404("Invalid URL for project with versions") redirect_path, http_status = self.get_redirect( project=final_project, @@ -261,10 +262,17 @@ def get(self, request, proxito_path, template_name='404.html'): proxito_path, urlconf='readthedocs.proxito.urls', ) + log.debug("Resolved a URL.") version_slug = kwargs.get('version_slug') version_slug = self.get_version_from_host(request, version_slug) - final_project, lang_slug, version_slug, filename = _get_project_data_from_request( # noqa + log.debug("Getting _get_project_data_from_request.") + ( + final_project, + lang_slug, + version_slug, + filename, + ) = _get_project_data_from_request( # noqa request, project_slug=kwargs.get('project_slug'), subproject_slug=kwargs.get('subproject_slug'), @@ -272,6 +280,7 @@ def get(self, request, proxito_path, template_name='404.html'): version_slug=version_slug, filename=kwargs.get('filename', ''), ) + log.debug("Finished _get_project_data_from_request.") log.bind( project_slug=final_project.slug, @@ -390,7 +399,13 @@ def get(self, request, proxito_path, template_name='404.html'): path=filename, full_path=proxito_path, ) - raise Http404('No custom 404 page found.') + log.debug("Raising ProxitoHttp404") + raise ProxitoHttp404( + "No custom 404 page found.", + project=final_project, + project_slug=kwargs.get("project_slug"), + subproject_slug=kwargs.get("subproject_slug"), + ) def _register_broken_link(self, project, version, path, full_path): try: @@ -472,7 +487,7 @@ def get(self, request, project): if no_serve_robots_txt: # ... we do return a 404 - raise Http404() + raise ProxitoHttp404() storage_path = project.get_storage_path( type_='html', @@ -595,7 +610,7 @@ def changefreqs_generator(): only_active=True, ) if not public_versions.exists(): - raise Http404 + raise ProxitoHttp404() sorted_versions = sort_version_aware(public_versions) diff --git a/readthedocs/proxito/views/utils.py b/readthedocs/proxito/views/utils.py index 77cb58d7fdb..84b14435686 100644 --- a/readthedocs/proxito/views/utils.py +++ b/readthedocs/proxito/views/utils.py @@ -2,7 +2,10 @@ import structlog from django.http import HttpResponse -from django.shortcuts import get_object_or_404, render +from django.shortcuts import render + +from readthedocs.projects.models import Project +from readthedocs.proxito.exceptions import ProxitoHttp404 from .decorators import map_project_slug, map_subproject_slug @@ -30,11 +33,23 @@ def proxito_404_page_handler(request, exception=None, template_name='404.html'): """ if request.resolver_match and request.resolver_match.url_name != 'proxito_404_handler': + log.debug("Displaying a 'fast 404'") return fast_404(request, exception, template_name) - resp = render(request, template_name) - resp.status_code = 404 - return resp + project_slug = getattr(exception, "project_slug", None) + log.debug(exception) + project = getattr(exception, "project", None) + log.debug( + "404 page detected a project slug in request.", + project_slug=project_slug, + ) + r = render( + request, + template_name, + context={"project": project, "project_slug": project_slug}, + ) + r.status_code = 404 + return r @map_project_slug @@ -70,9 +85,20 @@ def _get_project_data_from_request( if not lang_slug or lang_slug == current_project.language: final_project = current_project else: - final_project = get_object_or_404( - current_project.translations.all(), language=lang_slug - ) + try: + final_project = current_project.translations.get(language=lang_slug) + except Project.DoesNotExist: + log.debug( + "Raising 404 for language slug", + lang_slug=lang_slug, + project_slug=current_project.slug, + ) + raise ProxitoHttp404( + "Did not find translation", + project=current_project, + project_slug=current_project.slug, + subproject_slug=lang_slug, + ) # Handle single version by grabbing the default version # We might have version_slug when we're serving a PR From 8bd53b6faeb88546837276f964bf986f29319ee8 Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Thu, 3 Nov 2022 18:13:56 +0100 Subject: [PATCH 08/70] Refactor error templates to separate contextualized handling + some template DRY stuff. --- readthedocs/core/views/__init__.py | 18 ++-- readthedocs/proxito/exceptions.py | 47 ++++++++++ readthedocs/templates/401.html | 11 +-- readthedocs/templates/404.html | 86 +------------------ readthedocs/templates/429.html | 11 +-- readthedocs/templates/500.html | 11 +-- readthedocs/templates/error_header.html | 1 + readthedocs/templates/errors/404/base.html | 50 +++++++++++ .../templates/errors/404/community.html | 12 +++ .../templates/errors/404/include_tips.html | 11 +++ .../templates/errors/404/no_project.html | 16 ++++ .../templates/errors/404/no_project_page.html | 22 +++++ .../templates/errors/404/no_subproject.html | 22 +++++ readthedocs/templates/errors/base.html | 28 ++++++ 14 files changed, 227 insertions(+), 119 deletions(-) create mode 100644 readthedocs/templates/errors/404/base.html create mode 100644 readthedocs/templates/errors/404/community.html create mode 100644 readthedocs/templates/errors/404/include_tips.html create mode 100644 readthedocs/templates/errors/404/no_project.html create mode 100644 readthedocs/templates/errors/404/no_project_page.html create mode 100644 readthedocs/templates/errors/404/no_subproject.html create mode 100644 readthedocs/templates/errors/base.html diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py index 16592ed7474..3eb85bdd1b4 100644 --- a/readthedocs/core/views/__init__.py +++ b/readthedocs/core/views/__init__.py @@ -48,17 +48,25 @@ def get_context_data(self, **kwargs): return context -def server_error_404(request, template_name="404.html", exception=None): +def server_error_404( + request, template_name="errors/404/community.html", exception=None +): """A simple 404 handler.""" # This property is set by ProxitoHttp404. We could also have a look at the # subproject_slug + project_slug = getattr(exception, "project_slug", None) log.debug(exception) project = getattr(exception, "project", None) - log.debug( - "404 page detected a project slug in request.", - project_slug=project_slug, - ) + subproject_slug = getattr(exception, "subproject_slug", None) + + if subproject_slug: + template_name = "errors/404/no_subproject.html" + elif project: + template_name = "errors/404/no_project_page.html" + elif project_slug: + template_name = "errors/404/no_project.html" + r = render( request, template_name, diff --git a/readthedocs/proxito/exceptions.py b/readthedocs/proxito/exceptions.py index 233385f1f1b..7f65abb7f92 100644 --- a/readthedocs/proxito/exceptions.py +++ b/readthedocs/proxito/exceptions.py @@ -1,7 +1,54 @@ +""" +This module contains exceptions that may be raised in the proxito application +but are handled elsewhere in the Django project. + +-------------- +404 exceptions +-------------- + +Because an Http404 can be raised anywhere in the code, +we want to add more context for targeted error handling and user-facing communication +""" from django.http.response import Http404 class ProxitoHttp404(Http404): + """ + General error class for proxity 404s + """ + + def __init__(self, message): + super().__init__(message) + + +class ProxitoProjectHttp404(ProxitoHttp404): + """ + Raised if a project was not found + """ + + def __init__(self, message, project_slug=None): + self.project_slug = project_slug + super().__init__(message) + + +class ProxitoSubProjectHttp404(ProxitoProjectHttp404): + """ + Raised if a sub-project was not found + """ + def __init__(self, message, project_slug=None, project=None, subproject_slug=None): + self.project_slug = project_slug + self.project = project + self.subproject_slug = subproject_slug + super().__init__(message) + + +class ProxitoProjectPageHttp404(ProxitoHttp404): + """ + Raised if a page inside an existing project was not found + + Note: The containing project can be both a project or a subproject inside another project + """ + def __init__(self, message, project_slug=None, project=None, subproject_slug=None): self.project_slug = project_slug self.project = project diff --git a/readthedocs/templates/401.html b/readthedocs/templates/401.html index d9d818be0da..b5db786a6df 100644 --- a/readthedocs/templates/401.html +++ b/readthedocs/templates/401.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "errors/base.html" %} {% load core_tags %} {% load i18n %} @@ -6,15 +6,6 @@ {% trans "Maze Found" %} {% endblock %} -{% block header-wrapper %} - {% include "error_header.html" %} -{% endblock %} - -{% block notify %}{% endblock %} - -{# Hide the language select form so we don't set a CSRF cookie #} -{% block language-select-form %}{% endblock %} - {% block content %}

Permission Denied

diff --git a/readthedocs/templates/404.html b/readthedocs/templates/404.html index 5f57d22c304..790202fa6a0 100644 --- a/readthedocs/templates/404.html +++ b/readthedocs/templates/404.html @@ -1,84 +1,2 @@ -{% extends "base.html" %} -{% load core_tags %} -{% load i18n %} - -{% block title %} - {% trans "404 - Maze Found" %} -{% endblock %} - -{% block header-wrapper %} - {% include "error_header.html" %} -{% endblock %} - -{% block notify %}{% endblock %} - -{# Hide the language select form so we don't set a CSRF cookie #} -{% block language-select-form %}{% endblock %} - -{% block content %} - -

{% trans "404 - page not found" %}

- -{% if project %} - - {# project exists #} - -

- {% blocktrans with project_slug=project.slug request_path=request.path %}You are browsing the documentation of "{{ project_slug }}". The page you are looking for at {{ request.path }} was not found.{% endblocktrans %} -

- -

- {% blocktrans %}Documentation changes over time. Pages are moved around. You can try to navigate to the index page of the project and use its navigation or search function to find a similar page.{% endblocktrans %}

- -

{% trans "Are you the project owner?" %}

- -

{% trans "Here are some tips to address 404 errors:" %}

- - - -{% elif "_proxito_404_" in request.path %} - - {# we are looking at a 404 generated by proxito, we then know that the project itself doesn't exist or hasn't been built #} - -

- {% blocktrans with project_slug=project_slug %}The project "{{ project_slug }}" that you are looking for either does not exist, has been deleted or does not yet have any valid builds.{% endblocktrans %} -

- -{% else %} - -

{% trans "You have encountered a 404 on Read the Docs. You are either looking for a page that does not exist or a project that has been removed." %}

- -

{% trans "If you believe this is a technical error, please contact our support:" %} {% trans "Go to support" %}»

- -{% endif %} - -
-
-        \          SORRY            /
-         \                         /
-          \    This page does     /
-           ]   not exist yet.    [    ,'|
-           ]                     [   /  |
-           ]___               ___[ ,'   |
-           ]  ]\             /[  [ |:   |
-           ]  ] \           / [  [ |:   |
-           ]  ]  ]         [  [  [ |:   |
-           ]  ]  ]__     __[  [  [ |:   |
-           ]  ]  ] ]\ _ /[ [  [  [ |:   |
-           ]  ]  ] ] (#) [ [  [  [ :===='
-           ]  ]  ]_].nHn.[_[  [  [
-           ]  ]  ]  HHHHH. [  [  [
-           ]  ] /   `HH("N  \ [  [
-           ]__]/     HHH  "  \[__[
-           ]         NNN         [
-           ]         N/"         [
-           ]         N H         [
-          /          N            \
-         /           q,            \
-        /                           \
-
-{% endblock %} +{% extends "errors/404/base.html" %} +{# this is a deprecated template, will be removed. Please use one of the targeted 404s in templates/404/ #} diff --git a/readthedocs/templates/429.html b/readthedocs/templates/429.html index b7c79fd8c16..600a2b3ff7a 100644 --- a/readthedocs/templates/429.html +++ b/readthedocs/templates/429.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "errors/base.html" %} {% load core_tags %} {% load i18n %} @@ -6,15 +6,6 @@ {% trans "Too many requests" %} {% endblock %} -{% block header-wrapper %} - {% include "error_header.html" %} -{% endblock %} - -{% block notify %}{% endblock %} - -{# Hide the language select form so we don't set a CSRF cookie #} -{% block language-select-form %}{% endblock %} - {% block content %}
               .--~~,__
diff --git a/readthedocs/templates/500.html b/readthedocs/templates/500.html
index 70693b5ea3a..381ca625572 100644
--- a/readthedocs/templates/500.html
+++ b/readthedocs/templates/500.html
@@ -1,19 +1,10 @@
-{% extends "base.html" %}
+{% extends "errors/base.html" %}
 {% load i18n %}
 
 {% block title %}
   {% trans "Server Error" %}
 {% endblock %}
 
-{% block header-wrapper %}
-  {% include "error_header.html" %}
-{% endblock %}
-
-{% block notify %}{% endblock %}
-
-{# Hide the language select form so we don't set a CSRF cookie #}
-{% block language-select-form %}{% endblock %}
-
 {% block content %}
 
           .
diff --git a/readthedocs/templates/error_header.html b/readthedocs/templates/error_header.html
index e6cd5b2234f..ecebdbc745b 100644
--- a/readthedocs/templates/error_header.html
+++ b/readthedocs/templates/error_header.html
@@ -1,4 +1,5 @@
 {% load i18n %}
+{# this template is deprecated, please extend from errors/base.html #}
 
     
diff --git a/readthedocs/templates/errors/404/base.html b/readthedocs/templates/errors/404/base.html new file mode 100644 index 00000000000..b82c0e9ff37 --- /dev/null +++ b/readthedocs/templates/errors/404/base.html @@ -0,0 +1,50 @@ +{% extends "errors/base.html" %} +{% load core_tags %} +{% load i18n %} + +{% block title %} + {% trans "404 - Maze Found" %} +{% endblock %} + +{% block content %} + +

{% trans "404 - page not found" %}

+ +{% block 404_error_message %} + +{% endblock %} + +{% if project %} + +{% elif "_proxito_404_" in request.path %} + +{% else %} + +{% endif %} + +
+
+        \          SORRY            /
+         \                         /
+          \    This page does     /
+           ]   not exist yet.    [    ,'|
+           ]                     [   /  |
+           ]___               ___[ ,'   |
+           ]  ]\             /[  [ |:   |
+           ]  ] \           / [  [ |:   |
+           ]  ]  ]         [  [  [ |:   |
+           ]  ]  ]__     __[  [  [ |:   |
+           ]  ]  ] ]\ _ /[ [  [  [ |:   |
+           ]  ]  ] ] (#) [ [  [  [ :===='
+           ]  ]  ]_].nHn.[_[  [  [
+           ]  ]  ]  HHHHH. [  [  [
+           ]  ] /   `HH("N  \ [  [
+           ]__]/     HHH  "  \[__[
+           ]         NNN         [
+           ]         N/"         [
+           ]         N H         [
+          /          N            \
+         /           q,            \
+        /                           \
+
+{% endblock %} diff --git a/readthedocs/templates/errors/404/community.html b/readthedocs/templates/errors/404/community.html new file mode 100644 index 00000000000..81774812422 --- /dev/null +++ b/readthedocs/templates/errors/404/community.html @@ -0,0 +1,12 @@ +{% extends "errors/404/base.html" %} +{% load i18n %} + +{# something on readthedocs.org does not exist - this 404 is not intended for projects #} + +{% block 404_error_message %} + +

{% trans "You have encountered a 404 on Read the Docs. You are either looking for a page that does not exist or a project that has been removed." %}

+ +

{% trans "If you believe this is a technical error, please contact our support:" %} {% trans "Go to support" %}»

+ +{% endblock %} diff --git a/readthedocs/templates/errors/404/include_tips.html b/readthedocs/templates/errors/404/include_tips.html new file mode 100644 index 00000000000..ee42ee8b74f --- /dev/null +++ b/readthedocs/templates/errors/404/include_tips.html @@ -0,0 +1,11 @@ +{% load i18n %} + +

{% trans "Are you the project owner?" %}

+ +

{% trans "Here are some tips to address 404 errors:" %}

+ + diff --git a/readthedocs/templates/errors/404/no_project.html b/readthedocs/templates/errors/404/no_project.html new file mode 100644 index 00000000000..f3ff62966ed --- /dev/null +++ b/readthedocs/templates/errors/404/no_project.html @@ -0,0 +1,16 @@ +{% extends "errors/404/base.html" %} +{% load i18n %} + +{# visiting a slug or subdomain that doesn't match the intended project #} + +{% block 404_error_message %} + + {# we are looking at a 404 generated by proxito, we then know that the project itself doesn't exist or hasn't been built #} + +

+ {% blocktrans trimmed with project_slug=project_slug %} + The project "{{ project_slug }}" that you are looking for either does not exist, has been deleted or does not yet have any valid builds. + {% endblocktrans %} +

+ +{% endblock %} diff --git a/readthedocs/templates/errors/404/no_project_page.html b/readthedocs/templates/errors/404/no_project_page.html new file mode 100644 index 00000000000..bce6d1042bd --- /dev/null +++ b/readthedocs/templates/errors/404/no_project_page.html @@ -0,0 +1,22 @@ +{% extends "errors/404/base.html" %} +{% load i18n %} + +{# project exists but page doesn't #} + +{% block 404_error_message %} + +

+ {% blocktrans trimmed with project_slug=project.slug request_path=request.path %} + You are browsing the documentation of "{{ project_slug }}". The page you are looking for at {{ request.path }} was not found. + {% endblocktrans %} +

+ +

+ {% blocktrans trimmed %} + Documentation changes over time. Pages are moved around. You can try to navigate to the index page of the project and use its navigation or search function to find a similar page. + {% endblocktrans %} +

+ + {% include "errors/404/include_tips.html" %} + +{% endblock %} diff --git a/readthedocs/templates/errors/404/no_subproject.html b/readthedocs/templates/errors/404/no_subproject.html new file mode 100644 index 00000000000..bce6d1042bd --- /dev/null +++ b/readthedocs/templates/errors/404/no_subproject.html @@ -0,0 +1,22 @@ +{% extends "errors/404/base.html" %} +{% load i18n %} + +{# project exists but page doesn't #} + +{% block 404_error_message %} + +

+ {% blocktrans trimmed with project_slug=project.slug request_path=request.path %} + You are browsing the documentation of "{{ project_slug }}". The page you are looking for at {{ request.path }} was not found. + {% endblocktrans %} +

+ +

+ {% blocktrans trimmed %} + Documentation changes over time. Pages are moved around. You can try to navigate to the index page of the project and use its navigation or search function to find a similar page. + {% endblocktrans %} +

+ + {% include "errors/404/include_tips.html" %} + +{% endblock %} diff --git a/readthedocs/templates/errors/base.html b/readthedocs/templates/errors/base.html new file mode 100644 index 00000000000..6bc2a137ab4 --- /dev/null +++ b/readthedocs/templates/errors/base.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% load core_tags %} +{% load i18n %} + +{% block header-wrapper %} +
+
+ + + +
+

+ {% comment %}Translators: Name of the website{% endcomment %} + + Read the Docs + +

+
+ +
+
+ +{% endblock %} + +{% block notify %}{% endblock %} + +{# Hide the language select form so we don't set a CSRF cookie #} +{% block language-select-form %}{% endblock %} From 7d1c69ad06a0d31cfaf293c6287d832059a91831 Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Fri, 4 Nov 2022 01:20:23 +0100 Subject: [PATCH 09/70] Apply new exceptions in some of the cases --- readthedocs/proxito/views/decorators.py | 9 ++++++--- readthedocs/proxito/views/serve.py | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/readthedocs/proxito/views/decorators.py b/readthedocs/proxito/views/decorators.py index 1e422523e9e..924a62e2e35 100644 --- a/readthedocs/proxito/views/decorators.py +++ b/readthedocs/proxito/views/decorators.py @@ -3,7 +3,10 @@ import structlog from readthedocs.projects.models import Project, ProjectRelationship -from readthedocs.proxito.exceptions import ProxitoHttp404 +from readthedocs.proxito.exceptions import ( + ProxitoProjectHttp404, + ProxitoSubProjectHttp404, +) log = structlog.get_logger(__name__) # noqa @@ -44,7 +47,7 @@ def inner_view( # noqa subproject_slug=subproject_slug, project_slug=kwargs['project'].slug, ) - raise ProxitoHttp404( + raise ProxitoSubProjectHttp404( "Invalid subproject slug", subproject_slug=subproject_slug ) return view_func(request, subproject=subproject, *args, **kwargs) @@ -80,7 +83,7 @@ def inner_view( # noqa "Project not found.", project_slug=project_slug, ) - raise ProxitoHttp404( + raise ProxitoProjectHttp404( "Project does not exist.", project_slug=project_slug ) return view_func(request, project=project, *args, **kwargs) diff --git a/readthedocs/proxito/views/serve.py b/readthedocs/proxito/views/serve.py index a98dc2beda6..596eec5ef16 100644 --- a/readthedocs/proxito/views/serve.py +++ b/readthedocs/proxito/views/serve.py @@ -22,7 +22,7 @@ from readthedocs.projects.constants import SPHINX_HTMLDIR from readthedocs.projects.models import Feature from readthedocs.projects.templatetags.projects_tags import sort_version_aware -from readthedocs.proxito.exceptions import ProxitoHttp404 +from readthedocs.proxito.exceptions import ProxitoHttp404, ProxitoProjectPageHttp404 from readthedocs.redirects.exceptions import InfiniteRedirectException from readthedocs.storage import build_media_storage, staticfiles_storage @@ -400,7 +400,7 @@ def get(self, request, proxito_path, template_name='404.html'): full_path=proxito_path, ) log.debug("Raising ProxitoHttp404") - raise ProxitoHttp404( + raise ProxitoProjectPageHttp404( "No custom 404 page found.", project=final_project, project_slug=kwargs.get("project_slug"), From a30fa3cc1538a3eca33fa91f3a344d41bca84622 Mon Sep 17 00:00:00 2001 From: Benjamin Balder Bach Date: Mon, 7 Nov 2022 13:43:45 +0100 Subject: [PATCH 10/70] Update readthedocs/proxito/exceptions.py Co-authored-by: Manuel Kaufmann --- readthedocs/proxito/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/proxito/exceptions.py b/readthedocs/proxito/exceptions.py index 7f65abb7f92..c66c342f4e5 100644 --- a/readthedocs/proxito/exceptions.py +++ b/readthedocs/proxito/exceptions.py @@ -1,5 +1,5 @@ """ -This module contains exceptions that may be raised in the proxito application +This module contains exceptions that may be raised in El Proxito application but are handled elsewhere in the Django project. -------------- From c50554400120e11c533e3b494dc1361f34225360 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Wed, 15 Feb 2023 19:15:19 -0500 Subject: [PATCH 11/70] Refactor unresolver --- readthedocs/core/unresolver.py | 170 +++++++++++++----- .../rtd_tests/tests/test_unresolver.py | 134 +++++++++----- 2 files changed, 212 insertions(+), 92 deletions(-) diff --git a/readthedocs/core/unresolver.py b/readthedocs/core/unresolver.py index d12c27a0818..f1361e32ef6 100644 --- a/readthedocs/core/unresolver.py +++ b/readthedocs/core/unresolver.py @@ -44,6 +44,33 @@ class InvalidCustomDomainError(DomainError): pass +class VersionNotFoundError(UnresolverError): + def __init__(self, project, version_slug, filename): + self.project = project + self.version_slug = version_slug + self.filename = filename + + +class TranslationNotFoundError(UnresolverError): + def __init__(self, project, language, filename): + self.project = project + self.language = language + self.filename = filename + + +class UnresolvedPathError(UnresolverError): + def __init__(self, project, path): + self.project = project + self.path = path + + +class InvalidExternalVersionError(UnresolverError): + def __init__(self, project, version, external_version_slug): + self.project = project + self.version = version + self.external_version_slug = external_version_slug + + @dataclass(slots=True) class UnresolvedURL: @@ -57,9 +84,9 @@ class UnresolvedURL: # It can be the same as parent_project. project: Project - version: Version = None - filename: str = None - parsed_url: ParseResult = None + version: Version + filename: str + parsed_url: ParseResult domain: Domain = None external: bool = False @@ -76,6 +103,8 @@ class DomainSourceType(Enum): @dataclass(slots=True) class UnresolvedDomain: + # The domain that was used to extract the information from. + source_domain: str source: DomainSourceType project: Project domain: Domain = None @@ -141,46 +170,88 @@ def unresolve(self, url, append_indexhtml=True): :param append_indexhtml: If `True` directories will be normalized to end with ``/index.html``. """ - parsed = urlparse(url) - domain = self.get_domain_from_host(parsed.netloc) + parsed_url = urlparse(url) + domain = self.get_domain_from_host(parsed_url.netloc) unresolved_domain = self.unresolve_domain(domain) + return self._unresolve( + unresolved_domain=unresolved_domain, + parsed_url=parsed_url, + append_indexhtml=append_indexhtml, + ) + + def unresolve_path(self, unresolved_domain, path, append_indexhtml=True): + """ + Unresolve a path given a unresolved domain. + + This is the same as the unresolve method, + but this method takes an unresolved domain + from unresolve_domain as input. + + :param unresolved_domain: An UnresolvedDomain object. + :param path: Path to unresolve (this shouldn't include the protocol or querystrings). + :param append_indexhtml: If `True` directories will be normalized + to end with ``/index.html``. + """ + # We don't call unparse() on the path, + # since it could be parsed as a full URL if it starts with a protocol. + parsed_url = ParseResult( + scheme="", netloc="", path=path, params="", query="", fragment="" + ) + return self._unresolve( + unresolved_domain=unresolved_domain, + parsed_url=parsed_url, + append_indexhtml=append_indexhtml, + ) + def _unresolve(self, unresolved_domain, parsed_url, append_indexhtml): + """ + The actual unresolver. + + Extracted into a separate method so it can be re-used by + the unresolve and unresolve_path methods. + """ current_project, version, filename = self._unresolve_path( parent_project=unresolved_domain.project, - path=parsed.path, + path=parsed_url.path, external_version_slug=unresolved_domain.external_version_slug, ) # Make sure we are serving the external version from the subdomain. - if unresolved_domain.is_from_external_domain and version: + if unresolved_domain.is_from_external_domain: + domain = unresolved_domain.source_domain if unresolved_domain.external_version_slug != version.slug: log.warning( "Invalid version for external domain.", domain=domain, version_slug=version.slug, ) - version = None - filename = None - elif not version.is_external: + raise InvalidExternalVersionError( + project=current_project, + version=version, + external_version_slug=unresolved_domain.external_version_slug, + ) + if not version.is_external: log.warning( "Attempt of serving a non-external version from RTD_EXTERNAL_VERSION_DOMAIN.", domain=domain, version_slug=version.slug, version_type=version.type, - url=url, ) - version = None - filename = None + raise InvalidExternalVersionError( + project=current_project, + version=version, + external_version_slug=unresolved_domain.external_version_slug, + ) - if append_indexhtml and filename and filename.endswith("/"): + if append_indexhtml and filename.endswith("/"): filename += "index.html" return UnresolvedURL( parent_project=unresolved_domain.project, - project=current_project or unresolved_domain.project, + project=current_project, version=version, filename=filename, - parsed_url=parsed, + parsed_url=parsed_url, domain=unresolved_domain.domain, external=unresolved_domain.is_from_external_domain, ) @@ -197,12 +268,10 @@ def _match_multiversion_project(self, parent_project, path): """ Try to match a multiversion project. - If the translation exists, we return a result even if the version doesn't, - so the translation is taken as the current project (useful for 404 pages). + An exception is raised if we weren't able to find a matching version or language, + this exception has the current project (useful for 404 pages). - :returns: None or a tuple with the current project, version and file. - A tuple with only the project means we weren't able to find a version, - but the translation was correct. + :returns: A tuple with the current project, version and file. """ match = self.multiversion_pattern.match(path) if not match: @@ -216,14 +285,18 @@ def _match_multiversion_project(self, parent_project, path): project = parent_project else: project = parent_project.translations.filter(language=language).first() + if not project: + raise TranslationNotFoundError( + project=parent_project, language=language, filename=file + ) - if project: - version = project.versions.filter(slug=version_slug).first() - if version: - return project, version, file - return project, None, None + version = project.versions.filter(slug=version_slug).first() + if not version: + raise VersionNotFoundError( + project=project, version_slug=version_slug, filename=file + ) - return None + return project, version, file def _match_subproject(self, parent_project, path, external_version_slug=None): """ @@ -232,12 +305,7 @@ def _match_subproject(self, parent_project, path, external_version_slug=None): If the subproject exists, we try to resolve the rest of the path with the subproject as the canonical project. - If the subproject exists, we return a result even if version doesn't, - so the subproject is taken as the current project (useful for 404 pages). - - :returns: None or a tuple with the current project, version and file. - A tuple with only the project means we were able to find the subproject, - but we weren't able to resolve the rest of the path. + :returns: A tuple with the current project, version and file. """ match = self.subproject_pattern.match(path) if not match: @@ -260,12 +328,7 @@ def _match_subproject(self, parent_project, path, external_version_slug=None): check_subprojects=False, external_version_slug=external_version_slug, ) - # If we got a valid response, return that, - # otherwise return the current subproject - # as the current project without a valid version or path. - if response: - return response - return subproject, None, None + return response return None def _match_single_version_project( @@ -276,21 +339,30 @@ def _match_single_version_project( By default any path will match. If `external_version_slug` is given, that version is used instead of the project's default version. + + An exception is raised if we weren't able to find a matching version, + this exception has the current project (useful for 404 pages). + + :returns: A tuple with the current project, version and file. """ file = self._normalize_filename(path) if external_version_slug: + version_slug = external_version_slug version = ( parent_project.versions(manager=EXTERNAL) - .filter(slug=external_version_slug) + .filter(slug=version_slug) .first() ) else: - version = parent_project.versions.filter( - slug=parent_project.default_version - ).first() - if version: - return parent_project, version, file - return parent_project, None, None + version_slug = parent_project.default_version + version = parent_project.versions.filter(slug=version_slug).first() + + if not version: + raise VersionNotFoundError( + project=parent_project, version_slug=version_slug, filename=file + ) + + return parent_project, version, file def _unresolve_path( self, parent_project, path, check_subprojects=True, external_version_slug=None @@ -352,7 +424,7 @@ def _unresolve_path( if response: return response - return parent_project, None, None + raise UnresolvedPathError(project=parent_project, path=path) @staticmethod def get_domain_from_host(host): @@ -384,6 +456,7 @@ def unresolve_domain(self, domain): project_slug = subdomain log.debug("Public domain.", domain=domain) return UnresolvedDomain( + source_domain=domain, source=DomainSourceType.public_domain, project=self._resolve_project_slug(project_slug, domain), ) @@ -400,6 +473,7 @@ def unresolve_domain(self, domain): project_slug, version_slug = subdomain.rsplit("--", maxsplit=1) log.debug("External versions domain.", domain=domain) return UnresolvedDomain( + source_domain=domain, source=DomainSourceType.external_domain, project=self._resolve_project_slug(project_slug, domain), external_version_slug=version_slug, @@ -425,6 +499,7 @@ def unresolve_domain(self, domain): log.debug("Custom domain.", domain=domain) return UnresolvedDomain( + source_domain=domain, source=DomainSourceType.custom_domain, project=domain_object.project, domain=domain_object, @@ -464,6 +539,7 @@ def unresolve_domain_from_request(self, request): project_slug=project.slug, ) return UnresolvedDomain( + source_domain=host, source=DomainSourceType.http_header, project=project, ) diff --git a/readthedocs/rtd_tests/tests/test_unresolver.py b/readthedocs/rtd_tests/tests/test_unresolver.py index c6bdc44e8b7..f95d72cb731 100644 --- a/readthedocs/rtd_tests/tests/test_unresolver.py +++ b/readthedocs/rtd_tests/tests/test_unresolver.py @@ -8,7 +8,11 @@ from readthedocs.core.unresolver import ( InvalidCustomDomainError, InvalidExternalDomainError, + InvalidExternalVersionError, SuspiciousHostnameError, + TranslationNotFoundError, + UnresolvedPathError, + VersionNotFoundError, unresolve, ) from readthedocs.projects.models import Domain, Project @@ -63,17 +67,44 @@ def test_file_with_trailing_slash(self): self.assertEqual(parts.version, self.version) self.assertEqual(parts.filename, "/foo/index.html") + def test_project_no_version_and_language(self): + with pytest.raises(UnresolvedPathError) as excinfo: + unresolve("https://pip.readthedocs.io/") + exc = excinfo.value + self.assertEqual(exc.project, self.pip) + self.assertEqual(exc.path, "/") + + with pytest.raises(UnresolvedPathError) as excinfo: + unresolve("https://pip.readthedocs.io/foo/bar") + exc = excinfo.value + self.assertEqual(exc.project, self.pip) + self.assertEqual(exc.path, "/foo/bar") + + def test_subproject_no_version_and_language(self): + with pytest.raises(UnresolvedPathError) as excinfo: + unresolve("https://pip.readthedocs.io/projects/sub/") + exc = excinfo.value + self.assertEqual(exc.project, self.subproject) + self.assertEqual(exc.path, "/") + + with pytest.raises(UnresolvedPathError) as excinfo: + unresolve("https://pip.readthedocs.io/projects/sub/foo/bar") + exc = excinfo.value + self.assertEqual(exc.project, self.subproject) + self.assertEqual(exc.path, "/foo/bar") + def test_path_no_version(self): urls = [ "https://pip.readthedocs.io/en", "https://pip.readthedocs.io/en/", ] for url in urls: - parts = unresolve(url) - self.assertEqual(parts.parent_project, self.pip) - self.assertEqual(parts.project, self.pip) - self.assertEqual(parts.version, None) - self.assertEqual(parts.filename, None) + with pytest.raises(VersionNotFoundError) as excinfo: + unresolve(url) + exc = excinfo.value + self.assertEqual(exc.project, self.pip) + self.assertEqual(exc.version_slug, None) + self.assertEqual(exc.filename, "/") def test_unresolver_subproject(self): parts = unresolve("https://pip.readthedocs.io/projects/sub/ja/latest/foo.html") @@ -116,18 +147,22 @@ def test_unresolve_subproject_alias(self): self.assertEqual(parts.filename, "/index.html") def test_unresolve_subproject_invalid_version(self): - parts = unresolve("https://pip.readthedocs.io/projects/sub/ja/nothing/foo.html") - self.assertEqual(parts.parent_project, self.pip) - self.assertEqual(parts.project, self.subproject) - self.assertEqual(parts.version, None) - self.assertEqual(parts.filename, None) + with pytest.raises(VersionNotFoundError) as excinfo: + unresolve("https://pip.readthedocs.io/projects/sub/ja/nothing/foo.html") + exc = excinfo.value + + self.assertEqual(exc.project, self.subproject) + self.assertEqual(exc.version_slug, "nothing") + self.assertEqual(exc.filename, "/foo.html") def test_unresolve_subproject_invalid_translation(self): - parts = unresolve("https://pip.readthedocs.io/projects/sub/es/latest/foo.html") - self.assertEqual(parts.parent_project, self.pip) - self.assertEqual(parts.project, self.subproject) - self.assertEqual(parts.version, None) - self.assertEqual(parts.filename, None) + with pytest.raises(TranslationNotFoundError) as excinfo: + unresolve("https://pip.readthedocs.io/projects/sub/es/latest/foo.html") + + exc = excinfo.value + self.assertEqual(exc.project, self.subproject) + self.assertEqual(exc.language, "es") + self.assertEqual(exc.filename, "/foo.html") def test_unresolver_translation(self): parts = unresolve("https://pip.readthedocs.io/ja/latest/foo.html") @@ -137,11 +172,12 @@ def test_unresolver_translation(self): self.assertEqual(parts.filename, "/foo.html") def test_unresolve_no_existing_translation(self): - parts = unresolve("https://pip.readthedocs.io/es/latest/") - self.assertEqual(parts.parent_project, self.pip) - self.assertEqual(parts.project, self.pip) - self.assertEqual(parts.version, None) - self.assertEqual(parts.filename, None) + with pytest.raises(TranslationNotFoundError) as excinfo: + unresolve("https://pip.readthedocs.io/es/latest/") + exc = excinfo.value + self.assertEqual(exc.project, self.pip) + self.assertEqual(exc.language, "es") + self.assertEqual(exc.filename, "/") def test_unresolver_custom_domain(self): self.domain = fixture.get( @@ -229,41 +265,49 @@ def test_external_version_single_version_project(self): def test_unexisting_external_version_single_version_project(self): self.pip.single_version = True self.pip.save() - parts = unresolve("https://pip--10.dev.readthedocs.build/") - self.assertEqual(parts.parent_project, self.pip) - self.assertEqual(parts.project, self.pip) - self.assertEqual(parts.version, None) - self.assertEqual(parts.filename, None) + with pytest.raises(VersionNotFoundError) as excinfo: + unresolve("https://pip--10.dev.readthedocs.build/") + + exc = excinfo.value + self.assertEqual(exc.project, self.pip) + self.assertEqual(exc.version_slug, "10") + self.assertEqual(exc.filename, "/") def test_non_external_version_single_version_project(self): self.pip.single_version = True self.pip.save() - parts = unresolve("https://pip--latest.dev.readthedocs.build/") - self.assertEqual(parts.parent_project, self.pip) - self.assertEqual(parts.project, self.pip) - self.assertEqual(parts.version, None) - self.assertEqual(parts.filename, None) + + with pytest.raises(VersionNotFoundError) as excinfo: + unresolve("https://pip--latest.dev.readthedocs.build/") + exc = excinfo.value + self.assertEqual(exc.project, self.pip) + self.assertEqual(exc.version_slug, "latest") + self.assertEqual(exc.filename, "/") def test_unresolve_external_version_no_existing_version(self): - parts = unresolve("https://pip--10.dev.readthedocs.build/en/10/") - self.assertEqual(parts.parent_project, self.pip) - self.assertEqual(parts.project, self.pip) - self.assertEqual(parts.version, None) - self.assertEqual(parts.filename, None) + with pytest.raises(VersionNotFoundError) as excinfo: + unresolve("https://pip--10.dev.readthedocs.build/en/10/") + + exc = excinfo.value + self.assertEqual(exc.project, self.pip) + self.assertEqual(exc.version_slug, "10") + self.assertEqual(exc.filename, "/") def test_external_version_not_matching_final_version(self): - parts = unresolve("https://pip--10.dev.readthedocs.build/en/latest/") - self.assertEqual(parts.parent_project, self.pip) - self.assertEqual(parts.project, self.pip) - self.assertEqual(parts.version, None) - self.assertEqual(parts.filename, None) + with pytest.raises(InvalidExternalVersionError) as excinfo: + unresolve("https://pip--10.dev.readthedocs.build/en/latest/") + exc = excinfo.value + self.assertEqual(exc.project, self.pip) + self.assertEqual(exc.version, self.version) + self.assertEqual(exc.external_version_slug, "10") def test_normal_version_in_external_version_subdomain(self): - parts = unresolve("https://pip--latest.dev.readthedocs.build/en/latest/") - self.assertEqual(parts.parent_project, self.pip) - self.assertEqual(parts.project, self.pip) - self.assertEqual(parts.version, None) - self.assertEqual(parts.filename, None) + with pytest.raises(InvalidExternalVersionError) as excinfo: + unresolve("https://pip--latest.dev.readthedocs.build/en/latest/") + exc = excinfo.value + self.assertEqual(exc.project, self.pip) + self.assertEqual(exc.external_version_slug, "latest") + self.assertEqual(exc.version, self.version) def test_malformed_external_version(self): with pytest.raises(InvalidExternalDomainError): From 14bdbd25144f1349c3b7f46c315368c40b3d7a77 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Tue, 21 Feb 2023 14:45:45 -0500 Subject: [PATCH 12/70] Move external version checks inside unresolver --- readthedocs/core/unresolver.py | 59 +++++++------------ .../rtd_tests/tests/test_unresolver.py | 8 +-- 2 files changed, 25 insertions(+), 42 deletions(-) diff --git a/readthedocs/core/unresolver.py b/readthedocs/core/unresolver.py index f1361e32ef6..7e909d8ead3 100644 --- a/readthedocs/core/unresolver.py +++ b/readthedocs/core/unresolver.py @@ -6,7 +6,7 @@ import structlog from django.conf import settings -from readthedocs.builds.constants import EXTERNAL +from readthedocs.builds.constants import EXTERNAL, INTERNAL from readthedocs.builds.models import Version from readthedocs.constants import pattern_opts from readthedocs.projects.models import Domain, Feature, Project @@ -65,9 +65,9 @@ def __init__(self, project, path): class InvalidExternalVersionError(UnresolverError): - def __init__(self, project, version, external_version_slug): + def __init__(self, project, version_slug, external_version_slug): self.project = project - self.version = version + self.version_slug = version_slug self.external_version_slug = external_version_slug @@ -216,33 +216,6 @@ def _unresolve(self, unresolved_domain, parsed_url, append_indexhtml): external_version_slug=unresolved_domain.external_version_slug, ) - # Make sure we are serving the external version from the subdomain. - if unresolved_domain.is_from_external_domain: - domain = unresolved_domain.source_domain - if unresolved_domain.external_version_slug != version.slug: - log.warning( - "Invalid version for external domain.", - domain=domain, - version_slug=version.slug, - ) - raise InvalidExternalVersionError( - project=current_project, - version=version, - external_version_slug=unresolved_domain.external_version_slug, - ) - if not version.is_external: - log.warning( - "Attempt of serving a non-external version from RTD_EXTERNAL_VERSION_DOMAIN.", - domain=domain, - version_slug=version.slug, - version_type=version.type, - ) - raise InvalidExternalVersionError( - project=current_project, - version=version, - external_version_slug=unresolved_domain.external_version_slug, - ) - if append_indexhtml and filename.endswith("/"): filename += "index.html" @@ -264,7 +237,9 @@ def _normalize_filename(filename): filename = "/" + filename return filename - def _match_multiversion_project(self, parent_project, path): + def _match_multiversion_project( + self, parent_project, path, external_version_slug=None + ): """ Try to match a multiversion project. @@ -290,7 +265,15 @@ def _match_multiversion_project(self, parent_project, path): project=parent_project, language=language, filename=file ) - version = project.versions.filter(slug=version_slug).first() + if external_version_slug and external_version_slug != version_slug: + raise InvalidExternalVersionError( + project=project, + version_slug=version_slug, + external_version_slug=external_version_slug, + ) + + manager = EXTERNAL if external_version_slug else INTERNAL + version = project.versions(manager=manager).filter(slug=version_slug).first() if not version: raise VersionNotFoundError( project=project, version_slug=version_slug, filename=file @@ -348,15 +331,14 @@ def _match_single_version_project( file = self._normalize_filename(path) if external_version_slug: version_slug = external_version_slug - version = ( - parent_project.versions(manager=EXTERNAL) - .filter(slug=version_slug) - .first() - ) + manager = EXTERNAL else: version_slug = parent_project.default_version - version = parent_project.versions.filter(slug=version_slug).first() + manager = INTERNAL + version = ( + parent_project.versions(manager=manager).filter(slug=version_slug).first() + ) if not version: raise VersionNotFoundError( project=parent_project, version_slug=version_slug, filename=file @@ -400,6 +382,7 @@ def _unresolve_path( response = self._match_multiversion_project( parent_project=parent_project, path=path, + external_version_slug=external_version_slug, ) if response: return response diff --git a/readthedocs/rtd_tests/tests/test_unresolver.py b/readthedocs/rtd_tests/tests/test_unresolver.py index f95d72cb731..482806fdf01 100644 --- a/readthedocs/rtd_tests/tests/test_unresolver.py +++ b/readthedocs/rtd_tests/tests/test_unresolver.py @@ -298,16 +298,16 @@ def test_external_version_not_matching_final_version(self): unresolve("https://pip--10.dev.readthedocs.build/en/latest/") exc = excinfo.value self.assertEqual(exc.project, self.pip) - self.assertEqual(exc.version, self.version) + self.assertEqual(exc.version_slug, "latest") self.assertEqual(exc.external_version_slug, "10") def test_normal_version_in_external_version_subdomain(self): - with pytest.raises(InvalidExternalVersionError) as excinfo: + with pytest.raises(VersionNotFoundError) as excinfo: unresolve("https://pip--latest.dev.readthedocs.build/en/latest/") exc = excinfo.value self.assertEqual(exc.project, self.pip) - self.assertEqual(exc.external_version_slug, "latest") - self.assertEqual(exc.version, self.version) + self.assertEqual(exc.version_slug, "latest") + self.assertEqual(exc.filename, "/") def test_malformed_external_version(self): with pytest.raises(InvalidExternalDomainError): From dd4363153c8f2b67a3f0032478a368223eeccc27 Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Wed, 22 Feb 2023 14:13:17 +0100 Subject: [PATCH 13/70] !fixup 8cbff084845a9a7ad0fd660f96be8bab245ec171 --- readthedocs/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index 97202bb6d2f..99827f9f687 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -75,7 +75,7 @@ def SHOW_DEBUG_TOOLBAR(self): PRODUCTION_DOMAIN = 'readthedocs.org' PUBLIC_DOMAIN = None PUBLIC_DOMAIN_USES_HTTPS = False - USE_SUBDOMAIN = True + USE_SUBDOMAIN = False PUBLIC_API_URL = 'https://{}'.format(PRODUCTION_DOMAIN) RTD_INTERSPHINX_URL = 'https://{}'.format(PRODUCTION_DOMAIN) RTD_EXTERNAL_VERSION_DOMAIN = 'external-builds.readthedocs.io' From 0c07025aaeb6fd4b6ef590272c9693b60c9c6f0f Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Wed, 22 Feb 2023 14:51:43 +0100 Subject: [PATCH 14/70] Update link to Custom 404 docs, remove rediraffe from tips --- readthedocs/templates/errors/404/include_tips.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/readthedocs/templates/errors/404/include_tips.html b/readthedocs/templates/errors/404/include_tips.html index ee42ee8b74f..e6d1b3809f9 100644 --- a/readthedocs/templates/errors/404/include_tips.html +++ b/readthedocs/templates/errors/404/include_tips.html @@ -5,7 +5,6 @@

{% trans "Are you the project owner?" %}

{% trans "Here are some tips to address 404 errors:" %}

From 9367e9edc2dcc04f81d9c89c893e046b8a48d56b Mon Sep 17 00:00:00 2001 From: Benjamin Balder Bach Date: Wed, 22 Feb 2023 14:57:23 +0100 Subject: [PATCH 15/70] Apply suggestions from code review Co-authored-by: Manuel Kaufmann --- readthedocs/templates/errors/404/community.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/templates/errors/404/community.html b/readthedocs/templates/errors/404/community.html index 81774812422..22c08f3ddf0 100644 --- a/readthedocs/templates/errors/404/community.html +++ b/readthedocs/templates/errors/404/community.html @@ -1,7 +1,7 @@ {% extends "errors/404/base.html" %} {% load i18n %} -{# something on readthedocs.org does not exist - this 404 is not intended for projects #} +{# This page on the application site does not exist. This 404 is not intended for user hosted projects. #} {% block 404_error_message %} From d38491e4e0bfc581c35b4b7364f31213f3757cc7 Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Wed, 22 Feb 2023 15:05:13 +0100 Subject: [PATCH 16/70] Raise custom 404 if subproject isn't found --- readthedocs/proxito/views/serve.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/readthedocs/proxito/views/serve.py b/readthedocs/proxito/views/serve.py index cc546350d0f..29280ebbcc0 100644 --- a/readthedocs/proxito/views/serve.py +++ b/readthedocs/proxito/views/serve.py @@ -5,7 +5,7 @@ import structlog from django.conf import settings from django.http import Http404, HttpResponse, HttpResponseRedirect -from django.shortcuts import get_object_or_404, render +from django.shortcuts import render from django.urls import resolve as url_resolve from django.utils.decorators import method_decorator from django.views import View @@ -19,10 +19,14 @@ from readthedocs.core.resolver import resolve_path from readthedocs.core.utils.extend import SettingsOverrideObject from readthedocs.projects import constants -from readthedocs.projects.models import Domain, Feature +from readthedocs.projects.models import Domain, Feature, ProjectRelationship from readthedocs.projects.templatetags.projects_tags import sort_version_aware from readthedocs.proxito.constants import RedirectType -from readthedocs.proxito.exceptions import ProxitoHttp404, ProxitoProjectPageHttp404 +from readthedocs.proxito.exceptions import ( + ProxitoHttp404, + ProxitoProjectPageHttp404, + ProxitoSubProjectHttp404, +) from readthedocs.redirects.exceptions import InfiniteRedirectException from readthedocs.storage import build_media_storage @@ -44,11 +48,13 @@ def get(self, request, subproject_slug=None, filename=""): project = unresolved_domain.project # Use the project from the domain, or use the subproject slug. - # TODO: Do not raise a normal Http404 here, use custom... if subproject_slug: - project = get_object_or_404( - project.subprojects, alias=subproject_slug - ).child + try: + project = project.subprojects.get(alias=subproject_slug).child + except ProjectRelationship.DoesNotExist: + raise ProxitoSubProjectHttp404( + f"Did not find subproject slug {subproject_slug} for project {project.slug}" + ) # Get the default version from the current project, # or the version from the external domain. From 7f6f1b70f03b2b175643c613f29b8abf555219ef Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Wed, 22 Feb 2023 16:55:24 +0100 Subject: [PATCH 17/70] WIP: Conclude structure of exception classes and templates, needs more content work --- media/css/core.css | 2 + readthedocs/core/views/__init__.py | 68 ++++++++++++------- readthedocs/proxito/exceptions.py | 40 +++++++---- readthedocs/proxito/views/decorators.py | 12 ++-- readthedocs/proxito/views/serve.py | 6 +- readthedocs/proxito/views/utils.py | 32 +++++++-- readthedocs/templates/errors/404/base.html | 21 +++--- .../templates/errors/404/community.html | 12 ---- .../templates/errors/404/include_tips.html | 6 +- .../templates/errors/404/no_project.html | 4 +- .../templates/errors/404/no_project_page.html | 5 +- .../templates/errors/404/no_subproject.html | 7 +- .../templates/errors/404/no_version.html | 23 +++++++ 13 files changed, 155 insertions(+), 83 deletions(-) delete mode 100644 readthedocs/templates/errors/404/community.html create mode 100644 readthedocs/templates/errors/404/no_version.html diff --git a/media/css/core.css b/media/css/core.css index 7d40d19b3dc..a41dd9c67c3 100644 --- a/media/css/core.css +++ b/media/css/core.css @@ -1331,6 +1331,8 @@ div.module.project-subprojects li.subproject a.subproject-edit:before { content: "\f044"; } +#content ul.normal_list {list-style: disc; margin-left: 20px;} + /* Pygments */ div.highlight pre .hll { background-color: #ffffcc } div.highlight pre .c { color: #60a0b0; font-style: italic } /* Comment */ diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py index 3eb85bdd1b4..1f70cd70696 100644 --- a/readthedocs/core/views/__init__.py +++ b/readthedocs/core/views/__init__.py @@ -13,6 +13,12 @@ from readthedocs.core.mixins import PrivateViewMixin from readthedocs.projects.models import Project +from readthedocs.proxito.exceptions import ( + ProxitoProjectHttp404, + ProxitoProjectPageHttp404, + ProxitoProjectVersionHttp404, + ProxitoSubProjectHttp404, +) log = structlog.get_logger(__name__) @@ -48,32 +54,42 @@ def get_context_data(self, **kwargs): return context -def server_error_404( - request, template_name="errors/404/community.html", exception=None -): - """A simple 404 handler.""" - # This property is set by ProxitoHttp404. We could also have a look at the - # subproject_slug - - project_slug = getattr(exception, "project_slug", None) - log.debug(exception) - project = getattr(exception, "project", None) - subproject_slug = getattr(exception, "subproject_slug", None) - - if subproject_slug: - template_name = "errors/404/no_subproject.html" - elif project: - template_name = "errors/404/no_project_page.html" - elif project_slug: - template_name = "errors/404/no_project.html" - - r = render( - request, - template_name, - context={"project": project, "project_slug": project_slug}, - ) - r.status_code = 404 - return r +def server_error_404(request, template_name="errors/404/base.html", exception=None): + """Generic 404 handler, handling 404 exception types raised throughout the app""" + + try: + log.debug("404 view handler active") + log.debug(exception) + # Properties are set by ProxitoHttp404. We could also have a look at the + # subproject_slug + project_slug = getattr(exception, "project_slug", None) + version_slug = getattr(exception, "version_slug", None) + project = getattr(exception, "project", None) + subproject_slug = getattr(exception, "subproject_slug", None) + + if isinstance(exception, ProxitoProjectVersionHttp404): + template_name = "errors/404/no_version.html" + elif isinstance(exception, ProxitoSubProjectHttp404): + template_name = "errors/404/no_subproject.html" + elif isinstance(exception, ProxitoProjectPageHttp404): + template_name = "errors/404/no_project_page.html" + elif isinstance(exception, ProxitoProjectHttp404): + template_name = "errors/404/no_project.html" + + r = render( + request, + template_name, + context={ + "project": project, + "project_slug": project_slug, + "subproject_slug": subproject_slug, + "version_slug": version_slug, + }, + ) + r.status_code = 404 + return r + except Exception as e: + log.debug(e) def server_error_500(request, template_name='500.html'): diff --git a/readthedocs/proxito/exceptions.py b/readthedocs/proxito/exceptions.py index c66c342f4e5..d095ec6208d 100644 --- a/readthedocs/proxito/exceptions.py +++ b/readthedocs/proxito/exceptions.py @@ -14,16 +14,13 @@ class ProxitoHttp404(Http404): """ - General error class for proxity 404s + General error class for proxity 404s. """ - def __init__(self, message): - super().__init__(message) - class ProxitoProjectHttp404(ProxitoHttp404): """ - Raised if a project was not found + Raised if a project was not found. """ def __init__(self, message, project_slug=None): @@ -33,24 +30,41 @@ def __init__(self, message, project_slug=None): class ProxitoSubProjectHttp404(ProxitoProjectHttp404): """ - Raised if a sub-project was not found + Raised if a sub-project was not found. """ def __init__(self, message, project_slug=None, project=None, subproject_slug=None): - self.project_slug = project_slug + super().__init__(message, project_slug=project_slug) self.project = project self.subproject_slug = subproject_slug - super().__init__(message) class ProxitoProjectPageHttp404(ProxitoHttp404): """ - Raised if a page inside an existing project was not found - - Note: The containing project can be both a project or a subproject inside another project + Raised if a page inside an existing project was not found. """ def __init__(self, message, project_slug=None, project=None, subproject_slug=None): - self.project_slug = project_slug + super().__init__(message, project_slug=project_slug) self.project = project self.subproject_slug = subproject_slug - super().__init__(message) + + +class ProxitoProjectVersionHttp404(ProxitoHttp404): + """ + Raised if a version was not found. + + Note: The containing project can be both a project or a subproject inside another project. + """ + + def __init__( + self, + message, + project_slug=None, + project=None, + subproject_slug=None, + version_slug=None, + ): + super().__init__(message, project_slug=project_slug) + self.project = project + self.subproject_slug = subproject_slug + self.version_slug = version_slug diff --git a/readthedocs/proxito/views/decorators.py b/readthedocs/proxito/views/decorators.py index 59ca4030b55..73741882e94 100644 --- a/readthedocs/proxito/views/decorators.py +++ b/readthedocs/proxito/views/decorators.py @@ -23,14 +23,14 @@ def map_subproject_slug(view_func): @wraps(view_func) def inner_view( # noqa - request, subproject=None, subproject_slug=None, *args, **kwargs + request, project=None, subproject=None, subproject_slug=None, *args, **kwargs ): if subproject is None and subproject_slug: # Try to fetch by subproject alias first, otherwise we might end up # redirected to an unrelated project. # Depends on a project passed into kwargs rel = ( - ProjectRelationship.objects.filter(parent=kwargs["project"]) + ProjectRelationship.objects.filter(parent=project) .filter(Q(alias=subproject_slug) | Q(child__slug=subproject_slug)) .first() ) @@ -40,10 +40,14 @@ def inner_view( # noqa log.warning( "The slug is not subproject of project.", subproject_slug=subproject_slug, - project_slug=kwargs["project"].slug, + project_slug=project.slug, + project=project, ) raise ProxitoSubProjectHttp404( - "Invalid subproject slug", subproject_slug=subproject_slug + "Invalid subproject slug", + project_slug=project.slug, + project=project, + subproject_slug=subproject_slug, ) return view_func(request, subproject=subproject, *args, **kwargs) diff --git a/readthedocs/proxito/views/serve.py b/readthedocs/proxito/views/serve.py index 29280ebbcc0..da4ce1f4124 100644 --- a/readthedocs/proxito/views/serve.py +++ b/readthedocs/proxito/views/serve.py @@ -46,6 +46,7 @@ def get(self, request, subproject_slug=None, filename=""): unresolved_domain = request.unresolved_domain project = unresolved_domain.project + parent_project = project # Use the project from the domain, or use the subproject slug. if subproject_slug: @@ -53,7 +54,10 @@ def get(self, request, subproject_slug=None, filename=""): project = project.subprojects.get(alias=subproject_slug).child except ProjectRelationship.DoesNotExist: raise ProxitoSubProjectHttp404( - f"Did not find subproject slug {subproject_slug} for project {project.slug}" + f"Did not find subproject slug {subproject_slug} for project {project.slug}", + project=parent_project, + project_slug=parent_project.slug, + subproject_slug=subproject_slug, ) # Get the default version from the current project, diff --git a/readthedocs/proxito/views/utils.py b/readthedocs/proxito/views/utils.py index 84b14435686..39abed59954 100644 --- a/readthedocs/proxito/views/utils.py +++ b/readthedocs/proxito/views/utils.py @@ -5,7 +5,13 @@ from django.shortcuts import render from readthedocs.projects.models import Project -from readthedocs.proxito.exceptions import ProxitoHttp404 +from readthedocs.proxito.exceptions import ( + ProxitoHttp404, + ProxitoProjectHttp404, + ProxitoProjectPageHttp404, + ProxitoProjectVersionHttp404, + ProxitoSubProjectHttp404, +) from .decorators import map_project_slug, map_subproject_slug @@ -22,7 +28,9 @@ def fast_404(request, *args, **kwargs): return HttpResponse("Not Found.", status=404) -def proxito_404_page_handler(request, exception=None, template_name='404.html'): +def proxito_404_page_handler( + request, exception=None, template_name="errors/404/base.html" +): """ Decide what 404 page return depending if it's an internal NGINX redirect. @@ -36,9 +44,20 @@ def proxito_404_page_handler(request, exception=None, template_name='404.html'): log.debug("Displaying a 'fast 404'") return fast_404(request, exception, template_name) + if isinstance(exception, ProxitoProjectVersionHttp404): + template_name = "errors/404/no_version.html" + elif isinstance(exception, ProxitoSubProjectHttp404): + template_name = "errors/404/no_subproject.html" + elif isinstance(exception, ProxitoProjectPageHttp404): + template_name = "errors/404/no_project_page.html" + elif isinstance(exception, ProxitoProjectHttp404): + template_name = "errors/404/no_project.html" + project_slug = getattr(exception, "project_slug", None) - log.debug(exception) + version_slug = getattr(exception, "version_slug", None) project = getattr(exception, "project", None) + subproject_slug = getattr(exception, "subproject_slug", None) + log.debug( "404 page detected a project slug in request.", project_slug=project_slug, @@ -46,7 +65,12 @@ def proxito_404_page_handler(request, exception=None, template_name='404.html'): r = render( request, template_name, - context={"project": project, "project_slug": project_slug}, + context={ + "project": project, + "project_slug": project_slug, + "subproject_slug": subproject_slug, + "version_slug": version_slug, + }, ) r.status_code = 404 return r diff --git a/readthedocs/templates/errors/404/base.html b/readthedocs/templates/errors/404/base.html index b82c0e9ff37..a82af8670ae 100644 --- a/readthedocs/templates/errors/404/base.html +++ b/readthedocs/templates/errors/404/base.html @@ -3,31 +3,23 @@ {% load i18n %} {% block title %} - {% trans "404 - Maze Found" %} + {% trans "404 Not Found" %} {% endblock %} {% block content %} -

{% trans "404 - page not found" %}

+

{% trans "404 Not Found" %}

{% block 404_error_message %} -{% endblock %} - -{% if project %} - -{% elif "_proxito_404_" in request.path %} - -{% else %} - -{% endif %} +

{% trans "You have encountered a 404 on Read the Docs. You are either looking for a page that does not exist or a project that has been removed." %}

 
-        \          SORRY            /
+        \       What a maze!         /
          \                         /
           \    This page does     /
-           ]   not exist yet.    [    ,'|
+           ]     not exist.      [    ,'|
            ]                     [   /  |
            ]___               ___[ ,'   |
            ]  ]\             /[  [ |:   |
@@ -47,4 +39,7 @@ 

{% trans "404 - page not found" %}

/ q, \ / \
+ +{% endblock %} + {% endblock %} diff --git a/readthedocs/templates/errors/404/community.html b/readthedocs/templates/errors/404/community.html deleted file mode 100644 index 22c08f3ddf0..00000000000 --- a/readthedocs/templates/errors/404/community.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "errors/404/base.html" %} -{% load i18n %} - -{# This page on the application site does not exist. This 404 is not intended for user hosted projects. #} - -{% block 404_error_message %} - -

{% trans "You have encountered a 404 on Read the Docs. You are either looking for a page that does not exist or a project that has been removed." %}

- -

{% trans "If you believe this is a technical error, please contact our support:" %} {% trans "Go to support" %}»

- -{% endblock %} diff --git a/readthedocs/templates/errors/404/include_tips.html b/readthedocs/templates/errors/404/include_tips.html index e6d1b3809f9..78017e3a3c4 100644 --- a/readthedocs/templates/errors/404/include_tips.html +++ b/readthedocs/templates/errors/404/include_tips.html @@ -4,7 +4,7 @@

{% trans "Are you the project owner?" %}

{% trans "Here are some tips to address 404 errors:" %}

-
    -
  • {% trans "Use custom 404 pages:" %} {% trans "Read more" %}»
  • -
  • {% trans "Create redirects when you move contents:" %} {% trans "Read more" %}»
  • + diff --git a/readthedocs/templates/errors/404/no_project.html b/readthedocs/templates/errors/404/no_project.html index f3ff62966ed..31f627142cd 100644 --- a/readthedocs/templates/errors/404/no_project.html +++ b/readthedocs/templates/errors/404/no_project.html @@ -8,8 +8,8 @@ {# we are looking at a 404 generated by proxito, we then know that the project itself doesn't exist or hasn't been built #}

    - {% blocktrans trimmed with project_slug=project_slug %} - The project "{{ project_slug }}" that you are looking for either does not exist, has been deleted or does not yet have any valid builds. + {% blocktrans trimmed %} + The project {{ project_slug }} that you are looking for either does not exist, has been deleted or does not yet have any valid builds. {% endblocktrans %}

    diff --git a/readthedocs/templates/errors/404/no_project_page.html b/readthedocs/templates/errors/404/no_project_page.html index bce6d1042bd..77da321ceb2 100644 --- a/readthedocs/templates/errors/404/no_project_page.html +++ b/readthedocs/templates/errors/404/no_project_page.html @@ -6,8 +6,9 @@ {% block 404_error_message %}

    - {% blocktrans trimmed with project_slug=project.slug request_path=request.path %} - You are browsing the documentation of "{{ project_slug }}". The page you are looking for at {{ request.path }} was not found. + {% blocktrans trimmed with request_path=request.path %} + You are browsing the documentation of {{ project_slug }}. + The page you are looking for at {{ request_path }} was not found. {% endblocktrans %}

    diff --git a/readthedocs/templates/errors/404/no_subproject.html b/readthedocs/templates/errors/404/no_subproject.html index bce6d1042bd..1bd2dae4f9b 100644 --- a/readthedocs/templates/errors/404/no_subproject.html +++ b/readthedocs/templates/errors/404/no_subproject.html @@ -1,13 +1,14 @@ {% extends "errors/404/base.html" %} {% load i18n %} -{# project exists but page doesn't #} +{# project exists but subproject doesn't #} {% block 404_error_message %}

    - {% blocktrans trimmed with project_slug=project.slug request_path=request.path %} - You are browsing the documentation of "{{ project_slug }}". The page you are looking for at {{ request.path }} was not found. + {% blocktrans trimmed with request_path=request.path %} + You are browsing the documentation of project {{ project_slug }}. + The subproject {{ subproject_slug }} you are looking for at {{ request_path }} was not found. {% endblocktrans %}

    diff --git a/readthedocs/templates/errors/404/no_version.html b/readthedocs/templates/errors/404/no_version.html new file mode 100644 index 00000000000..7e5696ff8dd --- /dev/null +++ b/readthedocs/templates/errors/404/no_version.html @@ -0,0 +1,23 @@ +{% extends "errors/404/base.html" %} +{% load i18n %} + +{# project exists but version doesn't #} + +{% block 404_error_message %} + +

    + {% blocktrans trimmed with request_path=request.path %} + You are browsing the documentation of project {{ project_slug }}. + The version you are looking for at {{ request_path }} was not found. + {% endblocktrans %} +

    + +

    + {% blocktrans trimmed %} + Documentation changes over time. Pages are moved around. You can try to navigate to the index page of the project and use its navigation or search function to find a similar page. + {% endblocktrans %} +

    + + {% include "errors/404/include_tips.html" %} + +{% endblock %} From 36b9f02820ba79380b516acb7a28a3d87723670b Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Wed, 22 Feb 2023 17:11:42 -0500 Subject: [PATCH 18/70] Updates from review --- readthedocs/core/unresolver.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readthedocs/core/unresolver.py b/readthedocs/core/unresolver.py index 7e909d8ead3..52bea1a902e 100644 --- a/readthedocs/core/unresolver.py +++ b/readthedocs/core/unresolver.py @@ -107,6 +107,7 @@ class UnresolvedDomain: source_domain: str source: DomainSourceType project: Project + # Domain object for custom domains. domain: Domain = None external_version_slug: str = None @@ -247,6 +248,7 @@ def _match_multiversion_project( this exception has the current project (useful for 404 pages). :returns: A tuple with the current project, version and file. + Returns `None` if there isn't a total or partial match. """ match = self.multiversion_pattern.match(path) if not match: @@ -289,6 +291,7 @@ def _match_subproject(self, parent_project, path, external_version_slug=None): with the subproject as the canonical project. :returns: A tuple with the current project, version and file. + Returns `None` if there isn't a total or partial match. """ match = self.subproject_pattern.match(path) if not match: @@ -327,6 +330,7 @@ def _match_single_version_project( this exception has the current project (useful for 404 pages). :returns: A tuple with the current project, version and file. + Returns `None` if there isn't a total or partial match. """ file = self._normalize_filename(path) if external_version_slug: From a4dee70d0b1d83df41190a08d46b7242b8708818 Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Thu, 23 Feb 2023 12:52:22 +0100 Subject: [PATCH 19/70] Reuse 404 view between proxito and general app, remove debug logging --- readthedocs/core/views/__init__.py | 69 +++++++++---------- readthedocs/proxito/views/utils.py | 40 ++--------- readthedocs/templates/errors/404/base.html | 6 +- .../templates/errors/404/no_project.html | 2 +- 4 files changed, 42 insertions(+), 75 deletions(-) diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py index 1f70cd70696..87429afb4ac 100644 --- a/readthedocs/core/views/__init__.py +++ b/readthedocs/core/views/__init__.py @@ -55,41 +55,40 @@ def get_context_data(self, **kwargs): def server_error_404(request, template_name="errors/404/base.html", exception=None): - """Generic 404 handler, handling 404 exception types raised throughout the app""" - - try: - log.debug("404 view handler active") - log.debug(exception) - # Properties are set by ProxitoHttp404. We could also have a look at the - # subproject_slug - project_slug = getattr(exception, "project_slug", None) - version_slug = getattr(exception, "version_slug", None) - project = getattr(exception, "project", None) - subproject_slug = getattr(exception, "subproject_slug", None) - - if isinstance(exception, ProxitoProjectVersionHttp404): - template_name = "errors/404/no_version.html" - elif isinstance(exception, ProxitoSubProjectHttp404): - template_name = "errors/404/no_subproject.html" - elif isinstance(exception, ProxitoProjectPageHttp404): - template_name = "errors/404/no_project_page.html" - elif isinstance(exception, ProxitoProjectHttp404): - template_name = "errors/404/no_project.html" - - r = render( - request, - template_name, - context={ - "project": project, - "project_slug": project_slug, - "subproject_slug": subproject_slug, - "version_slug": version_slug, - }, - ) - r.status_code = 404 - return r - except Exception as e: - log.debug(e) + """ + Serves a 404 error message, handling 404 exception types raised throughout the app. + Notice that handling of 404 errors happens elsewhere in views and middleware, + this view is expected to serve an actual 404 message. + """ + + # Properties are set by ProxitoHttp404. We could also have a look at the + # subproject_slug + project_slug = getattr(exception, "project_slug", None) + version_slug = getattr(exception, "version_slug", None) + project = getattr(exception, "project", None) + subproject_slug = getattr(exception, "subproject_slug", None) + + if isinstance(exception, ProxitoProjectVersionHttp404): + template_name = "errors/404/no_version.html" + elif isinstance(exception, ProxitoSubProjectHttp404): + template_name = "errors/404/no_subproject.html" + elif isinstance(exception, ProxitoProjectPageHttp404): + template_name = "errors/404/no_project_page.html" + elif isinstance(exception, ProxitoProjectHttp404): + template_name = "errors/404/no_project.html" + + r = render( + request, + template_name, + context={ + "project": project, + "project_slug": project_slug, + "subproject_slug": subproject_slug, + "version_slug": version_slug, + }, + ) + r.status_code = 404 + return r def server_error_500(request, template_name='500.html'): diff --git a/readthedocs/proxito/views/utils.py b/readthedocs/proxito/views/utils.py index 39abed59954..7a31199a621 100644 --- a/readthedocs/proxito/views/utils.py +++ b/readthedocs/proxito/views/utils.py @@ -2,7 +2,6 @@ import structlog from django.http import HttpResponse -from django.shortcuts import render from readthedocs.projects.models import Project from readthedocs.proxito.exceptions import ( @@ -13,6 +12,7 @@ ProxitoSubProjectHttp404, ) +from ...core.views import server_error_404 from .decorators import map_project_slug, map_subproject_slug log = structlog.get_logger(__name__) # noqa @@ -41,39 +41,12 @@ def proxito_404_page_handler( """ if request.resolver_match and request.resolver_match.url_name != 'proxito_404_handler': - log.debug("Displaying a 'fast 404'") return fast_404(request, exception, template_name) - if isinstance(exception, ProxitoProjectVersionHttp404): - template_name = "errors/404/no_version.html" - elif isinstance(exception, ProxitoSubProjectHttp404): - template_name = "errors/404/no_subproject.html" - elif isinstance(exception, ProxitoProjectPageHttp404): - template_name = "errors/404/no_project_page.html" - elif isinstance(exception, ProxitoProjectHttp404): - template_name = "errors/404/no_project.html" - - project_slug = getattr(exception, "project_slug", None) - version_slug = getattr(exception, "version_slug", None) - project = getattr(exception, "project", None) - subproject_slug = getattr(exception, "subproject_slug", None) - - log.debug( - "404 page detected a project slug in request.", - project_slug=project_slug, + # Serve the general 404 error message + return server_error_404( + request, exception=None, template_name="errors/404/base.html" ) - r = render( - request, - template_name, - context={ - "project": project, - "project_slug": project_slug, - "subproject_slug": subproject_slug, - "version_slug": version_slug, - }, - ) - r.status_code = 404 - return r @map_project_slug @@ -112,11 +85,6 @@ def _get_project_data_from_request( try: final_project = current_project.translations.get(language=lang_slug) except Project.DoesNotExist: - log.debug( - "Raising 404 for language slug", - lang_slug=lang_slug, - project_slug=current_project.slug, - ) raise ProxitoHttp404( "Did not find translation", project=current_project, diff --git a/readthedocs/templates/errors/404/base.html b/readthedocs/templates/errors/404/base.html index a82af8670ae..71dbb95b19b 100644 --- a/readthedocs/templates/errors/404/base.html +++ b/readthedocs/templates/errors/404/base.html @@ -10,13 +10,13 @@

    {% trans "404 Not Found" %}

    -{% block 404_error_message %} + {% block 404_error_message %}

    {% trans "You have encountered a 404 on Read the Docs. You are either looking for a page that does not exist or a project that has been removed." %}

     
    -        \       What a maze!         /
    +        \       What a maze!        /
              \                         /
               \    This page does     /
                ]     not exist.      [    ,'|
    @@ -40,6 +40,6 @@ 

    {% trans "404 Not Found" %}

    / \
    -{% endblock %} + {% endblock %} {% endblock %} diff --git a/readthedocs/templates/errors/404/no_project.html b/readthedocs/templates/errors/404/no_project.html index 31f627142cd..6cadf24f80d 100644 --- a/readthedocs/templates/errors/404/no_project.html +++ b/readthedocs/templates/errors/404/no_project.html @@ -1,7 +1,7 @@ {% extends "errors/404/base.html" %} {% load i18n %} -{# visiting a slug or subdomain that doesn't match the intended project #} +{# visiting a slug or subdomain that doesn't match a project #} {% block 404_error_message %} From 797e96b8cbf2c0e708c2144ff84bbad1d163ab3c Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Thu, 23 Feb 2023 13:15:41 +0100 Subject: [PATCH 20/70] Removes log.debug in views.serve --- readthedocs/proxito/views/serve.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/readthedocs/proxito/views/serve.py b/readthedocs/proxito/views/serve.py index da4ce1f4124..50e98db452a 100644 --- a/readthedocs/proxito/views/serve.py +++ b/readthedocs/proxito/views/serve.py @@ -316,11 +316,9 @@ def get(self, request, proxito_path, template_name='404.html'): proxito_path, urlconf='readthedocs.proxito.urls', ) - log.debug("Resolved a URL.") version_slug = kwargs.get('version_slug') version_slug = self.get_version_from_host(request, version_slug) - log.debug("Getting _get_project_data_from_request.") ( final_project, lang_slug, @@ -334,7 +332,6 @@ def get(self, request, proxito_path, template_name='404.html'): version_slug=version_slug, filename=kwargs.get('filename', ''), ) - log.debug("Finished _get_project_data_from_request.") log.bind( project_slug=final_project.slug, From 8d38b8f4bfc1b5687351da34e898801f4d13465f Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Thu, 23 Feb 2023 15:47:31 +0100 Subject: [PATCH 21/70] More work on text copy, fix bugs, raise ProxitoProjectVersionHttp404 in an additional case --- media/css/core.css | 1 + readthedocs/proxito/exceptions.py | 4 +-- readthedocs/proxito/middleware.py | 12 +++++-- readthedocs/proxito/views/decorators.py | 5 ++- readthedocs/proxito/views/serve.py | 32 ++++++++++++------- readthedocs/proxito/views/utils.py | 4 +-- readthedocs/templates/core/widesearchbar.html | 2 +- readthedocs/templates/errors/404/base.html | 3 +- .../errors/404/include_error_message.html | 16 ++++++++++ .../templates/errors/404/include_search.html | 7 ++++ .../templates/errors/404/include_tips.html | 3 +- .../templates/errors/404/no_project.html | 22 ++++++++----- .../templates/errors/404/no_project_page.html | 16 ++-------- .../templates/errors/404/no_subproject.html | 6 ++++ .../templates/errors/404/no_version.html | 17 ++-------- readthedocs/templates/homepage.html | 3 +- 16 files changed, 92 insertions(+), 61 deletions(-) create mode 100644 readthedocs/templates/errors/404/include_error_message.html create mode 100644 readthedocs/templates/errors/404/include_search.html diff --git a/media/css/core.css b/media/css/core.css index a41dd9c67c3..fa58300a696 100644 --- a/media/css/core.css +++ b/media/css/core.css @@ -1332,6 +1332,7 @@ div.module.project-subprojects li.subproject a.subproject-edit:before { } #content ul.normal_list {list-style: disc; margin-left: 20px;} +#content code {background: #eee; border: 1px solid #ccc; padding: 3px; display: inline-block;} /* Pygments */ div.highlight pre .hll { background-color: #ffffcc } diff --git a/readthedocs/proxito/exceptions.py b/readthedocs/proxito/exceptions.py index d095ec6208d..d51b0fa6469 100644 --- a/readthedocs/proxito/exceptions.py +++ b/readthedocs/proxito/exceptions.py @@ -38,7 +38,7 @@ def __init__(self, message, project_slug=None, project=None, subproject_slug=Non self.subproject_slug = subproject_slug -class ProxitoProjectPageHttp404(ProxitoHttp404): +class ProxitoProjectPageHttp404(ProxitoProjectHttp404): """ Raised if a page inside an existing project was not found. """ @@ -49,7 +49,7 @@ def __init__(self, message, project_slug=None, project=None, subproject_slug=Non self.subproject_slug = subproject_slug -class ProxitoProjectVersionHttp404(ProxitoHttp404): +class ProxitoProjectVersionHttp404(ProxitoProjectHttp404): """ Raised if a version was not found. diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index c6d882a564c..7daf86cbea6 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -12,7 +12,6 @@ import structlog from django.conf import settings from django.core.exceptions import SuspiciousOperation -from django.http import Http404 from django.shortcuts import redirect, render from django.urls import reverse from django.utils.deprecation import MiddlewareMixin @@ -26,6 +25,7 @@ unresolver, ) from readthedocs.core.utils import get_cache_tag +from readthedocs.proxito.exceptions import ProxitoProjectHttp404 log = structlog.get_logger(__name__) @@ -213,9 +213,15 @@ def process_request(self, request): # noqa context={"host": exc.domain}, status=400, ) - except (InvalidSubdomainError, InvalidExternalDomainError): + except (InvalidSubdomainError, InvalidExternalDomainError) as e: log.debug("Invalid project set on the subdomain.") - raise Http404 + # We pass e.domain as 'project_slug' because we cannot unpack a slug from the + # domain at this stage. The domain can be a custom domain docs.foobar.tld, + # in which case 'docs' would be an incorrect slug. + raise ProxitoProjectHttp404( + f"No project found for requested domain {e.domain}.", + project_slug=e.domain, + ) except InvalidCustomDomainError as exc: # Some person is CNAMEing to us without configuring a domain - 404. log.debug("CNAME 404.", domain=exc.domain) diff --git a/readthedocs/proxito/views/decorators.py b/readthedocs/proxito/views/decorators.py index 73741882e94..00f0f4044e7 100644 --- a/readthedocs/proxito/views/decorators.py +++ b/readthedocs/proxito/views/decorators.py @@ -23,8 +23,11 @@ def map_subproject_slug(view_func): @wraps(view_func) def inner_view( # noqa - request, project=None, subproject=None, subproject_slug=None, *args, **kwargs + request, subproject=None, subproject_slug=None, *args, **kwargs ): + # Something not entirely clear is happening when unpacking args and kwargs + # so the project is fetched from kwarg dictionary. + project = kwargs["project"] if subproject is None and subproject_slug: # Try to fetch by subproject alias first, otherwise we might end up # redirected to an unrelated project. diff --git a/readthedocs/proxito/views/serve.py b/readthedocs/proxito/views/serve.py index 50e98db452a..219609b4bc0 100644 --- a/readthedocs/proxito/views/serve.py +++ b/readthedocs/proxito/views/serve.py @@ -25,6 +25,7 @@ from readthedocs.proxito.exceptions import ( ProxitoHttp404, ProxitoProjectPageHttp404, + ProxitoProjectVersionHttp404, ProxitoSubProjectHttp404, ) from readthedocs.redirects.exceptions import InfiniteRedirectException @@ -104,8 +105,8 @@ def get(self, version_slug = self.get_version_from_host(request, version_slug) final_project, lang_slug, version_slug, filename = _get_project_data_from_request( # noqa request, - project_slug=project_slug, - subproject_slug=subproject_slug, + project_slug, + subproject_slug, lang_slug=lang_slug, version_slug=version_slug, filename=filename, @@ -326,9 +327,9 @@ def get(self, request, proxito_path, template_name='404.html'): filename, ) = _get_project_data_from_request( # noqa request, - project_slug=kwargs.get('project_slug'), - subproject_slug=kwargs.get('subproject_slug'), - lang_slug=kwargs.get('lang_slug'), + kwargs.get("project_slug"), + kwargs.get("subproject_slug"), + lang_slug=kwargs.get("lang_slug"), version_slug=version_slug, filename=kwargs.get('filename', ''), ) @@ -373,6 +374,8 @@ def get(self, request, proxito_path, template_name='404.html'): # TODO: decide if we need to check for infinite redirect here # (from URL == to URL) return HttpResponseRedirect(redirect_url) + else: + log.debug("No version in request") # Check and perform redirects on 404 handler # NOTE: this redirect check must be done after trying files like @@ -395,6 +398,7 @@ def get(self, request, proxito_path, template_name='404.html'): version = Version.objects.filter( project=final_project, slug=version_slug ).first() + version_found = bool(version) # If there are no redirect, # try to serve the custom 404 of the current version (version_slug) @@ -438,12 +442,18 @@ def get(self, request, proxito_path, template_name='404.html'): resp.status_code = 404 return resp - raise ProxitoProjectPageHttp404( - "No custom 404 page found.", - project=final_project, - project_slug=kwargs.get("project_slug"), - subproject_slug=kwargs.get("subproject_slug"), - ) + if version_found: + raise ProxitoProjectPageHttp404( + "Page not found and no custom 404", + project=final_project, + project_slug=final_project.slug, + ) + else: + raise ProxitoProjectVersionHttp404( + "Version not found and no custom 404", + project=final_project, + project_slug=final_project.slug, + ) def _register_broken_link(self, project, version, path, full_path): try: diff --git a/readthedocs/proxito/views/utils.py b/readthedocs/proxito/views/utils.py index 7a31199a621..d3b3aa61270 100644 --- a/readthedocs/proxito/views/utils.py +++ b/readthedocs/proxito/views/utils.py @@ -44,9 +44,7 @@ def proxito_404_page_handler( return fast_404(request, exception, template_name) # Serve the general 404 error message - return server_error_404( - request, exception=None, template_name="errors/404/base.html" - ) + return server_error_404(request, exception=exception, template_name=template_name) @map_project_slug diff --git a/readthedocs/templates/core/widesearchbar.html b/readthedocs/templates/core/widesearchbar.html index d310e94b5e9..25cbf44a4e3 100644 --- a/readthedocs/templates/core/widesearchbar.html +++ b/readthedocs/templates/core/widesearchbar.html @@ -7,7 +7,7 @@

    {% trans "Search all the docs" %}

    -
    +
    diff --git a/readthedocs/templates/errors/404/base.html b/readthedocs/templates/errors/404/base.html index 71dbb95b19b..524fde40346 100644 --- a/readthedocs/templates/errors/404/base.html +++ b/readthedocs/templates/errors/404/base.html @@ -11,8 +11,9 @@

    {% trans "404 Not Found" %}

    {% block 404_error_message %} - +

    {% trans "You have encountered a 404 on Read the Docs. You are either looking for a page that does not exist or a project that has been removed." %}

    +
     
    diff --git a/readthedocs/templates/errors/404/include_error_message.html b/readthedocs/templates/errors/404/include_error_message.html
    new file mode 100644
    index 00000000000..3ec8de3482b
    --- /dev/null
    +++ b/readthedocs/templates/errors/404/include_error_message.html
    @@ -0,0 +1,16 @@
    +{% load i18n %}
    +
    +

    + {% blocktrans trimmed with request_path=request.path %} + You are browsing the documentation of {{ project_slug }}. + The {{ not_found_subject }} you are looking for at {{ request_path }} was not found. + {% endblocktrans %} +

    + +

    + {% blocktrans trimmed %} + Documentation changes over time and pages are moved around. + You can try to navigate to the index page of the project and use its navigation or search function to find a similar page. + {% endblocktrans %} +

    +
    diff --git a/readthedocs/templates/errors/404/include_search.html b/readthedocs/templates/errors/404/include_search.html new file mode 100644 index 00000000000..c8986f6ee00 --- /dev/null +++ b/readthedocs/templates/errors/404/include_search.html @@ -0,0 +1,7 @@ +{% load i18n %} + +

    {% trans "Try searching?" %}

    + +
    + {% include "core/widesearchbar.html" with form_action="https://readthedocs.org/search/" %} +
    diff --git a/readthedocs/templates/errors/404/include_tips.html b/readthedocs/templates/errors/404/include_tips.html index 78017e3a3c4..d507a918d33 100644 --- a/readthedocs/templates/errors/404/include_tips.html +++ b/readthedocs/templates/errors/404/include_tips.html @@ -1,5 +1,5 @@ {% load i18n %} - +

    {% trans "Are you the project owner?" %}

    {% trans "Here are some tips to address 404 errors:" %}

    @@ -8,3 +8,4 @@

    {% trans "Are you the project owner?" %}

  • {% trans "Use custom 404 pages:" %} {% trans "Read more" %} »
  • {% trans "Create redirects when you move contents:" %} {% trans "Read more" %} »
+ diff --git a/readthedocs/templates/errors/404/no_project.html b/readthedocs/templates/errors/404/no_project.html index 6cadf24f80d..c4ddc904217 100644 --- a/readthedocs/templates/errors/404/no_project.html +++ b/readthedocs/templates/errors/404/no_project.html @@ -4,13 +4,19 @@ {# visiting a slug or subdomain that doesn't match a project #} {% block 404_error_message %} +
+

+ {% blocktrans trimmed %} + The project {{ project_slug }} that you are looking for either does not exist, has been deleted or does not yet have any valid builds. + {% endblocktrans %} +

+

+ {% blocktrans trimmed with request_path=request.path %} + You are browsing the documentation of {{ project_slug }}. + The page you are looking for at {{ request_path }} was not found. + {% endblocktrans %} +

+
- {# we are looking at a 404 generated by proxito, we then know that the project itself doesn't exist or hasn't been built #} - -

- {% blocktrans trimmed %} - The project {{ project_slug }} that you are looking for either does not exist, has been deleted or does not yet have any valid builds. - {% endblocktrans %} -

- + {% include "errors/404/include_search.html" %} {% endblock %} diff --git a/readthedocs/templates/errors/404/no_project_page.html b/readthedocs/templates/errors/404/no_project_page.html index 77da321ceb2..3146523f3f8 100644 --- a/readthedocs/templates/errors/404/no_project_page.html +++ b/readthedocs/templates/errors/404/no_project_page.html @@ -4,20 +4,8 @@ {# project exists but page doesn't #} {% block 404_error_message %} - -

- {% blocktrans trimmed with request_path=request.path %} - You are browsing the documentation of {{ project_slug }}. - The page you are looking for at {{ request_path }} was not found. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - Documentation changes over time. Pages are moved around. You can try to navigate to the index page of the project and use its navigation or search function to find a similar page. - {% endblocktrans %} -

- + {% include "errors/404/include_error_message.html" with not_found_subject="page" %} + {% include "errors/404/include_search.html" %} {% include "errors/404/include_tips.html" %} {% endblock %} diff --git a/readthedocs/templates/errors/404/no_subproject.html b/readthedocs/templates/errors/404/no_subproject.html index 1bd2dae4f9b..16cfff8caa2 100644 --- a/readthedocs/templates/errors/404/no_subproject.html +++ b/readthedocs/templates/errors/404/no_subproject.html @@ -5,6 +5,9 @@ {% block 404_error_message %} + {% include "errors/404/include_error_message.html" with not_found_subject="subproject" %} + +

{% blocktrans trimmed with request_path=request.path %} You are browsing the documentation of project {{ project_slug }}. @@ -17,6 +20,9 @@ Documentation changes over time. Pages are moved around. You can try to navigate to the index page of the project and use its navigation or search function to find a similar page. {% endblocktrans %}

+
+ + {% include "errors/404/include_search.html" %} {% include "errors/404/include_tips.html" %} diff --git a/readthedocs/templates/errors/404/no_version.html b/readthedocs/templates/errors/404/no_version.html index 7e5696ff8dd..6405d3e80fa 100644 --- a/readthedocs/templates/errors/404/no_version.html +++ b/readthedocs/templates/errors/404/no_version.html @@ -4,20 +4,7 @@ {# project exists but version doesn't #} {% block 404_error_message %} - -

- {% blocktrans trimmed with request_path=request.path %} - You are browsing the documentation of project {{ project_slug }}. - The version you are looking for at {{ request_path }} was not found. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - Documentation changes over time. Pages are moved around. You can try to navigate to the index page of the project and use its navigation or search function to find a similar page. - {% endblocktrans %} -

- + {% include "errors/404/include_error_message.html" with not_found_subject="project version" %} + {% include "errors/404/include_search.html" %} {% include "errors/404/include_tips.html" %} - {% endblock %} diff --git a/readthedocs/templates/homepage.html b/readthedocs/templates/homepage.html index 96757dd222a..7832c09bdbb 100644 --- a/readthedocs/templates/homepage.html +++ b/readthedocs/templates/homepage.html @@ -112,7 +112,8 @@

{% trans "Multiple versions" %}

- {% include "core/widesearchbar.html" %} + {% url "search" as search_form_action %} + {% include "core/widesearchbar.html" with form_action=search_form_action %}
{% get_current_language as language %} From b60a1093f432485cc5b8900ac32913fa1ad0d142 Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Thu, 23 Feb 2023 16:24:32 +0100 Subject: [PATCH 22/70] Contextualize when subprojects aren't found. The disadvantages of the current decorator structure is worth paying attention to here. --- readthedocs/proxito/views/decorators.py | 5 ++ readthedocs/proxito/views/serve.py | 81 ++++++++++++++----- .../templates/errors/404/no_subproject.html | 15 ---- 3 files changed, 65 insertions(+), 36 deletions(-) diff --git a/readthedocs/proxito/views/decorators.py b/readthedocs/proxito/views/decorators.py index 00f0f4044e7..14664eea0fe 100644 --- a/readthedocs/proxito/views/decorators.py +++ b/readthedocs/proxito/views/decorators.py @@ -25,10 +25,14 @@ def map_subproject_slug(view_func): def inner_view( # noqa request, subproject=None, subproject_slug=None, *args, **kwargs ): + log.debug( + f"Checking if there is a subproject to be resolved. Got subproject_slug {subproject_slug}" + ) # Something not entirely clear is happening when unpacking args and kwargs # so the project is fetched from kwarg dictionary. project = kwargs["project"] if subproject is None and subproject_slug: + log.debug(f"Trying to find subproject slug {subproject_slug}") # Try to fetch by subproject alias first, otherwise we might end up # redirected to an unrelated project. # Depends on a project passed into kwargs @@ -83,6 +87,7 @@ def inner_view( # noqa try: project = Project.objects.get(slug=project_slug) except Project.DoesNotExist: + log.debug(f"No project found with slug {project_slug}") raise ProxitoProjectHttp404( "Project does not exist.", project_slug=project_slug ) diff --git a/readthedocs/proxito/views/serve.py b/readthedocs/proxito/views/serve.py index 219609b4bc0..fe690362c2f 100644 --- a/readthedocs/proxito/views/serve.py +++ b/readthedocs/proxito/views/serve.py @@ -24,6 +24,7 @@ from readthedocs.proxito.constants import RedirectType from readthedocs.proxito.exceptions import ( ProxitoHttp404, + ProxitoProjectHttp404, ProxitoProjectPageHttp404, ProxitoProjectVersionHttp404, ProxitoSubProjectHttp404, @@ -103,14 +104,31 @@ def get(self, so that we can decide if we need to serve docs or add a /. """ version_slug = self.get_version_from_host(request, version_slug) - final_project, lang_slug, version_slug, filename = _get_project_data_from_request( # noqa - request, - project_slug, - subproject_slug, - lang_slug=lang_slug, - version_slug=version_slug, - filename=filename, - ) + try: + ( + final_project, + lang_slug, + version_slug, + filename, + ) = _get_project_data_from_request( # noqa + request, + project_slug, + subproject_slug, + lang_slug=lang_slug, + version_slug=version_slug, + filename=filename, + ) + # This special treatment of ProxitoProjectHttp404 happens because the decorator that + # resolves a project doesn't know if it's resolving a subproject or a normal project + except ProxitoProjectHttp404 as e: + if subproject_slug: + log.debug("This is actually a subproject") + raise ProxitoSubProjectHttp404( + f"Could not find subproject for {subproject_slug}", + project_slug=e.project_slug, + subproject_slug=subproject_slug, + ) + raise version = final_project.versions.filter(slug=version_slug).first() is_external = request.unresolved_domain.is_from_external_domain @@ -320,19 +338,33 @@ def get(self, request, proxito_path, template_name='404.html'): version_slug = kwargs.get('version_slug') version_slug = self.get_version_from_host(request, version_slug) - ( - final_project, - lang_slug, - version_slug, - filename, - ) = _get_project_data_from_request( # noqa - request, - kwargs.get("project_slug"), - kwargs.get("subproject_slug"), - lang_slug=kwargs.get("lang_slug"), - version_slug=version_slug, - filename=kwargs.get('filename', ''), - ) + # This special treatment of ProxitoProjectHttp404 happens because the decorator that + # resolves a project doesn't know if it's resolving a subproject or a normal project + subproject_slug = kwargs.get("subproject_slug") + project_slug = kwargs.get("project_slug") + try: + ( + final_project, + lang_slug, + version_slug, + filename, + ) = _get_project_data_from_request( # noqa + request, + project_slug, + subproject_slug, + lang_slug=kwargs.get("lang_slug"), + version_slug=version_slug, + filename=kwargs.get("filename", ""), + ) + except ProxitoProjectHttp404 as e: + if subproject_slug: + log.debug("This is actually a subproject") + raise ProxitoSubProjectHttp404( + f"Could not find subproject for {subproject_slug}", + project_slug=e.project_slug, + subproject_slug=subproject_slug, + ) + raise log.bind( project_slug=final_project.slug, @@ -448,6 +480,13 @@ def get(self, request, proxito_path, template_name='404.html'): project=final_project, project_slug=final_project.slug, ) + elif kwargs.get("subproject_slug"): + raise ProxitoSubProjectHttp404( + "Subproject not found and no custom 404", + project=final_project, + project_slug=final_project.slug, + subproject_slug=kwargs.get("subproject_slug"), + ) else: raise ProxitoProjectVersionHttp404( "Version not found and no custom 404", diff --git a/readthedocs/templates/errors/404/no_subproject.html b/readthedocs/templates/errors/404/no_subproject.html index 16cfff8caa2..72ec359a7be 100644 --- a/readthedocs/templates/errors/404/no_subproject.html +++ b/readthedocs/templates/errors/404/no_subproject.html @@ -7,21 +7,6 @@ {% include "errors/404/include_error_message.html" with not_found_subject="subproject" %} -
-

- {% blocktrans trimmed with request_path=request.path %} - You are browsing the documentation of project {{ project_slug }}. - The subproject {{ subproject_slug }} you are looking for at {{ request_path }} was not found. - {% endblocktrans %} -

- -

- {% blocktrans trimmed %} - Documentation changes over time. Pages are moved around. You can try to navigate to the index page of the project and use its navigation or search function to find a similar page. - {% endblocktrans %} -

-
- {% include "errors/404/include_search.html" %} {% include "errors/404/include_tips.html" %} From b557590780958312060fc6b851a26f3f66c488e0 Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Thu, 23 Feb 2023 16:28:53 +0100 Subject: [PATCH 23/70] Clean up --- readthedocs/proxito/views/decorators.py | 4 ---- readthedocs/proxito/views/serve.py | 4 ++-- readthedocs/settings/base.py | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/readthedocs/proxito/views/decorators.py b/readthedocs/proxito/views/decorators.py index 14664eea0fe..bffad18e684 100644 --- a/readthedocs/proxito/views/decorators.py +++ b/readthedocs/proxito/views/decorators.py @@ -25,14 +25,10 @@ def map_subproject_slug(view_func): def inner_view( # noqa request, subproject=None, subproject_slug=None, *args, **kwargs ): - log.debug( - f"Checking if there is a subproject to be resolved. Got subproject_slug {subproject_slug}" - ) # Something not entirely clear is happening when unpacking args and kwargs # so the project is fetched from kwarg dictionary. project = kwargs["project"] if subproject is None and subproject_slug: - log.debug(f"Trying to find subproject slug {subproject_slug}") # Try to fetch by subproject alias first, otherwise we might end up # redirected to an unrelated project. # Depends on a project passed into kwargs diff --git a/readthedocs/proxito/views/serve.py b/readthedocs/proxito/views/serve.py index fe690362c2f..ea4b52a8d82 100644 --- a/readthedocs/proxito/views/serve.py +++ b/readthedocs/proxito/views/serve.py @@ -122,7 +122,7 @@ def get(self, # resolves a project doesn't know if it's resolving a subproject or a normal project except ProxitoProjectHttp404 as e: if subproject_slug: - log.debug("This is actually a subproject") + log.debug("Project expected to be a subproject was not found") raise ProxitoSubProjectHttp404( f"Could not find subproject for {subproject_slug}", project_slug=e.project_slug, @@ -358,7 +358,7 @@ def get(self, request, proxito_path, template_name='404.html'): ) except ProxitoProjectHttp404 as e: if subproject_slug: - log.debug("This is actually a subproject") + log.debug("Project expected to be a subproject was not found") raise ProxitoSubProjectHttp404( f"Could not find subproject for {subproject_slug}", project_slug=e.project_slug, diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index 99827f9f687..4b2e76fa7b7 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -46,7 +46,7 @@ class CommunityBaseSettings(Settings): DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' # Debug settings - DEBUG = False + DEBUG = True RTD_FORCE_SHOW_DEBUG_TOOLBAR = False @property From 3f7f2fd1cbacd0fe19dc2bf03b905d425a539e6d Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Thu, 23 Feb 2023 16:40:01 +0100 Subject: [PATCH 24/70] Add project:slug search scope when pages aren't found --- readthedocs/templates/core/widesearchbar.html | 5 ++++- readthedocs/templates/errors/404/include_search.html | 8 +++++++- readthedocs/templates/errors/404/no_project_page.html | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/readthedocs/templates/core/widesearchbar.html b/readthedocs/templates/core/widesearchbar.html index 25cbf44a4e3..bcb91283106 100644 --- a/readthedocs/templates/core/widesearchbar.html +++ b/readthedocs/templates/core/widesearchbar.html @@ -1,8 +1,11 @@ {% load i18n %} + {% if term|default_if_none:False %} +

{% trans "Search the docs" %}

+ {% else %}

{% trans "Search all the docs" %}

- + {% endif %}