diff options
author | Adriaan de Groot <groot@kde.org> | 2019-05-10 15:07:20 -0400 |
---|---|---|
committer | Adriaan de Groot <groot@kde.org> | 2019-05-10 15:07:20 -0400 |
commit | c3754126d0c200b26c1b55d7afa95c16e2cab265 (patch) | |
tree | 218215f79ec225e5f229e64a6bf429589a77e3e6 | |
parent | d194670625d09b5f0e62b9b8202d2bf06a5a568b (diff) | |
parent | f18f9dcd14c043b95abfbd4e4319611b66aa9223 (diff) |
Merge branch 'shuffle-geoip'
31 files changed, 874 insertions, 380 deletions
@@ -35,6 +35,11 @@ This release contains contributions from (alphabetically by first name): - *finished* has a new mechanism for configuring the behavior of the *restart now* button. The old-style boolean configuration is still supported but generates a warning. #1138 + - *locale* module GeoIP configuration has a new preferred format. + See `locale.conf` for details. The old configuration is still + supported but will be phased out before 3.3.0 -- in particular, + support for "legacy" format will be removed, since that was a + crutch for the disappearance of one GeoIP provider in 2018. - *oemid* is a new module for configuring OEM phase-0 (image pre-mastering, or pre-deployment) things. It has limited functionality at the moment, writing only a single batch-identifier file. #943 diff --git a/src/libcalamares/CMakeLists.txt b/src/libcalamares/CMakeLists.txt index c65fb6d39..959b0a9db 100644 --- a/src/libcalamares/CMakeLists.txt +++ b/src/libcalamares/CMakeLists.txt @@ -13,6 +13,9 @@ configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/CalamaresConfig.h.in configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/../calamares/CalamaresVersion.h.in ${CMAKE_CURRENT_BINARY_DIR}/CalamaresVersion.h ) +set( OPTIONAL_PRIVATE_LIBRARIES "" ) +set( OPTIONAL_PUBLIC_LIBRARIES "" ) + set( libSources CppJob.cpp GlobalStorage.cpp @@ -21,10 +24,17 @@ set( libSources JobQueue.cpp ProcessJob.cpp Settings.cpp - + + # GeoIP services + geoip/Interface.cpp + geoip/GeoIPJSON.cpp + geoip/Handler.cpp + # Locale-data service + locale/Label.cpp + locale/LabelModel.cpp locale/Lookup.cpp - + # Partition service partition/PartitionSize.cpp @@ -32,7 +42,6 @@ set( libSources utils/CalamaresUtilsSystem.cpp utils/CommandList.cpp utils/Dirs.cpp - utils/LocaleLabel.cpp utils/Logger.cpp utils/PluginFactory.cpp utils/Retranslator.cpp @@ -54,9 +63,11 @@ include_directories( ${YAMLCPP_INCLUDE_DIR} ) +### OPTIONAL Python support +# +# if( WITH_PYTHON ) - set( libSources - ${libSources} + list( APPEND libSources PythonHelper.cpp PythonJob.cpp PythonJobApi.cpp @@ -71,13 +82,24 @@ if( WITH_PYTHON ) include_directories(${Boost_INCLUDE_DIRS}) link_directories(${Boost_LIBRARY_DIRS}) - set( OPTIONAL_PRIVATE_LIBRARIES - ${OPTIONAL_PRIVATE_LIBRARIES} + list( APPEND OPTIONAL_PRIVATE_LIBRARIES ${PYTHON_LIBRARIES} ${Boost_LIBRARIES} ) endif() +### OPTIONAL GeoIP XML support +# +# +find_package(Qt5 COMPONENTS Xml) +if( Qt5Xml_FOUND ) + list( APPEND libSources geoip/GeoIPXML.cpp ) + list( APPEND OPTIONAL_PUBLIC_LIBRARIES Qt5::Network Qt5::Xml ) +endif() + +### LIBRARY +# +# add_library( calamares SHARED ${libSources} ${kdsagSources} ) set_target_properties( calamares PROPERTIES @@ -92,6 +114,7 @@ target_link_libraries( calamares LINK_PUBLIC ${YAMLCPP_LIBRARY} Qt5::Core + ${OPTIONAL_PUBLIC_LIBRARIES} ) install( TARGETS calamares @@ -101,19 +124,6 @@ install( TARGETS calamares ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} ) -if ( ECM_FOUND AND BUILD_TESTING ) - ecm_add_test( - Tests.cpp - TEST_NAME - libcalamarestest - LINK_LIBRARIES - calamares - Qt5::Core - Qt5::Test - ) - calamares_automoc( libcalamarestest ) -endif() - # Make symlink lib/calamares/libcalamares.so to lib/libcalamares.so.VERSION so # lib/calamares can be used as module path for the Python interpreter. install( CODE " @@ -130,3 +140,37 @@ install( FILES ${CMAKE_CURRENT_BINARY_DIR}/CalamaresConfig.h DESTINATION include install( FILES ${rootHeaders} DESTINATION include/libcalamares ) install( FILES ${kdsingleapplicationguardHeaders} DESTINATION include/libcalamares/kdsingleapplicationguard ) install( FILES ${utilsHeaders} DESTINATION include/libcalamares/utils ) + +### TESTING +# +# +if ( ECM_FOUND AND BUILD_TESTING ) + ecm_add_test( + Tests.cpp + TEST_NAME + libcalamarestest + LINK_LIBRARIES + calamares + Qt5::Core + Qt5::Test + ) + calamares_automoc( libcalamarestest ) + + ecm_add_test( + geoip/GeoIPTests.cpp + ${geoip_src} + TEST_NAME + geoiptest + LINK_LIBRARIES + calamares + Qt5::Test + ${YAMLCPP_LIBRARY} + ) + calamares_automoc( geoiptest ) +endif() + +if( BUILD_TESTING ) + add_executable( test_geoip geoip/test_geoip.cpp ${geoip_src} ) + target_link_libraries( test_geoip calamares Qt5::Network ${YAMLCPP_LIBRARY} ) + calamares_automoc( test_geoip ) +endif() diff --git a/src/modules/locale/GeoIPJSON.cpp b/src/libcalamares/geoip/GeoIPJSON.cpp index 8e5cc2e5c..61b9fd8d6 100644 --- a/src/modules/locale/GeoIPJSON.cpp +++ b/src/libcalamares/geoip/GeoIPJSON.cpp @@ -25,11 +25,20 @@ #include <QByteArray> +namespace CalamaresUtils::GeoIP +{ + GeoIPJSON::GeoIPJSON(const QString& attribute) - : GeoIP( attribute.isEmpty() ? QStringLiteral( "time_zone" ) : attribute ) + : Interface( attribute.isEmpty() ? QStringLiteral( "time_zone" ) : attribute ) { } +/** @brief Indexes into a map @m by selectors @p l + * + * Each element of @p l is an index into map @m or a sub-map thereof, + * so that "foo.bar.baz" looks up "baz" in the sub-map "bar" of sub-map + * "foo" of @p m, like a regular JSON lookup would. + */ static QString selectMap( const QVariantMap& m, const QStringList& l, int index) { @@ -48,8 +57,8 @@ selectMap( const QVariantMap& m, const QStringList& l, int index) } } -GeoIP::RegionZonePair -GeoIPJSON::processReply( const QByteArray& data ) +QString +GeoIPJSON::rawReply( const QByteArray& data ) { try { @@ -60,7 +69,7 @@ GeoIPJSON::processReply( const QByteArray& data ) var.isValid() && var.type() == QVariant::Map ) { - return splitTZString( selectMap( var.toMap(), m_element.split('.'), 0 ) ); + return selectMap( var.toMap(), m_element.split('.'), 0 ); } else cWarning() << "Invalid YAML data for GeoIPJSON"; @@ -70,5 +79,15 @@ GeoIPJSON::processReply( const QByteArray& data ) CalamaresUtils::explainYamlException( e, data, "GeoIP data"); } - return qMakePair( QString(), QString() ); + return QString(); +} + +GeoIP::RegionZonePair +GeoIPJSON::processReply( const QByteArray& data ) +{ + return splitTZString( rawReply( data ) ); } + + + +} // namespace diff --git a/src/modules/locale/GeoIPJSON.h b/src/libcalamares/geoip/GeoIPJSON.h index 3c08f577b..584825d70 100644 --- a/src/modules/locale/GeoIPJSON.h +++ b/src/libcalamares/geoip/GeoIPJSON.h @@ -1,6 +1,6 @@ /* === This file is part of Calamares - <http://github.com/calamares> === * - * Copyright 2018, Adriaan de Groot <groot@kde.org> + * Copyright 2018-2019, Adriaan de Groot <groot@kde.org> * * Calamares is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,19 +16,23 @@ * along with Calamares. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef GEOIPJSON_H -#define GEOIPJSON_H +#ifndef GEOIP_GEOIPJSON_H +#define GEOIP_GEOIPJSON_H -#include "GeoIP.h" +#include "Interface.h" +namespace CalamaresUtils::GeoIP +{ /** @brief GeoIP lookup for services that return JSON. * * This is the original implementation of GeoIP lookup, * (e.g. using the FreeGeoIP.net service), or similar. * * The data is assumed to be in JSON format with a time_zone attribute. + * + * @note This class is an implementation detail. */ -class GeoIPJSON : public GeoIP +class GeoIPJSON : public Interface { public: /** @brief Configure the attribute name which is selected. @@ -38,7 +42,9 @@ public: */ explicit GeoIPJSON( const QString& attribute = QString() ); - virtual RegionZonePair processReply( const QByteArray& ); + virtual RegionZonePair processReply( const QByteArray& ) override; + virtual QString rawReply(const QByteArray & ) override; } ; +} // namespace #endif diff --git a/src/modules/locale/GeoIPTests.cpp b/src/libcalamares/geoip/GeoIPTests.cpp index af114611e..ec7511370 100644 --- a/src/modules/locale/GeoIPTests.cpp +++ b/src/libcalamares/geoip/GeoIPTests.cpp @@ -19,9 +19,10 @@ #include "GeoIPTests.h" #include "GeoIPJSON.h" -#ifdef HAVE_XML +#ifdef QT_XML_LIB #include "GeoIPXML.h" #endif +#include "Handler.h" #include <QNetworkAccessManager> #include <QNetworkReply> @@ -31,6 +32,8 @@ QTEST_GUILESS_MAIN( GeoIPTests ) +using namespace CalamaresUtils::GeoIP; + GeoIPTests::GeoIPTests() { } @@ -118,7 +121,7 @@ static const char xml_data_ubiquity[] = void GeoIPTests::testXML() { -#ifdef HAVE_XML +#ifdef QT_XML_LIB GeoIPXML handler; auto tz = handler.processReply( xml_data_ubiquity ); @@ -133,7 +136,7 @@ GeoIPTests::testXML2() static const char data[] = "<Response><TimeZone>America/North Dakota/Beulah</TimeZone></Response>"; // With a space! -#ifdef HAVE_XML +#ifdef QT_XML_LIB GeoIPXML handler; auto tz = handler.processReply( data ); @@ -145,7 +148,7 @@ GeoIPTests::testXML2() void GeoIPTests::testXMLalt() { -#ifdef HAVE_XML +#ifdef QT_XML_LIB GeoIPXML handler( "ZT" ); auto tz = handler.processReply( "<A><B/><C><ZT>Moon/Dark_side</ZT></C></A>" ); @@ -157,7 +160,7 @@ void GeoIPTests::testXMLalt() void GeoIPTests::testXMLbad() { -#ifdef HAVE_XML +#ifdef QT_XML_LIB GeoIPXML handler; auto tz = handler.processReply( "{time_zone: \"Europe/Paris\"}" ); QCOMPARE( tz.first, QString() ); @@ -172,24 +175,25 @@ GeoIPTests::testXMLbad() void GeoIPTests::testSplitTZ() { - auto tz = GeoIP::splitTZString( QStringLiteral("Moon/Dark_side") ); + using namespace CalamaresUtils::GeoIP; + auto tz = splitTZString( QStringLiteral("Moon/Dark_side") ); QCOMPARE( tz.first, QStringLiteral("Moon") ); QCOMPARE( tz.second, QStringLiteral("Dark_side") ); // Some providers return weirdly escaped data - tz = GeoIP::splitTZString( QStringLiteral("America\\/NewYork") ); + tz = splitTZString( QStringLiteral("America\\/NewYork") ); QCOMPARE( tz.first, QStringLiteral("America") ); QCOMPARE( tz.second, QStringLiteral("NewYork") ); // That's not actually the zone name // Check that bogus data fails - tz = GeoIP::splitTZString( QString() ); + tz = splitTZString( QString() ); QCOMPARE( tz.first, QString() ); - tz = GeoIP::splitTZString( QStringLiteral("America.NewYork") ); + tz = splitTZString( QStringLiteral("America.NewYork") ); QCOMPARE( tz.first, QString() ); // Check that three-level is split properly and space is replaced - tz = GeoIP::splitTZString( QStringLiteral("America/North Dakota/Beulah") ); + tz = splitTZString( QStringLiteral("America/North Dakota/Beulah") ); QCOMPARE( tz.first, QStringLiteral("America") ); QCOMPARE( tz.second, QStringLiteral("North_Dakota/Beulah") ); } @@ -216,14 +220,18 @@ synchronous_get( const char* urlstring ) #define CHECK_GET(t, selector, url) \ { \ auto tz = GeoIP##t( selector ).processReply( synchronous_get( url ) ); \ + qDebug() << tz; \ QCOMPARE( default_tz, tz ); \ + auto tz2 = CalamaresUtils::GeoIP::Handler( ""#t, url, selector ).get(); \ + qDebug() << tz2; \ + QCOMPARE( default_tz, tz2 ); \ } void GeoIPTests::testGet() { if ( !QProcessEnvironment::systemEnvironment().contains( QStringLiteral("TEST_HTTP_GET") ) ) { - qDebug() << "Skipping HTTP GET tests"; + qDebug() << "Skipping HTTP GET tests, set TEST_HTTP_GET environment variable to enable"; return; } @@ -241,15 +249,12 @@ void GeoIPTests::testGet() // the TZ data is the same as the default_tz; this is fragile if the // services don't agree on the location of where the test is run. CHECK_GET( JSON, QString(), "https://geoip.kde.org/v1/calamares" ) // Check it's consistent - CHECK_GET( JSON, QString(), "http://freegeoip.net/json/" ) // Original FreeGeoIP service CHECK_GET( JSON, QStringLiteral("timezone"), "https://ipapi.co/json" ) // Different JSON CHECK_GET( JSON, QStringLiteral("timezone"), "http://ip-api.com/json" ) - CHECK_GET( JSON, QStringLiteral("location.time_zone"), "http://geoip.nekudo.com/api/" ) // 2-level JSON - CHECK_GET( JSON, QStringLiteral("Location.TimeZone"), "https://geoip.kde.org/debug" ) // 2-level JSON -#ifdef HAVE_XML +#ifdef QT_XML_LIB CHECK_GET( XML, QString(), "http://geoip.ubuntu.com/lookup" ) // Ubiquity's XML format CHECK_GET( XML, QString(), "https://geoip.kde.org/v1/ubiquity" ) // Temporary KDE service #endif diff --git a/src/modules/locale/GeoIPTests.h b/src/libcalamares/geoip/GeoIPTests.h index a320e3263..a320e3263 100644 --- a/src/modules/locale/GeoIPTests.h +++ b/src/libcalamares/geoip/GeoIPTests.h diff --git a/src/modules/locale/GeoIPXML.cpp b/src/libcalamares/geoip/GeoIPXML.cpp index bd675c2ef..a4b9bb146 100644 --- a/src/modules/locale/GeoIPXML.cpp +++ b/src/libcalamares/geoip/GeoIPXML.cpp @@ -23,38 +23,68 @@ #include <QNetworkReply> #include <QtXml/QDomDocument> +namespace CalamaresUtils::GeoIP +{ + GeoIPXML::GeoIPXML( const QString& element ) - : GeoIP( element.isEmpty() ? QStringLiteral( "TimeZone" ) : element ) + : Interface( element.isEmpty() ? QStringLiteral( "TimeZone" ) : element ) { } -GeoIP::RegionZonePair -GeoIPXML::processReply( const QByteArray& data ) +static QStringList +getElementTexts( const QByteArray& data, const QString& tag ) { + QStringList elements; + QString domError; int errorLine, errorColumn; QDomDocument doc; if ( doc.setContent( data, false, &domError, &errorLine, &errorColumn ) ) { - const auto tzElements = doc.elementsByTagName( m_element ); + const auto tzElements = doc.elementsByTagName( tag ); cDebug() << "GeoIP found" << tzElements.length() << "elements"; for ( int it = 0; it < tzElements.length(); ++it ) { auto e = tzElements.at(it).toElement(); - auto tz = splitTZString( e.text() ); - if ( !tz.first.isEmpty() ) - return tz; + auto e_text = e.text(); + if ( !e_text.isEmpty() ) + elements.append( e_text ); } - - // None of them valid - cWarning() << "GeopIP XML had no recognizable timezone"; - return qMakePair( QString(), QString() ); } else { cWarning() << "GeoIP XML data error:" << domError << "(line" << errorLine << errorColumn << ')'; } - return qMakePair( QString(), QString() ); + if ( elements.count() < 1 ) + cWarning() << "GeopIP XML had no non-empty elements" << tag; + + return elements; } + + +QString +GeoIPXML::rawReply( const QByteArray& data ) +{ + for ( const auto& e : getElementTexts( data, m_element ) ) + if ( !e.isEmpty() ) + return e; + + return QString(); +} + +GeoIP::RegionZonePair +GeoIPXML::processReply( const QByteArray& data ) +{ + for ( const auto& e : getElementTexts( data, m_element ) ) + { + auto tz = splitTZString( e ); + if ( !tz.first.isEmpty() ) + return tz; + } + + return RegionZonePair(); +} + +} // namespace diff --git a/src/modules/locale/GeoIPXML.h b/src/libcalamares/geoip/GeoIPXML.h index bc3f23bec..7dee2ecbe 100644 --- a/src/modules/locale/GeoIPXML.h +++ b/src/libcalamares/geoip/GeoIPXML.h @@ -1,6 +1,6 @@ /* === This file is part of Calamares - <http://github.com/calamares> === * - * Copyright 2018, Adriaan de Groot <groot@kde.org> + * Copyright 2018-2019, Adriaan de Groot <groot@kde.org> * * Calamares is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,19 +16,23 @@ * along with Calamares. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef GEOIPXML_H -#define GEOIPXML_H +#ifndef GEOIP_GEOIPXML_H +#define GEOIP_GEOIPXML_H -#include "GeoIP.h" +#include "Interface.h" +namespace CalamaresUtils::GeoIP +{ /** @brief GeoIP lookup with XML data * * The data is assumed to be in XML format with a * <Response><TimeZone></TimeZone></Response> * element, which contains the text (string) for the region/zone. This * format is expected by, e.g. the Ubiquity installer. + * + * @note This class is an implementation detail. */ -class GeoIPXML : public GeoIP +class GeoIPXML : public Interface { public: /** @brief Configure the element tag which is selected. @@ -38,7 +42,9 @@ public: */ explicit GeoIPXML( const QString& element = QString() ); - virtual RegionZonePair processReply( const QByteArray& ); + virtual RegionZonePair processReply( const QByteArray& ) override; + virtual QString rawReply(const QByteArray & ) override; } ; +} // namespace #endif diff --git a/src/libcalamares/geoip/Handler.cpp b/src/libcalamares/geoip/Handler.cpp new file mode 100644 index 000000000..1e8b03b26 --- /dev/null +++ b/src/libcalamares/geoip/Handler.cpp @@ -0,0 +1,183 @@ +/* === This file is part of Calamares - <http://github.com/calamares> === + * + * Copyright 2019, Adriaan de Groot <groot@kde.org> + * + * Calamares is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calamares is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Calamares. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "Handler.h" + +#include "GeoIPJSON.h" +#if defined(QT_XML_LIB) +#include "GeoIPXML.h" +#endif + +#include "utils/Logger.h" +#include "utils/NamedEnum.h" +#include "utils/Variant.h" + +#include <QEventLoop> +#include <QNetworkRequest> +#include <QNetworkReply> + +#include <memory> + +static const NamedEnumTable< CalamaresUtils::GeoIP::Handler::Type >& +handlerTypes() +{ + using Type = CalamaresUtils::GeoIP::Handler::Type; + + static const NamedEnumTable<Type> names{ + { QStringLiteral( "none" ), Type::None}, + { QStringLiteral( "json" ), Type::JSON}, + { QStringLiteral( "xml" ), Type::XML} + }; + + return names; +} + +namespace CalamaresUtils::GeoIP +{ + +Handler::Handler() + : m_type( Type::None ) +{ +} + +Handler::Handler( const QString& implementation, const QString& url, const QString& selector ) + : m_type( Type::None ) + , m_url( url ) + , m_selector( selector ) +{ + bool ok = false; + m_type = handlerTypes().find( implementation, ok ); +#if !defined(QT_XML_LIB) + if ( m_type == Type::XML ) + { + m_type = Type::None; + cWarning() << "GeoIP style XML is not supported in this version of Calamares."; + } +#endif + if ( !ok ) + { + cWarning() << "GeoIP Style" << implementation << "is not recognized."; + } +} + +Handler::~Handler() +{ +} + +static QByteArray +synchronous_get( const QString& urlstring ) +{ + QUrl url( urlstring ); + QNetworkAccessManager manager; + QEventLoop loop; + + QObject::connect( &manager, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit ); + + QNetworkRequest request( url ); + QNetworkReply* reply = manager.get( request ); + loop.exec(); + reply->deleteLater(); + return reply->readAll(); +} + +static std::unique_ptr< Interface > +create_interface( Handler::Type t, const QString& selector ) +{ + switch( t ) + { + case Handler::Type::None: + return nullptr; + case Handler::Type::JSON: + return std::make_unique< GeoIPJSON >( selector ); + case Handler::Type::XML: +#if defined(QT_XML_LIB) + return std::make_unique< GeoIPXML >( selector ); +#else + return nullptr; +#endif + default: // there are no others + return nullptr; + } +} + +static RegionZonePair +do_query( Handler::Type type, const QString& url, const QString& selector ) +{ + const auto interface = create_interface( type, selector ); + if ( !interface ) + return RegionZonePair(); + + return interface->processReply( synchronous_get( url ) ); +} + +static QString +do_raw_query( Handler::Type type, const QString& url, const QString& selector ) +{ + const auto interface = create_interface( type, selector ); + if ( !interface ) + return QString(); + + return interface->rawReply( synchronous_get( url ) ); +} + +RegionZonePair +Handler::get() const +{ + if ( !isValid() ) + return RegionZonePair(); + return do_query( m_type, m_url, m_selector ); +} + + +QFuture< RegionZonePair > +Handler::query() const +{ + Handler::Type type = m_type; + QString url = m_url; + QString selector = m_selector; + + return QtConcurrent::run( [=] + { + return do_query( type, url, selector ); + } ); +} + +QString +Handler::getRaw() const +{ + if ( !isValid() ) + return QString(); + return do_raw_query( m_type, m_url, m_selector ); +} + + +QFuture< QString > +Handler::queryRaw() const +{ + Handler::Type type = m_type; + QString url = m_url; + QString selector = m_selector; + + return QtConcurrent::run( [=] + { + return do_raw_query( type, url, selector ); + } ); +} + + +} // namespace diff --git a/src/libcalamares/geoip/Handler.h b/src/libcalamares/geoip/Handler.h new file mode 100644 index 000000000..92e5f326e --- /dev/null +++ b/src/libcalamares/geoip/Handler.h @@ -0,0 +1,91 @@ +/* === This file is part of Calamares - <http://github.com/calamares> === + * + * Copyright 2019, Adriaan de Groot <groot@kde.org> + * + * Calamares is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calamares is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Calamares. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef GEOIP_HANDLER_H +#define GEOIP_HANDLER_H + +#include "Interface.h" + +#include <QtConcurrent/QtConcurrentRun> +#include <QString> +#include <QVariantMap> + +namespace CalamaresUtils {} +namespace CalamaresUtils::GeoIP +{ + +/** @brief Handle one complete GeoIP lookup. + * + * This class handles one complete GeoIP lookup. Create it with + * suitable configuration values, then call get(). This is a + * synchronous API and will return an invalid zone pair on + * error or if the configuration is not understood. For an + * async API, use query(). + */ +class DLLEXPORT Handler +{ +public: + enum class Type + { + None, + JSON, + XML + } ; + + /** @brief An unconfigured handler; this always returns errors. */ + Handler(); + /** @brief A handler for a specific GeoIP source. + * + * The @p implementation name selects an implementation; currently JSON and XML + * are supported. The @p url is retrieved by query() and then the @p selector + * is used to select something from the data returned by the @url. + */ + Handler( const QString& implementation, const QString& url, const QString& selector ); + + ~Handler(); + + /** @brief Synchronously get the GeoIP result. + * + * If the Handler is valid, then do the actual fetching and interpretation + * of data and return the result. An invalid Handler will return an + * invalid (empty) result. + */ + RegionZonePair get() const; + /// @brief Like get, but don't interpret the contents + QString getRaw() const; + + /** @brief Asynchronously get the GeoIP result. + * + * See get() for the return value. + */ + QFuture< RegionZonePair > query() const; + /// @brief Like query, but don't interpret the contents + QFuture< QString > queryRaw() const; + + bool isValid() const { return m_type != Type::None; } + Type type() const { return m_type; } + +private: + Type m_type; + const QString m_url; + const QString m_selector; +}; + +} // namespace +#endif + diff --git a/src/modules/locale/GeoIP.cpp b/src/libcalamares/geoip/Interface.cpp index 4c031f286..50aa04683 100644 --- a/src/modules/locale/GeoIP.cpp +++ b/src/libcalamares/geoip/Interface.cpp @@ -16,21 +16,24 @@ * along with Calamares. If not, see <http://www.gnu.org/licenses/>. */ -#include "GeoIP.h" +#include "Interface.h" #include "utils/Logger.h" -GeoIP::GeoIP(const QString& e) +namespace CalamaresUtils::GeoIP +{ + +Interface::Interface(const QString& e) : m_element( e ) { } -GeoIP::~GeoIP() +Interface::~Interface() { } -GeoIP::RegionZonePair -GeoIP::splitTZString( const QString& tz ) +RegionZonePair +splitTZString( const QString& tz ) { QString timezoneString( tz ); timezoneString.remove( '\\' ); @@ -42,8 +45,10 @@ GeoIP::splitTZString( const QString& tz ) cDebug() << "GeoIP reporting" << timezoneString; QString region = tzParts.takeFirst(); QString zone = tzParts.join( '/' ); - return qMakePair( region, zone ); + return RegionZonePair( region, zone ); } - return qMakePair( QString(), QString() ); + return RegionZonePair( QString(), QString() ); } + +} // namespace diff --git a/src/libcalamares/geoip/Interface.h b/src/libcalamares/geoip/Interface.h new file mode 100644 index 000000000..4b2ff3a5a --- /dev/null +++ b/src/libcalamares/geoip/Interface.h @@ -0,0 +1,98 @@ +/* === This file is part of Calamares - <http://github.com/calamares> === + * + * Copyright 2018-2019, Adriaan de Groot <groot@kde.org> + * + * Calamares is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calamares is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Calamares. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef GEOIP_INTERFACE_H +#define GEOIP_INTERFACE_H + +#include "DllMacro.h" + +#include <QPair> +#include <QString> +#include <QUrl> + +class QByteArray; + +namespace CalamaresUtils {} +namespace CalamaresUtils::GeoIP +{ +/** @brief A Region, Zone pair of strings + * + * A GeoIP lookup returns a timezone, which is represented as a Region, + * Zone pair of strings (e.g. "Europe" and "Amsterdam"). Generally, + * pasting the strings back together with a "/" is the right thing to + * do. The Zone **may** contain a "/" (e.g. "Kentucky/Monticello"). + */ +class DLLEXPORT RegionZonePair : public QPair<QString, QString> +{ +public: + /** @brief Construct from an existing pair. */ + explicit RegionZonePair( const QPair& p ) : QPair(p) { } + /** @brief Construct from two strings, like qMakePair(). */ + RegionZonePair( const QString& region, const QString& zone ) : QPair( region, zone ) { } + /** @brief An invalid zone pair (empty strings). */ + RegionZonePair() : QPair( QString(), QString() ) { } + + bool isValid() const { return !first.isEmpty(); } +} ; + +/** @brief Splits a region/zone string into a pair. + * + * Cleans up the string by removing backslashes (\\) + * since some providers return silly-escaped names. Replaces + * spaces with _ since some providers return human-readable names. + * Splits on the first / in the resulting string, or returns a + * pair of empty QStrings if it can't. (e.g. America/North Dakota/Beulah + * will return "America", "North_Dakota/Beulah"). + */ +DLLEXPORT RegionZonePair +splitTZString( const QString& s ); + +/** + * @brief Interface for GeoIP retrievers. + * + * A GeoIP retriever takes a configured URL (from the config file) + * and can handle the data returned from its interpretation of that + * configured URL, returning a region and zone. + */ +class DLLEXPORT Interface +{ +public: + virtual ~Interface(); + + /** @brief Handle a (successful) request by interpreting the data. + * + * Should return a ( <zone>, <region> ) pair, e.g. + * ( "Europe", "Amsterdam" ). This is called **only** if the + * request to the fullUrl was successful; the handler + * is free to read as much, or as little, data as it + * likes. On error, returns a RegionZonePair with empty + * strings (e.g. ( "", "" ) ). + */ + virtual RegionZonePair processReply( const QByteArray& ) = 0; + + /** @brief Get the raw reply data. */ + virtual QString rawReply( const QByteArray& ) = 0; + +protected: + Interface( const QString& e = QString() ); + + QString m_element; // string for selecting from data +} ; + +} // namespace +#endif diff --git a/src/modules/locale/test_geoip.cpp b/src/libcalamares/geoip/test_geoip.cpp index 89c1b6030..5f7ab935c 100644 --- a/src/modules/locale/test_geoip.cpp +++ b/src/libcalamares/geoip/test_geoip.cpp @@ -23,11 +23,12 @@ #include <iostream> #include "GeoIPJSON.h" -#ifdef HAVE_XML +#ifdef QT_XML_LIB #include "GeoIPXML.h" #endif using std::cerr; +using namespace CalamaresUtils::GeoIP; int main(int argc, char** argv) { @@ -37,10 +38,10 @@ int main(int argc, char** argv) return 1; } - GeoIP* handler = nullptr; + Interface* handler = nullptr; if ( QStringLiteral( "json" ) == argv[1] ) handler = new GeoIPJSON; -#ifdef HAVE_XML +#ifdef QT_XML_LIB else if ( QStringLiteral( "xml" ) == argv[1] ) handler = new GeoIPXML; #endif diff --git a/src/libcalamares/utils/LocaleLabel.cpp b/src/libcalamares/locale/Label.cpp index 26480ef14..ca528dc75 100644 --- a/src/libcalamares/utils/LocaleLabel.cpp +++ b/src/libcalamares/locale/Label.cpp @@ -1,7 +1,7 @@ /* === This file is part of Calamares - <https://github.com/calamares> === * * Copyright 2014-2015, Teo Mrnjavac <teo@kde.org> - * Copyright 2017-2018, Adriaan de Groot <groot@kde.org> + * Copyright 2017-2019, Adriaan de Groot <groot@kde.org> * * Calamares is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,12 +17,12 @@ * along with Calamares. If not, see <http://www.gnu.org/licenses/>. */ -#include "LocaleLabel.h" +#include "Label.h" -namespace CalamaresUtils +namespace CalamaresUtils::Locale { -LocaleLabel::LocaleLabel() +Label::Label() : m_locale( QLocale() ) { m_localeId = m_locale.name(); @@ -30,15 +30,15 @@ LocaleLabel::LocaleLabel() setLabels( QString(), LabelFormat::IfNeededWithCountry ); } -LocaleLabel::LocaleLabel( const QString& locale, LabelFormat format ) - : m_locale( LocaleLabel::getLocale( locale ) ) +Label::Label( const QString& locale, LabelFormat format ) + : m_locale( Label::getLocale( locale ) ) , m_localeId( locale ) { setLabels( locale, format ); } void -LocaleLabel::setLabels( const QString& locale, LabelFormat format ) +Label::setLabels( const QString& locale, LabelFormat format ) { //: language[name] (country[name]) QString longFormat = QObject::tr( "%1 (%2)" ); @@ -59,7 +59,7 @@ LocaleLabel::setLabels( const QString& locale, LabelFormat format ) m_englishLabel = needsCountryName ? longFormat.arg( englishName, QLocale::countryToString( m_locale.country() ) ) : englishName; } -QLocale LocaleLabel::getLocale( const QString& localeName ) +QLocale Label::getLocale( const QString& localeName ) { if ( localeName.contains( "@latin" ) ) { diff --git a/src/libcalamares/utils/LocaleLabel.h b/src/libcalamares/locale/Label.h index b56b29f33..65befc6b4 100644 --- a/src/libcalamares/utils/LocaleLabel.h +++ b/src/libcalamares/locale/Label.h @@ -1,7 +1,7 @@ /* === This file is part of Calamares - <https://github.com/calamares> === * * Copyright 2014-2015, Teo Mrnjavac <teo@kde.org> - * Copyright 2017-2018, Adriaan de Groot <groot@kde.org> + * Copyright 2017-2019, Adriaan de Groot <groot@kde.org> * * Calamares is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,13 +17,14 @@ * along with Calamares. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef UTILS_LOCALELABEL_H -#define UTILS_LOCALELABEL_H +#ifndef LOCALE_LABEL_H +#define LOCALE_LABEL_H #include <QLocale> #include <QString> -namespace CalamaresUtils +namespace CalamaresUtils {} +namespace CalamaresUtils::Locale { /** @@ -33,14 +34,14 @@ namespace CalamaresUtils * translation system) into QLocales, and also into consistent * human-readable text labels. */ -class LocaleLabel +class Label { public: /** @brief Formatting option for label -- add (country) to label. */ enum class LabelFormat { AlwaysWithCountry, IfNeededWithCountry } ; /** @brief Empty locale. This uses the system-default locale. */ - LocaleLabel(); + Label(); /** @brief Construct from a locale name. * @@ -48,13 +49,13 @@ public: * The @p format determines whether the country name is always present * in the label (human-readable form) or only if needed for disambiguation. */ - LocaleLabel( const QString& localeName, LabelFormat format = LabelFormat::IfNeededWithCountry ); + Label( const QString& localeName, LabelFormat format = LabelFormat::IfNeededWithCountry ); /** @brief Define a sorting order. * * English (@see isEnglish() -- it means en_US) is sorted at the top. */ - bool operator <( const LocaleLabel& other ) const + bool operator <( const Label& other ) const { return m_localeId < other.m_localeId; } @@ -91,6 +92,18 @@ public: return m_locale.name(); } + /// @brief Convenience accessor to the language part of the locale + QLocale::Language language() const + { + return m_locale.language(); + } + + /// @brief Convenience accessor to the country part (if any) of the locale + QLocale::Country country() const + { + return m_locale.country(); + } + /** @brief Get a Qt locale for the given @p localeName * * This special-cases `sr@latin`, which is used as a translation diff --git a/src/modules/welcome/LocaleModel.cpp b/src/libcalamares/locale/LabelModel.cpp index 0ecf0fd1c..543417212 100644 --- a/src/modules/welcome/LocaleModel.cpp +++ b/src/libcalamares/locale/LabelModel.cpp @@ -16,30 +16,37 @@ * along with Calamares. If not, see <http://www.gnu.org/licenses/>. */ -#include "LocaleModel.h" +#include "LabelModel.h" -LocaleModel::LocaleModel( const QStringList& locales, QObject* parent ) +#include "Lookup.h" + +#include "CalamaresVersion.h" // For the list of translations + +namespace CalamaresUtils::Locale +{ + +LabelModel::LabelModel( const QStringList& locales, QObject* parent ) : QAbstractListModel( parent ) { Q_ASSERT( locales.count() > 0 ); m_locales.reserve( locales.count() ); for ( const auto& l : locales ) - m_locales.push_back( CalamaresUtils::LocaleLabel( l ) ); + m_locales.push_back( Label( l ) ); } -LocaleModel::~LocaleModel() +LabelModel::~LabelModel() { } int -LocaleModel::rowCount( const QModelIndex& ) const +LabelModel::rowCount( const QModelIndex& ) const { return m_locales.count(); } QVariant -LocaleModel::data( const QModelIndex& index, int role ) const +LabelModel::data( const QModelIndex& index, int role ) const { if ( ( role != LabelRole ) && ( role != EnglishLabelRole ) ) return QVariant(); @@ -59,8 +66,8 @@ LocaleModel::data( const QModelIndex& index, int role ) const } } -const CalamaresUtils::LocaleLabel& -LocaleModel::locale( int row ) +const Label& +LabelModel::locale( int row ) const { if ( ( row < 0 ) || ( row >= m_locales.count() ) ) { @@ -73,7 +80,7 @@ LocaleModel::locale( int row ) } int -LocaleModel::find( std::function<bool ( const LocaleLabel& )> predicate ) const +LabelModel::find( std::function<bool ( const Label& )> predicate ) const { for ( int row = 0; row < m_locales.count() ; ++row ) { @@ -84,26 +91,40 @@ LocaleModel::find( std::function<bool ( const LocaleLabel& )> predicate ) const } int -LocaleModel::find( std::function<bool ( const QLocale& )> predicate ) const +LabelModel::find( std::function<bool ( const QLocale& )> predicate ) const { - return find( [&]( const LocaleLabel& l ) + return find( [&]( const Label& l ) { return predicate( l.locale() ); } ); } int -LocaleModel::find( const QLocale& locale ) const +LabelModel::find( const QLocale& locale ) const { - return find( [&]( const LocaleLabel& l ) + return find( [&]( const Label& l ) { return locale == l.locale(); } ); } -void -LocaleTwoColumnDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const +int +LabelModel::find( const QString& countryCode ) const +{ + if ( countryCode.length() != 2 ) + return -1; + + auto c_l = countryData( countryCode ); + int r = find( [&]( const Label& l ){ return ( l.language() == c_l.second ) && ( l.country() == c_l.first ); } ); + if ( r >= 0 ) + return r; + return find( [&]( const Label& l ){ return l.language() == c_l.second; } ); +} + +LabelModel* const availableTranslations() { - QStyledItemDelegate::paint( painter, option, index ); - option.widget->style()->drawItemText( painter, option.rect, Qt::AlignRight | Qt::AlignVCenter, option.palette, false, index.data( LocaleModel::EnglishLabelRole ).toString() ); + static LabelModel model( QString( CALAMARES_TRANSLATION_LANGUAGES ).split( ';') ); + return &model; } + +} // namespace diff --git a/src/modules/welcome/LocaleModel.h b/src/libcalamares/locale/LabelModel.h index b1566d336..178f76343 100644 --- a/src/modules/welcome/LocaleModel.h +++ b/src/libcalamares/locale/LabelModel.h @@ -16,28 +16,31 @@ * along with Calamares. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef WELCOME_LOCALEMODEL_H -#define WELCOME_LOCALEMODEL_H +#ifndef LOCALE_LABELMODEL_H +#define LOCALE_LABELMODEL_H + +#include "DllMacro.h" +#include "Label.h" #include <QAbstractListModel> -#include <QStyledItemDelegate> #include <QVector> -#include "utils/LocaleLabel.h" -class LocaleModel : public QAbstractListModel +namespace CalamaresUtils {} +namespace CalamaresUtils::Locale { -public: - using LocaleLabel = CalamaresUtils::LocaleLabel; +class DLLEXPORT LabelModel : public QAbstractListModel +{ +public: enum { LabelRole = Qt::DisplayRole, EnglishLabelRole = Qt::UserRole + 1 }; - LocaleModel( const QStringList& locales, QObject* parent = nullptr ); - virtual ~LocaleModel() override; + LabelModel( const QStringList& locales, QObject* parent = nullptr ); + virtual ~LabelModel() override; int rowCount( const QModelIndex& parent ) const override; @@ -48,26 +51,34 @@ public: * This is the backing data for the model; if @p row is out-of-range, * returns a reference to en_US. */ - const LocaleLabel& locale( int row ); + const Label& locale( int row ) const; /** @brief Searches for an item that matches @p predicate * * Returns the row number of the first match, or -1 if there isn't one. */ int find( std::function<bool( const QLocale& )> predicate ) const; - int find( std::function<bool( const LocaleLabel& )> predicate ) const; + int find( std::function<bool( const Label& )> predicate ) const; + /// @brief Looks for an item using the same locale, -1 if there isn't one int find( const QLocale& ) const; + /// @brief Looks for an item that best matches the 2-letter country code + int find( const QString& countryCode ) const; private: - QVector< LocaleLabel > m_locales; + QVector< Label > m_locales; } ; -class LocaleTwoColumnDelegate : public QStyledItemDelegate -{ -public: - using QStyledItemDelegate::QStyledItemDelegate; - - void paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const override; -} ; +/** @brief Returns a model with all available translations. + * + * The translations are set when Calamares is compiled; the list + * is provided by CMake via the CALAMARES_TRANSLATION_LANGUAGES + * #define. + * + * This model is a singleton and can be shared. + * + * NOTE: While the model is not typed const, it should be. Do not modify. + */ +DLLEXPORT LabelModel* const availableTranslations(); +} // namespace #endif diff --git a/src/libcalamares/locale/Lookup.cpp b/src/libcalamares/locale/Lookup.cpp index d38d417d4..a096bc679 100644 --- a/src/libcalamares/locale/Lookup.cpp +++ b/src/libcalamares/locale/Lookup.cpp @@ -20,7 +20,7 @@ #include "CountryData_p.cpp" -namespace Calamares +namespace CalamaresUtils::Locale { struct TwoChar @@ -35,7 +35,7 @@ struct TwoChar cc2 = code[1].toLatin1(); } } - + char cc1; char cc2; }; @@ -44,7 +44,7 @@ static const CountryData* lookup( TwoChar c ) { if ( !c.cc1 ) return nullptr; - + const CountryData* p = std::find_if(country_data_table, country_data_table + country_data_size, [c=c]( const CountryData& d ){ return (d.cc1 == c.cc1) && (d.cc2 == c.cc2); } ); @@ -52,7 +52,7 @@ static const CountryData* lookup( TwoChar c ) return nullptr; return p; } - + QLocale::Country countryForCode(const QString& code) { const CountryData* p = lookup( TwoChar( code ) ); diff --git a/src/libcalamares/locale/Lookup.h b/src/libcalamares/locale/Lookup.h index 976c4dc21..5712a1120 100644 --- a/src/libcalamares/locale/Lookup.h +++ b/src/libcalamares/locale/Lookup.h @@ -24,25 +24,26 @@ #include <QLocale> #include <QPair> -namespace Calamares +namespace CalamaresUtils {} +namespace CalamaresUtils::Locale { /* All the functions in this file do lookups of locale data * based on CLDR tables; these are lookups that you can't (easily) * do with just QLocale (e.g. from 2-letter country code to a likely * locale). */ - + /// @brief Map a 2-letter code to a Country, or AnyCountry if not found DLLEXPORT QLocale::Country countryForCode( const QString& code ); /** @brief Map a Country to a Language, or AnyLanguage if not found - * + * * This is a *likely* language for the given country, based on the * CLDR tables. For instance, this maps Belgium to Dutch. */ DLLEXPORT QLocale::Language languageForCountry( QLocale::Country country ); /// @brief Map a 2-letter code to a Language, or AnyLanguage if not found DLLEXPORT QLocale::Language languageForCountry( const QString& code ); - + /// @brief Get both Country and Language for a 2-letter code DLLEXPORT QPair< QLocale::Country, QLocale::Language > countryData( const QString& code ); /// @brief Get a likely locale for a 2-letter country code diff --git a/src/modules/locale/CMakeLists.txt b/src/modules/locale/CMakeLists.txt index affaa3753..768a67543 100644 --- a/src/modules/locale/CMakeLists.txt +++ b/src/modules/locale/CMakeLists.txt @@ -8,16 +8,6 @@ endif() include_directories( ${PROJECT_BINARY_DIR}/src/libcalamaresui ) -set( geoip_src GeoIP.cpp GeoIPJSON.cpp ) -set( geoip_libs ) - -find_package(Qt5 COMPONENTS Xml) -if( Qt5Xml_FOUND ) - list( APPEND geoip_src GeoIPXML.cpp ) - list( APPEND geoip_libs Qt5::Xml ) - add_definitions( -DHAVE_XML ) -endif() - calamares_add_plugin( locale TYPE viewmodule EXPORT_MACRO PLUGINDLLEXPORT_PRO @@ -43,20 +33,6 @@ calamares_add_plugin( locale if( ECM_FOUND AND BUILD_TESTING ) ecm_add_test( - GeoIPTests.cpp - ${geoip_src} - TEST_NAME - geoiptest - LINK_LIBRARIES - calamaresui - Qt5::Network - Qt5::Test - ${geoip_libs} - ${YAMLCPP_LIBRARY} - ) - calamares_automoc( geoiptest ) - - ecm_add_test( Tests.cpp LocaleConfiguration.cpp TEST_NAME @@ -67,9 +43,3 @@ if( ECM_FOUND AND BUILD_TESTING ) ) calamares_automoc( localetest ) endif() - -if( BUILD_TESTING ) - add_executable( test_geoip test_geoip.cpp ${geoip_src} ) - target_link_libraries( test_geoip calamaresui Qt5::Network ${geoip_libs} ${YAMLCPP_LIBRARY} ) - calamares_automoc( test_geoip ) -endif() diff --git a/src/modules/locale/GeoIP.h b/src/modules/locale/GeoIP.h deleted file mode 100644 index 41abd2042..000000000 --- a/src/modules/locale/GeoIP.h +++ /dev/null @@ -1,70 +0,0 @@ -/* === This file is part of Calamares - <http://github.com/calamares> === - * - * Copyright 2018, Adriaan de Groot <groot@kde.org> - * - * Calamares is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Calamares is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Calamares. If not, see <http://www.gnu.org/licenses/>. - */ - -#ifndef GEOIP_H -#define GEOIP_H - -#include <QPair> -#include <QString> -#include <QUrl> - -class QByteArray; - -/** - * @brief Interface for GeoIP retrievers. - * - * A GeoIP retriever takes a configured URL (from the config file) - * and can handle the data returned from its interpretation of that - * configured URL, returning a region and zone. - */ -class GeoIP -{ -public: - using RegionZonePair = QPair<QString, QString>; - - virtual ~GeoIP(); - - /** @brief Handle a (successful) request by interpreting the data. - * - * Should return a ( <zone>, <region> ) pair, e.g. - * ( "Europe", "Amsterdam" ). This is called **only** if the - * request to the fullUrl was successful; the handler - * is free to read as much, or as little, data as it - * likes. On error, returns a RegionZonePair with empty - * strings (e.g. ( "", "" ) ). - */ - virtual RegionZonePair processReply( const QByteArray& ) = 0; - - /** @brief Splits a region/zone string into a pair. - * - * Cleans up the string by removing backslashes (\\) - * since some providers return silly-escaped names. Replaces - * spaces with _ since some providers return human-readable names. - * Splits on the first / in the resulting string, or returns a - * pair of empty QStrings if it can't. (e.g. America/North Dakota/Beulah - * will return "America", "North_Dakota/Beulah"). - */ - static RegionZonePair splitTZString( const QString& s ); - -protected: - GeoIP( const QString& e = QString() ); - - QString m_element; // string for selecting from data -} ; - -#endif diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index c8076866e..f6cdba436 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -27,8 +27,8 @@ #include "LCLocaleDialog.h" #include "Settings.h" +#include "locale/Label.h" #include "utils/CalamaresUtilsGui.h" -#include "utils/LocaleLabel.h" #include "utils/Logger.h" #include "utils/Retranslator.h" @@ -387,10 +387,10 @@ LocalePage::init( const QString& initialRegion, std::pair< QString, QString > LocalePage::prettyLocaleStatus( const LocaleConfiguration& lc ) const { - using CalamaresUtils::LocaleLabel; + using CalamaresUtils::Locale::Label; - LocaleLabel lang( lc.language(), LocaleLabel::LabelFormat::AlwaysWithCountry ); - LocaleLabel num( lc.lc_numeric, LocaleLabel::LabelFormat::AlwaysWithCountry ); + Label lang( lc.language(), Label::LabelFormat::AlwaysWithCountry ); + Label num( lc.lc_numeric, Label::LabelFormat::AlwaysWithCountry ); return std::make_pair< QString, QString >( tr( "The system language will be set to %1." ).arg( lang.label() ), diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index d321b8501..29006ec33 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -19,18 +19,15 @@ #include "LocaleViewStep.h" -#include "GeoIP.h" -#include "GeoIPJSON.h" -#ifdef HAVE_XML -#include "GeoIPXML.h" -#endif -#include "GlobalStorage.h" -#include "JobQueue.h" #include "LocalePage.h" - #include "timezonewidget/localeglobal.h" #include "widgets/WaitingWidget.h" +#include "GlobalStorage.h" +#include "JobQueue.h" + +#include "geoip/Handler.h" + #include "utils/CalamaresUtilsGui.h" #include "utils/Logger.h" #include "utils/Variant.h" @@ -116,54 +113,16 @@ LocaleViewStep::setUpPage() void LocaleViewStep::fetchGeoIpTimezone() { - QString actualUrl( m_geoipUrl ); - GeoIP *handler = nullptr; - - if ( m_geoipStyle.isEmpty() || m_geoipStyle == "legacy" ) - { - actualUrl.append( "/json/" ); - handler = new GeoIPJSON( m_geoipSelector ); - } - else if ( m_geoipStyle == "json" ) + CalamaresUtils::GeoIP::Handler h( m_geoipStyle, m_geoipUrl, m_geoipSelector ); + if ( h.isValid() ) { - handler = new GeoIPJSON( m_geoipSelector ); + m_startingTimezone = h.get(); + if ( !m_startingTimezone.isValid() ) + cWarning() << "GeoIP lookup at" << m_geoipUrl << "failed."; } -#if defined(HAVE_XML) - else if ( m_geoipStyle == "xml" ) - { - handler = new GeoIPXML( m_geoipSelector ); - } -#endif else - { cWarning() << "GeoIP Style" << m_geoipStyle << "is not recognized."; - setUpPage(); - return; - } - cDebug() << "Fetching GeoIP data from" << actualUrl; - - QNetworkAccessManager *manager = new QNetworkAccessManager( this ); - connect( manager, &QNetworkAccessManager::finished, - [=]( QNetworkReply* reply ) - { - if ( reply->error() == QNetworkReply::NoError ) - { - auto tz = handler->processReply( reply->readAll() ); - if ( !tz.first.isEmpty() ) - m_startingTimezone = tz; - else - cWarning() << "GeoIP lookup at" << reply->url() << "failed."; - } - delete handler; - reply->deleteLater(); - manager->deleteLater(); - setUpPage(); - } ); - - QNetworkRequest request; - request.setUrl( QUrl::fromUserInput( actualUrl ) ); - request.setAttribute( QNetworkRequest::FollowRedirectsAttribute, true ); - manager->get( request ); + setUpPage(); } @@ -251,35 +210,43 @@ LocaleViewStep::onLeave() void LocaleViewStep::setConfigurationMap( const QVariantMap& configurationMap ) { - if ( configurationMap.contains( "region" ) && - configurationMap.value( "region" ).type() == QVariant::String && - !configurationMap.value( "region" ).toString().isEmpty() && - configurationMap.contains( "zone" ) && - configurationMap.value( "zone" ).type() == QVariant::String && - !configurationMap.value( "zone" ).toString().isEmpty() ) + QString region = CalamaresUtils::getString( configurationMap, "region" ); + QString zone = CalamaresUtils::getString( configurationMap, "zone" ); + if ( !region.isEmpty() && !zone.isEmpty() ) { - m_startingTimezone = qMakePair( configurationMap.value( "region" ).toString(), - configurationMap.value( "zone" ).toString() ); + m_startingTimezone = CalamaresUtils::GeoIP::RegionZonePair( region, zone ); } else { - m_startingTimezone = qMakePair( QStringLiteral( "America" ), - QStringLiteral( "New_York" ) ); + m_startingTimezone = CalamaresUtils::GeoIP::RegionZonePair( QStringLiteral( "America" ), QStringLiteral( "New_York" ) ); } - if ( configurationMap.contains( "localeGenPath" ) && - configurationMap.value( "localeGenPath" ).type() == QVariant::String && - !configurationMap.value( "localeGenPath" ).toString().isEmpty() ) + m_localeGenPath = CalamaresUtils::getString( configurationMap, "localeGenPath" ); + if ( m_localeGenPath.isEmpty() ) + m_localeGenPath = QStringLiteral( "/etc/locale.gen" ); + + bool ok = false; + QVariantMap geoip = CalamaresUtils::getSubMap( configurationMap, "geoip", ok ); + if ( ok ) { - m_localeGenPath = configurationMap.value( "localeGenPath" ).toString(); + m_geoipUrl = CalamaresUtils::getString( geoip, "url" ); + m_geoipStyle = CalamaresUtils::getString( geoip, "style" ); + m_geoipSelector = CalamaresUtils::getString( geoip, "selector" ); } else { - m_localeGenPath = QStringLiteral( "/etc/locale.gen" ); - } + // Optional + m_geoipUrl = CalamaresUtils::getString( configurationMap, "geoipUrl" ); + m_geoipStyle = CalamaresUtils::getString( configurationMap, "geoipStyle" ); + m_geoipSelector = CalamaresUtils::getString( configurationMap, "geoipSelector" ); - // Optional - m_geoipUrl = CalamaresUtils::getString( configurationMap, "geoipUrl" ); - m_geoipStyle = CalamaresUtils::getString( configurationMap, "geoipStyle" ); - m_geoipSelector = CalamaresUtils::getString( configurationMap, "geoipSelector" ); + if ( !m_geoipUrl.isEmpty() && ( m_geoipStyle.isEmpty() || m_geoipStyle == "legacy" ) ) + { + m_geoipStyle = "json"; + m_geoipUrl.append( "/json/" ); + } + + if ( !m_geoipUrl.isEmpty() ) + cWarning() << "Legacy-style GeoIP configuration is deprecated. Use geoip: map."; + } } diff --git a/src/modules/locale/LocaleViewStep.h b/src/modules/locale/LocaleViewStep.h index 3f5c91621..8ab50b75d 100644 --- a/src/modules/locale/LocaleViewStep.h +++ b/src/modules/locale/LocaleViewStep.h @@ -20,14 +20,14 @@ #ifndef LOCALEVIEWSTEP_H #define LOCALEVIEWSTEP_H -#include <QObject> - -#include <utils/PluginFactory.h> -#include <viewpages/ViewStep.h> +#include "geoip/Interface.h" +#include "utils/PluginFactory.h" +#include "viewpages/ViewStep.h" -#include <PluginDllMacro.h> +#include "PluginDllMacro.h" #include <QFutureWatcher> +#include <QObject> class LocalePage; class WaitingWidget; @@ -71,7 +71,7 @@ private: bool m_nextEnabled; QString m_prettyStatus; - QPair< QString, QString > m_startingTimezone; + CalamaresUtils::GeoIP::RegionZonePair m_startingTimezone; QString m_localeGenPath; QString m_geoipUrl; // The URL, depening on style might be modified on lookup diff --git a/src/modules/locale/locale.conf b/src/modules/locale/locale.conf index ddd0bc97e..7c2ec332c 100644 --- a/src/modules/locale/locale.conf +++ b/src/modules/locale/locale.conf @@ -25,21 +25,46 @@ zone: "New_York" # custom path for locale.gen #localeGenPath: "PATH_TO/locale.gen" -# GeoIP based Language settings: +# GeoIP based Language settings: Leave commented out to disable GeoIP. # -# GeoIP need an working Internet connection. +# GeoIP needs a working Internet connection. # This can be managed from `welcome.conf` by adding # internet to the list of required conditions. # -# Leave commented out to disable GeoIP. +# The configuration +# is in three parts: a *style*, which can be "json" or "xml" +# depending on the kind of data returned by the service, and +# a *url* where the data is retrieved, and an optional *selector* +# to pick the right field out of the returned data (e.g. field +# name in JSON or element name in XML). # -# An HTTP request is made to *geoipUrl* -- depending on the geoipStyle, -# the URL may be modified before use. The request should return -# valid data in a suitable format, depending on geoipStyle; +# The default selector (when the setting is blank) is picked to +# work with existing JSON providers (which use "time_zone") and +# Ubiquity's XML providers (which use "TimeZone"). +# +# If the service configured via *url* uses +# a different attribute name (e.g. "timezone") in JSON or a +# different element tag (e.g. "<Time_Zone>") in XML, set this +# string to the name or tag to be used. +# +# In JSON: +# - if the string contains "." characters, this is used as a +# multi-level selector, e.g. "a.b" will select the timezone +# from data "{a: {b: "Europe/Amsterdam" } }". +# - each part of the string split by "." characters is used as +# a key into the JSON data. +# In XML: +# - all elements with the named tag (e.g. all TimeZone) elements +# from the document are checked; the first one with non-empty +# text value is used. +# +# +# An HTTP(S) request is made to *url*. The request should return +# valid data in a suitable format, depending on *style*; # generally this includes a string value with the timezone # in <region>/<zone> format. For services that return data which # does not follow the conventions of "suitable data" described -# below, *geoIPSelector* may be used to pick different data. +# below, *selector* may be used to pick different data. # # Note that this example URL works, but the service is shutting # down in June 2018. @@ -58,40 +83,9 @@ zone: "New_York" # - backslashes are removed # - spaces are replaced with _ # -#geoipUrl: "freegeoip.net" - -# GeoIP style. Leave commented out for the "legacy" interpretation. -# This setting only makes sense if geoipUrl is set, enabliing geoIP. -# -# Possible values are: -# unset same as "legacy" -# blank same as "legacy" -# "legacy" appends "/json" to geoipUrl, above, and uses JSON format -# (which is what freegeoip.net provides there). -# "json" URL is not modified, uses JSON format. -# "xml" URL is not modified, uses XML format. -# -# The JSON format is provided by freegeoip.net, but that service is -# shutting down in June 2018. There are other providers with the same -# format. XML format is provided for Ubiquity. -#geoipStyle: "legacy" - -# GeoIP selector. Leave commented out for the default selector -# (which depends on the style: JSON uses "time_zone" and XML -# uses TimeZone, for the FreeGeoIP-alike and the Ubiquity-alike -# respectively). If the service configured via *geoipUrl* uses -# a different attribute name (e.g. "timezone") in JSON or a -# different element tag (e.g. "<Time_Zone>") in XML, set this -# string to the name or tag to be used. -# -# In JSON: -# - if the string contains "." characters, this is used as a -# multi-level selector, e.g. "a.b" will select the timezone -# from data "{a: {b: "Europe/Amsterdam" } }". -# - each part of the string split by "." characters is used as -# a key into the JSON data. -# In XML: -# - all elements with the named tag (e.g. all TimeZone) elements -# from the document are checked; the first one with non-empty -# text value is used. -#geoipSelector: "" +# Legacy settings "geoipStyle", "geoipUrl" and "geoipSelector" +# in the top-level are still supported, but I'd advise against. +geoip: + style: "json" + url: "https://geoip.kde.org/v1/calamares" + selector: "" # leave blank for the default diff --git a/src/modules/welcome/CMakeLists.txt b/src/modules/welcome/CMakeLists.txt index e6ddd2bd7..e25b7f5d0 100644 --- a/src/modules/welcome/CMakeLists.txt +++ b/src/modules/welcome/CMakeLists.txt @@ -27,7 +27,6 @@ calamares_add_plugin( welcome EXPORT_MACRO PLUGINDLLEXPORT_PRO SOURCES ${CHECKER_SOURCES} - LocaleModel.cpp WelcomeViewStep.cpp WelcomePage.cpp UI diff --git a/src/modules/welcome/WelcomePage.cpp b/src/modules/welcome/WelcomePage.cpp index 65a475310..ae78aa8ff 100644 --- a/src/modules/welcome/WelcomePage.cpp +++ b/src/modules/welcome/WelcomePage.cpp @@ -21,16 +21,16 @@ #include "WelcomePage.h" #include "ui_WelcomePage.h" -#include "LocaleModel.h" #include "checker/CheckerContainer.h" #include "Branding.h" #include "CalamaresVersion.h" #include "Settings.h" #include "ViewManager.h" + +#include "locale/LabelModel.h" #include "modulesystem/ModuleManager.h" #include "utils/CalamaresUtilsGui.h" -#include "utils/LocaleLabel.h" #include "utils/Logger.h" #include "utils/Retranslator.h" @@ -131,7 +131,7 @@ WelcomePage::initLanguages() ui->languageWidget->clear(); ui->languageWidget->setInsertPolicy( QComboBox::InsertAtBottom ); - m_languages = new LocaleModel( QString( CALAMARES_TRANSLATION_LANGUAGES ).split( ';') ); + m_languages = CalamaresUtils::Locale::availableTranslations(); ui->languageWidget->setModel( m_languages ); ui->languageWidget->setItemDelegate( new LocaleTwoColumnDelegate( ui->languageWidget ) ); @@ -257,7 +257,23 @@ WelcomePage::focusInEvent( QFocusEvent* e ) e->accept(); } -bool WelcomePage::verdict() const +bool +WelcomePage::verdict() const { return m_checkingWidget->verdict(); } + +void +WelcomePage::externallySelectedLanguage( int row ) +{ + if ( ( row >= 0 ) && ( row < ui->languageWidget->count() ) ) + ui->languageWidget->setCurrentIndex( row ); +} + + +void +LocaleTwoColumnDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + QStyledItemDelegate::paint( painter, option, index ); + option.widget->style()->drawItemText( painter, option.rect, Qt::AlignRight | Qt::AlignVCenter, option.palette, false, index.data( CalamaresUtils::Locale::LabelModel::EnglishLabelRole ).toString() ); +} diff --git a/src/modules/welcome/WelcomePage.h b/src/modules/welcome/WelcomePage.h index ec689735b..f05426d38 100644 --- a/src/modules/welcome/WelcomePage.h +++ b/src/modules/welcome/WelcomePage.h @@ -20,6 +20,9 @@ #ifndef WELCOMEPAGE_H #define WELCOMEPAGE_H +#include "locale/LabelModel.h" + +#include <QStyledItemDelegate> #include <QWidget> namespace Ui @@ -28,7 +31,6 @@ class WelcomePage; } class CheckerContainer; -class LocaleModel; class WelcomePage : public QWidget { @@ -44,6 +46,8 @@ public: /// @brief Results of requirements checking bool verdict() const; + /// @brief Change the language from an external source. + void externallySelectedLanguage( int row ); protected: void focusInEvent( QFocusEvent* e ) override; //choose the child widget to focus @@ -53,7 +57,19 @@ private: Ui::WelcomePage* ui; CheckerContainer* m_checkingWidget; - LocaleModel *m_languages; + CalamaresUtils::Locale::LabelModel *m_languages; }; +/** @brief Delegate to display language information in two columns. + * + * Displays the native language name and the English language name. + */ +class LocaleTwoColumnDelegate : public QStyledItemDelegate +{ +public: + using QStyledItemDelegate::QStyledItemDelegate; + + void paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const override; +} ; + #endif // WELCOMEPAGE_H diff --git a/src/modules/welcome/WelcomeViewStep.cpp b/src/modules/welcome/WelcomeViewStep.cpp index 88b5f6324..e115565b7 100644 --- a/src/modules/welcome/WelcomeViewStep.cpp +++ b/src/modules/welcome/WelcomeViewStep.cpp @@ -22,9 +22,13 @@ #include "WelcomePage.h" #include "checker/GeneralRequirements.h" +#include "geoip/Handler.h" +#include "locale/Lookup.h" #include "modulesystem/ModuleManager.h" #include "utils/Logger.h" +#include "utils/Variant.h" +#include <QFutureWatcher> #include <QVariant> CALAMARES_PLUGIN_FACTORY_DEFINITION( WelcomeViewStepFactory, registerPlugin<WelcomeViewStep>(); ) @@ -97,18 +101,9 @@ WelcomeViewStep::jobs() const void WelcomeViewStep::setConfigurationMap( const QVariantMap& configurationMap ) { - bool showSupportUrl = - configurationMap.contains( "showSupportUrl" ) && - configurationMap.value( "showSupportUrl" ).type() == QVariant::Bool && - configurationMap.value( "showSupportUrl" ).toBool(); - bool showKnownIssuesUrl = - configurationMap.contains( "showKnownIssuesUrl" ) && - configurationMap.value( "showKnownIssuesUrl" ).type() == QVariant::Bool && - configurationMap.value( "showKnownIssuesUrl" ).toBool(); - bool showReleaseNotesUrl = - configurationMap.contains( "showReleaseNotesUrl" ) && - configurationMap.value( "showReleaseNotesUrl" ).type() == QVariant::Bool && - configurationMap.value( "showReleaseNotesUrl" ).toBool(); + bool showSupportUrl = CalamaresUtils::getBool( configurationMap, "showSupportUrl", false ); + bool showKnownIssuesUrl = CalamaresUtils::getBool( configurationMap, "showKnownIssuesUrl", false ); + bool showReleaseNotesUrl = CalamaresUtils::getBool( configurationMap, "showReleaseNotesUrl", false ); m_widget->setUpLinks( showSupportUrl, showKnownIssuesUrl, @@ -120,9 +115,57 @@ WelcomeViewStep::setConfigurationMap( const QVariantMap& configurationMap ) else cWarning() << "no valid requirements map found in welcome " "module configuration."; + + bool ok = false; + QVariantMap geoip = CalamaresUtils::getSubMap( configurationMap, "geoip", ok ); + if ( ok ) + { + using FWString = QFutureWatcher< QString >; + + auto* handler = new CalamaresUtils::GeoIP::Handler( + CalamaresUtils::getString( geoip, "style" ), + CalamaresUtils::getString( geoip, "url" ), + CalamaresUtils::getString( geoip, "selector" ) ); + auto* future = new FWString(); + connect( future, &FWString::finished, [view=this, f=future, h=handler]() + { + QString countryResult = f->future().result(); + cDebug() << "GeoIP result for welcome=" << countryResult; + view->setCountry( countryResult ); + f->deleteLater(); + delete h; + } ); + future->setFuture( handler->queryRaw() ); + } } -Calamares::RequirementsList WelcomeViewStep::checkRequirements() +Calamares::RequirementsList +WelcomeViewStep::checkRequirements() { return m_requirementsChecker->checkRequirements(); } + +void +WelcomeViewStep::setCountry( const QString& countryCode ) +{ + if ( countryCode.length() != 2 ) + { + cDebug() << "Unusable country code" << countryCode; + return; + } + + auto c_l = CalamaresUtils::Locale::countryData( countryCode ); + if ( c_l.first == QLocale::Country::AnyCountry ) + { + cDebug() << "Unusable country code" << countryCode; + return; + } + else + { + int r = CalamaresUtils::Locale::availableTranslations()->find( countryCode ); + if ( r < 0 ) + cDebug() << "Unusable country code" << countryCode << "(no suitable translation)"; + if ( ( r >= 0 ) && m_widget ) + m_widget->externallySelectedLanguage( r ); + } +} diff --git a/src/modules/welcome/WelcomeViewStep.h b/src/modules/welcome/WelcomeViewStep.h index 937cad246..7deed2167 100644 --- a/src/modules/welcome/WelcomeViewStep.h +++ b/src/modules/welcome/WelcomeViewStep.h @@ -54,6 +54,13 @@ public: void setConfigurationMap( const QVariantMap& configurationMap ) override; + /** @brief Sets the country that Calamares is running in. + * + * This (ideally) sets up language and locale settings that are right for + * the given 2-letter country code. + */ + void setCountry( const QString& ); + Calamares::RequirementsList checkRequirements() override; private: diff --git a/src/modules/welcome/welcome.conf b/src/modules/welcome/welcome.conf index 8b6ad3482..6d9a307fc 100644 --- a/src/modules/welcome/welcome.conf +++ b/src/modules/welcome/welcome.conf @@ -47,3 +47,16 @@ requirements: # - storage - ram # - root + +# GeoIP checking +# +# This can be used to pre-select a language based on the country +# the user is currently in. It *assumes* that there's internet +# connectivity, though. Configuration is like in the locale module, +# but remember to use a URL that returns full data **and** to +# use a selector that will pick the country, not the timezone. + +geoip: + style: "xml" + url: "https://geoip.kde.org/v1/ubiquity" # extended XML format + selector: "CountryCode" # blank uses default, which is wrong |