COMMIT_MESSAGE

 1EWS for security bugs
 2https://bugs.webkit.org/show_bug.cgi?id=186291
 3<rdar://problem/40829658>
 4
 5Reviewed by NOBODY (OOPS!).
 6
 7Part 1 of 2.
 8
 9Implements support for EWS processing of patches on security sensitive bugs. We add new
 10endpoints to the status server to support uploading and downloading of patches and associated
 11metadata. When webkit-patch submits a patch for EWS processing it will now upload the contents
 12and metadata for the patch to the status server if the patch is on a security sensitive bug.
 13We teach the EWS machinery in webkitpy to query the status server for a patch only if fetching
 14the patch from Bugzilla is not permitted due to an authorization error.
 15
 16Fetching patches from the status server requires an API key. The API key is read from the
 17environment variable WEBKIT_STATUS_API_KEY or the value of the Git configuration key webkit.status_api_key
 18(in that order). Contact me or another Apple engineer for an API key.
 19
 20Additionally, expose an optional command line option called --status-host-uses-https to
 21query the status server over HTTPS as opposed to HTTP.
 22
 23* QueueStatusServer/config/authorization.py: Added.
 24(_path_to_authorized_api_keys_file): Returns the absolute filesystem path to the file authorized_api_keys.txt.
 25(_parse_authorized_api_keys):
 26(authorized_api_keys):
 27(_parse_authorization_header): Parses the API key from the Authorization header. We use a
 28custom authentication scheme: "APIKey". See remark below for more details.
 29(is_authorized): Checks if the request includes an API key and whether that API key is in the
 30list of authorized keys (performs a case-sensitive match). The API key may be specified either
 31in a HTTP header Authorization or in the query string argument "api-key". When using the HTTP
 32headers approach the Authorization header should have the form: "Authorization: APIKey X" where
 33X is the case-sensitive API key.
 34* QueueStatusServer/handlers/fetchattachment.py: Added.
 35(FetchAttachment):
 36(FetchAttachment.get):
 37* QueueStatusServer/handlers/releasepatch.py:
 38(ReleasePatch.check_complete): Returns whether the specified attachment was processed by all the queues.
 39(ReleasePatch.post): Delete the patch from AppEngine (if we have it) once the patch was processed
 40by all the queues.
 41* QueueStatusServer/handlers/submittoews.py:
 42(SubmitToEWS._should_add_to_ews_queue): Fix a typo in a comment while I am working in this code.
 43* QueueStatusServer/handlers/uploadattachment.py: Added.
 44(UploadAttachment):
 45(UploadAttachment.get):
 46(UploadAttachment.post):
 47* QueueStatusServer/main.py: Add new routes /upload-attachment and /attachment to upload an attachment
 48and view an attachment (or its metadata), respectively.
 49* QueueStatusServer/model/attachmentdata.py: Added.
 50(AttachmentData):
 51(AttachmentData.add_attachment_data):
 52(AttachmentData.lookup_if_exists):
 53(AttachmentData.remove_attachment_data):
 54* QueueStatusServer/templates/uploadattachment.html: Added.
 55* Scripts/webkitpy/common/net/bugzilla/attachment.py:
 56(Attachment.committer):
 57(Attachment):
 58(Attachment.to_json): Serialize to JSON so that we can upload it to AppEngine.
 59(Attachment.from_json): Deserialize from JSON. This is used as part of downloading a patch from AppEngine.
 60* Scripts/webkitpy/common/net/bugzilla/attachment_unittest.py: Added.
 61(AttachmentTest):
 62(AttachmentTest.test_convert_to_json_and_back):
 63* Scripts/webkitpy/common/net/bugzilla/bug.py:
 64(Bug.group): Returns the group that bug is in or the empty string.
 65(Bug.is_security_sensitive): Returns whether the bug is in group Security-Sensitive.
 66* Scripts/webkitpy/common/net/bugzilla/bugzilla.py:
 67(BugzillaQueries.fetch_attachment_ids_from_review_queue): Modified to take an optional boolean, only_security_bugs,
 68as to whether to only fetch attachment ids for unreviewed patches associated with security bugs. By default, we
 69keep the current behavior and query for the attachment ids of all unreviewed patches that the currently logged in
 70Bugzilla user can see, which may include patches associated with security bugs.
 71(Bugzilla._parse_date): Update for moved and renamed constant. See remark for class Bugzilla.
 72(Bugzilla._parse_bug_dictionary_from_xml): Modified to return an empty dictionary if we do not have access to view the bug.
 73Otherwise, extract the name of the group the bug is in.
 74(Bugzilla.fetch_bug): Modified to return None if we do not have access to view the bug.
 75(Bugzilla._parse_bug_title_from_attachment_page): Extracted out logic to parse the title of the Attachment page
 76from _parse_bug_id_from_attachment_page() so that it can be used from both _parse_bug_id_from_attachment_page()
 77and get_bug_id_for_attachment_id().
 78(Bugzilla): Moved class constant _bugzilla_date_format to Scripts/webkitpy/common/net/bugzilla/constants.py
 79and renamed it to BUGZILLA_DATE_FORMAT.
 80(Bugzilla.AccessError):
 81(Bugzilla.AccessError.__init__):
 82(Bugzilla._parse_bug_id_from_attachment_page): Modified to return a tuple of ("bug id", "error code") so that
 83the caller can know the reason the parse failed if it did. The parse will fail if we do not have access to view
 84the bug.
 85(Bugzilla.bug_id_for_attachment_id): Modified to take a boolean throw_on_access_error (default: False)
 86as to whether to raise a Bugzilla.AccessError exception and pass it through to get_bug_id_for_attachment_id().
 87(Bugzilla.get_bug_id_for_attachment_id): Modified to take a boolean throw_on_access_error (default: False)
 88as to whether to raise a Bugzilla.AccessError exception if we do not have access to the bug associated with
 89the specified attachment id.
 90(Bugzilla.fetch_attachment):
 91* Scripts/webkitpy/common/net/bugzilla/bugzilla_mock.py:
 92(MockBugzillaQueries.fetch_attachment_ids_from_review_queue):
 93(MockBugzilla):
 94(MockBugzilla.fetch_attachment):
 95(MockBugzilla.fetch_attachment_contents):
 96(MockBugzilla.add_patch_to_bug):
 97* Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py:
 98* Scripts/webkitpy/common/net/bugzilla/constants.py: Added.
 99* Scripts/webkitpy/common/net/statusserver.py:
 100(StatusServer.set_host): Modified to take an boolean use_https as to whether to query the server using
 101HTTPS (default: False - use HTTP; our current behavior).
 102(StatusServer.set_api_key): Added.
 103(StatusServer._upload_attachment_to_server): Added.
 104(StatusServer.upload_attachment): Added.
 105(StatusServer._fetch_attachment_page): Added.
 106(StatusServer.fetch_attachment): Added.
 107* Scripts/webkitpy/common/net/statusserver_mock.py:
 108(MockStatusServer.upload_attachment): Added.
 109(MockStatusServer.fetch_attachment): Added.
 110* Scripts/webkitpy/tool/bot/feeders.py:
 111(EWSFeeder.feed): Modified to download patches on security bugs and upload them to the status server (AppEngine).
 112* Scripts/webkitpy/tool/commands/download.py:
 113(ProcessAttachmentsMixin._fetch_list_of_patches_to_process): Modified to handle the case when fetching the
 114bug details from Bugzilla fail, say because we are not allowed to the view the bug.
 115(ProcessBugsMixin._fetch_list_of_patches_to_process): Filter out None values for attachments that we failed
 116to fetch, say because we are not allowed to the view the bug the attachment is on.
 117* Scripts/webkitpy/tool/commands/earlywarningsystem.py:
 118(AbstractEarlyWarningSystem.refetch_patch): For now, refetch the patch from the status server. Ideally, we
 119need a way to ask the status server to fetch the patch again from Bugzilla (or at least its metadata) so
 120that the EWS can check the current state of the patch (i.e. is it still marked r?).
 121* Scripts/webkitpy/tool/commands/queries_unittest.py:
 122(QueryCommandsTest.test_patches_to_review): Update expected result.
 123* Scripts/webkitpy/tool/commands/queues.py:
 124(AbstractPatchQueue._next_patch): Fetch the patch from the status server if we failed to fetch it from
 125Bugzilla because we do not have permission to view it.
 126* Scripts/webkitpy/tool/commands/queues_unittest.py:
 127* Scripts/webkitpy/tool/commands/upload_unittest.py:
 128(test_upload_of_security_sensitive_patch_with_no_review_and_ews): Added.
 129* Scripts/webkitpy/tool/main.py:
 130(WebKitPatch):
 131(WebKitPatch._status_server_api_key_from_git): Read the API key from the Git configuration key webkit.status_api_key.
 132(WebKitPatch._status_server_api_key): Read the API key from the environment variable WEBKIT_STATUS_API_KEY.
 133(WebKitPatch.handle_global_options): Read the API key and update the state of the StatusServer object, if applicable.
 134* Scripts/webkitpy/tool/steps/obsoletepatches.py:
 135(ObsoletePatches.run): Modified to handle the case when fetching the bug details from Bugzilla fail, say because we
 136are not allowed to the view the bug.
 137* Scripts/webkitpy/tool/steps/submittoews.py:
 138(SubmitToEWS.run): Upload the contents of the patch and the Bugzilla metadata about it to the status server
 139if the patch was posted to a security bug.

