Skip to content

Commit

Permalink
Merge pull request #12851 from rtibbles/unsafe_inline
Browse files Browse the repository at this point in the history
Make Kolibri compliant with a secure Content Security Policy
  • Loading branch information
rtibbles authored Nov 22, 2024
2 parents d0acf93 + a41a079 commit d3ff5ec
Show file tree
Hide file tree
Showing 43 changed files with 695 additions and 534 deletions.
1 change: 1 addition & 0 deletions kolibri/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
INTERNAL_PLUGINS = [
"kolibri.plugins.app",
"kolibri.plugins.coach",
"kolibri.plugins.context_translation",
"kolibri.plugins.default_theme",
"kolibri.plugins.demo_server",
"kolibri.plugins.device",
Expand Down
7 changes: 2 additions & 5 deletions kolibri/core/assets/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@
* Provides the public API for the Kolibri FrontEnd core app.
* @module Facade
*/
// Import this first to ensure that we do a browser compatibility check before anything else
import './minimumBrowserRequirements';
import 'core-js';
import coreApp from 'kolibri';
import urls from 'kolibri/urls';
import logging from 'kolibri-logging';
import store from 'kolibri/store';
import heartbeat from 'kolibri/heartbeat';
import { i18nSetup } from 'kolibri/utils/i18n';
import coreModule from './state/modules/core';

// Do this before any async imports to ensure that public paths
// are set correctly
urls.setUp();

// set up logging
logging.setDefaultLevel(process.env.NODE_ENV === 'production' ? 2 : 0);

Expand Down
5 changes: 1 addition & 4 deletions kolibri/core/assets/src/minimumBrowserRequirements.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import isUndefined from 'lodash/isUndefined';
import browsers from 'browserslist-config-kolibri';
import plugin_data from 'kolibri-plugin-data';
// Do this to ensure that we sidestep the 'externals' configuration, and this code is
// directly bundled, and doesn't defer to the default bundle externals, which are
// loaded after this code is run.
import { browser, passesRequirements } from '../../../../packages/kolibri/utils/browserInfo';
import { browser, passesRequirements } from 'kolibri/utils/browserInfo';

const minimumBrowserRequirements = {};

Expand Down
2 changes: 1 addition & 1 deletion kolibri/core/assets/src/state/modules/core/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export function kolibriLogin(store, sessionPayload) {
browser,
os,
},
url: urls['kolibri:core:session-list'](),
url: urls['kolibri:core:session_list'](),
method: 'post',
})
.then(() => {
Expand Down
6 changes: 0 additions & 6 deletions kolibri/core/buildConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,4 @@ module.exports = [
},
},
},
{
bundle_id: 'frontend_head_assets',
webpack_config: {
entry: './assets/src/minimumBrowserRequirements.js',
},
},
];
20 changes: 12 additions & 8 deletions kolibri/core/content/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from abc import abstractmethod
from abc import abstractproperty

from django.core.serializers.json import DjangoJSONEncoder
from django.utils.safestring import mark_safe

from kolibri.core.webpack.hooks import WebpackBundleHook
Expand All @@ -32,14 +33,14 @@ def presets(self):
def html(cls):
tags = []
for hook in cls.registered_hooks:
tags.append(hook.render_to_page_load_async_html())
tags.append(hook.template_html())
return mark_safe("\n".join(tags))

def render_to_page_load_async_html(self):
def template_html(self):
"""
Generates script tag containing Javascript to register a content renderer.
Generates template tags containing data to register a content renderer.
:returns: HTML of a script tag to insert into a page.
:returns: HTML of a template tags to insert into a page.
"""
# Note, while most plugins use sorted chunks to filter by text direction
# content renderers do not, as they may need to have styling for a different
Expand All @@ -49,11 +50,14 @@ def render_to_page_load_async_html(self):
self.frontend_message_tag()
+ self.plugin_data_tag()
+ [
'<script>{kolibri_name}.registerContentRenderer("{bundle}", ["{urls}"], {presets});</script>'.format(
kolibri_name="kolibriCoreAppGlobal",
'<template data-viewer="{bundle}">{data}</template>'.format(
bundle=self.unique_id,
urls='","'.join(urls),
presets=json.dumps(self.presets),
data=json.dumps(
{"urls": urls, "presets": self.presets},
separators=(",", ":"),
ensure_ascii=False,
cls=DjangoJSONEncoder,
),
)
]
)
Expand Down
9 changes: 0 additions & 9 deletions kolibri/core/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from django.utils.safestring import mark_safe

