Files
AyuGramDesktop/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_links.cpp
T
2026-05-19 12:49:08 +04:00

335 lines
8.7 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "iv/markdown/iv_markdown_prepare_links.h"
#include "ui/basic_click_handlers.h"
#include <QtCore/QDir>
#include <QtCore/QFileInfo>
#include <QtCore/QUrl>
#include <algorithm>
namespace Iv::Markdown {
namespace {
[[nodiscard]] bool HasUrlScheme(const QString &target) {
if (target.isEmpty()) {
return false;
}
const auto colon = target.indexOf(QChar(':'));
if (colon <= 0) {
return false;
}
const auto slash = target.indexOf(QChar('/'));
const auto question = target.indexOf(QChar('?'));
const auto hash = target.indexOf(QChar('#'));
auto limit = target.size();
for (const auto value : { slash, question, hash }) {
if (value >= 0) {
limit = std::min(limit, value);
}
}
if (colon >= limit) {
return false;
}
if (!target[0].isLetter()) {
return false;
}
for (auto i = 1; i != colon; ++i) {
const auto ch = target[i];
if (!ch.isLetterOrNumber() && ch != QChar('+') && ch != QChar('-')
&& ch != QChar('.')) {
return false;
}
}
return true;
}
[[nodiscard]] bool LooksLikeWindowsDrivePath(const QString &target) {
return target.size() >= 2
&& target[0].isLetter()
&& target[1] == QChar(':');
}
[[nodiscard]] bool LooksLikeFileUrl(const QString &target) {
return target.size() >= 5
&& target.left(5).compare(u"file:"_q, Qt::CaseInsensitive) == 0;
}
[[nodiscard]] bool LooksLikeFilesystemTarget(const QString &target) {
return target.startsWith(u"/"_q)
|| target.startsWith(u"\\"_q)
|| target.startsWith(u"//"_q)
|| target.startsWith(u"\\\\"_q)
|| LooksLikeWindowsDrivePath(target)
|| LooksLikeFileUrl(target);
}
[[nodiscard]] QString ComparablePath(QString path) {
path = QDir::fromNativeSeparators(QDir::cleanPath(path));
return path.toLower();
}
[[nodiscard]] bool IsContainedPath(
const QString &baseDirectory,
const QString &resolvedPath) {
const auto base = ComparablePath(baseDirectory);
const auto resolved = ComparablePath(resolvedPath);
return (resolved == base) || resolved.startsWith(base + u"/"_q);
}
[[nodiscard]] bool IsEscapableAscii(char ch) {
const auto value = uchar(ch);
return (value >= 0x21 && value <= 0x2F)
|| (value >= 0x3A && value <= 0x40)
|| (value >= 0x5B && value <= 0x60)
|| (value >= 0x7B && value <= 0x7E);
}
[[nodiscard]] bool AppendHtmlEntityText(
const QByteArray &entity,
QString *result) {
if (entity == "amp") {
result->append(QChar('&'));
} else if (entity == "lt") {
result->append(QChar('<'));
} else if (entity == "gt") {
result->append(QChar('>'));
} else if (entity == "quot") {
result->append(QChar('"'));
} else if (entity == "apos") {
result->append(QChar('\''));
} else if (entity.startsWith("#x") || entity.startsWith("#X")) {
auto ok = false;
const auto value = entity.mid(2).toUInt(&ok, 16);
if (!ok || value > 0xFFFF) {
return false;
}
result->append(QChar(ushort(value)));
} else if (entity.startsWith("#")) {
auto ok = false;
const auto value = entity.mid(1).toUInt(&ok, 10);
if (!ok || value > 0xFFFF) {
return false;
}
result->append(QChar(ushort(value)));
} else {
return false;
}
return true;
}
[[nodiscard]] QString NormalizeMailtoTarget(const QString &target) {
const auto parsed = QUrl(target);
auto result = parsed.path(QUrl::FullyDecoded);
if (result.isEmpty()) {
result = target.mid(target.indexOf(QChar(':')) + 1);
const auto question = result.indexOf(QChar('?'));
if (question >= 0) {
result = result.left(question);
}
result = QString::fromUtf8(
QByteArray::fromPercentEncoding(result.toUtf8()));
}
while (result.startsWith(u"/"_q)) {
result.remove(0, 1);
}
return result.trimmed();
}
[[nodiscard]] QString ExternalLinkDisplayText(const PreparedLink &link) {
if (link.entityType == EntityType::Email) {
return link.target;
}
const auto original = QUrl(link.target);
const auto good = QUrl(original.isValid()
? original.toEncoded()
: QString());
return good.isValid() ? good.toDisplayString() : link.target;
}
} // namespace
void NormalizePreparedUrlLink(PreparedLink *result, const QString &target) {
if (!result) {
return;
}
result->fragment = QString();
if (target.startsWith(u"mailto:"_q, Qt::CaseInsensitive)) {
result->target = NormalizeMailtoTarget(target);
result->copyText = result->target;
result->entityType = EntityType::Email;
result->shown = EntityLinkShown::Full;
return;
}
result->target = UrlClickHandler::EncodeForOpening(target);
result->copyText = result->target;
result->entityType = EntityType::Url;
result->shown = EntityLinkShown::Full;
}
void FinalizePreparedUrlLink(
PreparedLink *link,
QStringView renderedText) {
if (!link || link->entityType != EntityType::Url) {
return;
}
if (renderedText == QStringView(ExternalLinkDisplayText(*link))) {
return;
}
if (UrlClickHandler::EncodeForOpening(renderedText.toString())
== link->target) {
link->shown = EntityLinkShown::Partial;
return;
}
link->entityType = EntityType::CustomUrl;
}
QString InternalLinkData(uint16 index) {
return u"internal:index"_q + QChar(index);
}
QString NormalizeFragmentId(QString fragment) {
fragment = QString::fromUtf8(
QByteArray::fromPercentEncoding(fragment.toUtf8()));
fragment = fragment.trimmed().toLower();
while (fragment.startsWith(u"#"_q)) {
fragment.remove(0, 1);
}
return fragment;
}
PreparedLink ClassifiedLink(
uint16 index,
QString target,
const PrepareState *state) {
auto result = PreparedLink();
result.index = index;
if (target.startsWith(QChar('#'))) {
result.kind = PreparedLinkKind::Anchor;
result.target = NormalizeFragmentId(target.mid(1));
return result;
}
if (LooksLikeFilesystemTarget(target)) {
result.kind = PreparedLinkKind::RejectedRelative;
return result;
}
if (HasUrlScheme(target)) {
result.kind = PreparedLinkKind::External;
NormalizePreparedUrlLink(&result, target);
return result;
}
auto fragmentIndex = target.indexOf(QChar('#'));
if (fragmentIndex >= 0) {
result.fragment = NormalizeFragmentId(target.mid(fragmentIndex + 1));
target = target.left(fragmentIndex);
}
result.target = target;
if (target.isEmpty()) {
result.kind = PreparedLinkKind::Anchor;
result.target = result.fragment;
result.fragment = QString();
return result;
}
if (!state
|| !state->request
|| state->request->sourcePath.isEmpty()) {
result.kind = PreparedLinkKind::RejectedRelative;
return result;
}
if (target.contains(QChar('?'))) {
result.kind = PreparedLinkKind::RejectedRelative;
return result;
}
const auto baseDirectory = QFileInfo(state->request->sourcePath).absolutePath();
if (baseDirectory.isEmpty()) {
result.kind = PreparedLinkKind::RejectedRelative;
return result;
}
const auto resolved = QDir(baseDirectory).absoluteFilePath(target);
const auto cleanedResolved = QDir::cleanPath(resolved);
if (!IsContainedPath(baseDirectory, cleanedResolved)
|| !LooksLikeMarkdownFile(cleanedResolved)) {
result.kind = PreparedLinkKind::RejectedRelative;
return result;
}
result.kind = PreparedLinkKind::LocalFile;
result.target = cleanedResolved;
return result;
}
QString FirstInfoToken(const QString &info) {
const auto trimmed = info.trimmed();
for (auto i = 0; i != trimmed.size(); ++i) {
if (trimmed[i].isSpace()) {
return trimmed.left(i);
}
}
return trimmed;
}
void SortEntities(TextWithEntities *text) {
auto &entities = text->entities;
std::sort(
entities.begin(),
entities.end(),
[](const EntityInText &left, const EntityInText &right) {
if (left.offset() != right.offset()) {
return left.offset() < right.offset();
} else if (left.length() != right.length()) {
return left.length() > right.length();
}
return int(left.type()) < int(right.type());
});
}
QString DecodeMarkdownTextPrefix(QByteArray bytes) {
auto result = QString();
auto plainFrom = 0;
const auto flushPlain = [&](int till) {
if (till > plainFrom) {
result.append(QString::fromUtf8(
bytes.constData() + plainFrom,
till - plainFrom));
}
};
for (auto i = 0; i != bytes.size();) {
if (bytes[i] == '\\'
&& (i + 1) < bytes.size()
&& IsEscapableAscii(bytes[i + 1])) {
flushPlain(i);
result.append(QChar(ushort(uchar(bytes[i + 1]))));
i += 2;
plainFrom = i;
} else if (bytes[i] == '&') {
const auto semicolon = bytes.indexOf(';', i + 1);
if (semicolon > i && semicolon - i <= 32) {
auto entityText = QString();
if (AppendHtmlEntityText(
bytes.mid(i + 1, semicolon - i - 1),
&entityText)) {
flushPlain(i);
result.append(entityText);
i = semicolon + 1;
plainFrom = i;
continue;
}
}
++i;
} else {
++i;
}
}
flushPlain(bytes.size());
return result;
}
} // namespace Iv::Markdown