1#!/usr/local/bin/python3.8
2# vim:fileencoding=utf-8
3# License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
4
5
6from collections import defaultdict
7from threading import Thread
8
9from qt.core import (
10    QCheckBox, QHBoxLayout, QIcon, QInputDialog, QLabel, QProgressBar, QSizePolicy,
11    QStackedWidget, Qt, QTextBrowser, QVBoxLayout, QWidget, pyqtSignal, QDialogButtonBox
12)
13
14from calibre.gui2 import error_dialog
15from calibre.gui2.tweak_book import current_container, editors, set_current_container, tprefs
16from calibre.gui2.tweak_book.boss import get_boss
17from calibre.gui2.tweak_book.widgets import Dialog
18from polyglot.builtins import iteritems
19
20
21def get_data(name):
22    'Get the data for name. Returns a unicode string if name is a text document/stylesheet'
23    if name in editors:
24        return editors[name].get_raw_data()
25    return current_container().raw_data(name)
26
27
28def set_data(name, val):
29    if name in editors:
30        editors[name].replace_data(val, only_if_different=False)
31    else:
32        with current_container().open(name, 'wb') as f:
33            if isinstance(val, str):
34                val = val.encode('utf-8')
35            f.write(val)
36    get_boss().set_modified()
37
38
39class CheckExternalLinks(Dialog):
40
41    progress_made = pyqtSignal(object, object)
42
43    def __init__(self, parent=None):
44        Dialog.__init__(self, _('Check external links'), 'check-external-links-dialog', parent)
45        self.progress_made.connect(self.on_progress_made, type=Qt.ConnectionType.QueuedConnection)
46
47    def show(self):
48        if self.rb.isEnabled():
49            self.refresh()
50        return Dialog.show(self)
51
52    def refresh(self):
53        self.stack.setCurrentIndex(0)
54        self.rb.setEnabled(False)
55        t = Thread(name='CheckLinksMaster', target=self.run)
56        t.daemon = True
57        t.start()
58
59    def setup_ui(self):
60        self.pb = pb = QProgressBar(self)
61        pb.setTextVisible(True)
62        pb.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
63        pb.setRange(0, 0)
64        self.w = w = QWidget(self)
65        self.w.l = l = QVBoxLayout(w)
66        l.addStretch(), l.addWidget(pb)
67        self.w.la = la = QLabel(_('Checking external links, please wait...'))
68        la.setStyleSheet('QLabel { font-size: 20px; font-weight: bold }')
69        l.addWidget(la, 0, Qt.AlignmentFlag.AlignCenter), l.addStretch()
70
71        self.l = l = QVBoxLayout(self)
72        self.results = QTextBrowser(self)
73        self.results.setOpenLinks(False)
74        self.results.anchorClicked.connect(self.anchor_clicked)
75        self.stack = s = QStackedWidget(self)
76        s.addWidget(w), s.addWidget(self.results)
77        l.addWidget(s)
78        self.bh = h = QHBoxLayout()
79        self.check_anchors = ca = QCheckBox(_('Check &anchors'))
80        ca.setToolTip(_('Check HTML anchors in links (the part after the #).\n'
81            ' This can be a little slow, since it requires downloading and parsing all the HTML pages.'))
82        ca.setChecked(tprefs.get('check_external_link_anchors', True))
83        ca.stateChanged.connect(self.anchors_changed)
84        h.addWidget(ca), h.addStretch(100), h.addWidget(self.bb)
85        l.addLayout(h)
86        self.bb.setStandardButtons(QDialogButtonBox.StandardButton.Close)
87        self.rb = b = self.bb.addButton(_('&Refresh'), QDialogButtonBox.ButtonRole.ActionRole)
88        b.setIcon(QIcon(I('view-refresh.png')))
89        b.clicked.connect(self.refresh)
90
91    def anchors_changed(self):
92        tprefs.set('check_external_link_anchors', self.check_anchors.isChecked())
93
94    def sizeHint(self):
95        ans = Dialog.sizeHint(self)
96        ans.setHeight(600)
97        ans.setWidth(max(ans.width(), 800))
98        return ans
99
100    def run(self):
101        from calibre.ebooks.oeb.polish.check.links import check_external_links
102        self.tb = None
103        self.errors = []
104        try:
105            self.errors = check_external_links(current_container(), self.progress_made.emit, check_anchors=self.check_anchors.isChecked())
106        except Exception:
107            import traceback
108            self.tb = traceback.format_exc()
109        self.progress_made.emit(None, None)
110
111    def on_progress_made(self, curr, total):
112        if curr is None:
113            self.results.setText('')
114            self.stack.setCurrentIndex(1)
115            self.fixed_errors = set()
116            self.rb.setEnabled(True)
117            if self.tb is not None:
118                return error_dialog(self, _('Checking failed'), _(
119                    'There was an error while checking links, click "Show details" for more information'),
120                             det_msg=self.tb, show=True)
121            if not self.errors:
122                self.results.setText(_('No broken links found'))
123            else:
124                self.populate_results()
125        else:
126            self.pb.setMaximum(total), self.pb.setValue(curr)
127
128    def populate_results(self, preserve_pos=False):
129        num = len(self.errors) - len(self.fixed_errors)
130        text = '<h3>%s</h3><ol>' % (ngettext(
131            'Found a broken link', 'Found {} broken links', num).format(num))
132        for i, (locations, err, url) in enumerate(self.errors):
133            if i in self.fixed_errors:
134                continue
135            text += '<li><b>%s</b> \xa0<a href="err:%d">[%s]</a><br>%s<br><ul>' % (url, i, _('Fix this link'), err)
136            for name, href, lnum, col in locations:
137                text += '<li>{name} \xa0<a href="loc:{lnum},{name}">[{line}: {lnum}]</a></li>'.format(
138                    name=name, lnum=lnum, line=_('line number'))
139            text += '</ul></li><hr>'
140        self.results.setHtml(text)
141
142    def anchor_clicked(self, qurl):
143        url = qurl.toString()
144        if url.startswith('err:'):
145            errnum = int(url[4:])
146            err = self.errors[errnum]
147            newurl, ok = QInputDialog.getText(self, _('Fix URL'), _('Enter the corrected URL:') + '\xa0'*40, text=err[2])
148            if not ok:
149                return
150            nmap = defaultdict(set)
151            for name, href in {(l[0], l[1]) for l in err[0]}:
152                nmap[name].add(href)
153
154            for name, hrefs in iteritems(nmap):
155                raw = oraw = get_data(name)
156                for href in hrefs:
157                    raw = raw.replace(href, newurl)
158                if raw != oraw:
159                    set_data(name, raw)
160            self.fixed_errors.add(errnum)
161            self.populate_results()
162        elif url.startswith('loc:'):
163            lnum, name = url[4:].partition(',')[::2]
164            lnum = int(lnum or 1)
165            editor = get_boss().edit_file(name)
166            if lnum and editor is not None and editor.has_line_numbers:
167                editor.current_line = lnum
168
169
170if __name__ == '__main__':
171    import sys
172    from calibre.gui2 import Application
173    from calibre.gui2.tweak_book.boss import get_container
174    app = Application([])
175    set_current_container(get_container(sys.argv[-1]))
176    d = CheckExternalLinks()
177    d.refresh()
178    d.exec()
179    del app
180