mirror of
https://github.com/QRouland/UTPass.git
synced 2025-07-04 03:02:28 +00:00
Fix build rnp for arm64
This commit is contained in:
@ -6,10 +6,12 @@ set(
|
||||
plugin.cpp
|
||||
pass.cpp
|
||||
passkeymodel.h
|
||||
passphraseprovider2.h
|
||||
jobs/rmjob.cpp
|
||||
jobs/rnpjob.cpp
|
||||
jobs/getkeysjob.cpp
|
||||
jobs/importkeyjob.cpp
|
||||
jobs/decryptjob.cpp
|
||||
)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
@ -29,37 +31,23 @@ set_target_properties(${PLUGIN} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PLUGIN})
|
||||
qt5_use_modules(${PLUGIN} Qml Quick DBus)
|
||||
|
||||
|
||||
|
||||
set(RNP_BUILD_DIR "${CMAKE_SOURCE_DIR}/build/${ARCH_TRIPLET}/rnp/install")
|
||||
set(RNP_INSTALL_DIR "${CMAKE_SOURCE_DIR}/build/${ARCH_TRIPLET}/rnp/install")
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
find_package(JSON-C 0.11)
|
||||
|
||||
INCLUDE_DIRECTORIES(${RNP_BUILD_DIR}/include)
|
||||
INCLUDE_DIRECTORIES(${RNP_INSTALL_DIR}/include)
|
||||
|
||||
add_library(rnp STATIC IMPORTED)
|
||||
set_property(TARGET rnp PROPERTY IMPORTED_LOCATION "${RNP_BUILD_DIR}/lib/librnp.a")
|
||||
# add_library(rnp STATIC IMPORTED)
|
||||
# set_property(TARGET rnp PROPERTY IMPORTED_LOCATION "${RNP_BUILD_DIR}/lib/librnp.a")
|
||||
|
||||
add_library(sexpp STATIC IMPORTED)
|
||||
set_property(TARGET sexpp PROPERTY IMPORTED_LOCATION "${RNP_BUILD_DIR}/lib/libsexpp.a")
|
||||
# add_library(sexpp STATIC IMPORTED)
|
||||
# set_property(TARGET sexpp PROPERTY IMPORTED_LOCATION "${RNP_BUILD_DIR}/lib/libsexpp.a")
|
||||
|
||||
add_library(gpgerror SHARED IMPORTED)
|
||||
set_property(TARGET gpgerror PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libgpg-error.so.0.28.0")
|
||||
add_library(rnp SHARED IMPORTED)
|
||||
set_property(TARGET rnp PROPERTY IMPORTED_LOCATION "${RNP_INSTALL_DIR}/lib/librnp.so")
|
||||
|
||||
add_library(libassuan SHARED IMPORTED)
|
||||
set_property(TARGET libassuan PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libassuan.so")
|
||||
|
||||
add_library(libgpgme SHARED IMPORTED)
|
||||
set_property(TARGET libgpgme PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libgpgme.so")
|
||||
|
||||
add_library(libgpgmepp SHARED IMPORTED)
|
||||
set_property(TARGET libgpgmepp PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libgpgmepp.so")
|
||||
|
||||
add_library(libqgpgme SHARED IMPORTED)
|
||||
set_property(TARGET libqgpgme PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libqgpgme.so")
|
||||
|
||||
|
||||
target_link_libraries(${PLUGIN} rnp sexpp gpgerror libassuan libgpgme libgpgmepp libqgpgme OpenSSL::Crypto JSON-C::JSON-C)
|
||||
target_link_libraries(${PLUGIN} rnp OpenSSL::Crypto JSON-C::JSON-C)
|
||||
|
||||
set(QT_IMPORTS_DIR "/lib/${ARCH_TRIPLET}")
|
||||
install(TARGETS ${PLUGIN} DESTINATION ${QT_IMPORTS_DIR}/${PLUGIN}/)
|
||||
|
@ -1,7 +1,13 @@
|
||||
#include "decryptjob.h"
|
||||
#include "qdebug.h"
|
||||
extern "C" {
|
||||
#include <rnp/rnp.h>
|
||||
#include <rnp/rnp_err.h>
|
||||
}
|
||||
|
||||
DecryptJob::DecryptJob(QString path, QString keyfile):
|
||||
m_path(path)
|
||||
DecryptJob::DecryptJob(QDir rnp_homedir, QString path):
|
||||
RnpJob(rnp_homedir),
|
||||
m_encrypted_file_path(path)
|
||||
{
|
||||
this->setObjectName("DecryptJob");
|
||||
}
|
||||
@ -9,7 +15,33 @@ DecryptJob::DecryptJob(QString path, QString keyfile):
|
||||
|
||||
void DecryptJob::run()
|
||||
{
|
||||
this->load_sec_keyring();
|
||||
rnp_input_from_path(&keyfile, "secring.pgp"));
|
||||
qFatal("To be implemented !")
|
||||
qDebug() << "[DecryptJob] Starting";
|
||||
this->load_sec_keyring(NULL);
|
||||
|
||||
rnp_input_t input = NULL;
|
||||
rnp_output_t output = NULL;
|
||||
uint8_t * buf = NULL;
|
||||
size_t buf_len = 0;
|
||||
|
||||
auto ret = rnp_input_from_path(&input, this->m_encrypted_file_path.toLocal8Bit().data());
|
||||
if (ret == RNP_SUCCESS) {
|
||||
ret = rnp_output_to_memory(&output, 0);
|
||||
}
|
||||
if (ret == RNP_SUCCESS) {
|
||||
ret = rnp_decrypt(this->m_ffi, input, output);
|
||||
}
|
||||
if (ret == RNP_SUCCESS) {
|
||||
ret = rnp_output_memory_get_buf(output, &buf, &buf_len, false);
|
||||
}
|
||||
if (ret == RNP_SUCCESS) {
|
||||
emit resultSuccess(this->m_encrypted_file_path, QString::fromUtf8((char*)buf));
|
||||
}
|
||||
|
||||
rnp_input_destroy(input);
|
||||
rnp_output_destroy(output);
|
||||
|
||||
terminateOnError(ret);
|
||||
|
||||
emit resultSuccess(this->m_encrypted_file_path, QString::fromUtf8((char*)buf));
|
||||
qDebug() << "[DecryptJob] Finished Successfully ";
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ signals:
|
||||
* @param encrypted_file_path The path to the encrypted file that was decrypted.
|
||||
* @param clear_txt The decrypted content in clear-text. If an error occurs, this may be empty.
|
||||
*/
|
||||
void resultReady(QString encrypted_file_path, QString clear_txt);
|
||||
void resultSuccess(QString encrypted_file_path, QString clear_txt);
|
||||
|
||||
private:
|
||||
QString m_encrypted_file_path; /**< The path to the encrypted file that is to be decrypted. */
|
||||
@ -46,9 +46,10 @@ public:
|
||||
* This constructor initializes the DecryptJob with the encrypted file path. The decryption
|
||||
* operation will be executed in a background thread when the job is started.
|
||||
*
|
||||
* @param rnp_homedir The directory containing the keyrings.
|
||||
* @param path The path to the encrypted file that needs to be decrypted.
|
||||
*/
|
||||
DecryptJob(QString path);
|
||||
DecryptJob(QDir rnp_homedir, QString path);
|
||||
};
|
||||
|
||||
#endif // DECRYPTJOB_H
|
||||
|
@ -32,13 +32,14 @@ void GetKeysJob::run()
|
||||
QSet<QString> fingerprints = QSet<QString>();
|
||||
this->load_full_keyring(&fingerprints);
|
||||
|
||||
//Get infos keys
|
||||
auto key_infos = QList<QJsonDocument>();
|
||||
QList<QJsonDocument>::iterator i;
|
||||
for (auto i = fingerprints.begin(), end = fingerprints.end(); i != end; ++i) {
|
||||
key_infos.append(this->fingerprint_map_key_info(*i));
|
||||
}
|
||||
|
||||
//Get all infos keys
|
||||
// Emit result
|
||||
emit resultSuccess(key_infos);
|
||||
qDebug() << "[GetKeysJob] Finished Successfully ";
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ bool RnpJob::passProvider(rnp_ffi_t ffi,
|
||||
|
||||
void RnpJob::load_key_file(QSet<QString> *result_fingerprints, const QString path, const uint32_t flags)
|
||||
{
|
||||
qDebug() << "[RnpJob] load keyring at" << path;
|
||||
qDebug() << "[RnpJob] Load keyring at" << path;
|
||||
rnp_input_t input = NULL;
|
||||
if (QFileInfo::exists(this->pubringPath())) {
|
||||
auto ret = rnp_input_from_path(&input, path.toLocal8Bit().constData());
|
||||
@ -75,9 +75,9 @@ void RnpJob::load_key_file(QSet<QString> *result_fingerprints, const QString pat
|
||||
rnp_input_destroy(input);
|
||||
rnp_buffer_destroy(json);
|
||||
terminateOnError(ret);
|
||||
qDebug() << "[RnpJob] keyring loaded successfully";
|
||||
qDebug() << "[RnpJob] Keyring loaded successfully";
|
||||
} else {
|
||||
qDebug() << "[RnpJob] No keyring" << path << "not found";
|
||||
qDebug() << "[RnpJob] Keyring" << path << "not found";
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,18 +85,15 @@ void RnpJob::load_key_file(QSet<QString> *result_fingerprints, const QString pat
|
||||
void RnpJob::load_pub_keyring(QSet<QString> *result_fingerprints = NULL)
|
||||
{
|
||||
this->load_key_file(result_fingerprints, this->pubringPath(), RNP_LOAD_SAVE_PUBLIC_KEYS);
|
||||
qDebug() << "[RnpJob] pub fingerprints" << *result_fingerprints;
|
||||
}
|
||||
|
||||
void RnpJob::load_sec_keyring(QSet<QString> *result_fingerprints = NULL)
|
||||
{
|
||||
this->load_key_file(result_fingerprints, this->secringPath(), RNP_LOAD_SAVE_SECRET_KEYS);
|
||||
qDebug() << "[RnpJob] sec fingerprints" << *result_fingerprints;
|
||||
}
|
||||
|
||||
void RnpJob::load_full_keyring(QSet<QString> *result_fingerprints = NULL)
|
||||
{
|
||||
this->load_pub_keyring(result_fingerprints);
|
||||
this->load_sec_keyring(result_fingerprints);
|
||||
qDebug() << "[RnpJob] full fingerprints" << *result_fingerprints;
|
||||
}
|
||||
|
@ -153,6 +153,11 @@ public:
|
||||
* the RNP FFI handle.
|
||||
*/
|
||||
~RnpJob();
|
||||
|
||||
|
||||
void setPassProvider(rnp_password_cb pass_provider_cb) {
|
||||
rnp_ffi_set_pass_provider(this->m_ffi, pass_provider_cb, NULL);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // RNPJOB_H
|
||||
|
@ -2,9 +2,12 @@
|
||||
#include <QtCore/QStandardPaths>
|
||||
#include <QtCore/QDir>
|
||||
|
||||
#include "jobs/decryptjob.h"
|
||||
#include "jobs/getkeysjob.h"
|
||||
#include "jobs/importkeyjob.h"
|
||||
#include "pass.h"
|
||||
#include "passphraseprovider2.h"
|
||||
//#include "passphraseprovider2.h"
|
||||
|
||||
|
||||
|
||||
@ -13,18 +16,19 @@ Pass::Pass():
|
||||
QStandardPaths::AppDataLocation).append("/.password-store")),
|
||||
m_gpg_home (QStandardPaths::writableLocation(
|
||||
QStandardPaths::AppDataLocation).append("/.rnp")),
|
||||
m_passphrase_provider(&UTPassphraseProvider::get_pass_provider),
|
||||
m_sem(std::unique_ptr<QSemaphore>(new QSemaphore(1)))
|
||||
{
|
||||
|
||||
}
|
||||
{}
|
||||
|
||||
void Pass::initialize(QObject *window)
|
||||
{
|
||||
if (!window) {
|
||||
qWarning("[Pass] Window should be null only for testing");
|
||||
}
|
||||
this->initGpgHome();
|
||||
this->initPasswordStore();
|
||||
if (!window) {
|
||||
qWarning("[Pass] Window should be null only for testing");
|
||||
} else {
|
||||
UTPassphraseProvider::instance().setWindow(window);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -53,36 +57,37 @@ void Pass::initPasswordStore()
|
||||
qInfo() << "[Pass] Password Store is :" << m_password_store;
|
||||
}
|
||||
|
||||
// bool Pass::show(QUrl url)
|
||||
// {
|
||||
// if (!this->m_sem->tryAcquire(1, 500)) {
|
||||
// return false;
|
||||
// }
|
||||
// auto path = url.toLocalFile();
|
||||
// qInfo() << "Pass show " << path;
|
||||
// QFileInfo file_info(path);
|
||||
// this->m_show_filename = file_info.completeBaseName();
|
||||
// return this->m_gpg->decryptFromFile(path);
|
||||
// }
|
||||
bool Pass::show(QUrl url)
|
||||
{
|
||||
if (!this->m_sem->tryAcquire(1, 500)) {
|
||||
qInfo() << "[Pass] A command is already running";
|
||||
return false;
|
||||
}
|
||||
auto job = new DecryptJob(this->m_gpg_home, url.toLocalFile());
|
||||
job->setPassProvider(this->m_passphrase_provider);
|
||||
QObject::connect(job, &DecryptJob::resultError, this, &Pass::slotShowError);
|
||||
QObject::connect(job, &DecryptJob::resultSuccess, this, &Pass::slotShowSucceed);
|
||||
connect(job, &DecryptJob::finished, job, &QObject::deleteLater);
|
||||
job->start();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// void Pass::showResult(Error err, QString plain_text)
|
||||
// {
|
||||
// qDebug() << "Pass show Result";
|
||||
// if (err) {
|
||||
// qInfo() << "Pass show Failed";
|
||||
// emit showFailed(err.asString());
|
||||
void Pass::slotShowError(rnp_result_t err)
|
||||
{
|
||||
qInfo() << "[Pass] Show Failed";
|
||||
emit showFailed(rnp_result_to_string(err));
|
||||
this->m_sem->release(1);
|
||||
}
|
||||
|
||||
// } else if (err.isCanceled()) {
|
||||
// qInfo() << "Pass show Cancelled";
|
||||
// emit showCancelled();
|
||||
// } else {
|
||||
// qInfo() << "Pass show Succeed";
|
||||
// emit showSucceed(this->m_show_filename, plain_text);
|
||||
// }
|
||||
// this->m_show_filename = QString();
|
||||
// this->m_sem->release(1);
|
||||
// }
|
||||
|
||||
void Pass::slotShowSucceed(QString encrypted_file_path, QString plain_text)
|
||||
{
|
||||
qDebug() << "[Pass] Show Succeed";
|
||||
QFileInfo file_info(encrypted_file_path);
|
||||
emit showSucceed(file_info.completeBaseName(), plain_text);
|
||||
this->m_sem->release(1);
|
||||
}
|
||||
|
||||
// bool Pass::deletePasswordStore()
|
||||
// {
|
||||
|
@ -6,14 +6,10 @@
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
#include <QVariant>
|
||||
#include <gpgme++/context.h>
|
||||
#include <QSemaphore>
|
||||
extern "C" {
|
||||
#include <rnp/rnp.h>
|
||||
}
|
||||
|
||||
using namespace GpgME;
|
||||
|
||||
/**
|
||||
* @class Pass
|
||||
* @brief A class for managing password storage using GPG encryption.
|
||||
@ -33,13 +29,15 @@ private slots:
|
||||
* @param err The error that occurred during the operation.
|
||||
* @param plain_text The decrypted plain text (password).
|
||||
*/
|
||||
void showResult(Error err, QString plain_text);
|
||||
void slotShowError(rnp_result_t err);
|
||||
|
||||
void slotShowSucceed(QString encrypted_file_path, QString plain_text);
|
||||
|
||||
/**
|
||||
* @brief Slot to handle the result of a GPG key deletion operation.
|
||||
* @param err The error that occurred during the operation.
|
||||
*/
|
||||
void deleteGPGKeyResult(Error err);
|
||||
// void deleteGPGKeyResult(Error err);
|
||||
|
||||
/**
|
||||
* @brief Slot to handle the error result of a GPG key import operation.
|
||||
@ -148,7 +146,7 @@ private:
|
||||
QString m_gpg_home; /**< The path to the gpg home. */
|
||||
std::unique_ptr<PassKeyringModel>
|
||||
m_keyring_model; /**< Meta data on the keyring uid, name, secrecy ... of the availble keys. */
|
||||
PassphraseProvider *m_passphrase_provider; /**< Pointer on passphrase povider for operations using secret keys. */
|
||||
rnp_password_cb m_passphrase_provider; /**< Pointer on passphrase povider for operations using secret keys. */
|
||||
std::unique_ptr<QSemaphore> m_sem; /**< Semaphore for managing concurrent operations. */
|
||||
|
||||
|
||||
@ -206,14 +204,8 @@ public:
|
||||
this->m_gpg_home = gpg_home;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Sets the window passphrase provider used for GPG authentication.
|
||||
*
|
||||
* PassphraseProvider will be deleted with destructor.
|
||||
*
|
||||
* @param The window used by passphrase provider.
|
||||
*/
|
||||
void set_passphrase_provider(PassphraseProvider* passphrase_provider)
|
||||
|
||||
void set_passphrase_provider(rnp_password_cb passphrase_provider)
|
||||
{
|
||||
this->m_passphrase_provider = passphrase_provider;
|
||||
}
|
||||
|
@ -11,12 +11,6 @@
|
||||
* @class PassKeyModel
|
||||
* @brief A model representing a GPG (GNU Privacy Guard) key.
|
||||
*
|
||||
* This class encapsulates the properties of a GPG key, such as its key ID, associated
|
||||
* user IDs, secret key status, and expiration status. It is used within an application
|
||||
* to manage and represent GPG keys, providing easy access to key data and related user information.
|
||||
*
|
||||
* This class supports properties such as the key's fingerprint, key ID, user IDs, and whether
|
||||
* the key has a secret key associated with it.
|
||||
*/
|
||||
class PassKeyModel : public QObject
|
||||
{
|
||||
|
132
plugins/Pass/passphraseprovider2.h
Normal file
132
plugins/Pass/passphraseprovider2.h
Normal file
@ -0,0 +1,132 @@
|
||||
#ifndef UTPASSPHRASEPROVIDER2_H
|
||||
#define UTPASSPHRASEPROVIDER2_H
|
||||
|
||||
#include <QDebug>
|
||||
#include <memory>
|
||||
#include <stdio.h>
|
||||
#include <QObject>
|
||||
#include <QQmlProperty>
|
||||
#include <QEventLoop>
|
||||
#include <QSemaphore>
|
||||
extern "C" {
|
||||
#include <rnp/rnp.h>
|
||||
}
|
||||
|
||||
/**
|
||||
* @class UTPassphraseProvider
|
||||
* @brief A passphrase provider for GPG operations that interacts with a QML dialog.
|
||||
*/
|
||||
|
||||
class UTPassphraseProvider : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Slot to handle the user's response from the passphrase dialog.
|
||||
*
|
||||
* This method processes the response from the passphrase dialog. If the user provides a passphrase,
|
||||
* it is stored; if the operation is canceled, a flag is set.
|
||||
*
|
||||
* @param canceled Whether the user canceled the passphrase entry.
|
||||
* @param passphrase The passphrase entered by the user.
|
||||
*/
|
||||
void handleResponse(bool canceled, QString passphrase)
|
||||
{
|
||||
qDebug() << "call handleResponse";
|
||||
if (!canceled)
|
||||
this->m_passphrase = passphrase;
|
||||
else
|
||||
m_canceled = true;
|
||||
emit unlockEventLoop();
|
||||
};
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief Signal to unlock the event loop.
|
||||
*
|
||||
* This signal is emitted when the passphrase has been entered or the operation has been canceled,
|
||||
* unlocking the event loop waiting for the response.
|
||||
*/
|
||||
void unlockEventLoop();
|
||||
|
||||
|
||||
private:
|
||||
explicit UTPassphraseProvider(QObject * parent = nullptr)
|
||||
: m_sem(std::make_unique<QSemaphore>(1)),
|
||||
m_passphrase(QString::Null()),
|
||||
m_canceled(false)
|
||||
{}
|
||||
QObject *m_window; /**< The window object that triggers the QML dialog. */
|
||||
std::unique_ptr<QSemaphore> m_sem; /**< Semaphore for managing access. */
|
||||
QString m_passphrase; /**< The passphrase provided by the user. */
|
||||
bool m_canceled; /**< Flag indicating whether the passphrase operation was canceled. */
|
||||
|
||||
public:
|
||||
~UTPassphraseProvider() = default;
|
||||
|
||||
static UTPassphraseProvider& instance()
|
||||
{
|
||||
static UTPassphraseProvider instance;
|
||||
return instance;
|
||||
}
|
||||
UTPassphraseProvider(UTPassphraseProvider const &) = delete;
|
||||
void operator=(UTPassphraseProvider const &) = delete;
|
||||
|
||||
static bool
|
||||
get_pass_provider(rnp_ffi_t ffi,
|
||||
void * app_ctx,
|
||||
rnp_key_handle_t key,
|
||||
const char * pgp_context,
|
||||
char buf[],
|
||||
size_t buf_len)
|
||||
{
|
||||
qDebug() << "Call the getPassphrase";
|
||||
|
||||
if (!UTPassphraseProvider::instance().m_sem->tryAcquire(1, 500))
|
||||
{
|
||||
qWarning() << "Cannot acquire UTPassphraseProvider semaphore.";
|
||||
UTPassphraseProvider::instance().m_canceled = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
UTPassphraseProvider::instance().m_passphrase = nullptr;
|
||||
UTPassphraseProvider::instance().m_canceled = false;
|
||||
|
||||
qDebug() << "Call the QML Dialog Passphrase Provider";
|
||||
QMetaObject::invokeMethod(
|
||||
UTPassphraseProvider::instance().m_window, "callPassphraseDialog",
|
||||
Q_ARG(QVariant, "useridHint"), // TODO
|
||||
Q_ARG(QVariant, "description"), // TODO
|
||||
Q_ARG(QVariant, "previousWasBad") // TODO
|
||||
);
|
||||
|
||||
qDebug() << "Waiting for response";
|
||||
|
||||
QEventLoop loop;
|
||||
QObject::connect(&UTPassphraseProvider::instance(), &UTPassphraseProvider::unlockEventLoop, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
qDebug() << "Prepare Returns";
|
||||
auto ret = false;
|
||||
if(!UTPassphraseProvider::instance().m_canceled) {
|
||||
strncpy(buf, UTPassphraseProvider::instance().m_passphrase.toLocal8Bit().data(), buf_len);
|
||||
ret = true;
|
||||
};
|
||||
|
||||
qDebug() << "Clean";
|
||||
if (UTPassphraseProvider::instance().m_passphrase.isNull())
|
||||
{
|
||||
UTPassphraseProvider::instance().m_passphrase = QString::Null();
|
||||
}
|
||||
UTPassphraseProvider::instance().m_canceled = false;
|
||||
UTPassphraseProvider::instance().m_sem->release(1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void setWindow(QObject* window){
|
||||
this->m_window = window;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
Reference in New Issue
Block a user