From 4cdfc7e90a21903fc033283dea59ae9393f3646f Mon Sep 17 00:00:00 2001
From: august-alt <77973983+august-alt@users.noreply.github.com>
Date: Wed, 22 Sep 2021 19:06:49 +0400
Subject: [PATCH] feat: add command line interface for gpui

---
 src/app/CMakeLists.txt        |   9 +-
 src/app/main.cpp              |  27 ++++-
 src/gui/CMakeLists.txt        |   3 +
 src/gui/commandlineoptions.h  |  36 ++++++
 src/gui/commandlineparser.cpp | 120 +++++++++++++++++++
 src/gui/commandlineparser.h   |  59 ++++++++++
 src/gui/contentwidget.cpp     |   2 +
 src/gui/contentwidget.h       |   1 +
 src/gui/mainwindow.cpp        | 210 ++++++++++++++++++++++++----------
 src/gui/mainwindow.h          |  12 +-
 src/gui/mainwindow.ui         |   1 -
 11 files changed, 415 insertions(+), 65 deletions(-)
 create mode 100644 src/gui/commandlineoptions.h
 create mode 100644 src/gui/commandlineparser.cpp
 create mode 100644 src/gui/commandlineparser.h

diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index 11ea79a..6ba2e56 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -21,7 +21,14 @@ include_directories(${GPUI_INCLUDE_DIRS})
 
 find_package(Qt5 COMPONENTS Widgets REQUIRED)
 
-add_gpui_executable(main main.cpp)
+set(HEADERS
+)
+
+set(SOURCES
+    main.cpp
+)
+
+add_gpui_executable(main ${SOURCES})
 
 target_link_libraries(main ${GPUI_LIBRARIES})
 target_link_libraries(main Qt5::Widgets)
diff --git a/src/app/main.cpp b/src/app/main.cpp
index 67fb835..0f69f5d 100644
--- a/src/app/main.cpp
+++ b/src/app/main.cpp
@@ -18,8 +18,8 @@
 **
 ***********************************************************************************************************************/
 
+#include "../gui/commandlineparser.h"
 #include "../gui/mainwindow.h"
-
 #include "../model/pluginstorage.h"
 
 #include <QApplication>
