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