-
Notifications
You must be signed in to change notification settings - Fork 54
/
Copy pathplugins.py
205 lines (159 loc) · 6.89 KB
/
plugins.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
from urllib.parse import urlparse
from trac.config import ListOption
from trac.core import Component, implements
from trac.web.chrome import INavigationContributor
from trac.web.api import IRequestFilter, IRequestHandler, RequestDone
from trac.web.auth import LoginModule
from trac.wiki.web_ui import WikiModule
from trac.ticket.query import QueryModule
from trac.util.html import tag
from tracext.github import GitHubLoginModule, GitHubBrowser
from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm
from django.utils.encoding import iri_to_uri
from django.utils.http import url_has_allowed_host_and_scheme
class CustomTheme(Component):
implements(IRequestFilter)
def pre_process_request(self, req, handler):
return handler
def post_process_request(self, req, template, data, metadata):
req.chrome["theme"] = "django_theme.html"
return template, data, metadata
class CustomWikiModule(WikiModule):
"""Works in combination with the CustomNavigationBar and replaces
the default wiki module. Has a different logic for active item
handling.
"""
def get_active_navigation_item(self, req):
pagename = req.args.get("page")
if pagename == "Reports":
return "custom_reports"
return "wiki"
class CustomNewTicket(Component):
"""Hide certain options for the new ticket page"""
implements(IRequestFilter, IRequestHandler)
hidden_fields = frozenset(
["stage", "needs_tests", "needs_docs", "needs_better_patch"]
)
def match_request(self, req):
return req.path_info == "/simpleticket"
def process_request(self, req):
req.redirect(req.href.newticket())
def pre_process_request(self, req, handler):
return handler
def post_process_request(self, req, template, data, metadata):
if data is None:
data = {}
if req.path_info == "/newticket" and not data.get("preview_mode", False):
simple_interface = "TICKET_BATCH_MODIFY" not in req.perm
if simple_interface and "fields" in data:
data["fields"] = [
f for f in data["fields"] if f["name"] not in self.hidden_fields
]
data["simple_interface"] = simple_interface
template = "custom_ticket.html"
return template, data, metadata
class CustomNavigationBar(Component):
"""Implements some more items for the navigation bar."""
implements(INavigationContributor)
def get_active_navigation_item(self, req):
return "custom_reports"
def get_navigation_items(self, req):
return [
(
"mainnav",
"custom_reports",
tag.a("Reports", href=req.href.wiki("Reports")),
),
]
class GitHubBrowserWithSVNChangesets(GitHubBrowser):
def _format_changeset_link(self, formatter, ns, chgset, label, fullmatch=None):
# Dead-simple version for SVN changesets.
if chgset.isnumeric():
href = formatter.href.changeset(chgset, None, "/")
return tag.a(label, class_="changeset", href=href)
# Fallback to the default implementation.
return super(GitHubBrowserWithSVNChangesets, self)._format_changeset_link(
formatter, ns, chgset, label, fullmatch
)
class PlainLoginComponent(Component):
"""
Enable login through a plain HTML form (no more HTTP basic auth)
"""
implements(IRequestHandler)
def match_request(self, req):
return req.path_info == "/login"
def process_request(self, req):
if req.method == "POST":
return self.do_post(req)
elif req.method == "GET":
return self.do_get(req)
else:
req.send_response(405)
raise RequestDone
def do_get(self, req):
# Not 100% sure why, but for some links (RSS especially) Trac likes
# to generate URLs pointing to `/login?referer=<the actual link>` when
# the user is already authenticated.
if req.is_authenticated:
req.redirect(self._get_safe_redirect_url(req))
return "plainlogin.html", {
"form": AuthenticationForm(),
"referer": req.args.get("referer", ""),
}
def do_post(self, req):
form = AuthenticationForm(data=req.args)
if form.is_valid():
req.environ["REMOTE_USER"] = form.get_user().username
LoginModule(self.compmgr)._do_login(req)
req.redirect(self._get_safe_redirect_url(req))
return "plainlogin.html", {"form": form, "referer": req.args.get("referer", "")}
def _get_safe_redirect_url(self, req):
host = urlparse(req.base_url).hostname
redirect_url = iri_to_uri(req.args.get("referer", ""))
if not redirect_url:
redirect_url = settings.LOGIN_REDIRECT_URL
elif not url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=[host]):
redirect_url = settings.LOGIN_REDIRECT_URL
return redirect_url
class ReservedUsernamesComponent(Component):
"""
Prevents some users from logging in on the website. Useful for example to prevent
users whose name clashes with a permission group.
The list of reserved usernames can be configured in trac.ini by specifying
`reserved.usernames` as a comma-separated list under the [djangoplugin] header.
If such a user tries to log in, they will be logged out and redirected to the login
page with a message telling them to choose a different account.
"""
implements(IRequestFilter)
reserved_names = ListOption(
section="djangoplugin",
name="reserved_usernames",
default="authenticated",
doc="A list (comma-separated) of usernames that won't be allowed to log in",
)
def pre_process_request(self, req, handler):
if req.authname in self.reserved_names:
self.force_logout_and_redirect(req)
return handler
def force_logout_and_redirect(self, req):
component = GitHubLoginModule(self.env)
# Trac's builtin LoginModule silently ignores logout requests that aren't POST,
# so we need to be a bit creative here
req.environ["REQUEST_METHOD"] = "POST"
try:
GitHubLoginModule(self.env)._do_logout(req)
except RequestDone:
pass # catch the redirection exception so we can make our own
req.redirect("/login?reserved=%s" % req.authname)
def post_process_request(self, req, template, data, metadata):
return template, data, metadata # required by Trac to exist
class RenameQueryTitleComponent(QueryModule):
"""
Change the title of the /query page so that the navmenu entry matches the
page's <h1>.
"""
def display_html(self, req, query):
template_name, context = super().display_html(req, query)
context["title"] = "View Tickets"
return template_name, context