1 /*
2 kopeteavatarmanager.cpp - Global avatar manager
3
4 Copyright (c) 2007 by Michaël Larouche <larouche@kde.org>
5
6 Kopete (c) 2002-2007 by the Kopete developers <kopete-devel@kde.org>
7
8 *************************************************************************
9 * *
10 * This library is free software; you can redistribute it and/or *
11 * modify it under the terms of the GNU Lesser General Public *
12 * License as published by the Free Software Foundation; either *
13 * version 2 of the License, or (at your option) any later version. *
14 * *
15 *************************************************************************
16 */
17 #include "kopeteavatarmanager.h"
18
19 // Qt includes
20
21 #include <QBuffer>
22 #include <QFile>
23 #include <QPointer>
24 #include <QStringList>
25 #include <QDir>
26 #include <QDebug>
27 #include <QCryptographicHash>
28 #include <QUrl>
29 #include <QPainter>
30 #include <QImageReader>
31 #include <QStandardPaths>
32
33 // KDE includes
34 #include <KSharedConfig>
35 #include <kconfig.h>
36 #include <kcodecs.h>
37 #include <kio/job.h>
38 #include <kio/netaccess.h>
39
40 // Kopete includes
41 #include <kopetecontact.h>
42 #include <kopeteprotocol.h>
43 #include <kopeteaccount.h>
44
45 namespace Kopete {
46 //BEGIN AvatarManager
47 AvatarManager *AvatarManager::s_self = 0;
48
self()49 AvatarManager *AvatarManager::self()
50 {
51 if (!s_self) {
52 s_self = new AvatarManager;
53 }
54 return s_self;
55 }
56
57 class AvatarManager::Private
58 {
59 public:
60 QUrl baseDir;
61
62 /**
63 * Create directory if needed
64 * @param directory URL of the directory to create
65 */
66 void createDirectory(const QUrl &directory);
67
68 /**
69 * Scale the given image to 96x96.
70 * @param source Original image
71 */
72 QImage scaleImage(const QImage &source);
73 };
74
75 static const QString UserDir(QStringLiteral("User"));
76 static const QString ContactDir(QStringLiteral("Contacts"));
77 static const QString AvatarConfig(QStringLiteral("avatarconfig.rc"));
78
AvatarManager(QObject * parent)79 AvatarManager::AvatarManager(QObject *parent)
80 : QObject(parent)
81 , d(new Private)
82 {
83 // Locate avatar data dir on disk
84 const QString avatarPath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1String("/avatars");
85 d->createDirectory(QUrl::fromLocalFile(avatarPath));
86 d->baseDir = QUrl::fromLocalFile(avatarPath);
87 }
88
~AvatarManager()89 AvatarManager::~AvatarManager()
90 {
91 s_self = nullptr;
92 delete d;
93 }
94
add(Kopete::AvatarManager::AvatarEntry newEntry)95 Kopete::AvatarManager::AvatarEntry AvatarManager::add(Kopete::AvatarManager::AvatarEntry newEntry)
96 {
97 Q_ASSERT(!newEntry.name.isEmpty());
98
99 QUrl avatarUrl = d->baseDir;
100
101 // First find where to save the file
102 switch (newEntry.category) {
103 case AvatarManager::User:
104 avatarUrl = avatarUrl.adjusted(QUrl::StripTrailingSlash);
105 avatarUrl.setPath(avatarUrl.path() + '/' + (UserDir));
106 d->createDirectory(avatarUrl);
107 break;
108 case AvatarManager::Contact:
109 avatarUrl = avatarUrl.adjusted(QUrl::StripTrailingSlash);
110 avatarUrl.setPath(avatarUrl.path() + '/' + (ContactDir));
111 d->createDirectory(avatarUrl);
112 // Use the account id for sub directory
113 if (newEntry.contact && newEntry.contact->account()) {
114 QString accountName = newEntry.contact->account()->accountId();
115 QString protocolName = newEntry.contact->account()->protocol()->pluginId();
116 avatarUrl = avatarUrl.adjusted(QUrl::StripTrailingSlash);
117 avatarUrl.setPath(avatarUrl.path() + '/' + (protocolName));
118 d->createDirectory(avatarUrl);
119 avatarUrl = avatarUrl.adjusted(QUrl::StripTrailingSlash);
120 avatarUrl.setPath(avatarUrl.path() + '/' + (accountName));
121 d->createDirectory(avatarUrl);
122 }
123 break;
124 default:
125 break;
126 }
127
128 QUrl dataUrl(avatarUrl);
129
130 qCDebug(LIBKOPETE_LOG) << "Base directory: " << avatarUrl.toLocalFile();
131
132 // Second, open the avatar configuration in current directory.
133 QUrl configUrl = avatarUrl;
134 configUrl = configUrl.adjusted(QUrl::StripTrailingSlash);
135 configUrl.setPath(configUrl.path() + '/' + (AvatarConfig));
136
137 QByteArray data = newEntry.data;
138 QImage avatar = newEntry.image;
139
140 if (!data.isNull()) {
141 avatar.loadFromData(data);
142 } else if (!newEntry.dataPath.isEmpty()) {
143 QFile f(newEntry.dataPath);
144 f.open(QIODevice::ReadOnly);
145 data = f.readAll();
146 f.close();
147
148 avatar.loadFromData(data);
149 } else if (!avatar.isNull()) {
150 QByteArray tempArray;
151 QBuffer tempBuffer(&tempArray);
152 tempBuffer.open(QIODevice::WriteOnly);
153 avatar.save(&tempBuffer, "PNG");
154
155 data = tempArray;
156 } else if (!newEntry.path.isEmpty()) {
157 avatar = QImage(newEntry.path);
158
159 QFile f(newEntry.path);
160 f.open(QIODevice::ReadOnly);
161 data = f.readAll();
162 f.close();
163 } else {
164 qCDebug(LIBKOPETE_LOG) << "Warning: No valid image source!";
165 }
166
167 // Scale avatar
168 avatar = d->scaleImage(avatar);
169
170 QString avatarFilename;
171
172 // for the contact avatar, save it with the contactId + .png
173 if (newEntry.category == AvatarManager::Contact && newEntry.contact) {
174 avatarFilename = KIO::encodeFileName(newEntry.contact->contactId()) + ".png";
175 } else {
176 // Find MD5 hash for filename
177 // MD5 always contain ASCII caracteres so it is more save for a filename.
178 // Name can use UTF-8 characters.
179 QByteArray tempArray;
180 QBuffer tempBuffer(&tempArray);
181 tempBuffer.open(QIODevice::WriteOnly);
182 avatar.save(&tempBuffer, "PNG");
183 QCryptographicHash context(QCryptographicHash::Md5);
184 context.addData(tempArray);
185 avatarFilename = context.result() + QLatin1String(".png");
186 }
187
188 // Save image on disk
189 qCDebug(LIBKOPETE_LOG) << "Saving " << avatarFilename << " on disk.";
190 avatarUrl = avatarUrl.adjusted(QUrl::StripTrailingSlash);
191 avatarUrl.setPath(avatarUrl.path() + '/' + (avatarFilename));
192
193 if (!avatar.save(avatarUrl.toLocalFile(), "PNG")) {
194 qCDebug(LIBKOPETE_LOG) << "Saving of scaled avatar to " << avatarUrl.toLocalFile() << " failed !";
195 return AvatarEntry();
196 }
197
198 QString dataFilename;
199
200 // for the contact avatar, save it with the contactId + .png
201 if (newEntry.category == AvatarManager::Contact && newEntry.contact) {
202 dataFilename = KIO::encodeFileName(newEntry.contact->contactId()) + QLatin1String("_");
203 }
204
205 QCryptographicHash md5(QCryptographicHash::Md5);
206 md5.addData(data);
207 dataFilename += md5.result();
208
209 QBuffer buffer(&data);
210 buffer.open(QIODevice::ReadOnly);
211 QImageReader ir(&buffer);
212 dataFilename += QLatin1String(".") + QLatin1String(ir.format());
213
214 // Save (original) data on disk
215 dataUrl = dataUrl.adjusted(QUrl::StripTrailingSlash);
216 dataUrl.setPath(dataUrl.path() + '/' + (dataFilename));
217 QFile f(dataUrl.toLocalFile());
218 if (!f.open(QIODevice::WriteOnly)) {
219 qCDebug(LIBKOPETE_LOG) << "Saving of original avatar to " << dataUrl.toLocalFile() << " failed !";
220 return AvatarEntry();
221 }
222 f.write(data);
223 f.flush();
224 f.close();
225
226 // Save metadata of image
227 KConfigGroup avatarConfig(KSharedConfig::openConfig(configUrl.toLocalFile(), KConfig::SimpleConfig), newEntry.name);
228
229 avatarConfig.writeEntry("Filename", avatarFilename);
230 avatarConfig.writeEntry("DataFilename", dataFilename);
231 avatarConfig.writeEntry("Category", int(newEntry.category));
232
233 avatarConfig.sync();
234
235 // Add final path to the new entry for avatarAdded signal
236 newEntry.path = avatarUrl.toLocalFile();
237 newEntry.dataPath = dataUrl.toLocalFile();
238
239 emit avatarAdded(newEntry);
240
241 return newEntry;
242 }
243
remove(Kopete::AvatarManager::AvatarEntry entryToRemove)244 bool AvatarManager::remove(Kopete::AvatarManager::AvatarEntry entryToRemove)
245 {
246 // We need name and path to remove an avatar from the storage.
247 if (entryToRemove.name.isEmpty() && entryToRemove.path.isEmpty()) {
248 return false;
249 }
250
251 // We don't allow removing avatars from Contact category
252 if (entryToRemove.category & Kopete::AvatarManager::Contact) {
253 return false;
254 }
255
256 // Delete the image file first, file delete is more likely to fail than config group remove.
257 if (KIO::NetAccess::del(QUrl(entryToRemove.path), 0)) {
258 qCDebug(LIBKOPETE_LOG) << "Removing avatar from config.";
259
260 QUrl configUrl = d->baseDir;
261 configUrl = configUrl.adjusted(QUrl::StripTrailingSlash);
262 configUrl.setPath(configUrl.path() + '/' + (UserDir));
263 configUrl = configUrl.adjusted(QUrl::StripTrailingSlash);
264 configUrl.setPath(configUrl.path() + '/' + (AvatarConfig));
265
266 KConfigGroup avatarConfig(KSharedConfig::openConfig(configUrl.toLocalFile(), KConfig::SimpleConfig), entryToRemove.name);
267 avatarConfig.deleteGroup();
268 avatarConfig.sync();
269
270 emit avatarRemoved(entryToRemove);
271
272 return true;
273 }
274
275 return false;
276 }
277
exists(Kopete::AvatarManager::AvatarEntry entryToCheck)278 bool AvatarManager::exists(Kopete::AvatarManager::AvatarEntry entryToCheck)
279 {
280 if (entryToCheck.name.isEmpty()) {
281 return false;
282 }
283 return exists(entryToCheck.name);
284 }
285
exists(const QString & avatarName)286 bool AvatarManager::exists(const QString &avatarName)
287 {
288 QUrl configUrl = d->baseDir;
289 configUrl = configUrl.adjusted(QUrl::StripTrailingSlash);
290 configUrl.setPath(configUrl.path() + '/' + (UserDir));
291 configUrl = configUrl.adjusted(QUrl::StripTrailingSlash);
292 configUrl.setPath(configUrl.path() + '/' + (AvatarConfig));
293
294 KConfigGroup avatarConfig(KSharedConfig::openConfig(configUrl.toLocalFile(), KConfig::SimpleConfig), avatarName);
295 qCDebug(LIBKOPETE_LOG) << "Checking if an avatar exists: " << avatarName;
296 if (!avatarConfig.exists()) {
297 return false;
298 }
299 return true;
300 }
301
createDirectory(const QUrl & directory)302 void AvatarManager::Private::createDirectory(const QUrl &directory)
303 {
304 if (!QFile::exists(directory.toLocalFile())) {
305 qCDebug(LIBKOPETE_LOG) << "Creating directory: " << directory.toLocalFile();
306 if (!KIO::NetAccess::mkdir(directory, 0)) {
307 qCDebug(LIBKOPETE_LOG) << "Directory " << directory.toLocalFile() <<" creating failed.";
308 }
309 }
310 }
311
scaleImage(const QImage & source)312 QImage AvatarManager::Private::scaleImage(const QImage &source)
313 {
314 if (source.isNull()) {
315 return QImage();
316 }
317
318 //make an empty image and fill with transparent color
319 QImage result(96, 96, QImage::Format_ARGB32);
320 result.fill(0);
321
322 QPainter paint(&result);
323 float x = 0, y = 0;
324
325 // scale and center the image
326 if (source.width() > 96 || source.height() > 96) {
327 const QImage scaled = source.scaled(96, 96, Qt::KeepAspectRatio, Qt::SmoothTransformation);
328
329 x = (96 - scaled.width()) / 2.0;
330 y = (96 - scaled.height()) / 2.0;
331
332 paint.translate(x, y);
333 paint.drawImage(QPoint(0, 0), scaled);
334 } else {
335 x = (96 - source.width()) / 2.0;
336 y = (96 - source.height()) / 2.0;
337
338 paint.translate(x, y);
339 paint.drawImage(QPoint(0, 0), source);
340 }
341
342 return result;
343 }
344
345 //END AvatarManager
346
347 //BEGIN AvatarQueryJob
348 class AvatarQueryJob::Private
349 {
350 public:
Private(AvatarQueryJob * parent)351 Private(AvatarQueryJob *parent)
352 : queryJob(parent)
353 , category(AvatarManager::All)
354 {
355 }
356
357 QPointer<AvatarQueryJob> queryJob;
358 AvatarManager::AvatarCategory category;
359 QList<AvatarManager::AvatarEntry> avatarList;
360 QUrl baseDir;
361
362 void listAvatarDirectory(const QString &path);
363 };
364
AvatarQueryJob(QObject * parent)365 AvatarQueryJob::AvatarQueryJob(QObject *parent)
366 : KJob(parent)
367 , d(new Private(this))
368 {
369 }
370
~AvatarQueryJob()371 AvatarQueryJob::~AvatarQueryJob()
372 {
373 delete d;
374 }
375
setQueryFilter(Kopete::AvatarManager::AvatarCategory category)376 void AvatarQueryJob::setQueryFilter(Kopete::AvatarManager::AvatarCategory category)
377 {
378 d->category = category;
379 }
380
start()381 void AvatarQueryJob::start()
382 {
383 d->baseDir = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1String("/avatars"));
384
385 if (d->category & Kopete::AvatarManager::User) {
386 d->listAvatarDirectory(UserDir);
387 }
388 if (d->category & Kopete::AvatarManager::Contact) {
389 QUrl contactUrl(d->baseDir);
390 contactUrl = contactUrl.adjusted(QUrl::StripTrailingSlash);
391 contactUrl.setPath(contactUrl.path() + '/' + (ContactDir));
392
393 const QDir contactDir(contactUrl.toLocalFile());
394 const QStringList subdirsList = contactDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
395 foreach (const QString &subdir, subdirsList) {
396 d->listAvatarDirectory(ContactDir + QDir::separator() + subdir);
397 }
398 }
399
400 // Finish the job
401 emitResult();
402 }
403
avatarList() const404 QList<Kopete::AvatarManager::AvatarEntry> AvatarQueryJob::avatarList() const
405 {
406 return d->avatarList;
407 }
408
listAvatarDirectory(const QString & relativeDirectory)409 void AvatarQueryJob::Private::listAvatarDirectory(const QString &relativeDirectory)
410 {
411 QUrl avatarDirectory = baseDir;
412 avatarDirectory = avatarDirectory.adjusted(QUrl::StripTrailingSlash);
413 avatarDirectory.setPath(avatarDirectory.path() + '/' + (relativeDirectory));
414
415 qCDebug(LIBKOPETE_LOG) << "Listing avatars in " << avatarDirectory.toLocalFile();
416
417 // Look for Avatar configuration
418 QUrl avatarConfigUrl = avatarDirectory;
419 avatarConfigUrl = avatarConfigUrl.adjusted(QUrl::StripTrailingSlash);
420 avatarConfigUrl.setPath(avatarConfigUrl.path() + '/' + (AvatarConfig));
421 if (QFile::exists(avatarConfigUrl.toLocalFile())) {
422 KConfig *avatarConfig = new KConfig(avatarConfigUrl.toLocalFile(), KConfig::SimpleConfig);
423 // Each avatar entry in configuration is a group
424 const QStringList groupEntryList = avatarConfig->groupList();
425 foreach (const QString &groupEntry, groupEntryList) {
426 KConfigGroup cg(avatarConfig, groupEntry);
427
428 Kopete::AvatarManager::AvatarEntry listedEntry;
429 listedEntry.name = groupEntry;
430 listedEntry.category = static_cast<Kopete::AvatarManager::AvatarCategory>(cg.readEntry("Category", 0));
431
432 const QString filename = cg.readEntry("Filename", QString());
433 QUrl avatarPath(avatarDirectory);
434 avatarPath = avatarPath.adjusted(QUrl::StripTrailingSlash);
435 avatarPath.setPath(avatarPath.path() + '/' + (filename));
436 listedEntry.path = avatarPath.toLocalFile();
437
438 const QString dataFilename = cg.readEntry("DataFilename", QString());
439 QUrl dataPath(avatarDirectory);
440 dataPath = dataPath.adjusted(QUrl::StripTrailingSlash);
441 dataPath.setPath(dataPath.path() + '/' + (dataFilename));
442 listedEntry.dataPath = dataPath.toLocalFile();
443
444 avatarList << listedEntry;
445 }
446 delete avatarConfig;
447 }
448 }
449
450 //END AvatarQueryJob
451 }
452