Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Kolibri compliant with a secure Content Security Policy #12851

Merged
merged 8 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
bjester marked this conversation as resolved.
Show resolved Hide resolved
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