1 /* This file is part of the KDE libraries
2 SPDX-FileCopyrightText: 2000 Matthias Hoelzer-Kluepfel <mhk@caldera.de>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "kio_man.h"
8 #include "kio_man_debug.h"
9
10 #include <QByteArray>
11 #include <QCoreApplication>
12 #include <QDir>
13 #include <QFile>
14 #include <QTextStream>
15 #include <QMap>
16 #include <QRegularExpression>
17 #include <QStandardPaths>
18 #include <QTextCodec>
19 #include <QProcess>
20
21 #include <KLocalizedString>
22
23 #include "man2html.h"
24 #include <karchive_version.h>
25 #if KARCHIVE_VERSION >= QT_VERSION_CHECK(5, 85, 0)
26 #include <KCompressionDevice>
27 #else
28 #include <KFilterDev>
29 #endif
30
31
32 using namespace KIO;
33
34 // Pseudo plugin class to embed meta data
35 class KIOPluginForMetaData : public QObject
36 {
37 Q_OBJECT
38 Q_PLUGIN_METADATA(IID "org.kde.kio.slave.man" FILE "man.json")
39 };
40
41 MANProtocol *MANProtocol::s_self = nullptr;
42
43 static const char *SGML2ROFF_DIRS = "/usr/lib/sgml";
44 static const char *SGML2ROFF_EXECUTABLE = "sgml2roff";
45
46
47 /*
48 * Drop trailing compression suffix from name
49 */
stripCompression(const QString & name)50 static QString stripCompression(const QString &name)
51 {
52 int pos = name.length();
53
54 if (name.endsWith(".gz")) pos -= 3;
55 else if (name.endsWith(".z", Qt::CaseInsensitive)) pos -= 2;
56 else if (name.endsWith(".bz2")) pos -= 4;
57 else if (name.endsWith(".bz")) pos -= 3;
58 else if (name.endsWith(".lzma")) pos -= 5;
59 else if (name.endsWith(".xz")) pos -= 3;
60
61 return (pos>0 ? name.left(pos) : name);
62 }
63
64 /*
65 * Drop trailing compression suffix and section from name
66 */
stripExtension(const QString & name)67 static QString stripExtension(const QString &name)
68 {
69 QString wc = stripCompression(name);
70 const int pos = wc.lastIndexOf('.');
71 return (pos>0 ? wc.left(pos) : wc);
72 }
73
parseUrl(const QString & _url,QString & title,QString & section)74 static bool parseUrl(const QString &_url, QString &title, QString §ion)
75 {
76 section.clear();
77
78 QString url = _url.trimmed();
79 if (url.isEmpty() || url.startsWith('/')) {
80 if (url.isEmpty() || QFile::exists(url)) {
81 // man:/usr/share/man/man1/ls.1.gz is a valid file
82 title = url;
83 return true;
84 } else {
85 // If a full path is specified but does not exist,
86 // then it is perhaps a normal man page.
87 qCDebug(KIO_MAN_LOG) << url << " does not exist";
88 }
89 }
90
91 while (url.startsWith('/')) url.remove(0, 1);
92 title = url;
93
94 int pos = url.indexOf('(');
95 if (pos < 0)
96 return true; // man:ls -> title=ls
97
98 title = title.left(pos);
99 section = url.mid(pos+1);
100
101 pos = section.indexOf(')');
102 if (pos >= 0) {
103 if (pos < section.length() - 2 && title.isEmpty()) {
104 title = section.mid(pos + 2);
105 }
106 section = section.left(pos);
107 }
108
109 // man:ls(2) -> title="ls", section="2"
110
111 return true;
112 }
113
114
MANProtocol(const QByteArray & pool_socket,const QByteArray & app_socket)115 MANProtocol::MANProtocol(const QByteArray &pool_socket, const QByteArray &app_socket)
116 : QObject(), SlaveBase("man", pool_socket, app_socket)
117 {
118 Q_ASSERT(s_self==nullptr);
119 s_self = this;
120
121 m_sectionNames << "0" << "0p" << "1" << "1p" << "2" << "3" << "3n" << "3p" << "4" << "5" << "6" << "7"
122 << "8" << "9" << "l" << "n";
123
124 const QString cssPath(QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kio_docfilter/kio_docfilter.css"));
125 m_manCSSFile = QFile::encodeName(QUrl::fromLocalFile(cssPath).url());
126 }
127
self()128 MANProtocol *MANProtocol::self()
129 {
130 return s_self;
131 }
132
~MANProtocol()133 MANProtocol::~MANProtocol()
134 {
135 s_self = nullptr;
136 }
137
138
parseWhatIs(QMap<QString,QString> & i,QTextStream & t,const QString & mark)139 void MANProtocol::parseWhatIs( QMap<QString, QString> &i, QTextStream &t, const QString &mark )
140 {
141 const QRegularExpression re(mark);
142 QString l;
143 while ( !t.atEnd() )
144 {
145 l = t.readLine();
146 QRegularExpressionMatch match = re.match(l);
147 int pos = match.capturedStart(0);
148 if (pos != -1)
149 {
150 QString names = l.left(pos);
151 QString descr = l.mid(match.capturedEnd(0));
152 while ((pos = names.indexOf(",")) != -1)
153 {
154 i[names.left(pos++)] = descr;
155 while (names[pos] == ' ')
156 pos++;
157 names = names.mid(pos);
158 }
159 i[names] = descr;
160 }
161 }
162 }
163
addWhatIs(QMap<QString,QString> & i,const QString & name,const QString & mark)164 bool MANProtocol::addWhatIs(QMap<QString, QString> &i, const QString &name, const QString &mark)
165 {
166 QFile f(name);
167 if (!f.open(QIODevice::ReadOnly)) return false;
168
169 QTextStream t(&f);
170 parseWhatIs( i, t, mark );
171 return true;
172 }
173
buildIndexMap(const QString & section)174 QMap<QString, QString> MANProtocol::buildIndexMap(const QString §ion)
175 {
176 qCDebug(KIO_MAN_LOG) << "for section" << section;
177
178 QMap<QString, QString> i;
179 QStringList man_dirs = manDirectories();
180 // Supplementary places for whatis databases
181 man_dirs += m_mandbpath;
182 if (!man_dirs.contains("/var/cache/man"))
183 man_dirs << "/var/cache/man";
184 if (!man_dirs.contains("/var/catman"))
185 man_dirs << "/var/catman";
186
187 QStringList names;
188 names << "whatis.db" << "whatis";
189 const QString mark = "\\s+\\(" + section + "[a-z]*\\)\\s+-\\s+";
190
191 int count0;
192 for (const QString &it_dir : qAsConst(man_dirs))
193 {
194 if (!QFile::exists(it_dir)) continue;
195
196 bool added = false;
197 for (const QString &it_name : qAsConst(names))
198 {
199 count0 = i.count();
200 if (addWhatIs(i, (it_dir+'/'+it_name), mark))
201 {
202 qCDebug(KIO_MAN_LOG) << "added" << (i.count()-count0) << "from" << it_name << "in" << it_dir;
203 added = true;
204 break;
205 }
206 }
207
208 if (!added)
209 {
210 // Nothing was able to be added by scanning the directory,
211 // so try parsing the output of the whatis(1) command.
212 QProcess proc;
213 proc.setProgram("whatis");
214 proc.setArguments(QStringList() << "-M" << it_dir << "-w" << "*");
215 proc.setProcessChannelMode( QProcess::ForwardedErrorChannel );
216 proc.start();
217 proc.waitForFinished();
218 QTextStream t( proc.readAllStandardOutput(), QIODevice::ReadOnly );
219
220 count0 = i.count();
221 parseWhatIs( i, t, mark );
222 qCDebug(KIO_MAN_LOG) << "added" << (i.count()-count0) << "from whatis in" << it_dir;
223 }
224 }
225
226 qCDebug(KIO_MAN_LOG) << "returning" << i.count() << "index entries";
227 return i;
228 }
229
230 //---------------------------------------------------------------------
231
manDirectories()232 QStringList MANProtocol::manDirectories()
233 {
234 checkManPaths();
235 //
236 // Build a list of man directories including translations
237 //
238 QStringList man_dirs;
239 const QList<QLocale> locales = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale::AnyScript, QLocale::AnyCountry);
240
241 for (const QString &it_dir : qAsConst(m_manpath))
242 {
243 // Translated pages in "<mandir>/<lang>" if the directory
244 // exists
245 for (const QLocale &it_loc : locales)
246 {
247 // TODO: languageToString() is wrong, that returns the readable name
248 // of the language. We want the country code returned by name().
249 QString lang = QLocale::languageToString(it_loc.language());
250 if ( !lang.isEmpty() && lang!=QString("C") )
251 {
252 QString dir = it_dir+'/'+lang;
253 QDir d(dir);
254 if (d.exists())
255 {
256 const QString p = d.canonicalPath();
257 if (!man_dirs.contains(p)) man_dirs += p;
258 }
259 }
260 }
261
262 // Untranslated pages in "<mandir>"
263 const QString p = QDir(it_dir).canonicalPath();
264 if (!man_dirs.contains(p)) man_dirs += p;
265 }
266
267 qCDebug(KIO_MAN_LOG) << "returning" << man_dirs.count() << "man directories";
268 return man_dirs;
269 }
270
findPages(const QString & _section,const QString & title,bool full_path)271 QStringList MANProtocol::findPages(const QString &_section,
272 const QString &title,
273 bool full_path)
274 {
275 QStringList list;
276 // qCDebug(KIO_MAN_LOG) << "findPages '" << section << "' '" << title << "'\n";
277 if (title.startsWith('/')) // absolute man page path
278 {
279 list.append(title); // just that and nothing else
280 return list;
281 }
282
283 const QString star("*"); // flag for finding sections
284 const QString man("man"); // prefix for ROFF subdirectories
285 const QString sman("sman"); // prefix for SGML subdirectories
286
287 // Generate a list of applicable sections
288 QStringList sect_list;
289 if (!_section.isEmpty()) // section requested as parameter
290 {
291 sect_list += _section;
292
293 // It's not clear what this code does. If a section with a letter
294 // suffix is specified, for example "3p", both "3p" and "3" are
295 // added to the section list. The result is that "man:(3p)" returns
296 // the same entries as "man:(3)"
297 QString section = _section;
298 while ( (!section.isEmpty()) && (section.at(section.length() - 1).isLetter()) ) {
299 section.truncate(section.length() - 1);
300 sect_list += section;
301 }
302 }
303 else
304 {
305 sect_list += star;
306 }
307
308 const QStringList man_dirs = manDirectories();
309 QStringList nameFilters;
310 nameFilters += man+'*';
311 nameFilters += sman+'*';
312
313 // Find man pages in the sections specified above,
314 // or all sections.
315 //
316 // Do not convert this loop to an iterator, or 'it_s' below
317 // to a reference. The 'sect_list' list can be modified
318 // within the loop.
319 for (int i = 0; i<sect_list.count(); ++i)
320 {
321 const QString it_s = sect_list.at(i);
322 QString it_real = it_s.toLower();
323
324 // Find applicable pages within all man directories.
325 for (const QString &man_dir : man_dirs)
326 {
327 // Find all subdirectories named "man*" and "sman*"
328 // to extend the list of sections and find the correct
329 // case for the section suffix.
330 QDir dp(man_dir);
331 dp.setFilter(QDir::Dirs|QDir::NoDotAndDotDot);
332 dp.setNameFilters(nameFilters);
333 const QStringList entries = dp.entryList();
334 for (const QString &file : entries)
335 {
336 QString sect;
337 if (file.startsWith(man)) sect = file.mid(man.length());
338 else if (file.startsWith(sman)) sect = file.mid(sman.length());
339 // Should never happen, because of the name filter above.
340 if (sect.isEmpty()) continue;
341
342 if (sect.toLower()==it_real) it_real = sect;
343
344 // Only add sect if not already contained, avoid duplicates
345 if (!sect_list.contains(sect) && _section.isEmpty())
346 {
347 //qCDebug(KIO_MAN_LOG) << "another section " << sect;
348 sect_list += sect;
349 }
350 }
351
352 if (it_s!=star) // finding pages, not just sections
353 {
354 const QString dir = man_dir + '/' + man + it_real + '/';
355 list.append(findManPagesInSection(dir, title, full_path));
356
357 const QString sdir = man_dir + '/' + sman + it_real + '/';
358 list.append(findManPagesInSection(sdir, title, full_path));
359 }
360 }
361 }
362
363 //qCDebug(KIO_MAN_LOG) << "finished " << list << " " << sect_list;
364 return list;
365 }
366
findManPagesInSection(const QString & dir,const QString & title,bool full_path)367 QStringList MANProtocol::findManPagesInSection(const QString &dir, const QString &title, bool full_path)
368 {
369 QStringList list;
370
371 qCDebug(KIO_MAN_LOG) << "in" << dir << "title" << title;
372 const bool title_given = !title.isEmpty();
373
374 QDir dp(dir);
375 dp.setFilter(QDir::Files);
376 const QStringList names = dp.entryList();
377 for (const QString &name : names)
378 {
379 if (title_given)
380 {
381 // check title if we're looking for a specific page
382 if (!name.startsWith(title)) continue;
383 // beginning matches, do a more thorough check...
384 const QString tmp_name = stripExtension(name);
385 if (tmp_name!=title) continue;
386 }
387
388 list.append(full_path ? dir+name : name);
389 }
390
391 qCDebug(KIO_MAN_LOG) << "returning" << list.count() << "pages";
392 return (list);
393 }
394
395 //---------------------------------------------------------------------
396
output(const char * insert)397 void MANProtocol::output(const char *insert)
398 {
399 if (insert)
400 {
401 m_outputBuffer.write(insert,strlen(insert));
402 }
403 if (!insert || m_outputBuffer.pos() >= 2048)
404 {
405 m_outputBuffer.close();
406 data(m_outputBuffer.buffer());
407 m_outputBuffer.setData(QByteArray());
408 m_outputBuffer.open(QIODevice::WriteOnly);
409 }
410 }
411
412 #ifndef SIMPLE_MAN2HTML
413 // called by man2html
read_man_page(const char * filename)414 extern char *read_man_page(const char *filename)
415 {
416 return MANProtocol::self()->readManPage(filename);
417 }
418
419 // called by man2html
output_real(const char * insert)420 extern void output_real(const char *insert)
421 {
422 MANProtocol::self()->output(insert);
423 }
424 #endif
425
426 //---------------------------------------------------------------------
427
get(const QUrl & url)428 void MANProtocol::get(const QUrl &url)
429 {
430 qCDebug(KIO_MAN_LOG) << "GET " << url.url();
431
432 // Indicate the mimetype - this will apply to both
433 // formatted man pages and error pages.
434 mimeType("text/html");
435
436 QString title, section;
437 if (!parseUrl(url.path(), title, section))
438 {
439 showMainIndex();
440 finished();
441 return;
442 }
443
444 // see if an index was requested
445 if (url.query().isEmpty() && (title.isEmpty() || title == "/" || title == "."))
446 {
447 if (section == "index" || section.isEmpty())
448 showMainIndex();
449 else
450 showIndex(section);
451 finished();
452 return;
453 }
454
455 QStringList foundPages = findPages(section, title);
456 if (foundPages.isEmpty())
457 {
458 outputError(xi18nc("@info", "No man page matching <resource>%1</resource> could be found."
459 "<nl/><nl/>"
460 "Check that you have not mistyped the name of the page, "
461 "and note that man page names are case sensitive."
462 "<nl/><nl/>"
463 "If the name is correct, then you may need to extend the search path "
464 "for man pages, either using the <envar>MANPATH</envar> environment "
465 "variable or a configuration file in the <filename>/etc</filename> "
466 "directory.", title.toHtmlEscaped()));
467 return;
468 }
469
470 // Sort the list of pages now, for display if required and for
471 // testing for equivalents below.
472 std::sort(foundPages.begin(), foundPages.end());
473 const QString pageFound = foundPages.first();
474
475 if (foundPages.count()>1)
476 {
477 // See if the multiple pages found refer to the same man page, for example
478 // if 'foo.1' and 'foo.1.gz' were both found. To make this generic with
479 // regard to compression suffixes, assume that the first page name (after
480 // the list has been sorted above) is the shortest. Then check that all of
481 // the others are the same with a possible compression suffix added.
482 for (int i = 1; i<foundPages.count(); ++i)
483 {
484 if (!foundPages[i].startsWith(pageFound+'.'))
485 {
486 // There is a page which is not the same as the reference, even
487 // allowing for a compression suffix. Output the list of multiple
488 // pages only.
489 outputMatchingPages(foundPages);
490 finished();
491 return;
492 }
493 }
494 }
495
496 setCssFile(m_manCSSFile);
497 m_outputBuffer.open(QIODevice::WriteOnly);
498 const QByteArray filename = QFile::encodeName(pageFound);
499 const char *buf = readManPage(filename);
500 if (buf==nullptr) return;
501
502 // will call output_real
503 scan_man_page(buf);
504 delete [] buf;
505
506 output(nullptr); // flush
507
508 m_outputBuffer.close();
509 data(m_outputBuffer.buffer());
510 m_outputBuffer.setData(QByteArray());
511
512 // tell we are done
513 data(QByteArray());
514 finished();
515 }
516
517 //---------------------------------------------------------------------
518
519 // If this function returns nullptr to indicate a problem,
520 // then it must call outputError() first.
readManPage(const char * _filename)521 char *MANProtocol::readManPage(const char *_filename)
522 {
523 QByteArray filename = _filename;
524 QByteArray array, dirName;
525
526 // Determine the type of man page file by checking its path
527 // Determination by MIME type with KMimeType doesn't work reliably.
528 // E.g., Solaris 7: /usr/man/sman7fs/pcfs.7fs -> text/x-csrc - WRONG
529 //
530 // If the path name contains a component "sman", assume that it's SGML
531 // and convert it to roff format (used on Solaris). Check for a pathname
532 // component of "sman" only - we don't want a man page called "gasman.1"
533 // to match.
534 //QString file_mimetype = KMimeType::findByPath(QString(filename), 0, false)->name();
535
536 if (QString(filename).contains("/sman/", Qt::CaseInsensitive))
537 {
538 QProcess proc;
539 // Determine path to sgml2roff, if not already done.
540 if (!getProgramPath()) return nullptr;
541 proc.setProgram(mySgml2RoffPath);
542 proc.setArguments(QStringList() << filename);
543 proc.setProcessChannelMode( QProcess::ForwardedErrorChannel );
544 proc.start();
545 proc.waitForFinished();
546 array = proc.readAllStandardOutput();
547 }
548 else
549 {
550 if (QDir::isRelativePath(filename))
551 {
552 qCDebug(KIO_MAN_LOG) << "relative" << filename;
553 filename = QDir::cleanPath(lastdir + '/' + filename).toUtf8();
554 qCDebug(KIO_MAN_LOG) << "resolved to" << filename;
555 }
556
557 lastdir = filename.left(filename.lastIndexOf('/'));
558
559 // get the last directory name (which might be a language name, to be able to guess the encoding)
560 QDir dir(lastdir);
561 dir.cdUp();
562 dirName = QFile::encodeName(dir.dirName());
563
564 if ( !QFile::exists(QFile::decodeName(filename)) ) // if given file does not exist, find with suffix
565 {
566 qCDebug(KIO_MAN_LOG) << "not existing " << filename;
567 QDir mandir(lastdir);
568 const QString nameFilter = filename.mid(filename.lastIndexOf('/') + 1) + ".*";
569 mandir.setNameFilters(QStringList(nameFilter));
570
571 const QStringList entries = mandir.entryList();
572 if (entries.isEmpty())
573 {
574 outputError(xi18nc("@info", "The specified man page references "
575 "another page <filename>%1</filename>,"
576 "<nl/>"
577 "but the referenced page <filename>%2</filename> "
578 "could not be found.",
579 QFile::decodeName(filename),
580 QDir::cleanPath(lastdir + '/' + nameFilter)));
581 return nullptr;
582 }
583
584 filename = lastdir + '/' + QFile::encodeName(entries.first());
585 qCDebug(KIO_MAN_LOG) << "resolved to " << filename;
586 }
587
588 #if KARCHIVE_VERSION >= QT_VERSION_CHECK(5, 85, 0)
589 KCompressionDevice fd(QFile::encodeName(filename));
590 #else
591 KFilterDev fd(QFile::encodeName(filename));
592 #endif
593
594 if (!fd.open(QIODevice::ReadOnly)) {
595 outputError(xi18nc("@info", "The man page <filename>%1</filename> could not be read.", QFile::decodeName(filename)));
596 return nullptr;
597 }
598
599 array = fd.readAll();
600 qCDebug(KIO_MAN_LOG) << "read " << array.size();
601 }
602
603 if (array.isEmpty()) {
604 outputError(xi18nc("@info", "The man page <filename>%1</filename> could not be converted.", QFile::decodeName(filename)));
605 return nullptr;
606 }
607
608 return manPageToUtf8(array, dirName); // never returns nullptr
609 }
610
611 //---------------------------------------------------------------------
612
outputHeader(QTextStream & os,const QString & header,const QString & title)613 void MANProtocol::outputHeader(QTextStream &os, const QString &header, const QString &title)
614 {
615 os.setCodec("UTF-8");
616
617 os << "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Strict//EN\">\n";
618 os << "<html><head>\n";
619 os << "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n";
620 os << "<title>" << (!title.isEmpty() ? title : header) << "</title>\n";
621 if (!m_manCSSFile.isEmpty()) {
622 os << "<link href=\"" << m_manCSSFile << "\" type=\"text/css\" rel=\"stylesheet\">\n";
623 }
624 os << "</head>\n\n";
625 os << "<body>\n";
626 os << "<h1>" << header << "</h1>\n";
627
628 os.flush();
629 }
630
631
632 // This calls SlaveBase::finished(), so do not call any other
633 // SlaveBase functions afterwards. It is assumed that mimeType()
634 // has already been called at the start of get().
outputError(const QString & errmsg)635 void MANProtocol::outputError(const QString& errmsg)
636 {
637 QByteArray array;
638 QTextStream os(&array, QIODevice::WriteOnly);
639
640 outputHeader(os, i18n("Manual Page Viewer Error"));
641 os << errmsg << "\n";
642 os << "</body>\n";
643 os << "</html>\n";
644
645 os.flush();
646 data(array);
647 data(QByteArray());
648 finished();
649 }
650
651
outputMatchingPages(const QStringList & matchingPages)652 void MANProtocol::outputMatchingPages(const QStringList &matchingPages)
653 {
654 QByteArray array;
655 QTextStream os(&array, QIODevice::WriteOnly);
656
657 outputHeader(os, i18n("There is more than one matching man page:"), i18n("Multiple Manual Pages"));
658 os << "<ul>\n";
659
660 int acckey = 1;
661 for (const QString &page : matchingPages)
662 {
663 os << "<li><a href='man:" << page << "' accesskey='" << acckey << "'>" << page << "</a><br>\n<br>\n";
664 ++acckey;
665 }
666
667 os << "</ul>\n";
668 os << "<hr>\n";
669 os << "<p>" << i18n("Note: if you read a man page in your language,"
670 " be aware it can contain some mistakes or be obsolete."
671 " In case of doubt, you should have a look at the English version.") << "</p>";
672
673 os << "</body>\n";
674 os << "</html>\n";
675 os.flush();
676 data(array);
677 // Do not call finished(), the caller will do that
678 }
679
680
stat(const QUrl & url)681 void MANProtocol::stat( const QUrl& url)
682 {
683 qCDebug(KIO_MAN_LOG) << "STAT " << url.url();
684
685 QString title, section;
686
687 if (!parseUrl(url.path(), title, section))
688 {
689 error(KIO::ERR_MALFORMED_URL, url.url());
690 return;
691 }
692
693 qCDebug(KIO_MAN_LOG) << "URL" << url.url() << "parsed to title" << title << "section" << section;
694
695 UDSEntry entry;
696 entry.reserve(3);
697 entry.fastInsert(KIO::UDSEntry::UDS_NAME, title);
698 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
699 entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("text/html"));
700
701 statEntry(entry);
702 finished();
703 }
704
705
706 extern "C"
707 {
708
kdemain(int argc,char ** argv)709 int Q_DECL_EXPORT kdemain( int argc, char **argv ) {
710
711 QCoreApplication app(argc, argv);
712 app.setApplicationName(QLatin1String("kio_man"));
713
714 qCDebug(KIO_MAN_LOG) << "STARTING";
715
716 if (argc != 4)
717 {
718 fprintf(stderr, "Usage: kio_man protocol domain-socket1 domain-socket2\n");
719 exit(-1);
720 }
721
722 MANProtocol slave(argv[2], argv[3]);
723 slave.dispatchLoop();
724
725 qCDebug(KIO_MAN_LOG) << "Done";
726
727 return 0;
728 }
729
730 }
731
mimetype(const QUrl &)732 void MANProtocol::mimetype(const QUrl & /*url*/)
733 {
734 mimeType("text/html");
735 finished();
736 }
737
738 //---------------------------------------------------------------------
739
sectionName(const QString & section)740 static QString sectionName(const QString& section)
741 {
742 if (section == "0") return i18n("Header Files");
743 else if (section == "0p") return i18n("Header Files (POSIX)");
744 else if (section == "1") return i18n("User Commands");
745 else if (section == "1p") return i18n("User Commands (POSIX)");
746 else if (section == "2") return i18n("System Calls");
747 else if (section == "3") return i18n("Subroutines");
748 // TODO: current usage of '3p' seems to be "Subroutines (POSIX)"
749 else if (section == "3p") return i18n("Perl Modules");
750 else if (section == "3n") return i18n("Network Functions");
751 else if (section == "4") return i18n("Devices");
752 else if (section == "5") return i18n("File Formats");
753 else if (section == "6") return i18n("Games");
754 else if (section == "7") return i18n("Miscellaneous");
755 else if (section == "8") return i18n("System Administration");
756 else if (section == "9") return i18n("Kernel");
757 else if (section == "l") return i18n("Local Documentation");
758 // TODO: current usage of 'n' seems to be TCL commands
759 else if (section == "n") return i18n("New");
760
761 return QString();
762 }
763
buildSectionList(const QStringList & dirs) const764 QStringList MANProtocol::buildSectionList(const QStringList& dirs) const
765 {
766 QStringList l;
767 for (const QString &it_sect : qAsConst(m_sectionNames))
768 {
769 for (const QString &it_dir : dirs)
770 {
771 QDir d(it_dir+"/man"+it_sect);
772 if (d.exists())
773 {
774 l << it_sect;
775 break;
776 }
777 }
778 }
779 return l;
780 }
781
782 //---------------------------------------------------------------------
783
showMainIndex()784 void MANProtocol::showMainIndex()
785 {
786 QByteArray array;
787 QTextStream os(&array, QIODevice::WriteOnly);
788
789 outputHeader(os, i18n("Main Manual Page Index"));
790
791 // ### TODO: why still the environment variable
792 // if keeping it, also use it in listDir()
793 const QString sectList = qgetenv("MANSECT");
794 QStringList sections;
795 if (sectList.isEmpty())
796 sections = buildSectionList(manDirectories());
797 else
798 sections = sectList.split(':');
799
800 os << "<table>\n";
801
802 QSet<QChar> accessKeys;
803 char alternateAccessKey = 'a';
804 for (const QString &it_sect : qAsConst(sections))
805 {
806 if (it_sect.isEmpty()) continue; // guard back() below
807
808 // create a unique access key
809 QChar accessKey = it_sect.back(); // rightmost char
810 while (accessKeys.contains(accessKey)) accessKey = alternateAccessKey++;
811 accessKeys.insert(accessKey);
812
813 os << "<tr><td><a href=\"man:(" << it_sect << ")\" accesskey=\"" << accessKey
814 << "\">" << i18n("Section %1", it_sect)
815 << "</a></td><td> </td><td> " << sectionName(it_sect) << "</td></tr>\n";
816 }
817
818 os << "</table>\n";
819 os << "</body>\n";
820 os << "</html>\n";
821 os.flush();
822 data(array);
823 data(QByteArray());
824 // Do not call finished(), the caller will do that
825 }
826
827 //---------------------------------------------------------------------
828
829 // TODO: constr_catmanpath modified here, should parameter be passed by reference?
constructPath(QStringList & constr_path,QStringList constr_catmanpath)830 void MANProtocol::constructPath(QStringList& constr_path, QStringList constr_catmanpath)
831 {
832 QMap<QString, QString> manpath_map;
833 QMap<QString, QString> mandb_map;
834
835 // Add paths from /etc/man.conf
836 //
837 // Explicit manpaths may be given by lines starting with "MANPATH" or
838 // "MANDATORY_MANPATH" (depending on system ?).
839 // Mappings from $PATH to manpath are given by lines starting with
840 // "MANPATH_MAP"
841
842 // The entry is e.g. "MANDATORY_MANPATH <manpath>"
843 const QRegularExpression manpath_regex("^(?:MANPATH|MANDATORY_MANPATH)\\s+(\\S+)");
844
845 // The entry is "MANPATH_MAP <path> <manpath>"
846 const QRegularExpression manpath_map_regex("^MANPATH_MAP\\s+(\\S+)\\s+(\\S+)");
847
848 // The entry is "MANDB_MAP <manpath> <catmanpath>"
849 const QRegularExpression mandb_map_regex("^MANDB_MAP\\s+(\\S+)\\s+(\\S+)");
850
851 QFile mc("/etc/man.conf"); // Caldera
852 if (!mc.exists())
853 mc.setFileName("/etc/manpath.config"); // SuSE, Debian
854 if (!mc.exists())
855 mc.setFileName("/etc/man.config"); // Mandrake
856
857 if (mc.open(QIODevice::ReadOnly))
858 {
859 QTextStream is(&mc);
860 is.setCodec( QTextCodec::codecForLocale () );
861
862 while (!is.atEnd())
863 {
864 const QString line = is.readLine();
865
866 QRegularExpressionMatch rmatch;
867 if (line.contains(manpath_regex, &rmatch)) {
868 constr_path += rmatch.captured(1);
869 } else if (line.contains(manpath_map_regex, &rmatch)) {
870 const QString dir = QDir::cleanPath(rmatch.captured(1));
871 const QString mandir = QDir::cleanPath(rmatch.captured(2));
872 manpath_map[dir] = mandir;
873 } else if (line.contains(mandb_map_regex, &rmatch)) {
874 const QString mandir = QDir::cleanPath(rmatch.captured(1));
875 const QString catmandir = QDir::cleanPath(rmatch.captured(2));
876 mandb_map[mandir] = catmandir;
877 }
878 /* sections are not used
879 else if ( section_regex.find(line, 0) == 0 )
880 {
881 if ( !conf_section.isEmpty() )
882 conf_section += ':';
883 conf_section += line.mid(8).trimmed();
884 }
885 */
886 }
887 mc.close();
888 }
889
890 // Default paths
891 static const char * const manpaths[] = {
892 "/usr/X11/man",
893 "/usr/X11R6/man",
894 "/usr/man",
895 "/usr/local/man",
896 "/usr/exp/man",
897 "/usr/openwin/man",
898 "/usr/dt/man",
899 "/opt/freetool/man",
900 "/opt/local/man",
901 "/usr/tex/man",
902 "/usr/www/man",
903 "/usr/lang/man",
904 "/usr/gnu/man",
905 "/usr/share/man",
906 "/usr/motif/man",
907 "/usr/titools/man",
908 "/usr/sunpc/man",
909 "/usr/ncd/man",
910 "/usr/newsprint/man",
911 nullptr
912 };
913
914
915 int i = 0;
916 while (manpaths[i]) {
917 if ( constr_path.indexOf( QString( manpaths[i] ) ) == -1 )
918 constr_path += QString( manpaths[i] );
919 i++;
920 }
921
922 // Directories in $PATH
923 // - if a manpath mapping exists, use that mapping
924 // - if a directory "<path>/man" or "<path>/../man" exists, add it
925 // to the man path (the actual existence check is done further down)
926
927 if ( ::getenv("PATH") ) {
928 const QStringList path =
929 QString::fromLocal8Bit( ::getenv("PATH") ).split( ':', Qt::SkipEmptyParts );
930
931 for (const QString &it : qAsConst(path))
932 {
933 const QString dir = QDir::cleanPath(it);
934 QString mandir = manpath_map[ dir ];
935
936 if ( !mandir.isEmpty() ) {
937 // a path mapping exists
938 if ( constr_path.indexOf( mandir ) == -1 )
939 constr_path += mandir;
940 }
941 else {
942 // no manpath mapping, use "<path>/man" and "<path>/../man"
943
944 mandir = dir + QString( "/man" );
945 if ( constr_path.indexOf( mandir ) == -1 )
946 constr_path += mandir;
947
948 int pos = dir.lastIndexOf( '/' );
949 if ( pos > 0 ) {
950 mandir = dir.left( pos ) + QString("/man");
951 if ( constr_path.indexOf( mandir ) == -1 )
952 constr_path += mandir;
953 }
954 }
955 QString catmandir = mandb_map[ mandir ];
956 if ( !mandir.isEmpty() )
957 {
958 if ( constr_catmanpath.indexOf( catmandir ) == -1 )
959 constr_catmanpath += catmandir;
960 }
961 else
962 {
963 // What is the default mapping?
964 catmandir = mandir;
965 catmandir.replace("/usr/share/","/var/cache/");
966 if ( constr_catmanpath.indexOf( catmandir ) == -1 )
967 constr_catmanpath += catmandir;
968 }
969 }
970 }
971 }
972
checkManPaths()973 void MANProtocol::checkManPaths()
974 {
975 static bool inited = false;
976 if (inited) return;
977 inited = true;
978
979 const QString manpath_env = qgetenv("MANPATH");
980
981 // Decide if $MANPATH is enough on its own or if it should be merged
982 // with the constructed path.
983 // A $MANPATH starting or ending with ":", or containing "::",
984 // should be merged with the constructed path.
985
986 const bool construct_path = (manpath_env.isEmpty() ||
987 manpath_env.startsWith(':') ||
988 manpath_env.endsWith(':') ||
989 manpath_env.contains("::"));
990
991 // Constructed man path -- consists of paths from
992 // /etc/man.conf
993 // default dirs
994 // $PATH
995 QStringList constr_path;
996 QStringList constr_catmanpath; // catmanpath
997
998 if (construct_path) // need to read config file
999 {
1000 constructPath(constr_path, constr_catmanpath);
1001 }
1002
1003 m_mandbpath = constr_catmanpath;
1004
1005 // Merge $MANPATH with the constructed path to form the
1006 // actual manpath.
1007 //
1008 // The merging syntax with ":" and "::" in $MANPATH will be
1009 // satisfied if any empty string in path_list_env (there
1010 // should be 1 or 0) is replaced by the constructed path.
1011
1012 const QStringList path_list_env = manpath_env.split(':', Qt::KeepEmptyParts);
1013 for (const QString &dir : path_list_env)
1014 {
1015 if (!dir.isEmpty()) // non empty part - add if exists
1016 {
1017 if (m_manpath.contains(dir)) continue; // ignore if already present
1018 if (QDir(dir).exists()) m_manpath += dir; // add to list if exists
1019 }
1020 else // empty part - merge constructed path
1021 {
1022 // Insert constructed path ($MANPATH was empty, or
1023 // there was a ":" at either end or "::" within)
1024 for (const QString &dir2 : qAsConst(constr_path))
1025 {
1026 if (dir2.isEmpty()) continue; // don't add a null entry
1027 if (m_manpath.contains(dir2)) continue; // ignore if already present
1028 // add to list if exists
1029 if (QDir(dir2).exists()) m_manpath += dir2;
1030 }
1031 }
1032 }
1033
1034 /* sections are not used
1035 // Sections
1036 QStringList m_mansect = mansect_env.split( ':', Qt::KeepEmptyParts);
1037
1038 const char* const default_sect[] =
1039 { "1", "2", "3", "4", "5", "6", "7", "8", "9", "n", 0L };
1040
1041 for ( int i = 0; default_sect[i] != 0L; i++ )
1042 if ( m_mansect.indexOf( QString( default_sect[i] ) ) == -1 )
1043 m_mansect += QString( default_sect[i] );
1044 */
1045
1046 qCDebug(KIO_MAN_LOG) << "manpath" << m_manpath;
1047 }
1048
1049
1050 // Setup my own structure, with char pointers.
1051 // from now on only pointers are copied, no strings
1052 //
1053 // containing the whole path string,
1054 // the beginning of the man page name
1055 // and the length of the name
1056 struct man_index_t {
1057 char *manpath; // the full path including man file
1058 const char *manpage_begin; // pointer to the begin of the man file name in the path
1059 int manpage_len; // len of the man file name
1060 };
1061 typedef man_index_t *man_index_ptr;
1062
compare_man_index(const void * s1,const void * s2)1063 int compare_man_index(const void *s1, const void *s2)
1064 {
1065 struct man_index_t *m1 = *(struct man_index_t **)s1;
1066 struct man_index_t *m2 = *(struct man_index_t **)s2;
1067 int i;
1068 // Compare the names of the pages
1069 // with the shorter length.
1070 // Man page names are not '\0' terminated, so
1071 // this is a bit tricky
1072 if ( m1->manpage_len > m2->manpage_len)
1073 {
1074 i = qstrnicmp(m1->manpage_begin,
1075 m2->manpage_begin,
1076 m2->manpage_len);
1077 if (!i)
1078 return 1;
1079 return i;
1080 }
1081
1082 if ( m1->manpage_len < m2->manpage_len)
1083 {
1084 i = qstrnicmp(m1->manpage_begin,
1085 m2->manpage_begin,
1086 m1->manpage_len);
1087 if (!i)
1088 return -1;
1089 return i;
1090 }
1091
1092 return qstrnicmp(m1->manpage_begin,
1093 m2->manpage_begin,
1094 m1->manpage_len);
1095 }
1096
showIndex(const QString & section)1097 void MANProtocol::showIndex(const QString& section)
1098 {
1099 QByteArray array_h;
1100 QTextStream os_h(&array_h, QIODevice::WriteOnly);
1101
1102 // print header
1103 outputHeader(os_h, i18n( "Index for section %1: %2", section, sectionName(section)), i18n("Manual Page Index"));
1104
1105 QByteArray array_d;
1106 QTextStream os(&array_d, QIODevice::WriteOnly);
1107 os.setCodec( "UTF-8" );
1108 os << "<div class=\"secidxmain\">\n";
1109
1110 // compose list of search paths -------------------------------------------------------------
1111
1112 checkManPaths();
1113 infoMessage(i18n("Generating Index"));
1114
1115 // search for the man pages
1116 QStringList pages = findPages( section, QString() );
1117
1118 if (pages.isEmpty()) // not a single page found
1119 {
1120 // print footer
1121 os << "</div></body></html>\n";
1122 os.flush();
1123 os_h.flush();
1124
1125 infoMessage(QString());
1126 data(array_h + array_d);
1127 return;
1128 }
1129
1130 QMap<QString, QString> indexmap = buildIndexMap(section);
1131
1132 // print out the list
1133 os << "<br/><br/>\n";
1134 os << "<table>\n";
1135
1136 int listlen = pages.count();
1137 man_index_ptr *indexlist = new man_index_ptr[listlen];
1138 listlen = 0;
1139
1140 QStringList::const_iterator page;
1141 for (const QString &page : qAsConst(pages))
1142 {
1143 // I look for the beginning of the man page name
1144 // i.e. "bla/pagename.3.gz" by looking for the last "/"
1145 // Then look for the end of the name by searching backwards
1146 // for the last ".", not counting zip extensions.
1147 // If the len of the name is >0,
1148 // store it in the list structure, to be sorted later
1149
1150 struct man_index_t *manindex = new man_index_t;
1151 manindex->manpath = strdup(page.toUtf8());
1152
1153 manindex->manpage_begin = strrchr(manindex->manpath, '/');
1154 if (manindex->manpage_begin)
1155 {
1156 manindex->manpage_begin++;
1157 Q_ASSERT(manindex->manpage_begin >= manindex->manpath);
1158 }
1159 else
1160 {
1161 manindex->manpage_begin = manindex->manpath;
1162 Q_ASSERT(manindex->manpage_begin >= manindex->manpath);
1163 }
1164
1165 // Skip extension ".section[.gz]"
1166
1167 const char *begin = manindex->manpage_begin;
1168 const int len = strlen( begin );
1169 const char *end = begin+(len-1);
1170
1171 if ( len >= 3 && strcmp( end-2, ".gz" ) == 0 )
1172 end -= 3;
1173 else if ( len >= 2 && strcmp( end-1, ".Z" ) == 0 )
1174 end -= 2;
1175 else if ( len >= 2 && strcmp( end-1, ".z" ) == 0 )
1176 end -= 2;
1177 else if ( len >= 4 && strcmp( end-3, ".bz2" ) == 0 )
1178 end -= 4;
1179 else if ( len >= 5 && strcmp( end-4, ".lzma" ) == 0 )
1180 end -= 5;
1181 else if ( len >= 3 && strcmp( end-2, ".xz" ) == 0 )
1182 end -= 3;
1183
1184 while ( end >= begin && *end != '.' )
1185 end--;
1186
1187 if (end <begin)
1188 {
1189 // no '.' ending ???
1190 // set the pointer past the end of the filename
1191 manindex->manpage_len = page.length();
1192 manindex->manpage_len -= (manindex->manpage_begin - manindex->manpath);
1193 Q_ASSERT(manindex->manpage_len >= 0);
1194 }
1195 else
1196 {
1197 const char *manpage_end = end;
1198 manindex->manpage_len = (manpage_end - manindex->manpage_begin);
1199 Q_ASSERT(manindex->manpage_len >= 0);
1200 }
1201
1202 if (manindex->manpage_len>0)
1203 {
1204 indexlist[listlen] = manindex;
1205 listlen++;
1206 }
1207 else delete manindex;
1208 }
1209
1210 //
1211 // Now do the sorting on the page names
1212 // and the printout afterwards
1213 // While printing avoid duplicate man page names
1214 //
1215
1216 struct man_index_t dummy_index = {nullptr,nullptr,0};
1217 struct man_index_t *last_index = &dummy_index;
1218
1219 // sort and print
1220 qsort(indexlist, listlen, sizeof(struct man_index_t *), compare_man_index);
1221
1222 QChar firstchar, tmp;
1223 QString indexLine="<div class=\"secidxshort\">\n";
1224 if (indexlist[0]->manpage_len>0)
1225 {
1226 firstchar=QChar((indexlist[0]->manpage_begin)[0]).toLower();
1227
1228 const QString appendixstr = QString(
1229 " [<a href=\"#%1\" accesskey=\"%2\">%3</a>]\n"
1230 ).arg(firstchar).arg(firstchar).arg(firstchar);
1231 indexLine.append(appendixstr);
1232 }
1233 os << "<tr><td class=\"secidxnextletter\"" << " colspan=\"3\">\n";
1234 os << "<a name=\"" << firstchar << "\">" << firstchar <<"</a>\n";
1235 os << "</td></tr>\n";
1236
1237 for (int i=0; i<listlen; i++)
1238 {
1239 struct man_index_t *manindex = indexlist[i];
1240
1241 // qstrncmp():
1242 // "last_man" has already a \0 string ending, but
1243 // "manindex->manpage_begin" has not,
1244 // so do compare at most "manindex->manpage_len" of the strings.
1245 if (last_index->manpage_len == manindex->manpage_len &&
1246 !qstrncmp(last_index->manpage_begin,
1247 manindex->manpage_begin,
1248 manindex->manpage_len)
1249 )
1250 {
1251 continue;
1252 }
1253
1254 tmp=QChar((manindex->manpage_begin)[0]).toLower();
1255 if (firstchar != tmp)
1256 {
1257 firstchar = tmp;
1258 os << "<tr><td class=\"secidxnextletter\"" << " colspan=\"3\">\n <a name=\""
1259 << firstchar << "\">" << firstchar << "</a>\n</td></tr>\n";
1260
1261 const QString appendixstr = QString(
1262 " [<a href=\"#%1\" accesskey=\"%2\">%3</a>]\n"
1263 ).arg(firstchar).arg(firstchar).arg(firstchar);
1264 indexLine.append(appendixstr);
1265 }
1266 os << "<tr><td><a href=\"man:"
1267 << manindex->manpath << "\">\n";
1268
1269 ((char *)manindex->manpage_begin)[manindex->manpage_len] = '\0';
1270 os << manindex->manpage_begin
1271 << "</a></td><td> </td><td> "
1272 << (indexmap.contains(manindex->manpage_begin) ? indexmap[manindex->manpage_begin] : "" )
1273 << "</td></tr>\n";
1274 last_index = manindex;
1275 }
1276 indexLine.append("</div>");
1277
1278 for (int i=0; i<listlen; i++) {
1279 ::free(indexlist[i]->manpath); // allocated by strdup
1280 delete indexlist[i];
1281 }
1282
1283 delete [] indexlist;
1284
1285 os << "</table></div>\n";
1286 os << "<br/><br/>\n";
1287
1288 os << indexLine << '\n';
1289
1290 // print footer
1291 os << "</body></html>\n";
1292
1293 // set the links "toolbar" also at the top
1294 os_h << indexLine << '\n';
1295 os.flush();
1296 os_h.flush();
1297
1298 infoMessage(QString());
1299 data(array_h + array_d);
1300 // Do not call finished(), the caller will do that
1301 }
1302
listDir(const QUrl & url)1303 void MANProtocol::listDir(const QUrl &url)
1304 {
1305 qCDebug(KIO_MAN_LOG) << url;
1306
1307 QString title;
1308 QString section;
1309
1310 if ( !parseUrl(url.path(), title, section) ) {
1311 error( KIO::ERR_MALFORMED_URL, url.url() );
1312 return;
1313 }
1314
1315 // stat() and listDir() declared that everything is a HTML file.
1316 // However we can list man: and man:(1) as a directory (e.g. in dolphin).
1317 // But we cannot list man:ls as a directory, this makes no sense (#154173).
1318
1319 if (!title.isEmpty() && title != "/") {
1320 error(KIO::ERR_IS_FILE, url.url());
1321 return;
1322 }
1323
1324 // There is no need to accumulate a list of UDSEntry's and deliver
1325 // them all in one block with SlaveBase::listEntries(), because
1326 // SlaveBase::listEntry() batches the entries and delivers them
1327 // timed to maximise application performance.
1328
1329 UDSEntry uds_entry;
1330 uds_entry.reserve(4);
1331
1332 uds_entry.fastInsert(KIO::UDSEntry::UDS_NAME, ".");
1333 uds_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
1334 listEntry(uds_entry);
1335
1336 if (section.isEmpty()) // list the top level directory
1337 {
1338 for (const QString § : m_sectionNames)
1339 {
1340 uds_entry.clear(); // sectionName() is already I18N'ed
1341 uds_entry.fastInsert( KIO::UDSEntry::UDS_NAME, (sect + " - " + sectionName(sect)) );
1342 uds_entry.fastInsert( KIO::UDSEntry::UDS_URL, ("man:/(" + sect + ')') );
1343 uds_entry.fastInsert( KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR );
1344 listEntry(uds_entry);
1345 }
1346 }
1347 else // list all pages in a section
1348 {
1349 const QStringList list = findPages(section, QString());
1350 for (const QString &page : list)
1351 {
1352 // Remove any compression suffix present
1353 QString name = stripCompression(page);
1354 // Remove any preceding pathname components, just leave the base name
1355 int pos = name.lastIndexOf('/');
1356 if (pos>0) name = name.mid(pos+1);
1357 // Reformat the section suffix into the standard form
1358 pos = name.lastIndexOf('.');
1359 if (pos>0) name = name.left(pos)+" ("+name.mid(pos+1)+')';
1360
1361 uds_entry.clear();
1362 uds_entry.fastInsert(KIO::UDSEntry::UDS_NAME, name);
1363 uds_entry.fastInsert(KIO::UDSEntry::UDS_URL, ("man:" + page));
1364 uds_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
1365 uds_entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("text/html"));
1366 listEntry(uds_entry);
1367 }
1368 }
1369
1370 finished();
1371 }
1372
1373
getProgramPath()1374 bool MANProtocol::getProgramPath()
1375 {
1376 if (!mySgml2RoffPath.isEmpty())
1377 return true;
1378
1379 mySgml2RoffPath = QStandardPaths::findExecutable(SGML2ROFF_EXECUTABLE);
1380 if (!mySgml2RoffPath.isEmpty())
1381 return true;
1382
1383 /* sgml2roff isn't found in PATH. Check some possible locations where it may be found. */
1384 mySgml2RoffPath = QStandardPaths::findExecutable(SGML2ROFF_EXECUTABLE, QStringList(QLatin1String(SGML2ROFF_DIRS)));
1385 if (!mySgml2RoffPath.isEmpty())
1386 return true;
1387
1388 /* Cannot find sgml2roff program: */
1389 outputError(xi18nc("@info", "Could not find the <command>%1</command> program on your system. "
1390 "Please install it if necessary, and ensure that it can be found using "
1391 "the environment variable <envar>PATH</envar>.", SGML2ROFF_EXECUTABLE));
1392 return false;
1393 }
1394
1395 #include "kio_man.moc"
1396