1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2016 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a scheme handler for the eric: scheme.
8"""
9
10from PyQt5.QtCore import pyqtSignal, QBuffer, QIODevice, QUrlQuery, QMutex
11from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler
12
13from E5Gui.E5Application import e5App
14
15from E5Utilities.E5MutexLocker import E5MutexLocker
16
17from ..Tools.WebBrowserTools import (
18    getHtmlPage, getJavascript, pixmapFileToDataUrl
19)
20
21_SupportedPages = [
22    "adblock",                      # error page for URLs blocked by AdBlock
23    "home", "start", "startpage",   # eric home page
24    "speeddial",                    # eric speeddial
25]
26
27
28class EricSchemeHandler(QWebEngineUrlSchemeHandler):
29    """
30    Class implementing a scheme handler for the eric: scheme.
31    """
32    def __init__(self, parent=None):
33        """
34        Constructor
35
36        @param parent reference to the parent object
37        @type QObject
38        """
39        super().__init__(parent)
40
41        self.__replies = []
42
43    def requestStarted(self, job):
44        """
45        Public method handling the URL request.
46
47        @param job URL request job
48        @type QWebEngineUrlRequestJob
49        """
50        reply = EricSchemeReply(job)
51        reply.closed.connect(lambda: self.__replyClosed(reply))
52        self.__replies.append(reply)
53        job.reply(b"text/html", reply)
54
55    def __replyClosed(self, reply):
56        """
57        Private slot handling the closed signal of a reply.
58
59        @param reply reference to the network reply
60        @type EricSchemeReply
61        """
62        if reply in self.__replies:
63            self.__replies.remove(reply)
64
65
66class EricSchemeReply(QIODevice):
67    """
68    Class implementing a reply for a requested eric: page.
69
70    @signal closed emitted to signal that the web engine has read
71        the data
72    """
73    closed = pyqtSignal()
74
75    _speedDialPage = ""
76
77    def __init__(self, job, parent=None):
78        """
79        Constructor
80
81        @param job reference to the URL request
82        @type QWebEngineUrlRequestJob
83        @param parent reference to the parent object
84        @type QObject
85        """
86        super().__init__(parent)
87
88        self.__loaded = False
89        self.__job = job
90        self.__mutex = QMutex()
91
92        self.__pageName = self.__job.requestUrl().path()
93        self.__buffer = QBuffer()
94
95        self.__loadPage()
96
97    def __loadPage(self):
98        """
99        Private method to load the requested page.
100        """
101        if self.__loaded:
102            return
103
104        with E5MutexLocker(self.__mutex):
105            if self.__pageName == "adblock":
106                contents = self.__adBlockPage()
107            elif self.__pageName in ["home", "start", "startpage"]:
108                contents = self.__startPage()
109            elif self.__pageName == "speeddial":
110                contents = self.__speedDialPage()
111            else:
112                contents = self.__errorPage()
113
114            self.__buffer.setData(contents.encode("utf-8"))
115            self.__buffer.open(QIODevice.OpenModeFlag.ReadOnly)
116            self.open(QIODevice.OpenModeFlag.ReadOnly)
117
118        self.readyRead.emit()
119
120        self.__loaded = True
121
122    def bytesAvailable(self):
123        """
124        Public method to get the number of available bytes.
125
126        @return number of available bytes
127        @rtype int
128        """
129        with E5MutexLocker(self.__mutex):
130            return self.__buffer.bytesAvailable()
131
132    def readData(self, maxlen):
133        """
134        Public method to retrieve data from the reply object.
135
136        @param maxlen maximum number of bytes to read (integer)
137        @return string containing the data (bytes)
138        """
139        with E5MutexLocker(self.__mutex):
140            return self.__buffer.read(maxlen)
141
142    def close(self):
143        """
144        Public method used to cloase the reply.
145        """
146        super().close()
147        self.closed.emit()
148
149    def __adBlockPage(self):
150        """
151        Private method to build the AdBlock page.
152
153        @return built AdBlock page
154        @rtype str
155        """
156        query = QUrlQuery(self.__job.requestUrl())
157        rule = query.queryItemValue("rule")
158        subscription = query.queryItemValue("subscription")
159        title = self.tr("Content blocked by AdBlock Plus")
160        message = self.tr(
161            "Blocked by rule: <i>{0} ({1})</i>").format(rule, subscription)
162
163        page = getHtmlPage("adblockPage.html")
164        page = page.replace(
165            "@FAVICON@", pixmapFileToDataUrl("adBlockPlus16.png", True))
166        page = page.replace(
167            "@IMAGE@", pixmapFileToDataUrl("adBlockPlus64.png", True))
168        page = page.replace("@TITLE@", title)
169        page = page.replace("@MESSAGE@", message)
170
171        return page
172
173    def __errorPage(self):
174        """
175        Private method to build the Error page.
176
177        @return built Error page
178        @rtype str
179        """
180        page = getHtmlPage("ericErrorPage.html")
181        page = page.replace(
182            "@FAVICON@", pixmapFileToDataUrl("ericWeb16.png", True))
183        page = page.replace(
184            "@IMAGE@", pixmapFileToDataUrl("ericWeb32.png", True))
185        page = page.replace(
186            "@TITLE@", self.tr("Error accessing eric: URL"))
187        page = page.replace(
188            "@MESSAGE@", self.tr(
189                "The special URL <strong>{0}</strong> is not supported."
190                " Please use one of these."
191            ).format(self.__job.requestUrl().toDisplayString())
192        )
193        page = page.replace(
194            "@ERICLIST@", "<br/>".join([
195                '<a href="eric:{0}">{0}</a>'.format(u)
196                for u in sorted(_SupportedPages)
197            ])
198        )
199
200        return page
201
202    def __startPage(self):
203        """
204        Private method to build the Start page.
205
206        @return built Start page
207        @rtype str
208        """
209        page = getHtmlPage("startPage.html")
210        page = page.replace(
211            "@FAVICON@", pixmapFileToDataUrl("ericWeb16.png", True))
212        page = page.replace(
213            "@IMAGE@", pixmapFileToDataUrl("ericWeb32.png", True))
214        page = page.replace(
215            "@TITLE@", self.tr("Welcome to eric Web Browser!"))
216        page = page.replace("@ERIC_LINK@", self.tr("About eric"))
217        page = page.replace("@HEADER_TITLE@", self.tr("eric Web Browser"))
218        page = page.replace("@SUBMIT@", self.tr("Search!"))
219        ltr = "LTR" if e5App().isLeftToRight() else"RTL"
220        page = page.replace("@QT_LAYOUT_DIRECTION@", ltr)
221
222        return page
223
224    def __speedDialPage(self):
225        """
226        Private method to create the Speeddial page.
227
228        @return prepared speeddial page
229        @rtype str
230        """
231        if not self._speedDialPage:
232            page = getHtmlPage("speeddialPage.html")
233            page = page.replace(
234                "@FAVICON@", pixmapFileToDataUrl("ericWeb16.png", True))
235            page = page.replace(
236                "@IMG_PLUS@", pixmapFileToDataUrl("plus.png", True))
237            page = page.replace(
238                "@IMG_CLOSE@", pixmapFileToDataUrl("close.png", True))
239            page = page.replace(
240                "@IMG_EDIT@", pixmapFileToDataUrl("edit.png", True))
241            page = page.replace(
242                "@IMG_RELOAD@", pixmapFileToDataUrl("reload.png", True))
243            page = page.replace(
244                "@IMG_SETTINGS@", pixmapFileToDataUrl("setting.png", True))
245            page = page.replace(
246                "@LOADING-IMG@", pixmapFileToDataUrl("loading.gif", True))
247            page = page.replace(
248                "@BOX-BORDER@",
249                pixmapFileToDataUrl("box-border-small.png", True))
250
251            page = page.replace("@JQUERY@", getJavascript("jquery.js"))
252            page = page.replace("@JQUERY-UI@", getJavascript("jquery-ui.js"))
253
254            page = page.replace("@SITE-TITLE@", self.tr("Speed Dial"))
255            page = page.replace("@URL@", self.tr("URL"))
256            page = page.replace("@TITLE@", self.tr("Title"))
257            page = page.replace("@APPLY@", self.tr("Apply"))
258            page = page.replace("@CLOSE@", self.tr("Close"))
259            page = page.replace("@NEW-PAGE@", self.tr("New Page"))
260            page = page.replace("@TITLE-EDIT@", self.tr("Edit"))
261            page = page.replace("@TITLE-REMOVE@", self.tr("Remove"))
262            page = page.replace("@TITLE-RELOAD@", self.tr("Reload"))
263            page = page.replace("@TITLE-WARN@",
264                                self.tr("Are you sure to remove this"
265                                        " speed dial?"))
266            page = page.replace("@TITLE-WARN-REL@",
267                                self.tr("Are you sure you want to reload"
268                                        " all speed dials?"))
269            page = page.replace("@TITLE-FETCHTITLE@",
270                                self.tr("Load title from page"))
271            page = page.replace("@SETTINGS-TITLE@",
272                                self.tr("Speed Dial Settings"))
273            page = page.replace("@ADD-TITLE@", self.tr("Add New Page"))
274            page = page.replace("@TXT_NRROWS@",
275                                self.tr("Maximum pages in a row:"))
276            page = page.replace("@TXT_SDSIZE@",
277                                self.tr("Change size of pages:"))
278            page = page.replace("@JAVASCRIPT_DISABLED@",
279                                self.tr("SpeedDial requires enabled"
280                                        " JavaScript."))
281
282            self._speedDialPage = page
283
284        from WebBrowser.WebBrowserWindow import WebBrowserWindow
285        dial = WebBrowserWindow.speedDial()
286        page = (
287            self._speedDialPage
288            .replace("@INITIAL-SCRIPT@", dial.initialScript())
289            .replace("@ROW-PAGES@", str(dial.pagesInRow()))
290            .replace("@SD-SIZE@", str(dial.sdSize()))
291        )
292
293        return page
294