From bf615b150a8038ed599e55d42d3d5e4d83fc6e7b Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 11 Nov 2024 18:06:06 +0100 Subject: [PATCH 1/6] Adddons: allow injecting an "always live" JavaScript file We talked about giving users a way to inject a JavaScript file they control using our Cloudflare Worker infrastructure to allow them manipulate frozen documentations. This could be used in different ways to fix bugs or add features to a particular frozen set of docs or even to all the versions. The user can make usage of API data to filter by version or not (e.g. `if (versions.current == "v3.0") { .. do something ...} `) The script could live in Read the Docs itself using a relative URL, or outside it, using an absolute URL. --- dockerfiles/nginx/proxito.conf.template | 2 ++ .../migrations/0132_addons_user_js_file.py | 25 +++++++++++++++++++ readthedocs/projects/models.py | 9 +++++++ readthedocs/proxito/middleware.py | 15 ++++++----- 4 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 readthedocs/projects/migrations/0132_addons_user_js_file.py diff --git a/dockerfiles/nginx/proxito.conf.template b/dockerfiles/nginx/proxito.conf.template index 592872f2737..3a7ef981cd2 100644 --- a/dockerfiles/nginx/proxito.conf.template +++ b/dockerfiles/nginx/proxito.conf.template @@ -125,6 +125,8 @@ server { add_header X-RTD-Hosting-Integrations $rtd_hosting_integrations always; set $rtd_force_addons $upstream_http_x_rtd_force_addons; add_header X-RTD-Force-Addons $rtd_force_addons always; + set $rtd_user_js_file $upstream_http_x_rtd_user_js_file; + add_header X-RTD-User-Js-File $rtd_user_js_file always; } # Serve 404 pages here diff --git a/readthedocs/projects/migrations/0132_addons_user_js_file.py b/readthedocs/projects/migrations/0132_addons_user_js_file.py new file mode 100644 index 00000000000..26480c51cda --- /dev/null +++ b/readthedocs/projects/migrations/0132_addons_user_js_file.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.16 on 2024-11-11 17:05 + +from django.db import migrations, models +from django_safemigrate import Safe + + +class Migration(migrations.Migration): + safe = Safe.before_deploy + + dependencies = [ + ('projects', '0131_increase_env_var_size'), + ] + + operations = [ + migrations.AddField( + model_name='addonsconfig', + name='js_file', + 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='js_file', + 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/models.py b/readthedocs/projects/models.py index 72c8c080dd0..91dc6498de6 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -162,6 +162,15 @@ class AddonsConfig(TimeStampedModel): help_text="Enable/Disable all the addons on this project", ) + # This is a user-defined file that will be injected at serve time by our + # Cloudflare Worker if defined + js_file = models.CharField( + max_length=512, + null=True, + blank=True, + help_text="URL to a JavaScript file to inject at serve time", + ) + # Analytics # NOTE: we keep analytics disabled by default to save resources. diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index 03928ffc20a..1d52e95ab29 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -34,7 +34,7 @@ unresolver, ) from readthedocs.core.utils import get_cache_tag -from readthedocs.projects.models import Feature, Project +from readthedocs.projects.models import AddonsConfig, Feature, Project from readthedocs.proxito.cache import add_cache_tags, cache_response, private_response from readthedocs.proxito.redirects import redirect_to_https @@ -303,13 +303,16 @@ def add_hosting_integrations_headers(self, request, response): tzinfo=tzinfo, ) if addons_enabled_by_default: - addons = Project.objects.filter( - slug=project_slug, addons__enabled=True - ).exists() + addons = AddonsConfig.objects.filter(project__slug=project_slug).first() + if not addons: + return - if addons: + if addons.enabled: response["X-RTD-Force-Addons"] = "true" - return + + if addons.js_file: + # response["X-RTD-User-Js-File"] = "/en/full-feature/readthedocs.js" + response["X-RTD-User-Js-File"] = addons.js_file else: # TODO: remove "else" code once DISABLE_SPHINX_MANIPULATION and addons becomes the default From f0718e02163b85615cab18c82b2f619e9cc631da Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 13 Nov 2024 13:46:26 +0100 Subject: [PATCH 2/6] Revert "Adddons: allow injecting an "always live" JavaScript file" This reverts commit bf615b150a8038ed599e55d42d3d5e4d83fc6e7b. --- dockerfiles/nginx/proxito.conf.template | 2 -- readthedocs/proxito/middleware.py | 15 ++++++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/dockerfiles/nginx/proxito.conf.template b/dockerfiles/nginx/proxito.conf.template index 3a7ef981cd2..592872f2737 100644 --- a/dockerfiles/nginx/proxito.conf.template +++ b/dockerfiles/nginx/proxito.conf.template @@ -125,8 +125,6 @@ server { add_header X-RTD-Hosting-Integrations $rtd_hosting_integrations always; set $rtd_force_addons $upstream_http_x_rtd_force_addons; add_header X-RTD-Force-Addons $rtd_force_addons always; - set $rtd_user_js_file $upstream_http_x_rtd_user_js_file; - add_header X-RTD-User-Js-File $rtd_user_js_file always; } # Serve 404 pages here diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index 1d52e95ab29..03928ffc20a 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -34,7 +34,7 @@ unresolver, ) from readthedocs.core.utils import get_cache_tag -from readthedocs.projects.models import AddonsConfig, Feature, Project +from readthedocs.projects.models import Feature, Project from readthedocs.proxito.cache import add_cache_tags, cache_response, private_response from readthedocs.proxito.redirects import redirect_to_https @@ -303,16 +303,13 @@ def add_hosting_integrations_headers(self, request, response): tzinfo=tzinfo, ) if addons_enabled_by_default: - addons = AddonsConfig.objects.filter(project__slug=project_slug).first() - if not addons: - return + addons = Project.objects.filter( + slug=project_slug, addons__enabled=True + ).exists() - if addons.enabled: + if addons: response["X-RTD-Force-Addons"] = "true" - - if addons.js_file: - # response["X-RTD-User-Js-File"] = "/en/full-feature/readthedocs.js" - response["X-RTD-User-Js-File"] = addons.js_file + return else: # TODO: remove "else" code once DISABLE_SPHINX_MANIPULATION and addons becomes the default From 11bdab89516a775b7e2cd71d5181e1f27812c476 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 13 Nov 2024 13:51:56 +0100 Subject: [PATCH 3/6] Rename `js_file` for `user_js_file` --- ...2_addons_user_js_file.py => 0135_addons_user_js_file.py} | 6 +++--- readthedocs/projects/models.py | 2 +- readthedocs/proxito/tests/responses/v1.json | 3 ++- readthedocs/proxito/views/hosting.py | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) rename readthedocs/projects/migrations/{0132_addons_user_js_file.py => 0135_addons_user_js_file.py} (84%) diff --git a/readthedocs/projects/migrations/0132_addons_user_js_file.py b/readthedocs/projects/migrations/0135_addons_user_js_file.py similarity index 84% rename from readthedocs/projects/migrations/0132_addons_user_js_file.py rename to readthedocs/projects/migrations/0135_addons_user_js_file.py index 26480c51cda..9589e380044 100644 --- a/readthedocs/projects/migrations/0132_addons_user_js_file.py +++ b/readthedocs/projects/migrations/0135_addons_user_js_file.py @@ -8,18 +8,18 @@ class Migration(migrations.Migration): safe = Safe.before_deploy dependencies = [ - ('projects', '0131_increase_env_var_size'), + ('projects', '0134_addons_load_when_embedded_notnull'), ] operations = [ migrations.AddField( model_name='addonsconfig', - name='js_file', + name='user_js_file', 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='js_file', + name='user_js_file', 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/models.py b/readthedocs/projects/models.py index 6b2a30b3c8e..bc2eacaf848 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -169,7 +169,7 @@ class AddonsConfig(TimeStampedModel): # This is a user-defined file that will be injected at serve time by our # Cloudflare Worker if defined - js_file = models.CharField( + user_js_file = models.CharField( max_length=512, null=True, blank=True, diff --git a/readthedocs/proxito/tests/responses/v1.json b/readthedocs/proxito/tests/responses/v1.json index 9739d2e614c..d32133debd3 100644 --- a/readthedocs/proxito/tests/responses/v1.json +++ b/readthedocs/proxito/tests/responses/v1.json @@ -122,7 +122,8 @@ }, "addons": { "configs": { - "load_when_embedded": false + "load_when_embedded": false, + "user_js_file": null }, "analytics": { "enabled": false, diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 3df2c3d37f6..6da105801d5 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -456,6 +456,7 @@ def _v1(self, project, version, build, filename, url, request): "addons": { "configs": { "load_when_embedded": project.addons.load_when_embedded, + "user_js_file": project.addons.user_js_file, }, "analytics": { "enabled": project.addons.analytics_enabled, From a3a93f8268bb9ebc9144cbd5e3f225a22943ccc5 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 13 Nov 2024 14:39:01 +0100 Subject: [PATCH 4/6] Return `addons.userjsfile` in the API response --- ...r_js_file.py => 0135_addons_userjsfile.py} | 16 +++++++++--- .../0136_addons_userjsfile_notnull.py | 25 +++++++++++++++++++ readthedocs/projects/models.py | 21 +++++++++------- readthedocs/proxito/tests/responses/v1.json | 7 ++++-- readthedocs/proxito/views/hosting.py | 5 +++- 5 files changed, 59 insertions(+), 15 deletions(-) rename readthedocs/projects/migrations/{0135_addons_user_js_file.py => 0135_addons_userjsfile.py} (58%) create mode 100644 readthedocs/projects/migrations/0136_addons_userjsfile_notnull.py diff --git a/readthedocs/projects/migrations/0135_addons_user_js_file.py b/readthedocs/projects/migrations/0135_addons_userjsfile.py similarity index 58% rename from readthedocs/projects/migrations/0135_addons_user_js_file.py rename to readthedocs/projects/migrations/0135_addons_userjsfile.py index 9589e380044..0182161c087 100644 --- a/readthedocs/projects/migrations/0135_addons_user_js_file.py +++ b/readthedocs/projects/migrations/0135_addons_userjsfile.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2024-11-11 17:05 +# Generated by Django 4.2.16 on 2024-11-13 13:34 from django.db import migrations, models from django_safemigrate import Safe @@ -14,12 +14,22 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='addonsconfig', - name='user_js_file', + name='userjsfile_enabled', + field=models.BooleanField(default=True, null=True), + ), + migrations.AddField( + model_name='addonsconfig', + name='userjsfile_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='user_js_file', + name='userjsfile_enabled', + field=models.BooleanField(default=True, null=True), + ), + migrations.AddField( + model_name='historicaladdonsconfig', + name='userjsfile_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_userjsfile_notnull.py b/readthedocs/projects/migrations/0136_addons_userjsfile_notnull.py new file mode 100644 index 00000000000..c30fe185946 --- /dev/null +++ b/readthedocs/projects/migrations/0136_addons_userjsfile_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_userjsfile'), + ] + + operations = [ + migrations.AlterField( + model_name='addonsconfig', + name='userjsfile_enabled', + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name='historicaladdonsconfig', + name='userjsfile_enabled', + field=models.BooleanField(default=True), + ), + ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index bc2eacaf848..de6ac3bcee8 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -167,15 +167,6 @@ class AddonsConfig(TimeStampedModel): help_text="Enable/Disable all the addons on this project", ) - # This is a user-defined file that will be injected at serve time by our - # Cloudflare Worker if defined - user_js_file = models.CharField( - max_length=512, - null=True, - blank=True, - help_text="URL to a JavaScript file to inject at serve time", - ) - # Whether or not load addons library when the requested page is embedded (e.g. inside an iframe) # https://github.com/readthedocs/addons/pull/415 load_when_embedded = models.BooleanField(default=False) @@ -231,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 + userjsfile_enabled = models.BooleanField(default=True) + + # This is a user-defined file that will be injected at serve time by our + # Cloudflare Worker if defined + userjsfile_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/tests/responses/v1.json b/readthedocs/proxito/tests/responses/v1.json index d32133debd3..d32116926a6 100644 --- a/readthedocs/proxito/tests/responses/v1.json +++ b/readthedocs/proxito/tests/responses/v1.json @@ -122,8 +122,7 @@ }, "addons": { "configs": { - "load_when_embedded": false, - "user_js_file": null + "load_when_embedded": false }, "analytics": { "enabled": false, @@ -165,6 +164,10 @@ "default_filter": "project:project/latest", "filters": [] }, + "userjsfile": { + "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 6da105801d5..026b9fc1887 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -456,7 +456,6 @@ def _v1(self, project, version, build, filename, url, request): "addons": { "configs": { "load_when_embedded": project.addons.load_when_embedded, - "user_js_file": project.addons.user_js_file, }, "analytics": { "enabled": project.addons.analytics_enabled, @@ -491,6 +490,10 @@ def _v1(self, project, version, build, filename, url, request): # "filepath": "/docs/index.rst", # }, }, + "userjsfile": { + "enabled": project.addons.userjsfile_enabled, + "src": project.addons.userjsfile_src, + }, "search": { "enabled": project.addons.search_enabled, # TODO: figure it out where this data comes from. From 0462fb4eccdc83da5fccc010de188d11f1171200 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 14 Nov 2024 11:45:26 +0100 Subject: [PATCH 5/6] Rename userjsfile to customscript --- ...5_addons_userjsfile.py => 0135_addons_customscript.py} | 8 ++++---- ...ile_notnull.py => 0136_addons_customscript_notnull.py} | 6 +++--- readthedocs/projects/models.py | 4 ++-- readthedocs/proxito/tests/responses/v1.json | 2 +- readthedocs/proxito/views/hosting.py | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) rename readthedocs/projects/migrations/{0135_addons_userjsfile.py => 0135_addons_customscript.py} (87%) rename readthedocs/projects/migrations/{0136_addons_userjsfile_notnull.py => 0136_addons_customscript_notnull.py} (80%) diff --git a/readthedocs/projects/migrations/0135_addons_userjsfile.py b/readthedocs/projects/migrations/0135_addons_customscript.py similarity index 87% rename from readthedocs/projects/migrations/0135_addons_userjsfile.py rename to readthedocs/projects/migrations/0135_addons_customscript.py index 0182161c087..8948a3848a7 100644 --- a/readthedocs/projects/migrations/0135_addons_userjsfile.py +++ b/readthedocs/projects/migrations/0135_addons_customscript.py @@ -14,22 +14,22 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='addonsconfig', - name='userjsfile_enabled', + name='customscript_enabled', field=models.BooleanField(default=True, null=True), ), migrations.AddField( model_name='addonsconfig', - name='userjsfile_src', + 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='userjsfile_enabled', + name='customscript_enabled', field=models.BooleanField(default=True, null=True), ), migrations.AddField( model_name='historicaladdonsconfig', - name='userjsfile_src', + 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_userjsfile_notnull.py b/readthedocs/projects/migrations/0136_addons_customscript_notnull.py similarity index 80% rename from readthedocs/projects/migrations/0136_addons_userjsfile_notnull.py rename to readthedocs/projects/migrations/0136_addons_customscript_notnull.py index c30fe185946..a4028812581 100644 --- a/readthedocs/projects/migrations/0136_addons_userjsfile_notnull.py +++ b/readthedocs/projects/migrations/0136_addons_customscript_notnull.py @@ -8,18 +8,18 @@ class Migration(migrations.Migration): safe = Safe.after_deploy dependencies = [ - ('projects', '0135_addons_userjsfile'), + ('projects', '0135_addons_customscript'), ] operations = [ migrations.AlterField( model_name='addonsconfig', - name='userjsfile_enabled', + name='customscript_enabled', field=models.BooleanField(default=True), ), migrations.AlterField( model_name='historicaladdonsconfig', - name='userjsfile_enabled', + name='customscript_enabled', field=models.BooleanField(default=True), ), ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index de6ac3bcee8..37810a53f51 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -223,11 +223,11 @@ class AddonsConfig(TimeStampedModel): search_default_filter = models.CharField(null=True, blank=True, max_length=128) # User JavaScript File - userjsfile_enabled = models.BooleanField(default=True) + customscript_enabled = models.BooleanField(default=True) # This is a user-defined file that will be injected at serve time by our # Cloudflare Worker if defined - userjsfile_src = models.CharField( + customscript_src = models.CharField( max_length=512, null=True, blank=True, diff --git a/readthedocs/proxito/tests/responses/v1.json b/readthedocs/proxito/tests/responses/v1.json index d32116926a6..f6a293be783 100644 --- a/readthedocs/proxito/tests/responses/v1.json +++ b/readthedocs/proxito/tests/responses/v1.json @@ -164,7 +164,7 @@ "default_filter": "project:project/latest", "filters": [] }, - "userjsfile": { + "customscript": { "enabled": false, "src": null }, diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 026b9fc1887..b24cdcd4237 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -490,9 +490,9 @@ def _v1(self, project, version, build, filename, url, request): # "filepath": "/docs/index.rst", # }, }, - "userjsfile": { - "enabled": project.addons.userjsfile_enabled, - "src": project.addons.userjsfile_src, + "customscript": { + "enabled": project.addons.customscript_enabled, + "src": project.addons.customscript_src, }, "search": { "enabled": project.addons.search_enabled, From 4db3594d03cfe0fd27a68c60375e5555423d6927 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 14 Nov 2024 11:59:15 +0100 Subject: [PATCH 6/6] Default customscript to false --- readthedocs/projects/migrations/0135_addons_customscript.py | 4 ++-- .../projects/migrations/0136_addons_customscript_notnull.py | 4 ++-- readthedocs/projects/models.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/readthedocs/projects/migrations/0135_addons_customscript.py b/readthedocs/projects/migrations/0135_addons_customscript.py index 8948a3848a7..d82ab66cc0a 100644 --- a/readthedocs/projects/migrations/0135_addons_customscript.py +++ b/readthedocs/projects/migrations/0135_addons_customscript.py @@ -15,7 +15,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='addonsconfig', name='customscript_enabled', - field=models.BooleanField(default=True, null=True), + field=models.BooleanField(default=False, null=True), ), migrations.AddField( model_name='addonsconfig', @@ -25,7 +25,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='historicaladdonsconfig', name='customscript_enabled', - field=models.BooleanField(default=True, null=True), + field=models.BooleanField(default=False, null=True), ), migrations.AddField( model_name='historicaladdonsconfig', diff --git a/readthedocs/projects/migrations/0136_addons_customscript_notnull.py b/readthedocs/projects/migrations/0136_addons_customscript_notnull.py index a4028812581..18ea9b495ac 100644 --- a/readthedocs/projects/migrations/0136_addons_customscript_notnull.py +++ b/readthedocs/projects/migrations/0136_addons_customscript_notnull.py @@ -15,11 +15,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='addonsconfig', name='customscript_enabled', - field=models.BooleanField(default=True), + field=models.BooleanField(default=False), ), migrations.AlterField( model_name='historicaladdonsconfig', name='customscript_enabled', - field=models.BooleanField(default=True), + field=models.BooleanField(default=False), ), ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 37810a53f51..285035f2a48 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -223,7 +223,7 @@ class AddonsConfig(TimeStampedModel): search_default_filter = models.CharField(null=True, blank=True, max_length=128) # User JavaScript File - customscript_enabled = models.BooleanField(default=True) + 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