1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2015-2021 Edgewall Software
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. The terms
9# are also available at https://trac.edgewall.org/wiki/TracLicense.
10#
11# This software consists of voluntary contributions made by many
12# individuals. For the exact contribution history, see the revision
13# history and logs, available at https://trac.edgewall.org/.
14
15import argparse
16import re
17import sys
18from contextlib import closing
19from pkg_resources import resource_listdir, resource_string
20
21from trac.loader import load_components
22from trac.test import EnvironmentStub, Mock, MockPerm
23from trac.util.text import printout
24from trac.web.chrome import web_context
25from trac.web.href import Href
26from trac.wiki.formatter import Formatter
27from trac.wiki.model import WikiPage
28
29
30TURN_ON = '\033[30m\033[41m'
31TURN_OFF = '\033[m'
32
33
34class DefaultWikiChecker(Formatter):
35
36    def __init__(self, env, context, name):
37        Formatter.__init__(self, env, context)
38        self.__name = name
39        self.__marks = []
40        self.__super = super()
41
42    def handle_match(self, fullmatch):
43        rv = self.__super.handle_match(fullmatch)
44        if rv:
45            text = str(rv) if not isinstance(rv, str) else rv
46            if text.startswith('<a ') and text.endswith('</a>') and \
47                    'class="missing ' in text:
48                self.__marks.append((fullmatch.start(0), fullmatch.end(0)))
49        return rv
50
51    def handle_code_block(self, line, startmatch=None):
52        prev_processor = getattr(self, 'code_processor', None)
53        try:
54            return self.__super.handle_code_block(line, startmatch)
55        finally:
56            processor = self.code_processor
57            if startmatch and processor and processor != prev_processor and \
58                    processor.error:
59                self.__marks.append((startmatch.start(0), startmatch.end(0)))
60
61    def format(self, text, out=None):
62        return self.__super.format(SourceWrapper(self, text), out)
63
64    def next_callback(self, line, idx):
65        marks = self.__marks
66        if marks:
67            buf = []
68            prev = 0
69            for start, end in self.__marks:
70                buf.append(line[prev:start])
71                buf.append(TURN_ON)
72                buf.append(line[start:end])
73                buf.append(TURN_OFF)
74                prev = end
75            buf.append(line[prev:])
76            printout('%s:%d:%s' % (self.__name, idx + 1, ''.join(buf)))
77            self.__marks[:] = ()
78
79
80class SourceWrapper(object):
81
82    def __init__(self, formatter, text):
83        self.formatter = formatter
84        self.text = text
85
86    def __iter__(self):
87        return LinesIterator(self.formatter, self.text.splitlines())
88
89
90class LinesIterator(object):
91
92    def __init__(self, formatter, lines):
93        self.formatter = formatter
94        self.lines = lines
95        self.idx = 0
96        self.current = None
97
98    def __next__(self):
99        idx = self.idx
100        if self.current is not None:
101            self.formatter.next_callback(self.current, idx)
102        if idx >= len(self.lines):
103            self.current = None
104            raise StopIteration
105        self.idx = idx + 1
106        self.current = self.lines[idx]
107        return self.current
108
109
110class DummyIO(object):
111
112    def write(self, data):
113        pass
114
115
116def parse_args(all_pages):
117    parser = argparse.ArgumentParser()
118    parser.add_argument('-d', '--download', action='store_true',
119                        help="download default pages from trac.edgewall.org "
120                             "before checking")
121    parser.add_argument('-p', '--prefix', default='',
122                        help="prepend PREFIX/ to the page name when "
123                             "downloading")
124    parser.add_argument('-s', '--strict', action='store_true',
125                        help="only download pages below PREFIX/ if -p given")
126    parser.add_argument('pages', metavar='page', nargs='*',
127                        help="the wiki page(s) to download and/or check")
128
129    args = parser.parse_args()
130    if args.pages:
131        for page in args.pages:
132            if page not in all_pages:
133                parser.error("%s is not one of the default pages." % page)
134
135    return args
136
137
138re_box_processor = re.compile(r'{{{#!box[^\}]+}}}\s*\r?\n?')
139
140
141def download_default_pages(names, prefix, strict):
142    from http.client import HTTPSConnection
143    host = 'trac.edgewall.org'
144    if prefix and not prefix.endswith('/'):
145        prefix += '/'
146    with closing(HTTPSConnection(host)) as conn:
147        for name in names:
148            if name in ('SandBox', 'TitleIndex', 'WikiStart'):
149                continue
150            sys.stdout.write('Downloading %s%s' % (prefix, name))
151            conn.request('GET', '/wiki/%s%s?format=txt' % (prefix, name))
152            response = conn.getresponse()
153            content = response.read()
154            if prefix and (response.status != 200 or not content) \
155                    and not strict:
156                sys.stdout.write(' %s' % name)
157                conn.request('GET', '/wiki/%s?format=txt' % name)
158                response = conn.getresponse()
159                content = response.read()
160            content = str(content, 'utf-8')
161            if response.status == 200 and content:
162                with open('trac/wiki/default-pages/' + name, 'w',
163                          encoding='utf-8') as f:
164                    if not strict:
165                        content = re_box_processor.sub('', content)
166                    lines = content.replace('\r\n', '\n').splitlines(True)
167                    f.write(''.join(line for line in lines
168                                         if strict or line.strip() !=
169                                            '[[TranslatedPages]]'))
170                sys.stdout.write('\tdone.\n')
171            else:
172                sys.stdout.write('\tmissing or empty.\n')
173
174
175def main():
176    all_pages = sorted(name for name
177                            in resource_listdir('trac.wiki', 'default-pages')
178                            if not name.startswith('.'))
179    args = parse_args(all_pages)
180    if args.pages:
181        pages = sorted(args.pages)
182    else:
183        pages = all_pages
184
185    if args.download:
186        download_default_pages(pages, args.prefix, args.strict)
187
188    env = EnvironmentStub(disable=['trac.mimeview.pygments.*'])
189    load_components(env)
190    with env.db_transaction:
191        for name in all_pages:
192            wiki = WikiPage(env, name)
193            wiki.text = resource_string('trac.wiki', 'default-pages/' +
194                                        name).decode('utf-8')
195            if wiki.text:
196                wiki.save('trac', '')
197            else:
198                printout('%s: Skipped empty page' % name)
199
200    req = Mock(href=Href('/'), abs_href=Href('http://localhost/'),
201               perm=MockPerm())
202    for name in pages:
203        wiki = WikiPage(env, name)
204        if not wiki.exists:
205            continue
206        context = web_context(req, wiki.resource)
207        out = DummyIO()
208        DefaultWikiChecker(env, context, name).format(wiki.text, out)
209
210
211if __name__ == '__main__':
212    main()
213