eric7/WebBrowser/OpenSearch/OpenSearchEngine.py

branch
eric7
changeset 8312
800c432b34c8
parent 8218
7c09585bd960
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the open search engine.
8 """
9
10 import re
11 import json
12
13 from PyQt5.QtCore import (
14 pyqtSignal, pyqtSlot, QLocale, QUrl, QUrlQuery, QByteArray, QBuffer,
15 QIODevice, QObject
16 )
17 from PyQt5.QtGui import QImage
18 from PyQt5.QtNetwork import (
19 QNetworkRequest, QNetworkAccessManager, QNetworkReply
20 )
21
22 from UI.Info import Program
23
24 import Preferences
25 import Utilities
26
27
28 class OpenSearchEngine(QObject):
29 """
30 Class implementing the open search engine.
31
32 @signal imageChanged() emitted after the icon has been changed
33 @signal suggestions(list of strings) emitted after the suggestions have
34 been received
35 """
36 imageChanged = pyqtSignal()
37 suggestions = pyqtSignal(list)
38
39 def __init__(self, parent=None):
40 """
41 Constructor
42
43 @param parent reference to the parent object (QObject)
44 """
45 super().__init__(parent)
46
47 self.__suggestionsReply = None
48 self.__networkAccessManager = None
49 self._name = ""
50 self._description = ""
51 self._searchUrlTemplate = ""
52 self._suggestionsUrlTemplate = ""
53 self._searchParameters = [] # list of two tuples
54 self._suggestionsParameters = [] # list of two tuples
55 self._imageUrl = ""
56 self.__image = QImage()
57 self.__iconMoved = False
58 self.__searchMethod = "get"
59 self.__suggestionsMethod = "get"
60 self.__requestMethods = {
61 "get": QNetworkAccessManager.Operation.GetOperation,
62 "post": QNetworkAccessManager.Operation.PostOperation,
63 }
64
65 self.__replies = []
66
67 @classmethod
68 def parseTemplate(cls, searchTerm, searchTemplate):
69 """
70 Class method to parse a search template.
71
72 @param searchTerm term to search for (string)
73 @param searchTemplate template to be parsed (string)
74 @return parsed template (string)
75 """
76 locale = QLocale(Preferences.getWebBrowser("SearchLanguage"))
77 language = locale.name().replace("_", "-")
78 country = locale.name().split("_")[0].lower()
79
80 result = searchTemplate
81 result = result.replace("{count}", "20")
82 result = result.replace("{startIndex}", "0")
83 result = result.replace("{startPage}", "0")
84 result = result.replace("{language}", language)
85 result = result.replace("{country}", country)
86 result = result.replace("{inputEncoding}", "UTF-8")
87 result = result.replace("{outputEncoding}", "UTF-8")
88 result = result.replace(
89 "{searchTerms}",
90 bytes(QUrl.toPercentEncoding(searchTerm)).decode())
91 result = re.sub(r"""\{([^\}]*:|)source\??\}""", Program, result)
92
93 return result
94
95 @pyqtSlot(result=str)
96 def name(self):
97 """
98 Public method to get the name of the engine.
99
100 @return name of the engine (string)
101 """
102 return self._name
103
104 def setName(self, name):
105 """
106 Public method to set the engine name.
107
108 @param name name of the engine (string)
109 """
110 self._name = name
111
112 def description(self):
113 """
114 Public method to get the description of the engine.
115
116 @return description of the engine (string)
117 """
118 return self._description
119
120 def setDescription(self, description):
121 """
122 Public method to set the engine description.
123
124 @param description description of the engine (string)
125 """
126 self._description = description
127
128 def searchUrlTemplate(self):
129 """
130 Public method to get the search URL template of the engine.
131
132 @return search URL template of the engine (string)
133 """
134 return self._searchUrlTemplate
135
136 def setSearchUrlTemplate(self, searchUrlTemplate):
137 """
138 Public method to set the engine search URL template.
139
140 The URL template is processed according to the specification:
141 <a
142 href="http://www.opensearch.org/Specifications/OpenSearch/1.1#OpenSearch_URL_template_syntax">
143 http://www.opensearch.org/Specifications/OpenSearch/1.1#OpenSearch_URL_template_syntax</a>
144
145 A list of template parameters currently supported and what they are
146 replaced with:
147 <table>
148 <tr><td><b>Parameter</b></td><td><b>Value</b></td></tr>
149 <tr><td>{count}</td><td>20</td></tr>
150 <tr><td>{startIndex}</td><td>0</td></tr>
151 <tr><td>{startPage}</td><td>0</td></tr>
152 <tr><td>{language}</td>
153 <td>the default language code (RFC 3066)</td></tr>
154 <tr><td>{country}</td>
155 <td>the default country code (first part of language)</td></tr>
156 <tr><td>{inputEncoding}</td><td>UTF-8</td></tr>
157 <tr><td>{outputEncoding}</td><td>UTF-8</td></tr>
158 <tr><td>{searchTerms}</td><td>the string supplied by the user</td></tr>
159 <tr><td>{*:source}</td>
160 <td>application name, QCoreApplication::applicationName()</td></tr>
161 </table>
162
163 @param searchUrlTemplate search URL template of the engine (string)
164 """
165 self._searchUrlTemplate = searchUrlTemplate
166
167 def searchUrl(self, searchTerm):
168 """
169 Public method to get a URL ready for searching.
170
171 @param searchTerm term to search for (string)
172 @return URL (QUrl)
173 """
174 if not self._searchUrlTemplate:
175 return QUrl()
176
177 ret = QUrl.fromEncoded(
178 self.parseTemplate(searchTerm, self._searchUrlTemplate)
179 .encode("utf-8"))
180
181 if self.__searchMethod != "post":
182 urlQuery = QUrlQuery(ret)
183 for parameter in self._searchParameters:
184 urlQuery.addQueryItem(
185 parameter[0],
186 self.parseTemplate(searchTerm, parameter[1]))
187 ret.setQuery(urlQuery)
188
189 return ret
190
191 def providesSuggestions(self):
192 """
193 Public method to check, if the engine provides suggestions.
194
195 @return flag indicating suggestions are provided (boolean)
196 """
197 return self._suggestionsUrlTemplate != ""
198
199 def suggestionsUrlTemplate(self):
200 """
201 Public method to get the search URL template of the engine.
202
203 @return search URL template of the engine (string)
204 """
205 return self._suggestionsUrlTemplate
206
207 def setSuggestionsUrlTemplate(self, suggestionsUrlTemplate):
208 """
209 Public method to set the engine suggestions URL template.
210
211 @param suggestionsUrlTemplate suggestions URL template of the
212 engine (string)
213 """
214 self._suggestionsUrlTemplate = suggestionsUrlTemplate
215
216 def suggestionsUrl(self, searchTerm):
217 """
218 Public method to get a URL ready for suggestions.
219
220 @param searchTerm term to search for (string)
221 @return URL (QUrl)
222 """
223 if not self._suggestionsUrlTemplate:
224 return QUrl()
225
226 ret = QUrl.fromEncoded(QByteArray(self.parseTemplate(
227 searchTerm, self._suggestionsUrlTemplate).encode("utf-8")))
228
229 if self.__searchMethod != "post":
230 urlQuery = QUrlQuery(ret)
231 for parameter in self._suggestionsParameters:
232 urlQuery.addQueryItem(
233 parameter[0],
234 self.parseTemplate(searchTerm, parameter[1]))
235 ret.setQuery(urlQuery)
236
237 return ret
238
239 def searchParameters(self):
240 """
241 Public method to get the search parameters of the engine.
242
243 @return search parameters of the engine (list of two tuples)
244 """
245 return self._searchParameters[:]
246
247 def setSearchParameters(self, searchParameters):
248 """
249 Public method to set the engine search parameters.
250
251 @param searchParameters search parameters of the engine
252 (list of two tuples)
253 """
254 self._searchParameters = searchParameters[:]
255
256 def suggestionsParameters(self):
257 """
258 Public method to get the suggestions parameters of the engine.
259
260 @return suggestions parameters of the engine (list of two tuples)
261 """
262 return self._suggestionsParameters[:]
263
264 def setSuggestionsParameters(self, suggestionsParameters):
265 """
266 Public method to set the engine suggestions parameters.
267
268 @param suggestionsParameters suggestions parameters of the
269 engine (list of two tuples)
270 """
271 self._suggestionsParameters = suggestionsParameters[:]
272
273 def searchMethod(self):
274 """
275 Public method to get the HTTP request method used to perform search
276 requests.
277
278 @return HTTP request method (string)
279 """
280 return self.__searchMethod
281
282 def setSearchMethod(self, method):
283 """
284 Public method to set the HTTP request method used to perform search
285 requests.
286
287 @param method HTTP request method (string)
288 """
289 requestMethod = method.lower()
290 if requestMethod not in self.__requestMethods:
291 return
292
293 self.__searchMethod = requestMethod
294
295 def suggestionsMethod(self):
296 """
297 Public method to get the HTTP request method used to perform
298 suggestions requests.
299
300 @return HTTP request method (string)
301 """
302 return self.__suggestionsMethod
303
304 def setSuggestionsMethod(self, method):
305 """
306 Public method to set the HTTP request method used to perform
307 suggestions requests.
308
309 @param method HTTP request method (string)
310 """
311 requestMethod = method.lower()
312 if requestMethod not in self.__requestMethods:
313 return
314
315 self.__suggestionsMethod = requestMethod
316
317 def imageUrl(self):
318 """
319 Public method to get the image URL of the engine.
320
321 @return image URL of the engine (string)
322 """
323 return self._imageUrl
324
325 def setImageUrl(self, imageUrl):
326 """
327 Public method to set the engine image URL.
328
329 @param imageUrl image URL of the engine (string)
330 """
331 self._imageUrl = imageUrl
332
333 def setImageUrlAndLoad(self, imageUrl):
334 """
335 Public method to set the engine image URL.
336
337 @param imageUrl image URL of the engine (string)
338 """
339 self.setImageUrl(imageUrl)
340 self.__iconMoved = False
341 self.loadImage()
342
343 def loadImage(self):
344 """
345 Public method to load the image of the engine.
346 """
347 if self.__networkAccessManager is None or not self._imageUrl:
348 return
349
350 reply = self.__networkAccessManager.get(
351 QNetworkRequest(QUrl.fromEncoded(self._imageUrl.encode("utf-8"))))
352 reply.finished.connect(lambda: self.__imageObtained(reply))
353 self.__replies.append(reply)
354
355 def __imageObtained(self, reply):
356 """
357 Private slot to receive the image of the engine.
358
359 @param reply reference to the network reply
360 @type QNetworkReply
361 """
362 response = reply.readAll()
363
364 reply.close()
365 if reply in self.__replies:
366 self.__replies.remove(reply)
367 reply.deleteLater()
368
369 if response.isEmpty():
370 return
371
372 if response.startsWith(b"<html>") or response.startsWith(b"HTML"):
373 self.__iconMoved = True
374 self.__image = QImage()
375 else:
376 self.__image.loadFromData(response)
377 self.imageChanged.emit()
378
379 def image(self):
380 """
381 Public method to get the image of the engine.
382
383 @return image of the engine (QImage)
384 """
385 if not self.__iconMoved and self.__image.isNull():
386 self.loadImage()
387
388 return self.__image
389
390 def setImage(self, image):
391 """
392 Public method to set the image of the engine.
393
394 @param image image to be set (QImage)
395 """
396 if not self._imageUrl:
397 imageBuffer = QBuffer()
398 imageBuffer.open(QIODevice.OpenModeFlag.ReadWrite)
399 if image.save(imageBuffer, "PNG"):
400 self._imageUrl = "data:image/png;base64,{0}".format(
401 bytes(imageBuffer.buffer().toBase64()).decode())
402
403 self.__image = QImage(image)
404 self.imageChanged.emit()
405
406 def isValid(self):
407 """
408 Public method to check, if the engine is valid.
409
410 @return flag indicating validity (boolean)
411 """
412 return self._name and self._searchUrlTemplate
413
414 def __eq__(self, other):
415 """
416 Special method implementing the == operator.
417
418 @param other reference to an open search engine (OpenSearchEngine)
419 @return flag indicating equality (boolean)
420 """
421 if not isinstance(other, OpenSearchEngine):
422 return NotImplemented
423
424 return (
425 self._name == other._name and
426 self._description == other._description and
427 self._imageUrl == other._imageUrl and
428 self._searchUrlTemplate == other._searchUrlTemplate and
429 self._suggestionsUrlTemplate == other._suggestionsUrlTemplate and
430 self._searchParameters == other._searchParameters and
431 self._suggestionsParameters == other._suggestionsParameters
432 )
433
434 def __lt__(self, other):
435 """
436 Special method implementing the < operator.
437
438 @param other reference to an open search engine (OpenSearchEngine)
439 @return flag indicating less than (boolean)
440 """
441 if not isinstance(other, OpenSearchEngine):
442 return NotImplemented
443
444 return self._name < other._name
445
446 def requestSuggestions(self, searchTerm):
447 """
448 Public method to request suggestions.
449
450 @param searchTerm term to get suggestions for (string)
451 """
452 if not searchTerm or not self.providesSuggestions():
453 return
454
455 if self.__networkAccessManager is None:
456 return
457
458 if self.__suggestionsReply is not None:
459 self.__suggestionsReply.finished.disconnect(
460 self.__suggestionsObtained)
461 self.__suggestionsReply.abort()
462 self.__suggestionsReply.deleteLater()
463 self.__suggestionsReply = None
464
465 if self.__suggestionsMethod not in self.__requestMethods:
466 # ignore
467 return
468
469 if self.__suggestionsMethod == "get":
470 self.__suggestionsReply = self.networkAccessManager().get(
471 QNetworkRequest(self.suggestionsUrl(searchTerm)))
472 else:
473 parameters = []
474 for parameter in self._suggestionsParameters:
475 parameters.append(parameter[0] + "=" + parameter[1])
476 data = "&".join(parameters)
477 self.__suggestionsReply = self.networkAccessManager().post(
478 QNetworkRequest(self.suggestionsUrl(searchTerm)), data)
479 self.__suggestionsReply.finished.connect(
480 self.__suggestionsObtained)
481
482 def __suggestionsObtained(self):
483 """
484 Private slot to receive the suggestions.
485 """
486 if (
487 self.__suggestionsReply.error() ==
488 QNetworkReply.NetworkError.NoError
489 ):
490 buffer = bytes(self.__suggestionsReply.readAll())
491 response = Utilities.decodeBytes(buffer)
492 response = response.strip()
493
494 self.__suggestionsReply.close()
495 self.__suggestionsReply.deleteLater()
496 self.__suggestionsReply = None
497
498 if len(response) == 0:
499 return
500
501 try:
502 result = json.loads(response)
503 except ValueError:
504 return
505
506 try:
507 suggestions = result[1]
508 except IndexError:
509 return
510
511 self.suggestions.emit(suggestions)
512
513 def networkAccessManager(self):
514 """
515 Public method to get a reference to the network access manager object.
516
517 @return reference to the network access manager object
518 (QNetworkAccessManager)
519 """
520 return self.__networkAccessManager
521
522 def setNetworkAccessManager(self, networkAccessManager):
523 """
524 Public method to set the reference to the network access manager.
525
526 @param networkAccessManager reference to the network access manager
527 object (QNetworkAccessManager)
528 """
529 self.__networkAccessManager = networkAccessManager

eric ide

mercurial