#include "page.hpp" #include "page/content.hpp" #include "page/navigation.hpp" using namespace app::browser::main::tab; Page::Page( sqlite3 * db, const Glib::RefPtr & ACTION__HISTORY_BACK, const Glib::RefPtr & ACTION__HISTORY_FORWARD, const Glib::RefPtr & ACTION__RELOAD, const Glib::RefPtr & ACTION__UPDATE ) { // Init meta title = _("New page"); mime = MIME::UNDEFINED; progress_fraction = 0; // Init database Database::Session::init( this->db = db ); // Init shared actions action__update = ACTION__UPDATE; // Init additional local action group (for clickable content) const auto ACTION_GROUP__PAGE = Gio::SimpleActionGroup::create(); const auto ACTION__OPEN_LINK_VARIANT = ACTION_GROUP__PAGE->add_action_with_parameter( "open_link_variant", Glib::VARIANT_TYPE_STRING, [this](const Glib::VariantBase & PARAMETER) { if (PARAMETER.is_of_type(Glib::VARIANT_TYPE_STRING)) { pageNavigation->set_request_text( Glib::VariantBase::cast_dynamic>( PARAMETER ).get() ); navigation_reload( true ); } } ); // Init widget insert_action_group( "page", ACTION_GROUP__PAGE ); set_orientation( Gtk::Orientation::VERTICAL ); // Init widget components pageNavigation = Gtk::make_managed( this->db, ACTION__HISTORY_BACK, ACTION__HISTORY_FORWARD, ACTION__OPEN_LINK_VARIANT, ACTION__RELOAD, ACTION__UPDATE ); append( * pageNavigation ); pageContent = Gtk::make_managed( ACTION__OPEN_LINK_VARIANT ); append( * pageContent ); // Connect events /* activated twice on tab change @TODO signal_realize().connect( [this] { action__update->activate(); } );*/ } // Actions int Page::session_restore( const sqlite3_int64 & APP_BROWSER_MAIN_TAB__SESSION__ID ) { sqlite3_stmt* statement; // @TODO move to the Database model namespace const int PREPARE_STATUS = sqlite3_prepare_v3( db, Glib::ustring::sprintf( R"SQL( SELECT * FROM `app_browser_main_tab_page__session` WHERE `app_browser_main_tab__session__id` = %d ORDER BY `id` DESC LIMIT 1 )SQL", APP_BROWSER_MAIN_TAB__SESSION__ID ).c_str(), -1, SQLITE_PREPARE_NORMALIZE, &statement, nullptr ); if (PREPARE_STATUS == SQLITE_OK) { // Restore page data from latest database record while (sqlite3_step(statement) == SQLITE_ROW) { // Restore page data switch ( sqlite3_column_int( statement, Database::Session::MIME ) ) { case 0: mime = MIME::TEXT_PLAIN; break; case 1: mime = MIME::TEXT_GEMINI; break; case 2: mime = MIME::UNDEFINED; break; default: throw _("Undefined MIME type"); } // @TODO title = reinterpret_cast( sqlite3_column_text( statement, Database::Session::TITLE ) ); description = reinterpret_cast( sqlite3_column_text( statement, Database::Session::DESCRIPTION ) ); // Restore children components pageNavigation->session_restore( sqlite3_column_int64( statement, Database::Session::ID ) ); } } return sqlite3_finalize( statement ); } void Page::session_save( const sqlite3_int64 & APP_BROWSER_MAIN_TAB__SESSION__ID ) { // Delegate save action to child components pageNavigation->session_save( Database::Session::add( db, APP_BROWSER_MAIN_TAB__SESSION__ID, mime, title, description ) ); } void Page::update() { // Update children components pageNavigation->update( progress_fraction ); } void Page::navigation_reload( const bool & ADD_HISTORY ) { // Close previous socket connection (on active) Socket::Connection::close( socket__connection ); // Update navigation history? if (ADD_HISTORY) { // Skip same Glib::ustring request; pageNavigation->try_history_current( request ); if (request != pageNavigation->get_request_text()) { pageNavigation->history_add( pageNavigation->get_request_text(), true ); } } // Parse request string uri = g_uri_parse( pageNavigation->get_request_text().c_str(), G_URI_FLAGS_NONE, NULL // @TODO GError * ); // On parse fail if (uri == NULL) { // Request contain host substring if (Glib::Regex::match_simple( R"regex(^[^\/\s]+\.[\w]{2,})regex", pageNavigation->get_request_text().c_str() )) { // Append default scheme pageNavigation->set_request_text( Glib::ustring::sprintf( "gemini://%s", pageNavigation->get_request_text() ) ); } // Plain text given, build search request to default provider else { pageNavigation->set_request_text( Glib::ustring::sprintf( "gemini://tlgs.one/search?%s", // @TODO settings g_uri_escape_string( pageNavigation->get_request_text().c_str(), NULL, true ) ).c_str() ); } // Redirect @TODO limit attempts navigation_reload( false ); } // Reset page meta data mime = MIME::UNDEFINED; title = _("Update"); description = Glib::ustring::sprintf( _("Begin update for %s.."), pageNavigation->get_request_text() ); progress_fraction = 0; action__update->activate(); // Route to protocol driver by scheme if (Glib::ustring("file").compare(g_uri_get_scheme(uri)) == 0) { // @TODO } else if (Glib::ustring("gemini").compare(g_uri_get_scheme(uri)) == 0) { // Create new socket connection socket__client = Page::Socket::Client::Gemini::create(); socket__client->connect_to_uri_async( g_uri_to_string( uri ), Socket::Client::Gemini::DEFAULT_PORT, [this](const Glib::RefPtr & RESULT) { // Update title = _("Connect"); description = Glib::ustring::sprintf( _("Connecting to %s.."), g_uri_get_host( uri ) ); progress_fraction = .25; action__update->activate(); try { socket__connection = socket__client->connect_to_uri_finish( RESULT ); } catch (const Glib::Error & EXCEPTION) { // Update title = _("Oops"); description = EXCEPTION.what(); progress_fraction = 1; action__update->activate(); } // Connection established, begin request if (Socket::Connection::is_active(socket__connection)) // @TODO { // Build gemini protocol request const auto REQUEST = Socket::Client::Gemini::Request::create_from_uri( uri ); socket__connection->get_output_stream()->write_async( REQUEST.data(), REQUEST.size(), [this](const Glib::RefPtr&) { // Update title = _("Request"); description = Glib::ustring::sprintf( _("Begin request to %s.."), g_uri_get_host( uri ) ); progress_fraction = .5; action__update->activate(); // Response // if (Socket::Connection::is_active(socket__connection)) // @TODO socket__connection->get_input_stream()->read_all_async( // | read_async @TODO buffer, sizeof( buffer ) - 1, // @TODO [this](const Glib::RefPtr&) { // Update title = _("Reading"); description = Glib::ustring::sprintf( _("Reading response from %s.."), g_uri_get_host( uri ) ); progress_fraction = .75; action__update->activate(); // Parse meta Socket::Client::Gemini::Response::Status status; // @TODO make page global? Socket::Client::Gemini::Response::Match::meta( buffer, status, mime ); // MIME type not detected if (mime == MIME::UNDEFINED) { // Try detect by file extension if (Glib::str_has_suffix(g_uri_get_path(uri), ".gmi")) { mime = MIME::TEXT_GEMINI; } } // Route by status code switch (status) { case Socket::Client::Gemini::Response::Status::SUCCESS: // Route by MIME switch (mime) { case MIME::TEXT_GEMINI: progress_fraction = 1; // Set content driver pageContent->update( page::Content::TEXT_GEMINI, buffer, uri ); // Update title on detected by document provider if (!pageContent->get_title().empty()) { title = pageContent->get_title(); } action__update->activate(); break; default: // Update title = _("Oops"); description = _("MIME type not supported"); progress_fraction = 1; action__update->activate(); } // Update title = _("Done"); // @TODO page title description = g_uri_get_host( uri ); break; // @TODO other statuses.. default: // Update title = _("Oops"); description = _("Response code not supported"); progress_fraction = 1; action__update->activate(); } // Finalize request Socket::Connection::close( socket__connection ); } ); // read_all_async } ); // write_async } } // connect_to_uri_async ); } else { throw _("Exception"); // @TODO } } void Page::navigation_history_back() { Glib::ustring request; if (pageNavigation->try_history_back(request, true)) { pageNavigation->set_request_text( request ); navigation_reload( false ); } } void Page::navigation_history_forward() { Glib::ustring request; if (pageNavigation->try_history_forward(request, true)) { pageNavigation->set_request_text( request ); navigation_reload( false ); } } // Getters Page::MIME Page::get_mime() { return mime; } Glib::ustring Page::get_title() { return title; } Glib::ustring Page::get_description() { return description; } // Database model int Page::Database::Session::init( sqlite3 * db ) { char * error; return sqlite3_exec( db, R"SQL( CREATE TABLE IF NOT EXISTS `app_browser_main_tab_page__session` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `app_browser_main_tab__session__id` INTEGER NOT NULL, `time` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, `mime` INTEGER NOT NULL, `title` VARCHAR(1024) NOT NULL, `description` VARCHAR(1024) NOT NULL ) )SQL", nullptr, nullptr, &error ); } int Page::Database::Session::clean( sqlite3 * db, const sqlite3_int64 & APP_BROWSER_MAIN_TAB__SESSION__ID ) { char * error; // @TODO sqlite3_stmt * statement; const int PREPARE_STATUS = sqlite3_prepare_v3( db, Glib::ustring::sprintf( R"SQL( SELECT * FROM `app_browser_main_tab_page__session` WHERE `app_browser_main_tab__session__id` = %d )SQL", APP_BROWSER_MAIN_TAB__SESSION__ID ).c_str(), -1, SQLITE_PREPARE_NORMALIZE, &statement, nullptr ); if (PREPARE_STATUS == SQLITE_OK) { while (sqlite3_step(statement) == SQLITE_ROW) { const sqlite3_int64 APP_BROWSER_MAIN_TAB_PAGE__SESSION__ID = sqlite3_column_int64( statement, Database::Session::ID ); // Delete record const int EXEC_STATUS = sqlite3_exec( db, Glib::ustring::sprintf( R"SQL( DELETE FROM `app_browser_main_tab_page__session` WHERE `id` = %d )SQL", APP_BROWSER_MAIN_TAB_PAGE__SESSION__ID ).c_str(), nullptr, nullptr, &error ); // Delegate children dependencies cleanup if (EXEC_STATUS == SQLITE_OK) { page::Navigation::Database::Session::clean( db, APP_BROWSER_MAIN_TAB_PAGE__SESSION__ID ); } } } return sqlite3_finalize( statement ); } sqlite3_int64 Page::Database::Session::add( sqlite3 * db, const sqlite3_int64 & APP_BROWSER_MAIN_TAB__SESSION__ID, const Page::MIME & MIME, const Glib::ustring & TITLE, const Glib::ustring & DESCRIPTION ) { char * error; // @TODO sqlite3_exec( db, Glib::ustring::sprintf( R"SQL( INSERT INTO `app_browser_main_tab_page__session` ( `app_browser_main_tab__session__id`, `mime`, `title`, `description` ) VALUES ( '%d', '%d', '%s', '%s' ) )SQL", APP_BROWSER_MAIN_TAB__SESSION__ID, MIME, TITLE, DESCRIPTION ).c_str(), nullptr, nullptr, &error ); return sqlite3_last_insert_rowid( db ); } // Socket tools /* * Private helper to build socket client connections * * Includes common preset used for all protocols in Page::Socket::Client class */ Glib::RefPtr Page::Socket::Client::create( const int & TIMEOUT ) { const auto CLIENT = Gio::SocketClient::create(); CLIENT->set_timeout( TIMEOUT ); return CLIENT; } /* * Create and return socket client for Gemini protocol * * https://geminiprotocol.net/docs/protocol-specification.gmi */ Glib::RefPtr Page::Socket::Client::Gemini::create() { const auto GEMINI_CLIENT = Socket::Client::create(); GEMINI_CLIENT->set_tls( true ); GEMINI_CLIENT->set_tls_validation_flags( Gio::TlsCertificateFlags::NO_FLAGS ); GEMINI_CLIENT->set_protocol( Gio::Socket::Protocol::TCP ); return GEMINI_CLIENT; } /* * Build request string for Gemini protocol from GUri pointer */ Glib::ustring Page::Socket::Client::Gemini::Request::create_from_uri( GUri * uri ) { return Glib::ustring::sprintf( "%s\r\n", g_uri_to_string( uri ) ); } /* * Parse meta data from response buffer */ bool Page::Socket::Client::Gemini::Response::Match::meta( const Glib::ustring & RESPONSE, Status & status, MIME & mime ) { // Parse response string const auto MATCH = Glib::Regex::split_simple( R"regex(^(\d+)?\s([\w]+\/[\w]+)?)regex", RESPONSE ); // Detect status code @TODO if (Glib::ustring("20").compare(MATCH[1]) == 0) { status = Status::SUCCESS; } else { status = Status::UNDEFINED; } // Detect MIME @TODO if (Glib::ustring("text/gemini").compare(MATCH[2]) == 0) { mime = MIME::TEXT_GEMINI; } else if (Glib::ustring("text/plain").compare(MATCH[2]) == 0) { mime = MIME::TEXT_PLAIN; } else { mime = MIME::UNDEFINED; } return true; // @TODO } /* * Check socket connection active according to page class implementation */ bool Page::Socket::Connection::is_active( const Glib::RefPtr & CONNECTION ) { return CONNECTION != nullptr && CONNECTION->is_connected(); } /* * Close socket, make &connection nullptr * * return true on success or false if connection not active */ bool Page::Socket::Connection::close( Glib::RefPtr & connection ) { if (is_active(connection)) { if (connection->close()) { connection = nullptr; return true; } } return false; }