Kevin Kofler 66a2554
From 89e4767148110a5566e463a03b3ed594276b7da0 Mon Sep 17 00:00:00 2001
Kevin Kofler 66a2554
Message-Id: <89e4767148110a5566e463a03b3ed594276b7da0.1317166378.git.kevin.kofler@chello.at>
Kevin Kofler 516b628
From: Kevin Kofler <kevin.kofler@chello.at>
Kevin Kofler 516b628
Date: Wed, 17 Aug 2011 04:54:37 +0200
Kevin Kofler 516b628
Subject: [PATCH] Implement automatic scanning of source code for required
Kevin Kofler 516b628
 data engines.
Kevin Kofler 516b628
Kevin Kofler 516b628
For packages in scripting languages and distributed through OCS, this is fully
Kevin Kofler 516b628
automatic and triggered from Package::installPackage. If an
Kevin Kofler 516b628
X-Plasma-RequiredDataEngines entry is present in the .desktop file (even if
Kevin Kofler 516b628
empty), the dependency extraction is not run and the explicitly provided
Kevin Kofler 516b628
information is trusted instead.
Kevin Kofler 516b628
Kevin Kofler 516b628
For native distribution packages, we ship a tool called
Kevin Kofler 516b628
plasma-dataengine-depextractor which can be run at any time during the build
Kevin Kofler 516b628
process and which adds the dependency information to the relevant .desktop file.
Kevin Kofler 516b628
Kevin Kofler 516b628
Authors of plasmoids are encouraged to run plasma-dataengine-depextractor and/or
Kevin Kofler 516b628
fill in X-Plasma-RequiredDataEngines manually. (Please note that the list is
Kevin Kofler 516b628
expected to be comma-separated.)
Kevin Kofler 516b628
---
Kevin Kofler 516b628
 plasma/CMakeLists.txt                 |   15 ++++
Kevin Kofler 66a2554
 plasma/depextractor/depextractor.cpp  |  125 +++++++++++++++++++++++++++++++++
Kevin Kofler 516b628
 plasma/package.cpp                    |   11 +++
Kevin Kofler 66a2554
 plasma/private/componentinstaller.cpp |   71 +++++++++++++++++++
Kevin Kofler 66a2554
 plasma/private/componentinstaller_p.h |   17 ++++-
Kevin Kofler 66a2554
 5 files changed, 238 insertions(+), 1 deletions(-)
Kevin Kofler 516b628
Kevin Kofler 516b628
diff --git a/plasma/CMakeLists.txt b/plasma/CMakeLists.txt
Kevin Kofler 516b628
index f929967..9a760ef 100644
Kevin Kofler 516b628
--- a/plasma/CMakeLists.txt
Kevin Kofler 516b628
+++ b/plasma/CMakeLists.txt
Kevin Kofler 516b628
@@ -304,6 +304,18 @@ set_target_properties(plasma PROPERTIES
Kevin Kofler 516b628
 
Kevin Kofler 516b628
 install(TARGETS plasma EXPORT kdelibsLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS})
Kevin Kofler 516b628
 
Kevin Kofler 516b628
+if(NOT PLASMA_NO_PACKAGEKIT)
Kevin Kofler 516b628
+    # we need a copy of the component installer because libplasma does not export it
Kevin Kofler 516b628
+    # plus, this avoids depending on GUI stuff in this command-line utility
Kevin Kofler 516b628
+    set(plasma_dataengine_depextractor_SRCS depextractor/depextractor.cpp
Kevin Kofler 516b628
+                                            private/componentinstaller.cpp)
Kevin Kofler 516b628
+    kde4_add_executable(plasma-dataengine-depextractor
Kevin Kofler 516b628
+                        ${plasma_dataengine_depextractor_SRCS})
Kevin Kofler 516b628
+    set_target_properties(plasma-dataengine-depextractor PROPERTIES
Kevin Kofler 516b628
+                          COMPILE_FLAGS -DPLASMA_COMPONENTINSTALLER_NO_QWIDGET=1)
Kevin Kofler 516b628
+    target_link_libraries(plasma-dataengine-depextractor ${KDE4_KDECORE_LIBS})
Kevin Kofler 516b628
+endif(NOT PLASMA_NO_PACKAGEKIT)
Kevin Kofler 516b628
+
Kevin Kofler 516b628
 
