1/*
2 Copyright (c) 2008 Volker Krause <vkrause@kde.org>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "firstrun_p.h"
21#include "dbusconnectionpool.h"
22#include "servermanager.h"
23
24#include <akonadi/agentinstance.h>
25#include <akonadi/agentinstancecreatejob.h>
26#include <akonadi/agentmanager.h>
27#include <akonadi/agenttype.h>
28
29#include <KConfig>
30#include <KConfigGroup>
31#include <KDebug>
32#include <KGlobal>
33#include <KProcess>
34#include <KStandardDirs>
35
36#include <QtDBus/QDBusConnection>
37#include <QtDBus/QDBusInterface>
38#include <QtDBus/QDBusReply>
39#include <QtCore/QDir>
40#include <QtCore/QMetaMethod>
41#include <QtCore/QMetaObject>
42
43static char FIRSTRUN_DBUSLOCK[] = "org.kde.Akonadi.Firstrun.lock";
44
45using namespace Akonadi;
46
47Firstrun::Firstrun(QObject *parent)
48 : QObject(parent)
49 , mConfig(new KConfig(ServerManager::addNamespace(QLatin1String("akonadi-firstrunrc"))))
50 , mCurrentDefault(0)
51 , mProcess(0)
52{
53 //The code in firstrun is not safe in multi-instance mode
54 Q_ASSERT(!ServerManager::hasInstanceIdentifier());
55 if (ServerManager::hasInstanceIdentifier()) {
56 deleteLater();
57 return;
58 }
59 kDebug();
60 if (DBusConnectionPool::threadConnection().registerService(QLatin1String(FIRSTRUN_DBUSLOCK))) {
61 findPendingDefaults();
62 kDebug() << mPendingDefaults;
63 setupNext();
64 } else {
65 kDebug() << "D-Bus lock found, so someone else does the work for us already.";
66 deleteLater();
67 }
68}
69
70Firstrun::~Firstrun()
71{
72 DBusConnectionPool::threadConnection().unregisterService(QLatin1String(FIRSTRUN_DBUSLOCK));
73 delete mConfig;
74 kDebug() << "done";
75}
76
77void Firstrun::findPendingDefaults()
78{
79 const KConfigGroup cfg(mConfig, "ProcessedDefaults");
80 foreach (const QString &dirName, KGlobal::dirs()->findDirs("data", QLatin1String("akonadi/firstrun"))) {
81 const QStringList files = QDir(dirName).entryList(QDir::Files | QDir::Readable);
82 foreach (const QString &fileName, files) {
83 const QString fullName = dirName + fileName;
84 KConfig c(fullName);
85 const QString id = KConfigGroup(&c, "Agent").readEntry("Id", QString());
86 if (id.isEmpty()) {
87 kWarning() << "Found invalid default configuration in " << fullName;
88 continue;
89 }
90 if (cfg.hasKey(id)) {
91 continue;
92 }
93 mPendingDefaults << dirName + fileName;
94 }
95 }
96
97#ifndef KDEPIM_NO_KRESOURCES
98 // always check legacy kres for migration, their migrator might have changed again
99 mPendingKres << QLatin1String("contact") << QLatin1String("calendar");
100#endif
101}
102
103#ifndef KDEPIM_NO_KRESOURCES
104static QString resourceTypeForMimetype(const QStringList &mimeTypes)
105{
106 if (mimeTypes.contains(QLatin1String("text/directory"))) {
107 return QString::fromLatin1("contact");
108 }
109 if (mimeTypes.contains(QLatin1String("text/calendar"))) {
110 return QString::fromLatin1("calendar");
111 }
112 // TODO notes
113 return QString();
114}
115
116void Firstrun::migrateKresType(const QString &resourceFamily)
117{
118 mResourceFamily = resourceFamily;
119 KConfig config(QLatin1String("kres-migratorrc"));
120 KConfigGroup migrationCfg(&config, "Migration");
121 const bool enabled = migrationCfg.readEntry("Enabled", false);
122 const bool setupClientBridge = migrationCfg.readEntry("SetupClientBridge", true);
123 const int currentVersion = migrationCfg.readEntry(QString::fromLatin1("Version-%1").arg(resourceFamily), 0);
124 const int targetVersion = migrationCfg.readEntry("TargetVersion", 0);
125 if (enabled && currentVersion < targetVersion) {
126 kDebug() << "Performing migration of legacy KResource settings. Good luck!";
127 mProcess = new KProcess(this);
128 connect(mProcess, SIGNAL(finished(int)), SLOT(migrationFinished(int)));
129 QStringList args = QStringList() << QLatin1String("--interactive-on-change")
130 << QLatin1String("--type") << resourceFamily;
131 if (!setupClientBridge) {
132 args << QLatin1String("--omit-client-bridge");
133 }
134 mProcess->setProgram(QLatin1String("kres-migrator"), args);
135 mProcess->start();
136 if (!mProcess->waitForStarted()) {
137 migrationFinished(-1);
138 }
139 } else {
140 // nothing to do
141 setupNext();
142 }
143}
144
145void Firstrun::migrationFinished(int exitCode)
146{
147 Q_ASSERT(mProcess);
148 if (exitCode == 0) {
149 kDebug() << "KResource -> Akonadi migration has been successful";
150 KConfig config(QLatin1String("kres-migratorrc"));
151 KConfigGroup migrationCfg(&config, "Migration");
152 const int targetVersion = migrationCfg.readEntry("TargetVersion", 0);
153 migrationCfg.writeEntry(QString::fromLatin1("Version-%1").arg(mResourceFamily), targetVersion);
154 migrationCfg.sync();
155 } else if (exitCode != 1) {
156 // exit code 1 means it is already running, so we are probably called by a migrator instance
157 kError() << "KResource -> Akonadi migration failed!";
158 kError() << "command was: " << mProcess->program();
159 kError() << "exit code: " << mProcess->exitCode();
160 kError() << "stdout: " << mProcess->readAllStandardOutput();
161 kError() << "stderr: " << mProcess->readAllStandardError();
162 }
163
164 setupNext();
165}
166#endif
167
168void Firstrun::setupNext()
169{
170 delete mCurrentDefault;
171 mCurrentDefault = 0;
172
173 if (mPendingDefaults.isEmpty()) {
174#ifndef KDEPIM_NO_KRESOURCES
175 if (!mPendingKres.isEmpty()) {
176 migrateKresType(mPendingKres.takeFirst());
177 return;
178 }
179#endif
180 deleteLater();
181 return;
182 }
183
184 mCurrentDefault = new KConfig(mPendingDefaults.takeFirst());
185 const KConfigGroup agentCfg = KConfigGroup(mCurrentDefault, "Agent");
186
187 AgentType type = AgentManager::self()->type(agentCfg.readEntry("Type", QString()));
188 if (!type.isValid()) {
189 kError() << "Unable to obtain agent type for default resource agent configuration " << mCurrentDefault->name();
190 setupNext();
191 return;
192 }
193
194#ifndef KDEPIM_NO_KRESOURCES
195 // KDE5: remove me
196 // check if there is a kresource setup for this type already
197 const QString kresType = resourceTypeForMimetype(type.mimeTypes());
198 if (!kresType.isEmpty()) {
199 const QString kresCfgFile = KStandardDirs::locateLocal("config", QString::fromLatin1("kresources/%1/stdrc").arg(kresType));
200 KConfig resCfg(kresCfgFile);
201 const KConfigGroup resGroup(&resCfg, "General");
202 bool legacyResourceFound = false;
203 const QStringList kresResources = resGroup.readEntry("ResourceKeys", QStringList())
204 + resGroup.readEntry("PassiveResourceKeys", QStringList());
205 foreach (const QString &kresResource, kresResources) {
206 const KConfigGroup cfg(&resCfg, QString::fromLatin1("Resource_%1").arg(kresResource));
207 if (cfg.readEntry("ResourceType", QString()) != QLatin1String("akonadi")) { // not a bridge
208 legacyResourceFound = true;
209 break;
210 }
211 }
212 if (legacyResourceFound) {
213 kDebug() << "ignoring " << mCurrentDefault->name() << " as there is a KResource setup for its type already.";
214 KConfigGroup cfg(mConfig, "ProcessedDefaults");
215 cfg.writeEntry(agentCfg.readEntry("Id", QString()), QString::fromLatin1("kres"));
216 cfg.sync();
217 setupNext();
218 return;
219 }
220 }
221#endif
222
223 AgentInstanceCreateJob *job = new AgentInstanceCreateJob(type);
224 connect(job, SIGNAL(result(KJob*)), SLOT(instanceCreated(KJob*)));
225 job->start();
226}
227
228void Firstrun::instanceCreated(KJob *job)
229{
230 Q_ASSERT(mCurrentDefault);
231
232 if (job->error()) {
233 kError() << "Creating agent instance failed for " << mCurrentDefault->name();
234 setupNext();
235 return;
236 }
237
238 AgentInstance instance = static_cast<AgentInstanceCreateJob *>(job)->instance();
239 const KConfigGroup agentCfg = KConfigGroup(mCurrentDefault, "Agent");
240 const QString agentName = agentCfg.readEntry("Name", QString());
241 if (!agentName.isEmpty()) {
242 instance.setName(agentName);
243 }
244
245 // agent specific settings, using the D-Bus <-> KConfigXT bridge
246 const KConfigGroup settings = KConfigGroup(mCurrentDefault, "Settings");
247
248 QDBusInterface *iface = new QDBusInterface(QString::fromLatin1("org.freedesktop.Akonadi.Agent.%1").arg(instance.identifier()),
249 QLatin1String("/Settings"), QString(),
250 DBusConnectionPool::threadConnection(), this);
251 if (!iface->isValid()) {
252 kError() << "Unable to obtain the KConfigXT D-Bus interface of " << instance.identifier();
253 setupNext();
254 delete iface;
255 return;
256 }
257
258 foreach (const QString &setting, settings.keyList()) {
259 kDebug() << "Setting up " << setting << " for agent " << instance.identifier();
260 const QString methodName = QString::fromLatin1("set%1").arg(setting);
261 const QVariant::Type argType = argumentType(iface->metaObject(), methodName);
262 if (argType == QVariant::Invalid) {
263 kError() << "Setting " << setting << " not found in agent configuration interface of " << instance.identifier();
264 continue;
265 }
266
267 QVariant arg;
268 if (argType == QVariant::String) {
269 // Since a string could be a path we always use readPathEntry here,
270 // that shouldn't harm any normal string settings
271 arg = settings.readPathEntry(setting, QString());
272 } else {
273 arg = settings.readEntry(setting, QVariant(argType));
274 }
275
276 const QDBusReply<void> reply = iface->call(methodName, arg);
277 if (!reply.isValid()) {
278 kError() << "Setting " << setting << " failed for agent " << instance.identifier();
279 }
280 }
281
282 iface->call(QLatin1String("writeConfig"));
283
284 instance.reconfigure();
285 instance.synchronize();
286 delete iface;
287
288 // remember we set this one up already
289 KConfigGroup cfg(mConfig, "ProcessedDefaults");
290 cfg.writeEntry(agentCfg.readEntry("Id", QString()), instance.identifier());
291 cfg.sync();
292
293 setupNext();
294}
295
296QVariant::Type Firstrun::argumentType(const QMetaObject *mo, const QString &method)
297{
298 QMetaMethod m;
299 for (int i = 0; i < mo->methodCount(); ++i) {
300 const QString signature = QString::fromLatin1(mo->method(i).signature());
301 if (signature.startsWith(method)) {
302 m = mo->method(i);
303 }
304 }
305
306 if (!m.signature()) {
307 return QVariant::Invalid;
308 }
309
310 const QList<QByteArray> argTypes = m.parameterTypes();
311 if (argTypes.count() != 1) {
312 return QVariant::Invalid;
313 }
314
315 return QVariant::nameToType(argTypes.first());
316}
317
318#include "moc_firstrun_p.cpp"
319