KNSBackend.cpp 20 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/***************************************************************************
 *   Copyright © 2012 Aleix Pol Gonzalez <aleixpol@blue-systems.com>       *
 *                                                                         *
 *   This program 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 2 of        *
 *   the License or (at your option) version 3 or any later version        *
 *   accepted by the membership of KDE e.V. (or its successor approved     *
 *   by the membership of KDE e.V.), which shall act as a proxy            *
 *   defined in Section 14 of version 3 of the license.                    *
 *                                                                         *
 *   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 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 <http://www.gnu.org/licenses/>. *
 ***************************************************************************/

21
// Qt includes
22
#include <QDebug>
23
#include <QDir>
24
#include <QFileInfo>
25
#include <QStandardPaths>
26
#include <QTimer>
27
#include <QDirIterator>
28

29
// Attica includes
David Faure's avatar
David Faure committed
30 31
#include <Attica/Content>
#include <Attica/ProviderManager>
32

33
// KDE includes
34
#include <knewstuffcore_version.h>
35
#include <KNSCore/Engine>
36
#include <KNSCore/QuestionManager>
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
37
#include <KConfig>
38
#include <KConfigGroup>
39 40
#include <KLocalizedString>

41
// DiscoverCommon includes
42
#include "Transaction/Transaction.h"
43
#include "Transaction/TransactionModel.h"
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
44
#include "Category/Category.h"
45 46 47 48 49

// Own includes
#include "KNSBackend.h"
#include "KNSResource.h"
#include "KNSReviews.h"
50
#include <resources/StandardBackendUpdater.h>
51
#include "utils.h"
52

53 54 55 56 57
class KNSBackendFactory : public AbstractResourcesBackendFactory {
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.kde.muon.AbstractResourcesBackendFactory")
    Q_INTERFACES(AbstractResourcesBackendFactory)
    public:
58 59 60 61 62 63 64
        KNSBackendFactory() {
            connect(KNSCore::QuestionManager::instance(), &KNSCore::QuestionManager::askQuestion, this, [](KNSCore::Question* q) {
                qWarning() << q->question() << q->questionType();
                q->setResponse(KNSCore::Question::InvalidResponse);
            });
        }

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
65 66
        QVector<AbstractResourcesBackend*> newInstance(QObject* parent, const QString &/*name*/) const override
        {
67 68 69 70 71 72
            QVector<AbstractResourcesBackend*> ret;
            for (const QString &path: QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation)) {
                QDirIterator dirIt(path, {QStringLiteral("*.knsrc")}, QDir::Files);
                for(; dirIt.hasNext(); ) {
                    dirIt.next();

73
                    auto bk = new KNSBackend(parent, QStringLiteral("plasma"), dirIt.filePath());
74 75 76 77
                    if (bk->isValid())
                        ret += bk;
                    else
                        delete bk;
78 79 80 81 82
                }
            }
            return ret;
        }
};
83

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
84
Q_DECLARE_METATYPE(KNSCore::EntryInternal)
85

86
KNSBackend::KNSBackend(QObject* parent, const QString& iconName, const QString &knsrc)
87
    : AbstractResourcesBackend(parent)
88
    , m_fetching(false)
89
    , m_isValid(true)
90
    , m_reviews(new KNSReviews(this))
91 92
    , m_name(knsrc)
    , m_iconName(iconName)
93
    , m_updater(new StandardBackendUpdater(this))
94
{
95 96
    const QString fileName = QFileInfo(m_name).fileName();
    setName(fileName);
97
    setObjectName(knsrc);
98

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
99 100
    const KConfig conf(m_name);
    if (!conf.hasGroup("KNewStuff3")) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
101
        markInvalid(QStringLiteral("Config group not found! Check your KNS3 installation."));
102
        return;
103
    }
104

105
    m_categories = QStringList{ fileName };
106

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
107
    const KConfigGroup group = conf.group("KNewStuff3");
108
    m_extends = group.readEntry("Extends", QStringList());
109
    m_reviews->setProviderUrl(QUrl(group.readEntry("ProvidersUrl", QString())));
110

111
    setFetching(true);
112

113 114
    m_engine = new KNSCore::Engine(this);
    m_engine->init(m_name);
115
    m_engine->setPageSize(100);
116
    connect(m_engine, &KNSCore::Engine::signalErrorCode, this, &KNSBackend::signalErrorCode);
