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.