File indexing completed on 2024-06-16 05:01:51

0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 #include "UiUtils/Formatting.h"
0023 #include <cmath>
0024 #include <QSslError>
0025 #include <QSslKey>
0026 #include <QStringList>
0027 #include <QTextDocument>
0028 #include <QFontInfo>
0029 #include <QLocale>
0030 #include "UiUtils/PlainTextFormatter.h"
0031 
0032 namespace UiUtils {
0033 
0034 Formatting::Formatting(QObject *parent): QObject(parent)
0035 {
0036 }
0037 
0038 QString Formatting::prettySize(quint64 bytes)
0039 {
0040     static const QStringList suffixes = QStringList() << tr("B")
0041                                                       << tr("kB")
0042                                                       << tr("MB")
0043                                                       << tr("GB")
0044                                                       << tr("TB");
0045     const int max_order = suffixes.size() - 1;
0046     double number = bytes;
0047     int order = 0;
0048     int frac_digits = 0;
0049     if (bytes >= 1000) {
0050         int magnitude = std::log10(number);
0051         number /= std::pow(10.0, magnitude); // x.yz... * 10^magnitude
0052         number = qRound(number * 100.0) / 100.0; // round to 3 significant digits
0053         if (number >= 10.0) { // rounding has caused one increase in magnitude
0054             magnitude += 1;
0055             number /= 10.0;
0056         }
0057         order = magnitude / 3;
0058         int rem = magnitude % 3;
0059         number *= std::pow(10.0, rem); // xy.z * 1000^order
0060         if (order <= max_order) {
0061             frac_digits = 2 - rem;
0062         } else { // shame on you for such large mails
0063             number *= std::pow(1000.0, order - max_order);
0064             order = max_order;
0065         }
0066     }
0067     return tr("%1 %2").arg(QString::number(number, 'f', frac_digits), suffixes.at(order));
0068 }
0069 
0070 /** @short Format a QDateTime for compact display in one column of the view */
0071 QString Formatting::prettyDate(const QDateTime &dateTime)
0072 {
0073     // The time is not always synced properly, so better accept even slightly too new messages as "from today"
0074     QDateTime now = QDateTime::currentDateTime().addSecs(15*60);
0075     if (dateTime >= now) {
0076         // Messages from future shall always be shown using full format to prevent nasty surprises.
0077         return QLocale().toString(dateTime, QLocale::ShortFormat);
0078     } else if (dateTime.date() == now.date() || dateTime > now.addSecs(-6 * 3600)) {
0079         // It's a "today's message", i.e. something which is either literally from today or at least something not older than
0080         // six hours (an arbitraty magic number).
0081         // Originally, the cut-off time interval was set to 24 hours, but it led to weird things in the GUI like showing mails
0082         // from yesterday's 18:33 just as "18:33" even though the local time was "18:20" already. In a perfect world, we would
0083         // also periodically emit dataChanged() in order to force a wrap once the view has been open for too long, but that will
0084         // have to wait a bit.
0085         // The time is displayed without seconds to conserve space as well.
0086         return dateTime.time().toString(tr("hh:mm", "Please do not translate the format specifiers. "
0087             "You can change their order or the separator to follow the local conventions. "
0088             "For valid specifiers see http://doc.qt.io/qt-5/qdatetime.html#toString"));
0089     } else if (dateTime > now.addDays(-7)) {
0090         // Messages from the last seven days can be formatted just with the weekday name
0091         return dateTime.toString(tr("ddd hh:mm", "Please do not translate the format specifiers. "
0092             "You can change their order or the separator to follow the local conventions. "
0093             "For valid specifiers see http://doc.qt.io/qt-5/qdatetime.html#toString"));
0094     } else if (dateTime.date().year() == now.date().year() || dateTime > now.addMonths(-6)) {
0095         // Originally, this used to handle messages fresher than an year old. However, this might get a wee bit confusing
0096         // when it's September and we're showing a message from November last year.
0097         // I think that it's OK-ish to assume that unchanged years are OK, but let's be more careful when crossing the year
0098         // boundary. This is just a number that I pulled out of my sleeve, but a six-month cutoff might do the trick here.
0099         return dateTime.toString(tr("d MMM hh:mm", "Please do not translate the format specifiers. "
0100             "You can change their order or the separator to follow the local conventions. "
0101             "For valid specifiers see http://doc.qt.io/qt-5/qdatetime.html#toString"));
0102     } else {
0103         // Old messagees shall have a full date
0104         return QLocale().toString(dateTime, QLocale::ShortFormat);
0105     }
0106 }
0107 
0108 QString Formatting::htmlizedTextPart(const QModelIndex &partIndex, const QFont &font,
0109                                      const QColor &backgroundColor, const QColor &textColor,
0110                                      const QColor &linkColor, const QColor &visitedLinkColor)
0111 {
0112     Q_ASSERT(partIndex.isValid());
0113     QFontInfo fontInfo(font);
0114     return UiUtils::htmlizedTextPart(partIndex, fontInfo,
0115                                      backgroundColor, textColor,
0116                                      linkColor, visitedLinkColor);
0117 }
0118 
0119 /** @short Produce a properly formatted HTML string which won't overflow the right edge of the display */
0120 QString Formatting::htmlHexifyByteArray(const QByteArray &rawInput)
0121 {
0122     QByteArray inHex = rawInput.toHex();
0123     QByteArray res;
0124     const int stepping = 4;
0125     for (int i = 0; i < inHex.length(); i += stepping) {
0126         // The individual blocks are formatted separately to allow line breaks to happen
0127         res.append("<code style=\"font-family: monospace;\">");
0128         res.append(inHex.mid(i, stepping));
0129         if (i + stepping < inHex.size()) {
0130             res.append(":");
0131         }
0132         // Produce the smallest possible space. "display: none" won't notice the space at all, leading to overly long lines
0133         res.append("</code><span style=\"font-size: 1px\"> </span>");
0134     }
0135     return QString::fromUtf8(res);
0136 }
0137 
0138 QString Formatting::sslChainToHtml(const QList<QSslCertificate> &sslChain)
0139 {
0140     QStringList certificateStrings;
0141     Q_FOREACH(const QSslCertificate &cert, sslChain) {
0142         certificateStrings << tr("<li><b>CN</b>: %1,<br/>\n<b>Organization</b>: %2,<br/>\n"
0143                                  "<b>Serial</b>: %3,<br/>\n"
0144                                  "<b>SHA1</b>: %4,<br/>\n<b>MD5</b>: %5</li>").arg(
0145                                   cert.subjectInfo(QSslCertificate::CommonName).join(tr(", ")).toHtmlEscaped(),
0146                                   cert.subjectInfo(QSslCertificate::Organization).join(tr(", ")).toHtmlEscaped(),
0147                                   QString::fromUtf8(cert.serialNumber()),
0148                                   htmlHexifyByteArray(cert.digest(QCryptographicHash::Sha1)),
0149                                   htmlHexifyByteArray(cert.digest(QCryptographicHash::Md5)));
0150     }
0151     return sslChain.isEmpty() ?
0152                 tr("<p>The remote side doesn't have a certificate.</p>\n") :
0153                 tr("<p>This is the certificate chain of the connection:</p>\n<ul>%1</ul>\n").arg(certificateStrings.join(tr("\n")));
0154 }
0155 
0156 QString Formatting::sslErrorsToHtml(const QList<QSslError> &sslErrors)
0157 {
0158     QStringList sslErrorStrings;
0159     Q_FOREACH(const QSslError &e, sslErrors) {
0160         sslErrorStrings << tr("<li>%1</li>").arg(e.errorString().toHtmlEscaped());
0161     }
0162     return sslErrors.isEmpty() ?
0163                 tr("<p>According to your system's policy, this connection is secure.</p>\n") :
0164                 tr("<p>The connection triggered the following SSL errors:</p>\n<ul>%1</ul>\n").arg(sslErrorStrings.join(tr("\n")));
0165 }
0166 
0167 void Formatting::formatSslState(const QList<QSslCertificate> &sslChain, const QByteArray &oldPubKey,
0168                                       const QList<QSslError> &sslErrors, QString *title, QString *message, IconType *icon)
0169 {
0170     bool pubKeyHasChanged = !oldPubKey.isEmpty() && (sslChain.isEmpty() || sslChain[0].publicKey().toPem() != oldPubKey);
0171 
0172     if (pubKeyHasChanged) {
0173         if (sslErrors.isEmpty()) {
0174             *icon = IconType::Warning;
0175             *title = tr("Different SSL certificate");
0176             *message = tr("<p>The public key of the SSL certificate has changed. "
0177                           "This should only happen when there was a security incident on the remote server. "
0178                           "Your system configuration is set to accept such certificates anyway.</p>\n%1\n"
0179                           "<p>Would you like to connect and remember the new certificate?</p>")
0180                     .arg(sslChainToHtml(sslChain));
0181         } else {
0182             // changed certificate which is not trusted per systemwide policy
0183             *title = tr("SSL certificate looks fishy");
0184             *message = tr("<p>The public key of the SSL certificate of the IMAP server has changed since the last time "
0185                           "and your system doesn't believe that the new certificate is genuine.</p>\n%1\n%2\n"
0186                           "<p>Would you like to connect anyway and remember the new certificate?</p>").
0187                     arg(sslChainToHtml(sslChain), sslErrorsToHtml(sslErrors));
0188             *icon = IconType::Critical;
0189         }
0190     } else {
0191         if (sslErrors.isEmpty()) {
0192             // this is the first time and the certificate looks valid -> accept
0193             *title = tr("Accept SSL connection?");
0194             *message = tr("<p>This is the first time you're connecting to this IMAP server; the certificate is trusted "
0195                           "by this system.</p>\n%1\n%2\n"
0196                           "<p>Would you like to connect and remember this certificate's public key for the next time?</p>")
0197                     .arg(sslChainToHtml(sslChain), sslErrorsToHtml(sslErrors));
0198             *icon = IconType::Information;
0199         } else {
0200             *title = tr("Accept SSL connection?");
0201             *message = tr("<p>This is the first time you're connecting to this IMAP server and the server certificate failed "
0202                           "validation test.</p>\n%1\n\n%2\n"
0203                           "<p>Would you like to connect and remember this certificate's public key for the next time?</p>")
0204                     .arg(sslChainToHtml(sslChain), sslErrorsToHtml(sslErrors));
0205             *icon = IconType::Question;
0206         }
0207     }
0208 }
0209 
0210 /** @short Input formatted as HTML with proper escaping and forced to be detected as HTML */
0211 QString Formatting::htmlEscaped(const QString &input)
0212 {
0213     if (input.isEmpty())
0214         return QString();
0215 
0216     // HTML entities are escaped, but not auto-detected as HTML
0217     return QLatin1String("<span>") + input.toHtmlEscaped() + QLatin1String("</span>");
0218 }
0219 
0220 QObject *Formatting::factory(QQmlEngine *engine, QJSEngine *scriptEngine)
0221 {
0222     Q_UNUSED(scriptEngine);
0223 
0224     // the reinterpret_cast is used to avoid haivng to depend on QtQuick when doing non-QML builds
0225     Formatting *f = new Formatting(reinterpret_cast<QObject*>(engine));
0226     return f;
0227 }
0228 
0229 bool elideAddress(QString &address)
0230 {
0231     if (address.length() < 66)
0232         return false;
0233 
0234     const int idx = address.lastIndexOf(QLatin1Char('@'));
0235     auto ellipsis = QStringLiteral("\u2026");
0236     if (idx > -1) {
0237         if (idx < 9) // local part is too short to strip anything
0238             return false;
0239 
0240         // do not stash the domain and leave at least 4 chars head and tail of the local part
0241         const int d = qMax(8, idx - (address.length() - 60))/2;
0242         address = address.leftRef(d) + ellipsis + address.rightRef(address.length() - idx + d);
0243     } else {
0244         // some longer something, just remove the overhead in the center to eg.
0245         // leave "https://" and "foo/index.html" intact
0246         address = address.leftRef(30) + ellipsis + address.rightRef(30);
0247     }
0248     return true;
0249 }
0250 
0251 }