diff --git a/CMakeLists.txt b/CMakeLists.txt index f04d82e..f7334bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,49 +1,54 @@ cmake_minimum_required(VERSION 3.0) project(itinerary VERSION 0.0.1) find_package(ECM 5.38 REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_SOURCE_DIR}/cmake) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMAddTests) include(ECMGenerateHeaders) include(ECMQtDeclareLoggingCategory) include(ECMSetupVersion) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(GenerateExportHeader) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) ecm_setup_version(PROJECT VARIABLE_PREFIX ITINERARY VERSION_HEADER itinerary_version.h) # build-time dependencies find_package(Qt5 REQUIRED COMPONENTS Test Quick) find_package(KF5 REQUIRED COMPONENTS I18n) find_package(KF5Contacts CONFIG REQUIRED) find_package(KPimPkPass CONFIG REQUIRED) find_package(KPimItinerary CONFIG REQUIRED) find_package(QmlLint) set_package_properties(QmlLint PROPERTIES URL "https://qt.io" PURPOSE "Validate QML code.") find_package(SharedMimeInfo 1.0 REQUIRED) include(ECMQMLModules) ecm_find_qmlmodule(org.kde.prison 1.0) # runtime dependencies are build-time dependencies on Android if (ANDROID) find_package(Qt5 REQUIRED COMPONENTS AndroidExtras Svg) find_package(KF5 REQUIRED COMPONENTS Archive Kirigami2 Prison) if (NOT DEFINED BREEZEICONS_DIR AND EXISTS ${CMAKE_SOURCE_DIR}/../breeze-icons) set(BREEZEICONS_DIR ${CMAKE_SOURCE_DIR}/../breeze-icons) endif() else() find_package(Qt5 REQUIRED COMPONENTS Positioning) endif() +add_definitions(-DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII -DQT_NO_URL_CAST_FROM_STRING) +add_definitions(-DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT) +add_definitions(-DQT_USE_QSTRINGBUILDER) +add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) + add_subdirectory(src) add_subdirectory(autotests) install(FILES org_kde_itinerary.categories DESTINATION ${KDE_INSTALL_CONFDIR}) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/app/main.cpp b/src/app/main.cpp index d6f57ca..f424152 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1,141 +1,141 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "itinerary_version.h" #include "logging.h" #include "applicationcontroller.h" #include "countryinformation.h" #include "localizer.h" #include "pkpassmanager.h" #include "timelinemodel.h" #include "pkpassimageprovider.h" #include "reservationmanager.h" #include #include #include #include #include #include #ifdef Q_OS_ANDROID #include #include #endif #include #include #include #include void handleViewIntent(PkPassManager *passMgr) { #ifdef Q_OS_ANDROID // handle opened files const auto activity = QtAndroid::androidActivity(); if (!activity.isValid()) return; const auto intent = activity.callObjectMethod("getIntent", "()Landroid/content/Intent;"); if (!intent.isValid()) return; const auto uri = intent.callObjectMethod("getData", "()Landroid/net/Uri;"); if (!uri.isValid()) return; const auto scheme = uri.callObjectMethod("getScheme", "()Ljava/lang/String;"); if (scheme.toString() == QLatin1String("content")) { const auto tmpFile = activity.callObjectMethod("receiveContent", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object()); passMgr->importPassFromTempFile(tmpFile.toString()); } else if (scheme.toString() == QLatin1String("file")) { const auto uriStr = uri.callObjectMethod("toString", "()Ljava/lang/String;"); passMgr->importPass(QUrl(uriStr.toString())); } else { const auto uriStr = uri.callObjectMethod("toString", "()Ljava/lang/String;"); qCWarning(Log) << "Unknown intent URI:" << uriStr.toString(); } #else Q_UNUSED(passMgr); #endif } #ifdef Q_OS_ANDROID Q_DECL_EXPORT #endif int main(int argc, char **argv) { QCoreApplication::setApplicationName(QStringLiteral("kde-itinerary")); QCoreApplication::setOrganizationName(QStringLiteral("KDE")); QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); QCoreApplication::setApplicationVersion(QStringLiteral(ITINERARY_VERSION_STRING)); QGuiApplication::setApplicationDisplayName(QStringLiteral("KDE Itinerary")); // TODO i18n QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QGuiApplication app(argc, argv); QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("map-globe"))); QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); - parser.addPositionalArgument("pass", QStringLiteral("PkPass file to import.")); + parser.addPositionalArgument(QStringLiteral("pass"), QStringLiteral("PkPass file to import.")); parser.process(app); PkPassManager passMgr; ReservationManager resMgr; resMgr.setPkPassManager(&passMgr); TimelineModel timelineModel; timelineModel.setReservationManager(&resMgr); ApplicationController appController; qmlRegisterUncreatableType("org.kde.pkpass", 1, 0, "Barcode", {}); qmlRegisterUncreatableType("org.kde.pkpass", 1, 0, "Field", {}); qRegisterMetaType(); qmlRegisterUncreatableType("org.kde.kitinerary", 1, 0, "Ticket", {}); qmlRegisterUncreatableMetaObject(KItinerary::KnowledgeDb::staticMetaObject, "org.kde.kitinerary", 1, 0, "KnowledgeDb", {}); qmlRegisterUncreatableType("org.kde.itinerary", 1, 0, "CountryInformation", {}); qmlRegisterUncreatableType("org.kde.itinerary", 1, 0, "TimelineModel", {}); qmlRegisterSingletonType("org.kde.itinerary", 1, 0, "Localizer", [](QQmlEngine*, QJSEngine*) -> QObject*{ return new Localizer; }); QQmlApplicationEngine engine; engine.addImageProvider(QStringLiteral("org.kde.pkpass"), new PkPassImageProvider(&passMgr)); engine.rootContext()->setContextProperty(QStringLiteral("_pkpassManager"), &passMgr); engine.rootContext()->setContextProperty(QStringLiteral("_reservationManager"), &resMgr); engine.rootContext()->setContextProperty(QStringLiteral("_timelineModel"), &timelineModel); engine.rootContext()->setContextProperty(QStringLiteral("_appController"), &appController); engine.load(QStringLiteral(":/main.qml")); for (const auto &file : parser.positionalArguments()) { if (file.endsWith(QLatin1String(".pkpass"))) passMgr.importPass(QUrl::fromLocalFile(file)); else resMgr.importReservation(QUrl::fromLocalFile(file)); } handleViewIntent(&passMgr); return app.exec(); } diff --git a/src/app/pkpassmanager.cpp b/src/app/pkpassmanager.cpp index c7f8296..cd1ccba 100644 --- a/src/app/pkpassmanager.cpp +++ b/src/app/pkpassmanager.cpp @@ -1,193 +1,193 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "pkpassmanager.h" #include "logging.h" #include #include #include #include #include #include #include #include #include #include #include PkPassManager::PkPassManager(QObject* parent) : QObject(parent) , m_nam(new QNetworkAccessManager(this)) { } PkPassManager::~PkPassManager() = default; QVector PkPassManager::passes() const { - const auto basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes"); + const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes"); QDir::root().mkpath(basePath); QVector passIds; for (QDirIterator topIt(basePath, QDir::NoDotAndDotDot | QDir::Dirs); topIt.hasNext();) { for (QDirIterator subIt(topIt.next(), QDir::Files); subIt.hasNext();) { QFileInfo fi(subIt.next()); passIds.push_back(fi.dir().dirName() + QLatin1Char('/') + fi.baseName()); } } return passIds; } KPkPass::Pass* PkPassManager::pass(const QString& passId) { const auto it = m_passes.constFind(passId); if (it != m_passes.constEnd() && it.value()) { return it.value(); } const QString passPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes/") + passId + QLatin1String(".pkpass"); if (!QFile::exists(passPath)) { return nullptr; } auto file = KPkPass::Pass::fromFile(passPath, this); // TODO error handling m_passes.insert(passId, file); return file; } QObject* PkPassManager::passObject(const QString& passId) { return pass(passId); } void PkPassManager::importPass(const QUrl& url) { doImportPass(url, Copy); } void PkPassManager::importPassFromTempFile(const QString& tmpFile) { doImportPass(QUrl::fromLocalFile(tmpFile), Move); } void PkPassManager::doImportPass(const QUrl& url, PkPassManager::ImportMode mode) { qCDebug(Log) << url << mode; if (!url.isLocalFile()) return; // TODO - const auto basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes"); + const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes"); QDir::root().mkpath(basePath); std::unique_ptr newPass(KPkPass::Pass::fromFile(url.toLocalFile())); if (!newPass) return; // TODO error handling if (newPass->passTypeIdentifier().isEmpty() || newPass->serialNumber().isEmpty()) return; // TODO error handling QDir dir(basePath); dir.mkdir(newPass->passTypeIdentifier()); dir.cd(newPass->passTypeIdentifier()); // serialNumber() can contain percent-encoding or slashes, // ie stuff we don't want to have in file names const auto serNum = QString::fromUtf8(newPass->serialNumber().toUtf8().toBase64(QByteArray::Base64UrlEncoding)); const QString passId = dir.dirName() + QLatin1Char('/') + serNum; auto oldPass = pass(passId); if (oldPass) { QFile::remove(dir.absoluteFilePath(serNum + QLatin1String(".pkpass"))); m_passes.remove(passId); } switch (mode) { case Move: QFile::rename(url.toLocalFile(), dir.absoluteFilePath(serNum + QLatin1String(".pkpass"))); break; case Copy: QFile::copy(url.toLocalFile(), dir.absoluteFilePath(serNum + QLatin1String(".pkpass"))); break; } if (oldPass) { // check for changes and generate change message QStringList changes; for (const auto &f : newPass->fields()) { const auto prevValue = oldPass->field(f.key()).value(); const auto curValue = f.value(); if (curValue != prevValue) { changes.push_back(f.changeMessage()); } } emit passUpdated(passId, changes); oldPass->deleteLater(); } else { emit passAdded(passId); } } void PkPassManager::removePass(const QString& passId) { - const auto basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes/"); + const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes/"); QFile::remove(basePath + QLatin1Char('/') + passId + QLatin1String(".pkpass")); emit passRemoved(passId); delete m_passes.take(passId); } void PkPassManager::updatePass(const QString& passId) { auto p = pass(passId); if (!p || p->webServiceUrl().isEmpty() || p->authenticationToken().isEmpty()) return; if (relevantDate(p) < QDateTime::currentDateTimeUtc()) // TODO check expiration date and voided property return; QNetworkRequest req(p->passUpdateUrl()); req.setRawHeader("Authorization", "ApplePass " + p->authenticationToken().toUtf8()); req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); auto reply = m_nam->get(req); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() != QNetworkReply::NoError) { qCWarning(Log) << "Failed to download pass:" << reply->errorString(); return; } QTemporaryFile tmp; tmp.open(); tmp.write(reply->readAll()); tmp.close(); importPassFromTempFile(tmp.fileName()); }); } void PkPassManager::updatePasses() { for (const auto &passId : passes()) updatePass(passId); } QDateTime PkPassManager::relevantDate(KPkPass::Pass *pass) { const auto dt = pass->relevantDate(); if (dt.isValid()) return dt; return pass->expirationDate(); } diff --git a/src/app/reservationmanager.cpp b/src/app/reservationmanager.cpp index 10acc1a..be90d4e 100644 --- a/src/app/reservationmanager.cpp +++ b/src/app/reservationmanager.cpp @@ -1,219 +1,219 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "reservationmanager.h" #include "pkpassmanager.h" #include "logging.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; ReservationManager::ReservationManager(QObject* parent) : QObject(parent) { } ReservationManager::~ReservationManager() = default; void ReservationManager::setPkPassManager(PkPassManager* mgr) { m_passMgr = mgr; connect(mgr, &PkPassManager::passAdded, this, &ReservationManager::passAdded); connect(mgr, &PkPassManager::passUpdated, this, &ReservationManager::passUpdated); connect(mgr, &PkPassManager::passRemoved, this, &ReservationManager::passRemoved); } QVector ReservationManager::reservations() const { - const auto basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations"); + const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations"); QDir::root().mkpath(basePath); QVector resIds; for (QDirIterator it(basePath, QDir::NoDotAndDotDot | QDir::Files); it.hasNext();) { it.next(); resIds.push_back(it.fileInfo().baseName()); } return resIds; } QVariant ReservationManager::reservation(const QString& id) const { if (id.isEmpty()) { return {}; } const auto it = m_reservations.constFind(id); if (it != m_reservations.constEnd()) { return it.value(); } const QString resPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations/") + id + QLatin1String(".jsonld"); QFile f(resPath); if (!f.open(QFile::ReadOnly)) { qCWarning(Log) << "Failed to open JSON-LD reservation data file:" << resPath << f.errorString(); return {}; } const auto doc = QJsonDocument::fromJson(f.readAll()); if (!doc.isArray() && doc.array().size() != 1) { qCWarning(Log) << "Invalid JSON-LD reservation data file:" << resPath; return {}; } const auto resData = JsonLdDocument::fromJson(doc.array()); if (resData.size() != 1) { qCWarning(Log) << "Unable to parse JSON-LD reservation data file:" << resPath; return {}; } m_reservations.insert(id, resData.at(0)); return resData.at(0); } void ReservationManager::importReservation(const QUrl& filename) { if (!filename.isLocalFile()) return; QFile f(filename.toLocalFile()); if (!f.open(QFile::ReadOnly)) { qCWarning(Log) << "Unable to open file:" << f.errorString(); return; } const auto doc = QJsonDocument::fromJson(f.readAll()); if (!doc.isArray()) { qCWarning(Log) << "Invalid JSON format."; return; } const auto resData = JsonLdDocument::fromJson(doc.array()); importReservations(resData); } void ReservationManager::importReservations(const QVector &resData) { ExtractorPostprocessor postproc; postproc.setContextDate(QDateTime(QDate::currentDate(), QTime(0, 0))); postproc.process(resData); - const auto basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations/"); + const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations/"); QDir::root().mkpath(basePath); for (auto res : postproc.result()) { QString resId; bool oldResFound = false; // check if we know this one already, and update if that's the case for (const auto &oldResId : reservations()) { const auto oldRes = reservation(oldResId); if (MergeUtil::isSame(oldRes, res)) { res = JsonLdDocument::apply(oldRes, res); resId = oldResId; oldResFound = true; break; } } if (resId.isEmpty()) { resId = QUuid::createUuid().toString(); } const QString path = basePath + resId + QLatin1String(".jsonld"); QFile f(path); if (!f.open(QFile::WriteOnly)) { qCWarning(Log) << "Unable to create file:" << f.errorString(); continue; } f.write(QJsonDocument(JsonLdDocument::toJson({res})).toJson()); m_reservations.insert(resId, res); if (oldResFound) { emit reservationUpdated(resId); } else { emit reservationAdded(resId); } } } void ReservationManager::addReservation(const QVariant &res) { QString resId = QUuid::createUuid().toString(); - const auto basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations/"); + const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations/"); QDir::root().mkpath(basePath); const QString path = basePath + resId + QLatin1String(".jsonld"); QFile f(path); if (!f.open(QFile::WriteOnly)) { qCWarning(Log) << "Unable to create file:" << f.errorString(); return; } f.write(QJsonDocument(JsonLdDocument::toJson({res})).toJson()); m_reservations.insert(resId, res); emit reservationAdded(resId); } void ReservationManager::removeReservation(const QString& id) { - const auto basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations/"); + const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/reservations/"); QFile::remove(basePath + QLatin1Char('/') + id + QLatin1String(".jsonld")); emit reservationRemoved(id); m_reservations.remove(id); } void ReservationManager::passAdded(const QString& passId) { const auto pass = m_passMgr->pass(passId); const auto extractors = m_extractorRepo.extractorsForPass(pass); for (const auto &extractor : extractors) { ExtractorEngine engine; engine.setExtractor(extractor); engine.setPass(pass); const auto data = engine.extract(); const auto res = JsonLdDocument::fromJson(data); importReservations(res); } } void ReservationManager::passUpdated(const QString& passId) { passAdded(passId); } void ReservationManager::passRemoved(const QString& passId) { Q_UNUSED(passId); // TODO }