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"
26 #include <QStringList>
27 #include <QTextDocument>
29 #include "UiUtils/PlainTextFormatter.h"
33 Formatting::Formatting(QObject
*parent
): QObject(parent
)
37 QString
Formatting::prettySize(uint bytes
, const BytesSuffix compactUnitFormat
)
42 int order
= std::log(static_cast<double>(bytes
)) / std::log(1024.0);
43 double number
= bytes
/ std::pow(1024.0, order
);
47 if (compactUnitFormat
== BytesSuffix::COMPACT_FORM
)
48 return QString::number(bytes
);
50 return tr("%n bytes", 0, bytes
);
51 } else if (order
== 1) {
53 } else if (order
== 2) {
55 } else if (order
== 3) {
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"));
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();
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()) {
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(),
140 Qt::escape(cert
.subjectInfo(QSslCertificate::CommonName
)),
141 Qt::escape(cert
.subjectInfo(QSslCertificate::Organization
)),
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());
159 sslErrorStrings
<< tr("<li>%1</li>").arg(Qt::escape(e
.errorString()));
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
));
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
;
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
;
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
)
217 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
218 res
= input
.toHtmlEscaped();
220 res
= Qt::escape(input
);
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
));