diff --git a/.eslintrc.json b/.eslintrc.json index 7b83892ba3..9efcbc9af5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -171,6 +171,7 @@ "unread_ops": false, "upload": false, "user_events": false, + "widgetize": false, "submessage": false, "Plotly": false, "emoji_codes": false, diff --git a/static/js/submessage.js b/static/js/submessage.js index 7adf3cc4fc..45e72f0118 100644 --- a/static/js/submessage.js +++ b/static/js/submessage.js @@ -15,7 +15,6 @@ exports.get_message_events = function (message) { return; } - // The server should sort messages for us, but this is defensive. message.submessages.sort(function (m1, m2) { return parseInt(m1.id, 10) - parseInt(m2.id, 10); }); @@ -45,11 +44,52 @@ exports.process_submessages = function (in_opts) { } blueslip.info('submessages found for message id: ' + message_id); + + var row = in_opts.row; + + // Right now, our only use of submessages is widgets. + + var data = events[0].data; + + if (data === undefined) { + return; + } + + var widget_type = data.widget_type; + + if (widget_type === undefined) { + return; + } + + var post_to_server = exports.make_server_callback(message_id); + + widgetize.activate({ + widget_type: widget_type, + extra_data: data.extra_data, + events: events, + row: row, + message: message, + post_to_server: post_to_server, + }); }; exports.handle_event = function (event) { blueslip.info('handle submessage: ' + JSON.stringify(event)); + + // Right now, our only use of submessages is widgets. + var msg_type = event.msg_type; + + if (msg_type !== 'widget') { + blueslip.warn('unknown msg_type: ' + msg_type); + return; + } + + widgetize.handle_event({ + sender_id: event.sender_id, + message_id: event.message_id, + data: event.data, + }); }; exports.make_server_callback = function (message_id) { diff --git a/static/js/widgetize.js b/static/js/widgetize.js new file mode 100644 index 0000000000..2e78417984 --- /dev/null +++ b/static/js/widgetize.js @@ -0,0 +1,77 @@ +var widgetize = (function () { + +var exports = {}; + +var widgets = {}; + +exports.activate = function (in_opts) { + var widget_type = in_opts.widget_type; + var extra_data = in_opts.extra_data; + var events = in_opts.events; + var row = in_opts.row; + var message = in_opts.message; + var post_to_server = in_opts.post_to_server; + + events.shift(); + + if (!widgets[widget_type]) { + blueslip.warn('unknown widget_type', widget_type); + return; + } + + var content_holder = row.find('.message_content'); + + var widget_elem; + if (message.widget) { + // Use local to work around linter. We can trust this + // value because it comes from a template. + widget_elem = message.widget_elem; + content_holder.html(widget_elem); + return; + } + + var callback = function (data) { + post_to_server({ + msg_type: 'widget', + data: data, + }); + }; + + // We depend on our widgets to use templates to build + // the HTML that will eventually go in this div. + widget_elem = $('
"[)])', + 'exclude_pattern': '[.]html[(]("|\'|templates|html|message.content|sub.rendered_description|i18n.t|rendered_|$|[)]|error_text|widget_elem|[$]error|[$][(]"
"[)])', 'exclude': ['static/js/portico', 'static/js/lightbox.js', 'static/js/ui_report.js', 'frontend_tests/'], 'description': 'Setting HTML content with jQuery .html() can lead to XSS security bugs. Consider .text() or using rendered_foo as a variable name if content comes from handlebars and thus is already sanitized.'}, diff --git a/zproject/settings.py b/zproject/settings.py index a1d5916600..4bda41da92 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -947,6 +947,7 @@ JS_SPECS = { 'js/top_left_corner.js', 'js/stream_list.js', 'js/filter.js', + 'js/widgetize.js', 'js/submessage.js', 'js/fetch_status.js', 'js/message_list_data.js',