File indexing completed on 2024-11-24 04:50:43

0001 // SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
0002 // SPDX-License-Identifier: GPL-3.0-or-later
0003 
0004 #include "contactimageprovider.h"
0005 
0006 #include <Akonadi/ContactSearchJob>
0007 #include <KIO/TransferJob>
0008 #include <QApplication>
0009 #include <QCryptographicHash>
0010 #include <QDir>
0011 #include <QDnsLookup>
0012 #include <QFileInfo>
0013 #include <QNetworkDiskCache>
0014 #include <QNetworkReply>
0015 #include <QStandardPaths>
0016 #include <QThread>
0017 
0018 #include <KLocalizedString>
0019 #include <kjob.h>
0020 #include <qobject.h>
0021 
0022 ContactImageProvider::ContactImageProvider()
0023     : QQuickAsyncImageProvider()
0024 {
0025     qnam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
0026 
0027     qnam.enableStrictTransportSecurityStore(true, QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1StringView("/hsts/"));
0028     qnam.setStrictTransportSecurityEnabled(true);
0029 
0030     auto namDiskCache = new QNetworkDiskCache(&qnam);
0031     namDiskCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1StringView("/nam/"));
0032     qnam.setCache(namDiskCache);
0033 }
0034 
0035 QQuickImageResponse *ContactImageProvider::requestImageResponse(const QString &email, const QSize &requestedSize)
0036 {
0037     return new ThumbnailResponse(email, requestedSize, &qnam);
0038 }
0039 
0040 ThumbnailResponse::ThumbnailResponse(QString email, QSize size, QNetworkAccessManager *qnam)
0041     : m_email(std::move(email))
0042     , requestedSize(size)
0043     , localFile(QStringLiteral("%1/contact_picture_provider/%2.png").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation), m_email))
0044     , m_qnam(qnam)
0045     , errorStr(QStringLiteral("Image request hasn't started"))
0046 {
0047     m_email = m_email.trimmed().toLower();
0048     QImage cachedImage;
0049     if (cachedImage.load(localFile)) {
0050         m_image = cachedImage;
0051         errorStr.clear();
0052         Q_EMIT finished();
0053         return;
0054     }
0055 
0056     // Execute a request on the main thread asynchronously
0057     moveToThread(QApplication::instance()->thread());
0058     QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, Qt::QueuedConnection);
0059 }
0060 
0061 void ThumbnailResponse::startRequest()
0062 {
0063     job = new Akonadi::ContactSearchJob();
0064     job->setQuery(Akonadi::ContactSearchJob::Email, m_email, Akonadi::ContactSearchJob::ExactMatch);
0065 
0066     // Runs in the main thread, not QML thread
0067     Q_ASSERT(QThread::currentThread() == QApplication::instance()->thread());
0068 
0069     // Connect to any possible outcome including abandonment
0070     // to make sure the QML thread is not left stuck forever.
0071     connect(job, &Akonadi::ContactSearchJob::finished, this, &ThumbnailResponse::prepareResult);
0072 }
0073 
0074 bool ThumbnailResponse::searchPhoto(const KContacts::AddresseeList &list)
0075 {
0076     bool foundPhoto = false;
0077     for (const KContacts::Addressee &addressee : list) {
0078         const KContacts::Picture photo = addressee.photo();
0079         if (!photo.isEmpty()) {
0080             m_photo = photo;
0081             foundPhoto = true;
0082             break;
0083         }
0084     }
0085     return foundPhoto;
0086 }
0087 
0088 void ThumbnailResponse::prepareResult()
0089 {
0090     Q_ASSERT(QThread::currentThread() == job->thread());
0091     auto searchJob = static_cast<Akonadi::ContactSearchJob *>(job);
0092     {
0093         QWriteLocker _(&lock);
0094         if (job->error() == KJob::NoError) {
0095             bool ok = false;
0096             const int contactSize(searchJob->contacts().size());
0097             if (contactSize >= 1) {
0098                 if (contactSize > 1) {
0099                     qWarning() << " more than 1 contact was found we return first contact";
0100                 }
0101 
0102                 const KContacts::Addressee addressee = searchJob->contacts().at(0);
0103                 if (searchPhoto(searchJob->contacts())) {
0104                     // We have a data raw => we can update message
0105                     if (m_photo.isIntern()) {
0106                         m_image = m_photo.data();
0107                         ok = true;
0108                     } else {
0109                         const QUrl url = QUrl::fromUserInput(m_photo.url(), QString(), QUrl::AssumeLocalFile);
0110                         if (!url.isEmpty()) {
0111                             if (url.isLocalFile()) {
0112                                 if (m_image.load(url.toLocalFile())) {
0113                                     ok = true;
0114                                 }
0115                             } else {
0116                                 QByteArray imageData;
0117                                 KIO::TransferJob *jobTransfert = KIO::get(url, KIO::NoReload);
0118                                 QObject::connect(jobTransfert, &KIO::TransferJob::data, this, [&imageData](KIO::Job *, const QByteArray &data) {
0119                                     imageData.append(data);
0120                                 });
0121                                 if (jobTransfert->exec()) {
0122                                     if (m_image.loadFromData(imageData)) {
0123                                         ok = true;
0124                                     }
0125                                 }
0126                             }
0127                         }
0128                     }
0129                 }
0130             }
0131             QString localPath = QFileInfo(localFile).absolutePath();
0132             QDir dir;
0133             if (!dir.exists(localPath)) {
0134                 dir.mkpath(localPath);
0135             }
0136 
0137             m_image.save(localFile);
0138 
0139             if (ok) {
0140                 errorStr.clear();
0141                 Q_EMIT finished();
0142                 return;
0143             } else {
0144                 errorStr = QStringLiteral("No image found");
0145             }
0146         } else if (job->error() == Akonadi::Job::UserCanceled) {
0147             errorStr = i18n("Image request has been cancelled");
0148         } else {
0149             errorStr = job->errorString();
0150         }
0151 
0152         // No image found in Akonadi, try libravatar
0153         auto dns = new QDnsLookup(this);
0154         connect(dns, &QDnsLookup::finished, this, [this, dns]() {
0155             dnsLookupFinished(dns);
0156         });
0157         const auto split = m_email.split(QLatin1Char('@'));
0158         if (split.length() < 2) {
0159             Q_EMIT finished();
0160             return;
0161         }
0162         const auto domain = split[1];
0163 
0164         dns->setType(QDnsLookup::SRV);
0165         dns->setName(QStringLiteral("_avatars._tcp.") + domain);
0166         dns->lookup();
0167         job = nullptr;
0168     }
0169 }
0170 
0171 void ThumbnailResponse::dnsLookupFinished(QDnsLookup *dns)
0172 {
0173     if (dns->error() != QDnsLookup::NoError) {
0174         queryImage();
0175         dns->deleteLater();
0176         return;
0177     }
0178 
0179     const auto records = dns->serviceRecords();
0180     if (records.count() < 1) {
0181         queryImage();
0182         dns->deleteLater();
0183         return;
0184     }
0185 
0186     const auto record = records[0];
0187 
0188     QString hostname = record.target();
0189     if (hostname.endsWith(QLatin1Char('.'))) {
0190         hostname.chop(1);
0191     }
0192 
0193     if (record.port() == 443) {
0194         queryImage(QStringLiteral("https://") + hostname + QStringLiteral("/avatar/"));
0195     } else {
0196         queryImage(QStringLiteral("http://") + hostname + QLatin1Char(':') + QString::number(record.port()) + QStringLiteral("/avatar/"));
0197     }
0198 
0199     dns->deleteLater();
0200 }
0201 
0202 void ThumbnailResponse::queryImage(const QString &hostname)
0203 {
0204     QCryptographicHash hash(QCryptographicHash::Md5);
0205     hash.addData(m_email.toUtf8());
0206 
0207     const QUrl url(hostname + QString::fromUtf8(hash.result().toHex()) + QStringLiteral("?d=404"));
0208 
0209     QByteArray imageData;
0210     auto reply = m_qnam->get(QNetworkRequest(url));
0211     connect(reply, &QNetworkReply::finished, this, [this, reply]() {
0212         imageQueried(reply);
0213     });
0214 }
0215 
0216 void ThumbnailResponse::imageQueried(QNetworkReply *reply)
0217 {
0218     reply->deleteLater();
0219     if (reply->error() != QNetworkReply::NoError) {
0220         Q_EMIT finished();
0221         return;
0222     }
0223 
0224     const QByteArray imageData = reply->readAll();
0225     if (m_image.loadFromData(imageData)) {
0226         QString localPath = QFileInfo(localFile).absolutePath();
0227         QDir dir;
0228         if (!dir.exists(localPath)) {
0229             dir.mkpath(localPath);
0230         }
0231 
0232         m_image.save(localFile);
0233     }
0234 
0235     Q_EMIT finished();
0236 }
0237 
0238 void ThumbnailResponse::doCancel()
0239 {
0240     // Runs in the main thread, not QML thread
0241     if (job) {
0242         Q_ASSERT(QThread::currentThread() == job->thread());
0243         job->kill();
0244     }
0245 }
0246 
0247 QQuickTextureFactory *ThumbnailResponse::textureFactory() const
0248 {
0249     QReadLocker _(&lock);
0250     return QQuickTextureFactory::textureFactoryForImage(m_image);
0251 }
0252 
0253 QString ThumbnailResponse::errorString() const
0254 {
0255     QReadLocker _(&lock);
0256     return errorStr;
0257 }
0258 
0259 void ThumbnailResponse::cancel()
0260 {
0261     QMetaObject::invokeMethod(this, &ThumbnailResponse::doCancel, Qt::QueuedConnection);
0262 }
0263 
0264 #include "moc_contactimageprovider.cpp"