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