| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2008-2010 Martin Scharrer <martin@scharrer-online.de> |
|---|
| 4 | # Copyright (C) 2015 Ryan J Ollos <ryan.j.ollos@gmail.com> |
|---|
| 5 | # All rights reserved. |
|---|
| 6 | # |
|---|
| 7 | # This software is licensed as described in the file COPYING, which |
|---|
| 8 | # you should have received as part of this distribution. |
|---|
| 9 | |
|---|
| 10 | import re |
|---|
| 11 | |
|---|
| 12 | from genshi.builder import tag |
|---|
| 13 | from trac.core import * |
|---|
| 14 | from trac.util.text import stripws |
|---|
| 15 | from trac.util.translation import _, tag_ |
|---|
| 16 | from trac.web.api import IRequestFilter, IRequestHandler |
|---|
| 17 | from trac.web.chrome import web_context |
|---|
| 18 | from trac.wiki.api import IWikiMacroProvider |
|---|
| 19 | from trac.wiki.formatter import concat_path_query_fragment |
|---|
| 20 | from trac.wiki.model import WikiPage |
|---|
| 21 | from trac.wiki.web_ui import WikiModule |
|---|
| 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 | This plug-in allow to place a redirect macro at the start of any wiki |
|---|
| 36 | page which will cause an server side redirect when the wiki page is |
|---|
| 37 | viewed. |
|---|
| 38 | |
|---|
| 39 | This plug-in is compatible (i.e. can be used) with the client side |
|---|
| 40 | redirect macro TracRedirect but doesn't depend on it. Because the |
|---|
| 41 | redirect is caused by the server (using a HTTP redirect request to the |
|---|
| 42 | browser) it is much faster and less noticeable for the user. The |
|---|
| 43 | back-link feature of TracRedirect can also be used for server side |
|---|
| 44 | redirected pages because both generate the same URL attributes. |
|---|
| 45 | |
|---|
| 46 | To edit a redirecting wiki page access its URL with `?action=edit` |
|---|
| 47 | appended. To view the page either use `?action=view`, which will print |
|---|
| 48 | the redirect target (if TracRedirect isn't active, which will redirect |
|---|
| 49 | the wiki using client side code), or `?redirect=no` which disables |
|---|
| 50 | redirection of both the ServerSideRedirectPlugin and TracRedirect |
|---|
| 51 | plug-in. |
|---|
| 52 | |
|---|
| 53 | Direct after the redirect target is added (or modified) Trac will |
|---|
| 54 | automatically reload it, as it does with all wiki pages. This plug-in |
|---|
| 55 | will detect this and not redirect but display the wiki page with the |
|---|
| 56 | redirect target URL printed to provide feedback about the successful |
|---|
| 57 | change. However, further visits will trigger the redirect. |
|---|
| 58 | |
|---|
| 59 | == Usage Examples == |
|---|
| 60 | The following 'macro' at the begin of the wiki page will cause a |
|---|
| 61 | redirect to the ''!OtherWikiPage''. |
|---|
| 62 | {{{ |
|---|
| 63 | [[redirect(OtherWikiPage)]] |
|---|
| 64 | [[Redirect(OtherWikiPage)]] |
|---|
| 65 | }}} |
|---|
| 66 | Any other [TracLinks TracLink] can be used: |
|---|
| 67 | {{{ |
|---|
| 68 | [[redirect(wiki:OtherWikiPage)]] |
|---|
| 69 | [[Redirect(wiki:OtherWikiPage)]] |
|---|
| 70 | [[redirect(source:/trunk/file.py)]] |
|---|
| 71 | [[Redirect(source:/trunk/file.py)]] |
|---|
| 72 | [[redirect(http://www.example.com/)]] |
|---|
| 73 | [[Redirect(http://www.example.com/)]] |
|---|
| 74 | }}} |
|---|
| 75 | """ |
|---|
| 76 | implements(IRequestHandler, IRequestFilter, IWikiMacroProvider) |
|---|
| 77 | |
|---|
| 78 | def expand_macro(self, formatter, name, content): |
|---|
| 79 | """Print redirect notice after edit.""" |
|---|
| 80 | |
|---|
| 81 | content = stripws(content) |
|---|
| 82 | target = extract_url(self.env, formatter.context, content) |
|---|
| 83 | if not target: |
|---|
| 84 | target = formatter.req.href.wiki(content) |
|---|
| 85 | |
|---|
| 86 | return tag.div( |
|---|
| 87 | tag.strong('This page redirects to: '), |
|---|
| 88 | tag.a(content, href=target), |
|---|
| 89 | class_='system-message', |
|---|
| 90 | id='notice' |
|---|
| 91 | ) |
|---|
| 92 | |
|---|
| 93 | def get_macros(self): |
|---|
| 94 | """Provide but do not redefine the 'redirect' macro.""" |
|---|
| 95 | get = self.env.config.get |
|---|
| 96 | if get('components', 'redirect.*') == 'enabled' or \ |
|---|
| 97 | get('components', 'redirect.redirect.*') == 'enabled' or \ |
|---|
| 98 | get('components', |
|---|
| 99 | 'redirect.redirect.tracredirect') == 'enabled': |
|---|
| 100 | return ['Redirect'] |
|---|
| 101 | else: |
|---|
| 102 | return ['redirect', 'Redirect'] |
|---|
| 103 | |
|---|
| 104 | def get_macro_description(self, name): |
|---|
| 105 | if name == 'Redirect': |
|---|
| 106 | return self.__doc__ |
|---|
| 107 | else: |
|---|
| 108 | return "See macro `Redirect`." |
|---|
| 109 | |
|---|
| 110 | # IRequestHandler methods |
|---|
| 111 | |
|---|
| 112 | def match_request(self, req): |
|---|
| 113 | """Only handle request when selected from `pre_process_request`.""" |
|---|
| 114 | return False |
|---|
| 115 | |
|---|
| 116 | def process_request(self, req): |
|---|
| 117 | """Redirect to pre-selected target.""" |
|---|
| 118 | target = req.args.get('redirect-target') |
|---|
| 119 | if target: |
|---|
| 120 | # Check for self-redirect: |
|---|
| 121 | if target and target == req.href(req.path_info): |
|---|
| 122 | change = tag.a(_("change"), href=target + "?action=edit") |
|---|
| 123 | message = tag_("Please %(change)s the redirect target to " |
|---|
| 124 | "another page.", change=change) |
|---|
| 125 | data = { |
|---|
| 126 | 'title': "Page redirects to itself!", |
|---|
| 127 | 'message': message, |
|---|
| 128 | 'type': 'TracError' |
|---|
| 129 | } |
|---|
| 130 | req.send_error(data['title'], status=409, |
|---|
| 131 | env=self.env, data=data) |
|---|
| 132 | |
|---|
| 133 | # Check for redirect pair, i.e. A->B, B->A |
|---|
| 134 | redirected_from = req.args.get('redirectedfrom', '') |
|---|
| 135 | if target and target == req.href.wiki(redirected_from): |
|---|
| 136 | this = tag.a(_("this"), |
|---|
| 137 | href=req.href(req.path_info, action="edit")) |
|---|
| 138 | redirecting = tag.a(_("redirecting"), |
|---|
| 139 | href=target + "?action=edit") |
|---|
| 140 | message = tag_("Please change the redirect target on " |
|---|
| 141 | "%(this)s page or the %(redirecting)s page.", |
|---|
| 142 | this=this, redirecting=redirecting) |
|---|
| 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 | |
|---|
| 151 | # Add back link information for internal links: |
|---|
| 152 | if target and target[0] == '/': |
|---|
| 153 | redirectfrom = "redirectedfrom=" + req.path_info[6:] |
|---|
| 154 | target = concat_path_query_fragment(target, redirectfrom) |
|---|
| 155 | req.redirect(target) |
|---|
| 156 | raise TracError("Invalid redirect target!") |
|---|
| 157 | |
|---|
| 158 | def _get_redirect(self, req): |
|---|
| 159 | """Checks if the request should be redirected.""" |
|---|
| 160 | if req.path_info in ('/', '/wiki'): |
|---|
| 161 | wiki = 'WikiStart' |
|---|
| 162 | elif not req.path_info.startswith('/wiki/'): |
|---|
| 163 | return None |
|---|
| 164 | else: |
|---|
| 165 | wiki = req.path_info[6:] |
|---|
| 166 | |
|---|
| 167 | wp = WikiPage(self.env, wiki, req.args.get('version')) |
|---|
| 168 | |
|---|
| 169 | if not wp.exists: |
|---|
| 170 | return None |
|---|
| 171 | |
|---|
| 172 | # Check for redirect "macro": |
|---|
| 173 | m = MACRO.match(wp.text) |
|---|
| 174 | if not m: |
|---|
| 175 | return None |
|---|
| 176 | wikitarget = stripws(m.group(1)) |
|---|
| 177 | ctxt = web_context(req) |
|---|
| 178 | redirect_target = extract_url(self.env, ctxt, wikitarget) |
|---|
| 179 | if not redirect_target: |
|---|
| 180 | redirect_target = req.href.wiki(wikitarget) |
|---|
| 181 | return redirect_target |
|---|
| 182 | |
|---|
| 183 | # IRequestFilter methods |
|---|
| 184 | |
|---|
| 185 | def pre_process_request(self, req, handler): |
|---|
| 186 | if not isinstance(handler, WikiModule): |
|---|
| 187 | return handler |
|---|
| 188 | |
|---|
| 189 | args = req.args |
|---|
| 190 | if not req.path_info.startswith('/wiki/') and \ |
|---|
| 191 | not req.path_info == '/wiki' and not req.path_info == '/': |
|---|
| 192 | self.log.debug("SSR: no redirect: Path is not a wiki path") |
|---|
| 193 | return handler |
|---|
| 194 | if req.method != 'GET': |
|---|
| 195 | self.log.debug("SSR: no redirect: No GET request") |
|---|
| 196 | return handler |
|---|
| 197 | if 'action' in args: |
|---|
| 198 | self.log.debug("SSR: no redirect: action=" + args['action']) |
|---|
| 199 | return handler |
|---|
| 200 | if 'version' in args: |
|---|
| 201 | self.log.debug("SSR: no redirect: version=...") |
|---|
| 202 | return handler |
|---|
| 203 | if 'redirect' in args and args['redirect'].lower() == 'no': |
|---|
| 204 | self.log.debug("SSR: no redirect: redirect=no") |
|---|
| 205 | return handler |
|---|
| 206 | if req.environ.get('HTTP_REFERER', '').find('action=edit') != -1: |
|---|
| 207 | self.log.debug("SSR: no redirect: HTTP_REFERER includes " |
|---|
| 208 | "action=edit") |
|---|
| 209 | return handler |
|---|
| 210 | target = self._get_redirect(req) |
|---|
| 211 | if target: |
|---|
| 212 | self.log.debug("SSR: redirect!") |
|---|
| 213 | req.args['redirect-target'] = target |
|---|
| 214 | return self |
|---|
| 215 | self.log.debug("SSR: no redirect: No redirect macro found.") |
|---|
| 216 | return handler |
|---|
| 217 | |
|---|
| 218 | def post_process_request(self, req, template, data, content_type): |
|---|
| 219 | return template, data, content_type |
|---|