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 | |
64 | using namespace KIO; |
65 | |
66 | QString 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 | |
116 | QString 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 | |
161 | void 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 | |
174 | HelpProtocol *slave = 0; |
175 | |
176 | HelpProtocol::HelpProtocol( bool ghelp, const QByteArray &pool, const QByteArray &app ) |
177 | : SlaveBase( ghelp ? "ghelp" : "help" , pool, app ), mGhelp( ghelp ) |
178 | { |
179 | slave = this; |
180 | } |
181 | |
182 | void 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 | |
359 | void 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 | |
383 | void 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 | |
393 | void 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 | |