Kevin Kofler 516b628
 ########### install files ###############
Kevin Kofler 516b628
 
Kevin Kofler 516b628
@@ -460,3 +472,6 @@ install(FILES data/operations/dataengineservice.operations DESTINATION ${DATA_IN
Kevin Kofler 516b628
 install(FILES data/operations/plasmoidservice.operations DESTINATION ${DATA_INSTALL_DIR}/plasma/services)
Kevin Kofler 516b628
 install(FILES data/operations/storage.operations DESTINATION ${DATA_INSTALL_DIR}/plasma/services)
Kevin Kofler 516b628
 
Kevin Kofler 516b628
+if(NOT PLASMA_NO_PACKAGEKIT)
Kevin Kofler 516b628
+    install(TARGETS plasma-dataengine-depextractor DESTINATION ${BIN_INSTALL_DIR})
Kevin Kofler 516b628
+endif(NOT PLASMA_NO_PACKAGEKIT)
Kevin Kofler 516b628
diff --git a/plasma/depextractor/depextractor.cpp b/plasma/depextractor/depextractor.cpp
Kevin Kofler 516b628
new file mode 100644
Kevin Kofler 66a2554
index 0000000..c489de7
Kevin Kofler 516b628
--- /dev/null
Kevin Kofler 516b628
+++ b/plasma/depextractor/depextractor.cpp
Kevin Kofler 66a2554
@@ -0,0 +1,125 @@
Kevin Kofler 516b628
+/* Plasma Data Engine dependency extractor
Kevin Kofler 516b628
+   Copyright (C) 2011 Kevin Kofler <kevin.kofler@chello.at>
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+   This program is free software: you can redistribute it and/or modify
Kevin Kofler 516b628
+   it under the terms of the GNU General Public License as published by
Kevin Kofler 516b628
+   the Free Software Foundation, either version 2 of the License, or
Kevin Kofler 516b628
+   (at your option) any later version.
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+   This program is distributed in the hope that it will be useful,
Kevin Kofler 516b628
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
Kevin Kofler 516b628
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
Kevin Kofler 516b628
+   GNU General Public License for more details.
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+   You should have received a copy of the GNU General Public License
Kevin Kofler 516b628
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+#include <QCoreApplication>
Kevin Kofler 516b628
+#include <QTextStream>
Kevin Kofler 516b628
+#include <QFileInfo>
Kevin Kofler 516b628
+#include <QDir>
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+#include <cstdio>
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+#include <kaboutdata.h>
Kevin Kofler 516b628
+#include <kcmdlineargs.h>
Kevin Kofler 516b628
+#include <kdesktopfile.h>
Kevin Kofler 516b628
+#include <kconfiggroup.h>
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+#include "private/componentinstaller_p.h"
Kevin Kofler 516b628
+
Kevin Kofler 66a2554
+static QString scriptingApi(const QString &desktopFile)
Kevin Kofler 66a2554
+{
Kevin Kofler 66a2554
+    KDesktopFile desktop(desktopFile);
Kevin Kofler 66a2554
+    KConfigGroup desktopGroup = desktop.desktopGroup();
Kevin Kofler 66a2554
+    if (desktopGroup.readEntry("X-KDE-ServiceTypes", QStringList())
Kevin Kofler 66a2554
+                      .contains("Plasma/ScriptEngine")
Kevin Kofler 66a2554
+        || desktopGroup.readEntry("ServiceTypes", QStringList())
Kevin Kofler 66a2554
+                         .contains("Plasma/ScriptEngine")) {
Kevin Kofler 66a2554
+        /* Script engines are always written in C++. Their X-Plasma-API is the
Kevin Kofler 66a2554
+           API they export, not the language they're written in. */
Kevin Kofler 66a2554
+        return QString();
Kevin Kofler 66a2554
+    }
Kevin Kofler 66a2554
+    return desktopGroup.readEntry("X-Plasma-API", QString());
Kevin Kofler 66a2554
+}
Kevin Kofler 66a2554
+
Kevin Kofler 516b628
+static void writeDataEngineDependencies(const QStringList &deps,
Kevin Kofler 516b628
+                                        const QString &desktopFile)
Kevin Kofler 516b628
+{
Kevin Kofler 516b628
+    if (!deps.isEmpty()) {
Kevin Kofler 516b628
+        KDesktopFile desktop(desktopFile);
Kevin Kofler 516b628
+        desktop.desktopGroup().writeEntry("X-Plasma-RequiredDataEngines", deps);
Kevin Kofler 516b628
+    }
Kevin Kofler 516b628
+}
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+int main(int argc, char **argv)
Kevin Kofler 516b628
+{
Kevin Kofler 516b628
+    KAboutData aboutData("plasma-dataengine-depextractor", QByteArray(),
Kevin Kofler 516b628
+                        ki18n("Plasma Data Engine dependency extractor"),
Kevin Kofler 66a2554
+                        "2",
Kevin Kofler 516b628
+                        ki18n("Plasma Data Engine dependency extractor"));
Kevin Kofler 516b628
+    aboutData.addAuthor(ki18n("Kevin Kofler"), ki18n("Author"),
Kevin Kofler 516b628
+                        "kevin.kofler@chello.at");
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+    KCmdLineArgs::init(argc, argv, &aboutData);
Kevin Kofler 516b628
+    KCmdLineOptions options;
Kevin Kofler 516b628
+    options.add("+[path]",
Kevin Kofler 516b628
+                ki18n("Source path (default: .)"));
Kevin Kofler 516b628
+    options.add("+[file]",
Kevin Kofler 516b628
+                ki18n(".desktop rel. to path (default: metadata.desktop)")
Kevin Kofler 516b628
+               );
Kevin Kofler 516b628
+    KCmdLineArgs::addCmdLineOptions(options);
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+    QCoreApplication *app = new QCoreApplication(KCmdLineArgs::qtArgc(),
Kevin Kofler 516b628
+                                                 KCmdLineArgs::qtArgv());
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+    KCmdLineArgs *args = KCmdLineArgs::parsedArgs();
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+    int exitCode = 0;
Kevin Kofler 516b628
+
Kevin Kofler 66a2554
+    QString path, desktopFile;
Kevin Kofler 516b628
+    int argCount = args->count();
Kevin Kofler 516b628
+    switch (argCount) {
Kevin Kofler 516b628
+        case 0:
Kevin Kofler 516b628
+            path = ".";
Kevin Kofler 516b628
+            desktopFile = "metadata.desktop";
Kevin Kofler 516b628
+            break;
Kevin Kofler 516b628
+        case 1:
Kevin Kofler 516b628
+            path = args->arg(0);
Kevin Kofler 516b628
+            desktopFile = "metadata.desktop";
Kevin Kofler 516b628
+            break;
Kevin Kofler 516b628
+        case 2:
Kevin Kofler 516b628
+            path = args->arg(0);
Kevin Kofler 516b628
+            desktopFile = args->arg(1);
Kevin Kofler 516b628
+            break;
Kevin Kofler 516b628
+        default:
Kevin Kofler 516b628
+        {
Kevin Kofler 516b628
+            QTextStream err(stderr, QIODevice::WriteOnly | QIODevice::Text);
Kevin Kofler 516b628
+            err << i18n("Expected at most 2 arguments, but %1 given", argCount)
Kevin Kofler 516b628
+                << endl;
Kevin Kofler 516b628
+            exitCode = 1;
Kevin Kofler 516b628
+            break;
Kevin Kofler 516b628
+        }
Kevin Kofler 516b628
+    }
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+    if (!exitCode) {
Kevin Kofler 516b628
+        if (QFileInfo(desktopFile).isRelative())
Kevin Kofler 66a2554
+            desktopFile = QDir(path).absoluteFilePath(desktopFile);
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+        if (QFileInfo(desktopFile).exists()) {
Kevin Kofler 516b628
+            writeDataEngineDependencies(Plasma::ComponentInstaller::self()
Kevin Kofler 66a2554
+                                          ->extractDataEngineDependencies(
Kevin Kofler 66a2554
+                                              path,
Kevin Kofler 66a2554
+                                              scriptingApi(desktopFile)),
Kevin Kofler 516b628
+                                        desktopFile);
Kevin Kofler 516b628
+        } else {
Kevin Kofler 516b628
+            QTextStream err(stderr, QIODevice::WriteOnly | QIODevice::Text);
Kevin Kofler 516b628
+            err << i18n("Desktop file \"%1\" not found", desktopFile) << endl;
Kevin Kofler 516b628
+            exitCode = 1;
Kevin Kofler 516b628
+        }
Kevin Kofler 516b628
+    }
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+    args->clear();
Kevin Kofler 516b628
+    delete app;
Kevin Kofler 516b628
+    return exitCode;
Kevin Kofler 516b628
+}
Kevin Kofler 516b628
diff --git a/plasma/package.cpp b/plasma/package.cpp
Kevin Kofler 516b628
index 0a45c87..131f204 100644
Kevin Kofler 516b628
--- a/plasma/package.cpp
Kevin Kofler 516b628
+++ b/plasma/package.cpp
Kevin Kofler 516b628
@@ -631,6 +631,17 @@ bool Package::installPackage(const QString &package,
Kevin Kofler 516b628
         }
