1# This file is part of Buildbot.  Buildbot is free software: you can
2# redistribute it and/or modify it under the terms of the GNU General Public
3# License as published by the Free Software Foundation, version 2.
4#
5# This program is distributed in the hope that it will be useful, but WITHOUT
6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
8# details.
9#
10# You should have received a copy of the GNU General Public License along with
11# this program; if not, write to the Free Software Foundation, Inc., 51
12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
13#
14# Copyright Buildbot Team Members
15#
16# code inspired/copied from contrib/github_buildbot
17#  and inspired from code from the Chromium project
18# otherwise, Andrew Melo <andrew.melo@gmail.com> wrote the rest
19# but "the rest" is pretty minimal
20
21import re
22from datetime import datetime
23
24from twisted.internet import defer
25from twisted.python import log
26from twisted.web import server
27
28from buildbot.plugins.db import get_plugins
29from buildbot.util import bytes2unicode
30from buildbot.util import datetime2epoch
31from buildbot.util import unicode2bytes
32from buildbot.www import resource
33
34
35class ChangeHookResource(resource.Resource):
36    # this is a cheap sort of template thingy
37    contentType = "text/html; charset=utf-8"
38    children = {}
39    needsReconfig = True
40
41    def __init__(self, dialects=None, master=None):
42        """
43        The keys of 'dialects' select a modules to load under
44        master/buildbot/www/hooks/
45        The value is passed to the module's getChanges function, providing
46        configuration options to the dialect.
47        """
48        super().__init__(master)
49
50        if dialects is None:
51            dialects = {}
52        self.dialects = dialects
53        self._dialect_handlers = {}
54        self.request_dialect = None
55        self._plugins = get_plugins("webhooks")
56
57    def reconfigResource(self, new_config):
58        self.dialects = new_config.www.get('change_hook_dialects', {})
59
60    def getChild(self, name, request):
61        return self
62
63    def render_GET(self, request):
64        """
65        Responds to events and starts the build process
66          different implementations can decide on what methods they will accept
67        """
68        return self.render_POST(request)
69
70    def render_POST(self, request):
71        """
72        Responds to events and starts the build process
73          different implementations can decide on what methods they will accept
74
75        :arguments:
76            request
77                the http request object
78        """
79        try:
80            d = self.getAndSubmitChanges(request)
81        except Exception:
82            d = defer.fail()
83
84        def ok(_):
85            request.setResponseCode(202)
86            request.finish()
87
88        def err(why):
89            code = 500
90            if why.check(ValueError):
91                code = 400
92                msg = unicode2bytes(why.getErrorMessage())
93            else:
94                log.err(why, "adding changes from web hook")
95                msg = b'Error processing changes.'
96            request.setResponseCode(code, msg)
97            request.write(msg)
98            request.finish()
99
100        d.addCallbacks(ok, err)
101
102        return server.NOT_DONE_YET
103
104    @defer.inlineCallbacks
105    def getAndSubmitChanges(self, request):
106        changes, src = yield self.getChanges(request)
107        if not changes:
108            request.write(b"no change found")
109        else:
110            yield self.submitChanges(changes, request, src)
111            request.write(unicode2bytes("{} change found".format(len(changes))))
112
113    def makeHandler(self, dialect):
114        """create and cache the handler object for this dialect"""
115        if dialect not in self.dialects:
116            m = "The dialect specified, '{}', wasn't whitelisted in change_hook".format(dialect)
117            log.msg(m)
118            log.msg("Note: if dialect is 'base' then it's possible your URL is "
119                    "malformed and we didn't regex it properly")
120            raise ValueError(m)
121
122        if dialect not in self._dialect_handlers:
123            if dialect not in self._plugins:
124                m = ("The dialect specified, '{}', is not registered as "
125                     "a buildbot.webhook plugin").format(dialect)
126                log.msg(m)
127                raise ValueError(m)
128            options = self.dialects[dialect]
129            if isinstance(options, dict) and 'custom_class' in options:
130                klass = options['custom_class']
131            else:
132                klass = self._plugins.get(dialect)
133            self._dialect_handlers[dialect] = klass(self.master, self.dialects[dialect])
134
135        return self._dialect_handlers[dialect]
136
137    @defer.inlineCallbacks
138    def getChanges(self, request):
139        """
140        Take the logic from the change hook, and then delegate it
141        to the proper handler
142
143        We use the buildbot plugin mechanisms to find out about dialects
144
145        and call getChanges()
146
147        the return value is a list of changes
148
149        if DIALECT is unspecified, a sample implementation is provided
150        """
151        uriRE = re.search(r'^/change_hook/?([a-zA-Z0-9_]*)', bytes2unicode(request.uri))
152
153        if not uriRE:
154            msg = "URI doesn't match change_hook regex: {}".format(request.uri)
155            log.msg(msg)
156            raise ValueError(msg)
157
158        changes = []
159        src = None
160
161        # Was there a dialect provided?
162        if uriRE.group(1):
163            dialect = uriRE.group(1)
164        else:
165            dialect = 'base'
166
167        handler = self.makeHandler(dialect)
168        changes, src = yield handler.getChanges(request)
169        return (changes, src)
170
171    @defer.inlineCallbacks
172    def submitChanges(self, changes, request, src):
173        for chdict in changes:
174            when_timestamp = chdict.get('when_timestamp')
175            if isinstance(when_timestamp, datetime):
176                chdict['when_timestamp'] = datetime2epoch(when_timestamp)
177            # unicodify stuff
178            for k in ('comments', 'author', 'committer', 'revision', 'branch', 'category',
179                    'revlink', 'repository', 'codebase', 'project'):
180                if k in chdict:
181                    chdict[k] = bytes2unicode(chdict[k])
182            if chdict.get('files'):
183                chdict['files'] = [bytes2unicode(f)
184                                for f in chdict['files']]
185            if chdict.get('properties'):
186                chdict['properties'] = dict((bytes2unicode(k), v)
187                                            for k, v in chdict['properties'].items())
188            chid = yield self.master.data.updates.addChange(src=bytes2unicode(src), **chdict)
189            log.msg("injected change {}".format(chid))
190