1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
2 
3    This file is part of the Trojita Qt IMAP e-mail client,
4    http://trojita.flaska.net/
5 
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.
13 
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.
18 
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/>.
21 */
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"
30 
31 namespace UiUtils {
32 
Formatting(QObject * parent)33 Formatting::Formatting(QObject *parent): QObject(parent)
34 {
35 }
36 
prettySize(quint64 bytes)37 QString Formatting::prettySize(quint64 bytes)
38 {
39     static const QStringList suffixes = QStringList() << tr("B")
40                                                       << tr("kB")
41                                                       << tr("MB")
42                                                       << tr("GB")
43                                                       << tr("TB");
44     const int max_order = suffixes.size() - 1;
45     double number = bytes;
46     int order = 0;
47     int frac_digits = 0;
48     if (bytes >= 1000) {
49         int magnitude = std::log10(number);
50         number /= std::pow(10.0, magnitude); // x.yz... * 10^magnitude
51         number = qRound(number * 100.0) / 100.0; // round to 3 significant digits
52         if (number >= 10.0) { // rounding has caused one increase in magnitude
53             magnitude += 1;
54             number /= 10.0;
55         }
56         order = magnitude / 3;
57         int rem = magnitude % 3;
58         number *= std::pow(10.0, rem); // xy.z * 1000^order
59         if (order <= max_order) {
60             frac_digits = 2 - rem;
61         } else { // shame on you for such large mails
62             number *= std::pow(1000.0, order - max_order);
63             order = max_order;
64         }
65     }
66     return tr("%1 %2").arg(QString::number(number, 'f', frac_digits), suffixes.at(order));
67 }
68 
69 /** @short Format a QDateTime for compact display in one column of the view */
prettyDate(const QDateTime & dateTime)70 QString Formatting::prettyDate(const QDateTime &dateTime)
71 {
72     // The time is not always synced properly, so better accept even slightly too new messages as "from today"
73     QDateTime now = QDateTime::currentDateTime().addSecs(15*60);
74     if (dateTime >= now) {
75         // Messages from future shall always be shown using full format to prevent nasty surprises.
76         return dateTime.toString(Qt::DefaultLocaleShortDate);
77     } else if (dateTime.date() == now.date() || dateTime > now.addSecs(-6 * 3600)) {
78         // It's a "today's message", i.e. something which is either literally from today or at least something not older than
79         // six hours (an arbitraty magic number).
80         // Originally, the cut-off time interval was set to 24 hours, but it led to weird things in the GUI like showing mails
81         // from yesterday's 18:33 just as "18:33" even though the local time was "18:20" already. In a perfect world, we would
82         // also periodically emit dataChanged() in order to force a wrap once the view has been open for too long, but that will
83         // have to wait a bit.
84         // The time is displayed without seconds to conserve space as well.
85         return dateTime.time().toString(tr("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.addDays(-7)) {
89         // Messages from the last seven days can be formatted just with the weekday name
90         return dateTime.toString(tr("ddd 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 if (dateTime > now.addYears(-1)) {
94         // Messages newer than one year don't have to show year
95         return dateTime.toString(tr("d MMM hh:mm", "Please do not translate the format specifiers. "
96             "You can change their order or the separator to follow the local conventions. "
97             "For valid specifiers see http://doc.qt.io/qt-5/qdatetime.html#toString"));
98     } else {
99         // Old messagees shall have a full date
100         return dateTime.toString(Qt::DefaultLocaleShortDate);
101     }
102 }
103 
htmlizedTextPart(const QModelIndex & partIndex,const QFont & font,const QColor & backgroundColor,const QColor & textColor,const QColor & linkColor,const QColor & visitedLinkColor)104 QString Formatting::htmlizedTextPart(const QModelIndex &partIndex, const QFont &font,
105                                      const QColor &backgroundColor, const QColor &textColor,
106                                      const QColor &linkColor, const QColor &visitedLinkColor)
107 {
108     Q_ASSERT(partIndex.isValid());
109     QFontInfo fontInfo(font);
110     return UiUtils::htmlizedTextPart(partIndex, fontInfo,
111                                      backgroundColor, textColor,
112                                      linkColor, visitedLinkColor);
113 }
114 
115 /** @short Produce a properly formatted HTML string which won't overflow the right edge of the display */
htmlHexifyByteArray(const QByteArray & rawInput)116 QString Formatting::htmlHexifyByteArray(const QByteArray &rawInput)
117 {
118     QByteArray inHex = rawInput.toHex();
119     QByteArray res;
120     const int stepping = 4;
121     for (int i = 0; i < inHex.length(); i += stepping) {
122         // The individual blocks are formatted separately to allow line breaks to happen
123         res.append("<code style=\"font-family: monospace;\">");
124         res.append(inHex.mid(i, stepping));
125         if (i + stepping < inHex.size()) {
126             res.append(":");
127         }
128         // Produce the smallest possible space. "display: none" won't notice the space at all, leading to overly long lines
129         res.append("</code><span style=\"font-size: 1px\"> </span>");
130     }
131     return QString::fromUtf8(res);
132 }
133 
sslChainToHtml(const QList<QSslCertificate> & sslChain)134 QString Formatting::sslChainToHtml(const QList<QSslCertificate> &sslChain)
135 {
136     QStringList certificateStrings;
137     Q_FOREACH(const QSslCertificate &cert, sslChain) {
138         certificateStrings << tr("<li><b>CN</b>: %1,<br/>\n<b>Organization</b>: %2,<br/>\n"
139                                  "<b>Serial</b>: %3,<br/>\n"
140                                  "<b>SHA1</b>: %4,<br/>\n<b>MD5</b>: %5</li>").arg(
141                                   cert.subjectInfo(QSslCertificate::CommonName).join(tr(", ")).toHtmlEscaped(),
142                                   cert.subjectInfo(QSslCertificate::Organization).join(tr(", ")).toHtmlEscaped(),
143                                   QString::fromUtf8(cert.serialNumber()),
144                                   htmlHexifyByteArray(cert.digest(QCryptographicHash::Sha1)),
145                                   htmlHexifyByteArray(cert.digest(QCryptographicHash::Md5)));
146     }
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")));
150 }
151 
sslErrorsToHtml(const QList<QSslError> & sslErrors)152 QString Formatting::sslErrorsToHtml(const QList<QSslError> &sslErrors)
153 {
154     QStringList sslErrorStrings;
155     Q_FOREACH(const QSslError &e, sslErrors) {
156         sslErrorStrings << tr("<li>%1</li>").arg(e.errorString().toHtmlEscaped());
157     }
158     return sslErrors.isEmpty() ?
159                 tr("<p>According to your system's policy, this connection is secure.</p>\n") :
160                 tr("<p>The connection triggered the following SSL errors:</p>\n<ul>%1</ul>\n").arg(sslErrorStrings.join(tr("\n")));
161 }
162 
formatSslState(const QList<QSslCertificate> & sslChain,const QByteArray & oldPubKey,const QList<QSslError> & sslErrors,QString * title,QString * message,IconType * icon)163 void Formatting::formatSslState(const QList<QSslCertificate> &sslChain, const QByteArray &oldPubKey,
164                                       const QList<QSslError> &sslErrors, QString *title, QString *message, IconType *icon)
165 {
166     bool pubKeyHasChanged = !oldPubKey.isEmpty() && (sslChain.isEmpty() || sslChain[0].publicKey().toPem() != oldPubKey);
167 
168     if (pubKeyHasChanged) {
169         if (sslErrors.isEmpty()) {
170             *icon = IconType::Warning;
171             *title = tr("Different SSL certificate");
172             *message = tr("<p>The public key of the SSL certificate has changed. "
173                           "This should only happen when there was a security incident on the remote server. "
174                           "Your system configuration is set to accept such certificates anyway.</p>\n%1\n"
175                           "<p>Would you like to connect and remember the new certificate?</p>")
176                     .arg(sslChainToHtml(sslChain));
177         } else {
178             // changed certificate which is not trusted per systemwide policy
179             *title = tr("SSL certificate looks fishy");
180             *message = tr("<p>The public key of the SSL certificate of the IMAP server has changed since the last time "
181                           "and your system doesn't believe that the new certificate is genuine.</p>\n%1\n%2\n"
182                           "<p>Would you like to connect anyway and remember the new certificate?</p>").
183                     arg(sslChainToHtml(sslChain), sslErrorsToHtml(sslErrors));
184             *icon = IconType::Critical;
185         }
186     } else {
187         if (sslErrors.isEmpty()) {
188             // this is the first time and the certificate looks valid -> accept
189             *title = tr("Accept SSL connection?");
190             *message = tr("<p>This is the first time you're connecting to this IMAP server; the certificate is trusted "
191                           "by this system.</p>\n%1\n%2\n"
192                           "<p>Would you like to connect and remember this certificate's public key for the next time?</p>")
193                     .arg(sslChainToHtml(sslChain), sslErrorsToHtml(sslErrors));
194             *icon = IconType::Information;
195         } else {
196             *title = tr("Accept SSL connection?");
197             *message = tr("<p>This is the first time you're connecting to this IMAP server and the server certificate failed "
198                           "validation test.</p>\n%1\n\n%2\n"
199                           "<p>Would you like to connect and remember this certificate's public key for the next time?</p>")
200                     .arg(sslChainToHtml(sslChain), sslErrorsToHtml(sslErrors));
201             *icon = IconType::Question;
202         }
203     }
204 }
205 
206 /** @short Input formatted as HTML with proper escaping and forced to be detected as HTML */
htmlEscaped(const QString & input)207 QString Formatting::htmlEscaped(const QString &input)
208 {
209     if (input.isEmpty())
210         return QString();
211 
212     // HTML entities are escaped, but not auto-detected as HTML
213     return QLatin1String("<span>") + input.toHtmlEscaped() + QLatin1String("</span>");
214 }
215 
factory(QQmlEngine * engine,QJSEngine * scriptEngine)216 QObject *Formatting::factory(QQmlEngine *engine, QJSEngine *scriptEngine)
217 {
218     Q_UNUSED(scriptEngine);
219 
220     // the reinterpret_cast is used to avoid haivng to depend on QtQuick when doing non-QML builds
221     Formatting *f = new Formatting(reinterpret_cast<QObject*>(engine));
222     return f;
223 }
224 
elideAddress(QString & address)225 bool elideAddress(QString &address)
226 {
227     if (address.length() < 66)
228         return false;
229 
230     const int idx = address.lastIndexOf(QLatin1Char('@'));
231     auto ellipsis = QStringLiteral("\u2026");
232     if (idx > -1) {
233         if (idx < 9) // local part is too short to strip anything
234             return false;
235 
236         // do not stash the domain and leave at least 4 chars head and tail of the local part
237         const int d = qMax(8, idx - (address.length() - 60))/2;
238         address = address.leftRef(d) + ellipsis + address.rightRef(address.length() - idx + d);
239     } else {
240         // some longer something, just remove the overhead in the center to eg.
241         // leave "https://" and "foo/index.html" intact
242         address = address.leftRef(30) + ellipsis + address.rightRef(30);
243     }
244     return true;
245 }
246 
247 }
248