Tools/ChangeLog

 12018-06-15 Daniel Bates <dabates@apple.com>
 2
 3 EWS for security bugs
 4 https://bugs.webkit.org/show_bug.cgi?id=186291
 5 <rdar://problem/40829658>
 6
 7 Reviewed by NOBODY (OOPS!).
 8
 9 Part 1 of 2.
 10
 11 Implements support for EWS processing of patches on security sensitive bugs. We add new
 12 endpoints to the status server to support uploading and downloading of patches and associated
 13 metadata. When webkit-patch submits a patch for EWS processing it will now upload the contents
 14 and metadata for the patch to the status server if the patch is on a security sensitive bug.
 15 We teach the EWS machinery in webkitpy to query the status server for a patch only if fetching
 16 the patch from Bugzilla is not permitted due to an authorization error.
 17
 18 Fetching patches from the status server requires an API key. The API key is read from the
 19 environment variable WEBKIT_STATUS_API_KEY or the value of the Git configuration key webkit.status_api_key
 20 (in that order). Contact me or another Apple engineer for an API key.
 21
 22 Additionally, expose an optional command line option called --status-host-uses-https to
 23 query the status server over HTTPS as opposed to HTTP.
 24
 25 * QueueStatusServer/config/authorization.py: Added.
 26 (_path_to_authorized_api_keys_file): Returns the absolute filesystem path to the file authorized_api_keys.txt.
 27 (_parse_authorized_api_keys):
 28 (authorized_api_keys):
 29 (_parse_authorization_header): Parses the API key from the Authorization header. We use a
 30 custom authentication scheme: "APIKey". See remark below for more details.
 31 (is_authorized): Checks if the request includes an API key and whether that API key is in the
 32 list of authorized keys (performs a case-sensitive match). The API key may be specified either
 33 in a HTTP header Authorization or in the query string argument "api-key". When using the HTTP
 34 headers approach the Authorization header should have the form: "Authorization: APIKey X" where
 35 X is the case-sensitive API key.
 36 * QueueStatusServer/handlers/fetchattachment.py: Added.
 37 (FetchAttachment):
 38 (FetchAttachment.get):
 39 * QueueStatusServer/handlers/releasepatch.py:
 40 (ReleasePatch.check_complete): Returns whether the specified attachment was processed by all the queues.
 41 (ReleasePatch.post): Delete the patch from AppEngine (if we have it) once the patch was processed
 42 by all the queues.
 43 * QueueStatusServer/handlers/submittoews.py:
 44 (SubmitToEWS._should_add_to_ews_queue): Fix a typo in a comment while I am working in this code.
 45 * QueueStatusServer/handlers/uploadattachment.py: Added.
 46 (UploadAttachment):
 47 (UploadAttachment.get):
 48 (UploadAttachment.post):
 49 * QueueStatusServer/main.py: Add new routes /upload-attachment and /attachment to upload an attachment
 50 and view an attachment (or its metadata), respectively.
 51 * QueueStatusServer/model/attachmentdata.py: Added.
 52 (AttachmentData):
 53 (AttachmentData.add_attachment_data):
 54 (AttachmentData.lookup_if_exists):
 55 (AttachmentData.remove_attachment_data):
 56 * QueueStatusServer/templates/uploadattachment.html: Added.
 57 * Scripts/webkitpy/common/net/bugzilla/attachment.py:
 58 (Attachment.committer):
 59 (Attachment):
 60 (Attachment.to_json): Serialize to JSON so that we can upload it to AppEngine.
 61 (Attachment.from_json): Deserialize from JSON. This is used as part of downloading a patch from AppEngine.
 62 * Scripts/webkitpy/common/net/bugzilla/attachment_unittest.py: Added.
 63 (AttachmentTest):
 64 (AttachmentTest.test_convert_to_json_and_back):
 65 * Scripts/webkitpy/common/net/bugzilla/bug.py:
 66 (Bug.group): Returns the group that bug is in or the empty string.
 67 (Bug.is_security_sensitive): Returns whether the bug is in group Security-Sensitive.
 68 * Scripts/webkitpy/common/net/bugzilla/bugzilla.py:
 69 (BugzillaQueries.fetch_attachment_ids_from_review_queue): Modified to take an optional boolean, only_security_bugs,
 70 as to whether to only fetch attachment ids for unreviewed patches associated with security bugs. By default, we
 71 keep the current behavior and query for the attachment ids of all unreviewed patches that the currently logged in
 72 Bugzilla user can see, which may include patches associated with security bugs.
 73 (Bugzilla._parse_date): Update for moved and renamed constant. See remark for class Bugzilla.
 74 (Bugzilla._parse_bug_dictionary_from_xml): Modified to return an empty dictionary if we do not have access to view the bug.
 75 Otherwise, extract the name of the group the bug is in.
 76 (Bugzilla.fetch_bug): Modified to return None if we do not have access to view the bug.
 77 (Bugzilla._parse_bug_title_from_attachment_page): Extracted out logic to parse the title of the Attachment page
 78 from _parse_bug_id_from_attachment_page() so that it can be used from both _parse_bug_id_from_attachment_page()
 79 and get_bug_id_for_attachment_id().
 80 (Bugzilla): Moved class constant _bugzilla_date_format to Scripts/webkitpy/common/net/bugzilla/constants.py
 81 and renamed it to BUGZILLA_DATE_FORMAT.
 82 (Bugzilla.AccessError):
 83 (Bugzilla.AccessError.__init__):
 84 (Bugzilla._parse_bug_id_from_attachment_page): Modified to return a tuple of ("bug id", "error code") so that
 85 the caller can know the reason the parse failed if it did. The parse will fail if we do not have access to view
 86 the bug.
 87 (Bugzilla.bug_id_for_attachment_id): Modified to take a boolean throw_on_access_error (default: False)
 88 as to whether to raise a Bugzilla.AccessError exception and pass it through to get_bug_id_for_attachment_id().
 89 (Bugzilla.get_bug_id_for_attachment_id): Modified to take a boolean throw_on_access_error (default: False)
 90 as to whether to raise a Bugzilla.AccessError exception if we do not have access to the bug associated with
 91 the specified attachment id.
 92 (Bugzilla.fetch_attachment):
 93 * Scripts/webkitpy/common/net/bugzilla/bugzilla_mock.py:
 94 (MockBugzillaQueries.fetch_attachment_ids_from_review_queue):
 95 (MockBugzilla):
 96 (MockBugzilla.fetch_attachment):
 97 (MockBugzilla.fetch_attachment_contents):
 98 (MockBugzilla.add_patch_to_bug):
 99 * Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py:
 100 * Scripts/webkitpy/common/net/bugzilla/constants.py: Added.
 101 * Scripts/webkitpy/common/net/statusserver.py:
 102 (StatusServer.set_host): Modified to take an boolean use_https as to whether to query the server using
 103 HTTPS (default: False - use HTTP; our current behavior).
 104 (StatusServer.set_api_key): Added.
 105 (StatusServer._upload_attachment_to_server): Added.
 106 (StatusServer.upload_attachment): Added.
 107 (StatusServer._fetch_attachment_page): Added.
 108 (StatusServer.fetch_attachment): Added.
 109 * Scripts/webkitpy/common/net/statusserver_mock.py:
 110 (MockStatusServer.upload_attachment): Added.
 111 (MockStatusServer.fetch_attachment): Added.
 112 * Scripts/webkitpy/tool/bot/feeders.py:
 113 (EWSFeeder.feed): Modified to download patches on security bugs and upload them to the status server (AppEngine).
 114 * Scripts/webkitpy/tool/commands/download.py:
 115 (ProcessAttachmentsMixin._fetch_list_of_patches_to_process): Modified to handle the case when fetching the
 116 bug details from Bugzilla fail, say because we are not allowed to the view the bug.
 117 (ProcessBugsMixin._fetch_list_of_patches_to_process): Filter out None values for attachments that we failed
 118 to fetch, say because we are not allowed to the view the bug the attachment is on.
 119 * Scripts/webkitpy/tool/commands/earlywarningsystem.py:
 120 (AbstractEarlyWarningSystem.refetch_patch): For now, refetch the patch from the status server. Ideally, we
 121 need a way to ask the status server to fetch the patch again from Bugzilla (or at least its metadata) so
 122 that the EWS can check the current state of the patch (i.e. is it still marked r?).
 123 * Scripts/webkitpy/tool/commands/queries_unittest.py:
 124 (QueryCommandsTest.test_patches_to_review): Update expected result.
 125 * Scripts/webkitpy/tool/commands/queues.py:
 126 (AbstractPatchQueue._next_patch): Fetch the patch from the status server if we failed to fetch it from
 127 Bugzilla because we do not have permission to view it.
 128 * Scripts/webkitpy/tool/commands/queues_unittest.py:
 129 * Scripts/webkitpy/tool/commands/upload_unittest.py:
 130 (test_upload_of_security_sensitive_patch_with_no_review_and_ews): Added.
 131 * Scripts/webkitpy/tool/main.py:
 132 (WebKitPatch):
 133 (WebKitPatch._status_server_api_key_from_git): Read the API key from the Git configuration key webkit.status_api_key.
 134 (WebKitPatch._status_server_api_key): Read the API key from the environment variable WEBKIT_STATUS_API_KEY.
 135 (WebKitPatch.handle_global_options): Read the API key and update the state of the StatusServer object, if applicable.
 136 * Scripts/webkitpy/tool/steps/obsoletepatches.py:
 137 (ObsoletePatches.run): Modified to handle the case when fetching the bug details from Bugzilla fail, say because we
 138 are not allowed to the view the bug.
 139 * Scripts/webkitpy/tool/steps/submittoews.py:
 140 (SubmitToEWS.run): Upload the contents of the patch and the Bugzilla metadata about it to the status server
 141 if the patch was posted to a security bug.
 142
