1 /* Ricochet - https://ricochet.im/
2  * Copyright (C) 2014, John Brooks <john.brooks@dereferenced.net>
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *    * Redistributions of source code must retain the above copyright
9  *      notice, this list of conditions and the following disclaimer.
10  *
11  *    * Redistributions in binary form must reproduce the above
12  *      copyright notice, this list of conditions and the following disclaimer
13  *      in the documentation and/or other materials provided with the
14  *      distribution.
15  *
16  *    * Neither the names of the copyright owners nor the names of its
17  *      contributors may be used to endorse or promote products derived from
18  *      this software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 
33 #include "Settings.h"
34 #include <QCoreApplication>
35 #include <QJsonDocument>
36 #include <QJsonParseError>
37 #include <QSaveFile>
38 #include <QFile>
39 #include <QDir>
40 #include <QFileInfo>
41 #include <QTimer>
42 #include <QDebug>
43 #include <QPointer>
44 
45 class SettingsFilePrivate : public QObject
46 {
47     Q_OBJECT
48 
49 public:
50     SettingsFile *q;
51     QString filePath;
52     QString errorMessage;
53     QTimer syncTimer;
54     QJsonObject jsonRoot;
55     SettingsObject *rootObject;
56 
57     SettingsFilePrivate(SettingsFile *qp);
58     virtual ~SettingsFilePrivate();
59 
60     void reset();
61     void setError(const QString &message);
62     bool checkDirPermissions(const QString &path);
63     bool readFile();
64     bool writeFile();
65 
66     static QStringList splitPath(const QString &input, bool &ok);
67     QJsonValue read(const QJsonObject &base, const QStringList &path);
68     bool write(const QStringList &path, const QJsonValue &value);
69 
70 signals:
71     void modified(const QStringList &path, const QJsonValue &value);
72 
73 private slots:
74     void sync();
75 };
76 
SettingsFile(QObject * parent)77 SettingsFile::SettingsFile(QObject *parent)
78     : QObject(parent), d(new SettingsFilePrivate(this))
79 {
80     d->rootObject = new SettingsObject(this, QString());
81 }
82 
~SettingsFile()83 SettingsFile::~SettingsFile()
84 {
85 }
86 
SettingsFilePrivate(SettingsFile * qp)87 SettingsFilePrivate::SettingsFilePrivate(SettingsFile *qp)
88     : QObject(qp)
89     , q(qp)
90     , rootObject(0)
91 {
92     syncTimer.setInterval(0);
93     syncTimer.setSingleShot(true);
94     connect(&syncTimer, &QTimer::timeout, this, &SettingsFilePrivate::sync);
95 }
96 
~SettingsFilePrivate()97 SettingsFilePrivate::~SettingsFilePrivate()
98 {
99     if (syncTimer.isActive())
100         sync();
101     delete rootObject;
102 }
103 
reset()104 void SettingsFilePrivate::reset()
105 {
106     filePath.clear();
107     errorMessage.clear();
108 
109     jsonRoot = QJsonObject();
110     emit modified(QStringList(), jsonRoot);
111 }
112 
filePath() const113 QString SettingsFile::filePath() const
114 {
115     return d->filePath;
116 }
117 
setFilePath(const QString & filePath)118 bool SettingsFile::setFilePath(const QString &filePath)
119 {
120     if (d->filePath == filePath)
121         return hasError();
122 
123     d->reset();
124     d->filePath = filePath;
125 
126     QFileInfo fileInfo(filePath);
127     QDir dir(fileInfo.path());
128     if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
129         d->setError(QStringLiteral("Cannot create directory: %1").arg(dir.path()));
130         return false;
131     }
132     d->checkDirPermissions(fileInfo.path());
133 
134     if (!d->readFile())
135         return false;
136 
137     return true;
138 }
139 
errorMessage() const140 QString SettingsFile::errorMessage() const
141 {
142     return d->errorMessage;
143 }
144 
hasError() const145 bool SettingsFile::hasError() const
146 {
147     return !d->errorMessage.isEmpty();
148 }
149 
setError(const QString & message)150 void SettingsFilePrivate::setError(const QString &message)
151 {
152     errorMessage = message;
153     emit q->error();
154 }
155 
checkDirPermissions(const QString & path)156 bool SettingsFilePrivate::checkDirPermissions(const QString &path)
157 {
158     static QFile::Permissions desired = QFileDevice::ReadUser | QFileDevice::WriteUser | QFileDevice::ExeUser;
159     static QFile::Permissions ignored = QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner;
160 
161     QFile file(path);
162     if ((file.permissions() & ~ignored) != desired) {
163         qDebug() << "Correcting permissions on configuration directory";
164         if (!file.setPermissions(desired)) {
165             qWarning() << "Correcting permissions on configuration directory failed";
166             return false;
167         }
168     }
169 
170     return true;
171 }
172 
root()173 SettingsObject *SettingsFile::root()
174 {
175     return d->rootObject;
176 }
177 
root() const178 const SettingsObject *SettingsFile::root() const
179 {
180     return d->rootObject;
181 }
182 
sync()183 void SettingsFilePrivate::sync()
184 {
185     if (filePath.isEmpty())
186         return;
187 
188     syncTimer.stop();
189     writeFile();
190 }
191 
readFile()192 bool SettingsFilePrivate::readFile()
193 {
194     QFile file(filePath);
195     if (!file.open(QIODevice::ReadWrite)) {
196         setError(file.errorString());
197         return false;
198     }
199 
200     QByteArray data = file.readAll();
201     if (data.isEmpty() && (file.error() != QFileDevice::NoError || file.size() > 0)) {
202         setError(file.errorString());
203         return false;
204     }
205 
206     if (data.isEmpty()) {
207         jsonRoot = QJsonObject();
208         return true;
209     }
210 
211     QJsonParseError parseError;
212     QJsonDocument document = QJsonDocument::fromJson(data, &parseError);
213     if (document.isNull()) {
214         setError(parseError.errorString());
215         return false;
216     }
217 
218     if (!document.isObject()) {
219         setError(QStringLiteral("Invalid configuration file (expected object)"));
220         return false;
221     }
222 
223     jsonRoot = document.object();
224 
225     emit modified(QStringList(), jsonRoot);
226     return true;
227 }
228 
writeFile()229 bool SettingsFilePrivate::writeFile()
230 {
231     QSaveFile file(filePath);
232     if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
233         setError(file.errorString());
234         return false;
235     }
236 
237     QJsonDocument document(jsonRoot);
238     QByteArray data = document.toJson();
239     if (data.isEmpty() && !document.isEmpty()) {
240         setError(QStringLiteral("Encoding failure"));
241         return false;
242     }
243 
244     if (file.write(data) < data.size() || !file.commit()) {
245         setError(file.errorString());
246         return false;
247     }
248 
249     return true;
250 }
251 
splitPath(const QString & input,bool & ok)252 QStringList SettingsFilePrivate::splitPath(const QString &input, bool &ok)
253 {
254     QStringList components = input.split(QLatin1Char('.'));
255 
256     // Allow a leading '.' to simplify concatenation
257     if (!components.isEmpty() && components.first().isEmpty())
258         components.takeFirst();
259 
260     // No other empty components, including a trailing .
261     foreach (const QString &word, components) {
262         if (word.isEmpty()) {
263             ok = false;
264             return QStringList();
265         }
266     }
267 
268     ok = true;
269     return components;
270 }
271 
read(const QJsonObject & base,const QStringList & path)272 QJsonValue SettingsFilePrivate::read(const QJsonObject &base, const QStringList &path)
273 {
274     QJsonValue current = base;
275 
276     foreach (const QString &key, path) {
277         QJsonObject object = current.toObject();
278         if (object.isEmpty() || (current = object.value(key)).isUndefined())
279             return QJsonValue::Undefined;
280     }
281 
282     return current;
283 }
284 
285 // Compare two QJsonValue to find keys that have changed,
286 // recursing into objects and building paths as necessary.
287 typedef QList<QPair<QStringList, QJsonValue> > ModifiedList;
findModifiedRecursive(ModifiedList & modified,const QStringList & path,const QJsonValue & oldValue,const QJsonValue & newValue)288 static void findModifiedRecursive(ModifiedList &modified, const QStringList &path, const QJsonValue &oldValue, const QJsonValue &newValue)
289 {
290     if (oldValue.isObject() || newValue.isObject()) {
291         // If either is a non-object type, this returns an empty object
292         QJsonObject oldObject = oldValue.toObject();
293         QJsonObject newObject = newValue.toObject();
294 
295         // Iterate keys of the original object and compare to new
296         for (QJsonObject::iterator it = oldObject.begin(); it != oldObject.end(); it++) {
297             QJsonValue newSubValue = newObject.value(it.key());
298             if (*it == newSubValue)
299                 continue;
300 
301             if ((*it).isObject() || newSubValue.isObject())
302                 findModifiedRecursive(modified, QStringList() << path << it.key(), *it, newSubValue);
303             else
304                 modified.append(qMakePair(QStringList() << path << it.key(), newSubValue));
305         }
306 
307         // Iterate keys of the new object that may not be in original
308         for (QJsonObject::iterator it = newObject.begin(); it != newObject.end(); it++) {
309             if (oldObject.contains(it.key()))
310                 continue;
311 
312             if ((*it).isObject())
313                 findModifiedRecursive(modified, QStringList() << path << it.key(), QJsonValue::Undefined, it.value());
314             else
315                 modified.append(qMakePair(QStringList() << path << it.key(), it.value()));
316         }
317     } else
318         modified.append(qMakePair(path, newValue));
319 }
320 
write(const QStringList & path,const QJsonValue & value)321 bool SettingsFilePrivate::write(const QStringList &path, const QJsonValue &value)
322 {
323     typedef QVarLengthArray<QPair<QString,QJsonObject> > ObjectStack;
324     ObjectStack stack;
325     QJsonValue current = jsonRoot;
326     QJsonValue originalValue;
327     QString currentKey;
328 
329     foreach (const QString &key, path) {
330         const QJsonObject &parent = current.toObject();
331         stack.append(qMakePair(currentKey, parent));
332         current = parent.value(key);
333         currentKey = key;
334     }
335 
336     // Stack now contains parent objects starting with the root, and current
337     // is the old value. Write back changes in reverse.
338     if (current == value)
339         return false;
340     originalValue = current;
341     current = value;
342 
343     ObjectStack::const_iterator it = stack.end(), begin = stack.begin();
344     while (it != begin) {
345         --it;
346         QJsonObject update = it->second;
347         update.insert(currentKey, current);
348         current = update;
349         currentKey = it->first;
350     }
351 
352     // current is now the updated jsonRoot
353     jsonRoot = current.toObject();
354     syncTimer.start();
355 
356     ModifiedList modified;
357     findModifiedRecursive(modified, path, originalValue, value);
358 
359     for (ModifiedList::iterator it = modified.begin(); it != modified.end(); it++)
360         emit this->modified(it->first, it->second);
361 
362     return true;
363 }
364 
365 class SettingsObjectPrivate : public QObject
366 {
367     Q_OBJECT
368 
369 public:
370     explicit SettingsObjectPrivate(SettingsObject *q);
371 
372     SettingsObject *q;
373     SettingsFile *file;
374     QStringList path;
375     QJsonObject object;
376     bool invalid;
377 
378     void setFile(SettingsFile *file);
379 
380 public slots:
381     void modified(const QStringList &absolutePath, const QJsonValue &value);
382 };
383 
SettingsObject(QObject * parent)384 SettingsObject::SettingsObject(QObject *parent)
385     : QObject(parent)
386     , d(new SettingsObjectPrivate(this))
387 {
388     d->setFile(defaultFile());
389     if (d->file)
390         setPath(QString());
391 }
392 
SettingsObject(const QString & path,QObject * parent)393 SettingsObject::SettingsObject(const QString &path, QObject *parent)
394     : QObject(parent)
395     , d(new SettingsObjectPrivate(this))
396 {
397     d->setFile(defaultFile());
398     setPath(path);
399 }
400 
SettingsObject(SettingsFile * file,const QString & path,QObject * parent)401 SettingsObject::SettingsObject(SettingsFile *file, const QString &path, QObject *parent)
402     : QObject(parent)
403     , d(new SettingsObjectPrivate(this))
404 {
405     d->setFile(file);
406     setPath(path);
407 }
408 
SettingsObject(SettingsObject * base,const QString & path,QObject * parent)409 SettingsObject::SettingsObject(SettingsObject *base, const QString &path, QObject *parent)
410     : QObject(parent)
411     , d(new SettingsObjectPrivate(this))
412 {
413     d->setFile(base->d->file);
414     setPath(base->path() + QLatin1Char('.') + path);
415 }
416 
SettingsObjectPrivate(SettingsObject * qp)417 SettingsObjectPrivate::SettingsObjectPrivate(SettingsObject *qp)
418     : QObject(qp)
419     , q(qp)
420     , file(0)
421     , invalid(true)
422 {
423 }
424 
setFile(SettingsFile * value)425 void SettingsObjectPrivate::setFile(SettingsFile *value)
426 {
427     if (file == value)
428         return;
429 
430     if (file)
431         disconnect(file, 0, this, 0);
432     file = value;
433     if (file)
434         connect(file->d, &SettingsFilePrivate::modified, this, &SettingsObjectPrivate::modified);
435 }
436 
437 // Emit SettingsObject::modified with a relative path if path is matched
modified(const QStringList & key,const QJsonValue & value)438 void SettingsObjectPrivate::modified(const QStringList &key, const QJsonValue &value)
439 {
440     if (key.size() < path.size())
441         return;
442 
443     for (int i = 0; i < path.size(); i++) {
444         if (path[i] != key[i])
445             return;
446     }
447 
448     object = file->d->read(file->d->jsonRoot, path).toObject();
449     emit q->modified(QStringList(key.mid(path.size())).join(QLatin1Char('.')), value);
450     emit q->dataChanged();
451 }
452 
453 static QPointer<SettingsFile> defaultObjectFile;
454 
defaultFile()455 SettingsFile *SettingsObject::defaultFile()
456 {
457     return defaultObjectFile;
458 }
459 
setDefaultFile(SettingsFile * file)460 void SettingsObject::setDefaultFile(SettingsFile *file)
461 {
462     defaultObjectFile = file;
463 }
464 
path() const465 QString SettingsObject::path() const
466 {
467     return d->path.join(QLatin1Char('.'));
468 }
469 
setPath(const QString & input)470 void SettingsObject::setPath(const QString &input)
471 {
472     bool ok = false;
473     QStringList newPath = SettingsFilePrivate::splitPath(input, ok);
474     if (!ok) {
475         d->invalid = true;
476         d->path.clear();
477         d->object = QJsonObject();
478 
479         emit pathChanged();
480         emit dataChanged();
481         return;
482     }
483 
484     if (!d->invalid && d->path == newPath)
485         return;
486 
487     d->path = newPath;
488     if (d->file) {
489         d->invalid = false;
490         d->object = d->file->d->read(d->file->d->jsonRoot, d->path).toObject();
491         emit dataChanged();
492     }
493 
494     emit pathChanged();
495 }
496 
data() const497 QJsonObject SettingsObject::data() const
498 {
499     return d->object;
500 }
501 
setData(const QJsonObject & input)502 void SettingsObject::setData(const QJsonObject &input)
503 {
504     if (d->invalid || d->object == input)
505         return;
506 
507     d->object = input;
508     d->file->d->write(d->path, d->object);
509 }
510 
read(const QString & key,const QJsonValue & defaultValue) const511 QJsonValue SettingsObject::read(const QString &key, const QJsonValue &defaultValue) const
512 {
513     bool ok = false;
514     QStringList splitKey = SettingsFilePrivate::splitPath(key, ok);
515     if (d->invalid || !ok || splitKey.isEmpty()) {
516         qDebug() << "Invalid settings read of path" << key;
517         return defaultValue;
518     }
519 
520     QJsonValue ret = d->file->d->read(d->object, splitKey);
521     if (ret.isUndefined())
522         ret = defaultValue;
523     return ret;
524 }
525 
write(const QString & key,const QJsonValue & value)526 void SettingsObject::write(const QString &key, const QJsonValue &value)
527 {
528     bool ok = false;
529     QStringList splitKey = SettingsFilePrivate::splitPath(key, ok);
530     if (d->invalid || !ok || splitKey.isEmpty()) {
531         qDebug() << "Invalid settings write of path" << key;
532         return;
533     }
534 
535     splitKey = d->path + splitKey;
536     d->file->d->write(splitKey, value);
537 }
538 
unset(const QString & key)539 void SettingsObject::unset(const QString &key)
540 {
541     write(key, QJsonValue());
542 }
543 
undefine()544 void SettingsObject::undefine()
545 {
546     if (d->invalid)
547         return;
548 
549     d->object = QJsonObject();
550     d->file->d->write(d->path, QJsonValue::Undefined);
551 }
552 
553 #include "Settings.moc"
554