1
# -*- coding: utf-8 -*-
9
from functools import wraps
10
from cookielib import Cookie, LWPCookieJar
14
bindings = ["PySide", "PyQt4"]
19
binding = __import__(name)
26
raise Exception("Ghost.py requires PySide or PyQt4")
29
PYSIDE = binding.__name__ == 'PySide'
33
sip.setapi('QVariant', 2)
37
name = "%s.%s" % (binding.__name__, name)
38
module = __import__(name)
39
for n in name.split(".")[1:]:
40
module = getattr(module, n)
44
QtCore = _import("QtCore")
46
QByteArray = QtCore.QByteArray
48
QDateTime = QtCore.QDateTime
49
QtCriticalMsg = QtCore.QtCriticalMsg
50
QtDebugMsg = QtCore.QtDebugMsg
51
QtFatalMsg = QtCore.QtFatalMsg
52
QtWarningMsg = QtCore.QtWarningMsg
53
qInstallMsgHandler = QtCore.qInstallMsgHandler
55
QtGui = _import("QtGui")
56
QApplication = QtGui.QApplication
58
QPainter = QtGui.QPainter
59
QPrinter = QtGui.QPrinter
61
QtNetwork = _import("QtNetwork")
62
QNetworkRequest = QtNetwork.QNetworkRequest
63
QNetworkAccessManager = QtNetwork.QNetworkAccessManager
64
QNetworkCookieJar = QtNetwork.QNetworkCookieJar
65
QNetworkDiskCache = QtNetwork.QNetworkDiskCache
66
QNetworkProxy = QtNetwork.QNetworkProxy
67
QNetworkCookie = QtNetwork.QNetworkCookie
69
QtWebKit = _import('QtWebKit')
72
default_user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.2 " +\
73
"(KHTML, like Gecko) Chrome/15.0.874.121 Safari/535.2"
76
logger = logging.getLogger('ghost')
79
class Error(Exception):
80
"""Base class for Ghost exceptions."""
84
class TimeoutError(Error):
85
"""Raised when a request times out"""
89
class Logger(logging.Logger):
91
def log(message, sender="Ghost", level="info"):
92
if not hasattr(logger, level):
93
raise Error('invalid log level')
94
getattr(logger, level)("%s: %s", sender, message)
97
class QTMessageProxy(object):
98
def __init__(self, debug=False):
101
def __call__(self, msgType, msg):
102
if msgType == QtDebugMsg and self.debug:
103
Logger.log(msg, sender='QT', level='debug')
104
elif msgType == QtWarningMsg and self.debug:
105
Logger.log(msg, sender='QT', level='warning')
106
elif msgType == QtCriticalMsg:
107
Logger.log(msg, sender='QT', level='critical')
108
elif msgType == QtFatalMsg:
109
Logger.log(msg, sender='QT', level='fatal')
111
Logger.log(msg, sender='QT', level='info')
114
class GhostWebPage(QtWebKit.QWebPage):
115
"""Overrides QtWebKit.QWebPage in order to intercept some graphical
116
behaviours like alert(), confirm().
117
Also intercepts client side console.log().
119
def __init__(self, app, ghost):
121
super(GhostWebPage, self).__init__(app)
123
def chooseFile(self, frame, suggested_file=None):
124
return Ghost._upload_file
126
def javaScriptConsoleMessage(self, message, line, source):
127
"""Prints client console message in current output stream."""
128
super(GhostWebPage, self).javaScriptConsoleMessage(message, line,
130
log_type = "error" if "Error" in message else "info"
131
Logger.log("%s(%d): %s" % (source or '<unknown>', line, message),
132
sender="Frame", level=log_type)
134
def javaScriptAlert(self, frame, message):
135
"""Notifies ghost for alert, then pass."""
136
Ghost._alert = message
137
self.ghost.append_popup_message(message)
138
Logger.log("alert('%s')" % message, sender="Frame")
140
def javaScriptConfirm(self, frame, message):
141
"""Checks if ghost is waiting for confirm, then returns the right
144
if Ghost._confirm_expected is None:
145
raise Error('You must specified a value to confirm "%s"' %
147
self.ghost.append_popup_message(message)
148
confirmation, callback = Ghost._confirm_expected
149
Logger.log("confirm('%s')" % message, sender="Frame")
150
if callback is not None:
154
def javaScriptPrompt(self, frame, message, defaultValue, result=None):
155
"""Checks if ghost is waiting for prompt, then enters the right
158
if Ghost._prompt_expected is None:
159
raise Error('You must specified a value for prompt "%s"' %
161
self.ghost.append_popup_message(message)
162
result_value, callback = Ghost._prompt_expected
163
Logger.log("prompt('%s')" % message, sender="Frame")
164
if callback is not None:
165
result_value = callback()
166
if result_value == '':
167
Logger.log("'%s' prompt filled with empty string" % message,
172
return True, result_value
173
result.append(unicode(result_value))
176
def setUserAgent(self, user_agent):
177
self.user_agent = user_agent
179
def userAgentForUrl(self, url):
180
return self.user_agent
183
def can_load_page(func):
184
"""Decorator that specifies if user can expect page loading from
185
this action. If expect_loading is set to True, ghost will wait
186
for page_loaded event.
189
def wrapper(self, *args, **kwargs):
190
expect_loading = False
191
if 'expect_loading' in kwargs:
192
expect_loading = kwargs['expect_loading']
193
del kwargs['expect_loading']
196
func(self, *args, **kwargs)
197
return self.wait_for_page_loaded()
198
return func(self, *args, **kwargs)
202
class HttpResource(object):
203
"""Represents an HTTP resource.
205
def __init__(self, reply, cache, content=None):
206
self.url = reply.url().toString()
207
self.content = content
208
if cache and self.content is None:
209
# Tries to get back content from cache
212
buffer = cache.data(reply.url().toString())
214
buffer = cache.data(reply.url())
215
if buffer is not None:
216
content = buffer.readAll()
218
self.content = unicode(content)
219
except UnicodeDecodeError:
220
self.content = content
221
self.http_status = reply.attribute(
222
QNetworkRequest.HttpStatusCodeAttribute)
223
Logger.log("Resource loaded: %s %s" % (self.url, self.http_status))
225
for header in reply.rawHeaderList():
227
self.headers[unicode(header)] = unicode(
228
reply.rawHeader(header))
229
except UnicodeDecodeError:
230
# it will lose the header value,
231
# but at least not crash the whole process
233
"Invalid characters in header {0}={1}".format(header, reply.rawHeader(header))
239
"""Ghost manages a QWebPage.
241
:param user_agent: The default User-Agent header.
242
:param wait_timeout: Maximum step duration in second.
243
:param wait_callback: An optional callable that is periodically
244
executed until Ghost stops waiting.
245
:param log_level: The optional logging level.
246
:param display: A boolean that tells ghost to displays UI.
247
:param viewport_size: A tuple that sets initial viewport size.
248
:param ignore_ssl_errors: A boolean that forces ignore ssl errors.
249
:param cache_dir: A directory path where to store cache datas.
250
:param plugins_enabled: Enable plugins (like Flash).
251
:param java_enabled: Enable Java JRE.
252
:param plugin_path: Array with paths to plugin directories
253
(default ['/usr/lib/mozilla/plugins'])
254
:param download_images: Indicate if the browser should download images
257
_confirm_expected = None
258
_prompt_expected = None
263
user_agent=default_user_agent,
266
log_level=logging.WARNING,
268
viewport_size=(800, 600),
269
ignore_ssl_errors=True,
270
cache_dir=os.path.join(tempfile.gettempdir(), "ghost.py"),
271
plugins_enabled=False,
273
plugin_path=['/usr/lib/mozilla/plugins', ],
274
download_images=True,
276
show_scrollbars=True,
277
network_access_manager_class=None):
279
self.http_resources = []
281
self.user_agent = user_agent
282
self.wait_timeout = wait_timeout
283
self.wait_callback = wait_callback
284
self.ignore_ssl_errors = ignore_ssl_errors
287
if sys.platform.startswith('linux') and not 'DISPLAY' in os.environ\
288
and not hasattr(Ghost, 'xvfb'):
290
os.environ['DISPLAY'] = ':99'
291
Ghost.xvfb = subprocess.Popen(['Xvfb', ':99'])
293
raise Error('Xvfb is required to a ghost run outside ' +
296
self.display = display
299
Ghost._app = QApplication.instance() or QApplication(['ghost'])
300
qInstallMsgHandler(QTMessageProxy(qt_debug))
302
for p in plugin_path:
303
Ghost._app.addLibraryPath(p)
305
self.popup_messages = []
306
self.page = GhostWebPage(Ghost._app, self)
308
if network_access_manager_class is not None:
309
self.page.setNetworkAccessManager(network_access_manager_class())
311
QtWebKit.QWebSettings.setMaximumPagesInCache(0)
312
QtWebKit.QWebSettings.setObjectCacheCapacities(0, 0, 0)
313
QtWebKit.QWebSettings.globalSettings().setAttribute(
314
QtWebKit.QWebSettings.LocalStorageEnabled, True)
316
self.page.setForwardUnsupportedContent(True)
317
self.page.settings().setAttribute(
318
QtWebKit.QWebSettings.AutoLoadImages, download_images)
319
self.page.settings().setAttribute(
320
QtWebKit.QWebSettings.PluginsEnabled, plugins_enabled)
321
self.page.settings().setAttribute(QtWebKit.QWebSettings.JavaEnabled,
324
if not show_scrollbars:
325
self.page.mainFrame().setScrollBarPolicy(QtCore.Qt.Vertical,
326
QtCore.Qt.ScrollBarAlwaysOff)
327
self.page.mainFrame().setScrollBarPolicy(QtCore.Qt.Horizontal,
328
QtCore.Qt.ScrollBarAlwaysOff)
330
self.set_viewport_size(*viewport_size)
333
self.page.loadFinished.connect(self._page_loaded)
334
self.page.loadStarted.connect(self._page_load_started)
335
self.page.unsupportedContent.connect(self._unsupported_content)
337
self.manager = self.page.networkAccessManager()
338
self.manager.finished.connect(self._request_ended)
339
self.manager.sslErrors.connect(self._on_manager_ssl_errors)
342
self.cache = QNetworkDiskCache()
343
self.cache.setCacheDirectory(cache_dir)
344
self.manager.setCache(self.cache)
348
self.cookie_jar = QNetworkCookieJar()
349
self.manager.setCookieJar(self.cookie_jar)
351
self.page.setUserAgent(self.user_agent)
353
self.page.networkAccessManager().authenticationRequired\
354
.connect(self._authenticate)
355
self.page.networkAccessManager().proxyAuthenticationRequired\
356
.connect(self._authenticate)
358
self.main_frame = self.page.mainFrame()
360
logger.setLevel(log_level)
362
class GhostQWebView(QtWebKit.QWebView):
364
return QSize(*viewport_size)
366
self.webview = GhostQWebView()
369
self.webview.settings().setAttribute(
370
QtWebKit.QWebSettings.PluginsEnabled, True)
372
self.webview.settings().setAttribute(
373
QtWebKit.QWebSettings.JavaEnabled, True)
375
self.webview.setPage(self.page)
383
def ascend_to_root_frame(self):
384
""" Set main frame as current main frame's parent.
386
# we can't ascend directly to parent frame because it might have been
388
self.main_frame = self.page.mainFrame()
390
def descend_frame(self, child_name):
391
""" Set main frame as one of current main frame's children.
393
:param child_name: The name of the child to descend to.
395
for frame in self.main_frame.childFrames():
396
if frame.frameName() == child_name:
397
self.main_frame = frame
399
# frame not found so we throw an exception
400
raise LookupError("Child frame '%s' not found." % child_name)
402
def capture(self, region=None, selector=None,
403
format=QImage.Format_ARGB32_Premultiplied):
404
"""Returns snapshot as QImage.
406
:param region: An optional tuple containing region as pixel
408
:param selector: A selector targeted the element to crop on.
409
:param format: The output image format.
412
self.main_frame.setScrollBarPolicy(QtCore.Qt.Vertical,
413
QtCore.Qt.ScrollBarAlwaysOff)
414
self.main_frame.setScrollBarPolicy(QtCore.Qt.Horizontal,
415
QtCore.Qt.ScrollBarAlwaysOff)
416
self.page.setViewportSize(self.main_frame.contentsSize())
417
image = QImage(self.page.viewportSize(), format)
418
painter = QPainter(image)
419
self.main_frame.render(painter)
422
if region is None and selector is not None:
423
region = self.region_for_selector(selector)
426
x1, y1, x2, y2 = region
427
w, h = (x2 - x1), (y2 - y1)
428
image = image.copy(x1, y1, w, h)
432
def capture_to(self, path, region=None, selector=None,
433
format=QImage.Format_ARGB32_Premultiplied):
434
"""Saves snapshot as image.
436
:param path: The destination path.
437
:param region: An optional tuple containing region as pixel
439
:param selector: A selector targeted the element to crop on.
440
:param format: The output image format.
442
self.capture(region=region, format=format,
443
selector=selector).save(path)
445
def print_to_pdf(self, path, paper_size=(8.5, 11.0),
446
paper_margins=(0, 0, 0, 0), paper_units=QPrinter.Inch,
448
"""Saves page as a pdf file.
450
See qt4 QPrinter documentation for more detailed explanations
453
:param path: The destination path.
454
:param paper_size: A 2-tuple indicating size of page to print to.
455
:param paper_margins: A 4-tuple indicating size of each margin.
456
:param paper_units: Units for pager_size, pager_margins.
457
:param zoom_factor: Scale the output content.
459
assert len(paper_size) == 2
460
assert len(paper_margins) == 4
461
printer = QPrinter(mode=QPrinter.ScreenResolution)
462
printer.setOutputFormat(QPrinter.PdfFormat)
463
printer.setPaperSize(QtCore.QSizeF(*paper_size), paper_units)
464
printer.setPageMargins(*(paper_margins + (paper_units,)))
465
printer.setFullPage(True)
466
printer.setOutputFileName(path)
467
if self.webview is None:
468
self.webview = QtWebKit.QWebView()
469
self.webview.setPage(self.page)
470
self.webview.setZoomFactor(zoom_factor)
471
self.webview.print_(printer)
474
def click(self, selector):
475
"""Click the targeted element.
477
:param selector: A CSS3 selector to targeted element.
479
if not self.exists(selector):
480
raise Error("Can't find element to click")
481
return self.evaluate("""
483
var element = document.querySelector(%s);
484
var evt = document.createEvent("MouseEvents");
485
evt.initMouseEvent("click", true, true, window, 1, 1, 1, 1, 1,
486
false, false, false, false, 0, element);
487
return element.dispatchEvent(evt);
489
""" % repr(selector))
492
"""Statement that tells Ghost how to deal with javascript confirm().
494
:param confirm: A boolean to set confirmation.
495
:param callable: A callable that returns a boolean for confirmation.
497
def __init__(self, confirm=True, callback=None):
498
self.confirm = confirm
499
self.callback = callback
502
Ghost._confirm_expected = (self.confirm, self.callback)
504
def __exit__(self, type, value, traceback):
505
Ghost._confirm_expected = None
508
def content(self, to_unicode=True):
509
"""Returns current frame HTML as a string.
511
:param to_unicode: Whether to convert html to unicode or not
514
return unicode(self.main_frame.toHtml())
516
return self.main_frame.toHtml()
520
"""Returns all cookies."""
521
return self.cookie_jar.allCookies()
523
def delete_cookies(self):
524
"""Deletes all cookies."""
525
self.cookie_jar.setAllCookies([])
527
def clear_alert_message(self):
528
"""Clears the alert message"""
532
def evaluate(self, script):
533
"""Evaluates script in page frame.
535
:param script: The script to evaluate.
537
return (self.main_frame.evaluateJavaScript("%s" % script),
538
self._release_last_resources())
540
def evaluate_js_file(self, path, encoding='utf-8', **kwargs):
541
"""Evaluates javascript file at given path in current frame.
542
Raises native IOException in case of invalid file.
544
:param path: The path of the file.
545
:param encoding: The file's encoding.
547
with codecs.open(path, encoding=encoding) as f:
548
return self.evaluate(f.read(), **kwargs)
550
def exists(self, selector):
551
"""Checks if element exists for given selector.
553
:param string: The element selector.
555
return not self.main_frame.findFirstElement(selector).isNull()
558
"""Exits application and related."""
565
if hasattr(self, 'xvfb'):
566
self.xvfb.terminate()
569
def fill(self, selector, values):
570
"""Fills a form with provided values.
572
:param selector: A CSS selector to the target form to fill.
573
:param values: A dict containing the values.
575
if not self.exists(selector):
576
raise Error("Can't find form")
579
r, res = self.set_field_value(
580
"%s [name=%s]" % (selector, repr(field)), values[field])
581
resources.extend(res)
582
return True, resources
585
def fire_on(self, selector, method):
586
"""Call method on element matching given selector.
588
:param selector: A CSS selector to the target element.
589
:param method: The name of the method to fire.
590
:param expect_loading: Specifies if a page loading is expected.
592
return self.evaluate('document.querySelector(%s)[%s]();' % \
593
(repr(selector), repr(method)))
595
def global_exists(self, global_name):
596
"""Checks if javascript global exists.
598
:param global_name: The name of the global.
600
return self.evaluate('!(typeof this[%s] === "undefined");' %
601
repr(global_name))[0]
604
"""Close the webview."""
608
raise Error("no webview to close")
610
def load_cookies(self, cookie_storage, keep_old=False):
611
"""load from cookielib's CookieJar or Set-Cookie3 format text file.
613
:param cookie_storage: file location string on disk or CookieJar
615
:param keep_old: Don't reset, keep cookies not overridden.
617
def toQtCookieJar(PyCookieJar, QtCookieJar):
618
allCookies = QtCookieJar.allCookies() if keep_old else []
619
for pc in PyCookieJar:
621
allCookies.append(qc)
622
QtCookieJar.setAllCookies(allCookies)
624
def toQtCookie(PyCookie):
625
qc = QNetworkCookie(PyCookie.name, PyCookie.value)
626
qc.setSecure(PyCookie.secure)
627
if PyCookie.path_specified:
628
qc.setPath(PyCookie.path)
629
if PyCookie.domain != "":
630
qc.setDomain(PyCookie.domain)
631
if PyCookie.expires and PyCookie.expires != 0:
633
t.setTime_t(PyCookie.expires)
634
qc.setExpirationDate(t)
635
# not yet handled(maybe less useful):
636
# py cookie.rest / QNetworkCookie.setHttpOnly()
639
if cookie_storage.__class__.__name__ == 'str':
640
cj = LWPCookieJar(cookie_storage)
642
toQtCookieJar(cj, self.cookie_jar)
643
elif cookie_storage.__class__.__name__.endswith('CookieJar'):
644
toQtCookieJar(cookie_storage, self.cookie_jar)
646
raise ValueError('unsupported cookie_storage type.')
648
def open(self, address, method='get', headers={}, auth=None, body=None,
649
default_popup_response=None, wait=True):
652
:param address: The resource URL.
653
:param method: The Http method.
654
:param headers: An optional dict of extra request hearders.
655
:param auth: An optional tuple of HTTP auth (username, password).
656
:param body: An optional string containing a payload.
657
:param default_popup_response: the default response for any confirm/
658
alert/prompt popup from the Javascript (replaces the need for the with
660
:param wait: If set to True (which is the default), this
661
method call waits for the page load to complete before
662
returning. Otherwise, it just starts the page load task and
663
it is the caller's responsibilty to wait for the load to
664
finish by other means (e.g. by calling wait_for_page_loaded()).
665
:return: Page resource, and all loaded resources, unless wait
666
is False, in which case it returns None.
668
body = body or QByteArray()
670
method = getattr(QNetworkAccessManager,
671
"%sOperation" % method.capitalize())
672
except AttributeError:
673
raise Error("Invalid http method %s" % method)
674
request = QNetworkRequest(QUrl(address))
675
request.CacheLoadControl(0)
676
for header in headers:
677
request.setRawHeader(header, headers[header])
679
self._auth_attempt = 0 # Avoids reccursion
681
self.main_frame.load(request, method, body)
684
if default_popup_response is not None:
685
Ghost._prompt_expected = (default_popup_response, None)
686
Ghost._confirm_expected = (default_popup_response, None)
689
return self.wait_for_page_loaded()
691
def scroll_to_anchor(self, anchor):
692
self.main_frame.scrollToAnchor(anchor)
695
"""Statement that tells Ghost how to deal with javascript prompt().
697
:param value: A string value to fill in prompt.
698
:param callback: A callable that returns the value to fill in.
700
def __init__(self, value='', callback=None):
702
self.callback = callback
705
Ghost._prompt_expected = (self.value, self.callback)
707
def __exit__(self, type, value, traceback):
708
Ghost._prompt_expected = None
710
def region_for_selector(self, selector):
711
"""Returns frame region for given selector as tuple.
713
:param selector: The targeted element.
715
geo = self.main_frame.findFirstElement(selector).geometry()
717
region = (geo.left(), geo.top(), geo.right(), geo.bottom())
719
raise Error("can't get region for selector '%s'" % selector)
722
def save_cookies(self, cookie_storage):
723
"""Save to cookielib's CookieJar or Set-Cookie3 format text file.
725
:param cookie_storage: file location string or CookieJar instance.
727
def toPyCookieJar(QtCookieJar, PyCookieJar):
728
for c in QtCookieJar.allCookies():
729
PyCookieJar.set_cookie(toPyCookie(c))
731
def toPyCookie(QtCookie):
733
port_specified = False
734
secure = QtCookie.isSecure()
735
name = str(QtCookie.name())
736
value = str(QtCookie.value())
737
v = str(QtCookie.path())
738
path_specified = bool(v != "")
739
path = v if path_specified else None
740
v = str(QtCookie.domain())
741
domain_specified = bool(v != "")
744
domain_initial_dot = v.startswith('.')
746
domain_initial_dot = None
747
v = long(QtCookie.expirationDate().toTime_t())
748
# Long type boundary on 32bit platfroms; avoid ValueError
749
expires = 2147483647 if v > 2147483647 else v
752
return Cookie(0, name, value, port, port_specified, domain,
753
domain_specified, domain_initial_dot, path, path_specified,
754
secure, expires, discard, None, None, rest)
756
if cookie_storage.__class__.__name__ == 'str':
757
cj = LWPCookieJar(cookie_storage)
758
toPyCookieJar(self.cookie_jar, cj)
760
elif cookie_storage.__class__.__name__.endswith('CookieJar'):
761
toPyCookieJar(self.cookie_jar, cookie_storage)
763
raise ValueError('unsupported cookie_storage type.')
766
def set_field_value(self, selector, value, blur=True):
767
"""Sets the value of the field matched by given selector.
769
:param selector: A CSS selector that target the field.
770
:param value: The value to fill in.
771
:param blur: An optional boolean that force blur when filled in.
773
def _set_checkbox_value(el, value):
776
el.setAttribute('checked', 'checked')
778
el.removeAttribute('checked')
780
def _set_checkboxes_value(els, value):
782
if el.attribute('value') == value:
783
_set_checkbox_value(el, True)
785
_set_checkbox_value(el, False)
787
def _set_radio_value(els, value):
789
if el.attribute('value') == value:
791
el.setAttribute('checked', 'checked')
793
def _set_text_value(el, value):
795
el.setAttribute('value', value)
797
def _set_select_value(el, value):
799
self.evaluate('document.querySelector(%s).value = %s;' %
800
(repr(selector), repr(value)))
802
def _set_textarea_value(el, value):
804
el.setPlainText(value)
806
res, ressources = None, []
807
element = self.main_frame.findFirstElement(selector)
809
raise Error('can\'t find element for %s"' % selector)
811
tag_name = str(element.tagName()).lower()
813
if tag_name == "select":
814
_set_select_value(element, value)
815
elif tag_name == "textarea":
816
_set_textarea_value(element, value)
817
elif tag_name == "input":
818
type_ = str(element.attribute('type')).lower()
820
"color", "date", "datetime",
821
"datetime-local", "email", "hidden", "month", "number",
822
"password", "range", "search", "tel", "text", "time",
824
_set_text_value(element, value)
825
elif type_ == "checkbox":
826
els = self.main_frame.findAllElements(selector)
828
_set_checkboxes_value(els, value)
830
_set_checkbox_value(element, value)
831
elif type_ == "radio":
832
_set_radio_value(self.main_frame.findAllElements(selector),
834
elif type_ == "file":
835
Ghost._upload_file = value
836
res, resources = self.click(selector)
837
Ghost._upload_file = None
839
raise Error('unsuported field tag')
841
self.fire_on(selector, 'blur')
842
return res, ressources
844
def set_proxy(self, type_, host='localhost', port=8888, user='',
846
"""Set up proxy for FURTHER connections.
848
:param type_: proxy type to use: \
849
none/default/socks5/https/http.
850
:param host: proxy server ip or host name.
851
:param port: proxy port.
854
'default': QNetworkProxy.DefaultProxy,
855
'none': QNetworkProxy.NoProxy,
856
'socks5': QNetworkProxy.Socks5Proxy,
857
'https': QNetworkProxy.HttpProxy,
858
'http': QNetworkProxy.HttpCachingProxy
863
type_ = type_.lower()
864
if type_ in ['none', 'default']:
865
self.manager.setProxy(QNetworkProxy(_types[type_]))
867
elif type_ in _types:
868
proxy = QNetworkProxy(_types[type_], hostName=host, port=port,
869
user=user, password=password)
870
self.manager.setProxy(proxy)
872
raise ValueError('Unsupported proxy type:' + type_ \
873
+ '\nsupported types are: none/socks5/http/https/default')
875
def set_viewport_size(self, width, height):
876
"""Sets the page viewport size.
878
:param width: An integer that sets width pixel count.
879
:param height: An integer that sets height pixel count.
881
self.page.setViewportSize(QSize(width, height))
883
def append_popup_message(self, message):
884
self.popup_messages.append(unicode(message))
887
"""Show current page inside a QWebView.
891
def sleep(self, value):
892
started_at = time.time()
895
Ghost._app.processEvents()
896
while time.time() <= (started_at + value):
898
Ghost._app.processEvents()
900
def wait_for(self, condition, timeout_message):
901
"""Waits until condition is True.
903
:param condition: A callable that returns the condition.
904
:param timeout_message: The exception message on timeout.
906
started_at = time.time()
907
while not condition():
908
if time.time() > (started_at + self.wait_timeout):
909
raise TimeoutError(timeout_message)
911
Ghost._app.processEvents()
912
if self.wait_callback is not None:
915
def wait_for_alert(self):
916
"""Waits for main frame alert().
918
self.wait_for(lambda: Ghost._alert is not None,
919
'User has not been alerted.')
922
return msg, self._release_last_resources()
924
def wait_for_page_loaded(self):
925
"""Waits until page is loaded, assumed that a page as been requested.
927
self.wait_for(lambda: self.loaded,
928
'Unable to load requested page')
929
resources = self._release_last_resources()
932
url = self.main_frame.url().toString()
933
url_without_hash = url.split("#")[0]
935
for resource in resources:
936
if url == resource.url or url_without_hash == resource.url:
938
return page, resources
940
def wait_for_selector(self, selector):
941
"""Waits until selector match an element on the frame.
943
:param selector: The selector to wait for.
945
self.wait_for(lambda: self.exists(selector),
946
'Can\'t find element matching "%s"' % selector)
947
return True, self._release_last_resources()
949
def wait_while_selector(self, selector):
950
"""Waits until the selector no longer matches an element on the frame.
952
:param selector: The selector to wait for.
954
self.wait_for(lambda: not self.exists(selector),
955
'Element matching "%s" is still available' % selector)
956
return True, self._release_last_resources()
958
def wait_for_text(self, text):
959
"""Waits until given text appear on main frame.
961
:param text: The text to wait for.
963
self.wait_for(lambda: text in self.content,
964
'Can\'t find "%s" in current frame' % text)
965
return True, self._release_last_resources()
967
def _authenticate(self, mix, authenticator):
968
"""Called back on basic / proxy http auth.
970
:param mix: The QNetworkReply or QNetworkProxy object.
971
:param authenticator: The QAuthenticator object.
973
if self._auth is not None and self._auth_attempt == 0:
974
username, password = self._auth
975
authenticator.setUser(username)
976
authenticator.setPassword(password)
977
self._auth_attempt += 1
979
def _page_loaded(self):
980
"""Called back when page is loaded.
986
def _page_load_started(self):
987
"""Called back when page load started.
991
def _release_last_resources(self):
992
"""Releases last loaded resources.
994
:return: The released resources.
996
last_resources = self.http_resources
997
self.http_resources = []
998
return last_resources
1000
def _request_ended(self, reply):
1001
"""Adds an HttpResource object to http_resources.
1003
:param reply: The QNetworkReply object.
1006
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute):
1007
Logger.log("[%s] bytesAvailable()= %s" % (str(reply.url()),
1008
reply.bytesAvailable()), level="debug")
1010
# Some web pages return cache headers that mandates not to cache
1011
# the reply, which means we won't find this QNetworkReply in
1012
# the cache object. In this case bytesAvailable will return > 0.
1013
# Such pages are www.etsy.com
1014
# This is a bit of a hack and due to the async nature of QT, might
1015
# not work at times. We should move to using some proxied
1016
# implementation of QNetworkManager and QNetworkReply in order to
1017
# get the contents of the requests properly rather than relying
1019
if reply.bytesAvailable() > 0:
1020
content = reply.peek(reply.bytesAvailable())
1023
self.http_resources.append(HttpResource(reply, self.cache,
1026
def _unsupported_content(self, reply):
1027
reply.readyRead.connect(
1028
lambda reply=reply: self._reply_download_content(reply))
1030
def _reply_download_content(self, reply):
1031
"""Adds an HttpResource object to http_resources with unsupported
1034
:param reply: The QNetworkReply object.
1036
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute):
1037
self.http_resources.append(HttpResource(reply, self.cache,
1040
def _on_manager_ssl_errors(self, reply, errors):
1041
url = unicode(reply.url().toString())
1042
if self.ignore_ssl_errors:
1043
reply.ignoreSslErrors()
1045
Logger.log('SSL certificate error: %s' % url, level='warning')
1047
def __enter__(self):
1050
def __exit__(self, exc_type, exc_val, exc_tb):