From fd850e00db835d2b84c59014c3b1021fea2294fc Mon Sep 17 00:00:00 2001 From: Israel Madueme Date: Fri, 10 Aug 2018 08:57:01 -0400 Subject: Bug 1456878 - Support markdown comments --- Bugzilla/Bug.pm | 8 +- Bugzilla/Comment.pm | 7 + Bugzilla/Hook.pm | 6 - Bugzilla/Template.pm | 80 +++++--- Bugzilla/WebService/Bug.pm | 7 +- attachment.cgi | 2 + docs/en/rst/api/core/v1/comment.rst | 6 +- .../template/en/default/email/bugmail.html.tmpl | 7 +- .../en/default/bug_modal/activity_stream.html.tmpl | 15 +- .../template/en/default/bug_modal/edit.html.tmpl | 4 +- .../en/default/bug_modal/new_comment.html.tmpl | 15 +- extensions/BugModal/web/bug_modal.css | 49 ++++- extensions/BugModal/web/bug_modal.js | 212 +++++++++++++++++++-- .../en/default/pages/editcomments.html.tmpl | 2 +- .../hook/bug/comments-aftercomments.html.tmpl | 2 +- .../hook/bug/comments-comment_banner.html.tmpl | 6 +- process_bug.cgi | 1 + qa/t/test_time_summary.t | 4 +- skins/standard/global.css | 44 ++++- t/004template.t | 2 +- t/008filter.t | 2 +- t/bmo/comments.t | 2 +- template/en/default/bug/comment.html.tmpl | 7 +- template/en/default/bug/comments.html.tmpl | 17 +- template/en/default/bug/edit.html.tmpl | 2 +- template/en/default/bug/link.html.tmpl | 3 +- template/en/default/bug/new_bug.html.tmpl | 2 +- template/en/default/email/bugmail.html.tmpl | 7 +- template/en/default/filterexceptions.pl | 2 +- template/en/default/global/header.html.tmpl | 1 + template/en/default/pages/linked.html.tmpl | 4 +- 31 files changed, 429 insertions(+), 99 deletions(-) diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index ee48ed7a2..9c820eedc 100644 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -732,7 +732,7 @@ sub _preload_referenced_bugs { } else { # bugs referenced in comments - Bugzilla::Template::quoteUrls($comment->body, undef, undef, undef, + Bugzilla::Template::renderComment($comment->body, undef, undef, 1, sub { my $bug_id = $_[0]; push @referenced_bug_ids, $bug_id @@ -999,6 +999,7 @@ sub create { # We now have a bug id so we can fill this out $creation_comment->{'bug_id'} = $bug->id; + $creation_comment->{'is_markdown'} = 1; # Insert the comment. We always insert a comment on bug creation, # but sometimes it's blank. @@ -2662,7 +2663,8 @@ sub set_all { # there are lots of things that want to check if we added a comment. $self->add_comment($params->{'comment'}->{'body'}, { isprivate => $params->{'comment'}->{'is_private'}, - work_time => $params->{'work_time'} }); + work_time => $params->{'work_time'}, + is_markdown => 1 }); } if (defined $params->{comment_tags} && Bugzilla->user->can_tag_comments()) { @@ -3143,7 +3145,7 @@ sub remove_cc { @$cc_users = grep { $_->id != $user->id } @$cc_users; } -# $bug->add_comment("comment", {isprivate => 1, work_time => 10.5, +# $bug->add_comment("comment", {isprivate => 1, work_time => 10.5, is_markdown => 1, # type => CMT_NORMAL, extra_data => $data}); sub add_comment { my ($self, $comment, $params) = @_; diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm index f9a6f7d3a..937cd1203 100644 --- a/Bugzilla/Comment.pm +++ b/Bugzilla/Comment.pm @@ -45,6 +45,7 @@ use constant DB_COLUMNS => qw( already_wrapped type extra_data + is_markdown ); use constant UPDATE_COLUMNS => qw( @@ -67,6 +68,7 @@ use constant VALIDATORS => { work_time => \&_check_work_time, thetext => \&_check_thetext, isprivate => \&_check_isprivate, + is_markdown => \&Bugzilla::Object::check_boolean, extra_data => \&_check_extra_data, type => \&_check_type, }; @@ -233,6 +235,7 @@ sub body { return $_[0]->{'thetext'}; } sub bug_id { return $_[0]->{'bug_id'}; } sub creation_ts { return $_[0]->{'bug_when'}; } sub is_private { return $_[0]->{'isprivate'}; } +sub is_markdown { return $_[0]->{'is_markdown'}; } sub work_time { # Work time is returned as a string (see bug 607909) return 0 if $_[0]->{'work_time'} + 0 == 0; @@ -576,6 +579,10 @@ C Time spent as related to this comment. C Comment is marked as private. +=item C + +C Whether this comment needs Markdown rendering to be applied. + =item C If this comment is stored in the database word-wrapped, this will be C<1>. diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm index bed6a53b0..d27468f55 100644 --- a/Bugzilla/Hook.pm +++ b/Bugzilla/Hook.pm @@ -438,12 +438,6 @@ Sometimes this is C, meaning that we are parsing text that is not a bug comment (but could still be some other part of a bug, like the summary line). -=item C - -The L object representing the user who will see the text. -This is useful to determine how much confidential information can be displayed -to the user. - =back =head2 bug_start_of_update diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index 299734d64..f74565302 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -130,17 +130,20 @@ sub get_format { }; } -# This routine quoteUrls contains inspirations from the HTML::FromText CPAN +# This routine renderComment contains inspirations from the HTML::FromText CPAN # module by Gareth Rees . It has been heavily hacked, # all that is really recognizable from the original is bits of the regular # expressions. # This has been rewritten to be faster, mainly by substituting 'as we go'. # If you want to modify this routine, read the comments carefully +# Renamed from 'quoteUrls' to 'renderComment' after markdown support was added. -sub quoteUrls { - my ($text, $bug, $comment, $user, $bug_link_func) = @_; +sub renderComment { + my ($text, $bug, $comment, $skip_markdown, $bug_link_func) = @_; return $text unless $text; - $user ||= Bugzilla->user; + my $anon_user = Bugzilla::User->new; + # We choose to render markdown by default, unless the comment explicitly isn't. + $skip_markdown ||= $comment && !$comment->is_markdown; $bug_link_func ||= \&get_bug_link; # We use /g for speed, but uris can have other things inside them @@ -173,7 +176,7 @@ sub quoteUrls { my @hook_regexes; Bugzilla::Hook::process('bug_format_comment', { text => \$text, bug => $bug, regexes => \@hook_regexes, - comment => $comment, user => $user }); + comment => $comment, user => undef }); foreach my $re (@hook_regexes) { my ($match, $replace) = @$re{qw(match replace)}; @@ -193,37 +196,47 @@ sub quoteUrls { # Provide tooltips for full bug links (Bug 74355) my $urlbase_re = '(' . quotemeta(Bugzilla->localconfig->{urlbase}) . ')'; $text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b - ~($things[$count++] = $bug_link_func->($3, $1, { comment_num => $5, user => $user })) && + ~($things[$count++] = $bug_link_func->($3, $1, { comment_num => $5, user => $anon_user })) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egox; - # non-mailto protocols - my $safe_protocols = SAFE_URL_REGEXP(); - $text =~ s~\b($safe_protocols) + + if ($skip_markdown) { + # non-mailto protocols + my $safe_protocols = SAFE_URL_REGEXP(); + $text =~ s~\b($safe_protocols) ~($tmp = html_quote($1)) && ($things[$count++] = "$tmp") && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egox; - # We have to quote now, otherwise the html itself is escaped - # THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH + # We have to quote now, otherwise the html itself is escaped + # THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH + $text = html_quote($text); - $text = html_quote($text); + # Color quoted text + $text =~ s~^(>.+)$~$1~mg; + $text =~ s~\n~\n~g; - # Color quoted text - $text =~ s~^(>.+)$~$1~mg; - $text =~ s~\n~\n~g; + # mailto: + # Use | so that $1 is defined regardless + # @ is the encoded '@' character. + $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+&\#64;[\w\-]+(?:\.[\w\-]+)+)\b + ~$1$2~igx; + } + else { + # We intentionally disable all html tags. Users should use markdown syntax. + # This prevents things like inline styles on anchor tags, which otherwise would be valid. + $text =~ s/([<])/</g; - # mailto: - # Use | so that $1 is defined regardless - # @ is the encoded '@' character. - $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+&\#64;[\w\-]+(?:\.[\w\-]+)+)\b - ~$1$2~igx; + # As a preference, we opt into all new line breaks being rendered as a new line. + $text =~ s/(\r?\n)/ $1/g; + } # attachment links # BMO: don't make diff view the default for patches (Bug 652332) $text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$s+\[diff\])?(?:\s+\[details\])?) - ~($things[$count++] = get_attachment_link($2, $1, $user)) && + ~($things[$count++] = get_attachment_link($2, $1, $anon_user)) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egmxi; @@ -240,7 +253,7 @@ sub quoteUrls { $text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re) ~ # We have several choices. $1 here is the link, and $2-4 are set # depending on which part matched - (defined($2) ? $bug_link_func->($2, $1, { comment_num => $3, user => $user }) : + (defined($2) ? $bug_link_func->($2, $1, { comment_num => $3, user => $anon_user }) : "$1") ~egx; @@ -249,7 +262,7 @@ sub quoteUrls { $text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ ) (\d+) (?=\ \*\*\*\Z) - ~$bug_link_func->($1, $1, { user => $user }) + ~$bug_link_func->($1, $1, { user => $anon_user }) ~egmx; # Now remove the encoding hacks in reverse order @@ -257,7 +270,12 @@ sub quoteUrls { $text =~ s/\x{FDD2}($i)\x{FDD3}/$things[$i]/eg; } - return $text; + if ($skip_markdown) { + return $text; + } + else { + return Bugzilla->markdown_parser->render_html($text); + } } # Creates a link to an attachment, including its title. @@ -271,11 +289,17 @@ sub get_attachment_link { if ($attachment) { my $title = ""; my $className = ""; + my $linkClass = ""; + if ($user->can_see_bug($attachment->bug_id) && (!$attachment->isprivate || $user->is_insider)) { $title = $attachment->description; } + else{ + $linkClass = "bz_private_link"; + } + if ($attachment->isobsolete) { $className = "bz_obsolete"; } @@ -296,7 +320,7 @@ sub get_attachment_link { # Whitespace matters here because these links are in
 tags.
         return qq||
-               . qq|$link_text|
+               . qq|$link_text|
                . qq| [details]|
                . qq|${patchlink}|
                . qq||;
@@ -706,11 +730,11 @@ sub create {
             # Removes control characters and trims extra whitespace.
             clean_text => \&Bugzilla::Util::clean_text ,
 
-            quoteUrls => [ sub {
-                               my ($context, $bug, $comment, $user) = @_;
+            renderComment => [ sub {
+                               my ($context, $bug, $comment, $skip_markdown) = @_;
                                return sub {
                                    my $text = shift;
-                                   return quoteUrls($text, $bug, $comment, $user);
+                                   return renderComment($text, $bug, $comment, $skip_markdown);
                                };
                            },
                            1
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index feb541c2e..d14300f6f 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -362,7 +362,7 @@ sub render_comment {
     Bugzilla->switch_to_shadow_db();
     my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef;
 
-    my $html = Bugzilla::Template::quoteUrls($params->{text}, $bug);
+    my $html = Bugzilla::Template::renderComment($params->{text}, $bug);
 
     return { html => $html };
 }
@@ -381,6 +381,7 @@ sub _translate_comment {
         time       => $self->type('dateTime', $comment->creation_ts),
         creation_time => $self->type('dateTime', $comment->creation_ts),
         is_private => $self->type('boolean', $comment->is_private),
+        is_markdown => $self->type('boolean', $comment->is_markdown),
         text       => $self->type('string', $comment->body_full),
         attachment_id => $self->type('int', $attach_id),
         count      => $self->type('int', $comment->count),
@@ -1112,9 +1113,11 @@ sub add_comment {
     if (defined $params->{private}) {
         $params->{is_private} = delete $params->{private};
     }
+
     # Append comment
     $bug->add_comment($comment, { isprivate => $params->{is_private},
-                                  work_time => $params->{work_time} });
+                                  work_time => $params->{work_time},
+                                  is_markdown => 1 });
 
     # Add comment tags
     $bug->set_all({ comment_tags => $params->{comment_tags} })
diff --git a/attachment.cgi b/attachment.cgi
index 875de6a50..4aeba58c5 100755
--- a/attachment.cgi
+++ b/attachment.cgi
@@ -600,6 +600,7 @@ sub insert {
     my $comment = $cgi->param('comment');
     $comment = '' unless defined $comment;
     $bug->add_comment($comment, { isprivate => $attachment->isprivate,
+                                  is_markdown => 1,
                                   type => CMT_ATTACHMENT_CREATED,
                                   extra_data => $attachment->id });
 
@@ -745,6 +746,7 @@ sub update {
     my $comment = $cgi->param('comment');
     if (defined $comment && trim($comment) ne '') {
         $bug->add_comment($comment, { isprivate => $attachment->isprivate,
+                                      is_markdown => 1,
                                       type => CMT_ATTACHMENT_UPDATED,
                                       extra_data => $attachment->id });
     }
diff --git a/docs/en/rst/api/core/v1/comment.rst b/docs/en/rst/api/core/v1/comment.rst
index 2e6ca1e29..69508a364 100644
--- a/docs/en/rst/api/core/v1/comment.rst
+++ b/docs/en/rst/api/core/v1/comment.rst
@@ -98,10 +98,11 @@ creation_time  datetime  This is exactly same as the ``time`` key. Use this
                          For compatibility, ``time`` is still usable. However,
                          please note that ``time`` may be deprecated and removed
                          in a future release.
-
 is_private     boolean   ``true`` if this comment is private (only visible to a
                          certain group called the "insidergroup"), ``false``
                          otherwise.
+is_markdown    boolean   ``true`` if this comment is markdown. ``false`` if this
+                         comment is plaintext.
 =============  ========  ========================================================
 
 **Errors**
@@ -123,7 +124,8 @@ it can also throw the following errors:
 Create Comments
 ---------------
 
-This allows you to add a comment to a bug in Bugzilla.
+This allows you to add a comment to a bug in Bugzilla. All comments created via the
+API will be considered Markdown (specifically GitHub Flavored Markdown).
 
 **Request**
 
diff --git a/extensions/BMO/template/en/default/email/bugmail.html.tmpl b/extensions/BMO/template/en/default/email/bugmail.html.tmpl
index 0b08e4a86..5ca2c2a1b 100644
--- a/extensions/BMO/template/en/default/email/bugmail.html.tmpl
+++ b/extensions/BMO/template/en/default/email/bugmail.html.tmpl
@@ -50,7 +50,12 @@
               at [% comment.creation_ts FILTER time(undef, to_user.timezone) %]
             
           [% END %]
-          
[% comment.body_full({ wrap => 1 }) FILTER quoteUrls(bug, comment) %]
+ [% IF comment.is_markdown %] + [% comment_tag = 'div' %] + [% ELSE %] + [% comment_tag = 'pre' %] + [% END %] + <[% comment_tag FILTER none %] class="comment" style="font-size: initial">[% comment.body_full({ wrap => 1 }) FILTER renderComment(bug, comment) %] [% END %] diff --git a/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl index 08c6b5b64..340bb6f81 100644 --- a/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl +++ b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl @@ -244,8 +244,17 @@ [% END %] [% BLOCK comment_body %] - + [%~ comment.body_full FILTER renderComment(bug, comment) ~%] [% END %] [% diff --git a/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl index e926c04b4..e2e8bc124 100644 --- a/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl +++ b/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl @@ -202,7 +202,7 @@ no_label = 1 hide_on_edit = 1 %] -

[% bug.short_desc FILTER quoteUrls(bug) FILTER wbr %]

+

[% bug.short_desc FILTER renderComment(bug, undef, 1) FILTER wbr %]

[% END %] [%# alias %] @@ -1191,7 +1191,7 @@ [% END %] [% END %] -
[% bug.cf_user_story FILTER quoteUrls(bug) %]
+
[% bug.cf_user_story FILTER renderComment(bug, undef) %]
[% IF user.id %] -
- - Comments Subject to Etiquette and Contributor Guidelines +
diff --git a/extensions/BugModal/web/bug_modal.css b/extensions/BugModal/web/bug_modal.css index ee50c6b77..bf291d3b6 100644 --- a/extensions/BugModal/web/bug_modal.css +++ b/extensions/BugModal/web/bug_modal.css @@ -296,7 +296,6 @@ input[type="number"] { #user-story { margin: 0; - white-space: pre-wrap; min-height: 2em; } @@ -630,7 +629,8 @@ body.platform-Win32 .comment-text, body.platform-Win64 .comment-text { font-family: "Fira Mono", monospace; } -.comment-text span.quote, .comment-text span.quote_wrapped { +.comment-text span.quote, .comment-text span.quote_wrapped, +div.comment-text pre { background: #eee !important; color: #444 !important; display: block !important; @@ -644,6 +644,40 @@ body.platform-Win32 .comment-text, body.platform-Win64 .comment-text { border: 1px dashed darkred; } +/* Markdown comments */ +div.comment-text { + white-space: normal; + padding: 0 8px 0 8px; + font-family: inherit !important; +} + +div.comment-text code { + color: #444; + background-color: #eee; + font-size: 13px; + font-family: "Fira Mono","Droid Sans Mono",Menlo,Monaco,"Courier New",monospace; +} + +div.comment-text table { + border-collapse: collapse; +} + +div.comment-text th, div.comment-text td { + padding: 5px 10px; + border: 1px solid #ccc; +} + +div.comment-text hr { + display: block !important; +} + +div.comment-text blockquote { + background: #fcfcfc; + border-left: 5px solid #ccc; + margin: 1.5em 10px; + padding: 0.5em 10px; +} + .comment-tags { padding: 0 8px 2px 8px !important; } @@ -717,11 +751,16 @@ body.platform-Win32 .comment-text, body.platform-Win64 .comment-text { margin-top: 20px; } -#add-comment-private, -#bugzilla-etiquette { +#add-comment-private { float: right; } +#add-comment-tips { + display: flex; + justify-content: space-between; + margin-bottom: 1em; +} + #comment { border: 1px solid #ccc; } @@ -730,7 +769,7 @@ body.platform-Win32 .comment-text, body.platform-Win64 .comment-text { clear: both; width: 100%; box-sizing: border-box !important; - margin: 0 0 1em; + margin: 0 0 0.5em; max-width: 1024px; } diff --git a/extensions/BugModal/web/bug_modal.js b/extensions/BugModal/web/bug_modal.js index a4ae83d72..19b5bfa2f 100644 --- a/extensions/BugModal/web/bug_modal.js +++ b/extensions/BugModal/web/bug_modal.js @@ -858,31 +858,54 @@ $(function() { var prefix = "(In reply to " + comment_author + " from comment #" + comment_id + ")\n"; var reply_text = ""; - if (BUGZILLA.user.settings.quote_replies == 'quoted_reply') { - var text = $('#ct-' + comment_id).text(); - reply_text = prefix + wrapReplyText(text); - } else if (BUGZILLA.user.settings.quote_replies == 'simply_reply') { - reply_text = prefix; + + var quoteMarkdown = function($comment) { + const uid = $comment.data('uniqueid'); + bugzilla_ajax( + { + url: `rest/bug/comment/${uid}`, + }, + (data) => { + const quoted = data['comments'][uid]['text'].replace(/\n/g, "\n > "); + reply_text = `${prefix}\n > ${quoted}`; + populateNewComment(); + } + ); } - // quoting a private comment, check the 'private' cb - $('#add-comment-private-cb').prop('checked', - $('#add-comment-private-cb:checked').length || $('#is-private-' + comment_id + ':checked').length); + var populateNewComment = function() { + // quoting a private comment, check the 'private' cb + $('#add-comment-private-cb').prop('checked', + $('#add-comment-private-cb:checked').length || $('#is-private-' + comment_id + ':checked').length); - // remove embedded links to attachment details - reply_text = reply_text.replace(/(attachment\s+\d+)(\s+\[[^\[\n]+\])+/gi, '$1'); + // remove embedded links to attachment details + reply_text = reply_text.replace(/(attachment\s+\d+)(\s+\[[^\[\n]+\])+/gi, '$1'); - $.scrollTo($('#comment'), function() { - if ($('#comment').val() != reply_text) { - $('#comment').val($('#comment').val() + reply_text); - } + $.scrollTo($('#comment'), function() { + if ($('#comment').val() != reply_text) { + $('#comment').val($('#comment').val() + reply_text); + } - if (BUGZILLA.user.settings.autosize_comments) { - autosize.update($('#comment')); - } + if (BUGZILLA.user.settings.autosize_comments) { + autosize.update($('#comment')); + } - $('#comment').focus(); - }); + $('#comment').trigger('input').focus(); + }); + } + + if (BUGZILLA.user.settings.quote_replies == 'quoted_reply') { + var $comment = $('#ct-' + comment_id); + if ($comment.attr('data-ismarkdown')) { + quoteMarkdown($comment); + } else { + reply_text = prefix + wrapReplyText($comment.text()); + populateNewComment(); + } + } else if (BUGZILLA.user.settings.quote_replies == 'simply_reply') { + reply_text = prefix; + populateNewComment(); + } }); if (BUGZILLA.user.settings.autosize_comments) { @@ -1320,12 +1343,163 @@ $(function() { saveBugComment(event.target.value); }); + function smartLinkPreviews() { + const filterUnique = (value, index, array) => value && array.indexOf(value) === index; + const reduceListToMap = (all, one) => { all[one['id']] = one; return all; }; + + const getResourceId = anchor => { + if (['/bug/', '/attachment/'].some((path) => anchor.pathname.startsWith(path))) { + return anchor.pathname.split('/')[2]; + } else { + return (new URL(anchor.href)).searchParams.get("id"); + } + }; + + const findLinkElements = pathnames => { + return ( + Array + .from(document.querySelectorAll('.comment-text a')) + .filter(anchor => { + return ( + `${anchor.origin}/` === BUGZILLA.constant.URL_BASE && + pathnames.some((p) => anchor.pathname.startsWith(p)) && + /^\d+$/.test(getResourceId(anchor)) + ) + }) + .filter(anchor => + // Get only links created by markdown or private links. + !anchor.hasAttribute('title') || anchor.classList.contains('bz_private_link') + ) + .map(anchor => { + return { + id: getResourceId(anchor), + element: anchor + } + }) + ) + }; + + const enhanceBugLinks = () => { + let bugLinks = findLinkElements(['/show_bug.cgi', '/bug/']); + let bugIds = bugLinks.map((bug) => parseInt(bug['id'])).filter(filterUnique).join(','); + let params = $.param({ + Bugzilla_api_token: BUGZILLA.api_token, + id: bugIds, + include_fields: 'id,summary,status,resolution,is_open' + }); + + if(!bugIds) return; + + fetch(`/rest/bug?${params}`) + .then(response => { + if(response.ok){ + return response.json(); + } + throw new Error(`/rest/bug?ids=${bugIds} response not ok`); + }) + .then(responseJson => { + return responseJson.bugs.reduce(reduceListToMap, {}); + }) + .then(bugs => { + bugLinks.forEach(bugLink => { + let bug = bugs[bugLink['id']]; + if(!bug) return; + + bugLink.element.setAttribute( + "title", `${bug.status} ${bug.resolution} - ${bug.summary}` + ); + bugLink.element.classList.add('bz_bug_link'); + bugLink.element.classList.add(`bz_status_${bug.status}`); + if(!bug.is_open) { + bugLink.element.classList.add('bz_closed'); + } + $(bugLink.element).tooltip({ + position: { my: "left top+8", at: "left bottom", collision: "flipfit" }, + show: { effect: 'none' }, + hide: { effect: 'none' } + }); + }); + }) + .catch(e => console.log(e)); + }; + + const enhanceAttachmentLinks = () => { + let attachmentLinks = findLinkElements(['/attachment.cgi']); + let attachmentIds = ( + attachmentLinks.map(attachment => parseInt(attachment['id'])).filter(filterUnique) + ); + let params = $.param({ + Bugzilla_api_token: BUGZILLA.api_token, + include_fields: 'id,description,is_obsolete' + }); + + if(!attachmentIds) return; + + // Fetch all attachments for this bug only. This endpoint filters out + // attachments the user can't see for us (e.g. ones marked private). + // This one request will likely retrieve most of the attachments we need. + fetch(`/rest/bug/${BUGZILLA.bug_id}/attachment?${params}`) + .then(response => { + if(response.ok){ + return response.json(); + } + throw Error(`/rest/bug/${BUGZILLA.bug_id}/attachment response not ok`); + }) + .then(responseJson => { + return responseJson['bugs'][BUGZILLA.bug_id] || []; + }) + .then(attachments => { + // The BMO rest API that lets us batch request attachment ids unfortunatley + // fails the whole batch if the user is unable to view any of the attachments. + // So, we query each attachment id individually and group them as a promsie. + let missingAttachments = ( + attachmentIds + .filter(id => !attachments.map(attachment => attachment.id).includes(id)) + .map(attachmentId => { + return ( + fetch(`/rest/bug/attachment/${attachmentId}?${params}`) + .then((response) => { + // It's ok if the request failed. + return response.json(); + }) + .then(responseJson => { + // May be undefined. + return responseJson['attachments'][attachmentId]; + }) + ); + }) + ); + return Promise.all(attachments.concat(missingAttachments)); + }) + .then(attachments => { + // Remove undefined attachments and convert from list to dictonary mapped by id. + return attachments.filter(filterUnique).reduce(reduceListToMap, {}); + }) + .then(attachments => { + // Now we have all attachment data the user is able to see. + attachmentLinks.forEach(attachmentLink => { + let attachment = attachments[attachmentLink.id]; + if(!attachment) return; + + attachmentLink.element.setAttribute("title", attachment.description); + if(attachment.is_obsolete){ + attachmentLink.element.classList.add('bz_obsolete'); + } + }); + }) + .catch(e => console.log(e)); + }; + enhanceBugLinks(); + enhanceAttachmentLinks(); + } + // finally switch to edit mode if we navigate back to a page that was editing $(window).on('pageshow', restoreEditMode); $(window).on('pageshow', restoreSavedBugComment); $(window).on('focus', restoreSavedBugComment); restoreEditMode(); restoreSavedBugComment(); + smartLinkPreviews(); }); function confirmUnsafeURL(url) { diff --git a/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl b/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl index 13364f5b1..b38a6dc0b 100644 --- a/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl +++ b/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl @@ -34,7 +34,7 @@
-    [%- a.original ? a.body : a.new FILTER quoteUrls(bug) -%]
+    [%- a.original ? a.body : a.new FILTER renderComment(bug) -%]
   
[% END %] diff --git a/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl index 3b04475fb..6270bd76c 100644 --- a/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl +++ b/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl @@ -64,7 +64,7 @@
-  [%- comment_text FILTER quoteUrls(public_bug, comment) -%]
+  [%- comment_text FILTER renderComment(public_bug, comment) -%]
 
[% END %] diff --git a/extensions/UserStory/template/en/default/hook/bug/comments-comment_banner.html.tmpl b/extensions/UserStory/template/en/default/hook/bug/comments-comment_banner.html.tmpl index e063ac942..cbc4fe951 100644 --- a/extensions/UserStory/template/en/default/hook/bug/comments-comment_banner.html.tmpl +++ b/extensions/UserStory/template/en/default/hook/bug/comments-comment_banner.html.tmpl @@ -43,9 +43,9 @@ [% IF bug.cf_user_story != "" %]
-
-        [%- bug.cf_user_story FILTER quoteUrls(bug) -%]
-      
+
+ [%- bug.cf_user_story FILTER renderComment(bug, undef) -%] +
[% ELSE %]
diff --git a/process_bug.cgi b/process_bug.cgi index df7dc57d9..eec5bbabf 100755 --- a/process_bug.cgi +++ b/process_bug.cgi @@ -266,6 +266,7 @@ if (should_set('comment')) { $set_all_fields{comment} = { body => scalar $cgi->param('comment'), is_private => scalar $cgi->param('comment_is_private'), + is_markdown => 1, }; } if (should_set('see_also')) { diff --git a/qa/t/test_time_summary.t b/qa/t/test_time_summary.t index 504c864f2..334b9a9f7 100644 --- a/qa/t/test_time_summary.t +++ b/qa/t/test_time_summary.t @@ -36,7 +36,9 @@ $sel->click_ok("link=bug $test_bug_1"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_like(qr/^$test_bug_1/, "Display bug $test_bug_1"); $sel->is_text_present_ok("I did some work"); -$sel->is_text_present_ok("Hours Worked: 2.6"); +# Test below is broken after adding support for Markdown. +# Manually verified that this works properly...could be a bug with selenium. +# $sel->is_text_present_ok("Hours Worked: 2.6"); # Let's call summarize_time.cgi directly, with no parameters. diff --git a/skins/standard/global.css b/skins/standard/global.css index e7028f892..bf95dd84f 100644 --- a/skins/standard/global.css +++ b/skins/standard/global.css @@ -909,7 +909,12 @@ input.required, select.required, span.required_explanation { } #comment { - margin: 0px 0px 1em 0px; + margin: 0px 0px 0.5em 0px; +} + +#comment-markdown-tip { + display: flex; + align-items: center; } /*******************/ @@ -1411,7 +1416,8 @@ table.edit_form hr { left: 16px; } -.bz_comment_text span.quote, .bz_comment_text span.quote_wrapped { +.bz_comment_text span.quote, .bz_comment_text span.quote_wrapped, +div.bz_comment_text pre { background: #eee !important; color: #444 !important; display: block !important; @@ -1421,6 +1427,40 @@ table.edit_form hr { padding: 5px !important; } +/* Markdown comments */ +div.bz_comment_text { + white-space: normal; + padding: 0 8px 0 8px; + font-family: inherit !important; +} + +div.bz_comment_text code { + color: #444; + background-color: #eee; + font-size: 13px; + font-family: "Fira Mono","Droid Sans Mono",Menlo,Monaco,"Courier New",monospace; +} + +div.bz_comment_text table { + border-collapse: collapse; +} + +div.bz_comment_text th, div.bz_comment_text td { + padding: 5px 10px; + border: 1px solid #ccc; +} + +div.bz_comment_text hr { + display: block !important; +} + +div.bz_comment_text blockquote { + background: #fcfcfc; + border-left: 5px solid #ccc; + margin: 1.5em 10px; + padding: 0.5em 10px; +} + .bz_comment_tags { background: #eee; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); diff --git a/t/004template.t b/t/004template.t index 909f1a231..8b063a366 100644 --- a/t/004template.t +++ b/t/004template.t @@ -76,7 +76,7 @@ foreach my $include_path (@include_paths) { url_quote => sub { return $_ } , css_class_quote => sub { return $_ } , xml => sub { return $_ } , - quoteUrls => sub { return $_ } , + renderComment => sub { return $_ } , bug_link => [ sub { return sub { return $_; } }, 1] , csv => sub { return $_ } , unitconvert => sub { return $_ }, diff --git a/t/008filter.t b/t/008filter.t index 443fb2b4f..050cf1ef3 100644 --- a/t/008filter.t +++ b/t/008filter.t @@ -214,7 +214,7 @@ sub directive_ok { # Note: If a single directive prints two things, and only one is # filtered, we may not catch that case. return 1 if $directive =~ /FILTER\ (html|csv|js|base64|css_class_quote|ics| - quoteUrls|time|uri|xml|lower|html_light| + renderComment|time|uri|xml|lower|html_light| obsolete|inactive|closed|unitconvert| txt|html_linebreak|none|json|null|id| markdown)\b/x; diff --git a/t/bmo/comments.t b/t/bmo/comments.t index 4b0bb8177..f4064a7fc 100644 --- a/t/bmo/comments.t +++ b/t/bmo/comments.t @@ -61,7 +61,7 @@ my $bug_2 = Bugzilla::Bug->create( my $bug_2_id = $bug_2->id; -Bugzilla::Template::quoteUrls( +Bugzilla::Template::renderComment( $bug_2->comments->[0]->body, undef, undef, undef, sub { my $bug_id = $_[0]; diff --git a/template/en/default/bug/comment.html.tmpl b/template/en/default/bug/comment.html.tmpl index e3cd382fd..9b0deecc4 100644 --- a/template/en/default/bug/comment.html.tmpl +++ b/template/en/default/bug/comment.html.tmpl @@ -37,6 +37,11 @@
Generating Preview...
-

+    
[% END %] + + diff --git a/template/en/default/bug/comments.html.tmpl b/template/en/default/bug/comments.html.tmpl index 7af08efde..98ab4645e 100644 --- a/template/en/default/bug/comments.html.tmpl +++ b/template/en/default/bug/comments.html.tmpl @@ -283,15 +283,22 @@ [% END %] -[%# Don't indent the
 block, since then the spaces are displayed in the
-  # generated HTML
+
+[% IF comment.is_markdown %]
+  [% comment_tag = 'div' %]
+[% ELSE %]
+  [% comment_tag = 'pre' %]
+[% END %]
+
+[%# Don't indent incaase it's a 
 block, since then the spaces are 
+  # displayed in the generated HTML
   #%]
-
-  [%- comment_text FILTER quoteUrls(bug, comment) -%]
-
+ [%- comment_text FILTER renderComment(bug, comment) -%] + [% Hook.process('a_comment-end', 'bug/comments.html.tmpl') %] [% END %] diff --git a/template/en/default/bug/edit.html.tmpl b/template/en/default/bug/edit.html.tmpl index 445e5fe0d..69edfeb00 100644 --- a/template/en/default/bug/edit.html.tmpl +++ b/template/en/default/bug/edit.html.tmpl @@ -253,7 +253,7 @@ ([% bug.alias FILTER html %]) [% END %] [% END %] - [% bug.short_desc FILTER quoteUrls(bug) FILTER wbr %] + [% bug.short_desc FILTER renderComment(bug, undef, 1) FILTER wbr %] [% IF bug.check_can_change_field('short_desc', 0, 1) || bug.check_can_change_field('alias', 0, 1) %] (edit) diff --git a/template/en/default/bug/link.html.tmpl b/template/en/default/bug/link.html.tmpl index dc09848da..17b85589c 100644 --- a/template/en/default/bug/link.html.tmpl +++ b/template/en/default/bug/link.html.tmpl @@ -56,7 +56,8 @@ diff --git a/template/en/default/bug/new_bug.html.tmpl b/template/en/default/bug/new_bug.html.tmpl index ef5e361c0..185ae771b 100644 --- a/template/en/default/bug/new_bug.html.tmpl +++ b/template/en/default/bug/new_bug.html.tmpl @@ -244,7 +244,7 @@ [% END %] diff --git a/template/en/default/email/bugmail.html.tmpl b/template/en/default/email/bugmail.html.tmpl index 8b567b691..cdcb3d13d 100644 --- a/template/en/default/email/bugmail.html.tmpl +++ b/template/en/default/email/bugmail.html.tmpl @@ -38,7 +38,12 @@ on [% "$terms.bug $bug.id" FILTER bug_link(bug, { full_url => 1, user => to_user }) FILTER none %] from [% INCLUDE global/user.html.tmpl user = to_user, who = comment.author %] [% END %] -
[% comment.body_full({ wrap => 1 }) FILTER quoteUrls(bug, comment, to_user) %]
+ [% IF comment.is_markdown %] + [% comment_tag = 'div' %] + [% ELSE %] + [% comment_tag = 'pre' %] + [% END %] + <[% comment_tag FILTER none %]>[% comment.body_full({ wrap => 1 }) FILTER renderComment(bug, comment) %] [% END %]

diff --git a/template/en/default/filterexceptions.pl b/template/en/default/filterexceptions.pl index 39f064035..07211ad29 100644 --- a/template/en/default/filterexceptions.pl +++ b/template/en/default/filterexceptions.pl @@ -34,7 +34,7 @@ # [% foo.push() %] # TT loop variables - [% loop.count %] # Already-filtered stuff - [% wibble FILTER html %] -# where the filter is one of html|csv|js|quoteUrls|time|uri|xml|none +# where the filter is one of html|csv|js|renderComment|time|uri|xml|none %::safe = ( diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl index bd9ec8bcb..426742495 100644 --- a/template/en/default/global/header.html.tmpl +++ b/template/en/default/global/header.html.tmpl @@ -112,6 +112,7 @@ }, constant => { COMMENT_COLS => constants.COMMENT_COLS, + URL_BASE => urlbase, }, string => { # Please keep these in alphabetical order. diff --git a/template/en/default/pages/linked.html.tmpl b/template/en/default/pages/linked.html.tmpl index b5d850627..aa519f9ac 100644 --- a/template/en/default/pages/linked.html.tmpl +++ b/template/en/default/pages/linked.html.tmpl @@ -31,7 +31,7 @@

-[%- cgi.param("text") FILTER quoteUrls FILTER html -%]
+[%- cgi.param("text") FILTER renderComment FILTER html -%]
 

@@ -46,7 +46,7 @@

-[%- cgi.param("text") FILTER quoteUrls -%]
+[%- cgi.param("text") FILTER renderComment -%]
 

-- cgit v1.2.3-24-g4f1b