Drop tooling to load studio-frontend components into mako templates and XSS testing features related to it.
273 lines
12 KiB
HTML
273 lines
12 KiB
HTML
## coding=utf-8
|
|
## mako
|
|
##
|
|
## Studio view template for rendering the whole Unit in an iframe with
|
|
## XBlocks controls specifically for Authoring MFE. This template renders
|
|
## a chromeless version of a unit container without headers, footers,
|
|
## and a navigation bar.
|
|
|
|
<%! main_css = "style-main-v1" %>
|
|
|
|
<%! course_unit_mfe_iframe_css = "course-unit-mfe-iframe-bundle" %>
|
|
|
|
<%namespace name='static' file='static_content.html'/>
|
|
<%!
|
|
from django.urls import reverse
|
|
from django.utils.translation import gettext as _
|
|
|
|
from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
|
|
from cms.djangoapps.contentstore.helpers import xblock_type_display_name
|
|
from lms.djangoapps.branding import api as branding_api
|
|
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
|
|
from openedx.core.djangolib.js_utils import (
|
|
dump_js_escaped_json, js_escaped_string
|
|
)
|
|
from openedx.core.djangolib.markup import HTML, Text
|
|
from openedx.core.release import RELEASE_LINE
|
|
%>
|
|
|
|
<%page expression_filter="h"/>
|
|
<!doctype html>
|
|
<!--[if lte IE 9]><html class="ie9 lte9" lang="${LANGUAGE_CODE}"><![endif]-->
|
|
<!--[if !IE]><<!--><html lang="${LANGUAGE_CODE}"><!--<![endif]-->
|
|
<head dir="${static.dir_rtl()}">
|
|
<meta charset="utf-8">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
|
<meta name="openedx-release-line" content="${RELEASE_LINE}" />
|
|
<title>
|
|
${xblock.display_name_with_default} ${xblock_type_display_name(xblock)} |
|
|
% if context_course:
|
|
<% ctx_loc = context_course.location %>
|
|
${context_course.display_name_with_default} |
|
|
% elif context_library:
|
|
${context_library.display_name_with_default} |
|
|
% endif
|
|
${settings.STUDIO_NAME}
|
|
</title>
|
|
|
|
<%
|
|
jsi18n_path = "js/i18n/{language}/djangojs.js".format(language=LANGUAGE_CODE)
|
|
%>
|
|
|
|
% if getattr(settings, 'CAPTURE_CONSOLE_LOG', False):
|
|
<script type="text/javascript">
|
|
var oldOnError = window.onerror;
|
|
window.localStorage.setItem('console_log_capture', JSON.stringify([]));
|
|
|
|
window.onerror = function (message, url, lineno, colno, error) {
|
|
if (oldOnError) {
|
|
oldOnError.apply(this, arguments);
|
|
}
|
|
|
|
var messages = JSON.parse(window.localStorage.getItem('console_log_capture'));
|
|
messages.push([message, url, lineno, colno, (error || {}).stack]);
|
|
window.localStorage.setItem('console_log_capture', JSON.stringify(messages));
|
|
}
|
|
</script>
|
|
% endif
|
|
|
|
<script type="text/javascript" src="${static.url(jsi18n_path)}"></script>
|
|
% if settings.DEBUG:
|
|
## Provides a fallback for gettext functions in development environment
|
|
<script type="text/javascript" src="${static.url('js/src/gettext_fallback.js')}"></script>
|
|
% endif
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<meta name="path_prefix" content="${EDX_ROOT_URL}">
|
|
<% favicon_url = branding_api.get_favicon_url() %>
|
|
<link rel="icon" type="image/x-icon" href="${favicon_url}"/>
|
|
<%static:css group='style-vendor'/>
|
|
<%static:css group='style-vendor-tinymce-content'/>
|
|
<%static:css group='style-vendor-tinymce-skin'/>
|
|
|
|
% if uses_bootstrap:
|
|
<link rel="stylesheet" href="${static.url(self.attr.main_css)}" type="text/css" media="all" />
|
|
% else:
|
|
<%static:css group='${self.attr.main_css}'/>
|
|
% endif
|
|
|
|
<%static:css group='${self.attr.course_unit_mfe_iframe_css}'/>
|
|
|
|
<%include file="widgets/segment-io.html" />
|
|
|
|
% for template_name in templates:
|
|
<script type="text/template" id="${template_name}-tpl">
|
|
<%static:include path="js/${template_name}.underscore" />
|
|
</script>
|
|
% endfor
|
|
<script type="text/template" id="image-modal-tpl">
|
|
<%static:include path="common/templates/image-modal.underscore" />
|
|
</script>
|
|
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
|
</head>
|
|
|
|
<body class="${static.dir_rtl()} is-signedin course container view-container lang_${LANGUAGE_CODE}">
|
|
|
|
<a class="nav-skip" href="#main">${_("Skip to main content")}</a>
|
|
|
|
<%static:js group='base_vendor'/>
|
|
|
|
<%static:webpack entry="commons"/>
|
|
|
|
<script type="text/javascript">
|
|
window.baseUrl = "${settings.STATIC_URL | n, js_escaped_string}";
|
|
require.config({
|
|
baseUrl: window.baseUrl
|
|
});
|
|
</script>
|
|
|
|
<script type="text/javascript" src="${static.url("cms/js/require-config.js")}"></script>
|
|
|
|
<!-- view -->
|
|
<div class="wrapper wrapper-view" dir="${static.dir_rtl()}">
|
|
<%
|
|
banner_messages = list(PageLevelMessages.user_messages(request))
|
|
%>
|
|
|
|
% if banner_messages:
|
|
<div class="page-banner">
|
|
<div class="user-messages">
|
|
% for message in banner_messages:
|
|
<div class="alert ${message.css_class}" role="alert">
|
|
<span class="icon icon-alert fa ${message.icon_class}" aria-hidden="true"></span>
|
|
${HTML(message.message_html)}
|
|
</div>
|
|
% endfor
|
|
</div>
|
|
</div>
|
|
% endif
|
|
|
|
<main id="main" aria-label="Content" tabindex="-1">
|
|
<div id="content">
|
|
|
|
<div class="wrapper-content wrapper">
|
|
<div class="inner-wrapper">
|
|
<section class="content-area">
|
|
<article class="content-primary ${'content-primary-fullwidth' if is_fullwidth_content else ''}">
|
|
<%
|
|
assets_url = reverse('assets_handler', kwargs={'course_key_string': str(xblock_locator.course_key)})
|
|
%>
|
|
<section class="wrapper-xblock level-page is-hidden studio-xblock-wrapper" data-locator="${xblock_locator}" data-course-key="${xblock_locator.course_key}" data-course-assets="${assets_url}">
|
|
</section>
|
|
<div class="ui-loading">
|
|
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<div id="page-prompt"></div>
|
|
|
|
% if context_course:
|
|
<%static:webpack entry="js/factories/context_course"/>
|
|
<script type="text/javascript">
|
|
window.course = new ContextCourse({
|
|
id: "${context_course.id | n, js_escaped_string}",
|
|
name: "${context_course.display_name_with_default | n, js_escaped_string}",
|
|
url_name: "${context_course.location.block_id | n, js_escaped_string}",
|
|
org: "${context_course.location.org | n, js_escaped_string}",
|
|
num: "${context_course.location.course | n, js_escaped_string}",
|
|
display_course_number: "${context_course.display_coursenumber | n, js_escaped_string}",
|
|
revision: "${context_course.location.branch | n, js_escaped_string}",
|
|
self_paced: ${ context_course.self_paced | n, dump_js_escaped_json },
|
|
is_custom_relative_dates_active: ${CUSTOM_RELATIVE_DATES.is_enabled(context_course.id) | n, dump_js_escaped_json},
|
|
start: ${context_course.start | n, dump_js_escaped_json},
|
|
discussions_settings: ${context_course.discussions_settings | n, dump_js_escaped_json}
|
|
});
|
|
</script>
|
|
% endif
|
|
<script type="text/javascript">
|
|
require(['js/factories/base'], function () {});
|
|
</script>
|
|
|
|
<%static:webpack entry="js/factories/container">
|
|
ContainerFactory(
|
|
${component_templates | n, dump_js_escaped_json},
|
|
${xblock_info | n, dump_js_escaped_json},
|
|
"${action | n, js_escaped_string}",
|
|
{
|
|
isUnitPage: ${is_unit_page | n, dump_js_escaped_json},
|
|
canEdit: true,
|
|
outlineURL: "${outline_url | n, js_escaped_string}",
|
|
clipboardData: ${user_clipboard | n, dump_js_escaped_json},
|
|
isIframeEmbed: true,
|
|
libraryContentPickerUrl: "${library_content_picker_url | n, js_escaped_string}",
|
|
}
|
|
);
|
|
</%static:webpack>
|
|
</body>
|
|
|
|
## Initialize MutationObserver and ResizeObserver to update the iframe size.
|
|
## These are used to provide resize events for the Authoring MFE.
|
|
<script type="text/javascript">
|
|
(function() {
|
|
// If this view is rendered in an iframe within the authoring microfrontend app
|
|
// it will report the height of its contents to the parent window when the
|
|
// document loads, window resizes, or DOM mutates.
|
|
if (window !== window.parent) {
|
|
var lastHeight = window.offsetHeight;
|
|
var lastWidth = window.offsetWidth;
|
|
var contentElement = document.getElementById('content');
|
|
|
|
function dispatchResizeMessage(event) {
|
|
// Note: event is actually an Array of MutationRecord objects when fired from the MutationObserver
|
|
var isLoadEvent = event.type === 'load';
|
|
var newHeight = contentElement.offsetHeight;
|
|
var newWidth = contentElement.offsetWidth;
|
|
|
|
// Monitor for messages and checks if the message contains an id. If
|
|
// there is an id, then the location of the selected focus element
|
|
// is sent through its offset attribute. The offset will allow the
|
|
// page to scroll to the location of the focus element so that it is
|
|
// at the top of the page. Unique ids and names are required for
|
|
// proper scrolling.
|
|
window.addEventListener('message', function (event) {
|
|
if (event.data.hashName) {
|
|
var targetId = event.data.hashName;
|
|
var targetName = event.data.hashName.slice(1);
|
|
// Checks if the target uses an id or name to focus and gets offset.
|
|
var targetOffset = $(targetId).offset() || $(document.getElementsByName(targetName)[0]).offset();
|
|
window.parent.postMessage({ 'offset': targetOffset.top }, document.referrer);
|
|
}
|
|
})
|
|
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'plugin.resize',
|
|
payload: {
|
|
width: newWidth,
|
|
height: newHeight,
|
|
}
|
|
}, document.referrer
|
|
);
|
|
|
|
lastHeight = newHeight;
|
|
lastWidth = newWidth;
|
|
|
|
// Within the authoring microfrontend the iframe resizes to match the
|
|
// height of this document and it should never scroll. It does scroll
|
|
// ocassionally when javascript is used to focus elements on the page
|
|
// before the parent iframe has been resized to match the content
|
|
// height. This window.scrollTo is an attempt to keep the content at the
|
|
// top of the page.
|
|
window.scrollTo(0, 0);
|
|
}
|
|
|
|
// Create an observer instance linked to the callback function
|
|
const observer = new MutationObserver(dispatchResizeMessage);
|
|
|
|
// Start observing the target node for configured mutations
|
|
observer.observe(document.body, { attributes: true, childList: true, subtree: true });
|
|
|
|
window.addEventListener('load', dispatchResizeMessage);
|
|
|
|
const resizeObserver = new ResizeObserver(dispatchResizeMessage);
|
|
resizeObserver.observe(document.body);
|
|
}
|
|
}());
|
|
</script>
|
|
</html>
|