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"