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