11432018-06-14 Carlos Alberto Lopez Perez <clopez@igalia.com>
2144
3145 [GTK] Enable tests on the GTK EWS queue

Tools/QueueStatusServer/config/authorization.py

 1# Copyright (C) 2018 Apple Inc. All rights reserved.
 2#
 3# Redistribution and use in source and binary forms, with or without
 4# modification, are permitted provided that the following conditions
 5# are met:
 6# 1. Redistributions of source code must retain the above copyright
 7# notice, this list of conditions and the following disclaimer.
 8# 2. Redistributions in binary form must reproduce the above copyright
 9# notice, this list of conditions and the following disclaimer in the
 10# documentation and/or other materials provided with the distribution.
 11#
 12# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
 13# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 14# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 15# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
 16# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 17# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 18# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 19# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 20# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 21# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 22
 23import os
 24
 25_cached_authorized_api_keys = frozenset([])
 26
 27
 28def _path_to_authorized_api_keys_file():
 29 return os.path.abspath(os.path.join(os.path.dirname(__file__), "authorized_api_keys.txt"))
 30
 31
 32def _parse_authorized_api_keys(file):
 33 api_keys = set()
 34 for line in file:
 35 line = line.strip()
 36 if not line or line.startswith("#"): # Skip empty lines and comments
 37 continue
 38 api_keys.add(line)
 39 return frozenset(api_keys)
 40
 41
 42def authorized_api_keys():
 43 global _cached_authorized_api_keys
 44 if _cached_authorized_api_keys:
 45 return _cached_authorized_api_keys
 46 authorized_api_keys_path = _path_to_authorized_api_keys_file()
 47 with open(authorized_api_keys_path, "r") as file:
 48 _cached_authorized_api_keys = _parse_authorized_api_keys(file)
 49 return _cached_authorized_api_keys
 50
 51
 52def _parse_authorization_header(credentials):
 53 # See <https://tools.ietf.org/html/rfc7235#section-4.2>.
 54 parts = credentials.split(" ", 1)
 55 if len(parts) < 2:
 56 return ""
 57 scheme = parts[0]
 58 token68_encoded_value = parts[1]
 59 if scheme.lower() == "apikey":
 60 return token68_encoded_value
 61 return ""
 62
 63
 64def is_authorized(request):
 65 api_key = ''
 66 credentials = request.headers.get("Authorization")
 67 if credentials:
 68 api_key = _parse_authorization_header(credentials)
 69 if not api_key:
 70 api_key = request.get("api-key")
 71 return api_key in authorized_api_keys()

Tools/QueueStatusServer/handlers/fetchattachment.py

 1# Copyright (C) 2018 Apple Inc. All rights reserved.
 2#
 3# Redistribution and use in source and binary forms, with or without
 4# modification, are permitted provided that the following conditions
 5# are met:
 6# 1. Redistributions of source code must retain the above copyright
 7# notice, this list of conditions and the following disclaimer.
 8# 2. Redistributions in binary form must reproduce the above copyright
 9# notice, this list of conditions and the following disclaimer in the
 10# documentation and/or other materials provided with the distribution.
 11#
 12# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
 13# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 14# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 15# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
 16# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 17# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 18# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 19# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 20# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 21# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 22
 23from config import authorization
 24from google.appengine.ext import webapp
 25from model.attachmentdata import AttachmentData
 26
 27
 28class FetchAttachment(webapp.RequestHandler):
 29 def get(self, action, attachment_id):
 30 if action not in ["data", "metadata"]:
 31 self.error(400)
 32 return
 33 if not authorization.is_authorized(self.request):
 34 # Return an HTTP 404 response code instead of an HTTP 401 to avoid leaking whether
 35 # the attachment id is known.
 36 self.error(404)
 37 return
 38 attachment_data = AttachmentData.lookup_if_exists(attachment_id)
 39 if not attachment_data:
 40 self.error(404)
 41 return
 42 if action == "metadata":
 43 self.response.out.write(attachment_data.metadata)
 44 else:
 45 self.response.out.write(attachment_data.data)

Tools/QueueStatusServer/handlers/releasepatch.py

2929from google.appengine.ext import webapp, db
3030from google.appengine.ext.webapp import template
3131
 32from config.queues import all_queue_names
3233from handlers.updatebase import UpdateBase
3334from loggers.recordpatchevent import RecordPatchEvent
3435from model.attachment import Attachment
 36from model.attachmentdata import AttachmentData
3537from model.queues import Queue
3638
3739

@@class ReleasePatch(UpdateBase):
3941 def get(self):
4042 self.response.out.write(template.render("templates/releasepatch.html", None))
4143
 44 @staticmethod
 45 def check_complete(attachment_id):
 46 for queue_name in all_queue_names:
 47 queue = Queue.queue_with_name(queue_name)
 48 if queue.work_items().display_position_for_attachment(attachment_id) is not None:
 49 return False
 50 return True
 51
4252 def post(self):
4353 queue_name = self.request.get("queue_name")
4454 # FIXME: This queue lookup should be shared between handlers.

@@class ReleasePatch(UpdateBase):
5868 RecordPatchEvent.stopped(attachment_id, queue_name, last_status.message)
5969
6070 queue.active_work_items().expire_item(attachment_id)
 71
 72 if self.check_complete(attachment_id):
 73 AttachmentData.remove_attachment_data(attachment_id)

Tools/QueueStatusServer/handlers/submittoews.py

@@class SubmitToEWS(UpdateBase):
4141
4242 def _should_add_to_ews_queue(self, queue, attachment):
4343 # This assert() is here to make sure we're not submitting to the commit-queue.
44  # The commit-queue clients check each patch anyway, but there is not sense
 44 # The commit-queue clients check each patch anyway, but there is no sense
4545 # in adding things to the commit-queue when they won't be processed by it.
4646 assert(queue.is_ews())
4747 latest_status = attachment.status_for_queue(queue)

