blob: db3dd754b0e32590cc34ae68f3b45bf664827631 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""This file sets up all the urls for monorail pages."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import logging
12import webapp2
13import settings
14
15from components import prpc
16
17from features import autolink
18from features import dateaction
19from features import banspammer
20from features import hotlistcreate
21from features import hotlistdetails
22from features import hotlistissues
23from features import hotlistissuescsv
24from features import hotlistpeople
25from features import filterrules
26from features import pubsub
27from features import userhotlists
28from features import inboundemail
29from features import notify
30from features import rerankhotlist
31from features import savedqueries
32from features import spammodel
33from features import spamtraining
34from features import componentexport
35
36from framework import banned
37from framework import clientmon
38from framework import csp_report
39from framework import deleteusers
40from framework import excessiveactivity
41from framework import trimvisitedpages
42from framework import framework_bizobj
43from framework import reap
44from framework import registerpages_helpers
45from framework import ts_mon_js
46from framework import urls
47from framework import warmup
48
49from project import peopledetail
50from project import peoplelist
51from project import project_constants
52from project import projectadmin
53from project import projectadminadvanced
54from project import projectexport
55from project import projectsummary
56from project import projectupdates
57from project import redirects
58
59from search import backendnonviewable
60from search import backendsearch
61
62from services import cachemanager_svc
63from services import client_config_svc
64
65from sitewide import custom_404
66from sitewide import groupadmin
67from sitewide import groupcreate
68from sitewide import groupdetail
69from sitewide import grouplist
70from sitewide import hostinghome
71from sitewide import moved
72from sitewide import projectcreate
73from sitewide import userprofile
74from sitewide import usersettings
75from sitewide import userclearbouncing
76from sitewide import userupdates
77
78from tracker import componentcreate
79from tracker import componentdetail
80from tracker import fieldcreate
81from tracker import fielddetail
82from tracker import issueadmin
83from tracker import issueadvsearch
84from tracker import issueattachment
85from tracker import issueattachmenttext
86from tracker import issuebulkedit
87from tracker import webcomponentspage
88from tracker import issuedetailezt
89from tracker import issueentry
90from tracker import issueentryafterlogin
91from tracker import issueexport
92from tracker import issueimport
93from tracker import issueoriginal
94from tracker import issuereindex
95from tracker import issuetips
96from tracker import spam
97from tracker import templatecreate
98from tracker import templatedetail
99from tracker import fltconversion
100
101from api import api_routes as api_routes_v0
102from api.v3 import api_routes as api_routes_v3
103
104
105class ServletRegistry(object):
106
107 _PROJECT_NAME_REGEX = project_constants.PROJECT_NAME_PATTERN
108 _USERNAME_REGEX = r'[-+\w=.%]+(@([-a-z0-9]+\.)*[a-z0-9]+)?'
109 _HOTLIST_ID_NAME_REGEX = r'\d+|[a-zA-Z][-0-9a-zA-Z\.]*'
110
111 def __init__(self):
112 self.routes = []
113
114 def _AddRoute(self, path_regex, servlet_class, method, does_write=False):
115 """Add a GET or POST handler to our webapp2 route list.
116
117 Args:
118 path_regex: string with webapp2 URL template regex.
119 servlet_class: a subclass of class Servlet.
120 method: string 'GET' or 'POST'.
121 does_write: True if the servlet could write to the database, we skip
122 registering such servlets when the site is in read_only mode. GET
123 handlers never write. Most, but not all, POST handlers do write.
124 """
125 if settings.read_only and does_write:
126 logging.info('Not registring %r because site is read-only', path_regex)
127 # TODO(jrobbins): register a helpful error page instead.
128 else:
129 self.routes.append(
130 webapp2.Route(path_regex, handler=servlet_class, methods=[method]))
131
132 def _SetupServlets(self, spec_dict, base='', post_does_write=True):
133 """Register each of the given servlets."""
134 for get_uri, servlet_class in spec_dict.items():
135 self._AddRoute(base + get_uri, servlet_class, 'GET')
136 post_uri = get_uri + ('edit.do' if get_uri.endswith('/') else '.do')
137 self._AddRoute(base + post_uri, servlet_class, 'POST',
138 does_write=post_does_write)
139
140 def _SetupProjectServlets(self, spec_dict, post_does_write=True):
141 """Register each of the given servlets in the project URI space."""
142 self._SetupServlets(
143 spec_dict, base='/p/<project_name:%s>' % self._PROJECT_NAME_REGEX,
144 post_does_write=post_does_write)
145
146 def _SetupUserServlets(self, spec_dict, post_does_write=True):
147 """Register each of the given servlets in the user URI space."""
148 self._SetupServlets(
149 spec_dict, base='/u/<viewed_username:%s>' % self._USERNAME_REGEX,
150 post_does_write=post_does_write)
151
152 def _SetupGroupServlets(self, spec_dict, post_does_write=True):
153 """Register each of the given servlets in the user group URI space."""
154 self._SetupServlets(
155 spec_dict, base='/g/<viewed_username:%s>' % self._USERNAME_REGEX,
156 post_does_write=post_does_write)
157
158 def _SetupUserHotlistServlets(self, spec_dict, post_does_write=True):
159 """ Register given user hotlist servlets in the user URI space."""
160 self._SetupServlets(
161 spec_dict,
162 base ='/u/<viewed_username:%s>/hotlists/<hotlist_id:%s>'
163 % (self._USERNAME_REGEX, self._HOTLIST_ID_NAME_REGEX),
164 post_does_write=post_does_write)
165
166 def Register(self, services):
167 """Register all the monorail request handlers."""
168 self._RegisterFrameworkHandlers()
169 self._RegisterSitewideHandlers()
170 self._RegisterProjectHandlers()
171 self._RegisterIssueHandlers()
172 self._RegisterWebComponentsHanders()
173 self._RegisterRedirects()
174 self._RegisterInboundMail()
175
176 # Register pRPC API routes
177 prpc_server = prpc.Server(
178 allowed_origins=client_config_svc.GetAllowedOriginsSet())
179 api_routes_v0.RegisterApiHandlers(prpc_server, services)
180 api_routes_v3.RegisterApiHandlers(prpc_server, services)
181 self.routes.extend(prpc_server.get_routes())
182
183 autolink.RegisterAutolink(services)
184 # Error pages should be the last to register.
185 self._RegisterErrorPages()
186 return self.routes
187
188 def _RegisterProjectHandlers(self):
189 """Register page and form handlers that operate within a project."""
190
191 self._SetupServlets({
192 # Note: the following are at URLS that are not externally accessible.
193 urls.NOTIFY_RULES_DELETED_TASK: notify.NotifyRulesDeletedTask,
194 })
195 self._SetupProjectServlets({
196 urls.ADMIN_INTRO: projectsummary.ProjectSummary,
197 urls.PEOPLE_LIST: peoplelist.PeopleList,
198 urls.PEOPLE_DETAIL: peopledetail.PeopleDetail,
199 urls.UPDATES_LIST: projectupdates.ProjectUpdates,
200 urls.ADMIN_META: projectadmin.ProjectAdmin,
201 urls.ADMIN_ADVANCED: projectadminadvanced.ProjectAdminAdvanced,
202 urls.ADMIN_EXPORT: projectexport.ProjectExport,
203 urls.ADMIN_EXPORT_JSON: projectexport.ProjectExportJSON,
204 })
205
206 def _RegisterIssueHandlers(self):
207 """Register page and form handlers for the issue tracker."""
208 self._SetupServlets({
209 # Note: the following are at URLs that are not externaly accessible.
210 urls.BACKEND_SEARCH: backendsearch.BackendSearch,
211 urls.BACKEND_NONVIEWABLE: backendnonviewable.BackendNonviewable,
212 urls.RECOMPUTE_DERIVED_FIELDS_TASK:
213 filterrules.RecomputeDerivedFieldsTask,
214 urls.REINDEX_QUEUE_CRON: filterrules.ReindexQueueCron,
215 urls.NOTIFY_ISSUE_CHANGE_TASK: notify.NotifyIssueChangeTask,
216 urls.NOTIFY_BLOCKING_CHANGE_TASK: notify.NotifyBlockingChangeTask,
217 urls.NOTIFY_BULK_CHANGE_TASK: notify.NotifyBulkChangeTask,
218 urls.NOTIFY_APPROVAL_CHANGE_TASK: notify.NotifyApprovalChangeTask,
219 urls.OUTBOUND_EMAIL_TASK: notify.OutboundEmailTask,
220 urls.SPAM_DATA_EXPORT_TASK: spammodel.TrainingDataExportTask,
221 urls.DATE_ACTION_CRON: dateaction.DateActionCron,
222 urls.SPAM_TRAINING_CRON: spamtraining.TrainSpamModelCron,
223 urls.PUBLISH_PUBSUB_ISSUE_CHANGE_TASK:
224 pubsub.PublishPubsubIssueChangeTask,
225 urls.ISSUE_DATE_ACTION_TASK: dateaction.IssueDateActionTask,
226 urls.COMPONENT_DATA_EXPORT_CRON:
227 componentexport.ComponentTrainingDataExport,
228 urls.COMPONENT_DATA_EXPORT_TASK:
229 componentexport.ComponentTrainingDataExportTask,
230 urls.FLT_ISSUE_CONVERSION_TASK: fltconversion.FLTConvertTask,
231 })
232
233 self._SetupProjectServlets(
234 {
235 urls.ISSUE_APPROVAL:
236 registerpages_helpers.MakeRedirectInScope(
237 urls.ISSUE_DETAIL, 'p', keep_qs=True),
238 urls.ISSUE_LIST:
239 webcomponentspage.WebComponentsPage,
240 urls.ISSUE_LIST_NEW_TEMP:
241 registerpages_helpers.MakeRedirectInScope(
242 urls.ISSUE_LIST, 'p', keep_qs=True),
243 urls.ISSUE_REINDEX:
244 issuereindex.IssueReindex,
245 urls.ISSUE_DETAIL_FLIPPER_NEXT:
246 issuedetailezt.FlipperNext,
247 urls.ISSUE_DETAIL_FLIPPER_PREV:
248 issuedetailezt.FlipperPrev,
249 urls.ISSUE_DETAIL_FLIPPER_LIST:
250 issuedetailezt.FlipperList,
251 urls.ISSUE_DETAIL_FLIPPER_INDEX:
252 issuedetailezt.FlipperIndex,
253 urls.ISSUE_DETAIL_LEGACY:
254 registerpages_helpers.MakeRedirectInScope(
255 urls.ISSUE_DETAIL, 'p', keep_qs=True),
256 urls.ISSUE_WIZARD:
257 webcomponentspage.WebComponentsPage,
258 urls.ISSUE_ENTRY:
259 issueentry.IssueEntry,
260 urls.ISSUE_ENTRY_NEW:
261 webcomponentspage.WebComponentsPage,
262 urls.ISSUE_ENTRY_AFTER_LOGIN:
263 issueentryafterlogin.IssueEntryAfterLogin,
264 urls.ISSUE_TIPS:
265 issuetips.IssueSearchTips,
266 urls.ISSUE_ATTACHMENT:
267 issueattachment.AttachmentPage,
268 urls.ISSUE_ATTACHMENT_TEXT:
269 issueattachmenttext.AttachmentText,
270 urls.ISSUE_BULK_EDIT:
271 issuebulkedit.IssueBulkEdit,
272 urls.COMPONENT_CREATE:
273 componentcreate.ComponentCreate,
274 urls.COMPONENT_DETAIL:
275 componentdetail.ComponentDetail,
276 urls.FIELD_CREATE:
277 fieldcreate.FieldCreate,
278 urls.FIELD_DETAIL:
279 fielddetail.FieldDetail,
280 urls.TEMPLATE_CREATE:
281 templatecreate.TemplateCreate,
282 urls.TEMPLATE_DETAIL:
283 templatedetail.TemplateDetail,
284 urls.WIKI_LIST:
285 redirects.WikiRedirect,
286 urls.WIKI_PAGE:
287 redirects.WikiRedirect,
288 urls.SOURCE_PAGE:
289 redirects.SourceRedirect,
290 urls.ADMIN_STATUSES:
291 issueadmin.AdminStatuses,
292 urls.ADMIN_LABELS:
293 issueadmin.AdminLabels,
294 urls.ADMIN_RULES:
295 issueadmin.AdminRules,
296 urls.ADMIN_TEMPLATES:
297 issueadmin.AdminTemplates,
298 urls.ADMIN_COMPONENTS:
299 issueadmin.AdminComponents,
300 urls.ADMIN_VIEWS:
301 issueadmin.AdminViews,
302 urls.ISSUE_ORIGINAL:
303 issueoriginal.IssueOriginal,
304 urls.ISSUE_EXPORT:
305 issueexport.IssueExport,
306 urls.ISSUE_EXPORT_JSON:
307 issueexport.IssueExportJSON,
308 urls.ISSUE_IMPORT:
309 issueimport.IssueImport,
310 urls.SPAM_MODERATION_QUEUE:
311 spam.ModerationQueue,
312 })
313
314 # GETs for /issues/detail are now handled by the web components page.
315 base = '/p/<project_name:%s>' % self._PROJECT_NAME_REGEX
316 self._AddRoute(base + urls.ISSUE_DETAIL,
317 webcomponentspage.WebComponentsPage, 'GET')
318
319 self._SetupUserServlets({
320 urls.SAVED_QUERIES: savedqueries.SavedQueries,
321 urls.HOTLISTS: userhotlists.UserHotlists,
322 })
323
324 user_hotlists_redir = registerpages_helpers.MakeRedirectInScope(
325 urls.HOTLISTS, 'u', keep_qs=True)
326 self._SetupUserServlets({
327 '/hotlists/': user_hotlists_redir,
328 })
329
330 # These servlets accept POST, but never write to the database, so they can
331 # still be used when the site is read-only.
332 self._SetupProjectServlets({
333 urls.ISSUE_ADVSEARCH: issueadvsearch.IssueAdvancedSearch,
334 }, post_does_write=False)
335
336 list_redir = registerpages_helpers.MakeRedirectInScope(
337 urls.ISSUE_LIST, 'p', keep_qs=True)
338 self._SetupProjectServlets({
339 '': list_redir,
340 '/': list_redir,
341 '/issues': list_redir,
342 '/issues/': list_redir,
343 })
344
345 list_redir = registerpages_helpers.MakeRedirect(urls.ISSUE_LIST)
346 self._SetupServlets({
347 '/issues': list_redir,
348 '/issues/': list_redir,
349 })
350
351 def _RegisterFrameworkHandlers(self):
352 """Register page and form handlers for framework functionality."""
353 self._SetupServlets({
354 urls.CSP_REPORT: csp_report.CSPReportPage,
355
356 # These are only shown to users if specific conditions are met.
357 urls.EXCESSIVE_ACTIVITY: excessiveactivity.ExcessiveActivity,
358 urls.BANNED: banned.Banned,
359 urls.PROJECT_MOVED: moved.ProjectMoved,
360
361 # These are not externally accessible
362 urls.RAMCACHE_CONSOLIDATE_CRON: cachemanager_svc.RamCacheConsolidate,
363 urls.REAP_CRON: reap.Reap,
364 urls.SPAM_DATA_EXPORT_CRON: spammodel.TrainingDataExport,
365 urls.LOAD_API_CLIENT_CONFIGS_CRON: (
366 client_config_svc.LoadApiClientConfigs),
367 urls.CLIENT_MON: clientmon.ClientMonitor,
368 urls.TRIM_VISITED_PAGES_CRON: trimvisitedpages.TrimVisitedPages,
369 urls.TS_MON_JS: ts_mon_js.MonorailTSMonJSHandler,
370 urls.WARMUP: warmup.Warmup,
371 urls.START: warmup.Start,
372 urls.STOP: warmup.Stop
373 })
374
375 def _RegisterSitewideHandlers(self):
376 """Register page and form handlers that aren't associated with projects."""
377 self._SetupServlets({
378 urls.PROJECT_CREATE: projectcreate.ProjectCreate,
379 # The user settings page is a site-wide servlet, not under /u/.
380 urls.USER_SETTINGS: usersettings.UserSettings,
381 urls.HOSTING_HOME: hostinghome.HostingHome,
382 urls.GROUP_CREATE: groupcreate.GroupCreate,
383 urls.GROUP_LIST: grouplist.GroupList,
384 urls.GROUP_DELETE: grouplist.GroupList,
385 urls.HOTLIST_CREATE: hotlistcreate.HotlistCreate,
386 urls.BAN_SPAMMER_TASK: banspammer.BanSpammerTask,
387 urls.WIPEOUT_SYNC_CRON: deleteusers.WipeoutSyncCron,
388 urls.SEND_WIPEOUT_USER_LISTS_TASK: deleteusers.SendWipeoutUserListsTask,
389 urls.DELETE_WIPEOUT_USERS_TASK: deleteusers.DeleteWipeoutUsersTask,
390 urls.DELETE_USERS_TASK: deleteusers.DeleteUsersTask,
391 })
392
393 self._SetupUserServlets({
394 urls.USER_PROFILE: userprofile.UserProfile,
395 urls.USER_PROFILE_POLYMER: userprofile.UserProfilePolymer,
396 urls.BAN_USER: userprofile.BanUser,
397 urls.BAN_SPAMMER: banspammer.BanSpammer,
398 urls.USER_CLEAR_BOUNCING: userclearbouncing.UserClearBouncing,
399 urls.USER_UPDATES_PROJECTS: userupdates.UserUpdatesProjects,
400 urls.USER_UPDATES_DEVELOPERS: userupdates.UserUpdatesDevelopers,
401 urls.USER_UPDATES_MINE: userupdates.UserUpdatesIndividual,
402 })
403
404 self._SetupUserHotlistServlets({
405 urls.HOTLIST_ISSUES: hotlistissues.HotlistIssues,
406 urls.HOTLIST_ISSUES_CSV: hotlistissuescsv.HotlistIssuesCsv,
407 urls.HOTLIST_PEOPLE: hotlistpeople.HotlistPeopleList,
408 urls.HOTLIST_DETAIL: hotlistdetails.HotlistDetails,
409 urls.HOTLIST_RERANK_JSON: rerankhotlist.RerankHotlistIssue,
410 })
411
412 profile_redir = registerpages_helpers.MakeRedirectInScope(
413 urls.USER_PROFILE, 'u')
414 self._SetupUserServlets({'': profile_redir})
415
416 self._SetupGroupServlets({
417 urls.GROUP_DETAIL: groupdetail.GroupDetail,
418 urls.GROUP_ADMIN: groupadmin.GroupAdmin,
419 })
420
421 def _RegisterWebComponentsHanders(self):
422 """Register page handlers that are handled by WebComponentsPage."""
423 self._AddRoute('/', webcomponentspage.ProjectListPage, 'GET')
424 self._AddRoute(
425 '/hotlists<unused:.*>', webcomponentspage.WebComponentsPage, 'GET')
426 self._AddRoute('/users<unused:.*>', webcomponentspage.WebComponentsPage,
427 'GET')
428
429 def _RegisterRedirects(self):
430 """Register redirects among pages inside monorail."""
431 redirect = registerpages_helpers.MakeRedirect('/')
432 self._SetupServlets(
433 {
434 '/projects/': redirect,
435 '/projects': redirect,
436 '/hosting/': redirect,
437 '/hosting': redirect,
438 '/p': redirect,
439 '/p/': redirect,
440 '/u': redirect,
441 '/u/': redirect,
442 '/': redirect,
443 })
444
445 redirect = registerpages_helpers.MakeRedirectInScope(
446 urls.PEOPLE_LIST, 'p')
447 self._SetupProjectServlets({
448 '/people': redirect,
449 '/people/': redirect,
450 })
451
452 redirect = registerpages_helpers.MakeRedirect(urls.GROUP_LIST)
453 self._SetupServlets({'/g': redirect})
454
455 group_redir = registerpages_helpers.MakeRedirectInScope(
456 urls.USER_PROFILE, 'g')
457 self._SetupGroupServlets({'': group_redir})
458
459 def _RegisterInboundMail(self):
460 """Register a handler for inbound email and email bounces."""
461 self.routes.append(webapp2.Route(
462 '/_ah/mail/<project_addr:.+>',
463 handler=inboundemail.InboundEmail,
464 methods=['POST', 'GET']))
465 self.routes.append(webapp2.Route(
466 '/_ah/bounce',
467 handler=inboundemail.BouncedEmail,
468 methods=['POST', 'GET']))
469
470 def _RegisterErrorPages(self):
471 """Register handlers for errors."""
472 self._AddRoute(
473 '/p/<project_name:%s>/<unrecognized:.+>' % self._PROJECT_NAME_REGEX,
474 custom_404.ErrorPage, 'GET')