1/* This file is part of the KDE libraries
2 Copyright (C) 2000 Matthias Hoelzer-Kluepfel <hoelzer@kde.org>
3 Copyright (C) 2001 Stephan Kulow <coolo@kde.org>
4 Copyright (C) 2003 Cornelius Schumacher <schumacher@kde.org>
5
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Library General Public
8 License as published by the Free Software Foundation; either
9 version 2 of the License, or (at your option) any later versio
10
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Library General Public License for more details.
15
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20*/
21
22
23#include <config.h>
24
25#include "kio_help.h"
26#include "xslt.h"
27#include "xslt_help.h"
28
29#include <kdebug.h>
30#include <kde_file.h>
31#include <kurl.h>
32#include <kglobal.h>
33#include <klocale.h>
34#include <kstandarddirs.h>
35#include <kcomponentdata.h>
36
37#include <QtCore/QDir>
38#include <QtCore/QFileInfo>
39#include <QtCore/QFile>
40#include <QtCore/QRegExp>
41#include <QtCore/QTextCodec>
42#include <QtGui/QTextDocument>
43
44
45#ifdef HAVE_SYS_TYPES_H
46# include <sys/types.h>
47#endif
48#ifdef HAVE_SYS_STAT_H
49# include <sys/stat.h>
50#endif
51
52#include <errno.h>
53#include <fcntl.h>
54#ifdef HAVE_STDIO_H
55# include <stdio.h>
56#endif
57#ifdef HAVE_STDLIB_H
58# include <stdlib.h>
59#endif
60
61#include <libxslt/xsltutils.h>
62#include <libxslt/transform.h>
63
64using namespace KIO;
65
66QString HelpProtocol::langLookup(const QString &fname)
67{
68 QStringList search;
69
70 // assemble the local search paths
71 const QStringList localDoc = KGlobal::dirs()->resourceDirs("html");
72
73 QStringList langs = KGlobal::locale()->languageList();
74 langs.append( "en" );
75 langs.removeAll( "C" );
76
77 // this is kind of compat hack as we install our docs in en/ but the
78 // default language is en_US
79 for (QStringList::Iterator it = langs.begin(); it != langs.end(); ++it)
80 if ( *it == "en_US" )
81 *it = "en";
82
83 // look up the different languages
84 int ldCount = localDoc.count();
85 for (int id=0; id < ldCount; id++)
86 {
87 QStringList::ConstIterator lang;
88 for (lang = langs.constBegin(); lang != langs.constEnd(); ++lang)
89 search.append(QString("%1%2/%3").arg(localDoc[id], *lang, fname));
90 }
91
92 // try to locate the file
93 for (QStringList::ConstIterator it = search.constBegin(); it != search.constEnd(); ++it)
94 {
95 kDebug( 7119 ) << "Looking for help in: " << *it;
96
97 QFileInfo info(*it);
98 if (info.exists() && info.isFile() && info.isReadable())
99 return *it;
100
101 if ( ( *it ).endsWith( QLatin1String(".html") ) )
102 {
103 QString file = (*it).left((*it).lastIndexOf('/')) + "/index.docbook";
104 kDebug( 7119 ) << "Looking for help in: " << file;
105 info.setFile(file);
106 if (info.exists() && info.isFile() && info.isReadable())
107 return *it;
108 }
109 }
110
111
112 return QString();
113}
114
115
116QString HelpProtocol::lookupFile(const QString &fname,
117 const QString &query, bool &redirect)
118{
119 redirect = false;
120
121 const QString path = fname;
122
123 QString result = langLookup(path);
124 if (result.isEmpty())
125 {
126 result = langLookup(path+"/index.html");
127 if (!result.isEmpty())
128 {
129 KUrl red( "help:/" );
130 red.setPath( path + "/index.html" );
131 red.setQuery( query );
132 redirection(red);
133 kDebug( 7119 ) << "redirect to " << red.url();
134 redirect = true;
135 }
136 else
137 {
138 const QString documentationNotFound = "khelpcenter/documentationnotfound/index.html";
139 if (!langLookup(documentationNotFound).isEmpty())
140 {
141 KUrl red;
142 red.setProtocol("help");
143 red.setPath(documentationNotFound);
144 red.setQuery(query);
145 redirection(red);
146 redirect = true;
147 }
148 else
149 {
150 unicodeError( i18n("There is no documentation available for %1." , Qt::escape(path)) );
151 return QString();
152 }
153 }
154 } else
155 kDebug( 7119 ) << "result " << result;
156
157 return result;
158}
159
160
161void HelpProtocol::unicodeError( const QString &t )
162{
163#ifdef Q_WS_WIN
164 QString encoding = "UTF-8";
165#else
166 QString encoding = QTextCodec::codecForLocale()->name();
167#endif
168 data(fromUnicode( QString(
169 "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=%1\"></head>\n"
170 "%2</html>" ).arg( encoding, Qt::escape(t) ) ) );
171
172}
173
174HelpProtocol *slave = 0;
175
176HelpProtocol::HelpProtocol( bool ghelp, const QByteArray &pool, const QByteArray &app )
177 : SlaveBase( ghelp ? "ghelp" : "help", pool, app ), mGhelp( ghelp )
178{
179 slave = this;
180}
181
182void HelpProtocol::get( const KUrl& url )
183{
184 kDebug( 7119 ) << "path=" << url.path()
185 << "query=" << url.query();
186
187 bool redirect;
188 QString doc = QDir::cleanPath(url.path());
189 if (doc.contains("..")) {
190 error( KIO::ERR_DOES_NOT_EXIST, url.url() );
191 return;
192 }
193
194 if ( !mGhelp ) {
195 if (!doc.startsWith('/'))
196 doc = doc.prepend(QLatin1Char('/'));
197
198 if (doc.endsWith('/'))
199 doc += "index.html";
200 }
201
202 infoMessage(i18n("Looking up correct file"));
203
204 if ( !mGhelp ) {
205 doc = lookupFile(doc, url.query(), redirect);
206
207 if (redirect)
208 {
209 finished();
210 return;
211 }
212 }
213
214 if (doc.isEmpty())
215 {
216 error( KIO::ERR_DOES_NOT_EXIST, url.url() );
217 return;
218 }
219
220 mimeType("text/html");
221 KUrl target;
222 target.setPath(doc);
223 if (url.hasHTMLRef())
224 target.setHTMLRef(url.htmlRef());
225
226 kDebug( 7119 ) << "target " << target.url();
227
228 QString file = target.scheme() == "file" ? target.toLocalFile() : target.path();
229
230 if ( mGhelp ) {
231 if ( !file.endsWith( QLatin1String( ".xml" ) ) ) {
232 get_file( target );
233 return;
234 }
235 } else {
236 QString docbook_file = file.left(file.lastIndexOf('/')) + "/index.docbook";
237 if (!KStandardDirs::exists(file)) {
238 file = docbook_file;
239 } else {
240 QFileInfo fi(file);
241 if (fi.isDir()) {
242 file = file + "/index.docbook";
243 } else {
244 if ( !file.endsWith( QLatin1String( ".html" ) ) || !compareTimeStamps( file, docbook_file ) ) {
245 get_file( target );
246 return;
247 } else
248 file = docbook_file;
249 }
250 }
251 }
252
253 infoMessage(i18n("Preparing document"));
254
255 if ( mGhelp ) {
256 QString xsl = "customization/kde-nochunk.xsl";
257 mParsed = transform(file, KStandardDirs::locate("dtd", xsl));
258
259 kDebug( 7119 ) << "parsed " << mParsed.length();
260
261 if (mParsed.isEmpty()) {
262 unicodeError( i18n( "The requested help file could not be parsed:<br />%1" , file ) );
263 } else {
264 int pos1 = mParsed.indexOf( "charset=" );
265 if ( pos1 > 0 ) {
266 int pos2 = mParsed.indexOf( '"', pos1 );
267 if ( pos2 > 0 ) {
268 mParsed.replace( pos1, pos2 - pos1, "charset=UTF-8" );
269 }
270 }
271 data( mParsed.toUtf8() );
272 }
273 } else {
274
275 kDebug( 7119 ) << "look for cache for " << file;
276
277 mParsed = lookForCache( file );
278
279 kDebug( 7119 ) << "cached parsed " << mParsed.length();
280
281 if ( mParsed.isEmpty() ) {
282 mParsed = transform(file, KStandardDirs::locate("dtd", "customization/kde-chunk.xsl"));
283 if ( !mParsed.isEmpty() ) {
284 infoMessage( i18n( "Saving to cache" ) );
285#ifdef Q_WS_WIN
286 QFileInfo fi(file);
287 // make sure filenames do not contain the base path, otherwise
288 // accessing user data from another location invalids cached files
289 // Accessing user data under a different path is possible
290 // when using usb sticks - this may affect unix/mac systems also
291 QString cache = '/' + fi.absolutePath().remove(KStandardDirs::installPath("html"),Qt::CaseInsensitive).replace('/','_') + '_' + fi.baseName() + '.';
292#else
293 QString cache = file.left( file.length() - 7 );
294#endif
295 saveToCache( mParsed, KStandardDirs::locateLocal( "cache",
296 "kio_help" + cache +
297 "cache.bz2" ) );
298 }
299 } else infoMessage( i18n( "Using cached version" ) );
300
301 kDebug( 7119 ) << "parsed " << mParsed.length();
302
303 if (mParsed.isEmpty()) {
304 unicodeError( i18n( "The requested help file could not be parsed:<br />%1" , file ) );
305 } else {
306 QString query = url.query(), anchor;
307
308 // if we have a query, look if it contains an anchor
309 if (!query.isEmpty())
310 if (query.startsWith(QLatin1String("?anchor="))) {
311 anchor = query.mid(8).toLower();
312
313 KUrl redirURL(url);
314
315 redirURL.setQuery(QString());
316 redirURL.setHTMLRef(anchor);
317 redirection(redirURL);
318 finished();
319 return;
320 }
321 if (anchor.isEmpty() && url.hasHTMLRef())
322 anchor = url.htmlRef();
323
324 kDebug( 7119 ) << "anchor: " << anchor;
325
326 if ( !anchor.isEmpty() )
327 {
328 int index = 0;
329 while ( true ) {
330 index = mParsed.indexOf( QRegExp( "<a name=" ), index);
331 if ( index == -1 ) {
332 kDebug( 7119 ) << "no anchor\n";
333 break; // use whatever is the target, most likely index.html
334 }
335
336 if ( mParsed.mid( index, 11 + anchor.length() ).toLower() ==
337 QString( "<a name=\"%1\">" ).arg( anchor ) )
338 {
339 index = mParsed.lastIndexOf( "<FILENAME filename=", index ) +
340 strlen( "<FILENAME filename=\"" );
341 QString filename=mParsed.mid( index, 2000 );
342 filename = filename.left( filename.indexOf( '\"' ) );
343 QString path = target.path();
344 path = path.left( path.lastIndexOf( '/' ) + 1) + filename;
345 target.setPath( path );
346 kDebug( 7119 ) << "anchor found in " << target.url();
347 break;
348 }
349 index++;
350 }
351 }
352 emitFile( target );
353 }
354 }
355
356 finished();
357}
358
359void HelpProtocol::emitFile( const KUrl& url )
360{
361 infoMessage(i18n("Looking up section"));
362
363 QString filename = url.path().mid(url.path().lastIndexOf('/') + 1);
364
365 int index = mParsed.indexOf(QString("<FILENAME filename=\"%1\"").arg(filename));
366 if (index == -1) {
367 if ( filename == "index.html" ) {
368 data( fromUnicode( mParsed ) );
369 return;
370 }
371
372 unicodeError( i18n("Could not find filename %1 in %2.", filename, url.url() ) );
373 return;
374 }
375
376 QString filedata = splitOut(mParsed, index);
377 replaceCharsetHeader( filedata );
378
379 data( fromUnicode( filedata ) );
380 data( QByteArray() );
381}
382
383void HelpProtocol::mimetype( const KUrl &)
384{
385 mimeType("text/html");
386 finished();
387}
388
389// Copied from kio_file to avoid redirects
390
391#define MAX_IPC_SIZE (1024*32)
392
393void HelpProtocol::get_file( const KUrl& url )
394{
395 kDebug( 7119 ) << "get_file " << url.url();
396
397#ifdef Q_WS_WIN
398 QFile f( url.toLocalFile() );
399 if ( !f.exists() ) {
400 error( KIO::ERR_DOES_NOT_EXIST, url.url() );
401 return;
402 }
403 if ( !f.open(QIODevice::ReadOnly) ) {
404 error( KIO::ERR_CANNOT_OPEN_FOR_READING, url.path() );
405 return;
406 }
407 int processed_size = 0;
408 totalSize( f.size() );
409
410 QByteArray array;
411 array.resize(MAX_IPC_SIZE);
412
413 while( 1 )
414 {
415 qint64 n = f.read(array.data(),array.size());
416 if (n == -1) {
417 error( KIO::ERR_COULD_NOT_READ, url.path());
418 f.close();
419 return;
420 }
421 if (n == 0)
422 break; // Finished
423
424 data( array );
425
426 processed_size += n;
427 processedSize( processed_size );
428 }
429
430 data( QByteArray() );
431 f.close();
432
433 processedSize( f.size() );
434 finished();
435#else
436 QByteArray _path( QFile::encodeName(url.path()));
437 KDE_struct_stat buff;
438 if ( KDE_stat( _path.data(), &buff ) == -1 ) {
439 if ( errno == EACCES )
440 error( KIO::ERR_ACCESS_DENIED, url.url() );
441 else
442 error( KIO::ERR_DOES_NOT_EXIST, url.url() );
443 return;
444 }
445
446 if ( S_ISDIR( buff.st_mode ) ) {
447 error( KIO::ERR_IS_DIRECTORY, url.path() );
448 return;
449 }
450 if ( S_ISFIFO( buff.st_mode ) || S_ISSOCK ( buff.st_mode ) ) {
451 error( KIO::ERR_CANNOT_OPEN_FOR_READING, url.path() );
452 return;
453 }
454
455 int fd = KDE_open( _path.data(), O_RDONLY);
456 if ( fd < 0 ) {
457 error( KIO::ERR_CANNOT_OPEN_FOR_READING, url.path() );
458 return;
459 }
460
461 totalSize( buff.st_size );
462 int processed_size = 0;
463
464 char buffer[ MAX_IPC_SIZE ];
465 QByteArray array;
466
467 while( 1 )
468 {
469 int n = ::read( fd, buffer, MAX_IPC_SIZE );
470 if (n == -1)
471 {
472 if (errno == EINTR)
473 continue;
474 error( KIO::ERR_COULD_NOT_READ, url.path());
475 ::close(fd);
476 return;
477 }
478 if (n == 0)
479 break; // Finished
480
481 array = array.fromRawData(buffer, n);
482 data( array );
483 array = array.fromRawData(buffer, n);
484
485 processed_size += n;
486 processedSize( processed_size );
487 }
488
489 data( QByteArray() );
490
491 ::close( fd );
492
493 processedSize( buff.st_size );
494
495 finished();
496#endif
497}
498