117
    connect(m_engine, &KNSCore::Engine::signalEntriesLoaded, this, &KNSBackend::receivedEntries, Qt::QueuedConnection);
118
    connect(m_engine, &KNSCore::Engine::signalEntryChanged, this, &KNSBackend::statusChanged, Qt::QueuedConnection);
119
    connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, this, &KNSBackend::statusChanged);
120
    connect(m_engine, &KNSCore::Engine::signalProvidersLoaded, this, &KNSBackend::fetchInstalled);
121

122 123 124 125 126 127 128 129 130 131 132 133
    // This ensures we have something to track when checking after the initialization timeout
    connect(this, &KNSBackend::initialized, this, [this](){ m_initialized = true; });
    // If we have not initialized in 60 seconds, consider this KNS backend invalid
    QTimer::singleShot(60000, this, [this](){
        if(!m_initialized) {
            markInvalid(i18n("Backend %1 took too long to initialize", m_displayName));
            m_responsePending = false;
            Q_EMIT searchFinished();
            Q_EMIT availableForQueries();
        }
    });

134
    const QVector<QPair<FilterType, QString>> filters = { {CategoryFilter, fileName } };
135
    const QSet<QString> backendName = { name() };
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
136 137 138 139
    m_displayName = group.readEntry("Name", QString());
    if (m_displayName.isEmpty()) {
        m_displayName = fileName.mid(0, fileName.indexOf(QLatin1Char('.')));
        m_displayName[0] = m_displayName[0].toUpper();
140
    }
141 142 143 144 145 146

    static const QSet<QString> knsrcPlasma = {
        QStringLiteral("aurorae.knsrc"), QStringLiteral("icons.knsrc"), QStringLiteral("kfontinst.knsrc"), QStringLiteral("lookandfeel.knsrc"), QStringLiteral("plasma-themes.knsrc"), QStringLiteral("plasmoids.knsrc"),
        QStringLiteral("wallpaper.knsrc"), QStringLiteral("xcursor.knsrc"),

        QStringLiteral("cgcgtk3.knsrc"), QStringLiteral("cgcicon.knsrc"), QStringLiteral("cgctheme.knsrc"), //GTK integration
147 148 149 150
        QStringLiteral("kwinswitcher.knsrc"), QStringLiteral("kwineffect.knsrc"), QStringLiteral("kwinscripts.knsrc"), //KWin
        QStringLiteral("comic.knsrc"), QStringLiteral("colorschemes.knsrc"),
        QStringLiteral("emoticons.knsrc"), QStringLiteral("plymouth.knsrc"),
        QStringLiteral("sddmtheme.knsrc")
151
    };
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
152
    auto actualCategory = new Category(m_displayName, QStringLiteral("plasma"), filters, backendName, {}, QUrl(), true);
153 154 155

    const auto topLevelName = knsrcPlasma.contains(fileName)? i18n("Plasma Addons") : i18n("Application Addons");
    const QUrl decoration(knsrcPlasma.contains(fileName)? QStringLiteral("https://c2.staticflickr.com/4/3148/3042248532_20bd2e38f4_b.jpg") : QStringLiteral("https://c2.staticflickr.com/8/7067/6847903539_d9324dcd19_o.jpg"));
156
    auto addonsCategory = new Category(topLevelName, QStringLiteral("plasma"), filters, backendName, {actualCategory}, decoration, true);
157
    m_rootCategories = { addonsCategory };
158 159
}

160 161 162 163
KNSBackend::~KNSBackend()
{
    qDeleteAll(m_rootCategories);
}
164 165 166

void KNSBackend::markInvalid(const QString &message)
{
167
    m_rootCategories.clear();
168 169 170
    qWarning() << "invalid kns backend!" << m_name << "because:" << message;
    m_isValid = false;
    setFetching(false);
171
    Q_EMIT initialized();
172 173
}

174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
void KNSBackend::fetchInstalled()
{
    auto search = new OneTimeAction([this]() {
        Q_EMIT startingSearch();
        m_onePage = true;
        m_responsePending = true;
        m_engine->checkForInstalled();
    }, this);

    if (m_responsePending) {
        connect(this, &KNSBackend::availableForQueries, search, &OneTimeAction::trigger, Qt::QueuedConnection);
    } else {
        search->trigger();
    }
}

190
void KNSBackend::setFetching(bool f)
191
{
192 193 194
    if(m_fetching!=f) {
        m_fetching = f;
        emit fetchingChanged();
195 196 197 198

        if (!m_fetching) {
            Q_EMIT initialized();
        }
199
    }
200

201 202
}