Tools/QueueStatusServer/handlers/uploadattachment.py

 1# Copyright (C) 2018 Apple Inc. All rights reserved.
 2#
 3# Redistribution and use in source and binary forms, with or without
 4# modification, are permitted provided that the following conditions
 5# are met:
 6# 1. Redistributions of source code must retain the above copyright
 7# notice, this list of conditions and the following disclaimer.
 8# 2. Redistributions in binary form must reproduce the above copyright
 9# notice, this list of conditions and the following disclaimer in the
 10# documentation and/or other materials provided with the distribution.
 11#
 12# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
 13# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 14# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 15# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
 16# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 17# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 18# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 19# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 20# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 21# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 22
 23from google.appengine.ext import webapp, db
 24from google.appengine.ext.webapp import template
 25
 26from handlers.updatebase import UpdateBase
 27from model.attachmentdata import AttachmentData
 28
 29
 30class UploadAttachment(UpdateBase):
 31 def get(self):
 32 self.response.out.write(template.render("templates/uploadattachment.html", None))
 33
 34 def post(self):
 35 attachment_id = self._int_from_request("attachment_id")
 36 attachment_metadata = self.request.get("attachment_metadata")
 37 attachment_data = self.request.get("attachment_data")
 38 AttachmentData.add_attachment_data(attachment_id, str(attachment_metadata), str(attachment_data))
 39 self.response.out.write(attachment_id)

Tools/QueueStatusServer/main.py

@@from google.appengine.ext import webapp
3535from google.appengine.ext.webapp.util import run_wsgi_app
3636
3737from handlers.activebots import ActiveBots
 38from handlers.fetchattachment import FetchAttachment
3839from handlers.gc import GC
3940from handlers.nextpatch import NextPatch
4041from handlers.patch import Patch

@@from handlers.syncqueuelogs import SyncQueueLogs
5556from handlers.updatestatus import UpdateStatus
5657from handlers.updatesvnrevision import UpdateSVNRevision
5758from handlers.updateworkitems import UpdateWorkItems
 59from handlers.uploadattachment import UploadAttachment
5860
5961
6062webapp.template.register_template_library('filters.webkit_extras')

@@routes = [
7678 (r'/queue-status/(.*)', QueueStatus),
7779 (r'/queue-status-json/(.*)', QueueStatusJSON),
7880 (r'/next-patch/(.*)', NextPatch),
 81 (r'/attachment/(.*)/(.*)', FetchAttachment),
7982 ('/release-patch', ReleasePatch),
8083 ('/release-lock', ReleaseLock),
8184 ('/update-status', UpdateStatus),
8285 ('/update-work-items', UpdateWorkItems),
8386 ('/update-svn-revision', UpdateSVNRevision),
 87 ('/upload-attachment', UploadAttachment),
8488 ('/active-bots', ActiveBots),
8589 (r'/processing-times-json/(\d+)\-(\d+)\-(\d+)\-(\d+)\-(\d+)\-(\d+)\-(\d+)\-(\d+)\-(\d+)\-(\d+)\-(\d+)\-(\d+)', ProcessingTimesJSON),
8690]

Tools/QueueStatusServer/model/attachmentdata.py

 1# Copyright (C) 2018 Apple Inc. All rights reserved.
 2#
 3# Redistribution and use in source and binary forms, with or without
 4# modification, are permitted provided that the following conditions
 5# are met:
 6# 1. Redistributions of source code must retain the above copyright
 7# notice, this list of conditions and the following disclaimer.
 8# 2. Redistributions in binary form must reproduce the above copyright
 9# notice, this list of conditions and the following disclaimer in the
 10# documentation and/or other materials provided with the distribution.
 11#
 12# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
 13# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 14# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 15# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
 16# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 17# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 18# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 19# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 20# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 21# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 22
 23from google.appengine.ext import db
 24
 25
 26class AttachmentData(db.Model):
 27 attachment_id = db.IntegerProperty()
 28 metadata = db.BlobProperty()
 29 data = db.BlobProperty()
 30
 31 @classmethod
 32 def add_attachment_data(cls, attachment_id, metadata, data):
 33 cls.get_or_insert(str(attachment_id), attachment_id=attachment_id, metadata=db.Blob(metadata), data=db.Blob(data))
 34
 35 @classmethod
 36 def lookup_if_exists(cls, attachment_id):
 37 return cls.get_by_key_name(str(attachment_id))
 38
 39 @classmethod
 40 def remove_attachment_data(cls, attachment_id):
 41 attachment_data = cls.lookup_if_exists(attachment_id)
 42 if attachment_data:
 43 attachment_data.delete()

Tools/QueueStatusServer/templates/uploadattachment.html

 1<form name="upload_attachment" enctype="multipart/form-data" method="POST">
 2Attachment id: <input name="attachment_id"><br>
 3Metadata: <input type="file" name="attachment_metadata"><br>
 4Data: <input type="file" name="attachment_data"><br>
 5<input type="submit" value="Upload Attachment">
 6</form>

Tools/Scripts/webkitpy/common/net/bugzilla/attachment.py

11# Copyright (c) 2009 Google Inc. All rights reserved.
2 # Copyright (c) 2009 Apple Inc. All rights reserved.
 2# Copyright (c) 2009, 2018 Apple Inc. All rights reserved.
33# Copyright (c) 2010 Research In Motion Limited. All rights reserved.
44#
55# Redistribution and use in source and binary forms, with or without

2828# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2929# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3030
 31import json
3132import logging
3233
 34from datetime import datetime
3335from webkitpy.common.memoized import memoized
 36from webkitpy.common.net.bugzilla.constants import BUGZILLA_DATE_FORMAT
3437
3538_log = logging.getLogger(__name__)
3639

@@class Attachment(object):
119122 if not self._committer:
120123 self._committer = self._validate_flag_value("committer")
121124 return self._committer
 125
 126 def to_json(self):
 127 temp = dict(self._attachment_dictionary)
 128 if 'attach_date' in temp:
 129 temp['attach_date'] = temp['attach_date'].strftime(BUGZILLA_DATE_FORMAT)
 130 return json.dumps(temp)
 131
 132 @classmethod
 133 def from_json(cls, json_string):
 134 temp = json.loads(json_string)
 135 if 'attach_date' in temp:
 136 temp['attach_date'] = datetime.strptime(temp['attach_date'], BUGZILLA_DATE_FORMAT)
 137 return Attachment(temp, None)

Tools/Scripts/webkitpy/common/net/bugzilla/attachment_unittest.py

 1# Copyright (C) 2018 Apple Inc. All rights reserved.
 2#
 3# Redistribution and use in source and binary forms, with or without
 4# modification, are permitted provided that the following conditions
 5# are met:
 6# 1. Redistributions of source code must retain the above copyright
 7# notice, this list of conditions and the following disclaimer.
 8# 2. Redistributions in binary form must reproduce the above copyright
 9# notice, this list of conditions and the following disclaimer in the
 10# documentation and/or other materials provided with the distribution.
 11#
 12# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
 13# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 14# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 15# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
 16# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 17# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 18# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 19# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 20# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 21# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 22
 23import unittest
 24
 25from datetime import datetime
 26from webkitpy.common.net.bugzilla.constants import BUGZILLA_DATE_FORMAT
 27
 28from .attachment import Attachment
 29
 30
 31class AttachmentTest(unittest.TestCase):
 32 def test_convert_to_json_and_back(self):
 33 bugzilla_formatted_date_string = datetime.today().strftime(BUGZILLA_DATE_FORMAT)
 34 expected_date = datetime.strptime(bugzilla_formatted_date_string, BUGZILLA_DATE_FORMAT)
 35 attachment = Attachment({'attach_date': expected_date}, None)
 36 self.assertEqual(Attachment.from_json(attachment.to_json()).attach_date(), expected_date)

Tools/Scripts/webkitpy/common/net/bugzilla/bug.py

@@class Bug(object):
6969 def status(self):
7070 return self.bug_dictionary["bug_status"]
7171
 72 def group(self):
 73 # FIXME: A bug may be in more than one group.
 74 return self.bug_dictionary.get('group', '')
 75
 76 def is_security_sensitive(self):
 77 return self.group() == 'Security-Sensitive'
 78
7279 # Bugzilla has many status states we don't really use in WebKit:
7380 # https://bugs.webkit.org/page.cgi?id=fields.html#status
7481 _open_states = ["UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED"]

Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py

11# Copyright (c) 2011 Google Inc. All rights reserved.
2 # Copyright (c) 2009 Apple Inc. All rights reserved.
 2# Copyright (c) 2009, 2018 Apple Inc. All rights reserved.
33# Copyright (c) 2010 Research In Motion Limited. All rights reserved.
44# Copyright (c) 2013 University of Szeged. All rights reserved.
55#

@@from .bug import Bug
4545
4646from webkitpy.common.config import committers
4747import webkitpy.common.config.urls as config_urls
 48from webkitpy.common.net.bugzilla.constants import BUGZILLA_DATE_FORMAT
4849from webkitpy.common.net.credentials import Credentials
4950from webkitpy.common.net.networktransaction import NetworkTransaction
5051from webkitpy.common.system.user import User