@@ -31,13 +31,36 @@ int main(int argc, char ** argv) {
     // Create window.
     QApplication app(argc, argv);
 
+    gpui::CommandLineParser parser(app);
+    gpui::CommandLineOptions options;
+    QString errorMessage;
+
+    gpui::CommandLineParser::CommandLineParseResult parserResult = parser.parseCommandLine(&options, &errorMessage);
+
+    switch (parserResult)
+    {
+    case gpui::CommandLineParser::CommandLineError:
+        printf("%s \n", qPrintable(errorMessage));
+        parser.showHelp();
+        return 1;
+    case gpui::CommandLineParser::CommandLineHelpRequested:
+        parser.showHelp();
+        return 0;
+    case gpui::CommandLineParser::CommandLineVersionRequested:
+        parser.showVersion();
+        return 0;
+    case gpui::CommandLineParser::CommandLineOk:
+    default:
+        break;
+    }
+
     // NOTE: set app variables which will be used to
     // construct settings path
     app.setOrganizationName("BaseALT");
     app.setOrganizationDomain("basealt.ru");
     app.setApplicationName("GPUI");
     
-    gpui::MainWindow window;
+    gpui::MainWindow window(options);
     window.show();
 
     return app.exec();
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index 272263c..058d5c0 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -12,6 +12,8 @@ include_directories(
 
 set(HEADERS
     contentwidget.h
+    commandlineoptions.h
+    commandlineparser.h
     gui.h
     mainwindow.h
     mainwindowsettings.h
@@ -20,6 +22,7 @@ set(HEADERS
 )
 
 set(SOURCES
+    commandlineparser.cpp
     contentwidget.cpp
     gui.cpp
     mainwindow.cpp
diff --git a/src/gui/commandlineoptions.h b/src/gui/commandlineoptions.h
new file mode 100644
index 0000000..c671973
--- /dev/null
+++ b/src/gui/commandlineoptions.h
@@ -0,0 +1,36 @@
+/***********************************************************************************************************************
+**
+** Copyright (C) 2021 BaseALT Ltd. <org@basealt.ru>
+**
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of the GNU General Public License
+** as published by the Free Software Foundation; either version 2
+** of the License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+**
+***********************************************************************************************************************/
+
+#ifndef GPUI_COMMAND_LINE_OPTIONS_H
+#define GPUI_COMMAND_LINE_OPTIONS_H
+
+#include <QString>
+#include <QUuid>
+
+namespace gpui {
+    class CommandLineOptions
+    {
+    public:
+        QString policyBundle;
+        QString path;
+    };
+}
+
+#endif // GPUI_COMMAND_LINE_OPTIONS_H
diff --git a/src/gui/commandlineparser.cpp b/src/gui/commandlineparser.cpp
new file mode 100644
index 0000000..7020edb
--- /dev/null
+++ b/src/gui/commandlineparser.cpp
@@ -0,0 +1,120 @@
+/***********************************************************************************************************************
+**
+** Copyright (C) 2021 BaseALT Ltd. <org@basealt.ru>
+**
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of the GNU General Public License
+** as published by the Free Software Foundation; either version 2
+** of the License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+**
+***********************************************************************************************************************/
+
+#include "commandlineparser.h"
+
+#include <memory>
+
+#include <QUuid>
+#include <QCommandLineParser>
+
+namespace gpui
+{
+
+class CommandLineParserPrivate
+{
+public:
+    QApplication& application;
+    std::unique_ptr<QCommandLineParser> parser;
+
+    CommandLineParserPrivate(QApplication &application)
+        : application(application)
+    {
+        parser = std::make_unique<QCommandLineParser>();
+    }
+};
+
+CommandLineParser::CommandLineParser(QApplication &application)
+    : d(new CommandLineParserPrivate(application))
+{
+
+}
+
+CommandLineParser::~CommandLineParser()
+{
+    delete d;
+}
+
+CommandLineParser::CommandLineParseResult CommandLineParser::parseCommandLine(CommandLineOptions *options, QString *errorMessage)
+{
+    const QCommandLineOption pathOption("p", "The full path of policy to edit.", "path");
+    const QCommandLineOption bundleOption("b", "The full path of policy bundle to load.", "path");
+
+    d->parser->setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
+    d->parser->addOption(pathOption);
+    d->parser->addOption(bundleOption);
+
+    const QCommandLineOption helpOption = d->parser->addHelpOption();
+    const QCommandLineOption versionOption = d->parser->addVersionOption();
+
+    if (!d->parser->parse(QCoreApplication::arguments()))
+    {
+        *errorMessage = d->parser->errorText();
+        return CommandLineError;
+    }
+
+    if (d->parser->isSet(versionOption))
+    {
+        return CommandLineVersionRequested;
+    }
+
+    if (d->parser->isSet(helpOption))
+    {
+        return CommandLineHelpRequested;
+    }
+
+    if (d->parser->isSet(pathOption))
+    {
+        const QString path = d->parser->value(pathOption);
+        options->path = path;
+
+        if (options->path.isNull() || options->path.isEmpty())
+        {
+            *errorMessage = "Bad policy path: " + path;
+            return CommandLineError;
+        }
+    }
+
+    if (d->parser->isSet(bundleOption))
+    {
+        const QString path = d->parser->value(bundleOption);
+        options->policyBundle = path;
+
+        if (options->policyBundle.isNull() || options->policyBundle.isEmpty())
+        {
+            *errorMessage = "Bad policy path: " + path;
+            return CommandLineError;
+        }
+    }
+
+    return CommandLineOk;
+}
+
+void CommandLineParser::showHelp() const
+{
+    d->parser->showHelp();
+}
+
+void CommandLineParser::showVersion() const
+{
+    d->parser->showVersion();
+}
+
+}
diff --git a/src/gui/commandlineparser.h b/src/gui/commandlineparser.h
new file mode 100644
index 0000000..011648b
--- /dev/null
+++ b/src/gui/commandlineparser.h
@@ -0,0 +1,59 @@
+/***********************************************************************************************************************
+**
+** Copyright (C) 2021 BaseALT Ltd. <org@basealt.ru>
+**
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of the GNU General Public License
+** as published by the Free Software Foundation; either version 2
+** of the License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+**
+***********************************************************************************************************************/
+
+#ifndef GPUI_COMMAND_LINE_PARSER_H
+#define GPUI_COMMAND_LINE_PARSER_H
+
+#include "gui.h"
+
+#include "../gui/commandlineoptions.h"
+
+#include <QApplication>
+#include <QCommandLineParser>
+
+namespace gpui {
+    class CommandLineParserPrivate;
+
+    class GPUI_GUI_EXPORT CommandLineParser
+    {
+    public:
+        enum CommandLineParseResult
+        {
+            CommandLineOk,
+            CommandLineError,
+            CommandLineVersionRequested,
+            CommandLineHelpRequested
+        };
+
+    public:
+        CommandLineParser(QApplication& application);
+        ~CommandLineParser();
+
+        CommandLineParseResult parseCommandLine(CommandLineOptions *options, QString *errorMessage);
+
+        void showHelp() const;
+        void showVersion() const;
+
+    private:
+        CommandLineParserPrivate* d;
+    };
+}
+
+#endif // GPUI_COMMAND_LINE_PARSER_H
diff --git a/src/gui/contentwidget.cpp b/src/gui/contentwidget.cpp
index 32d2a74..b5bedc7 100644
--- a/src/gui/contentwidget.cpp
+++ b/src/gui/contentwidget.cpp
@@ -240,6 +240,8 @@ void ContentWidget::onApplyClicked()
 {
     d->commandGroup.execute();
     d->commandGroup.clear();
+
+    savePolicyChanges();
 }
 
 void ContentWidget::onCancelClicked()
diff --git a/src/gui/contentwidget.h b/src/gui/contentwidget.h
index 62fe084..0552d3e 100644
--- a/src/gui/contentwidget.h
+++ b/src/gui/contentwidget.h
@@ -63,6 +63,7 @@ namespace gpui {
 
     signals:
         void modelItemSelected(const QModelIndex& index);
+        void savePolicyChanges();
 
     private:
         Ui::ContentWidget *ui;
diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp
index b3e8162..8671d95 100644
--- a/src/gui/mainwindow.cpp
+++ b/src/gui/mainwindow.cpp
@@ -22,6 +22,8 @@
 #include "ui_mainwindow.h"
 #include "mainwindowsettings.h"
 
+#include "commandlineoptions.h"
+
 #include "contentwidget.h"
 
 #include "../model/bundle/policybundle.h"
@@ -49,9 +51,11 @@ public:
 
     std::shared_ptr<model::registry::Registry> userRegistry;
     std::unique_ptr<model::registry::AbstractRegistrySource> userRegistrySource;
+    QString userRegistryPath;
 
     std::shared_ptr<model::registry::Registry> machineRegistry;
     std::unique_ptr<model::registry::AbstractRegistrySource> machineRegistrySource;
+    QString machineRegistryPath;
 
     std::unique_ptr<QSortFilterProxyModel> sortModel = nullptr;
 
@@ -81,23 +85,46 @@ void save(const std::string &fileName, std::shared_ptr<model::registry::Registry
         return;
     }
 
-    std::ofstream file;
+    auto oss = std::make_unique<std::ostringstream>();
 
-    file.open(fileName, std::ofstream::out | std::ofstream::binary);
-
-    if (file.good()) {
-        if (!format->write(file, fileData.get()))
-        {
-            qWarning() << fileName.c_str() << " " << format->getErrorString().c_str();
-        }
+    if (!format->write(*oss, fileData.get()))
+    {
+        qWarning() << fileName.c_str() << " " << format->getErrorString().c_str();
     }
 
-    file.close();
+    oss->flush();
+
+    qWarning() << "Current string values." << oss->str().c_str();
+
+    if (QString::fromStdString(fileName).startsWith("smb://"))
+    {
+        SmbLocationItemFile smbLocationItemFile(QString::fromStdString(fileName));
+        smbLocationItemFile.open(QFile::WriteOnly | QFile::Truncate);
+        if (!smbLocationItemFile.isOpen())
+        {
+            smbLocationItemFile.open(QFile::NewOnly | QFile::WriteOnly);
+        }
+        if (smbLocationItemFile.isOpen() && oss->str().size() > 0)
+        {
+            smbLocationItemFile.write(&oss->str().at(0), oss->str().size());
+        }
+        smbLocationItemFile.close();
+    }
+    else
+    {
+        QFile registryFile(QString::fromStdString(fileName));
+        registryFile.open(QFile::ReadWrite);
+        if (registryFile.isOpen() && registryFile.isWritable() && oss->str().size() > 0)
+        {
+            registryFile.write(&oss->str().at(0), oss->str().size());
+        }
+        registryFile.close();
+    }
 
     delete format;
 }
 
-MainWindow::MainWindow(QWidget *parent)
+MainWindow::MainWindow(CommandLineOptions &options, QWidget *parent)
     : QMainWindow(parent)
     , d(new MainWindowPrivate())
     , ui(new Ui::MainWindow())
@@ -118,6 +145,31 @@ MainWindow::MainWindow(QWidget *parent)
     connect(ui->actionOpenMachineRegistrySource, &QAction::triggered, this, &MainWindow::onMachineRegistrySourceOpen);
     connect(ui->actionSaveRegistrySource, &QAction::triggered, this, &MainWindow::onRegistrySourceSave);
     connect(ui->treeView, &QTreeView::clicked, d->contentWidget, &ContentWidget::modelItemSelected);
+
+    if (!options.policyBundle.isEmpty())
+    {
+        loadPolicyBundleFolder(options.policyBundle);
+    }
+
+    if (!options.path.isEmpty())
+    {
+        d->userRegistryPath = options.path + "/User/Registry.pol";
+        d->machineRegistryPath = options.path + "/Machine/Registry.pol";
+
+        onPolFileOpen(d->userRegistryPath, d->userRegistry, d->userRegistrySource,
+                      [&](model::registry::AbstractRegistrySource* source)
+        {
+            d->contentWidget->setUserRegistrySource(source);
+        });
+
+        onPolFileOpen(d->machineRegistryPath, d->machineRegistry, d->machineRegistrySource,
+                      [&](model::registry::AbstractRegistrySource* source)
+        {
+            d->contentWidget->setMachineRegistrySource(source);
+        });
+    }
+
+    connect(d->contentWidget, &ContentWidget::savePolicyChanges, this, &MainWindow::onRegistrySourceSave);
 }
 
 MainWindow::~MainWindow()
@@ -134,16 +186,10 @@ void MainWindow::closeEvent(QCloseEvent *event)
     QMainWindow::closeEvent(event);
 }
 
-void MainWindow::onDirectoryOpen()
+void gpui::MainWindow::loadPolicyBundleFolder(const QString& path)
 {
-    QString directory = QFileDialog::getExistingDirectory(
-                        this,
-                        tr("Open Directory"),
-                        QDir::homePath(),
-                        QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
-
     auto bundle = std::make_unique<model::bundle::PolicyBundle>();
-    d->model = bundle->loadFolder(directory.toStdString(), "ru-ru");
+    d->model = bundle->loadFolder(path.toStdString(), "ru-ru");
 
     d->sortModel = std::make_unique<QSortFilterProxyModel>();
     d->sortModel->setSourceModel(d->model.get());
@@ -157,6 +203,17 @@ void MainWindow::onDirectoryOpen()
     d->contentWidget->setSelectionModel(ui->treeView->selectionModel());
 }
 
+void MainWindow::onDirectoryOpen()
+{
+    QString directory = QFileDialog::getExistingDirectory(
+                        this,
+                        tr("Open Directory"),
+                        QDir::homePath(),
+                        QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
+
+    loadPolicyBundleFolder(directory);
+}
+
 void MainWindow::onUserRegistrySourceOpen()
 {
     onRegistrySourceOpen(d->userRegistry, d->userRegistrySource,
@@ -177,14 +234,30 @@ void MainWindow::onMachineRegistrySourceOpen()
 
 void MainWindow::onRegistrySourceSave()
 {
-    QString polFileName = QFileDialog::getSaveFileName(
-                        this,
-                        tr("Open Directory"),
-                        QDir::homePath(),
-                        "*.pol");
+    if (!d->machineRegistryPath.isEmpty())
+    {
+        qWarning() << "Saving machine registry to: " << d->machineRegistryPath;
+        save(d->machineRegistryPath.toStdString(), d->machineRegistry);
+    }
+    else
+    {
+        qWarning() << "Unable to save machine registry path is empty!";
+    }
 
-    save(polFileName.replace(".pol","Machine.pol").toStdString(), d->machineRegistry);
-    save(polFileName.replace(".pol","User.pol").toStdString(), d->userRegistry);
+    if (!d->userRegistryPath.isEmpty())
+    {
+        qWarning() << "Saving user registry to: " << d->userRegistryPath;
+        save(d->userRegistryPath.toStdString(), d->userRegistry);
+    }
+    else
+    {
+        qWarning() << "Unable to save user registry path is empty!";
+    }
+}
+
+void MainWindow::on_actionExit_triggered()
+{
+    QApplication::quit();
 }
 
 void MainWindow::onRegistrySourceOpen(std::shared_ptr<model::registry::Registry>& registry,
@@ -195,44 +268,61 @@ void MainWindow::onRegistrySourceOpen(std::shared_ptr<model::registry::Registry>
 
     connect(&browser, &SmbFileBrowser::onPolOpen, this, [&](const QString& path)
     {
-        qWarning() << "Path recieved: " << path;
-
-        auto stringvalues = std::make_unique<std::string>();
-
-        if (path.startsWith("smb://"))
-        {
-            SmbLocationItemFile smbLocationItemFile(path);
-            smbLocationItemFile.open(QFile::ReadWrite);
-            stringvalues->resize(smbLocationItemFile.size(), 0);
-            smbLocationItemFile.read(&stringvalues->at(0), smbLocationItemFile.size());
-        }
-        else
-        {
-            QFile registryFile(path);
-            registryFile.open(QFile::ReadWrite);
-            stringvalues->resize(registryFile.size(), 0);
-            registryFile.read(&stringvalues->at(0), registryFile.size());
-        }
-
-        auto iss = std::make_unique<std::istringstream>(*stringvalues);
-        std::string pluginName("pol");
-
-        auto reader = std::make_unique<io::GenericReader>();
-        auto registryFile = reader->load<io::RegistryFile, io::RegistryFileFormat<io::RegistryFile> >(*iss, pluginName);
-        if (!registryFile)
-        {
-            qWarning() << "Unable to load registry file contents.";
-            return;
-        }
-
-        registry = registryFile->getRegistry();
-
-        source = std::make_unique<model::registry::PolRegistrySource>(registry);
-
-        callback(source.get());
+        onPolFileOpen(path, registry, source, callback);
     });
 
     browser.exec();
 }
 
+void MainWindow::onPolFileOpen(const QString &path,
+                               std::shared_ptr<model::registry::Registry> &registry,
+                               std::unique_ptr<model::registry::AbstractRegistrySource> &source,
+                               std::function<void (model::registry::AbstractRegistrySource *)> callback)
+{
+    qWarning() << "Path recieved: " << path;
+
+    auto stringvalues = std::make_unique<std::string>();
+
+    try {
+
+    if (path.startsWith("smb://"))
+    {
+        SmbLocationItemFile smbLocationItemFile(path);
+        smbLocationItemFile.open(QFile::ReadWrite);
+        stringvalues->resize(smbLocationItemFile.size(), 0);
+        smbLocationItemFile.read(&stringvalues->at(0), smbLocationItemFile.size());
+        smbLocationItemFile.close();
+    }
+    else
+    {
+        QFile registryFile(path);
+        registryFile.open(QFile::ReadWrite);
+        stringvalues->resize(registryFile.size(), 0);
+        registryFile.read(&stringvalues->at(0), registryFile.size());
+        registryFile.close();
+    }
+
+    auto iss = std::make_unique<std::istringstream>(*stringvalues);
+    std::string pluginName("pol");
+
+    auto reader = std::make_unique<io::GenericReader>();
+    auto registryFile = reader->load<io::RegistryFile, io::RegistryFileFormat<io::RegistryFile> >(*iss, pluginName);
+    if (!registryFile)
+    {
+        qWarning() << "Unable to load registry file contents.";
+        return;
+    }
+
+    registry = registryFile->getRegistry();
+
+    source = std::make_unique<model::registry::PolRegistrySource>(registry);
+
+    callback(source.get());
+    }
+    catch (std::exception& e)
+    {
+        qWarning() << "Unable to read file: " << qPrintable(path) << " Error: " << e.what();
+    }
+}
+
 }
diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h
index 1318e48..86bb88b 100644
--- a/src/gui/mainwindow.h
+++ b/src/gui/mainwindow.h
@@ -38,6 +38,7 @@ namespace model {
 
 namespace gpui {
 
+    class CommandLineOptions;
     class MainWindowPrivate;
 
     class GPUI_GUI_EXPORT MainWindow : public QMainWindow {
@@ -45,7 +46,7 @@ namespace gpui {
 
     public:
         // construction and destruction
-        MainWindow(QWidget *parent = 0);
+        MainWindow(CommandLineOptions& options, QWidget *parent = 0);
         ~MainWindow();
 
     protected:
@@ -63,10 +64,19 @@ namespace gpui {
         void onUserRegistrySourceOpen();
         void onRegistrySourceSave();
 
+        void on_actionExit_triggered();
+
     private:
         void onRegistrySourceOpen(std::shared_ptr<model::registry::Registry>& registry,
                                   std::unique_ptr<model::registry::AbstractRegistrySource>& source,
                                   std::function<void(model::registry::AbstractRegistrySource* source)> callback);
+
+        void onPolFileOpen(const QString& path,
+                           std::shared_ptr<model::registry::Registry>& registry,
+                           std::unique_ptr<model::registry::AbstractRegistrySource>& source,
+                           std::function<void(model::registry::AbstractRegistrySource* source)> callback);
+
+        void loadPolicyBundleFolder(const QString& path);
     };
 }
 
diff --git a/src/gui/mainwindow.ui b/src/gui/mainwindow.ui
index 7926e0d..ebcb801 100644
--- a/src/gui/mainwindow.ui
+++ b/src/gui/mainwindow.ui
@@ -76,7 +76,6 @@
     <addaction name="separator"/>
     <addaction name="actionOpenUserRegistrySource"/>
     <addaction name="actionOpenMachineRegistrySource"/>
-    <addaction name="actionSaveRegistrySource"/>
     <addaction name="separator"/>
     <addaction name="actionOptions"/>
     <addaction name="separator"/>