203 204 205 206 207
bool KNSBackend::isValid() const
{
    return m_isValid;
}

208 209 210 211 212 213 214 215 216 217 218 219 220
KNSResource* KNSBackend::resourceForEntry(const KNSCore::EntryInternal& entry)
{
    KNSResource* r = static_cast<KNSResource*>(m_resourcesByName.value(entry.uniqueId()));
    if (!r) {
        r = new KNSResource(entry, m_categories, this);
        m_resourcesByName.insert(entry.uniqueId(), r);
    } else {
        r->setEntry(entry);
    }
    return r;
}

void KNSBackend::receivedEntries(const KNSCore::EntryInternal::List& entries)
221
{
222 223
    m_responsePending = false;

224 225
    const auto filtered = kFilter<KNSCore::EntryInternal::List>(entries, [this](const KNSCore::EntryInternal& entry){ return entry.isValid(); });
    const auto resources = kTransform<QVector<AbstractResource*>>(filtered, [this](const KNSCore::EntryInternal& entry){ return resourceForEntry(entry); });
226 227
    if (!resources.isEmpty()) {
        Q_EMIT receivedResources(resources);
228
    } else {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
229
        Q_EMIT searchFinished();
230
        Q_EMIT availableForQueries();
231
        setFetching(false);
232 233
        return;
    }
234
//     qDebug() << "received" << objectName() << this << m_resourcesByName.count();
235
    if (m_onePage) {
236
        Q_EMIT availableForQueries();
237
        setFetching(false);
238
    }
239 240
}

241 242 243 244 245 246 247 248 249 250 251 252
void KNSBackend::fetchMore()
{
    if (m_responsePending)
        return;

    // We _have_ to set this first. If we do not, we may run into a situation where the
    // data request will conclude immediately, causing m_responsePending to remain true
    // for perpetuity as the slots will be called before the function returns.
    m_responsePending = true;
    m_engine->requestMoreData();
}

253
void KNSBackend::statusChanged(const KNSCore::EntryInternal& entry)
254
{
255
    resourceForEntry(entry);
256 257
}

258 259 260
void KNSBackend::signalErrorCode(const KNSCore::ErrorCode& errorCode, const QString& message, const QVariant& metadata)
{
    QString error = message;
261
    qDebug() << "KNS error in" << m_displayName << ":" << errorCode << message << metadata;
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
    bool invalidFile = false;
    switch(errorCode) {
        case KNSCore::ErrorCode::UnknownError:
            // This is not supposed to be hit, of course, but any error coming to this point should be non-critical and safely ignored.
            break;
        case KNSCore::ErrorCode::NetworkError:
            // If we have a network error, we need to tell the user about it. This is almost always fatal, so mark invalid and tell the user.
            error = i18n("Network error in backend %1: %2", m_displayName, metadata.toInt());
            markInvalid(error);
            invalidFile = true;
            break;
        case KNSCore::ErrorCode::OcsError:
            if(metadata.toInt() == 200) {
                // Too many requests, try again in a couple of minutes - perhaps we can simply postpone it automatically, and give a message?
                error = i18n("Too many requests sent to the server for backend %1. Please try again in a few minutes.", m_displayName);
            } else {
                // Unknown API error, usually something critical, mark as invalid and cry a lot
                error = i18n("Invalid %1 backend, contact your distributor.", m_displayName);
                markInvalid(error);
                invalidFile = true;
            }
            break;
        case KNSCore::ErrorCode::ConfigFileError:
            error = i18n("Invalid %1 backend, contact your distributor.", m_displayName);
            markInvalid(error);
            invalidFile = true;
            break;
        case KNSCore::ErrorCode::ProviderError:
            error = i18n("Invalid %1 backend, contact your distributor.", m_displayName);
            markInvalid(error);
            invalidFile = true;
            break;
        case KNSCore::ErrorCode::InstallationError:
            // This error is handled already, by forwarding the KNS engine's installer error message.
            break;
        case KNSCore::ErrorCode::ImageError:
            // Image fetching errors are not critical as such, but may lead to weird layout issues, might want handling...
            error = i18n("Could not fetch screenshot for the entry %1 in backend %2", metadata.toList().at(0).toString(), m_displayName);
            break;
        default:
            // Having handled all current error values, we should by all rights never arrive here, but for good order and future safety...
            error = i18n("Unhandled error in %1 backend. Contact your distributor.", m_displayName);
            break;
    }
    m_responsePending = false;
    Q_EMIT searchFinished();
    Q_EMIT availableForQueries();
    // Setting setFetching to false when we get an error ensures we don't end up in an eternally-fetching state
    this->setFetching(false);
    qWarning() << "kns error" << objectName() << error;
    if (!invalidFile)
        Q_EMIT passiveMessage(i18n("%1: %2", name(), error));
}

