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 = $('
'); + content_holder.html(widget_elem); + + var widget = widgets[widget_type].activate({ + elem: widget_elem, + callback: callback, + message: message, + extra_data: extra_data, + }); + + // This is hacky, we should just maintain our own list. + message.widget = widget; + message.widget_elem = widget_elem; + + // Replay any events that already happened. (This is common + // when you narrow to a message after other users have already + // interacted with it.) + if (events.length > 0) { + widget.handle_events(events); + } +}; + +exports.handle_event = function (widget_event) { + var message = message_store.get(widget_event.message_id); + + var events = [widget_event]; + + message.widget.handle_events(events); +}; + +return exports; + +}()); +if (typeof module !== 'undefined') { + module.exports = widgetize; +} diff --git a/tools/compile-handlebars-templates b/tools/compile-handlebars-templates index 27ba19d7d0..5006d32443 100755 --- a/tools/compile-handlebars-templates +++ b/tools/compile-handlebars-templates @@ -22,7 +22,8 @@ STATIC_PATH = 'static/' def get_templates(): # type: () -> List[str] return (glob.glob(os.path.join(STATIC_PATH, 'templates/*.handlebars')) + - glob.glob(os.path.join(STATIC_PATH, 'templates/settings/*.handlebars'))) + glob.glob(os.path.join(STATIC_PATH, 'templates/settings/*.handlebars')) + + glob.glob(os.path.join(STATIC_PATH, 'templates/widgets/*.handlebars'))) def run(): # type: () -> None diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py index b786ada982..07b8ecab67 100644 --- a/tools/linter_lib/custom_check.py +++ b/tools/linter_lib/custom_check.py @@ -189,7 +189,7 @@ def build_custom_checkers(by_lang): {'pattern': '\+.*i18n\.t\(.+\)', 'description': 'Do not concatenate i18n strings'}, {'pattern': '[.]html[(]', - 'exclude_pattern': '[.]html[(]("|\'|templates|html|message.content|sub.rendered_description|i18n.t|rendered_|$|[)]|error_text|[$]error|[$][(]"

"[)])', + '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',