File indexing completed on 2024-05-05 03:56:05

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2000 Matthias Hoelzer-Kluepfel <hoelzer@kde.org>
0004     SPDX-FileCopyrightText: 2001 Stephan Kulow <coolo@kde.org>
0005     SPDX-FileCopyrightText: 2003 Cornelius Schumacher <schumacher@kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include <config-help.h>
0011 
0012 #include "kio_help.h"
0013 #include "xslt_help.h"
0014 
0015 #include <docbookxslt.h>
0016 
0017 #include <KLocalizedString>
0018 
0019 #include <QDebug>
0020 
0021 #include <QDir>
0022 #include <QFile>
0023 #include <QFileInfo>
0024 #include <QMimeDatabase>
0025 #include <QStandardPaths>
0026 #include <QUrl>
0027 
0028 #include <libxslt/transform.h>
0029 #include <libxslt/xsltutils.h>
0030 
0031 using namespace KIO;
0032 
0033 QString HelpProtocol::langLookup(const QString &fname)
0034 {
0035     QStringList search;
0036 
0037     // assemble the local search paths
0038     const QStringList localDoc = KDocTools::documentationDirs();
0039 
0040     QStringList langs = KLocalizedString::languages();
0041     langs.append(QStringLiteral("en"));
0042     langs.removeAll(QStringLiteral("C"));
0043 
0044     auto shouldReplace = [](const QString &l) {
0045         return l == QLatin1String("en_US");
0046     };
0047     // this is kind of compat hack as we install our docs in en/ but the
0048     // default language is en_US
0049     std::replace_if(langs.begin(), langs.end(), shouldReplace, QStringLiteral("en"));
0050 
0051     // look up the different languages
0052     int ldCount = localDoc.count();
0053     search.reserve(ldCount * langs.size());
0054     for (int id = 0; id < ldCount; id++) {
0055         for (const QString &lang : std::as_const(langs)) {
0056             search.append(QStringLiteral("%1/%2/%3").arg(localDoc[id], lang, fname));
0057         }
0058     }
0059 
0060     auto checkFile = [](const QString &str) {
0061         QFileInfo info(str);
0062         return info.exists() && info.isFile() && info.isReadable();
0063     };
0064 
0065     // try to locate the file
0066     for (const QString &path : std::as_const(search)) {
0067         // qDebug() << "Looking for help in: " << path;
0068 
0069         if (checkFile(path)) {
0070             return path;
0071         }
0072 
0073         if (path.endsWith(QLatin1String(".html"))) {
0074             const QString file = QStringView(path).left(path.lastIndexOf(QLatin1Char('/'))) + QLatin1String("/index.docbook");
0075             // qDebug() << "Looking for help in: " << file;
0076             if (checkFile(file)) {
0077                 return path;
0078             }
0079         }
0080     }
0081 
0082     return QString();
0083 }
0084 
0085 QString HelpProtocol::lookupFile(const QString &fname, const QString &query, bool &redirect)
0086 {
0087     redirect = false;
0088 
0089     const QString &path = fname;
0090 
0091     QString result = langLookup(path);
0092     if (result.isEmpty()) {
0093         result = langLookup(path + QLatin1String("/index.html"));
0094         if (!result.isEmpty()) {
0095             QUrl red;
0096             red.setScheme(QStringLiteral("help"));
0097             red.setPath(path + QLatin1String("/index.html"));
0098             red.setQuery(query);
0099             redirection(red);
0100             // qDebug() << "redirect to " << red;
0101             redirect = true;
0102         } else {
0103             const QString documentationNotFound = QStringLiteral("kioworker6/help/documentationnotfound/index.html");
0104             if (!langLookup(documentationNotFound).isEmpty()) {
0105                 QUrl red;
0106                 red.setScheme(QStringLiteral("help"));
0107                 red.setPath(documentationNotFound);
0108                 red.setQuery(query);
0109                 redirection(red);
0110                 redirect = true;
0111             } else {
0112                 sendError(i18n("There is no documentation available for %1.", path.toHtmlEscaped()));
0113                 return QString();
0114             }
0115         }
0116     } else {
0117         // qDebug() << "result " << result;
0118     }
0119 
0120     return result;
0121 }
0122 
0123 void HelpProtocol::sendError(const QString &t)
0124 {
0125     data(QStringLiteral("<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"></head>\n%1</html>")
0126              .arg(t.toHtmlEscaped())
0127              .toUtf8());
0128 }
0129 
0130 HelpProtocol::HelpProtocol(bool ghelp, const QByteArray &pool, const QByteArray &app)
0131     : WorkerBase(ghelp ? QByteArrayLiteral("ghelp") : QByteArrayLiteral("help"), pool, app)
0132     , mGhelp(ghelp)
0133 {
0134 }
0135 
0136 KIO::WorkerResult HelpProtocol::get(const QUrl &url)
0137 {
0138     ////qDebug() << "path=" << url.path()
0139     //<< "query=" << url.query();
0140 
0141     bool redirect;
0142     QString doc = QDir::cleanPath(url.path());
0143     if (doc.contains(QLatin1String(".."))) {
0144         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.toString());
0145     }
0146 
0147     if (!mGhelp) {
0148         if (!doc.startsWith(QLatin1Char('/'))) {
0149             doc.prepend(QLatin1Char('/'));
0150         }
0151 
0152         if (doc.endsWith(QLatin1Char('/'))) {
0153             doc += QLatin1String("index.html");
0154         }
0155     }
0156 
0157     infoMessage(i18n("Looking up correct file"));
0158 
0159     if (!mGhelp) {
0160         doc = lookupFile(doc, url.query(), redirect);
0161 
0162         if (redirect) {
0163             return KIO::WorkerResult::pass();
0164         }
0165     }
0166 
0167     if (doc.isEmpty()) {
0168         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.toString());
0169     }
0170 
0171     QUrl target;
0172     target.setPath(doc);
0173     if (url.hasFragment()) {
0174         target.setFragment(url.fragment());
0175     }
0176 
0177     // qDebug() << "target " << target;
0178 
0179     QString file = target.isLocalFile() ? target.toLocalFile() : target.path();
0180 
0181     if (mGhelp) {
0182         if (!file.endsWith(QLatin1String(".xml"))) {
0183             return get_file(file);
0184         }
0185     } else {
0186         const QString docbook_file = QStringView(file).left(file.lastIndexOf(QLatin1Char('/'))) + QLatin1String("/index.docbook");
0187         if (!QFile::exists(file)) {
0188             file = docbook_file;
0189         } else {
0190             QFileInfo fi(file);
0191             if (fi.isDir()) {
0192                 file += QLatin1String("/index.docbook");
0193             } else {
0194                 if (!file.endsWith(QLatin1String(".html")) || !compareTimeStamps(file, docbook_file)) {
0195                     return get_file(file);
0196                 } else {
0197                     file = docbook_file;
0198                 }
0199             }
0200         }
0201     }
0202 
0203     infoMessage(i18n("Preparing document"));
0204     mimeType(QStringLiteral("text/html"));
0205 
0206     if (mGhelp) {
0207         QString xsl = QStringLiteral("customization/kde-nochunk.xsl");
0208         mParsed = KDocTools::transform(file, KDocTools::locateFileInDtdResource(xsl));
0209 
0210         // qDebug() << "parsed " << mParsed.length();
0211 
0212         if (mParsed.isEmpty()) {
0213             sendError(i18n("The requested help file could not be parsed:<br />%1", file));
0214         } else {
0215             int pos1 = mParsed.indexOf(QLatin1String("charset="));
0216             if (pos1 > 0) {
0217                 int pos2 = mParsed.indexOf(QLatin1Char('"'), pos1);
0218                 if (pos2 > 0) {
0219                     mParsed.replace(pos1, pos2 - pos1, QStringLiteral("charset=UTF-8"));
0220                 }
0221             }
0222             data(mParsed.toUtf8());
0223         }
0224     } else {
0225         // qDebug() << "look for cache for " << file;
0226 
0227         mParsed = lookForCache(file);
0228 
0229         // qDebug() << "cached parsed " << mParsed.length();
0230 
0231         if (mParsed.isEmpty()) {
0232             mParsed = KDocTools::transform(file, KDocTools::locateFileInDtdResource(QStringLiteral("customization/kde-chunk.xsl")));
0233             if (!mParsed.isEmpty()) {
0234                 infoMessage(i18n("Saving to cache"));
0235 #ifdef Q_OS_WIN
0236                 QFileInfo fi(file);
0237                 // make sure filenames do not contain the base path, otherwise
0238                 // accessing user data from another location invalids cached files
0239                 // Accessing user data under a different path is possible
0240                 // when using usb sticks - this may affect unix/mac systems also
0241                 const QString installPath = KDocTools::documentationDirs().last();
0242 
0243                 QString cache = QLatin1Char('/') + fi.absolutePath().remove(installPath, Qt::CaseInsensitive).replace(QLatin1Char('/'), QLatin1Char('_'))
0244                     + QLatin1Char('_') + fi.baseName() + QLatin1Char('.');
0245 #else
0246                 QString cache = file.left(file.length() - 7);
0247 #endif
0248                 KDocTools::saveToCache(mParsed,
0249                                        QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/kio_help") + cache
0250                                            + QLatin1String("cache.bz2"));
0251             }
0252         } else {
0253             infoMessage(i18n("Using cached version"));
0254         }
0255 
0256         // qDebug() << "parsed " << mParsed.length();
0257 
0258         if (mParsed.isEmpty()) {
0259             sendError(i18n("The requested help file could not be parsed:<br />%1", file));
0260         } else {
0261             QString anchor;
0262             QString query = url.query();
0263 
0264             // if we have a query, look if it contains an anchor
0265             if (!query.isEmpty()) {
0266                 const QLatin1String anchorToken("?anchor=");
0267                 if (query.startsWith(anchorToken)) {
0268                     anchor = query.mid(anchorToken.size()).toLower();
0269 
0270                     QUrl redirURL(url);
0271                     redirURL.setQuery(QString());
0272                     redirURL.setFragment(anchor);
0273                     redirection(redirURL);
0274                     return KIO::WorkerResult::pass();
0275                 }
0276             }
0277             if (anchor.isEmpty() && url.hasFragment()) {
0278                 anchor = url.fragment();
0279             }
0280 
0281             // qDebug() << "anchor: " << anchor;
0282 
0283             if (!anchor.isEmpty()) {
0284                 int index = 0;
0285                 while (true) {
0286                     index = mParsed.indexOf(QStringLiteral("<a name="), index);
0287                     if (index == -1) {
0288                         // qDebug() << "no anchor\n";
0289                         break; // use whatever is the target, most likely index.html
0290                     }
0291 
0292                     if (mParsed.mid(index, 11 + anchor.length()).toLower() == QStringLiteral("<a name=\"%1\">").arg(anchor)) {
0293                         index = mParsed.lastIndexOf(QLatin1String("<FILENAME filename="), index) + strlen("<FILENAME filename=\"");
0294                         QString filename = mParsed.mid(index, 2000);
0295                         filename = filename.left(filename.indexOf(QLatin1Char('\"')));
0296                         QString path = target.path();
0297                         path = QStringView(path).left(path.lastIndexOf(QLatin1Char('/')) + 1) + filename;
0298                         target.setPath(path);
0299                         // qDebug() << "anchor found in " << target;
0300                         break;
0301                     }
0302                     ++index;
0303                 }
0304             }
0305             emitFile(target);
0306         }
0307     }
0308 
0309     return KIO::WorkerResult::pass();
0310 }
0311 
0312 void HelpProtocol::emitFile(const QUrl &url)
0313 {
0314     infoMessage(i18n("Looking up section"));
0315 
0316     QString filename = url.path().mid(url.path().lastIndexOf(QLatin1Char('/')) + 1);
0317 
0318     QByteArray result = KDocTools::extractFileToBuffer(mParsed, filename);
0319 
0320     if (result.isNull()) {
0321         sendError(i18n("Could not find filename %1 in %2.", filename, url.toString()));
0322     } else {
0323         data(result);
0324     }
0325     data(QByteArray());
0326 }
0327 
0328 KIO::WorkerResult HelpProtocol::mimetype(const QUrl &)
0329 {
0330     mimeType(QStringLiteral("text/html"));
0331     return KIO::WorkerResult::pass();
0332 }
0333 
0334 // Copied from kio_file to avoid redirects
0335 
0336 static constexpr int s_maxIPCSize = 1024 * 32;
0337 
0338 KIO::WorkerResult HelpProtocol::get_file(const QString &path)
0339 {
0340     // qDebug() << path;
0341 
0342     QFile f(path);
0343     if (!f.exists()) {
0344         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, path);
0345     }
0346     if (!f.open(QIODevice::ReadOnly) || f.isSequential() /*socket, fifo or pipe*/) {
0347         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_OPEN_FOR_READING, path);
0348     }
0349     mimeType(QMimeDatabase().mimeTypeForFile(path).name());
0350     int processed_size = 0;
0351     totalSize(f.size());
0352 
0353     char array[s_maxIPCSize];
0354 
0355     Q_FOREVER {
0356         const qint64 n = f.read(array, sizeof(array));
0357         if (n == -1) {
0358             return KIO::WorkerResult::fail(KIO::ERR_CANNOT_READ, path);
0359         }
0360         if (n == 0) {
0361             break; // Finished
0362         }
0363 
0364         data(QByteArray::fromRawData(array, n));
0365 
0366         processed_size += n;
0367         processedSize(processed_size);
0368     }
0369 
0370     data(QByteArray());
0371     f.close();
0372 
0373     processedSize(f.size());
0374     return KIO::WorkerResult::pass();
0375 }