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 &section)
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 &section)
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>&nbsp;</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>&nbsp;</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 &sect : 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