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"