1 | # -*- coding: utf-8 -*- |
---|
2 | """ ServerSideRedirectPlugin for Trac |
---|
3 | |
---|
4 | Copyright (c) 2008 Martin Scharrer <martin@scharrer-online.de> |
---|
5 | This is Free Software under the BSD or GPL v3 or later license. |
---|
6 | $Id: plugin.py 14859 2015-08-06 02:34:50Z rjollos $ |
---|
7 | """ |
---|
8 | |
---|
9 | __url__ = ur"$URL: //trac-hacks.org/svn/serversideredirectplugin/0.11/tracserversideredirect/plugin.py $"[6:-2] |
---|
10 | __author__ = ur"$Author: rjollos $"[9:-2] |
---|
11 | __revision__ = ur"$Rev: 14859 $"[6:-2] |
---|
12 | __date__ = ur"$Date: 2015-08-06 02:34:50 +0000 (Thu, 06 Aug 2015) $"[7:-2] |
---|
13 | |
---|
14 | import re |
---|
15 | |
---|
16 | from genshi.builder import tag |
---|
17 | from trac.core import * |
---|
18 | from trac.mimeview.api import Context |
---|
19 | from trac.web.api import IRequestHandler, IRequestFilter, RequestDone |
---|
20 | from trac.wiki.api import IWikiMacroProvider |
---|
21 | from trac.wiki.model import WikiPage |
---|
22 | |
---|
23 | from tracextracturl.extracturl import extract_url |
---|
24 | |
---|
25 | MACRO = re.compile(r'.*\[\[[rR]edirect\((.*)\)\]\]') |
---|
26 | |
---|
27 | |
---|
28 | class ServerSideRedirectPlugin(Component): |
---|
29 | """This Trac plug-in implements a server sided redirect functionality. |
---|
30 | The user interface is the wiki macro `Redirect` (alternatively `redirect`). |
---|
31 | |
---|
32 | == Description == |
---|
33 | Website: https://trac-hacks.org/wiki/ServerSideRedirectPlugin |
---|
34 | |
---|
35 | `$Id: plugin.py 14859 2015-08-06 02:34:50Z rjollos $` |
---|
36 | |
---|
37 | This plug-in allow to place a redirect macro at the start of any wiki |
---|
38 | page which will cause an server side redirect when the wiki page is |
---|
39 | viewed. |
---|
40 | |
---|
41 | This plug-in is compatible (i.e. can be used) with the client side |
---|
42 | redirect macro TracRedirect but doesn't depend on it. Because the |
---|
43 | redirect is caused by the server (using a HTTP redirect request to the |
---|
44 | browser) it is much faster and less noticeable for the user. The |
---|
45 | back-link feature of TracRedirect can also be used for server side |
---|
46 | redirected pages because both generate the same URL attributes. |
---|
47 | |
---|
48 | To edit a redirecting wiki page access its URL with `?action=edit` |
---|
49 | appended. To view the page either use `?action=view`, which will print |
---|
50 | the redirect target (if TracRedirect isn't active, which will redirect |
---|
51 | the wiki using client side code), or `?redirect=no` which disables |
---|
52 | redirection of both the ServerSideRedirectPlugin and TracRedirect |
---|
53 | plug-in. |
---|
54 | |
---|
55 | Direct after the redirect target is added (or modified) Trac will |
---|
56 | automatically reload it, as it does with all wiki pages. This plug-in |
---|
57 | will detect this and not redirect but display the wiki page with the |
---|
58 | redirect target URL printed to provide feedback about the successful |
---|
59 | change. However, further visits will trigger the redirect. |
---|
60 | |
---|
61 | == Usage Examples == |
---|
62 | The following 'macro' at the begin of the wiki page will cause a |
---|
63 | redirect to the ''!OtherWikiPage''. |
---|
64 | {{{ |
---|
65 | [[redirect(OtherWikiPage)]] |
---|
66 | [[Redirect(OtherWikiPage)]] |
---|
67 | }}} |
---|
68 | Any other [TracLinks TracLink] can be used: |
---|
69 | {{{ |
---|
70 | [[redirect(wiki:OtherWikiPage)]] |
---|
71 | [[Redirect(wiki:OtherWikiPage)]] |
---|
72 | [[redirect(source:/trunk/file.py)]] |
---|
73 | [[Redirect(source:/trunk/file.py)]] |
---|
74 | [[redirect(http://www.example.com/)]] |
---|
75 | [[Redirect(http://www.example.com/)]] |
---|
76 | }}} |
---|
77 | """ |
---|
78 | implements(IRequestHandler, IRequestFilter, IWikiMacroProvider) |
---|
79 | |
---|
80 | redirect_target = '' |
---|
81 | |
---|
82 | def expand_macro(self, formatter, name, content): |
---|
83 | """Print redirect notice after edit.""" |
---|
84 | |
---|
85 | target = extract_url(self.env, formatter.context, content) |
---|
86 | if not target: |
---|
87 | target = formatter.context.req.href.wiki(content) |
---|
88 | |
---|
89 | return tag.div( |
---|
90 | tag.strong('This page redirects to: '), |
---|
91 | tag.a(content, href=target), |
---|
92 | class_='system-message', |
---|
93 | id='notice' |
---|
94 | ) |
---|
95 | |
---|
96 | def get_macros(self): |
---|
97 | """Provide but do not redefine the 'redirect' macro.""" |
---|
98 | get = self.env.config.get |
---|
99 | if get('components', 'redirect.*') == 'enabled' or \ |
---|
100 | get('components', 'redirect.redirect.*') == 'enabled' or \ |
---|
101 | get('components', |
---|
102 | 'redirect.redirect.tracredirect') == 'enabled': |
---|
103 | return ['Redirect'] |
---|
104 | else: |
---|
105 | return ['redirect', 'Redirect'] |
---|
106 | |
---|
107 | def get_macro_description(self, name): |
---|
108 | if name == 'Redirect': |
---|
109 | return self.__doc__ |
---|
110 | else: |
---|
111 | return "See macro `Redirect`." |
---|
112 | |
---|
113 | # IRequestHandler methods |
---|
114 | |
---|
115 | def match_request(self, req): |
---|
116 | """Only handle request when selected from `pre_process_request`.""" |
---|
117 | return False |
---|
118 | |
---|
119 | def process_request(self, req): |
---|
120 | """Redirect to pre-selected target.""" |
---|
121 | if self.redirect_target or self._check_redirect(req): |
---|
122 | target = self.redirect_target |
---|
123 | |
---|
124 | # Check for self-redirect: |
---|
125 | if target and target == req.href(req.path_info): |
---|
126 | message = tag.div('Please ', |
---|
127 | tag.a("change the redirect target", |
---|
128 | href=target + "?action=edit"), |
---|
129 | ' to another page.', |
---|
130 | class_="system-message") |
---|
131 | data = { |
---|
132 | 'title': "Page redirects to itself!", |
---|
133 | 'message': message, |
---|
134 | 'type': 'TracError' |
---|
135 | } |
---|
136 | req.send_error(data['title'], status=409, |
---|
137 | env=self.env, data=data) |
---|
138 | raise RequestDone |
---|
139 | |
---|
140 | # Check for redirect pair, i.e. A->B, B->A |
---|
141 | redirected_from = req.args.get('redirectedfrom', '') |
---|
142 | if target and target == req.href.wiki(redirected_from): |
---|
143 | message = tag.div( |
---|
144 | 'Please change the redirect target from either ', |
---|
145 | tag.a("this page", |
---|
146 | href=req.href(req.path_info, action="edit")), |
---|
147 | ' or ', |
---|
148 | tag.a("the redirecting page", |
---|
149 | href=target + "?action=edit"), |
---|
150 | '.', class_="system-message") |
---|
151 | data = { |
---|
152 | 'title': "Redirect target redirects back to this page!", |
---|
153 | 'message': message, |
---|
154 | 'type': 'TracError' |
---|
155 | } |
---|
156 | req.send_error(data['title'], status=409, |
---|
157 | env=self.env, data=data) |
---|
158 | raise RequestDone |
---|
159 | |
---|
160 | # Add back link information for internal links: |
---|
161 | if target and target[0] == '/': |
---|
162 | redirectfrom = "redirectedfrom=" + req.path_info[6:] |
---|
163 | # anchor should be the last in url |
---|
164 | # according to http://trac.edgewall.org/ticket/8072 |
---|
165 | tgt, query, anchor = self.split_link(target) |
---|
166 | if not query: |
---|
167 | query = "?" + redirectfrom |
---|
168 | else: |
---|
169 | query += "&" + redirectfrom |
---|
170 | target = tgt + query + anchor |
---|
171 | req.redirect(target) |
---|
172 | raise RequestDone |
---|
173 | raise TracError("Invalid redirect target!") |
---|
174 | |
---|
175 | def _check_redirect(self, req): |
---|
176 | """Checks if the request should be redirected.""" |
---|
177 | if req.path_info == '/' or req.path_info == '/wiki': |
---|
178 | wiki = 'WikiStart' |
---|
179 | elif not req.path_info.startswith('/wiki/'): |
---|
180 | return False |
---|
181 | else: |
---|
182 | wiki = req.path_info[6:] |
---|
183 | |
---|
184 | wp = WikiPage(self.env, wiki, req.args.get('version')) |
---|
185 | |
---|
186 | if not wp.exists: |
---|
187 | return False |
---|
188 | |
---|
189 | # Check for redirect "macro": |
---|
190 | m = MACRO.match(wp.text) |
---|
191 | if not m: |
---|
192 | return False |
---|
193 | wikitarget = m.groups()[0] |
---|
194 | ctxt = Context.from_request(req) |
---|
195 | self.redirect_target = extract_url(self.env, ctxt, wikitarget) |
---|
196 | if not self.redirect_target: |
---|
197 | self.redirect_target = req.href.wiki(wikitarget) |
---|
198 | return True |
---|
199 | |
---|
200 | # IRequestFilter methods |
---|
201 | |
---|
202 | def pre_process_request(self, req, handler): |
---|
203 | from trac.wiki.web_ui import WikiModule |
---|
204 | args = req.args |
---|
205 | |
---|
206 | if not isinstance(handler, WikiModule): |
---|
207 | return handler |
---|
208 | if not req.path_info.startswith('/wiki/') and \ |
---|
209 | not req.path_info == '/wiki' and not req.path_info == '/': |
---|
210 | self.log.debug("SSR: no redirect: Path is not a wiki path") |
---|
211 | return handler |
---|
212 | if req.method != 'GET': |
---|
213 | self.log.debug("SSR: no redirect: No GET request") |
---|
214 | return handler |
---|
215 | if 'action' in args: |
---|
216 | self.log.debug("SSR: no redirect: action=" + args['action']) |
---|
217 | return handler |
---|
218 | if 'version' in args: |
---|
219 | self.log.debug("SSR: no redirect: version=...") |
---|
220 | return handler |
---|
221 | if 'redirect' in args and args['redirect'].lower() == 'no': |
---|
222 | self.log.debug("SSR: no redirect: redirect=no") |
---|
223 | return handler |
---|
224 | if req.environ.get('HTTP_REFERER', '').find('action=edit') != -1: |
---|
225 | self.log.debug("SSR: no redirect: HTTP_REFERER includes " |
---|
226 | "action=edit") |
---|
227 | return handler |
---|
228 | if self._check_redirect(req): |
---|
229 | self.log.debug("SSR: redirect!") |
---|
230 | return self |
---|
231 | self.log.debug("SSR: no redirect: No redirect macro found.") |
---|
232 | return handler |
---|
233 | |
---|
234 | def post_process_request(self, req, template, data, content_type): |
---|
235 | return template, data, content_type |
---|
236 | |
---|
237 | # Internal methods |
---|
238 | |
---|
239 | def split_link(self, target): |
---|
240 | """Split a target along "?" and "#" in `(path, query, fragment)`.""" |
---|
241 | query = fragment = '' |
---|
242 | idx = target.find('#') |
---|
243 | if idx >= 0: |
---|
244 | target, fragment = target[:idx], target[idx:] |
---|
245 | idx = target.find('?') |
---|
246 | if idx >= 0: |
---|
247 | target, query = target[:idx], target[idx:] |
---|
248 | return target, query, fragment |
---|