from kolibri.core.webpack.hooks import WebpackBundleHook
from kolibri.core.webpack.hooks import WebpackInclusionASyncMixin
from kolibri.core.webpack.hooks import WebpackInclusionSyncMixin
from kolibri.plugins.hooks import define_hook
from kolibri.plugins.hooks import KolibriHook
Expand Down Expand Up @@ -59,14 +58,6 @@ class FrontEndBaseSyncHook(WebpackInclusionSyncMixin):
"""


@define_hook
class FrontEndBaseASyncHook(WebpackInclusionASyncMixin):
"""
Inherit a hook defining assets to be loaded in kolibri/base.html, that means
ALL pages. Use with care.
"""


@define_hook
class FrontEndBaseHeadHook(KolibriHook):
"""
Expand Down
111 changes: 36 additions & 75 deletions kolibri/core/kolibri_plugin.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
from django.conf import settings
from django.template.loader import render_to_string
from django.templatetags.static import static
from django.urls import get_resolver
from django.urls import reverse
from django.utils.html import mark_safe
from django.utils.translation import get_language
from django.utils.translation import get_language_bidi
from django.utils.translation import get_language_info
from django_js_reverse.core import _safe_json
from django_js_reverse.core import generate_json
from django_js_reverse.rjsmin import jsmin

import kolibri
from kolibri.core.content.utils.paths import get_content_storage_url
from kolibri.core.content.utils.paths import get_hashi_path
from kolibri.core.content.utils.paths import get_zip_content_base_path
from kolibri.core.content.utils.paths import get_zip_content_config
from kolibri.core.device.utils import allow_other_browsers_to_connect
from kolibri.core.hooks import FrontEndBaseHeadHook
from kolibri.core.hooks import NavigationHook
from kolibri.core.oidc_provider_hook import OIDCProviderHook
from kolibri.core.theme_hook import ThemeHook
Expand All @@ -31,57 +29,6 @@
class FrontEndCoreAppAssetHook(WebpackBundleHook):
bundle_id = "default_frontend"

def url_tag(self):
# Modified from:
# https://github.com/ierror/django-js-reverse/blob/master/django_js_reverse/core.py#L101
js_name = "window.kolibriPluginDataGlobal['{bundle}'].urls".format(
bundle=self.unique_id
)
default_urlresolver = get_resolver(None)

data = generate_json(default_urlresolver)

# Generate the JS that exposes functions to reverse all Django URLs
# in the frontend.
js = render_to_string(
"django_js_reverse/urls_js.tpl",
{"data": _safe_json(data), "js_name": "__placeholder__"},
# For some reason the js_name gets escaped going into the template
# so this was the easiest way to inject it.
).replace("__placeholder__", js_name)
zip_content_origin, zip_content_port = get_zip_content_config()
return [
mark_safe(
"""<script type="text/javascript">"""
# Minify the generated Javascript
+ jsmin(js)
# Add URL references for our base static URL, the Django media URL
# and our content storage URL - this allows us to calculate
# the path at which to access a local file on the frontend if needed.
+ """
{js_name}.__staticUrl = '{static_url}';
{js_name}.__mediaUrl = '{media_url}';
{js_name}.__contentUrl = '{content_url}';
{js_name}.__zipContentUrl = '{zip_content_url}';
{js_name}.__hashiUrl = '{hashi_url}';
{js_name}.__zipContentOrigin = '{zip_content_origin}';
{js_name}.__zipContentPort = '{zip_content_port}';
</script>
""".format(
js_name=js_name,
static_url=settings.STATIC_URL,
media_url=settings.MEDIA_URL,
content_url=get_content_storage_url(
baseurl=OPTIONS["Deployment"]["URL_PATH_PREFIX"]
),
zip_content_url=get_zip_content_base_path(),
hashi_url=get_hashi_path(),
zip_content_origin=zip_content_origin,
zip_content_port=zip_content_port,
)
)
]

