diff --git a/ui/qt/main_status_bar.cpp b/ui/qt/main_status_bar.cpp index a0f4d3a462..0933ce44b5 100644 --- a/ui/qt/main_status_bar.cpp +++ b/ui/qt/main_status_bar.cpp @@ -604,6 +604,17 @@ void MainStatusBar::showProfileMenu(const QPoint &global_pos, Qt::MouseButton bu action->setProperty("dialog_action_", (int)ProfileDialog::ImportDirProfile); connect(action, SIGNAL(triggered()), this, SLOT(manageProfile())); ctx_menu_.addMenu(importMenu); + + QMenu * exportMenu = new QMenu(tr("Export")); + action = exportMenu->addAction(tr(UTF8_HORIZONTAL_ELLIPSIS" selected entry")); + action->setProperty("dialog_action_", (int)ProfileDialog::ExportSingleProfile); + action->setEnabled(enable_edit); + connect(action, SIGNAL(triggered()), this, SLOT(manageProfile())); + action = exportMenu->addAction(tr(UTF8_HORIZONTAL_ELLIPSIS" all user profiles")); + action->setProperty("dialog_action_", (int)ProfileDialog::ExportAllProfiles); + connect(action, SIGNAL(triggered()), this, SLOT(manageProfile())); + ctx_menu_.addMenu(exportMenu); + #else action = ctx_menu_.addAction(tr("Import" UTF8_HORIZONTAL_ELLIPSIS)); action->setProperty("dialog_action_", (int)ProfileDialog::ImportDirProfile); diff --git a/ui/qt/models/profile_model.cpp b/ui/qt/models/profile_model.cpp index 98bb90f953..750efb00e5 100644 --- a/ui/qt/models/profile_model.cpp +++ b/ui/qt/models/profile_model.cpp @@ -700,6 +700,54 @@ QFileInfoList ProfileModel::filterProfilePath(QString path, QFileInfoList ent, b } #ifdef HAVE_MINIZIP +QStringList ProfileModel::exportFileList(QModelIndexList items) +{ + QStringList result; + + foreach(QModelIndex idx, items) + { + profile_def * prof = guard(idx.row()); + if ( prof->is_global || QString(prof->name).compare(DEFAULT_PROFILE) == 0 ) + continue; + + if ( ! idx.data(ProfileModel::DATA_PATH_IS_NOT_DESCRIPTION).toBool() ) + continue; + + QString path = idx.data(ProfileModel::DATA_PATH).toString(); + QDir temp(path); + temp.setSorting(QDir::Name); + temp.setFilter(QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot); + QFileInfoList entries = temp.entryInfoList(); + foreach ( QFileInfo fi, entries ) + result << fi.absoluteFilePath(); + } + + return result; +} + +bool ProfileModel::exportProfiles(QString filename, QModelIndexList items, QString *err) +{ + if ( changesPending() ) + { + if ( err ) + err->append(tr("Exporting profiles while changes are pending is not allowed")); + return false; + } + + QStringList files = exportFileList(items); + if ( files.count() == 0 ) + { + if ( err ) + err->append((tr("No profiles found to export"))); + return false; + } + + if ( WireSharkZipHelper::zip(filename, files, QString(get_profiles_dir()) + QDir::separator() ) ) + return true; + + return false; +} + /* This check runs BEFORE the file has been unzipped! */ bool ProfileModel::acceptFile(QString fileName, int fileSize) { diff --git a/ui/qt/models/profile_model.h b/ui/qt/models/profile_model.h index 1e7246a02b..a095211292 100644 --- a/ui/qt/models/profile_model.h +++ b/ui/qt/models/profile_model.h @@ -93,6 +93,8 @@ public: bool changesPending() const; #ifdef HAVE_MINIZIP + QStringList exportFileList(QModelIndexList items); + bool exportProfiles(QString filename, QModelIndexList items, QString * err = Q_NULLPTR); int importProfilesFromZip(QString filename, int *skippedCnt = Q_NULLPTR); #endif int importProfilesFromDir(QString filename, int *skippedCnt = Q_NULLPTR, bool fromZip = false); diff --git a/ui/qt/profile_dialog.cpp b/ui/qt/profile_dialog.cpp index fb1926094a..84baace72b 100644 --- a/ui/qt/profile_dialog.cpp +++ b/ui/qt/profile_dialog.cpp @@ -41,6 +41,11 @@ #include #include #include +#include + +#define PROFILE_EXPORT_PROPERTY "export" +#define PROFILE_EXPORT_ALL "all" +#define PROFILE_EXPORT_SELECTED "selected" ProfileDialog::ProfileDialog(QWidget *parent) : GeometryStateDialog(parent), @@ -69,15 +74,26 @@ ProfileDialog::ProfileDialog(QWidget *parent) : pd_ui_->lblInfo->setAttribute(Qt::WA_MacSmallSize, true); #endif - import_button_ = pd_ui_->buttonBox->addButton(tr("Import"), QDialogButtonBox::ActionRole); + import_button_ = pd_ui_->buttonBox->addButton(tr("Import", "noun"), QDialogButtonBox::ActionRole); #ifdef HAVE_MINIZIP + export_button_ = pd_ui_->buttonBox->addButton(tr("Export", "noun"), QDialogButtonBox::ActionRole); + QMenu * importMenu = new QMenu(import_button_); QAction * entry = importMenu->addAction(tr(UTF8_HORIZONTAL_ELLIPSIS " from Zip")); connect( entry, &QAction::triggered, this, &ProfileDialog::importFromZip); entry = importMenu->addAction(tr(UTF8_HORIZONTAL_ELLIPSIS " from Directory")); connect( entry, &QAction::triggered, this, &ProfileDialog::importFromDirectory); import_button_->setMenu(importMenu); + + QMenu * exportMenu = new QMenu(export_button_); + export_selected_entry_ = exportMenu->addAction(tr(UTF8_HORIZONTAL_ELLIPSIS " selected entry")); + export_selected_entry_->setProperty(PROFILE_EXPORT_PROPERTY, PROFILE_EXPORT_SELECTED); + connect( export_selected_entry_, &QAction::triggered, this, &ProfileDialog::exportProfiles); + entry = exportMenu->addAction(tr(UTF8_HORIZONTAL_ELLIPSIS " all user profiles")); + entry->setProperty(PROFILE_EXPORT_PROPERTY, PROFILE_EXPORT_ALL); + connect( entry, &QAction::triggered, this, &ProfileDialog::exportProfiles); + export_button_->setMenu(exportMenu); #else connect( import_button_, &QPushButton::clicked, this, &ProfileDialog::importFromDirectory); #endif @@ -152,6 +168,16 @@ int ProfileDialog::execAction(ProfileDialog::ProfileAction profile_action) case ImportDirProfile: importFromDirectory(); break; + case ExportSingleProfile: +#ifdef HAVE_MINIZIP + exportProfiles(); +#endif + break; + case ExportAllProfiles: +#ifdef HAVE_MINIZIP + exportProfiles(true); +#endif + break; case EditCurrentProfile: item = pd_ui_->profileTreeView->currentIndex(); if (item.isValid()) { @@ -378,6 +404,42 @@ void ProfileDialog::filterChanged(const QString &text) } #ifdef HAVE_MINIZIP +void ProfileDialog::exportProfiles(bool exportAll) +{ + QAction * action = qobject_cast(sender()); + if ( action && action->property(PROFILE_EXPORT_PROPERTY).isValid() ) + exportAll = action->property(PROFILE_EXPORT_PROPERTY).toString().compare(PROFILE_EXPORT_ALL) == 0; + + QModelIndexList items; + + if ( ! exportAll && pd_ui_->profileTreeView->currentIndex().isValid() ) + items << sort_model_->mapToSource(pd_ui_->profileTreeView->currentIndex()); + else if ( exportAll ) + { + for ( int cnt = 0; cnt < sort_model_->rowCount(); cnt++ ) + { + QModelIndex idx = sort_model_->index(cnt, ProfileModel::COL_NAME); + if ( ! idx.data(ProfileModel::DATA_IS_GLOBAL).toBool() && ! idx.data(ProfileModel::DATA_IS_DEFAULT).toBool() ) + { + items << sort_model_->mapToSource(idx); + } + } + } + if ( items.count() == 0 ) + { + QMessageBox::warning(this, tr("Exporting profiles"), tr("No profiles found for export")); + return; + } + + QString zipFile = QFileDialog::getSaveFileName(this, tr("Select zip file for export"), QString(), tr("Zip File (*.zip)")); + + QString err; + if ( model_->exportProfiles(zipFile, items, &err) ) + QMessageBox::information(this, tr("Exporting profiles"), tr("%Ln profile(s) have been exported", "", items.count())); + else + QMessageBox::warning(this, tr("Exporting profiles"), QString("%1\n\n%2: %3").arg(tr("An error has occured while exporting profiles")).arg("Error").arg(err)); +} + void ProfileDialog::importFromZip() { QString zipFile = QFileDialog::getOpenFileName(this, tr("Select zip file for import"), QString(), tr("Zip File (*.zip)")); diff --git a/ui/qt/profile_dialog.h b/ui/qt/profile_dialog.h index 439f36ed6d..a1afcbb02b 100644 --- a/ui/qt/profile_dialog.h +++ b/ui/qt/profile_dialog.h @@ -28,7 +28,10 @@ class ProfileDialog : public GeometryStateDialog Q_OBJECT public: - enum ProfileAction { ShowProfiles, NewProfile, ImportZipProfile, ImportDirProfile, EditCurrentProfile, DeleteCurrentProfile }; + enum ProfileAction { + ShowProfiles, NewProfile, ImportZipProfile, ImportDirProfile, + ExportSingleProfile, ExportAllProfiles, EditCurrentProfile, DeleteCurrentProfile + }; explicit ProfileDialog(QWidget *parent = Q_NULLPTR); ~ProfileDialog(); @@ -51,6 +54,10 @@ private: Ui::ProfileDialog *pd_ui_; QPushButton *ok_button_; QPushButton *import_button_; +#ifdef HAVE_MINIZIP + QPushButton *export_button_; + QAction *export_selected_entry_; +#endif ProfileModel *model_; ProfileSortModel *sort_model_; @@ -60,6 +67,7 @@ private: private slots: void currentItemChanged(); #ifdef HAVE_MINIZIP + void exportProfiles(bool exportAll = false); void importFromZip(); #endif void importFromDirectory(); diff --git a/ui/qt/utils/wireshark_zip_helper.cpp b/ui/qt/utils/wireshark_zip_helper.cpp index 78dc9949fe..033da04da6 100644 --- a/ui/qt/utils/wireshark_zip_helper.cpp +++ b/ui/qt/utils/wireshark_zip_helper.cpp @@ -19,13 +19,16 @@ #include #include #include +#include #include "epan/prefs.h" #include "wsutil/file_util.h" #include #include +#include #include +#include bool WireSharkZipHelper::unzip(QString zipFile, QString directory, bool (*fileCheck)(QString, int)) { @@ -75,12 +78,8 @@ bool WireSharkZipHelper::unzip(QString zipFile, QString directory, bool (*fileCh QFile file(filePath); if ( file.open(QIODevice::WriteOnly) ) { - QDataStream out(&file); while ( ( err = unzReadCurrentFile(uf, buf, IO_BUF_SIZE) ) != UNZ_EOF ) - { - QByteArray buffer(buf, err); - out << buffer; - } + file.write(buf, err); file.close(); } @@ -107,6 +106,134 @@ bool WireSharkZipHelper::unzip(QString zipFile, QString directory, bool (*fileCh return files > 0 ? true : false; } +#ifndef UINT32_MAX +#define UINT32_MAX (0xffffffff) +#endif + +/* The following methods are being taken from https://github.com/nmoinvaz/minizip/blob/1.2/minishared.c */ +int invalid_date(const struct tm *ptm) +{ +#define datevalue_in_range(min, max, value) ((min) <= (value) && (value) <= (max)) + return (!datevalue_in_range(0, 207, ptm->tm_year) || + !datevalue_in_range(0, 11, ptm->tm_mon) || + !datevalue_in_range(1, 31, ptm->tm_mday) || + !datevalue_in_range(0, 23, ptm->tm_hour) || + !datevalue_in_range(0, 59, ptm->tm_min) || + !datevalue_in_range(0, 59, ptm->tm_sec)); +#undef datevalue_in_range +} + +uint32_t tm_to_dosdate(const struct tm *ptm) +{ + struct tm fixed_tm; + + /* Years supported: + * [00, 79] (assumed to be between 2000 and 2079) + * [80, 207] (assumed to be between 1980 and 2107, typical output of old + software that does 'year-1900' to get a double digit year) + * [1980, 2107] (due to the date format limitations, only years between 1980 and 2107 can be stored.) + */ + + memcpy(&fixed_tm, ptm, sizeof(struct tm)); + if (fixed_tm.tm_year >= 1980) /* range [1980, 2107] */ + fixed_tm.tm_year -= 1980; + else if (fixed_tm.tm_year >= 80) /* range [80, 99] */ + fixed_tm.tm_year -= 80; + else /* range [00, 79] */ + fixed_tm.tm_year += 20; + + if (invalid_date(ptm)) + return 0; + + return (uint32_t)(((fixed_tm.tm_mday) + (32 * (fixed_tm.tm_mon + 1)) + (512 * fixed_tm.tm_year)) << 16) | + ((fixed_tm.tm_sec / 2) + (32 * fixed_tm.tm_min) + (2048 * (uint32_t)fixed_tm.tm_hour)); +} + +unsigned long qDateToDosDate(QDateTime time) +{ + time_t rawtime = time.toTime_t(); + struct tm * timeinfo; + + timeinfo = localtime(&rawtime); + timeinfo->tm_year = time.date().year() - 1900; + timeinfo->tm_mon = time.date().month() - 1; + timeinfo->tm_mday = time.date().day(); + + mktime(timeinfo); + + return tm_to_dosdate(timeinfo); +} + +void WireSharkZipHelper::addFileToZip(zipFile zf, QString filepath, QString fileInZip) +{ + QFileInfo fi(filepath); + zip_fileinfo zi; + int err = ZIP_OK; + + memset(&zi, 0, sizeof(zi)); + + QDateTime fTime = fi.lastModified(); + zi.dosDate = qDateToDosDate(fTime); + + QFile fh(filepath); + /* Checks if a large file block has to be written */ + bool isLarge = ( fh.size() > UINT32_MAX ); + + err = zipOpenNewFileInZip3_64(zf, fileInZip.toUtf8().constData(), &zi, + Q_NULLPTR, 0, Q_NULLPTR, 0, Q_NULLPTR, Z_DEFLATED, 9 , 0, + -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, + Q_NULLPTR, 0, static_cast(isLarge)); + + if ( err != ZIP_OK ) + return; + + if ( fh.open(QIODevice::ReadOnly) ) + { + char * buf = static_cast(malloc(IO_BUF_SIZE)); + while ( ! fh.atEnd() && err == ZIP_OK ) + { + qint64 bytesIn = fh.read(buf, IO_BUF_SIZE); + if ( bytesIn > 0 && bytesIn <= IO_BUF_SIZE) + { + err = zipWriteInFileInZip(zf, buf, (unsigned int) bytesIn); + } + } + fh.close(); + } + + zipCloseFileInZip(zf); +} + +bool WireSharkZipHelper::zip(QString fileName, QStringList files, QString relativeTo) +{ + + QFileInfo fi(fileName); + if ( fi.exists() ) + QFile::remove(fileName); + + zipFile zf = zipOpen(fileName.toUtf8().constData(), APPEND_STATUS_CREATE); + if ( zf == Q_NULLPTR ) + return false; + + for ( int cnt = 0; cnt < files.count(); cnt++ ) + { + QFileInfo sf(files.at(cnt)); + QString fileInZip = sf.absoluteFilePath(); + fileInZip.replace(relativeTo, ""); + /* Windows cannot open zip files, if the filenames starts with a separator */ + while ( fileInZip.length() > 0 && fileInZip.startsWith(QDir::separator()) ) + fileInZip = fileInZip.right(fileInZip.length() - 1); + + WireSharkZipHelper::addFileToZip(zf, sf.absoluteFilePath(), fileInZip); + + } + + if ( zipClose(zf, Q_NULLPTR) ) + return false; + + return true; +} + #endif /* diff --git a/ui/qt/utils/wireshark_zip_helper.h b/ui/qt/utils/wireshark_zip_helper.h index 8b232dc966..bbf17c0709 100644 --- a/ui/qt/utils/wireshark_zip_helper.h +++ b/ui/qt/utils/wireshark_zip_helper.h @@ -18,10 +18,17 @@ #ifdef HAVE_MINIZIP +#include "minizip/zip.h" + class WireSharkZipHelper { public: + static bool zip(QString zipFile, QStringList files, QString relativeTo = QString()); static bool unzip(QString zipFile, QString directory, bool (*fileCheck)(QString fileName, int fileSize) ); + +protected: + static void addFileToZip(zipFile zf, QString filepath, QString fileInZip); + }; #endif