File indexing completed on 2025-01-19 03:53:11

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2011-04-12
0007  * Description : A tool to export items to Rajce web service
0008  *
0009  * SPDX-FileCopyrightText: 2011      by Lukas Krejci <krejci.l at centrum dot cz>
0010  * SPDX-FileCopyrightText: 2011-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "rajcecommand.h"
0017 
0018 // Qt includes
0019 
0020 #include <QWidget>
0021 #include <QMutex>
0022 #include <QQueue>
0023 #include <QCryptographicHash>
0024 #include <QXmlQuery>
0025 #include <QXmlResultItems>
0026 #include <QFileInfo>
0027 #include <QUrl>
0028 #include <QRandomGenerator>
0029 
0030 // Local includes
0031 
0032 #include "digikam_debug.h"
0033 #include "digikam_version.h"
0034 #include "dmetadata.h"
0035 #include "wstoolutils.h"
0036 #include "previewloadthread.h"
0037 
0038 using namespace Digikam;
0039 
0040 namespace DigikamGenericRajcePlugin
0041 {
0042 
0043 const unsigned THUMB_SIZE = 100;
0044 
0045 struct PreparedImage
0046 {
0047     QString scaledImagePath;
0048     QString thumbPath;
0049 };
0050 
0051 PreparedImage s_prepareImageForUpload(const QString& saveDir,
0052                                       const QImage& img,
0053                                       const QString& imagePath,
0054                                       unsigned maxDimension,
0055                                       unsigned thumbDimension,
0056                                       int      jpgQuality)
0057 {
0058     PreparedImage ret;
0059 
0060     if (img.isNull())
0061     {
0062         return ret;
0063     }
0064 
0065     QImage image(img);
0066 
0067     // get temporary file name
0068     QString baseName    = saveDir  + QFileInfo(imagePath).baseName().trimmed();
0069     ret.scaledImagePath = baseName + QLatin1String(".jpg");
0070     ret.thumbPath       = baseName + QLatin1String(".thumb.jpg");
0071 
0072     if ((maxDimension > 0) &&
0073         (((unsigned)image.width()  > maxDimension) ||
0074          ((unsigned)image.height() > maxDimension)))
0075     {
0076         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Resizing to " << maxDimension;
0077         image = image.scaled(maxDimension, maxDimension, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0078     }
0079 
0080     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Saving to temp file: " << ret.scaledImagePath;
0081     image.save(ret.scaledImagePath, "JPEG", jpgQuality);
0082 
0083     QImage thumb = image.scaled(thumbDimension, thumbDimension, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0084     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Saving thumb to temp file: " << ret.thumbPath;
0085     thumb.save(ret.thumbPath, "JPEG", jpgQuality);
0086 
0087     // copy meta data to temporary image
0088 
0089     QScopedPointer<DMetadata> meta(new DMetadata);
0090 
0091     if (meta->load(imagePath))
0092     {
0093         meta->setItemDimensions(image.size());
0094         meta->setItemOrientation(MetaEngine::ORIENTATION_NORMAL);
0095         meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY);
0096         meta->save(ret.scaledImagePath, true);
0097     }
0098 
0099     return ret;
0100 }
0101 
0102 // -----------------------------------------------------------------------
0103 
0104 class Q_DECL_HIDDEN RajceCommand::Private
0105 {
0106 public:
0107 
0108     explicit Private()
0109     {
0110         commandType = Logout;
0111     }
0112 
0113     QString                name;
0114     RajceCommandType       commandType;
0115     QMap<QString, QString> parameters;
0116 };
0117 
0118 RajceCommand::RajceCommand(const QString& name, RajceCommandType type)
0119     : QObject(nullptr),
0120       d(new Private)
0121 {
0122     d->name        = name;
0123     d->commandType = type;
0124 }
0125 
0126 RajceCommand::~RajceCommand()
0127 {
0128     delete d;
0129 }
0130 
0131 QMap<QString, QString>& RajceCommand::parameters() const
0132 {
0133     return const_cast<QMap<QString, QString> &>(d->parameters);
0134 }
0135 
0136 QString RajceCommand::getXml() const
0137 {
0138     QString ret(QLatin1String("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"));
0139 
0140     ret.append(QLatin1String("<request>\n"));
0141     ret.append(QLatin1String("  <command>")).append(d->name).append(QLatin1String("</command>\n"));
0142     ret.append(QLatin1String("  <parameters>\n"));
0143 
0144     Q_FOREACH (QString key, d->parameters.keys())
0145     {
0146         ret.append(QLatin1String("    <")).append(key).append(QLatin1String(">"));
0147         ret.append(d->parameters[key]);
0148         ret.append(QLatin1String("</")).append(key).append(QLatin1String(">\n"));
0149     }
0150 
0151     ret.append(QLatin1String("</parameters>\n"));
0152     ret.append(additionalXml());
0153     ret.append(QLatin1String("\n</request>\n"));
0154 
0155     return ret;
0156 }
0157 
0158 bool RajceCommand::parseErrorFromQuery(QXmlQuery& query, RajceSession& state)
0159 {
0160     QString results;
0161 
0162     query.setQuery(QLatin1String("/response/string(errorCode)"));
0163     query.evaluateTo(&results);
0164 
0165     if (results.trimmed().length() > 0)
0166     {
0167         state.lastErrorCode()    = results.toUInt();
0168         query.setQuery(QLatin1String("/response/string(result)"));
0169         query.evaluateTo(&results);
0170         state.lastErrorMessage() = results.trimmed();
0171 
0172         return true;
0173     }
0174 
0175     return false;
0176 }
0177 
0178 void RajceCommand::processResponse(const QString& response, RajceSession& state)
0179 {
0180     QXmlQuery q;
0181     q.setFocus(response);
0182 
0183     state.lastCommand() = d->commandType;
0184 
0185     if (parseErrorFromQuery(q, state))
0186     {
0187         cleanUpOnError(state);
0188     }
0189     else
0190     {
0191         parseResponse(q, state);
0192     }
0193 }
0194 
0195 QString RajceCommand::additionalXml() const
0196 {
0197     return QString();
0198 }
0199 
0200 QByteArray RajceCommand::encode() const
0201 {
0202     QByteArray ret = QString::fromLatin1("data=").toLatin1();
0203     ret.append(QUrl::toPercentEncoding(getXml()));
0204 
0205     return ret;
0206 }
0207 
0208 QString RajceCommand::contentType() const
0209 {
0210     return QLatin1String("application/x-www-form-urlencoded");
0211 }
0212 
0213 RajceCommandType RajceCommand::commandType() const
0214 {
0215     return d->commandType;
0216 }
0217 
0218 // -----------------------------------------------------------------------
0219 
0220 OpenAlbumCommand::OpenAlbumCommand(unsigned albumId, const RajceSession& state)
0221     : RajceCommand(QLatin1String("openAlbum"), OpenAlbum)
0222 {
0223     parameters()[QLatin1String("token")]   = state.sessionToken();
0224     parameters()[QLatin1String("albumID")] = QString::number(albumId);
0225 }
0226 
0227 void OpenAlbumCommand::parseResponse(QXmlQuery& q, RajceSession& state)
0228 {
0229     state.openAlbumToken() = QString();
0230 
0231     QString result;
0232 
0233     q.setQuery(QLatin1String("/response/data(albumToken)"));
0234     q.evaluateTo(&result);
0235 
0236     state.openAlbumToken() = result.trimmed();
0237 }
0238 
0239 void OpenAlbumCommand::cleanUpOnError(RajceSession& state)
0240 {
0241     state.openAlbumToken() = QString();
0242 }
0243 
0244 // -----------------------------------------------------------------------
0245 
0246 LoginCommand::LoginCommand(const QString& username, const QString& password)
0247     : RajceCommand(QLatin1String("login"), Login)
0248 {
0249     parameters()[QLatin1String("login")]    = username;
0250     parameters()[QLatin1String("password")] = QLatin1String(
0251         QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Md5).toHex());
0252 }
0253 
0254 void LoginCommand::parseResponse(QXmlQuery& q, RajceSession& state)
0255 {
0256     QString results;
0257 
0258     q.setQuery(QLatin1String("/response/string(maxWidth)"));
0259     q.evaluateTo(&results);
0260     state.maxWidth()     = results.toUInt();
0261 
0262     q.setQuery(QLatin1String("/response/string(maxHeight)"));
0263     q.evaluateTo(&results);
0264     state.maxHeight()    = results.toUInt();
0265 
0266     q.setQuery(QLatin1String("/response/string(quality)"));
0267     q.evaluateTo(&results);
0268     state.imageQuality() = results.toUInt();
0269 
0270     q.setQuery(QLatin1String("/response/string(nick)"));
0271     q.evaluateTo(&results);
0272     state.nickname()     = results.trimmed();
0273 
0274     q.setQuery(QLatin1String("data(/response/sessionToken)"));
0275     q.evaluateTo(&results);
0276     state.sessionToken() = results.trimmed();
0277 
0278     state.username()     = parameters()[QLatin1String("login")];
0279 }
0280 
0281 void LoginCommand::cleanUpOnError(RajceSession& state)
0282 {
0283     state.openAlbumToken() = QLatin1String("");
0284     state.nickname()       = QLatin1String("");
0285     state.username()       = QLatin1String("");
0286     state.imageQuality()   = 0;
0287     state.maxHeight()      = 0;
0288     state.maxWidth()       = 0;
0289     state.sessionToken()   = QLatin1String("");
0290     state.albums().clear();
0291 }
0292 
0293 // -----------------------------------------------------------------------
0294 
0295 CreateAlbumCommand::CreateAlbumCommand(const QString& name,
0296                                        const QString& description,
0297                                        bool visible,
0298                                        const RajceSession& state)
0299     : RajceCommand(QLatin1String("createAlbum"), CreateAlbum)
0300 {
0301     parameters()[QLatin1String("token")]            = state.sessionToken();
0302     parameters()[QLatin1String("albumName")]        = name;
0303     parameters()[QLatin1String("albumDescription")] = description;
0304     parameters()[QLatin1String("albumVisible")]     = visible ? QLatin1String("1") : QLatin1String("0");
0305 }
0306 
0307 void CreateAlbumCommand::parseResponse(QXmlQuery&, RajceSession&)
0308 {
0309 }
0310 
0311 void CreateAlbumCommand::cleanUpOnError(RajceSession&)
0312 {
0313 }
0314 
0315 void CloseAlbumCommand::parseResponse(QXmlQuery&, RajceSession&)
0316 {
0317 }
0318 
0319 void CloseAlbumCommand::cleanUpOnError(RajceSession&)
0320 {
0321 }
0322 
0323 // -----------------------------------------------------------------------
0324 
0325 CloseAlbumCommand::CloseAlbumCommand(const RajceSession& state)
0326     : RajceCommand(QLatin1String("closeAlbum"), CloseAlbum)
0327 {
0328     parameters()[QLatin1String("token")]      = state.sessionToken();
0329     parameters()[QLatin1String("albumToken")] = state.openAlbumToken();
0330 }
0331 
0332 // -----------------------------------------------------------------------
0333 
0334 AlbumListCommand::AlbumListCommand(const RajceSession& state)
0335     : RajceCommand(QLatin1String("getAlbumList"), ListAlbums)
0336 {
0337     parameters()[QLatin1String("token")] = state.sessionToken();
0338 }
0339 
0340 void AlbumListCommand::parseResponse(QXmlQuery& q, RajceSession& state)
0341 {
0342     state.albums().clear();
0343 
0344     QXmlResultItems results;
0345 
0346     q.setQuery(QLatin1String("/response/albums/album"));
0347     q.evaluateTo(&results);
0348 
0349     QXmlItem item(results.next());
0350 
0351     while (!item.isNull())
0352     {
0353         q.setFocus(item);
0354         RajceAlbum album;
0355         QString detail;
0356 
0357         q.setQuery(QLatin1String("data(./@id)"));
0358         q.evaluateTo(&detail);
0359         album.id          = detail.toUInt();
0360 
0361         q.setQuery(QLatin1String("data(./albumName)"));
0362         q.evaluateTo(&detail);
0363         album.name        = detail.trimmed();
0364 
0365         q.setQuery(QLatin1String("data(./description)"));
0366         q.evaluateTo(&detail);
0367         album.description = detail.trimmed();
0368 
0369         q.setQuery(QLatin1String("data(./url)"));
0370         q.evaluateTo(&detail);
0371         album.url         = detail.trimmed();
0372 
0373         q.setQuery(QLatin1String("data(./thumbUrl)"));
0374         q.evaluateTo(&detail);
0375         album.thumbUrl    = detail.trimmed();
0376 
0377         q.setQuery(QLatin1String("data(./createDate)"));
0378         q.evaluateTo(&detail);
0379         album.createDate  = QDateTime::fromString(detail.trimmed(), QLatin1String("yyyy-MM-dd hh:mm:ss"));
0380 
0381         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Create date: " << detail.trimmed() << " = "
0382                                          << QDateTime::fromString(detail.trimmed(), QLatin1String("yyyy-MM-dd hh:mm:ss"));
0383 
0384         q.setQuery(QLatin1String("data(./updateDate)"));
0385         q.evaluateTo(&detail);
0386         album.updateDate  = QDateTime::fromString(detail.trimmed(), QLatin1String("yyyy-MM-dd hh:mm:ss"));
0387 
0388         q.evaluateTo(&detail);
0389         album.isHidden    = detail.toUInt() != 0;
0390 
0391         q.setQuery(QLatin1String("data(./secure)"));
0392         q.evaluateTo(&detail);
0393         album.isSecure    = detail.toUInt() != 0;
0394 
0395         q.setQuery(QLatin1String("data(./startDateInterval)"));
0396         q.evaluateTo(&detail);
0397 
0398         if (detail.trimmed().length() > 0)
0399         {
0400             album.validFrom = QDateTime::fromString(detail, QLatin1String("yyyy-MM-dd hh:mm:ss"));
0401         }
0402 
0403         q.setQuery(QLatin1String("data(./endDateInterval)"));
0404         q.evaluateTo(&detail);
0405 
0406         if (detail.trimmed().length() > 0)
0407         {
0408             album.validTo = QDateTime::fromString(detail, QLatin1String("yyyy-MM-dd hh:mm:ss"));
0409         }
0410 
0411         q.setQuery(QLatin1String("data(./thumbUrlBest)"));
0412         q.evaluateTo(&detail);
0413         album.bestQualityThumbUrl = detail.trimmed();
0414 
0415         state.albums().append(album);
0416         item = results.next();
0417     }
0418 }
0419 
0420 void AlbumListCommand::cleanUpOnError(RajceSession& state)
0421 {
0422     state.albums().clear();
0423 }
0424 
0425 // -----------------------------------------------------------------------
0426 
0427 class Q_DECL_HIDDEN AddPhotoCommand::Private
0428 {
0429 public:
0430 
0431     explicit Private()
0432     {
0433         jpgQuality       = 90;
0434         desiredDimension = 0;
0435         maxDimension     = 0;
0436         form             = nullptr;
0437     }
0438 
0439     int          jpgQuality;
0440 
0441     unsigned     desiredDimension;
0442     unsigned     maxDimension;
0443 
0444     QString      tmpDir;
0445     QString      imagePath;
0446 
0447     QImage       image;
0448 
0449     RajceMPForm* form;
0450 };
0451 
0452 AddPhotoCommand::AddPhotoCommand(const QString& tmpDir,
0453                                  const QString& path,
0454                                  unsigned dimension,
0455                                  int      jpgQuality,
0456                                  const RajceSession& state)
0457     : RajceCommand(QLatin1String("addPhoto"), AddPhoto),
0458       d(new Private)
0459 {
0460     d->jpgQuality       = jpgQuality;
0461     d->desiredDimension = dimension;
0462     d->tmpDir           = tmpDir;
0463     d->imagePath        = path;
0464     d->image            = PreviewLoadThread::loadHighQualitySynchronously(path).copyQImage();
0465 
0466     if (d->image.isNull())
0467     {
0468         d->image.load(path);
0469     }
0470 
0471     // cppcheck-suppress duplicateCondition
0472     if (d->image.isNull())
0473     {
0474         qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Could not read in an image from "
0475                                          << path << ". Adding the photo will not work.";
0476         return;
0477     }
0478 
0479     d->maxDimension                                 = (state.maxHeight() > state.maxWidth()) ? state.maxWidth()
0480                                                                                              : state.maxHeight();
0481     parameters()[QLatin1String("token")]      = state.sessionToken();
0482     parameters()[QLatin1String("albumToken")] = state.openAlbumToken();
0483     d->form                                         = new RajceMPForm;
0484 }
0485 
0486 AddPhotoCommand::~AddPhotoCommand()
0487 {
0488     delete d->form;
0489     delete d;
0490 }
0491 
0492 void AddPhotoCommand::cleanUpOnError(RajceSession&)
0493 {
0494 }
0495 
0496 void AddPhotoCommand::parseResponse(QXmlQuery&, RajceSession&)
0497 {
0498 }
0499 
0500 QString AddPhotoCommand::additionalXml() const
0501 {
0502     if (d->image.isNull())
0503     {
0504         return QString();
0505     }
0506 
0507     QMap<QString, QString> metadata;
0508     QFileInfo f(d->imagePath);
0509 
0510     metadata[QLatin1String("FullFilePath")]          = d->imagePath;
0511     metadata[QLatin1String("OriginalFileName")]      = f.fileName();
0512     metadata[QLatin1String("OriginalFileExtension")] = QLatin1Char('.') + f.suffix();
0513     metadata[QLatin1String("PerceivedType")]         = QLatin1String("image"); //what are the other values here? video?
0514     metadata[QLatin1String("OriginalWidth")]         = QString::number(d->image.width());
0515     metadata[QLatin1String("OriginalHeight")]        = QString::number(d->image.height());
0516     metadata[QLatin1String("LengthMS")]              = QLatin1Char('0');
0517     metadata[QLatin1String("FileSize")]              = QString::number(f.size());
0518 
0519     //TODO extract these from exif
0520     metadata[QLatin1String("Title")]                 = QLatin1String("");
0521     metadata[QLatin1String("KeywordSet")]            = QLatin1String("");
0522     metadata[QLatin1String("PeopleRegionSet")]       = QLatin1String("");
0523 
0524     QString id = QString::number(QRandomGenerator::global()->generate());
0525     QString ret(QLatin1String("  <objectInfo>\n    <Item id=\""));
0526     ret.append(id).append(QLatin1String("\">\n"));
0527 
0528     Q_FOREACH (const QString& key, metadata.keys())
0529     {
0530         ret.append(QLatin1String("      <")).append(key);
0531         QString value = metadata[key];
0532 
0533         if (value.length() == 0)
0534         {
0535             ret.append(QLatin1String(" />\n"));
0536         }
0537         else
0538         {
0539             ret.append(QLatin1String(">"));
0540             ret.append(value);
0541             ret.append(QLatin1String("</"));
0542             ret.append(key);
0543             ret.append(QLatin1String(">\n"));
0544         }
0545     }
0546 
0547     ret.append(QLatin1String("    </Item>\n  </objectInfo>\n"));
0548 
0549     return ret;
0550 }
0551 
0552 QString AddPhotoCommand::contentType() const
0553 {
0554     return d->form->contentType();
0555 }
0556 
0557 QByteArray AddPhotoCommand::encode() const
0558 {
0559     if (d->image.isNull())
0560     {
0561         qCDebug(DIGIKAM_WEBSERVICES_LOG) << d->imagePath << " could not be read, no data will be sent.";
0562         return QByteArray();
0563     }
0564 
0565     PreparedImage prepared                = s_prepareImageForUpload(d->tmpDir,
0566                                                                     d->image,
0567                                                                     d->imagePath,
0568                                                                     d->desiredDimension,
0569                                                                     THUMB_SIZE,
0570                                                                     d->jpgQuality);
0571 
0572     //add the rest of the parameters to be encoded as xml
0573     QImage scaled(prepared.scaledImagePath);
0574     parameters()[QLatin1String("width")]  = QString::number(scaled.width());
0575     parameters()[QLatin1String("height")] = QString::number(scaled.height());
0576     QString xml                           = getXml();
0577 
0578     qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Really sending:\n" << xml;
0579 
0580     //now we can create the form with all the info
0581     d->form->reset();
0582 
0583     d->form->addPair(QLatin1String("data"),  xml);
0584 
0585     d->form->addFile(QLatin1String("thumb"), prepared.thumbPath);
0586     d->form->addFile(QLatin1String("photo"), prepared.scaledImagePath);
0587 
0588     QFile::remove(prepared.thumbPath);
0589     QFile::remove(prepared.scaledImagePath);
0590 
0591     d->form->finish();
0592 
0593     QByteArray ret                       = d->form->formData();
0594 
0595     return ret;
0596 }
0597 
0598 } // namespace DigikamGenericRajcePlugin
0599 
0600 #include "moc_rajcecommand.cpp"