@@class BugzillaQueries(object):
278279
279280 # NOTE: This is the only client of _fetch_attachment_ids_request_query
280281 # This method only makes one request to bugzilla.
281  def fetch_attachment_ids_from_review_queue(self, since=None):
 282 def fetch_attachment_ids_from_review_queue(self, since=None, only_security_bugs=False):
282283 review_queue_url = "request.cgi?action=queue&type=review&group=type"
 284 if only_security_bugs:
 285 review_queue_url += '&product=Security'
283286 return self._fetch_attachment_ids_request_query(review_queue_url, since)
284287
285288 # This only works if your account has edituser privileges.

@@class Bugzilla(object):
399402 # convert from NavigableString to a real unicode() object using unicode().
400403 return unicode(soup.string)
401404
402  # Example: 2010-01-20 14:31 PST
403  # FIXME: Some bugzilla dates seem to have seconds in them?
404  # Python does not support timezones out of the box.
405  # Assume that bugzilla always uses PST (which is true for bugs.webkit.org)
406  _bugzilla_date_format = "%Y-%m-%d %H:%M:%S"
407 
408405 @classmethod
409406 def _parse_date(cls, date_string):
410407 (date, time, time_zone) = date_string.split(" ")

@@class Bugzilla(object):
413410 time += ':0'
414411 # Ignore the timezone because python doesn't understand timezones out of the box.
415412 date_string = "%s %s" % (date, time)
416  return datetime.strptime(date_string, cls._bugzilla_date_format)
 413 return datetime.strptime(date_string, BUGZILLA_DATE_FORMAT)
417414
418415 def _date_contents(self, soup):
419416 return self._parse_date(self._string_contents(soup))

@@class Bugzilla(object):
451448
452449 def _parse_bug_dictionary_from_xml(self, page):
453450 soup = BeautifulStoneSoup(page, convertEntities=BeautifulStoneSoup.XML_ENTITIES)
 451 bug_element = soup.find('bug')
 452 if bug_element and bug_element.get('error', '') == 'NotPermitted':
 453 _log.warning("You don't have permission to view this bug.")
 454 return {}
454455 bug = {}
455456 bug["id"] = int(soup.find("bug_id").string)
456457 bug["title"] = self._string_contents(soup.find("short_desc"))

@@class Bugzilla(object):
463464 bug["cc_emails"] = [self._string_contents(element) for element in soup.findAll('cc')]
464465 bug["attachments"] = [self._parse_attachment_element(element, bug["id"]) for element in soup.findAll('attachment')]
465466 bug["comments"] = [self._parse_log_descr_element(element) for element in soup.findAll('long_desc')]
 467 # FIXME: A bug may be in more than one group.
 468 group = soup.find('group')
 469 if group:
 470 bug['group'] = self._string_contents(group)
466471
467472 return bug
468473

@@class Bugzilla(object):
484489 # FIXME: A BugzillaCache object should provide all these fetch_ methods.
485490
486491 def fetch_bug(self, bug_id):
487  return Bug(self.fetch_bug_dictionary(bug_id), self)
 492 bug_dictionary = self.fetch_bug_dictionary(bug_id)
 493 if bug_dictionary:
 494 return Bug(bug_dictionary, self)
 495 return None
488496
489497 def fetch_attachment_contents(self, attachment_id):
490498 attachment_url = self.attachment_url_for_id(attachment_id)

@@class Bugzilla(object):
493501 self.authenticate()
494502 return self.open_url(attachment_url).read()
495503
 504 def _parse_bug_title_from_attachment_page(self, page):
 505 return BeautifulSoup(page).find('div', attrs={'id': 'bug_title'})
 506
 507 class AccessError(Exception):
 508 NOT_PERMITTED = 1 << 0
 509 OTHER = 1 << 1
 510
 511 def __init__(self, attachment_id, error_code, bug_title):
 512 super(Bugzilla.AccessError, self).__init__('Failed to access {}'.format(attachment_id))
 513 self.attachment_id = attachment_id
 514 self.error_code = error_code
 515 self.bug_title = bug_title
 516
496517 def _parse_bug_id_from_attachment_page(self, page):
497518 # The "Up" relation happens to point to the bug.
498  title = BeautifulSoup(page).find('div', attrs={'id':'bug_title'})
499  if not title :
500  _log.warning("This attachment does not exist (or you don't have permissions to view it).")
501  return None
 519 title = self._parse_bug_title_from_attachment_page(page)
 520 if not title:
 521 _log.warning("This attachment does not exist (or you don't have permission to view it).")
 522 return (None, Bugzilla.AccessError.OTHER)
 523 if title.getText() == 'Bug Access Denied':
 524 _log.warning("You don't have permission to view this attachment.")
 525 return (None, Bugzilla.AccessError.NOT_PERMITTED)
502526 match = re.search("show_bug.cgi\?id=(?P<bug_id>\d+)", str(title))
503527 if not match:
504528 _log.warning("Unable to parse bug id from attachment")
505  return None
506  return int(match.group('bug_id'))
 529 return (None, None)
 530 return (int(match.group('bug_id')), None)
507531
508  def bug_id_for_attachment_id(self, attachment_id):
509  return NetworkTransaction().run(lambda: self.get_bug_id_for_attachment_id(attachment_id))
 532 def bug_id_for_attachment_id(self, attachment_id, throw_on_access_error=False):
 533 return NetworkTransaction().run(lambda: self.get_bug_id_for_attachment_id(attachment_id, throw_on_access_error))
510534
511  def get_bug_id_for_attachment_id(self, attachment_id):
 535 def get_bug_id_for_attachment_id(self, attachment_id, throw_on_access_error=False):
512536 self.authenticate()
513537
514538 attachment_url = self.attachment_url_for_id(attachment_id, 'edit')
515539 _log.info("Fetching: %s" % attachment_url)
516540 page = self.open_url(attachment_url)
517  return self._parse_bug_id_from_attachment_page(page)
 541 bug_id, error_code = self._parse_bug_id_from_attachment_page(page)
 542 if bug_id:
 543 return bug_id
 544 if error_code is not None and throw_on_access_error:
 545 raise Bugzilla.AccessError(attachment_id, error_code, str(self._parse_bug_title_from_attachment_page(page)))
 546 return None
518547
519548 # FIXME: This should just return Attachment(id), which should be able to
520549 # lazily fetch needed data.
521550
522  def fetch_attachment(self, attachment_id):
 551 def fetch_attachment(self, attachment_id, throw_on_access_error=False):
523552 # We could grab all the attachment details off of the attachment edit
524553 # page but we already have working code to do so off of the bugs page,
525554 # so re-use that.
526  bug_id = self.bug_id_for_attachment_id(attachment_id)
 555 bug_id = self.bug_id_for_attachment_id(attachment_id, throw_on_access_error)
527556 if not bug_id:
528557 _log.warning("Unable to parse bug_id from attachment {}".format(attachment_id))
529558 return None

Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_mock.py

11# Copyright (C) 2011 Google Inc. All rights reserved.
 2# Copyright (C) 2018 Apple Inc. All rights reserved.
23#
34# Redistribution and use in source and binary forms, with or without
45# modification, are permitted provided that the following conditions are

@@import datetime
3031import logging
3132
3233from .bug import Bug
 34from .bugzilla import Bugzilla
3335from .attachment import Attachment
3436from webkitpy.common.config.committers import CommitterList, Reviewer
3537

@@_patch8 = { # Resolved bug, without review flag, not marked obsolete (maybe alr
151153 "attacher_email": "eric@webkit.org",
152154}
153155
 156_patch9 = {
 157 'id': 10008,
 158 'bug_id': 50007,
 159 'url': 'http://example.com/10008',
 160 'name': 'Patch9',
 161 'is_obsolete': False,
 162 'is_patch': True,
 163 'review': '?',
 164 'commit-queue': '-',
 165 'attacher_email': 'dbates@webkit.org',
 166 'attach_date': datetime.datetime.today(),
 167}
 168
154169# This matches one of Bug.unassigned_emails
155170_unassigned_email = "webkit-unassigned@lists.webkit.org"
156171# This is needed for the FlakyTestReporter to believe the bug

