1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000 Matthias Hoelzer-Kluepfel <hoelzer@kde.org>
4 SPDX-FileCopyrightText: 2001 Stephan Kulow <coolo@kde.org>
5 SPDX-FileCopyrightText: 2003 Cornelius Schumacher <schumacher@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include <config-help.h>
11
12#include "kio_help.h"
13#include "xslt_help.h"
14
15#include <docbookxslt.h>
16
17#include <KLocalizedString>
18
19#include <QDebug>
20
21#include <QDir>
22#include <QFile>
23#include <QFileInfo>
24#include <QMimeDatabase>
25#include <QStandardPaths>
26#include <QUrl>
27
28#include <libxslt/transform.h>
29#include <libxslt/xsltutils.h>
30
31using namespace KIO;
32
33QString HelpProtocol::langLookup(const QString &fname)
34{
35 QStringList search;
36
37 // assemble the local search paths
38 const QStringList localDoc = KDocTools::documentationDirs();
39
40 QStringList langs = KLocalizedString::languages();
41 langs.append(QStringLiteral("en"));
42 langs.removeAll(QStringLiteral("C"));
43
44 auto shouldReplace = [](const QString &l) {
45 return l == QLatin1String("en_US");
46 };
47 // this is kind of compat hack as we install our docs in en/ but the
48 // default language is en_US
49 std::replace_if(first: langs.begin(), last: langs.end(), pred: shouldReplace, QStringLiteral("en"));
50
51 // look up the different languages
52 int ldCount = localDoc.count();
53 search.reserve(asize: ldCount * langs.size());
54 for (int id = 0; id < ldCount; id++) {
55 for (const QString &lang : std::as_const(t&: langs)) {
56 search.append(QStringLiteral("%1/%2/%3").arg(args: localDoc[id], args: lang, args: fname));
57 }
58 }
59
60 auto checkFile = [](const QString &str) {
61 QFileInfo info(str);
62 return info.exists() && info.isFile() && info.isReadable();
63 };
64
65 // try to locate the file
66 for (const QString &path : std::as_const(t&: search)) {
67 // qDebug() << "Looking for help in: " << path;
68
69 if (checkFile(path)) {
70 return path;
71 }
72
73 if (path.endsWith(s: QLatin1String(".html"))) {
74 const QString file = QStringView(path).left(n: path.lastIndexOf(c: QLatin1Char('/'))) + QLatin1String("/index.docbook");
75 // qDebug() << "Looking for help in: " << file;
76 if (checkFile(file)) {
77 return path;
78 }
79 }
80 }
81
82 return QString();
83}
84
85QString HelpProtocol::lookupFile(const QString &fname, const QString &query, bool &redirect)
86{
87 redirect = false;
88
89 const QString &path = fname;
90
91 QString result = langLookup(fname: path);
92 if (result.isEmpty()) {
93 result = langLookup(fname: path + QLatin1String("/index.html"));
94 if (!result.isEmpty()) {
95 QUrl red;
96 red.setScheme(QStringLiteral("help"));
97 red.setPath(path: path + QLatin1String("/index.html"));
98 red.setQuery(query);
99 redirection(url: red);
100 // qDebug() << "redirect to " << red;
101 redirect = true;
102 } else {
103 const QString documentationNotFound = QStringLiteral("kioworker6/help/documentationnotfound/index.html");
104 if (!langLookup(fname: documentationNotFound).isEmpty()) {
105 QUrl red;
106 red.setScheme(QStringLiteral("help"));
107 red.setPath(path: documentationNotFound);
108 red.setQuery(query);
109 redirection(url: red);
110 redirect = true;
111 } else {
112 sendError(i18n("There is no documentation available for %1.", path.toHtmlEscaped()));
113 return QString();
114 }
115 }
116 } else {
117 // qDebug() << "result " << result;
118 }
119
120 return result;
121}
122
123void HelpProtocol::sendError(const QString &t)
124{
125 data(QStringLiteral("<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"></head>\n%1</html>")
126 .arg(a: t.toHtmlEscaped())
127 .toUtf8());
128}
129
130HelpProtocol::HelpProtocol(bool ghelp, const QByteArray &pool, const QByteArray &app)
131 : WorkerBase(ghelp ? QByteArrayLiteral("ghelp") : QByteArrayLiteral("help"), pool, app)
132 , mGhelp(ghelp)
133{
134}
135
136KIO::WorkerResult HelpProtocol::get(const QUrl &url)
137{
138 ////qDebug() << "path=" << url.path()
139 //<< "query=" << url.query();
140
141 bool redirect;
142 QString doc = QDir::cleanPath(path: url.path());
143 if (doc.contains(s: QLatin1String(".."))) {
144 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: url.toString());
145 }
146
147 if (!mGhelp) {
148 if (!doc.startsWith(c: QLatin1Char('/'))) {
149 doc.prepend(c: QLatin1Char('/'));
150 }
151
152 if (doc.endsWith(c: QLatin1Char('/'))) {
153 doc += QLatin1String("index.html");
154 }
155 }
156
157 infoMessage(i18n("Looking up correct file"));
158
159 if (!mGhelp) {
160 doc = lookupFile(fname: doc, query: url.query(), redirect);
161
162 if (redirect) {
163 return KIO::WorkerResult::pass();
164 }
165 }
166
167 if (doc.isEmpty()) {
168 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: url.toString());
169 }
170
171 QUrl target;
172 target.setPath(path: doc);
173 if (url.hasFragment()) {
174 target.setFragment(fragment: url.fragment());
175 }
176
177 // qDebug() << "target " << target;
178
179 QString file = target.isLocalFile() ? target.toLocalFile() : target.path();
180
181 if (mGhelp) {
182 if (!file.endsWith(s: QLatin1String(".xml"))) {
183 return get_file(path: file);
184 }
185 } else {
186 const QString docbook_file = QStringView(file).left(n: file.lastIndexOf(c: QLatin1Char('/'))) + QLatin1String("/index.docbook");
187 if (!QFile::exists(fileName: file)) {
188 file = docbook_file;
189 } else {
190 QFileInfo fi(file);
191 if (fi.isDir()) {
192 file += QLatin1String("/index.docbook");
193 } else {
194 if (!file.endsWith(s: QLatin1String(".html")) || !compareTimeStamps(older: file, newer: docbook_file)) {
195 return get_file(path: file);
196 } else {
197 file = docbook_file;
198 }
199 }
200 }
201 }
202
203 infoMessage(i18n("Preparing document"));
204 mimeType(QStringLiteral("text/html"));
205
206 if (mGhelp) {
207 QString xsl = QStringLiteral("customization/kde-nochunk.xsl");
208 mParsed = KDocTools::transform(file, stylesheet: KDocTools::locateFileInDtdResource(file: xsl));
209
210 // qDebug() << "parsed " << mParsed.length();
211
212 if (mParsed.isEmpty()) {
213 sendError(i18n("The requested help file could not be parsed:<br />%1", file));
214 } else {
215 int pos1 = mParsed.indexOf(s: QLatin1String("charset="));
216 if (pos1 > 0) {
217 int pos2 = mParsed.indexOf(c: QLatin1Char('"'), from: pos1);
218 if (pos2 > 0) {
219 mParsed.replace(i: pos1, len: pos2 - pos1, QStringLiteral("charset=UTF-8"));
220 }
221 }
222 data(data: mParsed.toUtf8());
223 }
224 } else {
225 // qDebug() << "look for cache for " << file;
226
227 mParsed = lookForCache(filename: file);
228
229 // qDebug() << "cached parsed " << mParsed.length();
230
231 if (mParsed.isEmpty()) {
232 mParsed = KDocTools::transform(file, stylesheet: KDocTools::locateFileInDtdResource(QStringLiteral("customization/kde-chunk.xsl")));
233 if (!mParsed.isEmpty()) {
234 infoMessage(i18n("Saving to cache"));
235#ifdef Q_OS_WIN
236 QFileInfo fi(file);
237 // make sure filenames do not contain the base path, otherwise
238 // accessing user data from another location invalids cached files
239 // Accessing user data under a different path is possible
240 // when using usb sticks - this may affect unix/mac systems also
241 const QString installPath = KDocTools::documentationDirs().last();
242
243 QString cache = QLatin1Char('/') + fi.absolutePath().remove(installPath, Qt::CaseInsensitive).replace(QLatin1Char('/'), QLatin1Char('_'))
244 + QLatin1Char('_') + fi.baseName() + QLatin1Char('.');
245#else
246 QString cache = file.left(n: file.length() - 7);
247#endif
248 KDocTools::saveToCache(contents: mParsed,
249 filename: QStandardPaths::writableLocation(type: QStandardPaths::GenericCacheLocation) + QLatin1String("/kio_help") + cache
250 + QLatin1String("cache.bz2"));
251 }
252 } else {
253 infoMessage(i18n("Using cached version"));
254 }
255
256 // qDebug() << "parsed " << mParsed.length();
257
258 if (mParsed.isEmpty()) {
259 sendError(i18n("The requested help file could not be parsed:<br />%1", file));
260 } else {
261 QString anchor;
262 QString query = url.query();
263
264 // if we have a query, look if it contains an anchor
265 if (!query.isEmpty()) {
266 const QLatin1String anchorToken("?anchor=");
267 if (query.startsWith(s: anchorToken)) {
268 anchor = query.mid(position: anchorToken.size()).toLower();
269
270 QUrl redirURL(url);
271 redirURL.setQuery(query: QString());
272 redirURL.setFragment(fragment: anchor);
273 redirection(url: redirURL);
274 return KIO::WorkerResult::pass();
275 }
276 }
277 if (anchor.isEmpty() && url.hasFragment()) {
278 anchor = url.fragment();
279 }
280
281 // qDebug() << "anchor: " << anchor;
282
283 if (!anchor.isEmpty()) {
284 int index = 0;
285 while (true) {
286 index = mParsed.indexOf(QStringLiteral("<a name="), from: index);
287 if (index == -1) {
288 // qDebug() << "no anchor\n";
289 break; // use whatever is the target, most likely index.html
290 }
291
292 if (mParsed.mid(position: index, n: 11 + anchor.length()).toLower() == QStringLiteral("<a name=\"%1\">").arg(a: anchor)) {
293 index = mParsed.lastIndexOf(s: QLatin1String("<FILENAME filename="), from: index) + strlen(s: "<FILENAME filename=\"");
294 QString filename = mParsed.mid(position: index, n: 2000);
295 filename = filename.left(n: filename.indexOf(c: QLatin1Char('\"')));
296 QString path = target.path();
297 path = QStringView(path).left(n: path.lastIndexOf(c: QLatin1Char('/')) + 1) + filename;
298 target.setPath(path);
299 // qDebug() << "anchor found in " << target;
300 break;
301 }
302 ++index;
303 }
304 }
305 emitFile(url: target);
306 }
307 }
308
309 return KIO::WorkerResult::pass();
310}
311
312void HelpProtocol::emitFile(const QUrl &url)
313{
314 infoMessage(i18n("Looking up section"));
315
316 QString filename = url.path().mid(position: url.path().lastIndexOf(c: QLatin1Char('/')) + 1);
317
318 QByteArray result = KDocTools::extractFileToBuffer(content: mParsed, filename);
319
320 if (result.isNull()) {
321 sendError(i18n("Could not find filename %1 in %2.", filename, url.toString()));
322 } else {
323 data(data: result);
324 }
325 data(data: QByteArray());
326}
327
328KIO::WorkerResult HelpProtocol::mimetype(const QUrl &)
329{
330 mimeType(QStringLiteral("text/html"));
331 return KIO::WorkerResult::pass();
332}
333
334// Copied from kio_file to avoid redirects
335
336static constexpr int s_maxIPCSize = 1024 * 32;
337
338KIO::WorkerResult HelpProtocol::get_file(const QString &path)
339{
340 // qDebug() << path;
341
342 QFile f(path);
343 if (!f.exists()) {
344 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: path);
345 }
346 if (!f.open(flags: QIODevice::ReadOnly) || f.isSequential() /*socket, fifo or pipe*/) {
347 return KIO::WorkerResult::fail(error: KIO::ERR_CANNOT_OPEN_FOR_READING, errorString: path);
348 }
349 mimeType(type: QMimeDatabase().mimeTypeForFile(fileName: path).name());
350 int processed_size = 0;
351 totalSize(bytes: f.size());
352
353 char array[s_maxIPCSize];
354
355 Q_FOREVER {
356 const qint64 n = f.read(data: array, maxlen: sizeof(array));
357 if (n == -1) {
358 return KIO::WorkerResult::fail(error: KIO::ERR_CANNOT_READ, errorString: path);
359 }
360 if (n == 0) {
361 break; // Finished
362 }
363
364 data(data: QByteArray::fromRawData(data: array, size: n));
365
366 processed_size += n;
367 processedSize(bytes: processed_size);
368 }
369
370 data(data: QByteArray());
371 f.close();
372
373 processedSize(bytes: f.size());
374 return KIO::WorkerResult::pass();
375}
376

source code of kio/src/kioworkers/help/kio_help.cpp