<html>
    <head>
      <base href="https://bugs.webkit.org/" />
    </head>
    <body>
      <p>
        <div>
            <b><a class="bz_bug_link 
          bz_status_NEW "
   title="NEW - [Mac] Add API for open panel handling to WKWebView"
   href="https://bugs.webkit.org/show_bug.cgi?id=137759#c44">Comment # 44</a>
              on <a class="bz_bug_link 
          bz_status_NEW "
   title="NEW - [Mac] Add API for open panel handling to WKWebView"
   href="https://bugs.webkit.org/show_bug.cgi?id=137759">bug 137759</a>
              from <span class="vcard"><a class="email" href="mailto:brian.michel&#64;gmail.com" title="Brian Michel &lt;brian.michel&#64;gmail.com&gt;"> <span class="fn">Brian Michel</span></a>
</span></b>
        <pre>(In reply to <a href="show_bug.cgi?id=137759#c42">comment #42</a>)
<span class="quote">&gt; Comment on <span class=""><a href="attachment.cgi?id=269672&amp;action=diff" name="attach_269672" title="Patch">attachment 269672</a> <a href="attachment.cgi?id=269672&amp;action=edit" title="Patch">[details]</a></span>
&gt; Patch
&gt; 
&gt; View in context:
&gt; <a href="https://bugs.webkit.org/attachment.cgi?id=269672&amp;action=review">https://bugs.webkit.org/attachment.cgi?id=269672&amp;action=review</a>
&gt; 
&gt; Looks like good work. Thanks for tackling this!
&gt; 
&gt; I have many small comments, some are important, others are less important
&gt; details but still worth improving.
&gt; 
&gt; review- mainly because of the most serious problem, the lifetime mistake in
&gt; WKConcreteOpenPanelResultListener where it doesn’t ref/deref the underlying
&gt; listener proxy C++ object.
&gt; 
&gt; &gt; Source/WebKit2/ChangeLog:3
&gt; &gt; +        Handle Open Panel Functions Are Unimplemented
&gt; 
&gt; We don’t capitalize every word of bug titles; even though they are “titles”,
&gt; “title case” is not called for.
&gt; 
&gt; &gt; Source/WebKit2/ChangeLog:23
&gt; &gt; +        * Shared/API/Cocoa/WebKit.h:
&gt; &gt; +        * UIProcess/API/Cocoa/WKUIDelegate.h:
&gt; &gt; +        * UIProcess/API/Cocoa/WKUIOpenPanelParameters.h: Copied from Source/WebKit2/Shared/API/Cocoa/WebKit.h.
&gt; &gt; +        * UIProcess/API/Cocoa/WKUIOpenPanelParameters.m: Copied from Source/WebKit2/Shared/API/Cocoa/WebKit.h.
&gt; &gt; +        (-[WKUIOpenPanelParameters _setAllowsMultipleFiles:]):
&gt; &gt; +        * UIProcess/API/Cocoa/WKUIOpenPanelParametersPrivate.h: Copied from Source/WebKit2/Shared/API/Cocoa/WebKit.h.
&gt; &gt; +        * UIProcess/Cocoa/UIDelegate.h:
&gt; &gt; +        * UIProcess/Cocoa/UIDelegate.mm:
&gt; &gt; +        (WebKit::UIDelegate::setDelegate):
&gt; &gt; +        (WebKit::UIDelegate::UIClient::runOpenPanel):
&gt; &gt; +        * UIProcess/Cocoa/WKConcreteOpenPanelResultListener.h: Copied from Source/WebKit2/Shared/API/Cocoa/WebKit.h.
&gt; &gt; +        * UIProcess/Cocoa/WKConcreteOpenPanelResultListener.m: Copied from Source/WebKit2/Shared/API/Cocoa/WebKit.h.
&gt; &gt; +        (-[WKConcreteOpenPanelResultListener initWithListenerProxy:]):
&gt; &gt; +        (-[WKConcreteOpenPanelResultListener chooseFiles:]):
&gt; &gt; +        (-[WKConcreteOpenPanelResultListener cancel]):
&gt; &gt; +        * WebKit2.xcodeproj/project.pbxproj:
&gt; 
&gt; A good quality change log should have comments about what each change was,
&gt; and should only list function names when there is a change in the function.
&gt; 
&gt; All those &quot;Copied from Source/WebKit2/Shared/API/Cocoa/WebKit.h&quot; also look
&gt; incorrect. The script generated incorrect stuff there, and you need to
&gt; replace those mistakes with a useful change log entry.
&gt; 
&gt; &gt; Source/WebKit2/ChangeLog:108
&gt; &gt; -        (because createWebPage, which usually brings it up, hasn't happened yet), and we 
&gt; &gt; +        (because createWebPage, which usually brings it up, hasn't happened yet), and we
&gt; 
&gt; Trimming all the trailing whitespace from the ChangeLog is not typically
&gt; something we want to do; makes the patch bigger for no good reason.
&gt; 
&gt; I suppose we could add a pre-commit hook to prevent people from landing such
&gt; whitespace in the first place to avoid this in the future.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/API/Cocoa/WKUIDelegate.h:39
&gt; &gt; +&#64;class WKWebView;
&gt; &gt; +&#64;class WKUIOpenPanelParameters;
&gt; 
&gt; We normally keep these alphabetically, so please sort these in instead  of
&gt; adding them at the bottom.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/API/Cocoa/WKUIDelegate.h:43
&gt; &gt; +/*! A class conforming to WKOpenPanelResultListener provides methods
&gt; &gt; +for interacting with files.
&gt; &gt; +*/
&gt; 
&gt; This seems a bit vague and oblique. I looked at Apple documentation to see
&gt; how they write these and it would be something more like:
&gt; 
&gt; /*! The WKOpenPanelResultListener protocol defines the methods that a file
&gt; open panel implementation must invokes when the open interaction is complete.
&gt; */
&gt; 
&gt; One critical question is whether we have a threading rule about this class
&gt; or not. I think we would want these methods to be callable on any thread.
&gt; And we also want it to be safe to deallocate this object on any thread.
&gt; That’s tricky to get right.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/API/Cocoa/WKUIDelegate.h:46
&gt; &gt; +/*! &#64;abstract passes an array of file urls to the web view.
&gt; 
&gt; Should be capitalized: “Passes”.
&gt; 
&gt; “file URLs”, not “file urls”.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/API/Cocoa/WKUIDelegate.h:47
&gt; &gt; + &#64;param fileURLs The array of file urls to pass to the web view.
&gt; 
&gt; Ditto.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/API/Cocoa/WKUIDelegate.h:48
&gt; &gt; + &#64;discussion passing nil, or an empty array will have the effect of calling the cancel selector.
&gt; 
&gt; There should not be a comma after the word &quot;nil&quot;.
&gt; 
&gt; But more importantly in a new API we should not be talking about passing
&gt; nil, since we are not marking this as a nullable argument. Callers should
&gt; pass &#64;[] rather than nil.
&gt; 
&gt; I also don’t think the wording “will have the effect of calling the cancel
&gt; selector” is quite right. I would write something a little more like this:
&gt; 
&gt; &#64;discussion Passing an empty array cancels the file open interaction as the
&gt; cancel method does.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/API/Cocoa/WKUIDelegate.h:50
&gt; &gt; +- (void)chooseFiles:(NSArray *)fileURLs;
&gt; 
&gt; The type here should be NSArray&lt;NSURL *&gt; *, which will make things nicer
&gt; both for programmers using Objective-C and Swift. Not sure exactly how we
&gt; are handling using that feature in WebKit API at this time; there may be
&gt; some compatibility issue for older versions of clang. If nobody else knows,
&gt; Anders at least should know.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/API/Cocoa/WKUIDelegate.h:139
&gt; &gt; + &#64;param listener The listener object to be called when selection has completed
&gt; &gt; + or cancelled.
&gt; 
&gt; I’d put this all on one line. No need to wrap this. Even though some of the
&gt; comments above are wrapped like this.
&gt; 
&gt; Wording not quite right here. I don’t think we would say that &quot;selection has
&gt; completed&quot;. We don’t &quot;call&quot; an object, we invoke one of its methods. And a
&gt; selection doesn’t complete nor does it cancel. So I think better wording
&gt; would be more like:
&gt; 
&gt;     The listener object with methods to be invoked when the user makes a
&gt; selection or cancels.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/API/Cocoa/WKUIDelegate.h:140
&gt; &gt; + &#64;param parameters Parameters to configure the open panel with as specificed by the webpage.
&gt; 
&gt; typo: &quot;specificed&quot;
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/API/Cocoa/WKUIOpenPanelParameters.h:32
&gt; &gt; +/*! A class to pass configuration flags to an open panel based on options specified in the webpage.
&gt; 
&gt; Comment should be more like the ones on other classes in this directory.
&gt; Here’s a rough cut:
&gt; 
&gt;     A WKUIOpenPanelParameters object is a collection of options specified in
&gt; a webpage to customize an open panel.
&gt; 
&gt; My wording isn’t quite right either, though.
&gt; 
&gt; Not sure that “parameters” is the right name for this class. There aren’t
&gt; any other WebKit classes that use the name parameters like this. I looked
&gt; for other similar ones.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/API/Cocoa/WKUIOpenPanelParameters.h:37
&gt; &gt; +/*! &#64;abstract Whether or not multiple files are allowed.
&gt; &gt; + */
&gt; 
&gt; Looking at other comments, it seems this one should be more like this:
&gt; 
&gt; /*! &#64;abstract A Boolean value indicating whether or not the open panel
&gt; should allow selection of more than one file.
&gt; */
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/API/Cocoa/WKUIOpenPanelParameters.m:33
&gt; &gt; +- (void)_setAllowsMultipleFiles:(BOOL)allowsMultipleFiles {
&gt; 
&gt; WebKit coding style puts the brace on a separate line.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/API/Cocoa/WKUIOpenPanelParametersPrivate.h:30
&gt; &gt; +&#64;interface WKUIOpenPanelParameters (WKPrivate)
&gt; 
&gt; This should be Internal, not Private. Both the category name and the file
&gt; name. Things marked Internal are for use only within the WebKit2 framework.
&gt; Things marked Private are SPI, so can be used outside the framework. This is
&gt; Internal.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/UIDelegate.mm:104
&gt; &gt; +#if PLATFORM(MAC)
&gt; &gt; +    m_delegateMethods.webViewRunOpenPanelWithResultListenerParameters = [delegate respondsToSelector:&#64;selector(webView:runOpenPanelWithResultListener:parameters:)];
&gt; &gt; +#endif
&gt; 
&gt; Would be nice to put the conditional ones after the unconditional ones. I
&gt; see that PLATFORM(IOS) ones are above, but I suggest putting this down at
&gt; the bottom, after the ENABLE(CONTEXT_MENUS) ones.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/UIDelegate.mm:241
&gt; &gt; +bool UIDelegate::UIClient::runOpenPanel(WebKit::WebPageProxy *page, WebKit::WebFrameProxy *frame, WebKit::WebOpenPanelParameters *parameters, WebKit::WebOpenPanelResultListenerProxy *listener)
&gt; 
&gt; Formatting here is wrong for WebKit coding style (should be &quot;WebPageProxy*&quot;
&gt; with the * next to the type name), and the explicit WebKit prefixes are not
&gt; needed.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/UIDelegate.mm:247
&gt; &gt; +    // If our delegate does not support this method, invalidate the listener and return false.
&gt; &gt; +    if (!m_uiDelegate.m_delegateMethods.webViewRunOpenPanelWithResultListenerParameters) {
&gt; &gt; +        listener-&gt;invalidate();
&gt; &gt; +        return false;
&gt; &gt; +    }
&gt; 
&gt; In WebKit code we try to avoid writing comments that say the same thing the
&gt; code does. This is an example of that anti-pattern, so please omit the
&gt; comment.
&gt; 
&gt; You should write a comment when you need to explain *why* the code does what
&gt; it does.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/UIDelegate.mm:254
&gt; &gt; +    NSWindow *window = [m_uiDelegate.m_webView window];
&gt; &gt; +
&gt; &gt; +    if (!window) {
&gt; &gt; +        listener-&gt;invalidate();
&gt; &gt; +        return false;
&gt; &gt; +    }
&gt; 
&gt; I don’t understand why this code is here. Whether to do this without a
&gt; window seems like a decision for the delegate, not to be decided by WebKit.
&gt; Please don’t enforce a &quot;no window, no call to delegate&quot; policy here, unless
&gt; there is a compelling reason to do so.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/UIDelegate.mm:261
&gt; &gt; +    [delegate webView:m_uiDelegate.m_webView runOpenPanelWithResultListener:(id &lt;WKOpenPanelResultListener&gt;)adoptNS([[WKConcreteOpenPanelResultListener alloc] initWithListenerProxy:listener]) parameters:(WKUIOpenPanelParameters *)delegateParameters];
&gt; 
&gt; This should be using &quot;adoptNS([[WKConcreteOpenPanelResultListener alloc]
&gt; initWithListenerProxy:listener]).get()&quot; rather than &quot;(id
&gt; &lt;WKOpenPanelResultListener&gt;)adoptNS([[WKConcreteOpenPanelResultListener
&gt; alloc] initWithListenerProxy:listener])&quot;. Might also want to use a local
&gt; variable so this line isn’t so super-long.
&gt; 
&gt; This should be using &quot;delegateParameters.get()&quot; rather than
&gt; &quot;(WKUIOpenPanelParameters *)delegateParameters&quot;.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/WKConcreteOpenPanelResultListener.h:2
&gt; &gt; + * Copyright (C) 2015 Brian Michel (<a href="mailto:brian.michel&#64;gmail.com">brian.michel&#64;gmail.com</a>). All rights reserved.
&gt; 
&gt; 2016
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/WKConcreteOpenPanelResultListener.h:37
&gt; &gt; +- (instancetype)initWithListenerProxy:(WebKit::WebOpenPanelResultListenerProxy *)proxy;
&gt; 
&gt; In WebKit coding style there should not be a space before &quot;*&quot; on this line.
&gt; 
&gt; I suggest naming this method initWithProxy: rather than
&gt; initWithListenerProxy: because since this object is a listener, it stands to
&gt; reason the proxy would be a listener proxy.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/WKConcreteOpenPanelResultListener.m:41
&gt; &gt; +    WebOpenPanelResultListenerProxy *m_listener;
&gt; 
&gt; This object needs to ref the listener. Otherwise, this listener can outlive
&gt; the proxy. However, using RefPtr for this would be dangerous because it will
&gt; make the dealloc method call deref on whatever thread the object is released
&gt; on. Instead we probably want to use callOnMainThread in the dealloc method
&gt; to make sure the deref happens on the main thread.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/WKConcreteOpenPanelResultListener.m:44
&gt; &gt; +- (instancetype)initWithListenerProxy:(WebOpenPanelResultListenerProxy *)proxy {
&gt; 
&gt; WebKit coding style puts the open brace on a separate line.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/WKConcreteOpenPanelResultListener.m:53
&gt; &gt; +#pragma mark - WKOpenPanelResultListener
&gt; 
&gt; Not sure we really need this mark. This is a small simple class.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/WKConcreteOpenPanelResultListener.m:55
&gt; &gt; +- (void)chooseFiles:(NSArray *)fileURLs {
&gt; 
&gt; To make it safe to call this on any thread, I think we need to use
&gt; callOnMainThread in this method and put the actual work inside the block or
&gt; lambda that we call on the main thread. We also need to capture self (and
&gt; retain it here and release it in the block or lambda).
&gt; 
&gt; Also would be nice to make it safe to call this method multiple times. I
&gt; think the way to do that would be to null out m_listener after this is
&gt; called the first time, and then make sure it’s a no-op if m_listener is
&gt; already null. As with the work I mentioned above and the deref in the
&gt; dealloc function, the work has to be done on the main thread.
&gt; 
&gt; WebKit coding style puts the open brace on a separate line.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/WKConcreteOpenPanelResultListener.m:57
&gt; &gt; +    NSUInteger count = [fileURLs count];
&gt; &gt; +    if (!count)
&gt; 
&gt; No reason to put this into a local variable since we only use it once. I’d
&gt; write this:
&gt; 
&gt;     if (!fileURLs.count)
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/WKConcreteOpenPanelResultListener.m:60
&gt; &gt; +        Vector&lt;RefPtr&lt;API::Object&gt;&gt; urls;
&gt; 
&gt; I personally would dodge the ugly &quot;urls&quot; name by naming the vector something
&gt; else, like &quot;files&quot;.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/WKConcreteOpenPanelResultListener.m:64
&gt; &gt; +        for (NSURL *fileURL in fileURLs)
&gt; &gt; +            urls.uncheckedAppend(adoptRef(toImpl(WKURLCreateWithCFURL((CFURLRef)fileURL))));
&gt; 
&gt; We should consider throwing an Objective-C exception if any of the elements
&gt; in the array are not NSURL objects. That would make it easier for
&gt; programmers to figure out what they did wrong than the crash they might
&gt; otherwise see. Maybe not, though. Doesn’t seem like other WebKit API code is
&gt; taking this approach, so I guess it’s OK to leave as is for now.
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/WKConcreteOpenPanelResultListener.m:67
&gt; &gt; +        RefPtr&lt;API::Array&gt; fileURLsRef = API::Array::create(std::move(urls));
&gt; &gt; +        m_listener-&gt;chooseFiles(fileURLsRef.get());
&gt; 
&gt; No need for the local variable. In WebKit code, should use WTFMove instead
&gt; of std::move, so should be:
&gt; 
&gt;     m_listener-&gt;chooseFiles(API::Array::create(WTFMove(urls)).get());
&gt; 
&gt; &gt; Source/WebKit2/UIProcess/Cocoa/WKConcreteOpenPanelResultListener.m:71
&gt; &gt; +- (void)cancel {
&gt; 
&gt; Same threading issues apply here as in chooseFiles: and I think we should
&gt; probably make these share code. Simplest way is probably to just implement
&gt; this as [self chooseFiles:&#64;[]] unless we change the semantics of chooseFiles
&gt; to make choosing zero files something distinct from cancelling. In that case
&gt; I’m sure we can still find a way to share the tricky main thread code.
&gt; 
&gt; WebKit coding style puts the open brace on a separate line.
&gt; 
&gt; &gt; Source/WebKit2/WebKit2.xcodeproj/project.pbxproj:1082
&gt; &gt; +                72C06CFF1C531D1A001291DE /* WKUIOpenPanelParametersPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 72C06CFE1C531D1A001291DE /* WKUIOpenPanelParametersPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; };
&gt; 
&gt; We don’t want this visible outside the framework, so it should be Internal,
&gt; not private. The ATTRIBUTES = (Private, ) here is wrong.
&gt; 
&gt; &gt; Source/WebKit2/WebKit2.xcodeproj/project.pbxproj:1084
&gt; &gt; +                72FCB6B41C307C2B00E8A012 /* WKConcreteOpenPanelResultListener.h in Headers */ = {isa = PBXBuildFile; fileRef = 72FCB6B11C307C2B00E8A012 /* WKConcreteOpenPanelResultListener.h */; settings = {ATTRIBUTES = (Private, ); }; };
&gt; 
&gt; We don’t want this visible outside the framework, so it should be Internal,
&gt; not private. The ATTRIBUTES = (Private, ) here is wrong.
&gt; 
&gt; &gt; Tools/ChangeLog:15
&gt; &gt; +        * MiniBrowser/mac/WK2BrowserWindowController.m:
&gt; &gt; +        (-[WK2BrowserWindowController webView:runOpenPanelWithResultListener:parameters:]):
&gt; &gt; +        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
&gt; &gt; +        * TestWebKitAPI/Tests/WebKit2/basic-input-element.html: Added.
&gt; &gt; +        * TestWebKitAPI/Tests/WebKit2ObjC/WKUIDelegateOpenPanelTest.mm: Added.
&gt; &gt; +        (-[WKOpenPanelTestDelegate webView:runOpenPanelWithResultListener:parameters:]):
&gt; &gt; +        (-[WKOpenPanelTestDelegate webView:didFinishNavigation:]):
&gt; &gt; +        (TestWebKitAPI::TEST):
&gt; 
&gt; As above, a good quality change log should have comments about what each
&gt; change was. Per-function comments help people understand the change. If you
&gt; need an example, I suggest looking at change log entries I have written in
&gt; the past to see what I am asking for.
&gt; 
&gt; &gt; Tools/ChangeLog:549
&gt; &gt; -        I always forgot to provide a --jsc argument), I always found it easier to 
&gt; &gt; +        I always forgot to provide a --jsc argument), I always found it easier to
&gt; 
&gt; As above, trimming all the trailing whitespace from the ChangeLog is not
&gt; typically something we want to do; makes the patch bigger for no good reason.
&gt; 
&gt; &gt; Tools/MiniBrowser/mac/WK2BrowserWindowController.m:452
&gt; &gt; +    [openPanel setAllowsMultipleSelection:parameters.allowsMultipleFiles];
&gt; 
&gt; I would have written:
&gt; 
&gt;     openPanel.allowsMultipleSelection = parameters.allowsMultipleFiles;
&gt; 
&gt; &gt; Tools/TestWebKitAPI/Tests/WebKit2ObjC/WKUIDelegateOpenPanelTest.mm:26
&gt; &gt; +#include &quot;config.h&quot;
&gt; 
&gt; Should be #import.
&gt; 
&gt; &gt; Tools/TestWebKitAPI/Tests/WebKit2ObjC/WKUIDelegateOpenPanelTest.mm:31
&gt; &gt; +#include &lt;wtf/RetainPtr.h&gt;
&gt; 
&gt; Should be #import. Should be in the same paragraph with the other includes
&gt; just above.
&gt; 
&gt; &gt; Tools/TestWebKitAPI/Tests/WebKit2ObjC/WKUIDelegateOpenPanelTest.mm:33
&gt; &gt; +#if WK_API_ENABLED &amp;&amp; !PLATFORM(IOS)
&gt; 
&gt; Should use the same #if as the code. Since the code is in PLATFORM(MAC),
&gt; then this should be PLATFORM(MAC), as opposed to !PLATFORM(IOS).
&gt; 
&gt; &gt; Tools/TestWebKitAPI/Tests/WebKit2ObjC/WKUIDelegateOpenPanelTest.mm:36
&gt; &gt; +static bool doneOpening;
&gt; &gt; +static bool doneLoading;
&gt; 
&gt; It might be cleaner to use public fields in WKOpenPanelTestDelegate instead
&gt; of globals. Then we wouldn’t have to clear them on the way out of the test.
&gt; Not sure we need to do that anyway. But since it’s just a test, I think it’s
&gt; OK either wya.</span >

Darin, thank you so much for the feedback, I'll work on these changes and get them submitted by the end of the week!</pre>
        </div>
      </p>
      <hr>
      <span>You are receiving this mail because:</span>
      
      <ul>
          <li>You are the assignee for the bug.</li>
      </ul>
    </body>
</html>