def _on_renderer_process_terminated(self, tab, status, code): """Show an error when a renderer process terminated.""" if status == browsertab.TerminationStatus.normal: return messages = { browsertab.TerminationStatus.abnormal: "Renderer process exited with status {}".format(code), browsertab.TerminationStatus.crashed: "Renderer process crashed", browsertab.TerminationStatus.killed: "Renderer process was killed", browsertab.TerminationStatus.unknown: "Renderer process did not start", } msg = messages[status] def show_error_page(html): tab.set_html(html) log.webview.error(msg) if qtutils.version_check('5.9', compiled=False): url_string = tab.url(requested=True).toDisplayString() error_page = jinja.render( 'error.html', title="Error loading {}".format(url_string), url=url_string, error=msg) QTimer.singleShot(100, lambda: show_error_page(error_page)) else: # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698 message.error(msg) self._remove_tab(tab, crashed=True) if self.widget.count() == 0: self.tabopen(QUrl('about:blank'))
def _move_data(old, new): """Migrate data from an old to a new directory. If the old directory does not exist, the migration is skipped. If the new directory already exists, an error is shown. Return: True if moving succeeded, False otherwise. """ if not os.path.exists(old): return False log.init.debug("Migrating data from {} to {}".format(old, new)) if os.path.exists(new): if not os.path.isdir(new) or os.listdir(new): message.error("Failed to move data from {} as {} is non-empty!" .format(old, new)) return False os.rmdir(new) try: shutil.move(old, new) except OSError as e: message.error("Failed to move data from {} to {}: {}".format( old, new, e)) return False return True
def run(self, win_id, args=None, count=None): """Run the command. Note we don't catch CommandError here as it might happen async. Args: win_id: The window ID the command is run in. args: Arguments to the command. count: Command repetition count. """ dbgout = ["command called:", self.name] if args: dbgout.append(str(args)) elif args is None: args = [] if count is not None: dbgout.append("(count={})".format(count)) log.commands.debug(' '.join(dbgout)) try: self.namespace = self.parser.parse_args(args) except argparser.ArgumentParserError as e: message.error('{}: {}'.format(self.name, e), stack=traceback.format_exc()) return except argparser.ArgumentParserExit as e: log.commands.debug("argparser exited with status {}: {}".format( e.status, e)) return self._count = count self._check_prerequisites(win_id) posargs, kwargs = self._get_call_args(win_id) log.commands.debug('Calling {}'.format( debug_utils.format_call(self.handler, posargs, kwargs))) self.handler(*posargs, **kwargs)
def _parse_line(self, line): try: key, url = line.rsplit(maxsplit=1) except ValueError: message.error("Invalid quickmark '{}'".format(line)) else: self.marks[key] = url
def quickmark_add(self, url, name): """Add a new quickmark. You can view all saved quickmarks on the link:glimpse://bookmarks[bookmarks page]. Args: url: The url to add as quickmark. name: The name for the new quickmark. """ # We don't raise cmdutils.CommandError here as this can be called async # via prompt_save. if not name: message.error("Can't set mark with empty name!") return if not url: message.error("Can't set mark with empty URL!") return def set_mark(): """Really set the quickmark.""" self.marks[name] = url self.changed.emit() log.misc.debug("Added quickmark {} for {}".format(name, url)) if name in self.marks: message.confirm_async(title="Override existing quickmark?", yes_action=set_mark, default=True, url=url) else: set_mark()
def _hint_strings(self, elems): """Calculate the hint strings for elems. Inspired by Vimium. Args: elems: The elements to get hint strings for. Return: A list of hint strings, in the same order as the elements. """ if not elems: return [] hint_mode = self._context.hint_mode if hint_mode == 'word': try: return self._word_hinter.hint(elems) except HintingError as e: message.error(str(e)) # falls back on letter hints if hint_mode == 'number': chars = '0123456789' else: chars = config.val.hints.chars min_chars = config.val.hints.min_chars if config.val.hints.scatter and hint_mode != 'number': return self._hint_scattered(min_chars, chars, elems) else: return self._hint_linear(min_chars, chars, elems)
def autosave(self): """Slot used when the configs are auto-saved.""" for (key, saveable) in self.saveables.items(): try: saveable.save(silent=True) except OSError as e: message.error("Failed to auto-save {}: {}".format(key, e))
def set_target(self, target): """Set the target for a given download. Args: target: The DownloadTarget for this download. """ if isinstance(target, FileObjDownloadTarget): self._set_fileobj(target.fileobj, autoclose=False) elif isinstance(target, FileDownloadTarget): self._set_filename(target.filename, force_overwrite=target.force_overwrite) elif isinstance(target, (OpenFileDownloadTarget, PDFJSDownloadTarget)): try: fobj = temp_download_manager.get_tmpfile(self.basename) except OSError as exc: msg = "Download error: {}".format(exc) message.error(msg) self.cancel() return if isinstance(target, OpenFileDownloadTarget): self.finished.connect( functools.partial(self._open_if_successful, target.cmdline)) elif isinstance(target, PDFJSDownloadTarget): self.finished.connect(self._pdfjs_if_successful) else: raise utils.Unreachable self._set_tempfile(fobj) else: # pragma: no cover raise ValueError("Unsupported download target: {}".format(target))
def _prevnext_cb(elems): elem = _find_prevnext(prev, elems) word = 'prev' if prev else 'forward' if elem is None: message.error("No {} links found!".format(word)) return url = elem.resolve_url(baseurl) if url is None: message.error("No {} links found!".format(word)) return qtutils.ensure_valid(url) cur_tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) if window: new_window = mainwindow.MainWindow( private=cur_tabbed_browser.is_private) new_window.show() tabbed_browser = objreg.get('tabbed-browser', scope='window', window=new_window.win_id) tabbed_browser.tabopen(url, background=False) elif tab: cur_tabbed_browser.tabopen(url, background=background) else: browsertab.load_url(url)
def glimpse_settings(url): """Handler for glimpse://settings. View/change glimpse configuration.""" global csrf_token if url.path() == '/set': if url.password() != csrf_token: message.error("Invalid CSRF token for glimpse://settings!") raise RequestDeniedError("Invalid CSRF token!") return _glimpse_settings_set(url) # Requests to glimpse://settings/set should only be allowed from # glimpse://settings. As an additional security precaution, we generate a CSRF # token to use here. if secrets: csrf_token = secrets.token_urlsafe() else: # On Python < 3.6, from secrets.py token = base64.urlsafe_b64encode(os.urandom(32)) csrf_token = token.rstrip(b'=').decode('ascii') src = jinja.render('settings.html', title='settings', configdata=configdata, confget=config.instance.get_str, csrf_token=csrf_token) return 'text/html', src
def _on_finished(self, code, status): """Show a message when the process finished.""" self._started = False log.procs.debug("Process finished with code {}, status {}.".format( code, status)) encoding = locale.getpreferredencoding(do_setlocale=False) stderr = bytes(self._proc.readAllStandardError()).decode( encoding, 'replace') stdout = bytes(self._proc.readAllStandardOutput()).decode( encoding, 'replace') if status == QProcess.CrashExit: exitinfo = "{} crashed!".format(self._what.capitalize()) message.error(exitinfo) elif status == QProcess.NormalExit and code == 0: exitinfo = "{} exited successfully.".format( self._what.capitalize()) if self.verbose: message.info(exitinfo) else: assert status == QProcess.NormalExit # We call this 'status' here as it makes more sense to the user - # it's actually 'code'. exitinfo = ("{} exited with status {}, see :messages for " "details.").format(self._what.capitalize(), code) message.error(exitinfo) if stdout: log.procs.error("Process stdout:\n" + stdout.strip()) if stderr: log.procs.error("Process stderr:\n" + stderr.strip()) glimpsescheme.spawn_output = self._spawn_format(exitinfo, stdout, stderr)
def _load_session(name): """Load the default session. Args: name: The name of the session to load, or None to read state file. """ session_manager = objreg.get('session-manager') if name is None and session_manager.exists('_autosave'): name = '_autosave' elif name is None: try: name = configfiles.state['general']['session'] except KeyError: # No session given as argument and none in the session file -> # start without loading a session return try: session_manager.load(name) except sessions.SessionNotFoundError: message.error("Session {} not found!".format(name)) except sessions.SessionError as e: message.error("Failed to load session {}: {}".format(name, e)) try: del configfiles.state['general']['session'] except KeyError: pass # If this was a _restart session, delete it. if name == '_restart': session_manager.delete('_restart')
def on_file_updated() -> None: """Source the new config when editing finished. This can't use cmdutils.CommandError as it's run async. """ try: configfiles.read_config_py(filename) except configexc.ConfigFileErrors as e: message.error(str(e))
def callback(text): """Set the commandline to the edited text.""" if not text or text[0] not in modeparsers.STARTCHARS: message.error('command must start with one of {}'.format( modeparsers.STARTCHARS)) return self.set_cmd_text(text) if run: self.command_accept()
def accept(self, value=None, save=False): self._check_save_support(save) text = value if value is not None else self._lineedit.text() text = downloads.transform_path(text) if text is None: message.error("Invalid filename") return False self.question.answer = text return True
def _handle_error(self, safely) -> typing.Iterator[None]: """Show exceptions as errors if safely=True is given.""" try: yield except cmdexc.Error as e: if safely: message.error(str(e), stack=traceback.format_exc()) else: raise
def backup(self): """Create a backup if the content has changed from the original.""" if not self._content: return try: fname = self._create_tempfile(self._content, 'glimpsebrowser-editor-backup-') message.info('Editor backup at {}'.format(fname)) except OSError as e: message.error('Failed to create editor backup: {}'.format(e))
def prepare_run(self, *args, **kwargs): self._args = args self._kwargs = kwargs try: handle = tempfile.NamedTemporaryFile(delete=False) handle.close() self._filepath = handle.name except OSError as e: message.error("Error while creating tempfile: {}".format(e)) return
def event(self, e): """Handle macOS FileOpen events.""" if e.type() == QEvent.FileOpen: url = e.url() if url.isValid(): open_url(url, no_raise=True) else: message.error("Invalid URL: {}".format(url.errorString())) else: return super().event(e) return True
def invalid_url_error(url, action): """Display an error message for a URL. Args: action: The action which was interrupted by the error. """ if url.isValid(): raise ValueError("Calling invalid_url_error with valid URL {}".format( url.toDisplayString())) errstring = get_errstring( url, "Trying to {} with invalid URL".format(action)) message.error(errstring)
def start_detached(self, cmd, args): """Convenience wrapper around QProcess::startDetached.""" log.procs.debug("Starting detached.") self._pre_start(cmd, args) ok, _pid = self._proc.startDetached(cmd, args, None) if not ok: message.error("Error while spawning {}".format(self._what)) return False log.procs.debug("Process started.") self._started = True return True
def _on_file_changed(self, path): try: with open(path, 'r', encoding=config.val.editor.encoding) as f: text = f.read() except OSError as e: # NOTE: Do not replace this with "raise CommandError" as it's # executed async. message.error("Failed to read back edited file: {}".format(e)) return log.procs.debug("Read back: {}".format(text)) if self._content != text: self._content = text self.file_updated.emit(text)
def _set_filename(self, filename, *, force_overwrite=False, remember_directory=True): """Set the filename to save the download to. Args: filename: The full filename to save the download to. None: special value to stop the download. force_overwrite: Force overwriting existing files. remember_directory: If True, remember the directory for future downloads. """ filename = os.path.expanduser(filename) self._ensure_can_set_filename(filename) self._filename = create_full_filename(self.basename, filename) if self._filename is None: # We only got a filename (without directory) or a relative path # from the user, so we append that to the default directory and # try again. self._filename = create_full_filename( self.basename, os.path.join(download_dir(), filename)) # At this point, we have a misconfigured XDG_DOWNLOAD_DIR, as # download_dir() + filename is still no absolute path. # The config value is checked for "absoluteness", but # ~/.config/user-dirs.dirs may be misconfigured and a non-absolute path # may be set for XDG_DOWNLOAD_DIR if self._filename is None: message.error( "XDG_DOWNLOAD_DIR points to a relative path - please check" " your ~/.config/user-dirs.dirs. The download is saved in" " your home directory.", ) # fall back to $HOME as download_dir self._filename = create_full_filename(self.basename, os.path.expanduser('~')) dirname = os.path.dirname(self._filename) if not os.path.exists(dirname): txt = ("<b>{}</b> does not exist. Create it?".format( html.escape(os.path.join(dirname, "")))) self._ask_create_parent_question("Create directory?", txt, force_overwrite, remember_directory) else: self._after_create_parent_question(force_overwrite, remember_directory)
def ignore_certificate_errors(url, errors, abort_on): """Display a certificate error question. Args: url: The URL the errors happened in errors: A list of QSslErrors or QWebEngineCertificateErrors Return: True if the error should be ignored, False otherwise. """ ssl_strict = config.instance.get('content.ssl_strict', url=url) log.webview.debug("Certificate errors {!r}, strict {}".format( errors, ssl_strict)) for error in errors: assert error.is_overridable(), repr(error) if ssl_strict == 'ask': err_template = jinja.environment.from_string(""" Errors while loading <b>{{url.toDisplayString()}}</b>:<br/> <ul> {% for err in errors %} <li>{{err}}</li> {% endfor %} </ul> """.strip()) msg = err_template.render(url=url, errors=errors) urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) ignore = message.ask(title="Certificate errors - continue?", text=msg, mode=usertypes.PromptMode.yesno, default=False, abort_on=abort_on, url=urlstr) if ignore is None: # prompt aborted ignore = False return ignore elif ssl_strict is False: log.webview.debug("ssl_strict is False, only warning about errors") for err in errors: # FIXME we might want to use warn here (non-fatal error) # https://github.com/glimpsebrowser/glimpsebrowser/issues/114 message.error('Certificate error: {}'.format(err)) return True elif ssl_strict is True: return False else: raise ValueError("Invalid ssl_strict value {!r}".format(ssl_strict)) raise utils.Unreachable
def glimpse_help(url): """Handler for glimpse://help.""" urlpath = url.path() if not urlpath or urlpath == '/': urlpath = 'index.html' else: urlpath = urlpath.lstrip('/') if not docutils.docs_up_to_date(urlpath): message.error("Your documentation is outdated! Please re-run " "scripts/asciidoc2html.py.") path = 'html/doc/{}'.format(urlpath) if not urlpath.endswith('.html'): try: bdata = utils.read_file(path, binary=True) except OSError as e: raise SchemeOSError(e) mimetype = utils.guess_mimetype(urlpath) return mimetype, bdata try: data = utils.read_file(path) except OSError: asciidoc = _asciidoc_fallback_path(path) if asciidoc is None: raise preamble = textwrap.dedent(""" There was an error loading the documentation! This most likely means the documentation was not generated properly. If you are running glimpsebrowser from the git repository, please (re)run scripts/asciidoc2html.py and reload this page. If you're running a released version this is a bug, please use :report to report it. Falling back to the plaintext version. --------------------------------------------------------------- """) return 'text/plain', (preamble + asciidoc).encode('utf-8') else: return 'text/html', data
def _start_cb(self, elems): """Initialize the elements and labels based on the context set.""" if self._context is None: log.hints.debug("In _start_cb without context!") return if not elems: message.error("No elements found.") return # Because _start_cb is called asynchronously, it's possible that the # user switched to another tab or closed the tab/window. In that case # we should not start hinting. tabbed_browser = objreg.get('tabbed-browser', default=None, scope='window', window=self._win_id) tab = tabbed_browser.widget.currentWidget() if tab.tab_id != self._tab_id: log.hints.debug( "Current tab changed ({} -> {}) before _start_cb is run." .format(self._tab_id, tab.tab_id)) return strings = self._hint_strings(elems) log.hints.debug("hints: {}".format(', '.join(strings))) for elem, string in zip(elems, strings): label = HintLabel(elem, self._context) label.update_text('', string) self._context.all_labels.append(label) self._context.labels[string] = label keyparsers = objreg.get('keyparsers', scope='window', window=self._win_id) keyparser = keyparsers[usertypes.KeyMode.hint] keyparser.update_bindings(strings) message_bridge = objreg.get('message-bridge', scope='window', window=self._win_id) message_bridge.set_text(self._get_text()) modeman.enter(self._win_id, usertypes.KeyMode.hint, 'HintManager.start') if self._context.first: self._fire(strings[0]) return # to make auto_follow == 'always' work self._handle_auto_follow()
def early_init(args: argparse.Namespace) -> None: """Initialize the part of the config which works without a QApplication.""" configdata.init() yaml_config = configfiles.YamlConfig() config.instance = config.Config(yaml_config=yaml_config) config.val = config.ConfigContainer(config.instance) configapi.val = config.ConfigContainer(config.instance) config.key_instance = config.KeyConfig(config.instance) config.cache = configcache.ConfigCache() yaml_config.setParent(config.instance) for cf in config.change_filters: cf.validate() config_commands = configcommands.ConfigCommands( config.instance, config.key_instance) objreg.register('config-commands', config_commands) config_file = standarddir.config_py() try: if os.path.exists(config_file): configfiles.read_config_py(config_file) else: configfiles.read_autoconfig() except configexc.ConfigFileErrors as e: log.config.exception("Error while loading {}".format(e.basename)) global _init_errors _init_errors = e configfiles.init() for opt, val in args.temp_settings: try: config.instance.set_str(opt, val) except configexc.Error as e: message.error("set: {} - {}".format(e.__class__.__name__, e)) objects.backend = get_backend(args) configtypes.Font.monospace_fonts = config.val.fonts.monospace config.instance.changed.connect(_update_monospace_fonts) _init_envvars()
def prompt_yank(self, sel=False): """Yank URL to clipboard or primary selection. Args: sel: Use the primary selection instead of the clipboard. """ question = self._prompt.question if question.url is None: message.error('No URL found.') return if sel and utils.supports_selection(): target = 'primary selection' else: sel = False target = 'clipboard' utils.set_clipboard(question.url, sel) message.info("Yanked to {}: {}".format(target, question.url))
def _mousepress_backforward(self, e): """Handle back/forward mouse button presses. Args: e: The QMouseEvent. """ if e.button() in [Qt.XButton1, Qt.LeftButton]: # Back button on mice which have it, or rocker gesture if self._tab.history.can_go_back(): self._tab.history.back() else: message.error("At beginning of history.") elif e.button() in [Qt.XButton2, Qt.RightButton]: # Forward button on mice which have it, or rocker gesture if self._tab.history.can_go_forward(): self._tab.history.forward() else: message.error("At end of history.")
def download_dir(): """Get the download directory to use.""" directory = config.val.downloads.location.directory remember_dir = config.val.downloads.location.remember if remember_dir and last_used_directory is not None: ddir = last_used_directory elif directory is None: ddir = standarddir.download() else: ddir = directory try: os.makedirs(ddir, exist_ok=True) except OSError as e: message.error("Failed to create download directory: {}".format(e)) return ddir