Kevin Kofler 516b628
     }
Kevin Kofler 516b628
     QStringList requiredDataEngines = meta.requiredDataEngines();
Kevin Kofler 516b628
+    if (requiredDataEngines.isEmpty()) {
Kevin Kofler 516b628
+        // check whether this was explicitly specified as empty
Kevin Kofler 516b628
+        QString metaPath = targetName + "/metadata.desktop";
Kevin Kofler 516b628
+        KDesktopFile df(metaPath);
Kevin Kofler 516b628
+        KConfigGroup cg = df.desktopGroup();
Kevin Kofler 516b628
+        if (!cg.hasKey("X-Plasma-RequiredDataEngines")) {
Kevin Kofler 516b628
+            // not specified at all, try running the dependency extraction
Kevin Kofler 516b628
+            requiredDataEngines = ComponentInstaller::self()->extractDataEngineDependencies(targetName,
Kevin Kofler 516b628
+                                                                                            requiredScriptEngine);
Kevin Kofler 516b628
+        }
Kevin Kofler 516b628
+    }
Kevin Kofler 516b628
     if (!requiredDataEngines.isEmpty()) {
Kevin Kofler 516b628
         QStringList knownDataEngines = DataEngineManager::self()->listAllEngines(meta.application());
Kevin Kofler 516b628
         foreach (const QString &requiredDataEngine, requiredDataEngines) {
Kevin Kofler 516b628
diff --git a/plasma/private/componentinstaller.cpp b/plasma/private/componentinstaller.cpp
Kevin Kofler 66a2554
index 870667f..087d1c6 100644
Kevin Kofler 516b628
--- a/plasma/private/componentinstaller.cpp
Kevin Kofler 516b628
+++ b/plasma/private/componentinstaller.cpp
Kevin Kofler 516b628
@@ -28,6 +28,10 @@
Kevin Kofler 516b628
 #include <QWidget>
Kevin Kofler 516b628
 #include <QLatin1String>
Kevin Kofler 516b628
 #include <QStringList>
Kevin Kofler 516b628
+#include <QTextStream>
Kevin Kofler 516b628
+#include <QFile>
Kevin Kofler 516b628
+#include <QDirIterator>
Kevin Kofler 516b628
+#include <QRegExp>
Kevin Kofler 516b628
 #endif
Kevin Kofler 516b628
 
Kevin Kofler 516b628
 namespace Plasma
Kevin Kofler 516b628
@@ -85,9 +89,13 @@ void ComponentInstaller::installMissingComponent(const QString &type,
Kevin Kofler 516b628
     // We don't check packageKit.isValid() because the service is activated on
Kevin Kofler 516b628
     // demand, so it will show up as "not valid".
Kevin Kofler 516b628
     WId wid = 0;
Kevin Kofler 516b628
+#ifndef PLASMA_COMPONENTINSTALLER_NO_QWIDGET
Kevin Kofler 516b628
     if (parent) {
Kevin Kofler 516b628
         wid = parent->winId();
Kevin Kofler 516b628
     }
Kevin Kofler 516b628
+#else
Kevin Kofler 516b628
+    Q_UNUSED(parent);
Kevin Kofler 516b628
+#endif
Kevin Kofler 516b628
     QStringList resources;
Kevin Kofler 516b628
     resources.append(searchString);
Kevin Kofler 516b628
     packageKit.asyncCall(QLatin1String("InstallResources"), (unsigned int) wid,
Kevin Kofler 66a2554
@@ -100,4 +108,67 @@ void ComponentInstaller::installMissingComponent(const QString &type,
Kevin Kofler 516b628
 #endif
Kevin Kofler 516b628
 }
Kevin Kofler 516b628
 
Kevin Kofler 516b628
+QStringList ComponentInstaller::extractDataEngineDependencies(const QString &path,
Kevin Kofler 516b628
+                                                              const QString &api)
Kevin Kofler 516b628
+{
Kevin Kofler 516b628
+    QStringList deps;
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+#ifdef PLASMA_ENABLE_PACKAGEKIT_SUPPORT
Kevin Kofler 516b628
+    QStringList nameFilters;
Kevin Kofler 516b628
+    QRegExp searchRegExp("dataEngine *\\( *\"([^\"]+)\" *\\)");
Kevin Kofler 516b628
+    if (api.isEmpty()) {
Kevin Kofler 516b628
+        // no script engine API, this is native C++ code
Kevin Kofler 516b628
+        nameFilters.append("*.cpp");
Kevin Kofler 516b628
+        nameFilters.append("*.cxx");
Kevin Kofler 516b628
+        nameFilters.append("*.cc");
Kevin Kofler 516b628
+        nameFilters.append("*.C");
Kevin Kofler 516b628
+        nameFilters.append("*.h");
Kevin Kofler 516b628
+        nameFilters.append("*.hpp");
Kevin Kofler 516b628
+        nameFilters.append("*.hxx");
Kevin Kofler 516b628
+        nameFilters.append("*.hh");
Kevin Kofler 516b628
+        nameFilters.append("*.H");
Kevin Kofler 66a2554
+    } else if (api == "declarativeappletscript") {
Kevin Kofler 66a2554
+        nameFilters.append("*.qml");
Kevin Kofler 66a2554
+        searchRegExp = QRegExp("(?:^\\s*engine:\\s*|dataEngine *\\( *)\"([^\"]+)\"");
Kevin Kofler 516b628
+    } else if (api == "javascript") {
Kevin Kofler 516b628
+        nameFilters.append("*.js");
Kevin Kofler 516b628
+    } else if (api == "python") {
Kevin Kofler 516b628
+        nameFilters.append("*.py");
Kevin Kofler 516b628
+        searchRegExp = QRegExp("dataEngine *\\( *[\'\"]([^\'\"]+)[\'\"] *\\)");
Kevin Kofler 516b628
+    } else if (api == "ruby-script") {
Kevin Kofler 516b628
+        nameFilters.append("*.rb");
Kevin Kofler 516b628
+        searchRegExp = QRegExp("dataEngine *\\( *[\'\"]([^\'\"]+)[\'\"] *\\)");
Kevin Kofler 516b628
+    } else {
Kevin Kofler 516b628
+        // dependency extraction not supported for this API
Kevin Kofler 516b628
+        return deps;
Kevin Kofler 516b628
+    }
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+    QDirIterator it(path, nameFilters, QDir::Files | QDir::CaseSensitive,
Kevin Kofler 516b628
+                    QDirIterator::Subdirectories
Kevin Kofler 516b628
+                      | QDirIterator::FollowSymlinks);
Kevin Kofler 516b628
+    while (it.hasNext()) {
Kevin Kofler 516b628
+        QFile file(it.next());
Kevin Kofler 516b628
+        file.open(QIODevice::ReadOnly | QIODevice::Text);
Kevin Kofler 516b628
+        QTextStream stream(&file;;
Kevin Kofler 516b628
+        QString line;
Kevin Kofler 516b628
+        while (!(line = stream.readLine()).isNull()) {
Kevin Kofler 516b628
+             int column = 0;
Kevin Kofler 516b628
+             while ((column = searchRegExp.indexIn(line, column)) != -1) {
Kevin Kofler 516b628
+                 QString dep = searchRegExp.cap(1);
Kevin Kofler 516b628
+                 if (!deps.contains(dep)) {
Kevin Kofler 516b628
+                     deps.append(dep);
Kevin Kofler 516b628
+                 }
Kevin Kofler 516b628
+                 column += searchRegExp.matchedLength();
Kevin Kofler 516b628
+             }
Kevin Kofler 516b628
+        }
Kevin Kofler 516b628
+        file.close();
Kevin Kofler 516b628
+    }
Kevin Kofler 516b628
+#else
Kevin Kofler 516b628
+    Q_UNUSED(path);
Kevin Kofler 516b628
+    Q_UNUSED(api);
Kevin Kofler 516b628
+#endif
Kevin Kofler 516b628
+
Kevin Kofler 516b628
+    return deps;
Kevin Kofler 516b628
+}
Kevin Kofler 516b628
+
Kevin Kofler 516b628
 } // namespace Plasma
Kevin Kofler 516b628
diff --git a/plasma/private/componentinstaller_p.h b/plasma/private/componentinstaller_p.h
Kevin Kofler 516b628
index f85cbb6..d0d9c75 100644
Kevin Kofler 516b628
--- a/plasma/private/componentinstaller_p.h
Kevin Kofler 516b628
+++ b/plasma/private/componentinstaller_p.h
Kevin Kofler 516b628
@@ -20,7 +20,7 @@
Kevin Kofler 516b628
 #ifndef PLASMA_COMPONENTINSTALLER_H
Kevin Kofler 516b628
 #define PLASMA_COMPONENTINSTALLER_H
Kevin Kofler 516b628
 
Kevin Kofler 516b628
-class QString;
Kevin Kofler 516b628
+#include <QStringList>
Kevin Kofler 516b628
 class QWidget;
Kevin Kofler 516b628
 
Kevin Kofler 516b628
 namespace Plasma
Kevin Kofler 516b628
@@ -76,6 +76,21 @@ class ComponentInstaller
Kevin Kofler 516b628
         void installMissingComponent(const QString &type, const QString &name,
Kevin Kofler 516b628
                                      QWidget *parent = 0, bool force = false);
Kevin Kofler 516b628
 
Kevin Kofler 516b628
+        /**
Kevin Kofler 516b628
+         * Extracts the list of required data engines from source code.
Kevin Kofler 516b628
+         *
Kevin Kofler 516b628
+         * If the scripting API is not supported for dependency extraction or
Kevin Kofler 516b628
+         * if Plasma was compiled without support for missing component
Kevin Kofler 516b628
+         * installation, an empty list of dependencies is returned.
Kevin Kofler 516b628
+         *
Kevin Kofler 516b628
+         * @param path the path containing the source code
Kevin Kofler 516b628
+         * @param api the scripting API used;
Kevin Kofler 516b628
+         *            if empty (the default), assumes the native C++ API
Kevin Kofler 516b628
+         */
Kevin Kofler 516b628
+        QStringList extractDataEngineDependencies(const QString &path,
Kevin Kofler 516b628
+                                                  const QString &api
Kevin Kofler 516b628
+                                                    = QString());
Kevin Kofler 516b628
+
Kevin Kofler 516b628
     private:
Kevin Kofler 516b628
         /**
Kevin Kofler 516b628
          * Default constructor. The singleton method self() is the
Kevin Kofler 516b628
-- 
Kevin Kofler 66a2554
1.7.6.2
Kevin Kofler 516b628