@@_bug7 = {
278293}
279294
280295
 296_bug8 = {
 297 'id': 50007,
 298 'title': 'Security bug with a patch needing review.',
 299 'reporter_email': 'dbates@webkit.org',
 300 'assigned_to_email': 'foo@foo.com',
 301 'cc_emails': [],
 302 'attachments': [_patch9],
 303 'bug_status': 'ASSIGNED',
 304 'group': 'Security-Sensitive',
 305 'comments': [{'comment_date': datetime.datetime(2011, 6, 11, 9, 4, 3),
 306 'comment_email': 'bar@foo.com',
 307 'text': 'Message1.\nCommitted r35: <https://trac.webkit.org/changeset/35>',
 308 },
 309 ],
 310}
 311
 312
281313class MockBugzillaQueries(object):
282314
283315 def __init__(self, bugzilla):

@@class MockBugzillaQueries(object):
293325 self._all_bugs())
294326 return map(lambda bug: bug.id(), bugs_with_commit_queued_patches)
295327
296  def fetch_attachment_ids_from_review_queue(self, since=None):
 328 def fetch_attachment_ids_from_review_queue(self, since=None, only_security_bugs=False):
297329 unreviewed_patches = sum([bug.unreviewed_patches()
298330 for bug in self._all_bugs()], [])
299331 if since:
300332 unreviewed_patches = [patch for patch in unreviewed_patches
301333 if patch.attach_date() >= since]
 334 if only_security_bugs:
 335 unreviewed_patches = filter(lambda patch: patch.bug().is_security_sensitive(), unreviewed_patches)
302336 return map(lambda patch: patch.id(), unreviewed_patches)
303337
304338 def fetch_patches_from_commit_queue(self):

@@class MockBugzilla(object):
344378
345379 bug_server_url = "http://example.com"
346380
347  bug_cache = _id_to_object_dictionary(_bug1, _bug2, _bug3, _bug4, _bug5, _bug6, _bug7)
 381 bug_cache = _id_to_object_dictionary(_bug1, _bug2, _bug3, _bug4, _bug5, _bug6, _bug7, _bug8)
348382
349  attachment_cache = _id_to_object_dictionary(_patch1,
350  _patch2,
351  _patch3,
352  _patch4,
353  _patch5,
354  _patch6,
355  _patch7,
356  _patch8)
 383 attachment_cache = _id_to_object_dictionary(_patch1, _patch2, _patch3, _patch4, _patch5, _patch6,
 384 _patch7, _patch8, _patch9)
357385
358386 def __init__(self):
359387 self.queries = MockBugzillaQueries(self)

@@class MockBugzilla(object):
395423 def set_override_patch(self, patch):
396424 self._override_patch = patch
397425
398  def fetch_attachment(self, attachment_id):
 426 def fetch_attachment(self, attachment_id, throw_on_access_error=False):
399427 if self._override_patch:
400428 return self._override_patch
401429
402  attachment_dictionary = self.attachment_cache.get(attachment_id)
 430 attachment_dictionary = self.attachment_cache.get(int(attachment_id))
403431 if not attachment_dictionary:
404432 print("MOCK: fetch_attachment: %s is not a known attachment id" % attachment_id)
405433 return None
406434 bug = self.fetch_bug(attachment_dictionary["bug_id"])
 435 if bug.is_security_sensitive() and throw_on_access_error:
 436 raise Bugzilla.AccessError(attachment_id, Bugzilla.AccessError.NOT_PERMITTED, 'Bug Access Denied')
407437 for attachment in bug.attachments(include_obsolete=True):
408438 if attachment.id() == int(attachment_id):
409439 return attachment
410440
 441 def fetch_attachment_contents(self, attachment_id):
 442 return 'Patch'
 443
411444 def bug_url_for_bug_id(self, bug_id):
412445 return "%s/%s" % (self.bug_server_url, bug_id)
413446

@@class MockBugzilla(object):
461494 _log.info("-- Begin comment --")
462495 _log.info(comment_text)
463496 _log.info("-- End comment --")
 497 bug = self.fetch_bug(bug_id)
 498 if bug.bug_dictionary:
 499 patches = bug.patches()
 500 if len(patches) == 1:
 501 return patches[0].id()
464502 return '10001'
465503
466504 def add_cc_to_bug(self, bug_id, ccs):

Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py

@@ZEZpbmlzaExvYWRXaXRoUmVhc29uOnJlYXNvbl07Cit9CisKIEBlbmQKIAogI2VuZGlmCg==
167167</bugzilla>
168168""" % _bug_xml
169169
 170 _single_not_permitted_bug_xml = """
 171<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
 172<!DOCTYPE bugzilla SYSTEM "https://bugs.webkit.org/bugzilla.dtd">
 173<bugzilla version="3.2.3"
 174 urlbase="https://bugs.webkit.org/"
 175 maintainer="admin@webkit.org"
 176>
 177 <bug error="NotPermitted">
 178 <bug_id>32585</bug_id>
 179 </bug>
 180</bugzilla>
 181"""
 182
170183 _expected_example_bug_parsing = {
171184 "id" : 32585,
172185 "title" : u"bug to test webkit-patch's and commit-queue's failures",

@@Ignore this bug. Just for testing failure modes of webkit-patch and the commit-
207220 bug = Bugzilla()._parse_bug_dictionary_from_xml(self._single_bug_xml)
208221 self._assert_dictionaries_equal(bug, self._expected_example_bug_parsing)
209222
 223 def test_parse_bug_dictionary_from_xml_for_not_permitted_bug(self):
 224 bug = Bugzilla()._parse_bug_dictionary_from_xml(self._single_not_permitted_bug_xml)
 225 self.assertEqual(bug, {})
 226
210227 _sample_multi_bug_xml = """
211228<bugzilla version="3.2.3" urlbase="https://bugs.webkit.org/" maintainer="admin@webkit.org" exporter="eric@webkit.org">
212229 %s

@@Ignore this bug. Just for testing failure modes of webkit-patch and the commit-
245262
246263 def test_attachment_detail_bug_parsing(self):
247264 bugzilla = Bugzilla()
248  self.assertEqual(27314, bugzilla._parse_bug_id_from_attachment_page(self._sample_attachment_detail_page))
 265 self.assertEqual((27314, None), bugzilla._parse_bug_id_from_attachment_page(self._sample_attachment_detail_page))
249266
250267 def test_add_cc_to_bug(self):
251268 bugzilla = Bugzilla()

Tools/Scripts/webkitpy/common/net/bugzilla/constants.py

 1# Example: 2010-01-20 14:31 PST
 2# FIXME: Some bugzilla dates seem to have seconds in them?
 3# Python does not support timezones out of the box.
 4# Assume that bugzilla always uses PST (which is true for bugs.webkit.org)
 5BUGZILLA_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"

Tools/Scripts/webkitpy/common/net/statusserver.py

11# Copyright (C) 2009 Google Inc. All rights reserved.
 2# Copyright (C) 2018 Apple Inc. All rights reserved.
23#
34# Redistribution and use in source and binary forms, with or without
45# modification, are permitted provided that the following conditions are

2829#
2930# This the client designed to talk to Tools/QueueStatusServer.
3031
 32from webkitpy.common.config.urls import statusserver_default_host
 33from webkitpy.common.net.bugzilla.attachment import Attachment
3134from webkitpy.common.net.networktransaction import NetworkTransaction
3235from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
33 from webkitpy.common.config.urls import statusserver_default_host
3436
 37import StringIO
3538import logging
3639import urllib2
3740

@@class StatusServer:
4649 self._browser.set_handle_robots(False)
4750 self.set_bot_id(bot_id)
4851
49  def set_host(self, host):
 52 def set_host(self, host, use_https=False):
5053 self.host = host
51  self.url = "http://%s" % self.host
 54 if use_https:
 55 protocol_to_use = 'https'
 56 else:
 57 protocol_to_use = 'http'
 58 self.url = '{}://{}'.format(protocol_to_use, self.host)
5259
5360 def set_bot_id(self, bot_id):
5461 self.bot_id = bot_id
5562
 63 def set_api_key(self, api_key):
 64 AUTHORIZATION_HEADER_NAME = 'Authorization'
 65 new_headers = filter(lambda header: header[0] != AUTHORIZATION_HEADER_NAME, self._browser.addheaders)
 66 if api_key:
 67 new_headers.append((AUTHORIZATION_HEADER_NAME, 'APIKey ' + api_key))
 68 self._browser.addheaders = new_headers
 69
5670 def results_url_for_status(self, status_id):
5771 return "%s/results/%s" % (self.url, status_id)
5872

