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