1 /*
2     Copyright © 2017-2019 by The qTox Project Contributors
3 
4     This file is part of qTox, a Qt-based graphical interface for Tox.
5 
6     qTox is libre software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     qTox is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with qTox.  If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "profileinfo.h"
21 #include "src/core/core.h"
22 #include "src/nexus.h"
23 #include "src/persistence/profile.h"
24 #include "src/persistence/settings.h"
25 #include "src/model/toxclientstandards.h"
26 
27 #include <QApplication>
28 #include <QBuffer>
29 #include <QClipboard>
30 #include <QFile>
31 #include <QImageReader>
32 
33 /**
34  * @class ProfileInfo
35  * @brief Implement interface, that provides invormation about self profile.
36  * Also, provide methods to work with profile file.
37  * @note Should be used only when QAppliaction constructed.
38  */
39 
40 /**
41  * @brief ProfileInfo constructor.
42  * @param core Pointer to Tox Core.
43  * @param profile Pointer to Profile.
44  * @note All pointers parameters shouldn't be null.
45  */
ProfileInfo(Core * core,Profile * profile)46 ProfileInfo::ProfileInfo(Core* core, Profile* profile)
47     : profile{profile}
48     , core{core}
49 {
50     connect(core, &Core::idSet, this, &ProfileInfo::idChanged);
51     connect(core, &Core::usernameSet, this, &ProfileInfo::usernameChanged);
52     connect(core, &Core::statusMessageSet, this, &ProfileInfo::statusMessageChanged);
53 }
54 
55 /**
56  * @brief Set a user password for profile.
57  * @param password New password.
58  * @return True on success, false otherwise.
59  */
setPassword(const QString & password)60 bool ProfileInfo::setPassword(const QString& password)
61 {
62     QString errorMsg = profile->setPassword(password);
63     return errorMsg.isEmpty();
64 }
65 
66 /**
67  * @brief Delete a user password for profile.
68  * @return True on success, false otherwise.
69  */
deletePassword()70 bool ProfileInfo::deletePassword()
71 {
72     QString errorMsg = profile->setPassword("");
73     return errorMsg.isEmpty();
74 }
75 
76 /**
77  * @brief Check if current profile is encrypted.
78  * @return True if encrypted, false otherwise.
79  */
isEncrypted() const80 bool ProfileInfo::isEncrypted() const
81 {
82     return profile->isEncrypted();
83 }
84 
85 /**
86  * @brief Copy self ToxId to clipboard.
87  */
copyId() const88 void ProfileInfo::copyId() const
89 {
90     ToxId selfId = core->getSelfId();
91     QString txt = selfId.toString();
92     QClipboard* clip = QApplication::clipboard();
93     clip->setText(txt, QClipboard::Clipboard);
94     if (clip->supportsSelection()) {
95         clip->setText(txt, QClipboard::Selection);
96     }
97 }
98 
99 /**
100  * @brief Set self user name.
101  * @param name New name.
102  */
setUsername(const QString & name)103 void ProfileInfo::setUsername(const QString& name)
104 {
105     core->setUsername(name);
106 }
107 
108 /**
109  * @brief Set self status message.
110  * @param status New status message.
111  */
setStatusMessage(const QString & status)112 void ProfileInfo::setStatusMessage(const QString& status)
113 {
114     core->setStatusMessage(status);
115 }
116 
117 /**
118  * @brief Get name of tox profile file.
119  * @return Profile name.
120  */
getProfileName() const121 QString ProfileInfo::getProfileName() const
122 {
123     return profile->getName();
124 }
125 
126 /**
127  * @brief Remove characters not supported for profile name from string.
128  * @param src Source string.
129  * @return Sanitized string.
130  */
sanitize(const QString & src)131 static QString sanitize(const QString& src)
132 {
133     QString name = src;
134     // these are pretty much Windows banned filename characters
135     QList<QChar> banned{'/', '\\', ':', '<', '>', '"', '|', '?', '*'};
136     for (QChar c : banned) {
137         name.replace(c, '_');
138     }
139 
140     // also remove leading and trailing periods
141     if (name[0] == '.') {
142         name[0] = '_';
143     }
144 
145     if (name.endsWith('.')) {
146         name[name.length() - 1] = '_';
147     }
148 
149     return name;
150 }
151 
152 /**
153  * @brief Rename profile file.
154  * @param name New profile name.
155  * @return Result code of rename operation.
156  */
renameProfile(const QString & name)157 IProfileInfo::RenameResult ProfileInfo::renameProfile(const QString& name)
158 {
159     QString cur = profile->getName();
160     if (name.isEmpty()) {
161         return RenameResult::EmptyName;
162     }
163 
164     QString newName = sanitize(name);
165 
166     if (Profile::exists(newName)) {
167         return RenameResult::ProfileAlreadyExists;
168     }
169 
170     if (!profile->rename(name)) {
171         return RenameResult::Error;
172     }
173 
174     return RenameResult::OK;
175 }
176 
177 // TODO: Find out what is dangerous?
178 /**
179  * @brief Dangerous way to find out if a path is writable.
180  * @param filepath Path to file which should be deleted.
181  * @return True, if file writeable, false otherwise.
182  */
tryRemoveFile(const QString & filepath)183 static bool tryRemoveFile(const QString& filepath)
184 {
185     QFile tmp(filepath);
186     bool writable = tmp.open(QIODevice::WriteOnly);
187     tmp.remove();
188     return writable;
189 }
190 
191 /**
192  * @brief Save profile in custom place.
193  * @param path Path to save profile.
194  * @return Result code of save operation.
195  */
exportProfile(const QString & path) const196 IProfileInfo::SaveResult ProfileInfo::exportProfile(const QString& path) const
197 {
198     QString current = profile->getName() + Core::TOX_EXT;
199     if (path.isEmpty()) {
200         return SaveResult::EmptyPath;
201     }
202 
203     if (!tryRemoveFile(path)) {
204         return SaveResult::NoWritePermission;
205     }
206 
207     if (!QFile::copy(Settings::getInstance().getSettingsDirPath() + current, path)) {
208         return SaveResult::Error;
209     }
210 
211     return SaveResult::OK;
212 }
213 
214 /**
215  * @brief Remove profile.
216  * @return List of files, which couldn't be removed automaticaly.
217  */
removeProfile()218 QStringList ProfileInfo::removeProfile()
219 {
220     QStringList manualDeleteFiles = profile->remove();
221     QMetaObject::invokeMethod(&Nexus::getInstance(), "showLogin");
222     return manualDeleteFiles;
223 }
224 
225 /**
226  * @brief Log out from current profile.
227  */
logout()228 void ProfileInfo::logout()
229 {
230     // TODO(kriby): Refactor all of these invokeMethod calls with connect() properly when possible
231     Settings::getInstance().saveGlobal();
232     QMetaObject::invokeMethod(&Nexus::getInstance(), "showLogin",
233                               Q_ARG(QString, Settings::getInstance().getCurrentProfile()));
234 }
235 
236 /**
237  * @brief Copy image to clipboard.
238  * @param image Image to copy.
239  */
copyQr(const QImage & image) const240 void ProfileInfo::copyQr(const QImage& image) const
241 {
242     QApplication::clipboard()->setImage(image);
243 }
244 
245 /**
246  * @brief Save image to file.
247  * @param image Image to save.
248  * @param path Path to save.
249  * @return Result code of save operation.
250  */
saveQr(const QImage & image,const QString & path) const251 IProfileInfo::SaveResult ProfileInfo::saveQr(const QImage& image, const QString& path) const
252 {
253     QString current = profile->getName() + ".png";
254     if (path.isEmpty()) {
255         return SaveResult::EmptyPath;
256     }
257 
258     if (!tryRemoveFile(path)) {
259         return SaveResult::NoWritePermission;
260     }
261 
262     // nullptr - image format same as file extension,
263     // 75-quality, png file is ~6.3kb
264     if (!image.save(path, nullptr, 75)) {
265         return SaveResult::Error;
266     }
267 
268     return SaveResult::OK;
269 }
270 
271 /**
272  * @brief Convert QImage to png image.
273  * @param pic Picture to convert.
274  * @return Byte array with png image.
275  */
picToPng(const QImage & pic)276 QByteArray picToPng(const QImage& pic)
277 {
278     QByteArray bytes;
279     QBuffer buffer(&bytes);
280     buffer.open(QIODevice::WriteOnly);
281     pic.save(&buffer, "PNG");
282     buffer.close();
283     return bytes;
284 }
285 
286 /**
287  * @brief Set self avatar.
288  * @param path Path to image, which should be the new avatar.
289  * @return Code of set avatar operation.
290  */
setAvatar(const QString & path)291 IProfileInfo::SetAvatarResult ProfileInfo::setAvatar(const QString& path)
292 {
293     if (path.isEmpty()) {
294         return SetAvatarResult::EmptyPath;
295     }
296 
297     QFile file(path);
298     file.open(QIODevice::ReadOnly);
299     if (!file.isOpen()) {
300         return SetAvatarResult::CanNotOpen;
301     }
302     QByteArray avatar;
303     const auto err = createAvatarFromFile(file, avatar);
304     if (err == SetAvatarResult::OK) {
305         profile->setAvatar(avatar);
306     }
307     return err;
308 }
309 
310 /**
311  * @brief Create an avatar from an image file
312  * @param file Image file, which should be the new avatar.
313  * @param avatar Output avatar of correct file type and size.
314  * @return SetAvatarResult
315  */
createAvatarFromFile(QFile & file,QByteArray & avatar)316 IProfileInfo::SetAvatarResult ProfileInfo::createAvatarFromFile(QFile& file, QByteArray& avatar)
317 {
318     QByteArray fileContents{file.readAll()};
319     auto err = byteArrayToPng(fileContents, avatar);
320     if (err != SetAvatarResult::OK) {
321         return err;
322     }
323 
324     err = scalePngToAvatar(avatar);
325     return err;
326 }
327 
328 /**
329  * @brief Create a png from image data
330  * @param inData byte array from an image file.
331  * @param outPng byte array which the png will be written to.
332  * @return SetAvatarResult
333  */
byteArrayToPng(QByteArray inData,QByteArray & outPng)334 IProfileInfo::SetAvatarResult ProfileInfo::byteArrayToPng(QByteArray inData, QByteArray& outPng)
335 {
336     QBuffer inBuffer{&inData};
337     QImageReader reader{&inBuffer};
338     QImage image;
339     const auto format = reader.format();
340     // read whole image even if we're not going to use the QImage, to make sure the image is valid
341     if (!reader.read(&image)) {
342         return SetAvatarResult::CanNotRead;
343     }
344 
345     if (format == "png") {
346         // FIXME: re-encode the png even though inData is already valid. This strips the metadata
347         // since we don't have a good png metadata stripping method currently.
348         outPng = picToPng(image);
349     } else {
350         outPng = picToPng(image);
351     }
352     return SetAvatarResult::OK;
353 }
354 
355 /*
356  * @brief Scale a png to an acceptable file size.
357  * @param avatar byte array containing the avatar.
358  * @return SetAvatarResult
359  */
scalePngToAvatar(QByteArray & avatar)360 IProfileInfo::SetAvatarResult ProfileInfo::scalePngToAvatar(QByteArray& avatar)
361 {
362     // We do a first rescale to 256x256 in case the image was huge, then keep tryng from here
363     constexpr int scaleSizes[] = {256, 128, 64, 32};
364 
365     for (auto scaleSize : scaleSizes) {
366         if (ToxClientStandards::IsValidAvatarSize(avatar.size()))
367             break;
368         QImage image;
369         image.loadFromData(avatar);
370         image = image.scaled(scaleSize, scaleSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
371         avatar = picToPng(image);
372     }
373 
374     // If this happens, you're really doing it on purpose.
375     if (!ToxClientStandards::IsValidAvatarSize(avatar.size())) {
376         return SetAvatarResult::TooLarge;
377     }
378     return SetAvatarResult::OK;
379 }
380 
381 /**
382  * @brief Remove self avatar.
383  */
removeAvatar()384 void ProfileInfo::removeAvatar()
385 {
386     profile->removeSelfAvatar();
387 }
388