1 // Copyright (c) 2011-2020 The Bitcoin Core developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4 
5 #if defined(HAVE_CONFIG_H)
6 #include <config/bitcoin-config.h>
7 #endif
8 
9 #include <qt/optionsmodel.h>
10 
11 #include <qt/bitcoinunits.h>
12 #include <qt/guiconstants.h>
13 #include <qt/guiutil.h>
14 
15 #include <interfaces/node.h>
16 #include <validation.h> // For DEFAULT_SCRIPTCHECK_THREADS
17 #include <net.h>
18 #include <netbase.h>
19 #include <txdb.h> // for -dbcache defaults
20 #include <util/string.h>
21 
22 #include <QDebug>
23 #include <QSettings>
24 #include <QStringList>
25 
26 const char *DEFAULT_GUI_PROXY_HOST = "127.0.0.1";
27 
28 static const QString GetDefaultProxyAddress();
29 
OptionsModel(QObject * parent,bool resetSettings)30 OptionsModel::OptionsModel(QObject *parent, bool resetSettings) :
31     QAbstractListModel(parent)
32 {
33     Init(resetSettings);
34 }
35 
addOverriddenOption(const std::string & option)36 void OptionsModel::addOverriddenOption(const std::string &option)
37 {
38     strOverriddenByCommandLine += QString::fromStdString(option) + "=" + QString::fromStdString(gArgs.GetArg(option, "")) + " ";
39 }
40 
41 // Writes all missing QSettings with their default values
Init(bool resetSettings)42 void OptionsModel::Init(bool resetSettings)
43 {
44     if (resetSettings)
45         Reset();
46 
47     checkAndMigrate();
48 
49     QSettings settings;
50 
51     // Ensure restart flag is unset on client startup
52     setRestartRequired(false);
53 
54     // These are Qt-only settings:
55 
56     // Window
57     if (!settings.contains("fHideTrayIcon"))
58         settings.setValue("fHideTrayIcon", false);
59     fHideTrayIcon = settings.value("fHideTrayIcon").toBool();
60     Q_EMIT hideTrayIconChanged(fHideTrayIcon);
61 
62     if (!settings.contains("fMinimizeToTray"))
63         settings.setValue("fMinimizeToTray", false);
64     fMinimizeToTray = settings.value("fMinimizeToTray").toBool() && !fHideTrayIcon;
65 
66     if (!settings.contains("fMinimizeOnClose"))
67         settings.setValue("fMinimizeOnClose", false);
68     fMinimizeOnClose = settings.value("fMinimizeOnClose").toBool();
69 
70     // Display
71     if (!settings.contains("nDisplayUnit"))
72         settings.setValue("nDisplayUnit", BitcoinUnits::BTC);
73     nDisplayUnit = settings.value("nDisplayUnit").toInt();
74 
75     if (!settings.contains("strThirdPartyTxUrls"))
76         settings.setValue("strThirdPartyTxUrls", "");
77     strThirdPartyTxUrls = settings.value("strThirdPartyTxUrls", "").toString();
78 
79     if (!settings.contains("fCoinControlFeatures"))
80         settings.setValue("fCoinControlFeatures", false);
81     fCoinControlFeatures = settings.value("fCoinControlFeatures", false).toBool();
82 
83     // These are shared with the core or have a command-line parameter
84     // and we want command-line parameters to overwrite the GUI settings.
85     //
86     // If setting doesn't exist create it with defaults.
87     //
88     // If gArgs.SoftSetArg() or gArgs.SoftSetBoolArg() return false we were overridden
89     // by command-line and show this in the UI.
90 
91     // Main
92     if (!settings.contains("bPrune"))
93         settings.setValue("bPrune", false);
94     if (!settings.contains("nPruneSize"))
95         settings.setValue("nPruneSize", DEFAULT_PRUNE_TARGET_GB);
96     SetPruneEnabled(settings.value("bPrune").toBool());
97 
98     if (!settings.contains("nDatabaseCache"))
99         settings.setValue("nDatabaseCache", (qint64)nDefaultDbCache);
100     if (!gArgs.SoftSetArg("-dbcache", settings.value("nDatabaseCache").toString().toStdString()))
101         addOverriddenOption("-dbcache");
102 
103     if (!settings.contains("nThreadsScriptVerif"))
104         settings.setValue("nThreadsScriptVerif", DEFAULT_SCRIPTCHECK_THREADS);
105     if (!gArgs.SoftSetArg("-par", settings.value("nThreadsScriptVerif").toString().toStdString()))
106         addOverriddenOption("-par");
107 
108     if (!settings.contains("strDataDir"))
109         settings.setValue("strDataDir", GUIUtil::getDefaultDataDirectory());
110 
111     // Wallet
112 #ifdef ENABLE_WALLET
113     if (!settings.contains("bSpendZeroConfChange"))
114         settings.setValue("bSpendZeroConfChange", true);
115     if (!gArgs.SoftSetBoolArg("-spendzeroconfchange", settings.value("bSpendZeroConfChange").toBool()))
116         addOverriddenOption("-spendzeroconfchange");
117 #endif
118 
119     // Network
120     if (!settings.contains("fUseUPnP"))
121         settings.setValue("fUseUPnP", DEFAULT_UPNP);
122     if (!gArgs.SoftSetBoolArg("-upnp", settings.value("fUseUPnP").toBool()))
123         addOverriddenOption("-upnp");
124 
125     if (!settings.contains("fListen"))
126         settings.setValue("fListen", DEFAULT_LISTEN);
127     if (!gArgs.SoftSetBoolArg("-listen", settings.value("fListen").toBool()))
128         addOverriddenOption("-listen");
129 
130     if (!settings.contains("fUseProxy"))
131         settings.setValue("fUseProxy", false);
132     if (!settings.contains("addrProxy"))
133         settings.setValue("addrProxy", GetDefaultProxyAddress());
134     // Only try to set -proxy, if user has enabled fUseProxy
135     if ((settings.value("fUseProxy").toBool() && !gArgs.SoftSetArg("-proxy", settings.value("addrProxy").toString().toStdString())))
136         addOverriddenOption("-proxy");
137     else if(!settings.value("fUseProxy").toBool() && !gArgs.GetArg("-proxy", "").empty())
138         addOverriddenOption("-proxy");
139 
140     if (!settings.contains("fUseSeparateProxyTor"))
141         settings.setValue("fUseSeparateProxyTor", false);
142     if (!settings.contains("addrSeparateProxyTor"))
143         settings.setValue("addrSeparateProxyTor", GetDefaultProxyAddress());
144     // Only try to set -onion, if user has enabled fUseSeparateProxyTor
145     if ((settings.value("fUseSeparateProxyTor").toBool() && !gArgs.SoftSetArg("-onion", settings.value("addrSeparateProxyTor").toString().toStdString())))
146         addOverriddenOption("-onion");
147     else if(!settings.value("fUseSeparateProxyTor").toBool() && !gArgs.GetArg("-onion", "").empty())
148         addOverriddenOption("-onion");
149 
150     // Display
151     if (!settings.contains("language"))
152         settings.setValue("language", "");
153     if (!gArgs.SoftSetArg("-lang", settings.value("language").toString().toStdString()))
154         addOverriddenOption("-lang");
155 
156     language = settings.value("language").toString();
157 }
158 
159 /** Helper function to copy contents from one QSettings to another.
160  * By using allKeys this also covers nested settings in a hierarchy.
161  */
CopySettings(QSettings & dst,const QSettings & src)162 static void CopySettings(QSettings& dst, const QSettings& src)
163 {
164     for (const QString& key : src.allKeys()) {
165         dst.setValue(key, src.value(key));
166     }
167 }
168 
169 /** Back up a QSettings to an ini-formatted file. */
BackupSettings(const fs::path & filename,const QSettings & src)170 static void BackupSettings(const fs::path& filename, const QSettings& src)
171 {
172     qInfo() << "Backing up GUI settings to" << GUIUtil::boostPathToQString(filename);
173     QSettings dst(GUIUtil::boostPathToQString(filename), QSettings::IniFormat);
174     dst.clear();
175     CopySettings(dst, src);
176 }
177 
Reset()178 void OptionsModel::Reset()
179 {
180     QSettings settings;
181 
182     // Backup old settings to chain-specific datadir for troubleshooting
183     BackupSettings(GetDataDir(true) / "guisettings.ini.bak", settings);
184 
185     // Save the strDataDir setting
186     QString dataDir = GUIUtil::getDefaultDataDirectory();
187     dataDir = settings.value("strDataDir", dataDir).toString();
188 
189     // Remove all entries from our QSettings object
190     settings.clear();
191 
192     // Set strDataDir
193     settings.setValue("strDataDir", dataDir);
194 
195     // Set that this was reset
196     settings.setValue("fReset", true);
197 
198     // default setting for OptionsModel::StartAtStartup - disabled
199     if (GUIUtil::GetStartOnSystemStartup())
200         GUIUtil::SetStartOnSystemStartup(false);
201 }
202 
rowCount(const QModelIndex & parent) const203 int OptionsModel::rowCount(const QModelIndex & parent) const
204 {
205     return OptionIDRowCount;
206 }
207 
208 struct ProxySetting {
209     bool is_set;
210     QString ip;
211     QString port;
212 };
213 
GetProxySetting(QSettings & settings,const QString & name)214 static ProxySetting GetProxySetting(QSettings &settings, const QString &name)
215 {
216     static const ProxySetting default_val = {false, DEFAULT_GUI_PROXY_HOST, QString("%1").arg(DEFAULT_GUI_PROXY_PORT)};
217     // Handle the case that the setting is not set at all
218     if (!settings.contains(name)) {
219         return default_val;
220     }
221     // contains IP at index 0 and port at index 1
222     QStringList ip_port = settings.value(name).toString().split(":", QString::SkipEmptyParts);
223     if (ip_port.size() == 2) {
224         return {true, ip_port.at(0), ip_port.at(1)};
225     } else { // Invalid: return default
226         return default_val;
227     }
228 }
229 
SetProxySetting(QSettings & settings,const QString & name,const ProxySetting & ip_port)230 static void SetProxySetting(QSettings &settings, const QString &name, const ProxySetting &ip_port)
231 {
232     settings.setValue(name, ip_port.ip + ":" + ip_port.port);
233 }
234 
GetDefaultProxyAddress()235 static const QString GetDefaultProxyAddress()
236 {
237     return QString("%1:%2").arg(DEFAULT_GUI_PROXY_HOST).arg(DEFAULT_GUI_PROXY_PORT);
238 }
239 
SetPruneEnabled(bool prune,bool force)240 void OptionsModel::SetPruneEnabled(bool prune, bool force)
241 {
242     QSettings settings;
243     settings.setValue("bPrune", prune);
244     const int64_t prune_target_mib = PruneGBtoMiB(settings.value("nPruneSize").toInt());
245     std::string prune_val = prune ? ToString(prune_target_mib) : "0";
246     if (force) {
247         gArgs.ForceSetArg("-prune", prune_val);
248         return;
249     }
250     if (!gArgs.SoftSetArg("-prune", prune_val)) {
251         addOverriddenOption("-prune");
252     }
253 }
254 
SetPruneTargetGB(int prune_target_gb,bool force)255 void OptionsModel::SetPruneTargetGB(int prune_target_gb, bool force)
256 {
257     const bool prune = prune_target_gb > 0;
258     if (prune) {
259         QSettings settings;
260         settings.setValue("nPruneSize", prune_target_gb);
261     }
262     SetPruneEnabled(prune, force);
263 }
264 
265 // read QSettings values and return them
data(const QModelIndex & index,int role) const266 QVariant OptionsModel::data(const QModelIndex & index, int role) const
267 {
268     if(role == Qt::EditRole)
269     {
270         QSettings settings;
271         switch(index.row())
272         {
273         case StartAtStartup:
274             return GUIUtil::GetStartOnSystemStartup();
275         case HideTrayIcon:
276             return fHideTrayIcon;
277         case MinimizeToTray:
278             return fMinimizeToTray;
279         case MapPortUPnP:
280 #ifdef USE_UPNP
281             return settings.value("fUseUPnP");
282 #else
283             return false;
284 #endif
285         case MinimizeOnClose:
286             return fMinimizeOnClose;
287 
288         // default proxy
289         case ProxyUse:
290             return settings.value("fUseProxy", false);
291         case ProxyIP:
292             return GetProxySetting(settings, "addrProxy").ip;
293         case ProxyPort:
294             return GetProxySetting(settings, "addrProxy").port;
295 
296         // separate Tor proxy
297         case ProxyUseTor:
298             return settings.value("fUseSeparateProxyTor", false);
299         case ProxyIPTor:
300             return GetProxySetting(settings, "addrSeparateProxyTor").ip;
301         case ProxyPortTor:
302             return GetProxySetting(settings, "addrSeparateProxyTor").port;
303 
304 #ifdef ENABLE_WALLET
305         case SpendZeroConfChange:
306             return settings.value("bSpendZeroConfChange");
307 #endif
308         case DisplayUnit:
309             return nDisplayUnit;
310         case ThirdPartyTxUrls:
311             return strThirdPartyTxUrls;
312         case Language:
313             return settings.value("language");
314         case CoinControlFeatures:
315             return fCoinControlFeatures;
316         case Prune:
317             return settings.value("bPrune");
318         case PruneSize:
319             return settings.value("nPruneSize");
320         case DatabaseCache:
321             return settings.value("nDatabaseCache");
322         case ThreadsScriptVerif:
323             return settings.value("nThreadsScriptVerif");
324         case Listen:
325             return settings.value("fListen");
326         default:
327             return QVariant();
328         }
329     }
330     return QVariant();
331 }
332 
333 // write QSettings values
setData(const QModelIndex & index,const QVariant & value,int role)334 bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, int role)
335 {
336     bool successful = true; /* set to false on parse error */
337     if(role == Qt::EditRole)
338     {
339         QSettings settings;
340         switch(index.row())
341         {
342         case StartAtStartup:
343             successful = GUIUtil::SetStartOnSystemStartup(value.toBool());
344             break;
345         case HideTrayIcon:
346             fHideTrayIcon = value.toBool();
347             settings.setValue("fHideTrayIcon", fHideTrayIcon);
348     		Q_EMIT hideTrayIconChanged(fHideTrayIcon);
349             break;
350         case MinimizeToTray:
351             fMinimizeToTray = value.toBool();
352             settings.setValue("fMinimizeToTray", fMinimizeToTray);
353             break;
354         case MapPortUPnP: // core option - can be changed on-the-fly
355             settings.setValue("fUseUPnP", value.toBool());
356             node().mapPort(value.toBool());
357             break;
358         case MinimizeOnClose:
359             fMinimizeOnClose = value.toBool();
360             settings.setValue("fMinimizeOnClose", fMinimizeOnClose);
361             break;
362 
363         // default proxy
364         case ProxyUse:
365             if (settings.value("fUseProxy") != value) {
366                 settings.setValue("fUseProxy", value.toBool());
367                 setRestartRequired(true);
368             }
369             break;
370         case ProxyIP: {
371             auto ip_port = GetProxySetting(settings, "addrProxy");
372             if (!ip_port.is_set || ip_port.ip != value.toString()) {
373                 ip_port.ip = value.toString();
374                 SetProxySetting(settings, "addrProxy", ip_port);
375                 setRestartRequired(true);
376             }
377         }
378         break;
379         case ProxyPort: {
380             auto ip_port = GetProxySetting(settings, "addrProxy");
381             if (!ip_port.is_set || ip_port.port != value.toString()) {
382                 ip_port.port = value.toString();
383                 SetProxySetting(settings, "addrProxy", ip_port);
384                 setRestartRequired(true);
385             }
386         }
387         break;
388 
389         // separate Tor proxy
390         case ProxyUseTor:
391             if (settings.value("fUseSeparateProxyTor") != value) {
392                 settings.setValue("fUseSeparateProxyTor", value.toBool());
393                 setRestartRequired(true);
394             }
395             break;
396         case ProxyIPTor: {
397             auto ip_port = GetProxySetting(settings, "addrSeparateProxyTor");
398             if (!ip_port.is_set || ip_port.ip != value.toString()) {
399                 ip_port.ip = value.toString();
400                 SetProxySetting(settings, "addrSeparateProxyTor", ip_port);
401                 setRestartRequired(true);
402             }
403         }
404         break;
405         case ProxyPortTor: {
406             auto ip_port = GetProxySetting(settings, "addrSeparateProxyTor");
407             if (!ip_port.is_set || ip_port.port != value.toString()) {
408                 ip_port.port = value.toString();
409                 SetProxySetting(settings, "addrSeparateProxyTor", ip_port);
410                 setRestartRequired(true);
411             }
412         }
413         break;
414 
415 #ifdef ENABLE_WALLET
416         case SpendZeroConfChange:
417             if (settings.value("bSpendZeroConfChange") != value) {
418                 settings.setValue("bSpendZeroConfChange", value);
419                 setRestartRequired(true);
420             }
421             break;
422 #endif
423         case DisplayUnit:
424             setDisplayUnit(value);
425             break;
426         case ThirdPartyTxUrls:
427             if (strThirdPartyTxUrls != value.toString()) {
428                 strThirdPartyTxUrls = value.toString();
429                 settings.setValue("strThirdPartyTxUrls", strThirdPartyTxUrls);
430                 setRestartRequired(true);
431             }
432             break;
433         case Language:
434             if (settings.value("language") != value) {
435                 settings.setValue("language", value);
436                 setRestartRequired(true);
437             }
438             break;
439         case CoinControlFeatures:
440             fCoinControlFeatures = value.toBool();
441             settings.setValue("fCoinControlFeatures", fCoinControlFeatures);
442             Q_EMIT coinControlFeaturesChanged(fCoinControlFeatures);
443             break;
444         case Prune:
445             if (settings.value("bPrune") != value) {
446                 settings.setValue("bPrune", value);
447                 setRestartRequired(true);
448             }
449             break;
450         case PruneSize:
451             if (settings.value("nPruneSize") != value) {
452                 settings.setValue("nPruneSize", value);
453                 setRestartRequired(true);
454             }
455             break;
456         case DatabaseCache:
457             if (settings.value("nDatabaseCache") != value) {
458                 settings.setValue("nDatabaseCache", value);
459                 setRestartRequired(true);
460             }
461             break;
462         case ThreadsScriptVerif:
463             if (settings.value("nThreadsScriptVerif") != value) {
464                 settings.setValue("nThreadsScriptVerif", value);
465                 setRestartRequired(true);
466             }
467             break;
468         case Listen:
469             if (settings.value("fListen") != value) {
470                 settings.setValue("fListen", value);
471                 setRestartRequired(true);
472             }
473             break;
474         default:
475             break;
476         }
477     }
478 
479     Q_EMIT dataChanged(index, index);
480 
481     return successful;
482 }
483 
484 /** Updates current unit in memory, settings and emits displayUnitChanged(newUnit) signal */
setDisplayUnit(const QVariant & value)485 void OptionsModel::setDisplayUnit(const QVariant &value)
486 {
487     if (!value.isNull())
488     {
489         QSettings settings;
490         nDisplayUnit = value.toInt();
491         settings.setValue("nDisplayUnit", nDisplayUnit);
492         Q_EMIT displayUnitChanged(nDisplayUnit);
493     }
494 }
495 
setRestartRequired(bool fRequired)496 void OptionsModel::setRestartRequired(bool fRequired)
497 {
498     QSettings settings;
499     return settings.setValue("fRestartRequired", fRequired);
500 }
501 
isRestartRequired() const502 bool OptionsModel::isRestartRequired() const
503 {
504     QSettings settings;
505     return settings.value("fRestartRequired", false).toBool();
506 }
507 
checkAndMigrate()508 void OptionsModel::checkAndMigrate()
509 {
510     // Migration of default values
511     // Check if the QSettings container was already loaded with this client version
512     QSettings settings;
513     static const char strSettingsVersionKey[] = "nSettingsVersion";
514     int settingsVersion = settings.contains(strSettingsVersionKey) ? settings.value(strSettingsVersionKey).toInt() : 0;
515     if (settingsVersion < CLIENT_VERSION)
516     {
517         // -dbcache was bumped from 100 to 300 in 0.13
518         // see https://github.com/bitcoin/bitcoin/pull/8273
519         // force people to upgrade to the new value if they are using 100MB
520         if (settingsVersion < 130000 && settings.contains("nDatabaseCache") && settings.value("nDatabaseCache").toLongLong() == 100)
521             settings.setValue("nDatabaseCache", (qint64)nDefaultDbCache);
522 
523         settings.setValue(strSettingsVersionKey, CLIENT_VERSION);
524     }
525 
526     // Overwrite the 'addrProxy' setting in case it has been set to an illegal
527     // default value (see issue #12623; PR #12650).
528     if (settings.contains("addrProxy") && settings.value("addrProxy").toString().endsWith("%2")) {
529         settings.setValue("addrProxy", GetDefaultProxyAddress());
530     }
531 
532     // Overwrite the 'addrSeparateProxyTor' setting in case it has been set to an illegal
533     // default value (see issue #12623; PR #12650).
534     if (settings.contains("addrSeparateProxyTor") && settings.value("addrSeparateProxyTor").toString().endsWith("%2")) {
535         settings.setValue("addrSeparateProxyTor", GetDefaultProxyAddress());
536     }
537 }
538