SVN_SILENT made messages (.desktop file) - always resolve ours
[trojita.git] / src / UiUtils / Formatting.cpp
blob2b47198ef8ced5d229646512658ed731c22e3d5c
1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
3 This file is part of the Trojita Qt IMAP e-mail client,
4 http://trojita.flaska.net/
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
22 #include "UiUtils/Formatting.h"
23 #include <cmath>
24 #include <QSslError>
25 #include <QSslKey>
26 #include <QStringList>
27 #include <QTextDocument>
28 #include <QFontInfo>
29 #include "UiUtils/PlainTextFormatter.h"
31 namespace UiUtils {
33 Formatting::Formatting(QObject *parent): QObject(parent)
37 QString Formatting::prettySize(uint bytes, const BytesSuffix compactUnitFormat)
39 if (bytes == 0) {
40 return tr("0");
42 int order = std::log(static_cast<double>(bytes)) / std::log(1024.0);
43 double number = bytes / std::pow(1024.0, order);
45 QString suffix;
46 if (order <= 0) {
47 if (compactUnitFormat == BytesSuffix::COMPACT_FORM)
48 return QString::number(bytes);
49 else
50 return tr("%n bytes", 0, bytes);
51 } else if (order == 1) {
52 suffix = tr("kB");
53 } else if (order == 2) {
54 suffix = tr("MB");
55 } else if (order == 3) {
56 suffix = tr("GB");
57 } else {
58 // make sure not to show wrong size for those that have > 1024 TB e-mail messages
59 suffix = tr("TB"); // shame on you for such mails
61 return tr("%1 %2").arg(QString::number(number, 'f', number < 100 ? 1 : 0), suffix);
64 /** @short Format a QDateTime for compact display in one column of the view */
65 QString Formatting::prettyDate(const QDateTime &dateTime)
67 // The time is not always synced properly, so better accept even slightly too new messages as "from today"
68 QDateTime now = QDateTime::currentDateTime().addSecs(15*60);
69 if (dateTime >= now) {
70 // Messages from future shall always be shown using full format to prevent nasty surprises.
71 return dateTime.toString(Qt::DefaultLocaleShortDate);
72 } else if (dateTime.date() == now.date() || dateTime > now.addSecs(-6 * 3600)) {
73 // It's a "today's message", i.e. something which is either literally from today or at least something not older than
74 // six hours (an arbitraty magic number).
75 // Originally, the cut-off time interval was set to 24 hours, but it led to weird things in the GUI like showing mails
76 // from yesterday's 18:33 just as "18:33" even though the local time was "18:20" already. In a perfect world, we would
77 // also periodically emit dataChanged() in order to force a wrap once the view has been open for too long, but that will
78 // have to wait a bit.
79 // The time is displayed without seconds to conserve space as well.
80 return dateTime.time().toString(tr("hh:mm", "Please do not translate the format specifiers. "
81 "You can change their order or the separator to follow the local conventions. "
82 "For valid specifiers see http://doc.qt.io/qt-5/qdatetime.html#toString"));
83 } else if (dateTime > now.addDays(-7)) {
84 // Messages from the last seven days can be formatted just with the weekday name
85 return dateTime.toString(tr("ddd hh:mm", "Please do not translate the format specifiers. "
86 "You can change their order or the separator to follow the local conventions. "
87 "For valid specifiers see http://doc.qt.io/qt-5/qdatetime.html#toString"));
88 } else if (dateTime > now.addYears(-1)) {
89 // Messages newer than one year don't have to show year
90 return dateTime.toString(tr("d MMM hh:mm", "Please do not translate the format specifiers. "
91 "You can change their order or the separator to follow the local conventions. "
92 "For valid specifiers see http://doc.qt.io/qt-5/qdatetime.html#toString"));
93 } else {
94 // Old messagees shall have a full date
95 return dateTime.toString(Qt::DefaultLocaleShortDate);
99 QString Formatting::htmlizedTextPart(const QModelIndex &partIndex, const QFont &font,
100 const QColor &backgroundColor, const QColor &textColor,
101 const QColor &linkColor, const QColor &visitedLinkColor)
103 Q_ASSERT(partIndex.isValid());
104 QFontInfo fontInfo(font);
105 return UiUtils::htmlizedTextPart(partIndex, fontInfo,
106 backgroundColor, textColor,
107 linkColor, visitedLinkColor);
110 /** @short Produce a properly formatted HTML string which won't overflow the right edge of the display */
111 QString Formatting::htmlHexifyByteArray(const QByteArray &rawInput)
113 QByteArray inHex = rawInput.toHex();
114 QByteArray res;
115 const int stepping = 4;
116 for (int i = 0; i < inHex.length(); i += stepping) {
117 // The individual blocks are formatted separately to allow line breaks to happen
118 res.append("<code style=\"font-family: monospace;\">");
119 res.append(inHex.mid(i, stepping));
120 if (i + stepping < inHex.size()) {
121 res.append(":");
123 // Produce the smallest possible space. "display: none" won't notice the space at all, leading to overly long lines
124 res.append("</code><span style=\"font-size: 1px\"> </span>");
126 return QString::fromUtf8(res);
129 QString Formatting::sslChainToHtml(const QList<QSslCertificate> &sslChain)
131 QStringList certificateStrings;
132 Q_FOREACH(const QSslCertificate &cert, sslChain) {
133 certificateStrings << tr("<li><b>CN</b>: %1,<br/>\n<b>Organization</b>: %2,<br/>\n"
134 "<b>Serial</b>: %3,<br/>\n"
135 "<b>SHA1</b>: %4,<br/>\n<b>MD5</b>: %5</li>").arg(
136 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
137 cert.subjectInfo(QSslCertificate::CommonName).join(tr(", ")).toHtmlEscaped(),
138 cert.subjectInfo(QSslCertificate::Organization).join(tr(", ")).toHtmlEscaped(),
139 #else
140 Qt::escape(cert.subjectInfo(QSslCertificate::CommonName)),
141 Qt::escape(cert.subjectInfo(QSslCertificate::Organization)),
142 #endif
143 QString::fromUtf8(cert.serialNumber()),
144 htmlHexifyByteArray(cert.digest(QCryptographicHash::Sha1)),
145 htmlHexifyByteArray(cert.digest(QCryptographicHash::Md5)));
147 return sslChain.isEmpty() ?
148 tr("<p>The remote side doesn't have a certificate.</p>\n") :
149 tr("<p>This is the certificate chain of the connection:</p>\n<ul>%1</ul>\n").arg(certificateStrings.join(tr("\n")));
152 QString Formatting::sslErrorsToHtml(const QList<QSslError> &sslErrors)
154 QStringList sslErrorStrings;
155 Q_FOREACH(const QSslError &e, sslErrors) {
156 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
157 sslErrorStrings << tr("<li>%1</li>").arg(e.errorString().toHtmlEscaped());
158 #else
159 sslErrorStrings << tr("<li>%1</li>").arg(Qt::escape(e.errorString()));
160 #endif
162 return sslErrors.isEmpty() ?
163 tr("<p>According to your system's policy, this connection is secure.</p>\n") :
164 tr("<p>The connection triggered the following SSL errors:</p>\n<ul>%1</ul>\n").arg(sslErrorStrings.join(tr("\n")));
167 void Formatting::formatSslState(const QList<QSslCertificate> &sslChain, const QByteArray &oldPubKey,
168 const QList<QSslError> &sslErrors, QString *title, QString *message, IconType *icon)
170 bool pubKeyHasChanged = !oldPubKey.isEmpty() && (sslChain.isEmpty() || sslChain[0].publicKey().toPem() != oldPubKey);
172 if (pubKeyHasChanged) {
173 if (sslErrors.isEmpty()) {
174 *icon = IconType::Warning;
175 *title = tr("Different SSL certificate");
176 *message = tr("<p>The public key of the SSL certificate has changed. "
177 "This should only happen when there was a security incident on the remote server. "
178 "Your system configuration is set to accept such certificates anyway.</p>\n%1\n"
179 "<p>Would you like to connect and remember the new certificate?</p>")
180 .arg(sslChainToHtml(sslChain));
181 } else {
182 // changed certificate which is not trusted per systemwide policy
183 *title = tr("SSL certificate looks fishy");
184 *message = tr("<p>The public key of the SSL certificate of the IMAP server has changed since the last time "
185 "and your system doesn't believe that the new certificate is genuine.</p>\n%1\n%2\n"
186 "<p>Would you like to connect anyway and remember the new certificate?</p>").
187 arg(sslChainToHtml(sslChain), sslErrorsToHtml(sslErrors));
188 *icon = IconType::Critical;
190 } else {
191 if (sslErrors.isEmpty()) {
192 // this is the first time and the certificate looks valid -> accept
193 *title = tr("Accept SSL connection?");
194 *message = tr("<p>This is the first time you're connecting to this IMAP server; the certificate is trusted "
195 "by this system.</p>\n%1\n%2\n"
196 "<p>Would you like to connect and remember this certificate's public key for the next time?</p>")
197 .arg(sslChainToHtml(sslChain), sslErrorsToHtml(sslErrors));
198 *icon = IconType::Information;
199 } else {
200 *title = tr("Accept SSL connection?");
201 *message = tr("<p>This is the first time you're connecting to this IMAP server and the server certificate failed "
202 "validation test.</p>\n%1\n\n%2\n"
203 "<p>Would you like to connect and remember this certificate's public key for the next time?</p>")
204 .arg(sslChainToHtml(sslChain), sslErrorsToHtml(sslErrors));
205 *icon = IconType::Question;
210 /** @short Input formatted as HTML with proper escaping and forced to be detected as HTML */
211 QString Formatting::htmlEscaped(const QString &input)
213 if (input.isEmpty())
214 return QString();
216 QString res;
217 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
218 res = input.toHtmlEscaped();
219 #else
220 res = Qt::escape(input);
221 #endif
223 // HTML entities are escaped, but not auto-detected as HTML
224 return QLatin1String("<span>") + res + QLatin1String("</span>");
227 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
228 QObject *Formatting::factory(QQmlEngine *engine, QJSEngine *scriptEngine)
230 Q_UNUSED(scriptEngine);
232 // the reinterpret_cast is used to avoid haivng to depend on QtQuick when doing non-QML builds
233 Formatting *f = new Formatting(reinterpret_cast<QObject*>(engine));
234 return f;
236 #endif