1 /* profile_model.cpp
2  *
3  * Wireshark - Network traffic analyzer
4  * By Gerald Combs <gerald@wireshark.org>
5  * Copyright 1998 Gerald Combs
6  *
7  * SPDX-License-Identifier: GPL-2.0-or-later
8  */
9 
10 #include "config.h"
11 
12 #include <errno.h>
13 
14 #include "glib.h"
15 #include "ui/profile.h"
16 #include "ui/recent.h"
17 #include "wsutil/filesystem.h"
18 #include "epan/prefs.h"
19 
20 #include <ui/qt/models/profile_model.h>
21 
22 #include <ui/qt/utils/color_utils.h>
23 #include <ui/qt/utils/qt_ui_utils.h>
24 #include <ui/qt/utils/wireshark_zip_helper.h>
25 
26 #include <QDir>
27 #include <QFont>
28 #include <QTemporaryDir>
29 
30 Q_LOGGING_CATEGORY(profileLogger, "wireshark.profiles")
31 
ProfileSortModel(QObject * parent)32 ProfileSortModel::ProfileSortModel(QObject * parent):
33     QSortFilterProxyModel (parent),
34     ft_(ProfileSortModel::AllProfiles),
35     ftext_(QString())
36 {}
37 
lessThan(const QModelIndex & source_left,const QModelIndex & source_right) const38 bool ProfileSortModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
39 {
40     QModelIndex left = source_left;
41     if (source_left.column() != ProfileModel::COL_NAME)
42         left = source_left.sibling(source_left.row(), ProfileModel::COL_NAME);
43 
44     QModelIndex right = source_right;
45     if (source_right.column() != ProfileModel::COL_NAME)
46         right = source_right.sibling(source_right.row(), ProfileModel::COL_NAME);
47 
48     bool igL = left.data(ProfileModel::DATA_IS_GLOBAL).toBool();
49     bool igR = right.data(ProfileModel::DATA_IS_GLOBAL).toBool();
50 
51     if (left.data(ProfileModel::DATA_STATUS).toInt() == PROF_STAT_DEFAULT)
52         igL = true;
53     if (right.data(ProfileModel::DATA_STATUS).toInt() == PROF_STAT_DEFAULT)
54         igR = true;
55 
56     if (igL && ! igR)
57         return true;
58     else if (! igL && igR)
59         return false;
60     else if (igL && igR)
61     {
62         if (left.data(ProfileModel::DATA_STATUS) == PROF_STAT_DEFAULT)
63             return true;
64     }
65 
66     if (left.data().toString().compare(right.data().toString()) <= 0)
67         return true;
68 
69     return false;
70 }
71 
setFilterType(FilterType ft)72 void ProfileSortModel::setFilterType(FilterType ft)
73 {
74     ft_ = ft;
75     invalidateFilter();
76 }
77 
setFilterString(QString txt)78 void ProfileSortModel::setFilterString(QString txt)
79 {
80     ftext_ = ! txt.isEmpty() ? txt : "";
81     invalidateFilter();
82 }
83 
filterTypes()84 QStringList ProfileSortModel::filterTypes()
85 {
86     QMap<int, QString> filter_types_;
87     filter_types_.insert(ProfileSortModel::AllProfiles, tr("All profiles"));
88     filter_types_.insert(ProfileSortModel::PersonalProfiles, tr("Personal profiles"));
89     filter_types_.insert(ProfileSortModel::GlobalProfiles, tr("Global profiles"));
90 
91     return filter_types_.values();
92 }
93 
filterAcceptsRow(int source_row,const QModelIndex &) const94 bool ProfileSortModel::filterAcceptsRow(int source_row, const QModelIndex &) const
95 {
96     bool accept = true;
97     QModelIndex idx = sourceModel()->index(source_row, ProfileModel::COL_NAME);
98 
99     if (ft_ != ProfileSortModel::AllProfiles)
100     {
101         bool gl = idx.data(ProfileModel::DATA_IS_GLOBAL).toBool();
102         if (ft_ == ProfileSortModel::PersonalProfiles && gl)
103             accept = false;
104         else if (ft_ == ProfileSortModel::GlobalProfiles && ! gl)
105             accept = false;
106     }
107 
108     if (ftext_.length() > 0)
109     {
110         QString name = idx.data().toString();
111         if (! name.contains(ftext_, Qt::CaseInsensitive))
112             accept = false;
113     }
114 
115     return accept;
116 }
117 
ProfileModel(QObject * parent)118 ProfileModel::ProfileModel(QObject * parent) :
119     QAbstractTableModel(parent)
120 {
121     /* Store preset profile name */
122     set_profile_ = get_profile_name();
123 
124     reset_default_ = false;
125     profiles_imported_ = false;
126 
127     last_set_row_ = 0;
128 
129     /* Set filenames for profiles */
130     GList *files, *file;
131     files = g_hash_table_get_keys(const_cast<GHashTable *>(allowed_profile_filenames()));
132     file = g_list_first(files);
133     while (file) {
134         profile_files_ << static_cast<char *>(file->data);
135         file = gxx_list_next(file);
136     }
137     g_list_free(files);
138 
139     loadProfiles();
140 }
141 
loadProfiles()142 void ProfileModel::loadProfiles()
143 {
144     emit beginResetModel();
145 
146     bool refresh = profiles_.count() > 0;
147 
148      if (refresh)
149          profiles_.clear();
150      else
151          init_profile_list();
152 
153     GList *fl_entry = edited_profile_list();
154     while (fl_entry && fl_entry->data)
155     {
156         profiles_ << reinterpret_cast<profile_def *>(fl_entry->data);
157         fl_entry = gxx_list_next(fl_entry);
158     }
159 
160     emit endResetModel();
161 }
162 
entry(profile_def * ref) const163 GList * ProfileModel::entry(profile_def *ref) const
164 {
165     GList *fl_entry = edited_profile_list();
166     while (fl_entry && fl_entry->data)
167     {
168         profile_def *profile = reinterpret_cast<profile_def *>(fl_entry->data);
169         if (QString(ref->name).compare(profile->name) == 0 &&
170              QString(ref->reference).compare(profile->reference) == 0 &&
171              ref->is_global == profile->is_global &&
172              ref->status == profile->status)
173         {
174             return fl_entry;
175         }
176 
177         fl_entry = gxx_list_next(fl_entry);
178     }
179 
180     return Q_NULLPTR;
181 }
182 
at(int row) const183 GList *ProfileModel::at(int row) const
184 {
185     if (row < 0 || row >= profiles_.count())
186         return Q_NULLPTR;
187 
188     profile_def * prof = profiles_.at(row);
189     return entry(prof);
190 }
191 
changesPending() const192 bool ProfileModel::changesPending() const
193 {
194     if (reset_default_)
195         return true;
196 
197     if (g_list_length(edited_profile_list()) != g_list_length(current_profile_list()))
198         return true;
199 
200     bool pending = false;
201     GList *fl_entry = edited_profile_list();
202     while (fl_entry && fl_entry->data && ! pending) {
203         profile_def *profile = reinterpret_cast<profile_def *>(fl_entry->data);
204         pending = (profile->status == PROF_STAT_NEW || profile->status == PROF_STAT_CHANGED || profile->status == PROF_STAT_COPY);
205         fl_entry = gxx_list_next(fl_entry);
206     }
207 
208     return pending;
209 }
210 
importPending() const211 bool ProfileModel::importPending() const
212 {
213     return profiles_imported_;
214 }
215 
userProfilesExist() const216 bool ProfileModel::userProfilesExist() const
217 {
218     bool user_exists = false;
219     for (int cnt = 0; cnt < rowCount() && ! user_exists; cnt++)
220     {
221         QModelIndex idx = index(cnt, ProfileModel::COL_NAME);
222         if (! idx.data(ProfileModel::DATA_IS_GLOBAL).toBool() && ! idx.data(ProfileModel::DATA_IS_DEFAULT).toBool())
223             user_exists = true;
224     }
225 
226     return user_exists;
227 }
228 
rowCount(const QModelIndex &) const229 int ProfileModel::rowCount(const QModelIndex &) const
230 {
231     return profiles_.count();
232 }
233 
columnCount(const QModelIndex &) const234 int ProfileModel::columnCount(const QModelIndex &) const
235 {
236     return static_cast<int>(_LAST_ENTRY);
237 }
238 
guard(const QModelIndex & index) const239 profile_def * ProfileModel::guard(const QModelIndex &index) const
240 {
241     if (! index.isValid())
242         return Q_NULLPTR;
243 
244     return guard(index.row());
245 }
246 
guard(int row) const247 profile_def * ProfileModel::guard(int row) const
248 {
249     if (row < 0 || profiles_.count() <= row)
250         return Q_NULLPTR;
251 
252     if (! edited_profile_list())
253     {
254         static_cast<QList<profile_def *>>(profiles_).clear();
255         return Q_NULLPTR;
256     }
257 
258     return profiles_.value(row, Q_NULLPTR);
259 }
260 
dataDisplay(const QModelIndex & index) const261 QVariant ProfileModel::dataDisplay(const QModelIndex &index) const
262 {
263     profile_def * prof = guard(index);
264     if (! prof)
265         return QVariant();
266 
267     switch (index.column())
268     {
269     case COL_NAME:
270         return QString(prof->name);
271     case COL_TYPE:
272         if (prof->status == PROF_STAT_DEFAULT)
273             return tr("Default");
274         else if (prof->is_global)
275             return tr("Global");
276         else
277             return tr("Personal");
278     default:
279         break;
280     }
281 
282     return QVariant();
283 }
284 
dataFontRole(const QModelIndex & index) const285 QVariant ProfileModel::dataFontRole(const QModelIndex &index) const
286 {
287     if (! index.isValid() || profiles_.count() <= index.row())
288         return QVariant();
289 
290     profile_def * prof = guard(index.row());
291     if (! prof)
292         return QVariant();
293 
294     QFont font;
295 
296     if (prof->is_global)
297         font.setItalic(true);
298 
299     if (! prof->is_global && ! checkDuplicate(index))
300     {
301         if ((set_profile_.compare(prof->name) == 0 &&  prof->status == PROF_STAT_EXISTS) ||
302              (set_profile_.compare(prof->reference) == 0 &&  prof->status == PROF_STAT_CHANGED) )
303             font.setBold(true);
304     }
305 
306     if (prof->status == PROF_STAT_DEFAULT && reset_default_)
307         font.setStrikeOut(true);
308 
309     return font;
310 }
311 
checkIfDeleted(int row) const312 bool ProfileModel::checkIfDeleted(int row) const
313 {
314     QModelIndex idx = index(row, ProfileModel::COL_NAME);
315     return checkIfDeleted(idx);
316 }
317 
checkIfDeleted(const QModelIndex & index) const318 bool ProfileModel::checkIfDeleted(const QModelIndex &index) const
319 {
320     profile_def * prof = guard(index);
321     if (! prof)
322         return false;
323 
324     QStringList deletedNames;
325 
326     GList * current = current_profile_list();
327 
328     /* search the current list as long as we have not found anything */
329     while (current)
330     {
331         bool found = false;
332         GList * edited = edited_profile_list();
333         profile_def * profcurr = static_cast<profile_def *>(current->data);
334 
335         if (! profcurr->is_global && profcurr->status != PROF_STAT_DEFAULT)
336         {
337             while (edited && ! found)
338             {
339                 profile_def * profed = static_cast<profile_def *>(edited->data);
340                 if (! profed->is_global && profed->status != PROF_STAT_DEFAULT)
341                 {
342                     if (g_strcmp0(profcurr->name, profed->name) == 0 || g_strcmp0(profcurr->name, profed->reference) == 0)
343                     {
344                         if (profed->status == profcurr->status && prof->status != PROF_STAT_NEW && prof->status != PROF_STAT_COPY)
345                             found = true;
346                     }
347                 }
348 
349                 edited = gxx_list_next(edited);
350             }
351 
352             /* profile has been deleted, check if it has the name we ask for */
353             if (! found)
354                 deletedNames << profcurr->name;
355         }
356 
357         if (profcurr->is_global && deletedNames.contains(profcurr->name))
358             deletedNames.removeAll(profcurr->name);
359 
360         current = gxx_list_next(current);
361     }
362 
363     if (deletedNames.contains(prof->name))
364         return true;
365 
366     return false;
367 }
368 
checkInvalid(const QModelIndex & index) const369 bool ProfileModel::checkInvalid(const QModelIndex &index) const
370 {
371     profile_def * prof = guard(index);
372     if (! prof)
373         return false;
374 
375     int ref = this->findAsReference(prof->name);
376     if (ref == index.row())
377         return false;
378 
379     profile_def * pg = guard(ref);
380     if (pg && pg->status == PROF_STAT_CHANGED && g_strcmp0(pg->name, pg->reference) != 0 && ! prof->is_global)
381         return true;
382 
383     return false;
384 }
385 
checkDuplicate(const QModelIndex & index,bool isOriginalToDuplicate) const386 bool ProfileModel::checkDuplicate(const QModelIndex &index, bool isOriginalToDuplicate) const
387 {
388     profile_def * prof = guard(index);
389     if (! prof || (! isOriginalToDuplicate && prof->status == PROF_STAT_EXISTS) )
390         return false;
391 
392     QList<int> rows = this->findAllByNameAndVisibility(prof->name, prof->is_global, false);
393     int found = 0;
394     profile_def * check = Q_NULLPTR;
395     for (int cnt = 0; cnt < rows.count(); cnt++)
396     {
397         int row = rows.at(cnt);
398 
399         if (row == index.row())
400             continue;
401 
402         check = guard(row);
403         if (! check || (isOriginalToDuplicate && check->status == PROF_STAT_EXISTS) )
404             continue;
405 
406         found++;
407     }
408 
409     if (found > 0)
410         return true;
411     return false;
412 }
413 
dataBackgroundRole(const QModelIndex & index) const414 QVariant ProfileModel::dataBackgroundRole(const QModelIndex &index) const
415 {
416     if (! index.isValid() || profiles_.count() <= index.row())
417         return QVariant();
418 
419     profile_def * prof = guard(index.row());
420     if (! prof)
421         return QVariant();
422 
423     if (prof->status == PROF_STAT_DEFAULT && reset_default_)
424         return ColorUtils::fromColorT(&prefs.gui_text_deprecated);
425 
426     if (prof->status != PROF_STAT_DEFAULT && ! prof->is_global)
427     {
428         /* Highlights errorneous line */
429         if (checkInvalid(index) || checkIfDeleted(index) || checkDuplicate(index) || ! checkNameValidity(prof->name))
430             return ColorUtils::fromColorT(&prefs.gui_text_invalid);
431 
432         /* Highlights line, which has been duplicated by another index */
433         if (checkDuplicate(index, true))
434             return ColorUtils::fromColorT(&prefs.gui_text_valid);
435     }
436 
437     return QVariant();
438 }
439 
dataToolTipRole(const QModelIndex & idx) const440 QVariant ProfileModel::dataToolTipRole(const QModelIndex &idx) const
441 {
442     if (! idx.isValid() || profiles_.count() <= idx.row())
443         return QVariant();
444 
445     profile_def * prof = guard(idx.row());
446     if (! prof)
447         return QVariant();
448 
449     if (prof->is_global)
450         return tr("This is a system provided profile");
451     else
452         return dataPath(idx);
453 }
454 
dataPath(const QModelIndex & index) const455 QVariant ProfileModel::dataPath(const QModelIndex &index) const
456 {
457     if (! index.isValid() || profiles_.count() <= index.row())
458         return QVariant();
459 
460     profile_def * prof = guard(index.row());
461     if (! prof)
462         return QVariant();
463 
464     if (checkInvalid(index))
465     {
466         int ref = this->findAsReference(prof->name);
467         if (ref != index.row() && ref >= 0)
468         {
469             profile_def * prof = guard(ref);
470             QString msg = tr("A profile change for this name is pending");
471             if (prof)
472                 msg.append(tr(" (See: %1)").arg(prof->name));
473             return msg;
474         }
475 
476         return tr("This is an invalid profile definition");
477     }
478 
479     if ((prof->status == PROF_STAT_NEW || prof->status == PROF_STAT_CHANGED || prof->status == PROF_STAT_COPY) && checkDuplicate(index))
480         return tr("A profile already exists with this name");
481 
482     if (checkIfDeleted(index))
483     {
484         return tr("A profile with this name is being deleted");
485     }
486 
487     if (prof->is_import)
488         return tr("Imported profile");
489 
490     switch (prof->status)
491     {
492     case PROF_STAT_DEFAULT:
493         if (!reset_default_)
494             return gchar_free_to_qstring(get_persconffile_path("", FALSE));
495         else
496             return tr("Resetting to default");
497     case PROF_STAT_EXISTS:
498         {
499             QString profile_path;
500             if (prof->is_global) {
501                 profile_path = gchar_free_to_qstring(get_global_profiles_dir());
502             } else {
503                 profile_path = gchar_free_to_qstring(get_profiles_dir());
504             }
505             profile_path.append("/").append(prof->name);
506             return profile_path;
507         }
508     case PROF_STAT_NEW:
509         {
510             QString errMsg;
511 
512             if (! checkNameValidity(prof->name, &errMsg))
513                 return errMsg;
514             else
515                 return tr("Created from default settings");
516         }
517     case PROF_STAT_CHANGED:
518         {
519             QString msg;
520             if (! ProfileModel::checkNameValidity(QString(prof->name), &msg))
521                 return msg;
522 
523             if (prof->reference)
524                 return tr("Renamed from: %1").arg(prof->reference);
525 
526             return QVariant();
527         }
528     case PROF_STAT_COPY:
529         {
530             QString msg;
531 
532             /* this should always be the case, but just as a precaution it is checked */
533             if (prof->reference)
534             {
535                 msg = tr("Copied from: %1").arg(prof->reference);
536                 QString appendix;
537 
538                 /* A global profile is neither deleted or removed, only system provided is allowed as appendix */
539                 if (profile_exists(prof->reference, TRUE) && prof->from_global)
540                     appendix = tr("system provided");
541                 /* A default model as reference can neither be deleted or renamed, so skip if the reference was one */
542                 else  if (! index.data(ProfileModel::DATA_IS_DEFAULT).toBool())
543                 {
544                     /* find a non-global, non-default profile which could be referenced by this one. Those are the only
545                      * ones which could be renamed or deleted */
546                     int row = this->findByNameAndVisibility(prof->reference, false, true);
547                     profile_def * ref = guard(row);
548 
549                     /* The reference is itself a copy of the original, therefore it is not accepted */
550                     if (ref && (ref->status == PROF_STAT_COPY || ref->status == PROF_STAT_NEW) && QString(ref->name).compare(prof->reference) != 0)
551                         ref = Q_NULLPTR;
552 
553                     /* found no other profile, original one had to be deleted */
554                     if (! ref || row == index.row() || checkIfDeleted(row))
555                     {
556                         appendix = tr("deleted");
557                     }
558                     /* found another profile, so the reference had been renamed, it the status is changed */
559                     else if (ref && ref->status == PROF_STAT_CHANGED)
560                     {
561                         appendix = tr("renamed to %1").arg(ref->name);
562                     }
563                 }
564 
565                 if (appendix.length() > 0)
566                     msg.append(QString(" (%1)").arg(appendix));
567             }
568 
569             return msg;
570         }
571     }
572 
573     return QVariant();
574 }
575 
data(const QModelIndex & index,int role) const576 QVariant ProfileModel::data(const QModelIndex &index, int role) const
577 {
578     profile_def * prof = guard(index);
579     if (! prof)
580         return QVariant();
581 
582     switch (role)
583     {
584     case Qt::DisplayRole:
585         return dataDisplay(index);
586     case Qt::FontRole:
587         return dataFontRole(index);
588     case Qt::BackgroundRole:
589         return dataBackgroundRole(index);
590     case Qt::ToolTipRole:
591         return dataToolTipRole(index);
592     case ProfileModel::DATA_STATUS:
593         return QVariant::fromValue(prof->status);
594     case ProfileModel::DATA_IS_DEFAULT:
595         return QVariant::fromValue(prof->status == PROF_STAT_DEFAULT);
596     case ProfileModel::DATA_IS_GLOBAL:
597         return QVariant::fromValue(prof->is_global);
598     case ProfileModel::DATA_IS_SELECTED:
599         {
600             QModelIndex selected = activeProfile();
601             profile_def * selprof = guard(selected);
602             if (selprof)
603             {
604                 if (selprof && selprof->is_global != prof->is_global)
605                     return QVariant::fromValue(false);
606 
607                 if (selprof && strcmp(selprof->name, prof->name) == 0)
608                     return QVariant::fromValue(true);
609             }
610             return QVariant::fromValue(false);
611         }
612     case ProfileModel::DATA_PATH:
613         return dataPath(index);
614     case ProfileModel::DATA_INDEX_VALUE_IS_URL:
615         if (index.column() <= ProfileModel::COL_TYPE)
616             return QVariant::fromValue(false);
617         return QVariant::fromValue(true);
618     case ProfileModel::DATA_PATH_IS_NOT_DESCRIPTION:
619         if (prof->status == PROF_STAT_NEW || prof->status == PROF_STAT_COPY
620              || (prof->status == PROF_STAT_DEFAULT && reset_default_)
621              || prof->status == PROF_STAT_CHANGED || prof->is_import)
622             return QVariant::fromValue(false);
623         else
624             return QVariant::fromValue(true);
625 
626     default:
627         break;
628     }
629 
630     return QVariant();
631 }
632 
headerData(int section,Qt::Orientation orientation,int role) const633 QVariant ProfileModel::headerData(int section, Qt::Orientation orientation, int role) const
634 {
635     if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
636     {
637         switch (section)
638         {
639         case COL_NAME:
640             return tr("Profile");
641         case COL_TYPE:
642             return tr("Type");
643         default:
644             break;
645         }
646     }
647 
648     return QVariant();
649 }
650 
flags(const QModelIndex & index) const651 Qt::ItemFlags ProfileModel::flags(const QModelIndex &index) const
652 {
653     Qt::ItemFlags fl = QAbstractTableModel::flags(index);
654 
655     profile_def * prof = guard(index);
656     if (! prof)
657         return fl;
658 
659     if (index.column() == ProfileModel::COL_NAME && prof->status != PROF_STAT_DEFAULT  && ! prof->is_global)
660         fl |= Qt::ItemIsEditable;
661 
662     return fl;
663 }
664 
findByName(QString name)665 int ProfileModel::findByName(QString name)
666 {
667     int row = findByNameAndVisibility(name, false);
668     if (row < 0)
669         row = findByNameAndVisibility(name, true);
670 
671     return row;
672 }
673 
findAsReference(QString reference) const674 int ProfileModel::findAsReference(QString reference) const
675 {
676     int found = -1;
677     if (reference.length() <= 0)
678         return found;
679 
680     for (int cnt = 0; cnt < profiles_.count() && found < 0; cnt++)
681     {
682         profile_def * prof = guard(cnt);
683         if (prof && reference.compare(prof->reference) == 0)
684             found = cnt;
685     }
686 
687     return found;
688 }
689 
findByNameAndVisibility(QString name,bool isGlobal,bool searchReference) const690 int ProfileModel::findByNameAndVisibility(QString name, bool isGlobal, bool searchReference) const
691 {
692     QList<int> result = findAllByNameAndVisibility(name, isGlobal, searchReference);
693     return result.count() == 0 ? -1 : result.at(0);
694 }
695 
findAllByNameAndVisibility(QString name,bool isGlobal,bool searchReference) const696 QList<int> ProfileModel::findAllByNameAndVisibility(QString name, bool isGlobal, bool searchReference) const
697 {
698     QList<int> result;
699 
700     for (int cnt = 0; cnt < profiles_.count(); cnt++)
701     {
702         profile_def * prof = guard(cnt);
703         if (prof && static_cast<bool>(prof->is_global) == isGlobal)
704         {
705             if (name.compare(prof->name) == 0 || (searchReference && name.compare(prof->reference) == 0) )
706                 result << cnt;
707         }
708     }
709 
710     return result;
711 
712 }
713 
addNewProfile(QString name)714 QModelIndex ProfileModel::addNewProfile(QString name)
715 {
716     int cnt = 1;
717     QString newName = name;
718     while (findByNameAndVisibility(newName) >= 0)
719     {
720         newName = QString("%1 %2").arg(name).arg(QString::number(cnt));
721         cnt++;
722     }
723 
724     add_to_profile_list(newName.toUtf8().constData(), newName.toUtf8().constData(), PROF_STAT_NEW, FALSE, FALSE, FALSE);
725     loadProfiles();
726 
727     return index(findByName(newName), COL_NAME);
728 }
729 
duplicateEntry(QModelIndex idx,int new_status)730 QModelIndex ProfileModel::duplicateEntry(QModelIndex idx, int new_status)
731 {
732     profile_def * prof = guard(idx);
733     if (! prof)
734         return QModelIndex();
735 
736     /* only new and copied stati can be set */
737     if (new_status != PROF_STAT_NEW && new_status != PROF_STAT_COPY)
738         new_status = PROF_STAT_COPY;
739 
740     /* this is a copy from a personal profile, check if the original has been a
741      * new profile or a preexisting one. In the case of a new profile, restart
742      * with the state PROF_STAT_NEW */
743     if (prof->status == PROF_STAT_COPY && ! prof->from_global)
744     {
745         int row = findByNameAndVisibility(prof->reference, false);
746         profile_def * copyParent = guard(row);
747         if (copyParent && copyParent->status == PROF_STAT_NEW)
748             return duplicateEntry(index(row, ProfileModel::COL_NAME), PROF_STAT_NEW);
749     }
750 
751     /* Rules for figuring out the name to copy from:
752      *
753      * General, use copy name
754      * If status of copy is new or changed => use copy reference
755      * If copy is non global and status of copy is != changed, use original parent name
756      */
757     QString parent = prof->name;
758     if (prof->status == PROF_STAT_CHANGED)
759         parent = prof->reference;
760     else if (! prof->is_global && prof->status != PROF_STAT_NEW && prof->status != PROF_STAT_CHANGED)
761         parent = get_profile_parent (prof->name);
762 
763     if (parent.length() == 0)
764         return QModelIndex();
765 
766     /* parent references the parent profile to be used, parentName is the base for the new name */
767     QString parentName = parent;
768     /* the user has changed the profile name, therefore this is also the name to be used */
769     if (prof->status != PROF_STAT_EXISTS)
770         parentName = prof->name;
771 
772     /* check to ensure we do not end up with (copy) (copy) (copy) ... */
773     QRegExp rx("\\s+(\\(\\s*" + tr("copy", "noun") + "\\s*\\d*\\))");
774     if (rx.indexIn(parentName) >= 0)
775         parentName.replace(rx, "");
776 
777     QString new_name;
778     /* if copy is global and name has not been used before, use that, else create first copy */
779     if (prof->is_global && findByNameAndVisibility(parentName) < 0)
780         new_name = QString(prof->name);
781     else
782         new_name = QString("%1 (%2)").arg(parentName).arg(tr("copy", "noun"));
783 
784     /* check if copy already exists and iterate, until an unused version is found */
785     int cnt = 1;
786     while (findByNameAndVisibility(new_name) >= 0)
787     {
788         new_name = QString("%1 (%2 %3)").arg(parentName).arg(tr("copy", "noun")).arg(QString::number(cnt));
789         cnt++;
790     }
791 
792     /* if this would be a copy, but the original is already a new one, this is a copy as well */
793     if (new_status == PROF_STAT_COPY && prof->status == PROF_STAT_NEW)
794         new_status = PROF_STAT_NEW;
795 
796     /* add element */
797     add_to_profile_list(new_name.toUtf8().constData(), parent.toUtf8().constData(), new_status, FALSE, prof->from_global ? prof->from_global : prof->is_global, FALSE);
798 
799     /* reload profile list in model */
800     loadProfiles();
801 
802     int row = findByNameAndVisibility(new_name, false);
803     /* sanity check, if adding the profile went correctly */
804     if (row < 0 || row == idx.row())
805         return QModelIndex();
806 
807     /* return the index of the profile */
808     return index(row, COL_NAME);
809 }
810 
deleteEntry(QModelIndex idx)811 void ProfileModel::deleteEntry(QModelIndex idx)
812 {
813     if (! idx.isValid())
814         return;
815 
816     QModelIndexList temp;
817     temp << idx;
818     deleteEntries(temp);
819 }
820 
deleteEntries(QModelIndexList idcs)821 void ProfileModel::deleteEntries(QModelIndexList idcs)
822 {
823     bool changes = false;
824 
825     QList<int> indeces;
826     foreach (QModelIndex idx, idcs)
827     {
828         if (! indeces.contains(idx.row()) && ! idx.data(ProfileModel::DATA_IS_GLOBAL).toBool())
829             indeces << idx.row();
830     }
831     /* Security blanket. This ensures, that we start deleting from the end and do not get any issues iterating the list */
832     std::sort(indeces.begin(), indeces.end(), std::less<int>());
833 
834     foreach (int row, indeces)
835     {
836         profile_def * prof = guard(row);
837         if (! prof)
838             continue;
839 
840         if (prof->is_global)
841             continue;
842 
843         if (prof->status == PROF_STAT_DEFAULT)
844         {
845             reset_default_ = ! reset_default_;
846         }
847         else
848         {
849             GList * fl_entry = entry(prof);
850             if (fl_entry)
851             {
852                 changes = true;
853                 remove_from_profile_list(fl_entry);
854             }
855         }
856     }
857 
858     if (changes)
859         loadProfiles();
860 
861     if (reset_default_)
862     {
863         emit layoutAboutToBeChanged();
864         emit dataChanged(index(0, 0), index(rowCount(), columnCount()));
865         emit layoutChanged();
866     }
867 }
868 
resetDefault() const869 bool ProfileModel::resetDefault() const
870 {
871     return reset_default_;
872 }
873 
doResetModel(bool reset_import)874 void ProfileModel::doResetModel(bool reset_import)
875 {
876     reset_default_ = false;
877     if (reset_import)
878         profiles_imported_ = false;
879 
880     loadProfiles();
881 }
882 
activeProfile() const883 QModelIndex ProfileModel::activeProfile() const
884 {
885     QList<int> rows = this->findAllByNameAndVisibility(set_profile_, false, true);
886     foreach (int row, rows)
887     {
888         profile_def * prof = profiles_.at(row);
889         if (prof->is_global || checkDuplicate(index(row, ProfileModel::COL_NAME)) )
890             return QModelIndex();
891 
892         if ((set_profile_.compare(prof->name) == 0 && (prof->status == PROF_STAT_EXISTS || prof->status == PROF_STAT_DEFAULT) ) ||
893              (set_profile_.compare(prof->reference) == 0 &&  prof->status == PROF_STAT_CHANGED) )
894             return index(row, ProfileModel::COL_NAME);
895     }
896 
897     return QModelIndex();
898 }
899 
setData(const QModelIndex & idx,const QVariant & value,int role)900 bool ProfileModel::setData(const QModelIndex &idx, const QVariant &value, int role)
901 {
902     last_set_row_ = -1;
903 
904     if (role != Qt::EditRole ||  ! value.isValid() || value.toString().isEmpty())
905         return false;
906 
907     QString newValue = value.toString();
908     profile_def * prof = guard(idx);
909     if (! prof || prof->status == PROF_STAT_DEFAULT)
910         return false;
911 
912     last_set_row_ = idx.row();
913 
914     QString current(prof->name);
915     if (current.compare(newValue) != 0)
916     {
917         g_free(prof->name);
918         prof->name = qstring_strdup(newValue);
919 
920         if (prof->reference && g_strcmp0(prof->name, prof->reference) == 0 && ! (prof->status == PROF_STAT_NEW || prof->status == PROF_STAT_COPY)) {
921             prof->status = PROF_STAT_EXISTS;
922         } else if (prof->status == PROF_STAT_EXISTS) {
923             prof->status = PROF_STAT_CHANGED;
924         }
925         emit itemChanged(idx);
926     }
927 
928     loadProfiles();
929 
930     return true;
931 }
932 
lastSetRow() const933 int ProfileModel::lastSetRow() const
934 {
935     return last_set_row_;
936 }
937 
copyTempToProfile(QString tempPath,QString profilePath,bool * wasEmpty)938 bool ProfileModel::copyTempToProfile(QString tempPath, QString profilePath, bool * wasEmpty)
939 {
940     bool was_empty = true;
941 
942     QDir profileDir(profilePath);
943     if (! profileDir.mkpath(profilePath) || ! QFile::exists(tempPath))
944         return false;
945 
946     QDir tempProfile(tempPath);
947     tempProfile.setFilter(QDir::Files | QDir::Hidden | QDir::NoSymLinks);
948     QFileInfoList files = tempProfile.entryInfoList();
949     if (files.count() <= 0)
950         return false;
951 
952     int created = 0;
953     foreach (QFileInfo finfo, files)
954     {
955         QString tempFile = finfo.absoluteFilePath();
956         QString profileFile = profilePath + "/" + finfo.fileName();
957 
958         if (! profile_files_.contains(finfo.fileName()))
959         {
960             was_empty = false;
961             continue;
962         }
963 
964         if (! QFile::exists(tempFile) || QFile::exists(profileFile))
965             continue;
966 
967         if (QFile::copy(tempFile, profileFile))
968             created++;
969     }
970 
971     if (wasEmpty)
972         *wasEmpty = was_empty;
973 
974     if (created > 0)
975         return true;
976 
977     return false;
978 }
979 
uniquePaths(QFileInfoList lst)980 QFileInfoList ProfileModel::uniquePaths(QFileInfoList lst)
981 {
982     QStringList files;
983     QFileInfoList newLst;
984 
985     foreach (QFileInfo entry, lst)
986     {
987         if (! files.contains(entry.absoluteFilePath()))
988         {
989             if (entry.exists() && entry.isDir())
990             {
991                 newLst << entry.absoluteFilePath();
992                 files << entry.absoluteFilePath();
993             }
994         }
995     }
996 
997     return newLst;
998 }
999 
filterProfilePath(QString path,QFileInfoList ent,bool fromZip)1000 QFileInfoList ProfileModel::filterProfilePath(QString path, QFileInfoList ent, bool fromZip)
1001 {
1002     QFileInfoList result = ent;
1003     QDir temp(path);
1004     temp.setSorting(QDir::Name);
1005     temp.setFilter(QDir::Dirs | QDir::NoSymLinks | QDir::NoDotAndDotDot);
1006     QFileInfoList entries = temp.entryInfoList();
1007     if (! fromZip)
1008         entries << QFileInfo(path);
1009     foreach (QFileInfo entry, entries)
1010     {
1011         QDir fPath(entry.absoluteFilePath());
1012         fPath.setSorting(QDir::Name);
1013         fPath.setFilter(QDir::Files | QDir::NoSymLinks);
1014         QFileInfoList fEntries = fPath.entryInfoList();
1015         bool found = false;
1016         for (int cnt = 0; cnt < fEntries.count() && ! found; cnt++)
1017         {
1018             if (config_file_exists_with_entries(fEntries[cnt].absoluteFilePath().toUtf8().constData(), '#'))
1019                  found = true;
1020         }
1021 
1022         if (found)
1023         {
1024             result.append(entry);
1025         }
1026         else
1027         {
1028             if (path.compare(entry.absoluteFilePath()) != 0)
1029                 result.append(filterProfilePath(entry.absoluteFilePath(), result, fromZip));
1030         }
1031     }
1032 
1033     return result;
1034 }
1035 
1036 #ifdef HAVE_MINIZIP
exportFileList(QModelIndexList items)1037 QStringList ProfileModel::exportFileList(QModelIndexList items)
1038 {
1039     QStringList result;
1040 
1041     foreach(QModelIndex idx, items)
1042     {
1043         profile_def * prof = guard(idx);
1044         if (! prof || prof->is_global || QString(prof->name).compare(DEFAULT_PROFILE) == 0)
1045             continue;
1046 
1047         if (! idx.data(ProfileModel::DATA_PATH_IS_NOT_DESCRIPTION).toBool())
1048             continue;
1049 
1050         QString path = idx.data(ProfileModel::DATA_PATH).toString();
1051         QDir temp(path);
1052         temp.setSorting(QDir::Name);
1053         temp.setFilter(QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot);
1054         QFileInfoList entries = temp.entryInfoList();
1055         foreach (QFileInfo fi, entries)
1056             result << fi.absoluteFilePath();
1057     }
1058 
1059     return result;
1060 }
1061 
exportProfiles(QString filename,QModelIndexList items,QString * err)1062 bool ProfileModel::exportProfiles(QString filename, QModelIndexList items, QString *err)
1063 {
1064     if (changesPending())
1065     {
1066         if (err)
1067             err->append(tr("Exporting profiles while changes are pending is not allowed"));
1068         return false;
1069     }
1070 
1071     /* Write recent file for current profile before exporting */
1072     write_profile_recent();
1073 
1074     QStringList files = exportFileList(items);
1075     if (files.count() == 0)
1076     {
1077         if (err)
1078             err->append((tr("No profiles found to export")));
1079         return false;
1080     }
1081 
1082     if (WiresharkZipHelper::zip(filename, files, gchar_free_to_qstring(get_profiles_dir()) + "/") )
1083         return true;
1084 
1085     return false;
1086 }
1087 
1088 /* This check runs BEFORE the file has been unzipped! */
acceptFile(QString fileName,int fileSize)1089 bool ProfileModel::acceptFile(QString fileName, int fileSize)
1090 {
1091     if (fileName.toLower().endsWith(".zip"))
1092         return false;
1093 
1094     /* Arbitrary maximum config file size accepted: 256MB */
1095     if (fileSize > 1024 * 1024 * 256)
1096         return false;
1097 
1098     return true;
1099 }
1100 
cleanName(QString fileName)1101 QString ProfileModel::cleanName(QString fileName)
1102 {
1103     QStringList parts = fileName.split("/");
1104     QString temp = parts[parts.count() - 1].replace(QRegExp("[" + QRegExp::escape(illegalCharacters()) + "]"), QString("_") );
1105     temp = parts.join("/");
1106     return temp;
1107 }
1108 
importProfilesFromZip(QString filename,int * skippedCnt,QStringList * result)1109 int ProfileModel::importProfilesFromZip(QString filename, int * skippedCnt, QStringList *result)
1110 {
1111     QTemporaryDir dir;
1112 #if 0
1113     dir.setAutoRemove(false);
1114     g_printerr("Temp dir for unzip: %s\n", dir.path().toUtf8().constData());
1115 #endif
1116 
1117     int cnt = 0;
1118     if (dir.isValid())
1119     {
1120         WiresharkZipHelper::unzip(filename, dir.path(), &ProfileModel::acceptFile, &ProfileModel::cleanName);
1121         cnt = importProfilesFromDir(dir.path(), skippedCnt, true, result);
1122     }
1123 
1124     return cnt;
1125 }
1126 #endif
1127 
importProfilesFromDir(QString dirname,int * skippedCnt,bool fromZip,QStringList * result)1128 int ProfileModel::importProfilesFromDir(QString dirname, int * skippedCnt, bool fromZip, QStringList *result)
1129 {
1130     int count = 0;
1131     int skipped = 0;
1132     QDir profileDir(gchar_free_to_qstring(get_profiles_dir()));
1133     QDir dir(dirname);
1134 
1135     if (skippedCnt)
1136         *skippedCnt = 0;
1137 
1138     if (dir.exists())
1139     {
1140         QFileInfoList entries = uniquePaths(filterProfilePath(dirname, QFileInfoList(), fromZip));
1141 
1142         int entryCount = 0;
1143         foreach (QFileInfo fentry, entries)
1144         {
1145             if (fentry.fileName().length() <= 0)
1146                 continue;
1147 
1148             bool wasEmpty = true;
1149             bool success = false;
1150 
1151             entryCount++;
1152 
1153             QString profilePath = profileDir.absolutePath() + "/" + fentry.fileName();
1154             QString tempPath = fentry.absoluteFilePath();
1155 
1156             if (fentry.fileName().compare(DEFAULT_PROFILE, Qt::CaseInsensitive) == 0 || QFile::exists(profilePath))
1157             {
1158                 skipped++;
1159                 continue;
1160             }
1161 
1162             if (result)
1163                 *result << fentry.fileName();
1164 
1165             success = copyTempToProfile(tempPath, profilePath, &wasEmpty);
1166             if (success)
1167             {
1168                 count++;
1169                 add_to_profile_list(fentry.fileName().toUtf8().constData(), fentry.fileName().toUtf8().constData(), PROF_STAT_NEW, FALSE, FALSE, TRUE);
1170             }
1171             else if (! wasEmpty && QFile::exists(profilePath))
1172             {
1173                 QDir dh(profilePath);
1174                 dh.rmdir(profilePath);
1175             }
1176         }
1177 
1178     }
1179 
1180     if (count > 0)
1181     {
1182         profiles_imported_ = true;
1183         loadProfiles();
1184     }
1185 
1186     if (skippedCnt)
1187         *skippedCnt = skipped;
1188 
1189     return count;
1190 }
1191 
markAsImported(QStringList importedItems)1192 void ProfileModel::markAsImported(QStringList importedItems)
1193 {
1194     if (importedItems.count() <= 0)
1195         return;
1196 
1197     profiles_imported_ = true;
1198 
1199     foreach (QString item, importedItems)
1200     {
1201         int row = findByNameAndVisibility(item, false);
1202         profile_def * prof = guard(row);
1203         if (! prof)
1204             continue;
1205 
1206         prof->is_import = true;
1207     }
1208 }
1209 
clearImported(QString * msg)1210 bool ProfileModel::clearImported(QString *msg)
1211 {
1212     QList<int> rows;
1213     bool result = true;
1214     for (int cnt = 0; cnt < rowCount(); cnt++)
1215     {
1216         profile_def * prof = guard(cnt);
1217         if (prof && prof->is_import && ! rows.contains(cnt))
1218             rows << cnt;
1219     }
1220     /* Security blanket. This ensures, that we start deleting from the end and do not get any issues iterating the list */
1221     std::sort(rows.begin(), rows.end(), std::less<int>());
1222 
1223     char * ret_path = Q_NULLPTR;
1224     for (int cnt = 0; cnt < rows.count() && result; cnt++)
1225     {
1226         int row = rows.at(cnt);
1227         if (delete_persconffile_profile (index(row, ProfileModel::COL_NAME).data().toString().toUtf8().constData(), &ret_path) != 0)
1228         {
1229             if (msg)
1230             {
1231                 QString errmsg = QString("%1\n\"%2\":\n%3").arg(tr("Can't delete profile directory")).arg(ret_path).arg(g_strerror(errno));
1232                 msg->append(errmsg);
1233             }
1234 
1235             result = false;
1236         }
1237     }
1238 
1239     return result;
1240 }
1241 
illegalCharacters()1242 QString ProfileModel::illegalCharacters()
1243 {
1244 #ifdef _WIN32
1245     /* According to https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions */
1246     return QString("<>:\"/\\|?*");
1247 #else
1248     return QDir::separator();
1249 #endif
1250 
1251 }
1252 
checkNameValidity(QString name,QString * msg)1253 bool ProfileModel::checkNameValidity(QString name, QString *msg)
1254 {
1255     QString message;
1256     bool invalid = false;
1257     QString msgChars;
1258 
1259     QString invalid_dir_chars = illegalCharacters();
1260 
1261     for (int cnt = 0; cnt < invalid_dir_chars.length() && ! invalid; cnt++)
1262     {
1263         msgChars += invalid_dir_chars[cnt] + " ";
1264         if (name.contains(invalid_dir_chars[cnt]))
1265             invalid = true;
1266     }
1267 #ifdef _WIN32
1268     if (invalid)
1269     {
1270         message = tr("A profile name cannot contain the following characters: %1").arg(msgChars);
1271     }
1272 
1273     if (message.isEmpty() && (name.startsWith('.') || name.endsWith('.')) )
1274         message = tr("A profile cannot start or end with a period (.)");
1275 #else
1276     if (invalid)
1277         message = tr("A profile name cannot contain the '/' character");
1278 #endif
1279 
1280     if (! message.isEmpty()) {
1281         if (msg)
1282             msg->append(message);
1283         return false;
1284     }
1285 
1286     return true;
1287 }
1288 
activeProfileName()1289 QString ProfileModel::activeProfileName()
1290 {
1291     ProfileModel model;
1292     QModelIndex idx = model.activeProfile();
1293     return idx.data(ProfileModel::COL_NAME).toString();
1294 }
1295 
activeProfilePath()1296 QString ProfileModel::activeProfilePath()
1297 {
1298     ProfileModel model;
1299     QModelIndex idx = model.activeProfile();
1300     return idx.data(ProfileModel::DATA_PATH).toString();
1301 }
1302