@@class StatusServer:
111125 self._browser["high_priority_work_items"] = " ".join(high_priority_work_items)
112126 return self._browser.submit().read()
113127
 128 def _upload_attachment_to_server(self, attachment_id, attachment_metadata, attachment_data):
 129 upload_attachment_url = '{}/upload-attachment'.format(self.url)
 130 self._browser.open(upload_attachment_url)
 131 self._browser.select_form(name='upload_attachment')
 132 self._browser['attachment_id'] = unicode(attachment_id)
 133 self._browser.add_file(StringIO.StringIO(unicode(attachment_metadata)), 'application/json', 'attachment-{}-metadata.json'.format(attachment_id), 'attachment_metadata')
 134 if isinstance(attachment_data, unicode):
 135 attachment_data = attachment_data.encode('utf-8')
 136 self._browser.add_file(StringIO.StringIO(attachment_data), 'text/plain', 'attachment-{}.patch'.format(attachment_id), 'attachment_data')
 137 self._browser.submit()
 138
 139 def upload_attachment(self, attachment):
 140 _log.info('Uploading attachment {} to status server'.format(attachment.id()))
 141 return NetworkTransaction().run(lambda: self._upload_attachment_to_server(attachment.id(), attachment.to_json(), attachment.contents()))
 142
114143 def _post_work_item_to_ews(self, attachment_id):
115144 submit_to_ews_url = "%s/submit-to-ews" % self.url
116145 self._browser.open(submit_to_ews_url)

@@class StatusServer:
163192 _log.info("SVN revision: %s broke %s" % (svn_revision_number, broken_bot))
164193 return NetworkTransaction().run(lambda: self._post_svn_revision_to_server(svn_revision_number, broken_bot))
165194
 195 def _fetch_attachment_page(self, action, attachment_id):
 196 attachment_url = '{}/attachment/{}/{}'.format(self.url, action, attachment_id)
 197 return self._fetch_url(attachment_url)
 198
 199 def fetch_attachment(self, attachment_id):
 200 # We will neither have metadata nor content if our API key is missing, invalid, or revoked.
 201 attachment_metadata = self._fetch_attachment_page('metadata', attachment_id)
 202 if not attachment_metadata:
 203 return None
 204 attachment_contents = self._fetch_attachment_page('data', attachment_id)
 205 if not attachment_contents:
 206 return None
 207 attachment = Attachment.from_json(attachment_metadata)
 208 attachment.contents = lambda: attachment_contents
 209 return attachment
 210
166211 def _fetch_url(self, url):
167212 # FIXME: This should use NetworkTransaction's 404 handling instead.
168213 try:

Tools/Scripts/webkitpy/common/net/statusserver_mock.py

2828
2929import logging
3030
 31from webkitpy.common.net.bugzilla.attachment import Attachment
 32
3133_log = logging.getLogger(__name__)
3234
3335

@@class MockStatusServer(object):
5961 self._work_items = work_items
6062 _log.info("MOCK: update_work_items: %s %s" % (queue_name, high_priority_work_items + work_items))
6163
 64 def upload_attachment(self, attachment):
 65 _log.info('MOCK: upload_attachment: {}'.format(attachment.id()))
 66
6267 def submit_to_ews(self, patch_id):
6368 _log.info("MOCK: submit_to_ews: %s" % (patch_id))
6469

@@class MockStatusServer(object):
7176
7277 def results_url_for_status(self, status_id):
7378 return "http://dummy_url"
 79
 80 def fetch_attachment(self, attachment_id):
 81 attachment = Attachment({'id': 10008}, None)
 82 attachment.content = lambda: 'Patch'
 83 return attachment

Tools/Scripts/webkitpy/tool/bot/feeders.py

11# Copyright (c) 2010 Google Inc. All rights reserved.
 2# Copyright (C) 2018 Apple Inc. All rights reserved.
23#
34# Redistribution and use in source and binary forms, with or without
45# modification, are permitted provided that the following conditions are

@@class EWSFeeder(AbstractFeeder):
8485 AbstractFeeder.__init__(self, tool)
8586
8687 def feed(self):
87  ids_needing_review = set(self._tool.bugs.queries.fetch_attachment_ids_from_review_queue(datetime.today() - timedelta(7)))
 88 current_time = datetime.today()
 89 ids_needing_review = set(self._tool.bugs.queries.fetch_attachment_ids_from_review_queue(current_time - timedelta(7)))
 90 security_ids_needing_review = frozenset(self._tool.bugs.queries.fetch_attachment_ids_from_review_queue(current_time - timedelta(7), only_security_bugs=True))
8891 new_ids = ids_needing_review.difference(self._ids_sent_to_server)
8992 _log.info("Feeding EWS (%s, %s new)" % (pluralize(len(ids_needing_review), "r? patch"), len(new_ids)))
9093 for attachment_id in new_ids: # Order doesn't really matter for the EWS.
 94 # Download patches from security sensitive bugs and upload them to the status server since the
 95 # EWS queues do not have permission to fetch them directly from Bugzilla.
 96 attachment_data = None
 97 if attachment_id in security_ids_needing_review:
 98 attachment = self._tool.bugs.fetch_attachment(attachment_id)
 99 if not attachment:
 100 _log.error('Failed to retrieve attachment {}'.format(attachment_id))
 101 continue
 102 self._tool.status_server.upload_attachment(attachment)
91103 self._tool.status_server.submit_to_ews(attachment_id)
92104 self._ids_sent_to_server.add(attachment_id)

Tools/Scripts/webkitpy/tool/commands/download.py

@@class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand):
217217
218218class ProcessAttachmentsMixin(object):
219219 def _fetch_list_of_patches_to_process(self, options, args, tool):
220  return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args)
 220 return filter(None, map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args))
221221
222222
223223class ProcessBugsMixin(object):
224224 def _fetch_list_of_patches_to_process(self, options, args, tool):
225225 all_patches = []
226226 for bug_id in args:
227  patches = tool.bugs.fetch_bug(bug_id).reviewed_patches()
 227 bug = tool.bugs.fetch_bug(bug_id)
 228 if not bug:
 229 continue
 230 patches = bug.reviewed_patches()
228231 _log.info("%s found on bug %s." % (pluralize(len(patches), "reviewed patch"), bug_id))
229232 all_patches += patches
230233 if not all_patches:
231234 _log.info("No reviewed patches found, looking for unreviewed patches.")
232235 for bug_id in args:
233  patches = tool.bugs.fetch_bug(bug_id).patches()
 236 bug = tool.bugs.fetch_bug(bug_id)
 237 if not bug:
 238 continue
 239 patches = bug.patches()
234240 _log.info("%s found on bug %s." % (pluralize(len(patches), "patch"), bug_id))
235241 all_patches += patches
236242 return all_patches

Tools/Scripts/webkitpy/tool/commands/earlywarningsystem.py

@@from optparse import make_option
3434
3535from webkitpy.common.config.committers import CommitterList
3636from webkitpy.common.config.ports import DeprecatedPort
 37from webkitpy.common.net.bugzilla import Bugzilla
3738from webkitpy.common.system.filesystem import FileSystem
3839from webkitpy.common.system.executive import ScriptError
3940from webkitpy.tool.bot.earlywarningsystemtask import EarlyWarningSystemTask, EarlyWarningSystemTaskDelegate

@@class AbstractEarlyWarningSystem(AbstractReviewQueue, EarlyWarningSystemTaskDele
150151 return self._group
151152
152153 def refetch_patch(self, patch):
153  return self._tool.bugs.fetch_attachment(patch.id())
 154 patch_id = patch.id()
 155 try:
 156 patch = self._tool.bugs.fetch_attachment(patch_id, throw_on_access_error=True)
 157 except Bugzilla.AccessError as e:
 158 # FIXME: Need a way to ask the status server to fetch the patch again. For now
 159 # we return the attachment as it was when it was originally uploaded to the
 160 # status server.
 161 if e.error_code == Bugzilla.AccessError.NOT_PERMITTED:
 162 patch = self._tool.status_server.fetch_attachment(patch_id)
 163 return patch
154164
155165 def report_flaky_tests(self, patch, flaky_test_results, results_archive):
156166 pass

Tools/Scripts/webkitpy/tool/commands/queries_unittest.py

@@class QueryCommandsTest(CommandsTest):
118118 "Bugs with attachments pending review:\n" \
119119 "http://webkit.org/b/bugid Description (age in days)\n" \
120120 "http://webkit.org/b/50001 Bug with a patch needing review. (0)\n" \
121  "Total: 1\n"
 121 "http://webkit.org/b/50007 Security bug with a patch needing review. (0)\n" \
 122 "Total: 2\n"
122123 self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr, options=options)
123124
124125 options.cc_email = None

Tools/Scripts/webkitpy/tool/commands/queues.py

