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 }