def _find_prevnext(self, frame, prev=False): """Find a prev/next element in frame.""" # First check for <link rel="prev(ious)|next"> elems = frame.findAllElements(webelem.SELECTORS[webelem.Group.links]) rel_values = ('prev', 'previous') if prev else ('next') for e in elems: e = webelem.WebElementWrapper(e) try: rel_attr = e['rel'] except KeyError: continue if rel_attr in rel_values: log.hints.debug("Found '{}' with rel={}".format( e.debug_text(), rel_attr)) return e # Then check for regular links/buttons. elems = frame.findAllElements( webelem.SELECTORS[webelem.Group.prevnext]) option = 'prev-regexes' if prev else 'next-regexes' if not elems: return None for regex in config.get('hints', option): log.hints.vdebug("== Checking regex '{}'.".format(regex.pattern)) for e in elems: e = webelem.WebElementWrapper(e) text = str(e) if not text: continue if regex.search(text): log.hints.debug("Regex '{}' matched on '{}'.".format( regex.pattern, text)) return e else: log.hints.vdebug("No match on '{}'!".format(text)) return None
def _init_elements(self, mainframe, group): """Initialize the elements and labels based on the context set. Args: mainframe: The main QWebFrame. group: A Group enum member (which elements to find). """ elems = [] for f in self._context.frames: elems += f.findAllElements(webelem.SELECTORS[group]) elems = [e for e in elems if webelem.is_visible(e, mainframe)] # We wrap the elements late for performance reasons, as wrapping 1000s # of elements (with ~50 methods each) just takes too much time... elems = [webelem.WebElementWrapper(e) for e in elems] filterfunc = webelem.FILTERS.get(group, lambda e: True) elems = [e for e in elems if filterfunc(e)] if not elems: raise cmdexc.CommandError("No elements found.") strings = self._hint_strings(elems) for e, string in zip(elems, strings): label = self._draw_label(e, string) self._context.elems[string] = ElemTuple(e, label) keyparsers = objreg.get('keyparsers', scope='window', window=self._win_id) keyparser = keyparsers[usertypes.KeyMode.hint] keyparser.update_bindings(strings)
def on_load_finished(self, ok): """Handle auto-insert-mode after loading finished.""" if ok and not self._has_ssl_errors: self._set_load_status(LoadStatus.success) elif ok: self._set_load_status(LoadStatus.warn) else: self._set_load_status(LoadStatus.error) if not config.get('input', 'auto-insert-mode'): return mode_manager = objreg.get('mode-manager', scope='window', window=self._win_id) cur_mode = mode_manager.mode() if cur_mode == usertypes.KeyMode.insert or not ok: return frame = self.page().currentFrame() try: elem = webelem.WebElementWrapper(frame.findFirstElement(':focus')) except webelem.IsNullError: log.webview.debug("Focused element is null!") return log.modes.debug("focus element: {}".format(repr(elem))) if elem.is_editable(): modeman.maybe_enter(self._win_id, usertypes.KeyMode.insert, 'load finished')
def _draw_label(self, elem, string): """Draw a hint label over an element. Args: elem: The QWebElement to use. string: The hint string to print. Return: The newly created label element """ doc = elem.webFrame().documentElement() # It seems impossible to create an empty QWebElement for which isNull() # is false so we can work with it. # As a workaround, we use appendInside() with markup as argument, and # then use lastChild() to get a reference to it. # See: http://stackoverflow.com/q/7364852/2085149 body = doc.findFirst('body') if not body.isNull(): parent = body else: parent = doc parent.appendInside('<span></span>') label = webelem.WebElementWrapper(parent.lastChild()) label['class'] = 'qutehint' self._set_style_properties(elem, label) label.setPlainText(string) return label
def on_load_finished(self): """Handle auto-insert-mode after loading finished. We don't take loadFinished's ok argument here as it always seems to be true when the QWebPage has an ErrorPageExtension implemented. See https://github.com/The-Compiler/qutebrowser/issues/84 """ ok = not self.page().error_occured if ok and not self._has_ssl_errors: self._set_load_status(LoadStatus.success) elif ok: self._set_load_status(LoadStatus.warn) else: self._set_load_status(LoadStatus.error) if not config.get('input', 'auto-insert-mode'): return mode_manager = objreg.get('mode-manager', scope='window', window=self._win_id) cur_mode = mode_manager.mode() if cur_mode == usertypes.KeyMode.insert or not ok: return frame = self.page().currentFrame() try: elem = webelem.WebElementWrapper(frame.findFirstElement(':focus')) except webelem.IsNullError: log.webview.debug("Focused element is null!") return log.modes.debug("focus element: {}".format(repr(elem))) if elem.is_editable(): modeman.maybe_enter(self._win_id, usertypes.KeyMode.insert, 'load finished')
def _init_elements(self): """Initialize the elements and labels based on the context set.""" elems = [] for f in self._context.frames: elems += f.findAllElements(webelem.SELECTORS[self._context.group]) elems = [ e for e in elems if webelem.is_visible(e, self._context.mainframe) ] # We wrap the elements late for performance reasons, as wrapping 1000s # of elements (with ~50 methods each) just takes too much time... elems = [webelem.WebElementWrapper(e) for e in elems] filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True) elems = [e for e in elems if filterfunc(e)] if not elems: raise cmdexc.CommandError("No elements found.") strings = self._hint_strings(elems) log.hints.debug("hints: {}".format(', '.join(strings))) for e, string in zip(elems, strings): label = self._draw_label(e, string) elem = ElemTuple(e, label) self._context.all_elems.append(elem) self._context.elems[string] = elem keyparsers = objreg.get('keyparsers', scope='window', window=self._win_id) keyparser = keyparsers[usertypes.KeyMode.hint] keyparser.update_bindings(strings)
def get_webelem(geometry=None, frame=None, null=False, visibility='', display='', attributes=None, tagname=None, classes=None): """Factory for WebElementWrapper objects based on a mock. Args: geometry: The geometry of the QWebElement as QRect. frame: The QWebFrame the element is in. null: Whether the element is null or not. visibility: The CSS visibility style property value. display: The CSS display style property value. attributes: Boolean HTML attributes to be added. tagname: The tag name. classes: HTML classes to be added. """ elem = mock.Mock() elem.isNull.return_value = null elem.geometry.return_value = geometry elem.webFrame.return_value = frame elem.tagName.return_value = tagname elem.toOuterXml.return_value = '<fakeelem/>' if attributes is not None: if not isinstance(attributes, collections.abc.Mapping): attributes = {e: None for e in attributes} elem.hasAttribute.side_effect = lambda k: k in attributes elem.attribute.side_effect = lambda k: attributes.get(k, '') elem.attributeNames.return_value = list(attributes) else: elem.hasAttribute.return_value = False elem.attribute.return_value = '' elem.attributeNames.return_value = [] if classes is not None: elem.classes.return_value = classes.split(' ') else: elem.classes.return_value = [] def _style_property(name, strategy): """Helper function to act as styleProperty method.""" if strategy != QWebElement.ComputedStyle: raise ValueError("styleProperty called with strategy != " "ComputedStyle ({})!".format(strategy)) if name == 'visibility': return visibility elif name == 'display': return display else: raise ValueError("styleProperty called with unknown name " "'{}'".format(name)) elem.styleProperty.side_effect = _style_property wrapped = webelem.WebElementWrapper(elem) if attributes is not None: wrapped.update(attributes) return wrapped
def test_selectors(self, webframe, group, val, matching): webframe.setHtml('<html><body>{}</body></html>'.format(val)) # Make sure setting HTML succeeded and there's a new element assert len(webframe.findAllElements('*')) == 3 elems = webframe.findAllElements(webelem.SELECTORS[group]) elems = [webelem.WebElementWrapper(e) for e in elems] filterfunc = webelem.FILTERS.get(group, lambda e: True) elems = [e for e in elems if filterfunc(e)] assert bool(elems) == matching
def get_webelem(geometry=None, frame=None, null=False, style=None, display='', attributes=None, tagname=None, classes=None): """Factory for WebElementWrapper objects based on a mock. Args: geometry: The geometry of the QWebElement as QRect. frame: The QWebFrame the element is in. null: Whether the element is null or not. style: A dict with the styleAttributes of the element. attributes: Boolean HTML attributes to be added. tagname: The tag name. classes: HTML classes to be added. """ elem = mock.Mock() elem.isNull.return_value = null elem.geometry.return_value = geometry elem.webFrame.return_value = frame elem.tagName.return_value = tagname elem.toOuterXml.return_value = '<fakeelem/>' elem.toPlainText.return_value = 'text' attribute_dict = {} if attributes is None: pass elif not isinstance(attributes, collections.abc.Mapping): attribute_dict.update({e: None for e in attributes}) else: attribute_dict.update(attributes) elem.hasAttribute.side_effect = lambda k: k in attribute_dict elem.attribute.side_effect = lambda k: attribute_dict.get(k, '') elem.setAttribute.side_effect = (lambda k, v: operator.setitem(attribute_dict, k, v)) elem.removeAttribute.side_effect = attribute_dict.pop elem.attributeNames.return_value = list(attribute_dict) if classes is not None: elem.classes.return_value = classes.split(' ') else: elem.classes.return_value = [] style_dict = {'visibility': '', 'display': ''} if style is not None: style_dict.update(style) def _style_property(name, strategy): """Helper function to act as styleProperty method.""" if strategy != QWebElement.ComputedStyle: raise ValueError("styleProperty called with strategy != " "ComputedStyle ({})!".format(strategy)) return style_dict[name] elem.styleProperty.side_effect = _style_property wrapped = webelem.WebElementWrapper(elem) return wrapped
def _mousepress_insertmode(self, e): """Switch to insert mode when an editable element was clicked. Args: e: The QMouseEvent. """ pos = e.pos() frame = self.page().frameAt(pos) if frame is None: # This happens when we click inside the webview, but not actually # on the QWebPage - for example when clicking the scrollbar # sometimes. log.mouse.debug("Clicked at {} but frame is None!".format(pos)) return # You'd think we have to subtract frame.geometry().topLeft() from the # position, but it seems QWebFrame::hitTestContent wants a position # relative to the QWebView, not to the frame. This makes no sense to # me, but it works this way. hitresult = frame.hitTestContent(pos) if hitresult.isNull(): # For some reason, the whole hit result can be null sometimes (e.g. # on doodle menu links). If this is the case, we schedule a check # later (in mouseReleaseEvent) which uses webelem.focus_elem. log.mouse.debug("Hitresult is null!") self._check_insertmode = True return try: elem = webelem.WebElementWrapper(hitresult.element()) except webelem.IsNullError: # For some reason, the hit result element can be a null element # sometimes (e.g. when clicking the timetable fields on # http://www.sbb.ch/ ). If this is the case, we schedule a check # later (in mouseReleaseEvent) which uses webelem.focus_elem. log.mouse.debug("Hitresult element is null!") self._check_insertmode = True return if ((hitresult.isContentEditable() and elem.is_writable()) or elem.is_editable()): log.mouse.debug("Clicked editable element!") modeman.enter(self.win_id, usertypes.KeyMode.insert, 'click', only_if_normal=True) else: log.mouse.debug("Clicked non-editable element!") if config.get('input', 'auto-leave-insert-mode'): modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert, 'click')
def _handle_auto_insert_mode(self, ok): """Handle auto-insert-mode after loading finished.""" if not config.get('input', 'auto-insert-mode'): return mode_manager = objreg.get('mode-manager', scope='window', window=self.win_id) cur_mode = mode_manager.mode if cur_mode == usertypes.KeyMode.insert or not ok: return frame = self.page().currentFrame() try: elem = webelem.WebElementWrapper(frame.findFirstElement(':focus')) except webelem.IsNullError: log.webview.debug("Focused element is null!") return log.modes.debug("focus element: {}".format(repr(elem))) if elem.is_editable(): modeman.enter(self.win_id, usertypes.KeyMode.insert, 'load finished', only_if_normal=True)
def test_double_wrap(self, elem): """Test wrapping a WebElementWrapper.""" with pytest.raises(TypeError) as excinfo: webelem.WebElementWrapper(elem) assert str(excinfo.value) == "Trying to wrap a wrapper!"
def run(self): """Download and save the page. The object must not be reused, you should create a new one if you want to download another page. """ if self._used: raise ValueError("Downloader already used") self._used = True web_url = self.web_view.url() web_frame = self.web_view.page().mainFrame() self.writer = MHTMLWriter( web_frame.toHtml().encode('utf-8'), content_location=urlutils.encoded_url(web_url), # I've found no way of getting the content type of a QWebView, but # since we're using .toHtml, it's probably safe to say that the # content-type is HTML content_type='text/html; charset="UTF-8"', ) # Currently only downloading <link> (stylesheets), <script> # (javascript) and <img> (image) elements. elements = web_frame.findAllElements('link, script, img') for element in elements: element = webelem.WebElementWrapper(element) # Websites are free to set whatever rel=... attribute they want. # We just care about stylesheets and icons. if not _check_rel(element): continue if 'src' in element: element_url = element['src'] elif 'href' in element: element_url = element['href'] else: # Might be a local <script> tag or something else continue absolute_url = web_url.resolved(QUrl(element_url)) self._fetch_url(absolute_url) styles = web_frame.findAllElements('style') for style in styles: style = webelem.WebElementWrapper(style) # The Mozilla Developer Network says: # type: This attribute defines the styling language as a MIME type # (charset should not be specified). This attribute is optional and # default to text/css if it's missing. # https://developer.mozilla.org/en/docs/Web/HTML/Element/style if 'type' in style and style['type'] != 'text/css': continue for element_url in _get_css_imports(str(style)): self._fetch_url(web_url.resolved(QUrl(element_url))) # Search for references in inline styles for element in web_frame.findAllElements('[style]'): element = webelem.WebElementWrapper(element) style = element['style'] for element_url in _get_css_imports(style, inline=True): self._fetch_url(web_url.resolved(QUrl(element_url))) # Shortcut if no assets need to be downloaded, otherwise the file would # never be saved. Also might happen if the downloads are fast enough to # complete before connecting their finished signal. self._collect_zombies() if not self.pending_downloads and not self._finished_file: self._finish_file()
def get_webelem(geometry=None, frame=None, *, null=False, style=None, attributes=None, tagname=None, classes=None, parent=None, js_rect_return=None, zoom_text_only=False): """Factory for WebElementWrapper objects based on a mock. Args: geometry: The geometry of the QWebElement as QRect. frame: The QWebFrame the element is in. null: Whether the element is null or not. style: A dict with the styleAttributes of the element. attributes: Boolean HTML attributes to be added. tagname: The tag name. classes: HTML classes to be added. js_rect_return: If None, what evaluateJavaScript returns is based on geometry. If set, the return value of evaluateJavaScript. zoom_text_only: Whether zoom-text-only is set in the config """ # pylint: disable=too-many-locals,too-many-branches elem = mock.Mock() elem.isNull.return_value = null elem.geometry.return_value = geometry elem.webFrame.return_value = frame elem.tagName.return_value = tagname elem.toOuterXml.return_value = '<fakeelem/>' elem.toPlainText.return_value = 'text' elem.parent.return_value = parent if geometry is not None: if frame is None: scroll_x = 0 scroll_y = 0 else: scroll_x = frame.scrollPosition().x() scroll_y = frame.scrollPosition().y() if js_rect_return is None: if frame is None or zoom_text_only: zoom = 1.0 else: zoom = frame.zoomFactor() elem.evaluateJavaScript.return_value = { "length": 1, "0": { "left": (geometry.left() - scroll_x) / zoom, "top": (geometry.top() - scroll_y) / zoom, "right": (geometry.right() - scroll_x) / zoom, "bottom": (geometry.bottom() - scroll_y) / zoom, "width": geometry.width() / zoom, "height": geometry.height() / zoom, } } else: elem.evaluateJavaScript.return_value = js_rect_return attribute_dict = {} if attributes is None: pass elif not isinstance(attributes, collections.abc.Mapping): attribute_dict.update({e: None for e in attributes}) else: attribute_dict.update(attributes) elem.hasAttribute.side_effect = lambda k: k in attribute_dict elem.attribute.side_effect = lambda k: attribute_dict.get(k, '') elem.setAttribute.side_effect = (lambda k, v: operator.setitem(attribute_dict, k, v)) elem.removeAttribute.side_effect = attribute_dict.pop elem.attributeNames.return_value = list(attribute_dict) if classes is not None: elem.classes.return_value = classes.split(' ') else: elem.classes.return_value = [] style_dict = {'visibility': '', 'display': ''} if style is not None: style_dict.update(style) def _style_property(name, strategy): """Helper function to act as styleProperty method.""" if strategy != QWebElement.ComputedStyle: raise ValueError("styleProperty called with strategy != " "ComputedStyle ({})!".format(strategy)) return style_dict[name] elem.styleProperty.side_effect = _style_property wrapped = webelem.WebElementWrapper(elem) return wrapped