@@from StringIO import StringIO
4040
4141from webkitpy.common.config.committervalidator import CommitterValidator
4242from webkitpy.common.config.ports import DeprecatedPort
43 from webkitpy.common.net.bugzilla import Attachment
 43from webkitpy.common.net.bugzilla import Bugzilla, Attachment
4444from webkitpy.common.system.executive import ScriptError
4545from webkitpy.tool.bot.botinfo import BotInfo
4646from webkitpy.tool.bot.commitqueuetask import CommitQueueTask, CommitQueueTaskDelegate

@@class AbstractPatchQueue(AbstractQueue):
217217 patch_id = self._tool.status_server.next_work_item(self.name)
218218 if not patch_id:
219219 return None
220  patch = self._tool.bugs.fetch_attachment(patch_id)
 220 try:
 221 patch = self._tool.bugs.fetch_attachment(patch_id, throw_on_access_error=True)
 222 except Bugzilla.AccessError as e:
 223 if e.error_code == Bugzilla.AccessError.NOT_PERMITTED:
 224 patch = self._tool.status_server.fetch_attachment(patch_id)
221225 if not patch:
222226 # FIXME: Using a fake patch because release_work_item has the wrong API.
223227 # We also don't really need to release the lock (although that's fine),

Tools/Scripts/webkitpy/tool/commands/queues_unittest.py

@@MOCK setting flag 'commit-queue' to '-' on attachment '10001' with comment 'Reje
143143- If you have committer rights please correct the error in Tools/Scripts/webkitpy/common/config/contributors.json by adding yourself to the file (no review needed). The commit-queue restarts itself every 2 hours. After restart the commit-queue will correctly respect your committer rights.'
144144Feeding commit-queue high priority items [10005], regular items [10000]
145145MOCK: update_work_items: commit-queue [10005, 10000]
146 Feeding EWS (1 r? patch, 1 new)
 146Feeding EWS (2 r? patches, 2 new)
 147MOCK: upload_attachment: 10008
 148MOCK: submit_to_ews: 10008
147149MOCK: submit_to_ews: 10002
148150""",
149151 "handle_unexpected_error": "Mock error message\n",

@@class AbstractPatchQueueTest(CommandsTest):
159161 queue._options = Mock()
160162 queue._options.port = None
161163 self.assertIsNone(queue._next_patch())
162  tool.status_server = MockStatusServer(work_items=[2, 10000, 10001])
 164 tool.status_server = MockStatusServer(work_items=[2, 10000, 10001, 10008])
163165 expected_stdout = "MOCK: fetch_attachment: 2 is not a known attachment id\n" # A mock-only message to prevent us from making mistakes.
164166 expected_logs = "MOCK: release_work_item: None 2\n"
165167 patch = OutputCapture().assert_outputs(self, queue._next_patch, expected_stdout=expected_stdout, expected_logs=expected_logs)
166168 # The patch.id() == 2 is ignored because it doesn't exist.
167169 self.assertEqual(patch.id(), 10000)
168170 self.assertEqual(queue._next_patch().id(), 10001)
 171 self.assertEqual(queue._next_patch().id(), 10008)
169172 self.assertEqual(queue._next_patch(), None) # When the queue is empty
170173
171174

Tools/Scripts/webkitpy/tool/commands/upload_unittest.py

11# Copyright (C) 2009 Google Inc. All rights reserved.
 2# Copyright (C) 2018 Apple Inc. All rights reserved.
23#
34# Redistribution and use in source and binary forms, with or without
45# modification, are permitted provided that the following conditions are

@@MOCK: submit_to_ews: 10001
159160"""
160161 self.assert_execute_outputs(Upload(), [50000], options=options, expected_logs=expected_logs)
161162
 163 def test_upload_of_security_sensitive_patch_with_no_review_and_ews(self):
 164 options = MockOptions()
 165 options.cc = None
 166 options.check_style = True
 167 options.check_style_filter = None
 168 options.comment = None
 169 options.description = 'MOCK description'
 170 options.non_interactive = False
 171 options.request_commit = False
 172 options.review = False
 173 options.ews = True
 174 options.sort_xcode_project = False
 175 options.suggest_reviewers = False
 176 expected_logs = """MOCK: user.open_url: file://...
 177Was that diff correct?
 178Obsoleting 1 old patch on bug 50007
 179MOCK add_patch_to_bug: bug_id=50007, description=MOCK description, mark_for_review=False, mark_for_commit_queue=False, mark_for_landing=False
 180MOCK: user.open_url: http://example.com/50007
 181MOCK: upload_attachment: 10008
 182MOCK: submit_to_ews: 10008
 183"""
 184 self.assert_execute_outputs(Upload(), [50007], options=options, expected_logs=expected_logs)
 185
162186 def test_mark_bug_fixed(self):
163187 tool = MockTool()
164188 tool._scm.last_svn_commit_log = lambda: "r9876 |"

Tools/Scripts/webkitpy/tool/main.py

@@from optparse import make_option
3333import os
3434import threading
3535
 36from webkitpy.common.checkout.scm import Git
3637from webkitpy.common.config.ports import DeprecatedPort
3738from webkitpy.common.host import Host
3839from webkitpy.common.net.irc import ircproxy

@@class WebKitPatch(MultiCommandTool, Host):
4647 make_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="enable all logging"),
4748 make_option("-d", "--directory", action="append", dest="patch_directories", default=[], help="Directory to look at for changed files"),
4849 make_option("--status-host", action="store", dest="status_host", type="string", help="Hostname (e.g. localhost or commit.webkit.org) where status updates should be posted."),
 50 make_option("--status-host-uses-https", action="store_true", default=False, dest="status_host_uses_https", help="Use HTTPS when querying the status host."),
4951 make_option("--bot-id", action="store", dest="bot_id", type="string", help="Identifier for this bot (if multiple bots are running for a queue)"),
5052 make_option("--irc-password", action="store", dest="irc_password", type="string", help="Password to use when communicating via IRC."),
5153 make_option("--seconds-to-sleep", action="store", default=120, type="int", help="Number of seconds to sleep in the task queue."),

@@class WebKitPatch(MultiCommandTool, Host):
8991 return self.scm().supports_local_commits()
9092 return True
9193
 94 @staticmethod
 95 def _status_server_api_key_from_git():
 96 try:
 97 if not Git.in_working_directory(os.getcwd()):
 98 return None
 99 return Git.read_git_config('webkit.status_api_key')
 100 except OSError as e:
 101 # Catch and ignore OSError exceptions such as "no such file
 102 # or directory" (OSError errno 2), which imply that the Git
 103 # command cannot be found/is not installed.
 104 pass
 105 return None
 106
 107 @staticmethod
 108 def _status_server_api_key():
 109 api_key = os.environ.get('WEBKIT_STATUS_API_KEY')
 110 if not api_key:
 111 api_key = WebKitPatch._status_server_api_key_from_git()
 112 return api_key
 113
92114 # FIXME: This may be unnecessary since we pass global options to all commands during execute() as well.
93115 def handle_global_options(self, options):
94116 self.initialize_scm(options.patch_directories)
 117
 118 api_key = self._status_server_api_key()
 119 if api_key:
 120 self.status_server.set_api_key(api_key)
 121
95122 if options.status_host:
96  self.status_server.set_host(options.status_host)
 123 self.status_server.set_host(options.status_host, use_https=options.status_host_uses_https)
97124 if options.bot_id:
98125 self.status_server.set_bot_id(options.bot_id)
99126 if options.irc_password:

Tools/Scripts/webkitpy/tool/steps/obsoletepatches.py

@@class ObsoletePatches(AbstractStep):
4646 if not self._options.obsolete_patches:
4747 return
4848 bug_id = state["bug_id"]
49  patches = self._tool.bugs.fetch_bug(bug_id).patches()
 49 bug = self._tool.bugs.fetch_bug(bug_id)
 50 if not bug:
 51 return
 52 patches = bug.patches()
5053 if not patches:
5154 return
5255 _log.info("Obsoleting %s on bug %s" % (pluralize(len(patches), "old patch"), bug_id))

Tools/Scripts/webkitpy/tool/steps/submittoews.py

@@class SubmitToEWS(AbstractStep):
3535
3636 def run(self, state):
3737 for attachment_id in state.get('attachment_ids', []):
 38 attachment = self._tool.bugs.fetch_attachment(attachment_id)
 39 if attachment is None:
 40 continue # Either Bugzilla is down or we do not have permission to view the attachment.
 41 if attachment.bug().is_security_sensitive():
 42 self._tool.status_server.upload_attachment(attachment)
3843 self._tool.status_server.submit_to_ews(attachment_id)