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 }