1// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net>
2// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
3// SPDX-License-Identifier: LGPL-2.1-or-later
4
5#include "util.h"
6
7#include <QtCore/QCryptographicHash>
8#include <QtCore/QDataStream>
9#include <QtCore/QDir>
10#include <QtCore/QRegularExpression>
11#include <QtCore/QStandardPaths>
12#include <QtCore/QStringBuilder>
13#include <QtCore/QtEndian>
14
15static const auto RegExpOptions =
16 QRegularExpression::CaseInsensitiveOption
17 | QRegularExpression::UseUnicodePropertiesOption;
18
19// Converts all that looks like a URL into HTML links
20void Quotient::linkifyUrls(QString& htmlEscapedText)
21{
22 // Note: outer parentheses are a part of C++ raw string delimiters, not of
23 // the regex (see http://en.cppreference.com/w/cpp/language/string_literal).
24 // Note2: the next-outer parentheses are \N in the replacement.
25
26 // generic url:
27 // regexp is originally taken from Konsole (https://github.com/KDE/konsole)
28 // protocolname:// or www. followed by anything other than whitespaces,
29 // <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :,
30 // comma or dot
31 static const QRegularExpression FullUrlRegExp(
32 QStringLiteral(
33 R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp):(//)?\w|(magnet|matrix):)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"),
34 RegExpOptions);
35 static const QRegularExpression EmailAddressRegExp(
36 QStringLiteral(R"((^|[][[:space:](){}`'";<>])(mailto:)?((\w|[!#$%&'*+=^_‘{|}~.-])+@(\w|\.|-)+\.\w+\b))"),
37 RegExpOptions);
38 // An interim liberal implementation of
39 // https://matrix.org/docs/spec/appendices.html#identifier-grammar
40 static const QRegularExpression MxIdRegExp(
41 QStringLiteral(
42 R"((^|[][[:space:](){}`'";])([!#@][-a-z0-9_=#/.]{1,252}:\w(?:\w|\.|-)*\.\w+(?::\d{1,5})?))"),
43 RegExpOptions);
44 Q_ASSERT(FullUrlRegExp.isValid() && EmailAddressRegExp.isValid()
45 && MxIdRegExp.isValid());
46
47 // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&,"
48
49 htmlEscapedText.replace(re: EmailAddressRegExp,
50 after: R"(\1<a href='mailto:\3'>\2\3</a>)"_ls);
51 htmlEscapedText.replace(re: FullUrlRegExp, after: R"(<a href='\1'>\1</a>)"_ls);
52 htmlEscapedText.replace(re: MxIdRegExp,
53 after: R"(\1<a href='https://matrix.to/#/\2'>\2</a>)"_ls);
54}
55
56QString Quotient::sanitized(const QString& plainText)
57{
58 auto text = plainText;
59 text.remove(c: QChar(0x202e)); // RLO
60 text.remove(c: QChar(0x202d)); // LRO
61 text.remove(c: QChar(0xfffc)); // Object replacement character
62 return text;
63}
64
65QString Quotient::prettyPrint(const QString& plainText)
66{
67 auto pt = plainText.toHtmlEscaped();
68 linkifyUrls(htmlEscapedText&: pt);
69 pt.replace(c: u'\n', QStringLiteral("<br/>"));
70 return QStringLiteral("<span style='white-space:pre-wrap'>") + pt
71 + QStringLiteral("</span>");
72}
73
74QString Quotient::cacheLocation(const QString& dirName)
75{
76 const QString cachePath =
77 QStandardPaths::writableLocation(type: QStandardPaths::CacheLocation) % u'/'
78 % dirName % u'/';
79 if (const QDir dir(cachePath); !dir.exists())
80 dir.mkpath(dirPath: "."_ls);
81 return cachePath;
82}
83
84qreal Quotient::stringToHueF(const QString& s)
85{
86 Q_ASSERT(!s.isEmpty());
87 const auto hash =
88 QCryptographicHash::hash(data: s.toUtf8(), method: QCryptographicHash::Sha1);
89 QDataStream dataStream(hash.left(n: 2));
90 dataStream.setByteOrder(QDataStream::LittleEndian);
91 quint16 hashValue = 0;
92 dataStream >> hashValue;
93 const auto hueF = qreal(hashValue) / std::numeric_limits<quint16>::max();
94 Q_ASSERT((0 <= hueF) && (hueF <= 1));
95 return hueF;
96}
97
98static const auto ServerPartRegEx = QStringLiteral(
99 "(\\[[^][:space:]]+]|[-[:alnum:].]+)" // IPv6 address or hostname/IPv4 address
100 "(?::(\\d{1,5}))?" // Optional port
101);
102
103QString Quotient::serverPart(const QString& mxId)
104{
105 static const QString re("^[@!#$+].*?:("_ls // Localpart and colon
106 % ServerPartRegEx % ")$"_ls);
107 static const QRegularExpression parser(
108 re,
109 QRegularExpression::UseUnicodePropertiesOption); // Because Asian digits
110 Q_ASSERT(parser.isValid());
111 return parser.match(subject: mxId).captured(nth: 1);
112}
113
114QString Quotient::versionString()
115{
116 return QStringLiteral(Quotient_VERSION_STRING);
117}
118
119int Quotient::majorVersion()
120{
121 return Quotient_VERSION_MAJOR;
122}
123
124int Quotient::minorVersion()
125{
126 return Quotient_VERSION_MINOR;
127}
128
129int Quotient::patchVersion()
130{
131 return Quotient_VERSION_PATCH;
132}
133
134bool Quotient::encryptionSupported()
135{
136 return E2EE_Enabled;
137}
138