316
class KNSTransaction : public Transaction
317
{
318
public:
319
    KNSTransaction(QObject* parent, KNSResource* res, Transaction::Role role)
320
        : Transaction(parent, res, role)
321
        , m_id(res->entry().uniqueId())
322 323
    {
        setCancellable(false);
324

325
        auto manager = res->knsBackend()->engine();
326
        connect(manager, &KNSCore::Engine::signalEntryChanged, this, &KNSTransaction::anEntryChanged);
327
        TransactionModel::global()->addTransaction(this);
328

329 330 331 332 333 334 335 336 337 338 339 340 341
        std::function<void()> actionFunction;
        auto engine = res->knsBackend()->engine();
        if(role == RemoveRole)
            actionFunction = [res, engine]() {
                engine->uninstall(res->entry());
            };
        else if (res->linkIds().isEmpty())
            actionFunction = [res, engine]() {
                engine->install(res->entry());
            };
        else
            actionFunction = [res, engine]() {
                for(auto i : res->linkIds())
342
                    engine->install(res->entry(), i);
343 344
            };
        QTimer::singleShot(0, res, actionFunction);
345 346
    }

347 348
    void anEntryChanged(const KNSCore::EntryInternal& entry) {
        if (entry.uniqueId() == m_id) {
349 350
            switch (entry.status()) {
                case KNS3::Entry::Invalid:
351
                    qWarning() << "invalid status for" << entry.uniqueId() << entry.status();
352 353 354 355 356 357 358 359 360
                    break;
                case KNS3::Entry::Installing:
                case KNS3::Entry::Updating:
                    setStatus(CommittingStatus);
                    break;
                case KNS3::Entry::Downloadable:
                case KNS3::Entry::Installed:
                case KNS3::Entry::Deleted:
                case KNS3::Entry::Updateable:
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
361 362 363
                    if (status() != DoneStatus) {
                        setStatus(DoneStatus);
                    }
364 365 366
                    break;
            }
        }
367
    }
Jonathan Thomas's avatar
Jonathan Thomas committed
368

369
    void cancel() override {}
370 371 372

private:
    const QString m_id;
373
};
374

375
Transaction* KNSBackend::removeApplication(AbstractResource* app)
376
{
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
377
    auto res = qobject_cast<KNSResource*>(app);
378
    return new KNSTransaction(this, res, Transaction::RemoveRole);
379 380
}

381
Transaction* KNSBackend::installApplication(AbstractResource* app)
382
{
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
383
    auto res = qobject_cast<KNSResource*>(app);
384
    return new KNSTransaction(this, res, Transaction::InstallRole);
385 386
}

387
Transaction* KNSBackend::installApplication(AbstractResource* app, const AddonList& /*addons*/)
388
{
389
    return installApplication(app);
390 391 392 393
}

int KNSBackend::updatesCount() const
{
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
394
    return m_updater->updatesCount();
395 396
}

397 398 399 400 401
AbstractReviewsBackend* KNSBackend::reviewsBackend() const
{
    return m_reviews;
}

