File indexing completed on 2024-05-19 05:17:28

0001 /*
0002     SPDX-FileCopyrightText: 2002-2004 Marc Mutz <mutz@kde.org>
0003     SPDX-FileCopyrightText: 2007 Tom Albers <tomalbers@kde.nl>
0004     SPDX-FileCopyrightText: 2009 Thomas McGuire <mcguire@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "signature.h"
0010 
0011 #include "kidentitymanagementcore_debug.h"
0012 #include <KConfigGroup>
0013 #include <KLocalizedString>
0014 #include <KProcess>
0015 
0016 #include <QDir>
0017 #include <QTextBlock>
0018 #include <QTextDocument>
0019 
0020 #include <cassert>
0021 
0022 using namespace KIdentityManagementCore;
0023 
0024 class KIdentityManagementCore::SignaturePrivate
0025 {
0026 public:
0027     explicit SignaturePrivate(Signature *qq)
0028         : q(qq)
0029     {
0030     }
0031 
0032     void assignFrom(const KIdentityManagementCore::Signature &that);
0033     void cleanupImages();
0034     void saveImages() const;
0035     [[nodiscard]] QString textFromFile(bool *ok) const;
0036     [[nodiscard]] QString textFromCommand(bool *ok, QString *errorMessage) const;
0037 
0038     /// List of images that belong to this signature. Either added by addImage() or
0039     /// by readConfig().
0040     QList<Signature::EmbeddedImagePtr> embeddedImages;
0041 
0042     /// The directory where the images will be saved to.
0043     QString saveLocation;
0044     QString path;
0045     QString text;
0046     Signature::Type type = Signature::Disabled;
0047     bool enabled = false;
0048     bool inlinedHtml = false;
0049     Signature *const q;
0050 };
0051 
0052 // Returns the names of all images in the HTML code
0053 static QStringList findImageNames(const QString &htmlCode)
0054 {
0055     QStringList imageNames;
0056     QTextDocument doc;
0057     doc.setHtml(htmlCode);
0058     for (auto block = doc.begin(); block.isValid(); block = block.next()) {
0059         for (auto it = block.begin(); !it.atEnd(); ++it) {
0060             const auto fragment = it.fragment();
0061             if (fragment.isValid()) {
0062                 const auto imageFormat = fragment.charFormat().toImageFormat();
0063                 if (imageFormat.isValid() && !imageFormat.name().startsWith(QLatin1StringView("http")) && !imageNames.contains(imageFormat.name())) {
0064                     imageNames.push_back(imageFormat.name());
0065                 }
0066             }
0067         }
0068     }
0069     return imageNames;
0070 }
0071 
0072 void SignaturePrivate::assignFrom(const KIdentityManagementCore::Signature &that)
0073 {
0074     path = that.path();
0075     inlinedHtml = that.isInlinedHtml();
0076     text = that.text();
0077     type = that.type();
0078     enabled = that.isEnabledSignature();
0079     saveLocation = that.imageLocation();
0080     embeddedImages = that.embeddedImages();
0081 }
0082 
0083 void SignaturePrivate::cleanupImages()
0084 {
0085     // Remove any images from the internal structure that are no longer there
0086     if (inlinedHtml) {
0087         auto it = std::remove_if(embeddedImages.begin(), embeddedImages.end(), [this](const Signature::EmbeddedImagePtr &imageInList) {
0088             const QStringList lstImage = findImageNames(text);
0089             for (const QString &imageInHtml : lstImage) {
0090                 if (imageInHtml == imageInList->name) {
0091                     return false;
0092                 }
0093             }
0094             return true;
0095         });
0096         embeddedImages.erase(it, embeddedImages.end());
0097     }
0098 
0099     // Delete all the old image files
0100     if (!saveLocation.isEmpty()) {
0101         QDir dir(saveLocation);
0102         const QStringList lst = dir.entryList(QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks);
0103         for (const QString &fileName : lst) {
0104             if (fileName.endsWith(QLatin1StringView(".png"), Qt::CaseInsensitive)) {
0105                 qCDebug(KIDENTITYMANAGEMENT_LOG) << "Deleting old image" << dir.path() + fileName;
0106                 dir.remove(fileName);
0107             }
0108         }
0109     }
0110 }
0111 
0112 void SignaturePrivate::saveImages() const
0113 {
0114     if (inlinedHtml && !saveLocation.isEmpty()) {
0115         for (const Signature::EmbeddedImagePtr &image : std::as_const(embeddedImages)) {
0116             const QString location = saveLocation + QLatin1Char('/') + image->name;
0117             if (!image->image.save(location, "PNG")) {
0118                 qCWarning(KIDENTITYMANAGEMENT_LOG) << "Failed to save image" << location;
0119             }
0120         }
0121     }
0122 }
0123 
0124 QString SignaturePrivate::textFromFile(bool *ok) const
0125 {
0126     assert(type == Signature::FromFile);
0127 
0128     QFile f(path);
0129     if (!f.open(QIODevice::ReadOnly)) {
0130         qCWarning(KIDENTITYMANAGEMENT_LOG) << "Failed to open" << path << ":" << f.errorString();
0131         if (ok) {
0132             *ok = false;
0133         }
0134         return {};
0135     }
0136 
0137     if (ok) {
0138         *ok = true;
0139     }
0140     const QByteArray ba = f.readAll();
0141     return QString::fromLocal8Bit(ba.data(), ba.size());
0142 }
0143 
0144 QString SignaturePrivate::textFromCommand(bool *ok, QString *errorMessage) const
0145 {
0146     assert(type == Signature::FromCommand);
0147 
0148     // handle pathological cases:
0149     if (path.isEmpty()) {
0150         if (ok) {
0151             *ok = true;
0152         }
0153         return {};
0154     }
0155 
0156     // create a shell process:
0157     KProcess proc;
0158     proc.setOutputChannelMode(KProcess::SeparateChannels);
0159     proc.setShellCommand(path);
0160     int rc = proc.execute();
0161 
0162     // handle errors, if any:
0163     if (rc != 0) {
0164         if (ok) {
0165             *ok = false;
0166         }
0167         if (errorMessage) {
0168             *errorMessage = i18n(
0169                 "<qt>Failed to execute signature script<p><b>%1</b>:</p>"
0170                 "<p>%2</p></qt>",
0171                 path,
0172                 QString::fromUtf8(proc.readAllStandardError()));
0173         }
0174         return {};
0175     }
0176 
0177     // no errors:
0178     if (ok) {
0179         *ok = true;
0180     }
0181 
0182     // get output:
0183     const QByteArray output = proc.readAllStandardOutput();
0184 
0185     // TODO: hmm, should we allow other encodings, too?
0186     return QString::fromLocal8Bit(output.data(), output.size());
0187 }
0188 
0189 QDataStream &operator<<(QDataStream &stream, const KIdentityManagementCore::Signature::EmbeddedImagePtr &img)
0190 {
0191     return stream << img->image << img->name;
0192 }
0193 
0194 QDataStream &operator>>(QDataStream &stream, const KIdentityManagementCore::Signature::EmbeddedImagePtr &img)
0195 {
0196     return stream >> img->image >> img->name;
0197 }
0198 
0199 Signature::Signature()
0200     : d(new SignaturePrivate(this))
0201 {
0202     d->type = Disabled;
0203     d->inlinedHtml = false;
0204 }
0205 
0206 Signature::Signature(const QString &text)
0207     : d(new SignaturePrivate(this))
0208 {
0209     d->type = Inlined;
0210     d->inlinedHtml = false;
0211     d->text = text;
0212 }
0213 
0214 Signature::Signature(const QString &path, bool isExecutable)
0215     : d(new SignaturePrivate(this))
0216 {
0217     d->type = isExecutable ? FromCommand : FromFile;
0218     d->path = path;
0219 }
0220 
0221 Signature::Signature(const Signature &that)
0222     : d(new SignaturePrivate(this))
0223 {
0224     d->assignFrom(that);
0225 }
0226 
0227 Signature &Signature::operator=(const KIdentityManagementCore::Signature &that)
0228 {
0229     if (this == &that) {
0230         return *this;
0231     }
0232 
0233     d->assignFrom(that);
0234     return *this;
0235 }
0236 
0237 Signature::~Signature() = default;
0238 
0239 QString Signature::rawText(bool *ok, QString *errorMessage) const
0240 {
0241     switch (d->type) {
0242     case Disabled:
0243         if (ok) {
0244             *ok = true;
0245         }
0246         return {};
0247     case Inlined:
0248         if (ok) {
0249             *ok = true;
0250         }
0251         return d->text;
0252     case FromFile:
0253         return d->textFromFile(ok);
0254     case FromCommand:
0255         return d->textFromCommand(ok, errorMessage);
0256     }
0257     qCritical() << "Signature::type() returned unknown value!";
0258     return {}; // make compiler happy
0259 }
0260 
0261 QString Signature::withSeparator(bool *ok, QString *errorMessage) const
0262 {
0263     QString signature = rawText(ok, errorMessage);
0264     if (ok && (*ok) == false) {
0265         return {};
0266     }
0267 
0268     if (signature.isEmpty()) {
0269         return signature; // don't add a separator in this case
0270     }
0271 
0272     const bool htmlSig = (isInlinedHtml() && d->type == Inlined);
0273     QString newline = htmlSig ? QStringLiteral("<br>") : QStringLiteral("\n");
0274     if (htmlSig && signature.startsWith(QLatin1StringView("<p"))) {
0275         newline.clear();
0276     }
0277 
0278     if (signature.startsWith(QLatin1StringView("-- ") + newline) || (signature.indexOf(newline + QLatin1StringView("-- ") + newline) != -1)) {
0279         // already have signature separator at start of sig or inside sig:
0280         return signature;
0281     } else {
0282         // need to prepend one:
0283         return QLatin1StringView("-- ") + newline + signature;
0284     }
0285 }
0286 
0287 void Signature::setPath(const QString &path, bool isExecutable)
0288 {
0289     d->path = path;
0290     d->type = isExecutable ? FromCommand : FromFile;
0291 }
0292 
0293 void Signature::setInlinedHtml(bool isHtml)
0294 {
0295     d->inlinedHtml = isHtml;
0296 }
0297 
0298 bool Signature::isInlinedHtml() const
0299 {
0300     return d->inlinedHtml;
0301 }
0302 
0303 // config keys and values:
0304 static const char sigTypeKey[] = "Signature Type";
0305 static const char sigTypeInlineValue[] = "inline";
0306 static const char sigTypeFileValue[] = "file";
0307 static const char sigTypeCommandValue[] = "command";
0308 static const char sigTypeDisabledValue[] = "disabled";
0309 static const char sigTextKey[] = "Inline Signature";
0310 static const char sigFileKey[] = "Signature File";
0311 static const char sigCommandKey[] = "Signature Command";
0312 static const char sigTypeInlinedHtmlKey[] = "Inlined Html";
0313 static const char sigImageLocation[] = "Image Location";
0314 static const char sigEnabled[] = "Signature Enabled";
0315 
0316 void Signature::readConfig(const KConfigGroup &config)
0317 {
0318     QString sigType = config.readEntry(sigTypeKey);
0319     if (sigType == QLatin1StringView(sigTypeInlineValue)) {
0320         d->type = Inlined;
0321         d->inlinedHtml = config.readEntry(sigTypeInlinedHtmlKey, false);
0322     } else if (sigType == QLatin1StringView(sigTypeFileValue)) {
0323         d->type = FromFile;
0324         d->path = config.readPathEntry(sigFileKey, QString());
0325     } else if (sigType == QLatin1StringView(sigTypeCommandValue)) {
0326         d->type = FromCommand;
0327         d->path = config.readPathEntry(sigCommandKey, QString());
0328     } else if (sigType == QLatin1StringView(sigTypeDisabledValue)) {
0329         d->enabled = false;
0330     }
0331     if (d->type != Disabled) {
0332         d->enabled = config.readEntry(sigEnabled, true);
0333     }
0334 
0335     d->text = config.readEntry(sigTextKey);
0336     d->saveLocation = config.readEntry(sigImageLocation);
0337 
0338     if (isInlinedHtml() && !d->saveLocation.isEmpty()) {
0339         QDir dir(d->saveLocation);
0340         const QStringList lst = dir.entryList(QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks);
0341         for (const QString &fileName : lst) {
0342             if (fileName.endsWith(QLatin1StringView(".png"), Qt::CaseInsensitive)) {
0343                 QImage image;
0344                 if (image.load(dir.path() + QLatin1Char('/') + fileName)) {
0345                     addImage(image, fileName);
0346                 } else {
0347                     qCWarning(KIDENTITYMANAGEMENT_LOG) << "Unable to load image" << dir.path() + QLatin1Char('/') + fileName;
0348                 }
0349             }
0350         }
0351     }
0352 }
0353 
0354 void Signature::writeConfig(KConfigGroup &config) const
0355 {
0356     switch (d->type) {
0357     case Inlined:
0358         config.writeEntry(sigTypeKey, sigTypeInlineValue);
0359         config.writeEntry(sigTypeInlinedHtmlKey, d->inlinedHtml);
0360         break;
0361     case FromFile:
0362         config.writeEntry(sigTypeKey, sigTypeFileValue);
0363         config.writePathEntry(sigFileKey, d->path);
0364         break;
0365     case FromCommand:
0366         config.writeEntry(sigTypeKey, sigTypeCommandValue);
0367         config.writePathEntry(sigCommandKey, d->path);
0368         break;
0369     default:
0370         break;
0371     }
0372     config.writeEntry(sigTextKey, d->text);
0373     config.writeEntry(sigImageLocation, d->saveLocation);
0374     config.writeEntry(sigEnabled, d->enabled);
0375 
0376     d->cleanupImages();
0377     d->saveImages();
0378 }
0379 
0380 QList<Signature::EmbeddedImagePtr> Signature::embeddedImages() const
0381 {
0382     return d->embeddedImages;
0383 }
0384 
0385 void Signature::setEmbeddedImages(const QList<Signature::EmbeddedImagePtr> &embedded)
0386 {
0387     d->embeddedImages = embedded;
0388 }
0389 
0390 // --------------------- Operators -------------------//
0391 
0392 QDataStream &KIdentityManagementCore::operator<<(QDataStream &stream, const KIdentityManagementCore::Signature &sig)
0393 {
0394     return stream << static_cast<quint8>(sig.type()) << sig.path() << sig.text() << sig.imageLocation() << sig.embeddedImages() << sig.isEnabledSignature();
0395 }
0396 
0397 QDataStream &KIdentityManagementCore::operator>>(QDataStream &stream, KIdentityManagementCore::Signature &sig)
0398 {
0399     quint8 s;
0400     QString path;
0401     QString text;
0402     QString saveLocation;
0403     QList<Signature::EmbeddedImagePtr> lst;
0404     bool enabled;
0405     stream >> s >> path >> text >> saveLocation >> lst >> enabled;
0406     sig.setText(text);
0407     sig.setPath(path);
0408     sig.setImageLocation(saveLocation);
0409     sig.setEmbeddedImages(lst);
0410     sig.setEnabledSignature(enabled);
0411     sig.setType(static_cast<Signature::Type>(s));
0412     return stream;
0413 }
0414 
0415 bool Signature::operator==(const Signature &other) const
0416 {
0417     if (d->type != other.type()) {
0418         return false;
0419     }
0420 
0421     if (d->enabled != other.isEnabledSignature()) {
0422         return false;
0423     }
0424 
0425     if (d->type == Inlined && d->inlinedHtml) {
0426         if (d->saveLocation != other.imageLocation()) {
0427             return false;
0428         }
0429         if (d->embeddedImages != other.embeddedImages()) {
0430             return false;
0431         }
0432     }
0433 
0434     switch (d->type) {
0435     case Inlined:
0436         return d->text == other.text();
0437     case FromFile:
0438     case FromCommand:
0439         return d->path == other.path();
0440     default:
0441     case Disabled:
0442         return true;
0443     }
0444 }
0445 
0446 QString Signature::toPlainText() const
0447 {
0448     QString sigText = rawText();
0449     if (!sigText.isEmpty() && isInlinedHtml() && type() == Inlined) {
0450         // Use a QTextDocument as a helper, it does all the work for us and
0451         // strips all HTML tags.
0452         QTextDocument helper;
0453         QTextCursor helperCursor(&helper);
0454         helperCursor.insertHtml(sigText);
0455         sigText = helper.toPlainText();
0456     }
0457     return sigText;
0458 }
0459 
0460 void Signature::addImage(const QImage &imageData, const QString &imageName)
0461 {
0462     Q_ASSERT(!(d->saveLocation.isEmpty()));
0463     Signature::EmbeddedImagePtr image(new Signature::EmbeddedImage());
0464     image->image = imageData;
0465     image->name = imageName;
0466     d->embeddedImages.append(image);
0467 }
0468 
0469 void Signature::setImageLocation(const QString &path)
0470 {
0471     d->saveLocation = path;
0472 }
0473 
0474 QString Signature::imageLocation() const
0475 {
0476     return d->saveLocation;
0477 }
0478 
0479 // --------------- Getters -----------------------//
0480 
0481 QString Signature::text() const
0482 {
0483     return d->text;
0484 }
0485 
0486 QString Signature::path() const
0487 {
0488     return d->path;
0489 }
0490 
0491 Signature::Type Signature::type() const
0492 {
0493     return d->type;
0494 }
0495 
0496 // --------------- Setters -----------------------//
0497 
0498 void Signature::setText(const QString &text)
0499 {
0500     d->text = text;
0501     d->type = Inlined;
0502 }
0503 
0504 void Signature::setType(Type type)
0505 {
0506     d->type = type;
0507 }
0508 
0509 void Signature::setEnabledSignature(bool enabled)
0510 {
0511     d->enabled = enabled;
0512 }
0513 
0514 bool Signature::isEnabledSignature() const
0515 {
0516     return d->enabled;
0517 }