def navigation_tags(self):
return [
hook.render_to_page_load_sync_html()
Expand All @@ -96,7 +43,6 @@ def render_to_page_load_sync_html(self):
"""
tags = (
self.plugin_data_tag()
+ self.url_tag()
+ list(self.js_and_css_tags())
+ self.navigation_tags()
)
Expand All @@ -108,6 +54,35 @@ def plugin_data(self):
language_code = get_language()
static_root = static("assets/fonts/noto-full")
full_file = "{}.{}.{}.css?v={}"

default_urlresolver = get_resolver(None)

url_data = generate_json(default_urlresolver)

# Convert the urls key, value pairs to a dictionary
# Turn all dashes in keys into underscores
# This should maintain consistency with our naming, as all namespaces
# are either 'kolibri:core' or 'kolibri:plugin_module_path'
# neither of which can contain dashes.
url_data["urls"] = {
key.replace("-", "_"): value for key, value in url_data["urls"]
}

zip_content_origin, zip_content_port = get_zip_content_config()

url_data.update(
{
"__staticUrl": settings.STATIC_URL,
"__mediaUrl": settings.MEDIA_URL,
"__contentUrl": get_content_storage_url(
baseurl=OPTIONS["Deployment"]["URL_PATH_PREFIX"]
),
"__zipContentUrl": get_zip_content_base_path(),
"__hashiUrl": get_hashi_path(),
"__zipContentOrigin": zip_content_origin,
"__zipContentPort": zip_content_port,
}
)
return {
"fullCSSFileModern": full_file.format(
static_root, language_code, "modern", kolibri.__version__
Expand All @@ -121,6 +96,8 @@ def plugin_data(self):
"languageGlobals": self.language_globals(),
"oidcProviderEnabled": OIDCProviderHook.is_enabled(),
"kolibriTheme": ThemeHook.get_theme(),
"urls": url_data,
"unsupportedUrl": reverse("kolibri:core:unsupported"),
}

def language_globals(self):
Expand Down Expand Up @@ -148,26 +125,14 @@ def language_globals(self):


@register_hook
class FrontendHeadAssetsHook(WebpackBundleHook):
class FrontendHeadAssetsHook(FrontEndBaseHeadHook):
"""
Render these assets in the <head> tag of base.html, before other JS and assets.
"""

bundle_id = "frontend_head_assets"

def render_to_page_load_sync_html(self):
"""
Add in the extra language font file tags needed
for preloading our custom font files.
"""
tags = (
self.plugin_data_tag()
+ self.language_font_file_tags()
+ self.frontend_message_tag()
+ list(self.js_and_css_tags())
)

return mark_safe("\n".join(tags))
@property
def head_html(self):
return mark_safe("\n".join(self.language_font_file_tags()))

def language_font_file_tags(self):
language_code = get_language()
Expand All @@ -187,7 +152,3 @@ def language_font_file_tags(self):
subset_css_file=subset_file, version=kolibri.__version__
),
]

@property
def plugin_data(self):
return {"unsupportedUrl": reverse("kolibri:core:unsupported")}
11 changes: 1 addition & 10 deletions kolibri/core/templates/kolibri/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,6 @@
<meta name="google" content="notranslate">
{% theme_favicon %}
<title>{% site_title %}</title>
{% if LANGUAGE_CODE == "ach-ug" %}
<script type="text/javascript">
var _jipt = [];
_jipt.push(['project', 'kolibri']);
</script>
<script type="text/javascript" src="//cdn.crowdin.com/jipt/jipt.js"></script>
{% endif %}
{% webpack_asset 'kolibri.core.frontend_head_assets' %}
{% frontend_base_head_markup %}
</head>
<body>
Expand Down Expand Up @@ -52,10 +44,9 @@
</div>
</rootvue>
{% block frontend_assets %}
{% content_renderer_assets %}
{% webpack_asset 'kolibri.core.default_frontend' %}
{% frontend_base_assets %}
{% frontend_base_async_assets %}
{% content_renderer_assets %}
{% endblock %}

{% block content %}
Expand Down
13 changes: 0 additions & 13 deletions kolibri/core/templatetags/core_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from django.templatetags.static import static
from django.utils.html import format_html

from kolibri.core.hooks import FrontEndBaseASyncHook
from kolibri.core.hooks import FrontEndBaseHeadHook
from kolibri.core.hooks import FrontEndBaseSyncHook
from kolibri.core.theme_hook import ThemeHook
Expand All @@ -27,18 +26,6 @@ def frontend_base_assets():
return FrontEndBaseSyncHook.html()


@register.simple_tag()
def frontend_base_async_assets():
"""
This is a script tag for all ``FrontEndAssetHook`` hooks that implement a
render_to_html() method - this is used in ``/base.html`` template to
populate any Javascript and CSS that should be loaded at page load.
:return: HTML of script tags to insert into base.html
"""
return FrontEndBaseASyncHook.html()


@register.simple_tag()
def frontend_base_head_markup():
"""
Expand Down
Loading

0 comments on commit d3ff5ec

Please sign in to comment.