File indexing completed on 2024-04-28 05:49:19

0001 /***************************************************************************
0002                            plugin_katexmlcheck.cpp - checks XML files using xmllint
0003                            -------------------
0004     begin                : 2002-07-06
0005     copyright            : (C) 2002 by Daniel Naber
0006     email                : daniel.naber@t-online.de
0007  ***************************************************************************/
0008 
0009 /***************************************************************************
0010  This program is free software; you can redistribute it and/or
0011  modify it under the terms of the GNU General Public License
0012  as published by the Free Software Foundation; either version 2
0013  of the License, or (at your option) any later version.
0014 
0015  This program is distributed in the hope that it will be useful,
0016  but WITHOUT ANY WARRANTY; without even the implied warranty of
0017  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0018  GNU General Public License for more details.
0019 
0020  You should have received a copy of the GNU General Public License
0021  along with this program; if not, write to the Free Software
0022  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
0023  ***************************************************************************/
0024 
0025 /*
0026 -fixme: show dock if "Validate XML" is selected (doesn't currently work when Kate
0027  was just started and the dockwidget isn't yet visible)
0028 -fixme(?): doesn't correctly disappear when deactivated in config
0029 */
0030 
0031 // TODO:
0032 // Cleanup unneeded headers
0033 // Find resources and translate i18n messages
0034 // all translations were deleted in https://websvn.kde.org/?limit_changes=0&view=revision&revision=1433517
0035 // What to do with catalogs? What is it for?
0036 // Implement hot key shortcut to do xml validation
0037 // Remove copyright above due to author orphaned this plugin?
0038 // Possibility to check only well-formdness without validation
0039 // Hide output in dock when switching to another tab
0040 // Make ability to validate against xml schema and then edit docbook
0041 // Should del space in [km] strang in katexmlcheck.desktop?
0042 // Which variant should I choose? QUrl.adjusted(rm filename).path() or QUrl.toString(rm filename|rm schema)
0043 // What about replace xmllint xmlstarlet or something?
0044 // Maybe use QXmlReader to take dtds and xsds?
0045 
0046 #include "plugin_katexmlcheck.h"
0047 
0048 #include <KActionCollection>
0049 #include <QApplication>
0050 #include <QFile>
0051 #include <QString>
0052 #include <QTextStream>
0053 
0054 #include "hostprocess.h"
0055 #include "ktexteditor_utils.h"
0056 
0057 #include <KLocalizedString>
0058 #include <KPluginFactory>
0059 #include <QAction>
0060 #include <QTemporaryFile>
0061 
0062 #include <QFile>
0063 #include <QPushButton>
0064 #include <QRegularExpression>
0065 #include <QStandardPaths>
0066 #include <QString>
0067 #include <QUrl>
0068 
0069 #include <ktexteditor/editor.h>
0070 
0071 #include <kxmlguifactory.h>
0072 #include <qregularexpression.h>
0073 
0074 K_PLUGIN_FACTORY_WITH_JSON(PluginKateXMLCheckFactory, "katexmlcheck.json", registerPlugin<PluginKateXMLCheck>();)
0075 
0076 PluginKateXMLCheck::PluginKateXMLCheck(QObject *const parent, const QVariantList &)
0077     : KTextEditor::Plugin(parent)
0078 {
0079 }
0080 
0081 PluginKateXMLCheck::~PluginKateXMLCheck()
0082 {
0083 }
0084 
0085 QObject *PluginKateXMLCheck::createView(KTextEditor::MainWindow *mainWindow)
0086 {
0087     return new PluginKateXMLCheckView(this, mainWindow);
0088 }
0089 
0090 //---------------------------------
0091 PluginKateXMLCheckView::PluginKateXMLCheckView(KTextEditor::Plugin *, KTextEditor::MainWindow *mainwin)
0092     : QObject(mainwin)
0093     , m_mainWindow(mainwin)
0094     , m_provider(mainwin, this)
0095 {
0096     KXMLGUIClient::setComponentName(QStringLiteral("katexmlcheck"), i18n("XML Check")); // where i18n resources?
0097     setXMLFile(QStringLiteral("ui.rc"));
0098 
0099     m_tmp_file = nullptr;
0100     QAction *a = actionCollection()->addAction(QStringLiteral("xml_check"));
0101     a->setText(i18n("Validate XML"));
0102     connect(a, &QAction::triggered, this, &PluginKateXMLCheckView::slotValidate);
0103     // TODO?:
0104     //(void)  new KAction ( i18n("Indent XML"), KShortcut(), this,
0105     //  SLOT(slotIndent()), actionCollection(), "xml_indent" );
0106 
0107     connect(&m_proc, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &PluginKateXMLCheckView::slotProcExited);
0108     // we currently only want errors:
0109     m_proc.setProcessChannelMode(QProcess::SeparateChannels);
0110     // m_proc.setProcessChannelMode(QProcess::ForwardedChannels); // For Debugging. Do not use this.
0111 
0112     mainwin->guiFactory()->addClient(this);
0113 }
0114 
0115 PluginKateXMLCheckView::~PluginKateXMLCheckView()
0116 {
0117     m_mainWindow->guiFactory()->removeClient(this);
0118     delete m_tmp_file;
0119 }
0120 
0121 void PluginKateXMLCheckView::slotProcExited(int exitCode, QProcess::ExitStatus exitStatus)
0122 {
0123     Q_UNUSED(exitCode);
0124 
0125     // FIXME: doesn't work correct the first time:
0126     // if( m_dockwidget->isDockBackPossible() ) {
0127     //  m_dockwidget->dockBack();
0128     //  }
0129 
0130     if (exitStatus != QProcess::NormalExit) {
0131         Utils::showMessage(i18n("Validate process crashed"), {}, i18n("XMLCheck"), MessageType::Error);
0132         return;
0133     }
0134 
0135     qDebug() << "slotProcExited()";
0136     QApplication::restoreOverrideCursor();
0137     delete m_tmp_file;
0138     QString proc_stderr = QString::fromLocal8Bit(m_proc.readAllStandardError());
0139     m_tmp_file = nullptr;
0140     uint err_count = 0;
0141     if (!m_validating) {
0142         // no i18n here, so we don't get an ugly English<->Non-english mixup:
0143         QString msg;
0144         if (m_dtdname.isEmpty()) {
0145             msg = i18n("No DOCTYPE found, will only check well-formedness.");
0146         } else {
0147             msg = i18nc("%1 refers to the XML DTD", "'%1' not found, will only check well-formedness.", m_dtdname);
0148         }
0149         Utils::showMessage(msg, {}, i18n("XMLCheck"), MessageType::Warning);
0150     }
0151     if (!proc_stderr.isEmpty()) {
0152         QList<Diagnostic> diags;
0153         QStringList lines = proc_stderr.split('\n', Qt::SkipEmptyParts);
0154         QString linenumber, msg;
0155         int line_count = 0;
0156         for (QStringList::Iterator it = lines.begin(); it != lines.end(); ++it) {
0157             const QString line = *it;
0158             line_count++;
0159             int semicolon_1 = line.indexOf(':');
0160             int semicolon_2 = line.indexOf(':', semicolon_1 + 1);
0161             int semicolon_3 = line.indexOf(':', semicolon_2 + 2);
0162             int caret_pos = line.indexOf('^');
0163             if (semicolon_1 != -1 && semicolon_2 != -1 && semicolon_3 != -1) {
0164                 linenumber = line.mid(semicolon_1 + 1, semicolon_2 - semicolon_1 - 1).trimmed();
0165                 linenumber = linenumber.rightJustified(6, ' '); // for sorting numbers
0166                 msg = line.mid(semicolon_3 + 1, line.length() - semicolon_3 - 1).trimmed();
0167             } else if (caret_pos != -1 || line_count == lines.size()) {
0168                 // TODO: this fails if "^" occurs in the real text?!
0169                 if (line_count == lines.size() && caret_pos == -1) {
0170                     msg = msg + '\n' + line;
0171                 }
0172                 QString col = QString::number(caret_pos);
0173                 if (col == QLatin1String("-1")) {
0174                     col = QLatin1String("");
0175                 }
0176                 err_count++;
0177                 // Diag item here
0178                 Diagnostic d;
0179                 int ln = linenumber.toInt() - 1;
0180                 ln = ln >= 0 ? ln : 0;
0181                 int cl = col.toInt() - 1;
0182                 cl = cl >= 0 ? cl : 0;
0183                 d.range = {ln, cl, ln, cl};
0184                 d.message = msg;
0185                 d.source = QStringLiteral("xmllint");
0186                 d.severity = DiagnosticSeverity::Warning;
0187                 diags << d;
0188             } else {
0189                 msg = msg + '\n' + line;
0190             }
0191         }
0192         if (!diags.empty()) {
0193             if (auto v = m_mainWindow->activeView()) {
0194                 FileDiagnostics fd;
0195                 fd.uri = v->document()->url();
0196                 fd.diagnostics = diags;
0197                 Q_EMIT m_provider.diagnosticsAdded(fd);
0198                 m_provider.showDiagnosticsView();
0199             }
0200         }
0201     }
0202     if (err_count == 0) {
0203         QString msg;
0204         if (m_validating) {
0205             msg = QStringLiteral("No errors found, document is valid."); // no i18n here
0206         } else {
0207             msg = QStringLiteral("No errors found, document is well-formed."); // no i18n here
0208         }
0209         Utils::showMessage(msg, {}, i18n("XMLCheck"), MessageType::Info);
0210     }
0211 }
0212 
0213 void PluginKateXMLCheckView::slotUpdate()
0214 {
0215     qDebug() << "slotUpdate() (not implemented yet)";
0216 }
0217 
0218 bool PluginKateXMLCheckView::slotValidate()
0219 {
0220     qDebug() << "slotValidate()";
0221 
0222     m_validating = false;
0223     m_dtdname = QLatin1String("");
0224 
0225     KTextEditor::View *kv = m_mainWindow->activeView();
0226     if (!kv) {
0227         return false;
0228     }
0229     delete m_tmp_file;
0230     m_tmp_file = new QTemporaryFile();
0231     if (!m_tmp_file->open()) {
0232         qDebug() << "Error (slotValidate()): could not create '" << m_tmp_file->fileName() << "': " << m_tmp_file->errorString();
0233         const QString msg = i18n("<b>Error:</b> Could not create temporary file '%1'.", m_tmp_file->fileName());
0234         Utils::showMessage(msg, {}, i18n("XMLCheck"), MessageType::Error, m_mainWindow);
0235         delete m_tmp_file;
0236         m_tmp_file = nullptr;
0237         return false;
0238     }
0239 
0240     QTextStream s(m_tmp_file);
0241     s << kv->document()->text();
0242     s.flush();
0243 
0244     // ensure we only execute xmllint from PATH or application package
0245     static const auto executableName = QStringLiteral("xmllint");
0246     QString exe = safeExecutableName(executableName);
0247     if (exe.isEmpty()) {
0248         exe = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, executableName);
0249     }
0250     if (exe.isEmpty()) {
0251         const QString msg = i18n(
0252             "<b>Error:</b> Failed to find xmllint. Please make "
0253             "sure that xmllint is installed. It is part of libxml2.");
0254         Utils::showMessage(msg, {}, i18n("XMLCheck"), MessageType::Error, m_mainWindow);
0255         return false;
0256     }
0257 
0258     // qDebug() << "exe=" <<exe;
0259     //  // use catalogs for KDE docbook:
0260     //  if( ! getenv("XML_CATALOG_FILES") ) {
0261     //      KComponentData ins("katexmlcheckplugin");
0262     //      QString catalogs;
0263     //      catalogs += ins.dirs()->findResource("data", "ksgmltools2/customization/catalog.xml");
0264     //      qDebug() << "catalogs: " << catalogs;
0265     //      setenv("XML_CATALOG_FILES", QFile::encodeName( catalogs ).data(), 1);
0266     //  }
0267     // qDebug() << "**catalogs: " << getenv("XML_CATALOG_FILES");
0268     QStringList args;
0269     args << QStringLiteral("--noout");
0270 
0271     // tell xmllint the working path of the document's file, if possible.
0272     // otherwise it will not find relative DTDs
0273 
0274     // I should give path to location of file, but remove filename
0275     // I can make QUrl.adjusted(rm filename).path()
0276     // or QUrl.toString(rm filename|rm schema)
0277     // Result is the same. Which variant should I choose?
0278     // QString path = kv->document()->url().adjusted(QUrl::RemoveFilename).path();
0279     // xmllint uses space- or colon-separated path option, so spaces should be encoded to %20. It is done with EncodeSpaces.
0280 
0281     // Now what about colons in file names or paths?
0282     // This way xmllint works normally:
0283     // xmllint --noout --path "/home/user/my/with:colon/" --valid "/home/user/my/with:colon/demo-1.xml"
0284     // but because this plugin makes temp file path to file is another and this way xmllint refuses to find dtd:
0285     // xmllint --noout --path "/home/user/my/with:colon/" --valid "/tmp/kate.X23725"
0286     // As workaround we can encode ':' with %3A
0287     QString path = kv->document()->url().toString(QUrl::RemoveFilename | QUrl::PreferLocalFile | QUrl::EncodeSpaces);
0288     path.replace(':', QLatin1String("%3A"));
0289     // because of such inconvenience with xmllint and paths, maybe switch to xmlstarlet?
0290 
0291     qDebug() << "path=" << path;
0292 
0293     if (!path.isEmpty()) {
0294         args << QStringLiteral("--path") << path;
0295     }
0296 
0297     // heuristic: assume that the doctype is in the first 10,000 bytes:
0298     QString text_start = kv->document()->text().left(10000);
0299     // remove comments before looking for doctype (as a doctype might be commented out
0300     // and needs to be ignored then):
0301     static const QRegularExpression re("<!--.*-->", QRegularExpression::InvertedGreedinessOption);
0302     text_start.remove(re);
0303     static const QRegularExpression re_doctype("<!DOCTYPE\\s+(.*)\\s+(?:PUBLIC\\s+[\"'].*[\"']\\s+[\"'](.*)[\"']|SYSTEM\\s+[\"'](.*)[\"'])",
0304                                                QRegularExpression::InvertedGreedinessOption | QRegularExpression::CaseInsensitiveOption);
0305 
0306     if (QRegularExpressionMatch match = re_doctype.match(text_start); match.hasMatch()) {
0307         QString dtdname;
0308         if (!match.captured(2).isEmpty()) {
0309             dtdname = match.captured(2);
0310         } else {
0311             dtdname = match.captured(2);
0312         }
0313         if (!dtdname.startsWith(QLatin1String("http:"))) { // todo: u_dtd.isLocalFile() doesn't work :-(
0314             // a local DTD is used
0315             m_validating = true;
0316             args << QStringLiteral("--valid");
0317         } else {
0318             m_validating = true;
0319             args << QStringLiteral("--valid");
0320         }
0321     } else if (text_start.indexOf(QLatin1String("<!DOCTYPE")) != -1) {
0322         // DTD is inside the XML file
0323         m_validating = true;
0324         args << QStringLiteral("--valid");
0325     }
0326     args << m_tmp_file->fileName();
0327     qDebug() << "m_tmp_file->fileName()=" << m_tmp_file->fileName();
0328 
0329     startHostProcess(m_proc, exe, args);
0330     qDebug() << "m_proc.program():" << m_proc.program(); // I want to see parameters
0331     qDebug() << "args=" << args;
0332     qDebug() << "exit code:" << m_proc.exitCode();
0333     if (!m_proc.waitForStarted(-1)) {
0334         const QString msg = i18n(
0335             "<b>Error:</b> Failed to execute xmllint. Please make "
0336             "sure that xmllint is installed. It is part of libxml2.");
0337         Utils::showMessage(msg, {}, i18n("XMLCheck"), MessageType::Error, m_mainWindow);
0338         return false;
0339     }
0340     QApplication::setOverrideCursor(Qt::WaitCursor);
0341     return true;
0342 }
0343 
0344 #include "moc_plugin_katexmlcheck.cpp"
0345 #include "plugin_katexmlcheck.moc"