#include "browsertab.hpp" #include "ui_browsertab.h" #include "mainwindow.hpp" #include "settingsdialog.hpp" #include "gophermaprenderer.hpp" #include "geminirenderer.hpp" #include "plaintextrenderer.hpp" #include "certificateselectiondialog.hpp" #include "geminiclient.hpp" #include "webclient.hpp" #include "gopherclient.hpp" #include "fingerclient.hpp" #include "abouthandler.hpp" #include "filehandler.hpp" #include "ioutil.hpp" #include "kristall.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include BrowserTab::BrowserTab(MainWindow *mainWindow) : QWidget(nullptr), ui(new Ui::BrowserTab), mainWindow(mainWindow), current_handler(nullptr), outline(), graphics_scene() { ui->setupUi(this); addProtocolHandler(); addProtocolHandler(); addProtocolHandler(); addProtocolHandler(); addProtocolHandler(); addProtocolHandler(); this->updateUI(); this->ui->media_browser->setVisible(false); this->ui->graphics_browser->setVisible(false); this->ui->text_browser->setVisible(true); this->ui->text_browser->setContextMenuPolicy(Qt::CustomContextMenu); } BrowserTab::~BrowserTab() { delete ui; } void BrowserTab::navigateTo(const QUrl &url, PushToHistory mode) { if (mainWindow->protocols.isSchemeSupported(url.scheme()) != ProtocolSetup::Enabled) { QMessageBox::warning(this, "Kristall", "URI scheme not supported or disabled: " + url.scheme()); return; } if ((this->current_handler != nullptr) and not this->current_handler->cancelRequest()) { QMessageBox::warning(this, "Kristall", "Failed to cancel running gemini request!"); return; } this->current_handler = nullptr; for(auto & ptr : this->protocol_handlers) { if(ptr->supportsScheme(url.scheme())) { this->current_handler = ptr.get(); break; } } assert((this->current_handler != nullptr) and "If this error happens, someone forgot to add a new protocol handler class in the constructor. Shame on the programmer!"); if(this->current_identitiy.isValid()) { if(not this->current_handler->enableClientCertificate(this->current_identitiy)) { auto answer = QMessageBox::question( this, "Kristall", QString("You requested a %1-URL with a client certificate, but these are not supported for this scheme. Continue?").arg(url.scheme()) ); if(answer != QMessageBox::Yes) return; this->current_handler->disableClientCertificate(); } } else { this->current_handler->disableClientCertificate(); } this->timer.start(); this->current_location = url; this->ui->url_bar->setText(url.toString(QUrl::FormattingOptions(QUrl::FullyEncoded))); this->redirection_count = 0; this->successfully_loaded = false; if(not this->current_handler->startRequest(url)) { QMessageBox::critical(this, "Kristall", QString("Failed to execute request to %1").arg(url.toString())); return; } if(mode == PushImmediate) { pushToHistory(url); } this->updateUI(); } void BrowserTab::navigateBack(QModelIndex history_index) { auto url = history.get(history_index); if (url.isValid()) { current_history_index = history_index; navigateTo(url, DontPush); } } void BrowserTab::navOneBackback() { navigateBack(history.oneBackward(current_history_index)); } void BrowserTab::navOneForward() { navigateBack(history.oneForward(current_history_index)); } void BrowserTab::scrollToAnchor(QString const &anchor) { qDebug() << "scroll to anchor" << anchor; this->ui->text_browser->scrollToAnchor(anchor); } void BrowserTab::reloadPage() { if (current_location.isValid()) this->navigateTo(this->current_location, DontPush); } void BrowserTab::toggleIsFavourite() { toggleIsFavourite(not this->ui->fav_button->isChecked()); } void BrowserTab::toggleIsFavourite(bool isFavourite) { if (isFavourite) { global_favourites.add(this->current_location); } else { global_favourites.remove(this->current_location); } this->updateUI(); } void BrowserTab::focusUrlBar() { this->ui->url_bar->setFocus(Qt::ShortcutFocusReason); this->ui->url_bar->selectAll(); } void BrowserTab::on_url_bar_returnPressed() { QUrl url{this->ui->url_bar->text()}; if (url.scheme().isEmpty()) { url = QUrl{"gemini://" + this->ui->url_bar->text()}; } this->navigateTo(url, PushImmediate); } void BrowserTab::on_refresh_button_clicked() { reloadPage(); } void BrowserTab::on_networkError(ProtocolHandler::NetworkError error_code, const QString &reason) { QString file_name; switch(error_code) { case ProtocolHandler::UnknownError: file_name = "UnknownError.gemini"; break; case ProtocolHandler::ProtocolViolation: file_name = "ProtocolViolation.gemini"; break; case ProtocolHandler::HostNotFound: file_name = "HostNotFound.gemini"; break; case ProtocolHandler::ConnectionRefused: file_name = "ConnectionRefused.gemini"; break; case ProtocolHandler::ResourceNotFound: file_name = "ResourceNotFound.gemini"; break; case ProtocolHandler::BadRequest: file_name = "BadRequest.gemini"; break; case ProtocolHandler::ProxyRequest: file_name = "ProxyRequest.gemini"; break; case ProtocolHandler::InternalServerError: file_name = "InternalServerError.gemini"; break; case ProtocolHandler::InvalidClientCertificate: file_name = "InvalidClientCertificate.gemini"; break; case ProtocolHandler::UntrustedHost: file_name = "UntrustedHost.gemini"; break; case ProtocolHandler::MistrustedHost: file_name = "MistrustedHost.gemini"; break; case ProtocolHandler::Unauthorized: file_name = "Unauthorized.gemini"; break; case ProtocolHandler::TlsFailure: file_name = "TlsFailure.gemini"; break; case ProtocolHandler::Timeout: file_name = "Timeout.gemini"; break; } file_name = ":/error_page/" + file_name; QFile file_src { file_name }; if(not file_src.open(QFile::ReadOnly)) { assert(false); } auto contents = QString::fromUtf8(file_src.readAll()).arg(reason).toUtf8(); this->on_requestComplete( contents, "text/gemini"); this->updateUI(); } void BrowserTab::on_certificateRequired(const QString &reason) { if (not trySetClientCertificate(reason)) { setErrorMessage(QString("The page requested a authorized client certificate, but none was provided.\r\nOriginal query was: %1").arg(reason)); } else { this->navigateTo(this->current_location, DontPush); } this->updateUI(); } void BrowserTab::on_requestComplete(const QByteArray &data, const QString &mime) { qDebug() << "Loaded" << data.length() << "bytes of type" << mime; this->current_mime = mime; this->current_buffer = data; this->graphics_scene.clear(); this->ui->text_browser->setText(""); ui->text_browser->setStyleSheet(""); enum DocumentType { Text, Image, Media }; DocumentType doc_type = Text; std::unique_ptr document; this->outline.clear(); auto doc_style = mainWindow->current_style.derive(this->current_location); this->ui->text_browser->setStyleSheet(QString("QTextBrowser { background-color: %1; }").arg(doc_style.background_color.name())); bool plaintext_only = (global_settings.value("text_display").toString() == "plain"); if (not plaintext_only and mime.startsWith("text/gemini")) { document = GeminiRenderer::render( data, this->current_location, doc_style, this->outline); } else if (not plaintext_only and mime.startsWith("text/gophermap")) { document = GophermapRenderer::render( data, this->current_location, doc_style); } else if (not plaintext_only and mime.startsWith("text/finger")) { document = PlainTextRenderer::render(data, doc_style); } else if (not plaintext_only and mime.startsWith("text/html")) { document = std::make_unique(); document->setDefaultFont(doc_style.standard_font); document->setDefaultStyleSheet(doc_style.toStyleSheet()); document->setDocumentMargin(doc_style.margin); document->setHtml(QString::fromUtf8(data)); } #if defined(QT_FEATURE_textmarkdownreader) else if (not plaintext_only and mime.startsWith("text/markdown")) { document = std::make_unique(); document->setDefaultFont(doc_style.standard_font); document->setDefaultStyleSheet(doc_style.toStyleSheet()); document->setDocumentMargin(doc_style.margin); document->setMarkdown(QString::fromUtf8(data)); } #endif else if (mime.startsWith("text/")) { document = PlainTextRenderer::render(data, doc_style); } else if (mime.startsWith("image/")) { doc_type = Image; QBuffer buffer; buffer.setData(data); QImageReader reader{&buffer}; reader.setAutoTransform(true); reader.setAutoDetectImageFormat(true); QImage img; if (reader.read(&img)) { auto pixmap = QPixmap::fromImage(img); this->graphics_scene.addPixmap(pixmap); this->graphics_scene.setSceneRect(pixmap.rect()); } else { this->graphics_scene.addText(QString("Failed to load picture:\r\n%1").arg(reader.errorString())); } this->ui->graphics_browser->setScene(&graphics_scene); auto *invoker = new QObject(); connect(invoker, &QObject::destroyed, [this]() { this->ui->graphics_browser->fitInView(graphics_scene.sceneRect(), Qt::KeepAspectRatio); }); invoker->deleteLater(); this->ui->graphics_browser->fitInView(graphics_scene.sceneRect(), Qt::KeepAspectRatio); } else if (mime.startsWith("video/") or mime.startsWith("audio/")) { doc_type = Media; this->ui->media_browser->setMedia(data, this->current_location, mime); } else { document = std::make_unique(); document->setDefaultFont(doc_style.standard_font); document->setDefaultStyleSheet(doc_style.toStyleSheet()); document->setPlainText(QString(R"md(You accessed an unsupported media type! Use the *File* menu to save the file to your local disk or navigate somewhere else. I cannot display this for you. ☹ Info: MIME Type: %1 File Size: %2 )md") .arg(mime) .arg(IoUtil::size_human(data.size()))); } assert((document != nullptr) == (doc_type == Text)); this->ui->text_browser->setVisible(doc_type == Text); this->ui->graphics_browser->setVisible(doc_type == Image); this->ui->media_browser->setVisible(doc_type == Media); this->ui->text_browser->setDocument(document.get()); this->current_document = std::move(document); emit this->locationChanged(this->current_location); QString title = this->current_location.toString(); emit this->titleChanged(title); emit this->fileLoaded(data.size(), mime, this->timer.elapsed()); this->successfully_loaded = true; this->updateUI(); } void BrowserTab::on_inputRequired(const QString &query) { QInputDialog dialog{this}; dialog.setInputMode(QInputDialog::TextInput); dialog.setLabelText(query); if (dialog.exec() != QDialog::Accepted) { setErrorMessage(QString("Site requires input:\n%1").arg(query)); return; } QUrl new_location = current_location; new_location.setQuery(dialog.textValue()); this->navigateTo(new_location, DontPush); } void BrowserTab::on_redirected(const QUrl &uri, bool is_permanent) { Q_UNUSED(is_permanent); // TODO: Make this a setting if (redirection_count >= 5) { setErrorMessage("Too many redirections!"); return; } else { // TODO: Implement cross-protocol redirections // TODO: Implement cross-host redirection if (this->current_handler->startRequest(uri)) { redirection_count += 1; this->current_location = uri; this->ui->url_bar->setText(uri.toString()); } } } void BrowserTab::on_linkHovered(const QString &url) { this->mainWindow->setUrlPreview(QUrl(url)); } void BrowserTab::setErrorMessage(const QString &msg) { this->on_requestComplete( QString("An error happened:\r\n%0").arg(msg).toUtf8(), "text/plain charset=utf-8"); this->updateUI(); } void BrowserTab::pushToHistory(const QUrl &url) { this->current_history_index = this->history.pushUrl(this->current_history_index, url); this->updateUI(); } void BrowserTab::on_fav_button_clicked() { toggleIsFavourite(this->ui->fav_button->isChecked()); } #include void BrowserTab::on_text_browser_anchorClicked(const QUrl &url) { qDebug() << url; QUrl real_url = url; if (real_url.isRelative()) real_url = this->current_location.resolved(url); auto support = mainWindow->protocols.isSchemeSupported(real_url.scheme()); if (support == ProtocolSetup::Enabled) { this->navigateTo(real_url, PushImmediate); } else { bool use_os_proxy = global_settings.value("use_os_scheme_handler").toBool(); if (use_os_proxy) { if (not QDesktopServices::openUrl(url)) { QMessageBox::warning(this, "Kristall", QString("Failed to start system URL handler for\r\n%1").arg(real_url.toString())); } } else if (support == ProtocolSetup::Disabled) { QMessageBox::warning(this, "Kristall", QString("The requested url uses a scheme that has been disabled in the settings:\r\n%1").arg(real_url.toString())); } else { QMessageBox::warning(this, "Kristall", QString("The requested url cannot be processed by Kristall:\r\n%1").arg(real_url.toString())); } } } void BrowserTab::on_text_browser_highlighted(const QUrl &url) { if (url.isValid()) { QUrl real_url = url; if (real_url.isRelative()) real_url = this->current_location.resolved(url); this->mainWindow->setUrlPreview(real_url); } else { this->mainWindow->setUrlPreview(QUrl{}); } } void BrowserTab::on_stop_button_clicked() { if(this->current_handler != nullptr) { this->current_handler->cancelRequest(); } this->updateUI(); } void BrowserTab::on_requestProgress(qint64 transferred) { emit this->fileLoaded(transferred, "Loading...", timer.elapsed()); } void BrowserTab::on_back_button_clicked() { navOneBackback(); } void BrowserTab::on_forward_button_clicked() { navOneForward(); } void BrowserTab::updateUI() { this->ui->back_button->setEnabled(history.oneBackward(current_history_index).isValid()); this->ui->forward_button->setEnabled(history.oneForward(current_history_index).isValid()); this->ui->refresh_button->setVisible(this->successfully_loaded); this->ui->stop_button->setVisible(not this->successfully_loaded); this->ui->fav_button->setEnabled(this->successfully_loaded); this->ui->fav_button->setChecked(global_favourites.contains(this->current_location)); } bool BrowserTab::trySetClientCertificate(const QString &query) { CertificateSelectionDialog dialog{this}; dialog.setServerQuery(query); if (dialog.exec() != QDialog::Accepted) { for(auto & handler : this->protocol_handlers) { handler->disableClientCertificate(); } this->ui->enable_client_cert_button->setChecked(false); return false; } this->current_identitiy = dialog.identity(); if (not current_identitiy.isValid()) { QMessageBox::warning(this, "Kristall", "Failed to generate temporary crypto-identitiy"); this->ui->enable_client_cert_button->setChecked(false); return false; } this->ui->enable_client_cert_button->setChecked(true); return true; } void BrowserTab::resetClientCertificate() { if (this->current_identitiy.isValid() and not this->current_identitiy.is_persistent) { auto respo = QMessageBox::question(this, "Kristall", "You currently have a transient session active!\r\nIf you disable the session, you will not be able to restore it. Continue?"); if (respo != QMessageBox::Yes) { this->ui->enable_client_cert_button->setChecked(true); return; } } this->current_identitiy = CryptoIdentity(); for(auto & handler : this->protocol_handlers) { handler->disableClientCertificate(); } this->ui->enable_client_cert_button->setChecked(false); } void BrowserTab::addProtocolHandler(std::unique_ptr &&handler) { connect(handler.get(), &ProtocolHandler::requestProgress, this, &BrowserTab::on_requestProgress); connect(handler.get(), &ProtocolHandler::requestComplete, this, &BrowserTab::on_requestComplete); connect(handler.get(), &ProtocolHandler::redirected, this, &BrowserTab::on_redirected); connect(handler.get(), &ProtocolHandler::inputRequired, this, &BrowserTab::on_inputRequired); connect(handler.get(), &ProtocolHandler::networkError, this, &BrowserTab::on_networkError); connect(handler.get(), &ProtocolHandler::certificateRequired, this, &BrowserTab::on_certificateRequired); this->protocol_handlers.emplace_back(std::move(handler)); } void BrowserTab::on_text_browser_customContextMenuRequested(const QPoint &pos) { QMenu menu; QString anchor = ui->text_browser->anchorAt(pos); if (not anchor.isEmpty()) { QUrl real_url{anchor}; if (real_url.isRelative()) real_url = this->current_location.resolved(real_url); connect(menu.addAction("Follow link…"), &QAction::triggered, [this, real_url]() { this->navigateTo(real_url, PushImmediate); }); connect(menu.addAction("Open in new tab…"), &QAction::triggered, [this, real_url]() { mainWindow->addNewTab(false, real_url); }); connect(menu.addAction("Copy link"), &QAction::triggered, [real_url]() { global_clipboard->setText(real_url.toString(QUrl::FullyEncoded)); }); menu.addSeparator(); } connect(menu.addAction("Select all"), &QAction::triggered, [this]() { this->ui->text_browser->selectAll(); }); menu.addSeparator(); QAction * copy = menu.addAction("Copy to clipboard"); copy->setShortcut(QKeySequence("Ctrl-C")); connect(copy, &QAction::triggered, [this]() { this->ui->text_browser->copy(); }); menu.exec(ui->text_browser->mapToGlobal(pos)); } void BrowserTab::on_enable_client_cert_button_clicked(bool checked) { if (checked) { trySetClientCertificate(QString{}); } else { resetClientCertificate(); } }