From 13679ad12ed70e4cbe0c77772fa35d2a9fe08975 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 18 Nov 2024 17:54:06 +0100 Subject: [PATCH] Addons: `load_when_embedded` config (#11765) Add an extra addons config to decide whether or not force the injection of addons when the page is embedded (eg. iframe). By default, we are not loading addons if embedded. Required by https://github.com/readthedocs/addons/pull/415 Closes https://github.com/readthedocs/addons/issues/412 --- .../0133_addons_load_when_embedded.py | 25 +++++++++++++ .../0134_addons_load_when_embedded_notnull.py | 25 +++++++++++++ .../migrations/0135_addons_customscript.py | 35 +++++++++++++++++++ .../0136_addons_customscript_notnull.py | 25 +++++++++++++ readthedocs/projects/models.py | 16 +++++++++ readthedocs/proxito/middleware.py | 9 +++-- readthedocs/proxito/tests/responses/v1.json | 7 ++++ readthedocs/proxito/views/hosting.py | 7 ++++ 8 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 readthedocs/projects/migrations/0133_addons_load_when_embedded.py create mode 100644 readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py create mode 100644 readthedocs/projects/migrations/0135_addons_customscript.py create mode 100644 readthedocs/projects/migrations/0136_addons_customscript_notnull.py diff --git a/readthedocs/projects/migrations/0133_addons_load_when_embedded.py b/readthedocs/projects/migrations/0133_addons_load_when_embedded.py new file mode 100644 index 00000000000..f261ec119f4 --- /dev/null +++ b/readthedocs/projects/migrations/0133_addons_load_when_embedded.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.16 on 2024-11-13 12:00 + +from django.db import migrations, models +from django_safemigrate import Safe + + +class Migration(migrations.Migration): + safe = Safe.before_deploy + + dependencies = [ + ('projects', '0132_addons_linkpreviews_fields'), + ] + + operations = [ + migrations.AddField( + model_name='addonsconfig', + name='options_load_when_embedded', + field=models.BooleanField(default=False, null=True), + ), + migrations.AddField( + model_name='historicaladdonsconfig', + name='options_load_when_embedded', + field=models.BooleanField(default=False, null=True), + ), + ] diff --git a/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py b/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py new file mode 100644 index 00000000000..b08aeca53e4 --- /dev/null +++ b/readthedocs/projects/migrations/0134_addons_load_when_embedded_notnull.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.16 on 2024-11-13 12:01 + +from django.db import migrations, models +from django_safemigrate import Safe + + +class Migration(migrations.Migration): + safe = Safe.after_deploy + + dependencies = [ + ('projects', '0133_addons_load_when_embedded'), + ] + + operations = [ + migrations.AlterField( + model_name='addonsconfig', + name='options_load_when_embedded', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='historicaladdonsconfig', + name='options_load_when_embedded', + field=models.BooleanField(default=False), + ), + ] diff --git a/readthedocs/projects/migrations/0135_addons_customscript.py b/readthedocs/projects/migrations/0135_addons_customscript.py new file mode 100644 index 00000000000..d82ab66cc0a --- /dev/null +++ b/readthedocs/projects/migrations/0135_addons_customscript.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.16 on 2024-11-13 13:34 + +from django.db import migrations, models +from django_safemigrate import Safe + + +class Migration(migrations.Migration): + safe = Safe.before_deploy + + dependencies = [ + ('projects', '0134_addons_load_when_embedded_notnull'), + ] + + operations = [ + migrations.AddField( + model_name='addonsconfig', + name='customscript_enabled', + field=models.BooleanField(default=False, null=True), + ), + migrations.AddField( + model_name='addonsconfig', + name='customscript_src', + field=models.CharField(blank=True, help_text='URL to a JavaScript file to inject at serve time', max_length=512, null=True), + ), + migrations.AddField( + model_name='historicaladdonsconfig', + name='customscript_enabled', + field=models.BooleanField(default=False, null=True), + ), + migrations.AddField( + model_name='historicaladdonsconfig', + name='customscript_src', + field=models.CharField(blank=True, help_text='URL to a JavaScript file to inject at serve time', max_length=512, null=True), + ), + ] diff --git a/readthedocs/projects/migrations/0136_addons_customscript_notnull.py b/readthedocs/projects/migrations/0136_addons_customscript_notnull.py new file mode 100644 index 00000000000..18ea9b495ac --- /dev/null +++ b/readthedocs/projects/migrations/0136_addons_customscript_notnull.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.16 on 2024-11-13 13:36 + +from django.db import migrations, models +from django_safemigrate import Safe + + +class Migration(migrations.Migration): + safe = Safe.after_deploy + + dependencies = [ + ('projects', '0135_addons_customscript'), + ] + + operations = [ + migrations.AlterField( + model_name='addonsconfig', + name='customscript_enabled', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='historicaladdonsconfig', + name='customscript_enabled', + field=models.BooleanField(default=False), + ), + ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index de284079542..b8358f0f7f9 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -167,6 +167,10 @@ class AddonsConfig(TimeStampedModel): help_text="Enable/Disable all the addons on this project", ) + # Whether or not load addons library when the requested page is embedded (e.g. inside an iframe) + # https://github.com/readthedocs/addons/pull/415 + options_load_when_embedded = models.BooleanField(default=False) + # Analytics # NOTE: we keep analytics disabled by default to save resources. @@ -218,6 +222,18 @@ class AddonsConfig(TimeStampedModel): search_enabled = models.BooleanField(default=True) search_default_filter = models.CharField(null=True, blank=True, max_length=128) + # User JavaScript File + customscript_enabled = models.BooleanField(default=False) + + # This is a user-defined file that will be injected at serve time by our + # Cloudflare Worker if defined + customscript_src = models.CharField( + max_length=512, + null=True, + blank=True, + help_text="URL to a JavaScript file to inject at serve time", + ) + # Notifications notifications_enabled = models.BooleanField(default=True) notifications_show_on_latest = models.BooleanField(default=True) diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index 5db6f24e5ca..772ce06a781 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -30,7 +30,7 @@ unresolver, ) from readthedocs.core.utils import get_cache_tag -from readthedocs.projects.models import Project +from readthedocs.projects.models import AddonsConfig from readthedocs.proxito.cache import add_cache_tags, cache_response, private_response from readthedocs.proxito.redirects import redirect_to_https @@ -283,12 +283,11 @@ def add_hosting_integrations_headers(self, request, response): project_slug = getattr(request, "path_project_slug", "") if project_slug: - addons = Project.objects.filter( - slug=project_slug, addons__enabled=True - ).exists() + addons = AddonsConfig.objects.filter(project__slug=project_slug).first() if addons: - response["X-RTD-Force-Addons"] = "true" + if addons.enabled: + response["X-RTD-Force-Addons"] = "true" def add_cors_headers(self, request, response): """ diff --git a/readthedocs/proxito/tests/responses/v1.json b/readthedocs/proxito/tests/responses/v1.json index 70e6fa26556..9a9920754e6 100644 --- a/readthedocs/proxito/tests/responses/v1.json +++ b/readthedocs/proxito/tests/responses/v1.json @@ -121,6 +121,9 @@ } }, "addons": { + "options": { + "load_when_embedded": false + }, "analytics": { "enabled": false, "code": null @@ -161,6 +164,10 @@ "default_filter": "project:project/latest", "filters": [] }, + "customscript": { + "enabled": false, + "src": null + }, "linkpreviews": { "enabled": false, "root_selector": "[role=main] a.internal", diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 3e943e0014f..3964954cb83 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -454,6 +454,9 @@ def _v1(self, project, version, build, filename, url, request): # Mainly, all the fields including a Project, Version or Build will use the exact same # serializer than the keys ``project``, ``version`` and ``build`` from the top level. "addons": { + "options": { + "load_when_embedded": project.addons.options_load_when_embedded, + }, "analytics": { "enabled": project.addons.analytics_enabled, # TODO: consider adding this field into the ProjectSerializer itself. @@ -487,6 +490,10 @@ def _v1(self, project, version, build, filename, url, request): # "filepath": "/docs/index.rst", # }, }, + "customscript": { + "enabled": project.addons.customscript_enabled, + "src": project.addons.customscript_src, + }, "search": { "enabled": project.addons.search_enabled, # TODO: figure it out where this data comes from.