diff --git a/CMakeLists.txt b/CMakeLists.txt index d8a8324..f69414e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,7 +56,7 @@ if(USE_SYSTEM_PUGIXML) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE pugixml) else() include(cmake/BuildPugiXML.cmake) - target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE libpugixml) + target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE libpugixml_internal) endif() include(cmake/BuildJSONCONS.cmake) @@ -65,6 +65,9 @@ target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE jsoncons) include(cmake/BuildInja.cmake) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE inja) +include(cmake/BuildLexbor.cmake) +target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE liblexbor_internal) + target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE vendor/nlohmann-json) target_sources(${CMAKE_PROJECT_NAME} PRIVATE src/plugin-main.c src/url-source.cpp src/ui/RequestBuilder.cpp diff --git a/cmake/BuildLexbor.cmake b/cmake/BuildLexbor.cmake new file mode 100644 index 0000000..105b16c --- /dev/null +++ b/cmake/BuildLexbor.cmake @@ -0,0 +1,47 @@ +include(ExternalProject) + +if(APPLE) + set(LEXBOR_CMAKE_PLATFORM_OPTIONS -DCMAKE_OSX_ARCHITECTURES=x86_64$arm64) +else() + if(WIN32) + add_compile_definitions(LEXBOR_STATIC=1) + set(LEXBOR_CMAKE_PLATFORM_OPTIONS "-DCMAKE_C_FLAGS=/W3 /utf-8 /MP" "-DCMAKE_CXX_FLAGS=/W3 /utf-8 /MP") + else() + set(LEXBOR_CMAKE_PLATFORM_OPTIONS -DCMAKE_SYSTEM_NAME=Linux) + endif() +endif() + +set(lexbor_lib_filename ${CMAKE_STATIC_LIBRARY_PREFIX}lexbor_static${CMAKE_STATIC_LIBRARY_SUFFIX}) + +ExternalProject_Add( + lexbor_build + GIT_REPOSITORY https://github.com/lexbor/lexbor.git + GIT_TAG v2.3.0 + CMAKE_GENERATOR ${CMAKE_GENERATOR} + INSTALL_BYPRODUCTS /lib/${lexbor_lib_filename} + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX= + -DLEXBOR_BUILD_SHARED=OFF + -DLEXBOR_BUILD_STATIC=ON + -DLEXBOR_BUILD_TESTS_CPP=OFF + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} + -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + -DCMAKE_LINKER=${CMAKE_LINKER} + ${LEXBOR_CMAKE_PLATFORM_OPTIONS}) + +ExternalProject_Get_Property(lexbor_build INSTALL_DIR) + +message(STATUS "lexbor will be installed to ${INSTALL_DIR}") + +# find the library +set(lexbor_lib_location ${INSTALL_DIR}/lib/${lexbor_lib_filename}) + +message(STATUS "lexbor library expected at ${lexbor_lib_location}") + +add_library(lexbor_internal STATIC IMPORTED) +set_target_properties(lexbor_internal PROPERTIES IMPORTED_LOCATION ${lexbor_lib_location}) + +add_library(liblexbor_internal INTERFACE) +add_dependencies(liblexbor_internal lexbor_build) +target_link_libraries(liblexbor_internal INTERFACE lexbor_internal) +set_target_properties(liblexbor_internal PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${INSTALL_DIR}/include) diff --git a/cmake/BuildPugiXML.cmake b/cmake/BuildPugiXML.cmake index 56d98c9..8dd66fb 100644 --- a/cmake/BuildPugiXML.cmake +++ b/cmake/BuildPugiXML.cmake @@ -13,7 +13,7 @@ ExternalProject_Add( CMAKE_GENERATOR ${CMAKE_GENERATOR} INSTALL_BYPRODUCTS /lib/${CMAKE_STATIC_LIBRARY_PREFIX}pugixml${CMAKE_STATIC_LIBRARY_SUFFIX} CMAKE_ARGS -DCMAKE_INSTALL_PREFIX= -DBUILD_SHARED_LIBS=OFF -DPUGIXML_BUILD_TESTS=OFF - ${PUGIXML_CMAKE_PLATFORM_OPTIONS}) + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} ${PUGIXML_CMAKE_PLATFORM_OPTIONS}) ExternalProject_Get_Property(pugixml_build INSTALL_DIR) @@ -25,10 +25,10 @@ set(pugixml_lib_location ${INSTALL_DIR}/lib/${pugixml_lib_filename}) message(STATUS "pugixml library expected at ${pugixml_lib_location}") -add_library(pugixml STATIC IMPORTED) -set_target_properties(pugixml PROPERTIES IMPORTED_LOCATION ${pugixml_lib_location}) +add_library(pugixml_internal STATIC IMPORTED) +set_target_properties(pugixml_internal PROPERTIES IMPORTED_LOCATION ${pugixml_lib_location}) -add_library(libpugixml INTERFACE) -add_dependencies(libpugixml pugixml_build) -target_link_libraries(libpugixml INTERFACE pugixml) -set_target_properties(libpugixml PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${INSTALL_DIR}/include) +add_library(libpugixml_internal INTERFACE) +add_dependencies(libpugixml_internal pugixml_build) +target_link_libraries(libpugixml_internal INTERFACE pugixml_internal) +set_target_properties(libpugixml_internal PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${INSTALL_DIR}/include) diff --git a/src/parsers/CMakeLists.txt b/src/parsers/CMakeLists.txt index 568ac5b..fd51be4 100644 --- a/src/parsers/CMakeLists.txt +++ b/src/parsers/CMakeLists.txt @@ -1,4 +1,4 @@ -target_sources(${CMAKE_PROJECT_NAME} PRIVATE jsonpointer.cpp jsonpath.cpp regex.cpp xml.cpp errors.cpp) +target_sources(${CMAKE_PROJECT_NAME} PRIVATE jsonpointer.cpp jsonpath.cpp regex.cpp xml.cpp errors.cpp html.cpp) # on linux, disable conversion errors if(UNIX AND NOT APPLE) diff --git a/src/parsers/html.cpp b/src/parsers/html.cpp new file mode 100644 index 0000000..2b710a7 --- /dev/null +++ b/src/parsers/html.cpp @@ -0,0 +1,118 @@ +#include "request-data.h" +#include "plugin-support.h" +#include "errors.h" + +#include +#include +#include +#include +#include + +#include + +lxb_inline lxb_status_t serializer_callback(const lxb_char_t *data, size_t len, void *ctx) +{ + ((std::string *)ctx)->append((const char *)data, len); + return LXB_STATUS_OK; +} + +lxb_status_t find_callback(lxb_dom_node_t *node, lxb_css_selector_specificity_t spec, void *data) +{ + UNUSED_PARAMETER(spec); + std::string str; + (void)lxb_html_serialize_deep_cb(node, serializer_callback, &str); + ((std::vector *)data)->push_back(str); + return LXB_STATUS_OK; +} + +lxb_status_t find_with_selectors(const std::string &slctrs, lxb_html_document_t *document, + std::vector &found) +{ + /* Create CSS parser. */ + lxb_css_parser_t *parser; + lxb_css_selector_list_t *list; + lxb_status_t status; + lxb_dom_node_t *body; + lxb_selectors_t *selectors; + + parser = lxb_css_parser_create(); + status = lxb_css_parser_init(parser, NULL); + if (status != LXB_STATUS_OK) { + obs_log(LOG_ERROR, "Failed to setup CSS parser"); + return EXIT_FAILURE; + } + + /* Selectors. */ + selectors = lxb_selectors_create(); + status = lxb_selectors_init(selectors); + if (status != LXB_STATUS_OK) { + obs_log(LOG_ERROR, "Failed to setup Selectors"); + return EXIT_FAILURE; + } + + /* Parse and get the log. */ + + list = lxb_css_selectors_parse(parser, (const lxb_char_t *)slctrs.c_str(), slctrs.length()); + if (parser->status != LXB_STATUS_OK) { + obs_log(LOG_ERROR, "Failed to parse CSS selectors"); + return EXIT_FAILURE; + } + + /* Find HTML nodes by CSS Selectors. */ + body = lxb_dom_interface_node(lxb_html_document_body_element(document)); + + status = lxb_selectors_find(selectors, body, list, find_callback, &found); + if (status != LXB_STATUS_OK) { + obs_log(LOG_ERROR, "Failed to find HTML nodes by CSS Selectors"); + return EXIT_FAILURE; + } + + /* Destroy Selectors object. */ + (void)lxb_selectors_destroy(selectors, true); + + /* Destroy resources for CSS Parser. */ + (void)lxb_css_parser_destroy(parser, true); + + /* Destroy all object for all CSS Selector List. */ + lxb_css_selector_list_destroy_memory(list); + + return LXB_STATUS_OK; +} + +struct request_data_handler_response parse_html(struct request_data_handler_response response, + const url_source_request_data *request_data) +{ + lxb_status_t status; + lxb_html_document_t *document; + + document = lxb_html_document_create(); + if (document == NULL) { + return make_fail_parse_response("Failed to setup HTML parser"); + } + + status = lxb_html_document_parse(document, (const lxb_char_t *)response.body.c_str(), + response.body.length()); + if (status != LXB_STATUS_OK) { + return make_fail_parse_response("Failed to parse HTML"); + } + + std::string parsed_output = response.body; + // Get the output value + if (request_data->output_cssselector != "") { + std::vector found; + if (find_with_selectors(request_data->output_cssselector, document, found) != + LXB_STATUS_OK) { + return make_fail_parse_response("Failed to find element with CSS selector"); + } else { + if (found.size() > 0) { + std::copy(found.begin(), found.end(), + std::back_inserter(response.body_parts_parsed)); + } + } + } else { + // Return the whole HTML object + response.body_parts_parsed.push_back(parsed_output); + } + + return response; +} diff --git a/src/parsers/parsers.h b/src/parsers/parsers.h index f2f6c06..f07300f 100644 --- a/src/parsers/parsers.h +++ b/src/parsers/parsers.h @@ -19,6 +19,9 @@ struct request_data_handler_response parse_regex(struct request_data_handler_res struct request_data_handler_response parse_xml(struct request_data_handler_response response, const url_source_request_data *request_data); +struct request_data_handler_response parse_html(struct request_data_handler_response response, + const url_source_request_data *request_data); + struct request_data_handler_response parse_xml_by_xquery(struct request_data_handler_response response, const url_source_request_data *request_data); diff --git a/src/request-data.cpp b/src/request-data.cpp index b712c5d..960337b 100644 --- a/src/request-data.cpp +++ b/src/request-data.cpp @@ -224,11 +224,12 @@ struct request_data_handler_response request_data_handler(url_source_request_dat // attempt to parse as json and return the whole object response = parse_json(response, request_data); } - } else if (request_data->output_type == "XML (XPath)" || - request_data->output_type == "HTML") { + } else if (request_data->output_type == "XML (XPath)") { response = parse_xml(response, request_data); } else if (request_data->output_type == "XML (XQuery)") { response = parse_xml_by_xquery(response, request_data); + } else if (request_data->output_type == "HTML") { + response = parse_html(response, request_data); } else if (request_data->output_type == "Text") { response = parse_regex(response, request_data); } else { @@ -295,6 +296,7 @@ std::string serialize_request_data(url_source_request_data *request_data) json["output_regex"] = request_data->output_regex; json["output_regex_flags"] = request_data->output_regex_flags; json["output_regex_group"] = request_data->output_regex_group; + json["output_cssselector"] = request_data->output_cssselector; // postprocess options json["post_process_regex"] = request_data->post_process_regex; json["post_process_regex_is_replace"] = request_data->post_process_regex_is_replace; @@ -395,6 +397,9 @@ url_source_request_data unserialize_request_data(std::string serialized_request_ request_data.output_regex = json["output_regex"].get(); request_data.output_regex_flags = json["output_regex_flags"].get(); request_data.output_regex_group = json["output_regex_group"].get(); + if (json.contains("output_cssselector")) + request_data.output_cssselector = + json["output_cssselector"].get(); // postprocess options if (json.contains("post_process_regex")) { diff --git a/src/request-data.h b/src/request-data.h index 2b67e26..7332921 100644 --- a/src/request-data.h +++ b/src/request-data.h @@ -35,6 +35,7 @@ struct url_source_request_data { std::string output_regex; std::string output_regex_flags; std::string output_regex_group; + std::string output_cssselector; // post process options std::string post_process_regex; bool post_process_regex_is_replace; @@ -60,6 +61,7 @@ struct url_source_request_data { output_regex = std::string(""); output_regex_flags = std::string(""); output_regex_group = std::string("0"); + output_cssselector = std::string(""); } }; diff --git a/src/ui/RequestBuilder.cpp b/src/ui/RequestBuilder.cpp index 74e5582..f3846cc 100644 --- a/src/ui/RequestBuilder.cpp +++ b/src/ui/RequestBuilder.cpp @@ -1,86 +1,10 @@ #include "RequestBuilder.h" +#include "ui_requestbuilder.h" #include "CollapseButton.h" #include -class KeyValueWidget : public QWidget { -public: - KeyValueWidget(QWidget *parent = nullptr) : QWidget(parent) - { - QHBoxLayout *layout = new QHBoxLayout; - setLayout(layout); - - QLineEdit *keyLineEdit = new QLineEdit; - keyLineEdit->setPlaceholderText("Key"); - layout->addWidget(keyLineEdit); - - QLineEdit *valueLineEdit = new QLineEdit; - valueLineEdit->setPlaceholderText("Value"); - layout->addWidget(valueLineEdit); - - QPushButton *removeButton = new QPushButton("-"); - layout->addWidget(removeButton); - connect(removeButton, &QPushButton::clicked, this, [=]() { - // Remove this widget from the parent - delete this; - }); - } -}; - -class KeyValueListWidget : public QWidget { -private: - QVBoxLayout *listLayout; - -public: - KeyValueListWidget(QWidget *parent = nullptr) : QWidget(parent), listLayout(new QVBoxLayout) - { - setLayout(listLayout); - - // Add a "+" button to add a new key-value widget - QPushButton *addButton = new QPushButton("+"); - listLayout->addWidget(addButton); - connect(addButton, &QPushButton::clicked, this, [=]() { - // Add a new key-value widget - KeyValueWidget *keyValueWidget = new KeyValueWidget(this); - listLayout->insertWidget(listLayout->count() - 1, keyValueWidget); - // adjust the size of the widget to fit the content - adjustSize(); - }); - } - - void populateFromPairs(std::vector> &pairs) - { - for (auto &pair : pairs) { - KeyValueWidget *keyValueWidget = new KeyValueWidget; - QLineEdit *keyLineEdit = - (QLineEdit *)keyValueWidget->layout()->itemAt(0)->widget(); - QLineEdit *valueLineEdit = - (QLineEdit *)keyValueWidget->layout()->itemAt(1)->widget(); - keyLineEdit->setText(QString::fromStdString(pair.first)); - valueLineEdit->setText(QString::fromStdString(pair.second)); - listLayout->insertWidget(listLayout->count() - 1, keyValueWidget); - } - // adjust the size of the widget to fit the content - adjustSize(); - } -}; - -void get_key_value_as_pairs_from_key_value_list_widget( - KeyValueListWidget *widget, std::vector> &pairs) -{ - pairs.clear(); - for (int i = 0; i < widget->layout()->count() - 1; i++) { - KeyValueWidget *keyValueWidget = - (KeyValueWidget *)widget->layout()->itemAt(i)->widget(); - QLineEdit *keyLineEdit = (QLineEdit *)keyValueWidget->layout()->itemAt(0)->widget(); - QLineEdit *valueLineEdit = - (QLineEdit *)keyValueWidget->layout()->itemAt(1)->widget(); - pairs.push_back(std::make_pair(keyLineEdit->text().toStdString(), - valueLineEdit->text().toStdString())); - } -} - void set_form_row_visibility(QFormLayout *layout, QWidget *widget, bool visible) { for (int i = 0; i < layout->rowCount(); i++) { @@ -112,8 +36,10 @@ bool add_sources_to_qcombobox(void *list, obs_source_t *obs_source) RequestBuilder::RequestBuilder(url_source_request_data *request_data, std::function update_handler, QWidget *parent) - : QDialog(parent), layout(new QVBoxLayout) + : QDialog(parent), layout(new QVBoxLayout), ui(new Ui::RequestBuilder) { + ui->setupUi(this); + setWindowTitle("HTTP Request Builder"); setLayout(layout); // Make modal @@ -122,387 +48,272 @@ RequestBuilder::RequestBuilder(url_source_request_data *request_data, // set a minimum width for the dialog setMinimumWidth(500); - QFormLayout *formLayout = new QFormLayout; - layout->addLayout(formLayout); - // expand the form layout to fill the parent horizontally - formLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow); - - // radio buttons to choose URL or File - QHBoxLayout *urlOrFileLayout = new QHBoxLayout; - formLayout->addRow("Source:", urlOrFileLayout); - QRadioButton *urlRadioButton = new QRadioButton("URL"); - urlOrFileLayout->addWidget(urlRadioButton); - QRadioButton *fileRadioButton = new QRadioButton("File"); - urlOrFileLayout->addWidget(fileRadioButton); - // mark selected radio button from request_data if (request_data->url_or_file == "url") { - urlRadioButton->setChecked(true); + ui->urlRadioButton->setChecked(true); } else { - fileRadioButton->setChecked(true); + ui->fileRadioButton->setChecked(true); } - // URL or file path - QHBoxLayout *urlOrFileInputLayout = new QHBoxLayout; - formLayout->addRow("URL/File", urlOrFileInputLayout); - - QLineEdit *urlLineEdit = new QLineEdit; - urlLineEdit->setPlaceholderText("URL/File"); - // set value from request_data - urlLineEdit->setText(QString::fromStdString(request_data->url)); - urlOrFileInputLayout->addWidget(urlLineEdit); - // add file selector button if file is selected - QPushButton *urlOrFileButton = new QPushButton("..."); - urlOrFileInputLayout->addWidget(urlOrFileButton); - // set value from request_data - urlOrFileButton->setEnabled(request_data->url_or_file == "file"); - - QGroupBox *urlRequestOptionsGroup = new QGroupBox("URL Request Options", this); + ui->urlLineEdit->setText(QString::fromStdString(request_data->url)); + ui->urlOrFileButton->setEnabled(request_data->url_or_file == "file"); // URL or file dialog - connect(urlOrFileButton, &QPushButton::clicked, this, [=]() { + connect(ui->urlOrFileButton, &QPushButton::clicked, this, [=]() { QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), "", tr("All Files (*.*)")); if (fileName != "") { - urlLineEdit->setText(fileName); + ui->urlLineEdit->setText(fileName); } }); - urlRequestOptionsGroup->setVisible(request_data->url_or_file == "url"); + ui->urlRequestOptionsGroup->setVisible(request_data->url_or_file == "url"); auto toggleFileUrlButtons = [=]() { - urlOrFileButton->setEnabled(fileRadioButton->isChecked()); + ui->urlOrFileButton->setEnabled(ui->fileRadioButton->isChecked()); // hide the urlRequestOptionsGroup if file is selected - urlRequestOptionsGroup->setVisible(urlRadioButton->isChecked()); + ui->urlRequestOptionsGroup->setVisible(ui->urlRadioButton->isChecked()); // adjust the size of the dialog to fit the content this->adjustSize(); }; // show file selector button if file is selected - connect(fileRadioButton, &QRadioButton::toggled, this, toggleFileUrlButtons); - connect(urlRadioButton, &QRadioButton::toggled, this, toggleFileUrlButtons); - - QFormLayout *urlRequestLayout = new QFormLayout; - urlRequestOptionsGroup->setLayout(urlRequestLayout); - urlRequestLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow); - layout->addWidget(urlRequestOptionsGroup); - - // Method select from dropdown: GET, POST, PUT, DELETE, PATCH - QComboBox *methodComboBox = new QComboBox; - methodComboBox->addItem("GET"); - methodComboBox->addItem("POST"); - // set value from request_data - methodComboBox->setCurrentText(QString::fromStdString(request_data->method)); - // add a label and the method dropdown to the url request options group with horizontal layout - urlRequestLayout->addRow("Method:", methodComboBox); - - /* --- Authentication via SSL certificates --- */ - - QGroupBox *sslGroupBox = new QGroupBox("SSL", this); - QVBoxLayout *sslLayout = new QVBoxLayout; - sslGroupBox->setLayout(sslLayout); - - // SSL certificate file using a file selector - QHBoxLayout *sslCertFileLayout = new QHBoxLayout; - sslLayout->addLayout(sslCertFileLayout); - QLineEdit *sslCertFileLineEdit = new QLineEdit; - sslCertFileLineEdit->setPlaceholderText("SSL certificate file"); - sslCertFileLineEdit->setText(QString::fromStdString(request_data->ssl_client_cert_file)); - sslCertFileLayout->addWidget(sslCertFileLineEdit); - QPushButton *sslCertFileButton = new QPushButton("..."); - sslCertFileLayout->addWidget(sslCertFileButton); + connect(ui->fileRadioButton, &QRadioButton::toggled, this, toggleFileUrlButtons); + connect(ui->urlRadioButton, &QRadioButton::toggled, this, toggleFileUrlButtons); + + ui->methodComboBox->setCurrentText(QString::fromStdString(request_data->method)); + + // populate headers in ui->tableView_headers from request_data->headers + QStandardItemModel *model = new QStandardItemModel; + for (auto &pair : request_data->headers) { + // add a new row + model->appendRow({new QStandardItem(QString::fromStdString(pair.first)), + new QStandardItem(QString::fromStdString(pair.second))}); + } + ui->tableView_headers->setModel(model); + connect(ui->pushButton_addHeader, &QPushButton::clicked, this, [=]() { + // add a new row + ((QStandardItemModel *)ui->tableView_headers->model()) + ->appendRow({new QStandardItem(""), new QStandardItem("")}); + }); + connect(ui->pushButton_removeHeader, &QPushButton::clicked, this, [=]() { + // remove the selected row + ui->tableView_headers->model()->removeRow( + ui->tableView_headers->selectionModel()->currentIndex().row()); + }); + + ui->sslCertFileLineEdit->setText( + QString::fromStdString(request_data->ssl_client_cert_file)); + ui->sslKeyFileLineEdit->setText(QString::fromStdString(request_data->ssl_client_key_file)); + ui->sslKeyPasswordLineEdit->setText( + QString::fromStdString(request_data->ssl_client_key_pass)); + ui->verifyPeerCheckBox->setChecked(request_data->ssl_verify_peer); // SSL certificate file dialog - connect(sslCertFileButton, &QPushButton::clicked, this, [=]() { + connect(ui->sslCertFileButton, &QPushButton::clicked, this, [=]() { QString fileName = QFileDialog::getOpenFileName(this, tr("Open SSL certificate file"), "", tr("SSL certificate files (*.pem)")); if (fileName != "") { - sslCertFileLineEdit->setText(fileName); + ui->sslCertFileLineEdit->setText(fileName); } }); - // SSL key file using a file selector - QHBoxLayout *sslKeyFileLayout = new QHBoxLayout; - sslLayout->addLayout(sslKeyFileLayout); - QLineEdit *sslKeyFileLineEdit = new QLineEdit; - sslKeyFileLineEdit->setPlaceholderText("SSL key file"); - sslKeyFileLineEdit->setText(QString::fromStdString(request_data->ssl_client_key_file)); - sslKeyFileLayout->addWidget(sslKeyFileLineEdit); - QPushButton *sslKeyFileButton = new QPushButton("..."); - sslKeyFileLayout->addWidget(sslKeyFileButton); - // SSL key file dialog - connect(sslKeyFileButton, &QPushButton::clicked, this, [=]() { + connect(ui->sslKeyFileButton, &QPushButton::clicked, this, [=]() { QString fileName = QFileDialog::getOpenFileName(this, tr("Open SSL key file"), "", tr("SSL key files (*.pem)")); if (fileName != "") { - sslKeyFileLineEdit->setText(fileName); + ui->sslKeyFileLineEdit->setText(fileName); } }); - // SSL key password - QLineEdit *sslKeyPasswordLineEdit = new QLineEdit; - sslKeyPasswordLineEdit->setPlaceholderText("SSL key password"); - sslKeyPasswordLineEdit->setText(QString::fromStdString(request_data->ssl_client_key_pass)); - sslLayout->addWidget(sslKeyPasswordLineEdit); - - // Verify peer checkbox - QCheckBox *verifyPeerCheckBox = new QCheckBox("Verify peer"); - verifyPeerCheckBox->setChecked(request_data->ssl_verify_peer); - sslLayout->addWidget(verifyPeerCheckBox); - - CollapseButton *advancedOptionsCollapseButton = new CollapseButton(this); - advancedOptionsCollapseButton->setText("SSL options"); - - /* --- End SSL options --- */ - - // Headers - KeyValueListWidget *headersWidget = new KeyValueListWidget; - headersWidget->populateFromPairs(request_data->headers); - // add headers widget to urlRequestLayout with label - urlRequestLayout->addRow("Headers:", headersWidget); - - // Dynamic data from OBS text source - QWidget *obsTextSourceWidget = new QWidget; - QHBoxLayout *obsTextSourceLayout = new QHBoxLayout; - obsTextSourceWidget->setLayout(obsTextSourceLayout); - // Add a dropdown to select a OBS text source or None - QComboBox *obsTextSourceComboBox = new QComboBox; - obsTextSourceComboBox->addItem("None"); + connect(ui->sslOptionsCheckbox, &QCheckBox::toggled, this, [=](bool checked) { + // Show the SSL options if the checkbox is checked + ui->sslOptionsGroup->setVisible(checked); + // adjust the size of the dialog to fit the content + this->adjustSize(); + }); + if (request_data->ssl_client_cert_file != "" || request_data->ssl_client_key_file != "" || + request_data->ssl_client_key_pass != "" || request_data->ssl_verify_peer) { + ui->sslOptionsCheckbox->setChecked(true); + } + ui->sslOptionsGroup->setVisible(ui->sslOptionsCheckbox->isChecked()); + // populate list of OBS text sources - obs_enum_sources(add_sources_to_qcombobox, obsTextSourceComboBox); + obs_enum_sources(add_sources_to_qcombobox, ui->obsTextSourceComboBox); // Select the current OBS text source, if any - int itemIdx = obsTextSourceComboBox->findText( + int itemIdx = ui->obsTextSourceComboBox->findText( QString::fromStdString(request_data->obs_text_source)); if (itemIdx != -1) { - obsTextSourceComboBox->setCurrentIndex(itemIdx); + ui->obsTextSourceComboBox->setCurrentIndex(itemIdx); } else { - obsTextSourceComboBox->setCurrentIndex(0); + ui->obsTextSourceComboBox->setCurrentIndex(0); } - // add a checkbox to disable the request if the OBS text source is empty - QCheckBox *obsTextSourceEnabledCheckBox = new QCheckBox("Skip empty"); - obsTextSourceEnabledCheckBox->setChecked(request_data->obs_text_source_skip_if_empty); - QCheckBox *obsTextSourceSkipSameCheckBox = new QCheckBox("Skip same"); - obsTextSourceSkipSameCheckBox->setChecked(request_data->obs_text_source_skip_if_same); - // add to urlRequestLayout with horizontal layout - obsTextSourceLayout->addWidget(obsTextSourceComboBox); - obsTextSourceLayout->addWidget(obsTextSourceEnabledCheckBox); - obsTextSourceLayout->addWidget(obsTextSourceSkipSameCheckBox); - // add to urlRequestLayout as a row - urlRequestLayout->addRow("Dynamic Input:", obsTextSourceWidget); - // add a tooltip to explain the dynamic input - obsTextSourceComboBox->setToolTip( - "Select a OBS text source to use its current text in the querystring or request body as `{{input}}`."); - - // Body - QLineEdit *bodyLineEdit = new QLineEdit; - bodyLineEdit->setPlaceholderText("Body"); - bodyLineEdit->setText(QString::fromStdString(request_data->body)); - // add to urlRequestLayout with horizontal layout - urlRequestLayout->addRow("Body:", bodyLineEdit); + ui->obsTextSourceEnabledCheckBox->setChecked(request_data->obs_text_source_skip_if_empty); + ui->obsTextSourceSkipSameCheckBox->setChecked(request_data->obs_text_source_skip_if_same); + ui->bodyTextEdit->setText(QString::fromStdString(request_data->body)); auto setVisibilityOfBody = [=]() { // If method is not GET, show the body input - set_form_row_visibility(urlRequestLayout, bodyLineEdit, - methodComboBox->currentText() != "GET"); + set_form_row_visibility(ui->urlRequestLayout, ui->bodyTextEdit, + ui->methodComboBox->currentText() != "GET"); this->adjustSize(); }; setVisibilityOfBody(); // Method select from dropdown change - connect(methodComboBox, &QComboBox::currentTextChanged, this, setVisibilityOfBody); - - urlRequestLayout->addWidget(advancedOptionsCollapseButton); - sslGroupBox->adjustSize(); - urlRequestLayout->addWidget(sslGroupBox); - advancedOptionsCollapseButton->setContent(sslGroupBox, this); - - // ------------ Output parsing options -------------- - QGroupBox *outputGroupBox = new QGroupBox("Output Parsing", this); - layout->addWidget(outputGroupBox); - - QFormLayout *formOutputParsing = new QFormLayout; - formOutputParsing->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow); - outputGroupBox->setLayout(formOutputParsing); - - // Output type: Text, JSON, XML, HTML - QComboBox *outputTypeComboBox = new QComboBox; - outputTypeComboBox->addItem("Text"); - outputTypeComboBox->addItem("JSON"); - outputTypeComboBox->addItem("XML (XPath)"); - outputTypeComboBox->addItem("XML (XQuery)"); - outputTypeComboBox->addItem("HTML"); - outputTypeComboBox->setCurrentIndex( - outputTypeComboBox->findText(QString::fromStdString(request_data->output_type))); - formOutputParsing->addRow("Content-Type", outputTypeComboBox); - - QLineEdit *outputJSONPointerLineEdit = new QLineEdit; - outputJSONPointerLineEdit->setText( + connect(ui->methodComboBox, &QComboBox::currentTextChanged, this, setVisibilityOfBody); + + ui->outputTypeComboBox->setCurrentIndex(ui->outputTypeComboBox->findText( + QString::fromStdString(request_data->output_type))); + ui->outputJSONPointerLineEdit->setText( QString::fromStdString(request_data->output_json_pointer)); - outputJSONPointerLineEdit->setPlaceholderText("JSON Pointer"); - formOutputParsing->addRow("JSON Pointer", outputJSONPointerLineEdit); - QLineEdit *outputJSONPathLineEdit = new QLineEdit; - outputJSONPathLineEdit->setText(QString::fromStdString(request_data->output_json_path)); - outputJSONPathLineEdit->setPlaceholderText("JSON Path"); - formOutputParsing->addRow("JSON Path", outputJSONPathLineEdit); - QLineEdit *outputXPathLineEdit = new QLineEdit; - outputXPathLineEdit->setText(QString::fromStdString(request_data->output_xpath)); - outputXPathLineEdit->setPlaceholderText("XPath"); - formOutputParsing->addRow("XPath", outputXPathLineEdit); - QLineEdit *outputXQueryLineEdit = new QLineEdit; - outputXQueryLineEdit->setText(QString::fromStdString(request_data->output_xquery)); - outputXQueryLineEdit->setPlaceholderText("XQuery"); - formOutputParsing->addRow("XQuery", outputXQueryLineEdit); - QLineEdit *outputRegexLineEdit = new QLineEdit; - outputRegexLineEdit->setText(QString::fromStdString(request_data->output_regex)); - outputRegexLineEdit->setPlaceholderText("Regex"); - formOutputParsing->addRow("Regex", outputRegexLineEdit); - QLineEdit *outputRegexFlagsLineEdit = new QLineEdit; - outputRegexFlagsLineEdit->setText(QString::fromStdString(request_data->output_regex_flags)); - outputRegexFlagsLineEdit->setPlaceholderText("Regex flags"); - formOutputParsing->addRow("Regex flags", outputRegexFlagsLineEdit); - QLineEdit *outputRegexGroupLineEdit = new QLineEdit; - outputRegexGroupLineEdit->setText(QString::fromStdString(request_data->output_regex_group)); - outputRegexGroupLineEdit->setPlaceholderText("Regex group"); - formOutputParsing->addRow("Regex group", outputRegexGroupLineEdit); + ui->outputJSONPathLineEdit->setText(QString::fromStdString(request_data->output_json_path)); + ui->outputXPathLineEdit->setText(QString::fromStdString(request_data->output_xpath)); + ui->outputXQueryLineEdit->setText(QString::fromStdString(request_data->output_xquery)); + ui->outputRegexLineEdit->setText(QString::fromStdString(request_data->output_regex)); + ui->outputRegexFlagsLineEdit->setText( + QString::fromStdString(request_data->output_regex_flags)); + ui->outputRegexGroupLineEdit->setText( + QString::fromStdString(request_data->output_regex_group)); + ui->cssSelectorLineEdit->setText(QString::fromStdString(request_data->output_cssselector)); auto setVisibilityOfOutputParsingOptions = [=]() { // Hide all output parsing options for (const auto &widget : - {outputJSONPathLineEdit, outputXPathLineEdit, outputXQueryLineEdit, - outputRegexLineEdit, outputRegexFlagsLineEdit, outputRegexGroupLineEdit, - outputJSONPointerLineEdit}) { - set_form_row_visibility(formOutputParsing, widget, false); + {ui->outputJSONPathLineEdit, ui->outputXPathLineEdit, ui->outputXQueryLineEdit, + ui->outputRegexLineEdit, ui->outputRegexFlagsLineEdit, + ui->outputRegexGroupLineEdit, ui->outputJSONPointerLineEdit, + ui->cssSelectorLineEdit}) { + set_form_row_visibility(ui->formOutputParsing, widget, false); } // Show the output parsing options for the selected output type - if (outputTypeComboBox->currentText() == "JSON") { - set_form_row_visibility(formOutputParsing, outputJSONPathLineEdit, true); - set_form_row_visibility(formOutputParsing, outputJSONPointerLineEdit, true); - } else if (outputTypeComboBox->currentText() == "XML (XPath)" || - outputTypeComboBox->currentText() == "HTML") { - set_form_row_visibility(formOutputParsing, outputXPathLineEdit, true); - } else if (outputTypeComboBox->currentText() == "XML (XQuery)") { - set_form_row_visibility(formOutputParsing, outputXQueryLineEdit, true); - } else if (outputTypeComboBox->currentText() == "Text") { - set_form_row_visibility(formOutputParsing, outputRegexLineEdit, true); - set_form_row_visibility(formOutputParsing, outputRegexFlagsLineEdit, true); - set_form_row_visibility(formOutputParsing, outputRegexGroupLineEdit, true); + if (ui->outputTypeComboBox->currentText() == "JSON") { + set_form_row_visibility(ui->formOutputParsing, ui->outputJSONPathLineEdit, + true); + set_form_row_visibility(ui->formOutputParsing, + ui->outputJSONPointerLineEdit, true); + } else if (ui->outputTypeComboBox->currentText() == "XML (XPath)") { + set_form_row_visibility(ui->formOutputParsing, ui->outputXPathLineEdit, + true); + } else if (ui->outputTypeComboBox->currentText() == "XML (XQuery)") { + set_form_row_visibility(ui->formOutputParsing, ui->outputXQueryLineEdit, + true); + } else if (ui->outputTypeComboBox->currentText() == "HTML") { + set_form_row_visibility(ui->formOutputParsing, ui->cssSelectorLineEdit, + true); + } else if (ui->outputTypeComboBox->currentText() == "Text") { + set_form_row_visibility(ui->formOutputParsing, ui->outputRegexLineEdit, + true); + set_form_row_visibility(ui->formOutputParsing, ui->outputRegexFlagsLineEdit, + true); + set_form_row_visibility(ui->formOutputParsing, ui->outputRegexGroupLineEdit, + true); } }; // Show the output parsing options for the selected output type setVisibilityOfOutputParsingOptions(); // Respond to changes in the output type - connect(outputTypeComboBox, &QComboBox::currentTextChanged, this, + connect(ui->outputTypeComboBox, &QComboBox::currentTextChanged, this, setVisibilityOfOutputParsingOptions); - // Add postprocess regex options - // The main option is a regex to run on the output, then a checkbox to indicate if it's a - // replace, and a replace string which should appear only if the checkbox is checked - // All options should be packed in Horizontal layout and added to the form as a row - QLineEdit *postProcessRegexLineEdit = new QLineEdit; - postProcessRegexLineEdit->setText(QString::fromStdString(request_data->post_process_regex)); - postProcessRegexLineEdit->setPlaceholderText("Regex"); - - QCheckBox *postProcessRegexIsReplaceCheckBox = new QCheckBox("Replace"); - postProcessRegexIsReplaceCheckBox->setChecked(request_data->post_process_regex_is_replace); - - QLineEdit *postProcessRegexReplaceLineEdit = new QLineEdit; - postProcessRegexReplaceLineEdit->setText( + ui->postProcessRegexLineEdit->setText( + QString::fromStdString(request_data->post_process_regex)); + ui->postProcessRegexIsReplaceCheckBox->setChecked( + request_data->post_process_regex_is_replace); + ui->postProcessRegexReplaceLineEdit->setText( QString::fromStdString(request_data->post_process_regex_replace)); - postProcessRegexReplaceLineEdit->setPlaceholderText("Replace"); - - QHBoxLayout *postProcessRegexLayout = new QHBoxLayout; - postProcessRegexLayout->addWidget(postProcessRegexLineEdit); - postProcessRegexLayout->addWidget(postProcessRegexIsReplaceCheckBox); - postProcessRegexLayout->addWidget(postProcessRegexReplaceLineEdit); - - formOutputParsing->addRow("Postprocess Regex", postProcessRegexLayout); auto setVisibilityOfPostProcessRegexOptions = [=]() { // Hide the replace string if the checkbox is not checked - postProcessRegexReplaceLineEdit->setVisible( - postProcessRegexIsReplaceCheckBox->isChecked()); + ui->postProcessRegexReplaceLineEdit->setVisible( + ui->postProcessRegexIsReplaceCheckBox->isChecked()); }; setVisibilityOfPostProcessRegexOptions(); // Respond to changes in the checkbox - connect(postProcessRegexIsReplaceCheckBox, &QCheckBox::toggled, this, + connect(ui->postProcessRegexIsReplaceCheckBox, &QCheckBox::toggled, this, setVisibilityOfPostProcessRegexOptions); - // Add an error message label, hidden by default, color red - QLabel *errorMessageLabel = new QLabel("Error message"); - errorMessageLabel->setStyleSheet("QLabel { color : red; }"); - errorMessageLabel->setVisible(false); - layout->addWidget(errorMessageLabel); + ui->errorMessageLabel->setVisible(false); // Lambda to save the request settings to a request_data struct auto saveSettingsToRequestData = [=](url_source_request_data *request_data_for_saving) { // Save the request settings to the request_data struct - request_data_for_saving->url = urlLineEdit->text().toStdString(); - request_data_for_saving->url_or_file = urlRadioButton->isChecked() ? "url" : "file"; - request_data_for_saving->method = methodComboBox->currentText().toStdString(); - request_data_for_saving->body = bodyLineEdit->text().toStdString(); - if (obsTextSourceComboBox->currentText() != "None") { + request_data_for_saving->url = ui->urlLineEdit->text().toStdString(); + request_data_for_saving->url_or_file = ui->urlRadioButton->isChecked() ? "url" + : "file"; + request_data_for_saving->method = ui->methodComboBox->currentText().toStdString(); + request_data_for_saving->body = ui->bodyTextEdit->toPlainText().toStdString(); + if (ui->obsTextSourceComboBox->currentText() != "None") { request_data_for_saving->obs_text_source = - obsTextSourceComboBox->currentText().toStdString(); + ui->obsTextSourceComboBox->currentText().toStdString(); } else { request_data_for_saving->obs_text_source = ""; } request_data_for_saving->obs_text_source_skip_if_empty = - obsTextSourceEnabledCheckBox->isChecked(); + ui->obsTextSourceEnabledCheckBox->isChecked(); request_data_for_saving->obs_text_source_skip_if_same = - obsTextSourceSkipSameCheckBox->isChecked(); + ui->obsTextSourceSkipSameCheckBox->isChecked(); // Save the SSL certificate file request_data_for_saving->ssl_client_cert_file = - sslCertFileLineEdit->text().toStdString(); + ui->sslCertFileLineEdit->text().toStdString(); // Save the SSL key file request_data_for_saving->ssl_client_key_file = - sslKeyFileLineEdit->text().toStdString(); + ui->sslKeyFileLineEdit->text().toStdString(); // Save the SSL key password request_data_for_saving->ssl_client_key_pass = - sslKeyPasswordLineEdit->text().toStdString(); + ui->sslKeyPasswordLineEdit->text().toStdString(); // Save the verify peer option - request_data_for_saving->ssl_verify_peer = verifyPeerCheckBox->isChecked(); - - // Save the headers - get_key_value_as_pairs_from_key_value_list_widget(headersWidget, - request_data_for_saving->headers); + request_data_for_saving->ssl_verify_peer = ui->verifyPeerCheckBox->isChecked(); + + // Save the headers from ui->tableView_headers's model + request_data_for_saving->headers.clear(); + QStandardItemModel *model = (QStandardItemModel *)ui->tableView_headers->model(); + for (int i = 0; i < model->rowCount(); i++) { + request_data_for_saving->headers.push_back( + std::make_pair(model->item(i, 0)->text().toStdString(), + model->item(i, 1)->text().toStdString())); + } // Save the output parsing options request_data_for_saving->output_type = - outputTypeComboBox->currentText().toStdString(); + ui->outputTypeComboBox->currentText().toStdString(); request_data_for_saving->output_json_pointer = - outputJSONPointerLineEdit->text().toStdString(); + ui->outputJSONPointerLineEdit->text().toStdString(); request_data_for_saving->output_json_path = - outputJSONPathLineEdit->text().toStdString(); - request_data_for_saving->output_xpath = outputXPathLineEdit->text().toStdString(); - request_data_for_saving->output_xquery = outputXQueryLineEdit->text().toStdString(); - request_data_for_saving->output_regex = outputRegexLineEdit->text().toStdString(); + ui->outputJSONPathLineEdit->text().toStdString(); + request_data_for_saving->output_xpath = + ui->outputXPathLineEdit->text().toStdString(); + request_data_for_saving->output_xquery = + ui->outputXQueryLineEdit->text().toStdString(); + request_data_for_saving->output_regex = + ui->outputRegexLineEdit->text().toStdString(); request_data_for_saving->output_regex_flags = - outputRegexFlagsLineEdit->text().toStdString(); + ui->outputRegexFlagsLineEdit->text().toStdString(); request_data_for_saving->output_regex_group = - outputRegexGroupLineEdit->text().toStdString(); + ui->outputRegexGroupLineEdit->text().toStdString(); + request_data_for_saving->output_cssselector = + ui->cssSelectorLineEdit->text().toStdString(); // Save the postprocess regex options request_data_for_saving->post_process_regex = - postProcessRegexLineEdit->text().toStdString(); + ui->postProcessRegexLineEdit->text().toStdString(); request_data_for_saving->post_process_regex_is_replace = - postProcessRegexIsReplaceCheckBox->isChecked(); + ui->postProcessRegexIsReplaceCheckBox->isChecked(); request_data_for_saving->post_process_regex_replace = - postProcessRegexReplaceLineEdit->text().toStdString(); + ui->postProcessRegexReplaceLineEdit->text().toStdString(); }; - QPushButton *sendButton = new QPushButton("Test Request"); - - connect(sendButton, &QPushButton::clicked, this, [=]() { + connect(ui->sendButton, &QPushButton::clicked, this, [=]() { // Hide the error message label - errorMessageLabel->setVisible(false); + ui->errorMessageLabel->setVisible(false); // Get an interim request_data struct with the current settings url_source_request_data *request_data_test = new url_source_request_data; @@ -514,8 +325,9 @@ RequestBuilder::RequestBuilder(url_source_request_data *request_data, if (response.status_code != URL_SOURCE_REQUEST_SUCCESS) { // Show the error message label - errorMessageLabel->setText(QString::fromStdString(response.error_message)); - errorMessageLabel->setVisible(true); + ui->errorMessageLabel->setText( + QString::fromStdString(response.error_message)); + ui->errorMessageLabel->setVisible(true); return; } @@ -562,9 +374,11 @@ RequestBuilder::RequestBuilder(url_source_request_data *request_data, responseBodyGroupBox->setLayout(new QVBoxLayout); // Add scroll area for the response body QScrollArea *responseBodyScrollArea = new QScrollArea; - QLabel *responseLabel = new QLabel(QString::fromStdString(response.body)); + QLabel *responseLabel = new QLabel(QString::fromStdString(response.body).trimmed()); // Wrap the text responseLabel->setWordWrap(true); + // dont allow rich text + responseLabel->setTextFormat(Qt::PlainText); // Set the label as the scroll area's widget responseBodyScrollArea->setWidget(responseLabel); responseBodyGroupBox->layout()->addWidget(responseBodyScrollArea); @@ -590,8 +404,11 @@ RequestBuilder::RequestBuilder(url_source_request_data *request_data, } } else { // Add a QLabel to show a single parsed output - parsedOutputLayout->addWidget(new QLabel( - QString::fromStdString(response.body_parts_parsed[0]))); + QLabel *parsedOutputLabel = new QLabel( + QString::fromStdString(response.body_parts_parsed[0])); + parsedOutputLabel->setWordWrap(true); + parsedOutputLabel->setTextFormat(Qt::PlainText); + parsedOutputLayout->addWidget(parsedOutputLabel); } } @@ -599,16 +416,11 @@ RequestBuilder::RequestBuilder(url_source_request_data *request_data, responseDialog->adjustSize(); }); - // Save button - QPushButton *saveButton = new QPushButton("Save"); - - // put send and save buttons in a horizontal layout - QHBoxLayout *saveButtonLayout = new QHBoxLayout; - saveButtonLayout->addWidget(sendButton); - saveButtonLayout->addWidget(saveButton); - layout->addLayout(saveButtonLayout); - - connect(saveButton, &QPushButton::clicked, this, [=]() { + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, [=]() { + // Close dialog + close(); + }); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, [=]() { // Save the request settings to the request_data struct saveSettingsToRequestData(request_data); diff --git a/src/ui/RequestBuilder.h b/src/ui/RequestBuilder.h index 1dfeb7f..22951e8 100644 --- a/src/ui/RequestBuilder.h +++ b/src/ui/RequestBuilder.h @@ -2,6 +2,10 @@ #include "request-data.h" +namespace Ui { +class RequestBuilder; +} + class RequestBuilder : public QDialog { Q_OBJECT public: @@ -11,4 +15,5 @@ class RequestBuilder : public QDialog { private: QVBoxLayout *layout; + Ui::RequestBuilder *ui; }; diff --git a/src/ui/requestbuilder.ui b/src/ui/requestbuilder.ui new file mode 100644 index 0000000..923f502 --- /dev/null +++ b/src/ui/requestbuilder.ui @@ -0,0 +1,721 @@ + + + RequestBuilder + + + + 0 + 0 + 511 + 855 + + + + + 0 + 0 + + + + Dialog + + + + 3 + + + + + + 0 + 0 + + + + + QLayout::SetDefaultConstraint + + + QFormLayout::ExpandingFieldsGrow + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Source + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + URL + + + + + + + File + + + + + + + + + + URL/File + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + ... + + + + + + + + + + + + + URL Request Options + + + + QFormLayout::ExpandingFieldsGrow + + + 3 + + + + + Method + + + + + + + + GET + + + + + POST + + + + + + + + Headers + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + QAbstractScrollArea::AdjustToContents + + + true + + + false + + + true + + + false + + + + + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + - + + + + + + + + + + + + + Dynamic Input + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + "Select a OBS text source to use its current text in the querystring or request body as `{{input}}`." + + + + None + + + + + + + + Skip empty + + + + + + + Skip same + + + + + + + + + + + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + false + + + + 6 + + + 6 + + + 0 + + + 6 + + + 6 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + SSL Certificate File + + + + + + + + 0 + 0 + + + + ... + + + + + + + + + + + 0 + 0 + + + + + -1 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + SSL Key File + + + + + + + ... + + + + + + + + + + SSL Key Password + + + + + + + Verify Peer? + + + + + + + + + + Body + + + + + + + + 0 + 0 + + + + QAbstractScrollArea::AdjustToContents + + + false + + + Body + + + + + + + SSL Options + + + + + + + + + + Output Parsing Options + + + + QFormLayout::ExpandingFieldsGrow + + + 3 + + + + + + 0 + 0 + + + + + Text + + + + + JSON + + + + + XML (XPath) + + + + + XML (XQuery) + + + + + HTML + + + + + + + + Content-Type + + + + + + + JSON Pointer + + + + + + + + + + + + + + JSON Path + + + + + + + + + + XPath + + + + + + + + + + XQuery + + + + + + + + + + Regex + + + + + + + + + + Regex Flags + + + + + + + + + + Regex Group + + + + + + + 0 + + + + + + + Postprocess Regex + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Replace + + + + + + + + + + + + + CSS Selector + + + + + + + + + + + + + QLabel { color : red; } + + + TextLabel + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + false + + + + + + + + 0 + 0 + + + + Test Request + + + + + + + + + + +