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 | |
15 | static const auto RegExpOptions = |
16 | QRegularExpression::CaseInsensitiveOption |
17 | | QRegularExpression::UseUnicodePropertiesOption; |
18 | |
19 | // Converts all that looks like a URL into HTML links |
20 | void 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 | |
56 | QString 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 | |
65 | QString 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 | |
74 | QString 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 | |
84 | qreal 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 | |
98 | static const auto ServerPartRegEx = QStringLiteral( |
99 | "(\\[[^][:space:]]+]|[-[:alnum:].]+)" // IPv6 address or hostname/IPv4 address |
100 | "(?::(\\d{1,5}))?" // Optional port |
101 | ); |
102 | |
103 | QString 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 | |
114 | QString Quotient::versionString() |
115 | { |
116 | return QStringLiteral(Quotient_VERSION_STRING); |
117 | } |
118 | |
119 | int Quotient::majorVersion() |
120 | { |
121 | return Quotient_VERSION_MAJOR; |
122 | } |
123 | |
124 | int Quotient::minorVersion() |
125 | { |
126 | return Quotient_VERSION_MINOR; |
127 | } |
128 | |
129 | int Quotient::patchVersion() |
130 | { |
131 | return Quotient_VERSION_PATCH; |
132 | } |
133 | |
134 | bool Quotient::encryptionSupported() |
135 | { |
136 | return E2EE_Enabled; |
137 | } |
138 | |