402 403 404 405 406
static ResultsStream* voidStream()
{
    return new ResultsStream(QStringLiteral("KNS-void"), {});
}

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
407
ResultsStream* KNSBackend::search(const AbstractResourcesBackend::Filters& filter)
408
{
409
    if (!m_isValid || (!filter.resourceUrl.isEmpty() && filter.resourceUrl.scheme() != QLatin1String("kns")) || !filter.mimetype.isEmpty())
410 411 412 413 414
        return voidStream();

    if (filter.resourceUrl.scheme() == QLatin1String("kns")) {
        return findResourceByPackageName(filter.resourceUrl);
    } else if (filter.state >= AbstractResource::Installed) {
415 416 417 418 419 420 421 422
        auto stream = new ResultsStream(QStringLiteral("KNS-installed"));

        const auto start = [this, stream, filter]() {
            if (m_isValid) {
                auto filterFunction = [&filter](AbstractResource* r) { return r->state()>=filter.state && (r->name().contains(filter.search, Qt::CaseInsensitive) || r->comment().contains(filter.search, Qt::CaseInsensitive)); };
                const auto ret = kFilter<QVector<AbstractResource*>>(m_resourcesByName, filterFunction);

                if (!ret.isEmpty())
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
423
                    Q_EMIT stream->resourcesFound(ret);
424 425 426 427 428 429 430
            }
            stream->finish();
        };
        if (isFetching()) {
            connect(this, &KNSBackend::initialized, stream, start);
        } else {
            QTimer::singleShot(0, stream, start);
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
431
        }
432 433

        return stream;
434
    } else if (filter.category && filter.category->matchesCategoryName(m_categories.constFirst())) {
435 436 437
        auto r = new ResultsStream(QStringLiteral("KNS-search-")+name());
        searchStream(r, filter.search);
        return r;
438
    }
439
    return voidStream();
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
440 441
}

442
void KNSBackend::searchStream(ResultsStream* stream, const QString &searchText)
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
443
{
444 445
    Q_EMIT startingSearch();

446
    auto start = [this, stream, searchText]() {
447 448 449 450 451
        Q_ASSERT(!isFetching());
        if (!m_isValid) {
            stream->finish();
            return;
        }
452
        // No need to explicitly launch a search, setting the search term already does that for us
453
        m_engine->setSearchTerm(searchText);
454
        m_onePage = false;
455
        m_responsePending = true;
456

457
        connect(stream, &ResultsStream::fetchMore, this, &KNSBackend::fetchMore);
458 459 460
        connect(this, &KNSBackend::receivedResources, stream, &ResultsStream::resourcesFound);
        connect(this, &KNSBackend::searchFinished, stream, &ResultsStream::finish);
        connect(this, &KNSBackend::startingSearch, stream, &ResultsStream::finish);
461
    };
462

463
    if (m_responsePending) {
464
        connect(this, &KNSBackend::availableForQueries, stream, start, Qt::QueuedConnection);
465 466
    } else if (isFetching()) {
        connect(this, &KNSBackend::initialized, stream, start);
467
    } else {
468
        QTimer::singleShot(0, stream, start);
469
    }
470 471
}

472
ResultsStream * KNSBackend::findResourceByPackageName(const QUrl& search)
473
{
474 475 476 477 478
    if (search.scheme() != QLatin1String("kns") || search.host() != name())
        return voidStream();

    const auto pathParts = search.path().split(QLatin1Char('/'), QString::SkipEmptyParts);
    if (pathParts.size() != 2) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
479
        Q_EMIT passiveMessage(i18n("Wrong KNewStuff URI: %1", search.toString()));
480 481 482 483 484 485
        return voidStream();
    }
    const auto providerid = pathParts.at(0);
    const auto entryid = pathParts.at(1);

    auto stream = new ResultsStream(QStringLiteral("KNS-byname-")+entryid);
486
    auto start = [this, entryid, stream, providerid]() {
487
        m_responsePending = true;
488
        m_engine->fetchEntryById(entryid);
489
        m_onePage = false;
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
490
        connect(m_engine, &KNSCore::Engine::signalError, stream, &ResultsStream::finish);
491 492
        connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, stream, [this, stream, entryid, providerid](const KNSCore::EntryInternal &entry) {
            if (entry.uniqueId() == entryid && providerid == QUrl(entry.providerId()).host()) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
493
                Q_EMIT stream->resourcesFound({resourceForEntry(entry)});
494 495
            } else
                qWarning() << "found invalid" << entryid << entry.uniqueId() << providerid << QUrl(entry.providerId()).host();
496 497
            m_responsePending = false;
            QTimer::singleShot(0, this, &KNSBackend::availableForQueries);
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
498
            stream->finish();
499
        });
500 501 502 503 504 505 506
    };
    if (m_responsePending) {
        connect(this, &KNSBackend::availableForQueries, stream, start);
    } else {
        start();
    }
    return stream;
507
}
508 509 510 511 512

bool KNSBackend::isFetching() const
{
    return m_fetching;
}
513 514 515 516 517

AbstractBackendUpdater* KNSBackend::backendUpdater() const
{
    return m_updater;
}
518

519 520 521 522 523
QString KNSBackend::displayName() const
{
    return QStringLiteral("KNewStuff");
}

524
#include "KNSBackend.moc"