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