1/***************************************************************************
2 * Copyright (C) 2007 by Kevin Krammer <kevin.krammer@gmx.at> *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU Library General Public License as *
6 * published by the Free Software Foundation; either version 2 of the *
7 * License, or (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU Library General Public *
15 * License along with this program; if not, write to the *
16 * Free Software Foundation, Inc., *
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
18 ***************************************************************************/
19
20#include "xdgbasedirs_p.h"
21
22#include "akonadi-prefix.h" // for prefix defines
23
24#include <QtCore/QCoreApplication>
25#include <QtCore/QDebug>
26#include <QtCore/QDir>
27#include <QtCore/QFileInfo>
28#include <QtCore/QProcess>
29#include <QtCore/QSettings>
30
31#include <cstdlib>
32
33static QStringList alternateExecPaths(const QString &path)
34{
35 QStringList pathList;
36
37 pathList << path;
38
39#if defined(Q_OS_WIN) //krazy:exclude=cpp
40 pathList << path + QLatin1String(".exe");
41#elif defined(Q_OS_MAC) //krazy:exclude=cpp
42 pathList << path + QLatin1String(".app/Contents/MacOS/") + path.section(QLatin1Char('/'), -1);
43#endif
44
45 return pathList;
46}
47
48static QStringList splitPathList(const QString &pathList)
49{
50#if defined(Q_OS_WIN) //krazy:exclude=cpp
51 return pathList.split(QLatin1Char(';'));
52#else
53 return pathList.split(QLatin1Char(':'));
54#endif
55}
56
57namespace Akonadi {
58
59class XdgBaseDirsPrivate
60{
61public:
62 XdgBaseDirsPrivate()
63 {
64 }
65
66 ~XdgBaseDirsPrivate()
67 {
68 }
69};
70
71class XdgBaseDirsSingleton
72{
73public:
74 QString homePath(const char *variable, const char *defaultSubDir);
75
76 QStringList systemPathList(const char *variable, const char *defaultDirList);
77
78public:
79 QString mConfigHome;
80 QString mDataHome;
81
82 QStringList mConfigDirs;
83 QStringList mDataDirs;
84 QStringList mExecutableDirs;
85 QStringList mPluginDirs;
86};
87
88Q_GLOBAL_STATIC(XdgBaseDirsSingleton, instance)
89
90}
91
92using namespace Akonadi;
93
94XdgBaseDirs::XdgBaseDirs()
95 : d(new XdgBaseDirsPrivate())
96{
97}
98
99XdgBaseDirs::~XdgBaseDirs()
100{
101 delete d;
102}
103
104QString XdgBaseDirs::homePath(const char *resource)
105{
106 if (qstrncmp("data", resource, 4) == 0) {
107 if (instance()->mDataHome.isEmpty()) {
108 instance()->mDataHome = instance()->homePath("XDG_DATA_HOME", ".local/share");
109 }
110 return instance()->mDataHome;
111 } else if (qstrncmp("config", resource, 6) == 0) {
112 if (instance()->mConfigHome.isEmpty()) {
113 instance()->mConfigHome = instance()->homePath("XDG_CONFIG_HOME", ".config");
114 }
115 return instance()->mConfigHome;
116 }
117
118 return QString();
119}
120
121QStringList XdgBaseDirs::systemPathList(const char *resource)
122{
123 if (qstrncmp("data", resource, 4) == 0) {
124 if (instance()->mDataDirs.isEmpty()) {
125#ifdef Q_OS_WIN
126 QDir dir(QCoreApplication::applicationDirPath());
127 dir.cdUp();
128 const QString defaultPathList = dir.absoluteFilePath(QLatin1String("share"));
129 QStringList dataDirs = instance()->systemPathList("XDG_DATA_DIRS", defaultPathList.toLocal8Bit().constData());
130#else
131 QStringList dataDirs = instance()->systemPathList("XDG_DATA_DIRS", "/usr/local/share:/usr/share");
132#endif
133
134#ifdef Q_OS_WIN
135 const QString prefixDataDir = QLatin1String(AKONADIPREFIX "/" AKONADIDATA);
136#else
137 const QString prefixDataDir = QLatin1String(AKONADIDATA);
138#endif
139 if (!dataDirs.contains(prefixDataDir)) {
140 dataDirs << prefixDataDir;
141 }
142
143#if QT_VERSION < 0x050000
144 // fallback for users with KDE in a different prefix and not correctly set up XDG_DATA_DIRS, hi David ;-)
145 QProcess proc;
146 // ### should probably rather be --path xdg-something
147 const QStringList args = QStringList() << QLatin1String("--prefix");
148 proc.start(QLatin1String("kde4-config"), args);
149 if (proc.waitForStarted() && proc.waitForFinished() && proc.exitCode() == 0) {
150 proc.setReadChannel(QProcess::StandardOutput);
151 Q_FOREACH (const QString &basePath, splitPathList(QString::fromLocal8Bit(proc.readLine().trimmed()))) {
152 const QString path = basePath + QDir::separator() + QLatin1String("share");
153 if (!dataDirs.contains(path)) {
154 dataDirs << path;
155 }
156 }
157 }
158#endif
159
160 instance()->mDataDirs = dataDirs;
161 }
162#ifdef Q_OS_WIN
163 QStringList dataDirs = instance()->mDataDirs;
164 // on Windows installation might be scattered across several directories
165 // so check if any installer providing agents has registered its base path
166 QSettings agentProviders(QSettings::SystemScope, QLatin1String("Akonadi"), QLatin1String("Akonadi"));
167 agentProviders.beginGroup(QLatin1String("AgentProviders"));
168 Q_FOREACH (const QString &agentProvider, agentProviders.childKeys()) {
169 const QString basePath = agentProviders.value(agentProvider).toString();
170 if (!basePath.isEmpty()) {
171 const QString path = basePath + QDir::separator() + QLatin1String("share");
172 if (!dataDirs.contains(path)) {
173 dataDirs << path;
174 }
175 }
176 }
177
178 return dataDirs;
179#else
180 return instance()->mDataDirs;
181#endif
182 } else if (qstrncmp("config", resource, 6) == 0) {
183 if (instance()->mConfigDirs.isEmpty()) {
184#ifdef Q_OS_WIN
185 QDir dir(QCoreApplication::applicationDirPath());
186 dir.cdUp();
187 const QString defaultPathList = dir.absoluteFilePath(QLatin1String("etc")) + QLatin1Char(';') + dir.absoluteFilePath(QLatin1String("share/config"));
188 QStringList configDirs = instance()->systemPathList("XDG_CONFIG_DIRS", defaultPathList.toLocal8Bit().constData());
189#else
190 QStringList configDirs = instance()->systemPathList("XDG_CONFIG_DIRS", "/etc/xdg");
191#endif
192
193#ifdef Q_OS_WIN
194 const QString prefixConfigDir = QLatin1String(AKONADIPREFIX "/" AKONADICONFIG);
195#else
196 const QString prefixConfigDir = QLatin1String(AKONADICONFIG);
197#endif
198 if (!configDirs.contains(prefixConfigDir)) {
199 configDirs << prefixConfigDir;
200 }
201
202 instance()->mConfigDirs = configDirs;
203 }
204 return instance()->mConfigDirs;
205 }
206
207 return QStringList();
208}
209
210QString XdgBaseDirs::findResourceFile(const char *resource, const QString &relPath)
211{
212 const QString fullPath = homePath(resource) + QLatin1Char('/') + relPath;
213
214 QFileInfo fileInfo(fullPath);
215 if (fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable()) {
216 return fullPath;
217 }
218
219 const QStringList pathList = systemPathList(resource);
220
221 Q_FOREACH (const QString &path, pathList) {
222 fileInfo = QFileInfo(path + QLatin1Char('/') + relPath);
223 if (fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable()) {
224 return fileInfo.absoluteFilePath();
225 }
226 }
227
228 return QString();
229}
230
231QString XdgBaseDirs::findExecutableFile(const QString &relPath, const QStringList &searchPath)
232{
233 if (instance()->mExecutableDirs.isEmpty()) {
234 QStringList executableDirs = instance()->systemPathList("PATH", "/usr/local/bin:/usr/bin");
235
236 const QString prefixExecutableDir = QLatin1String(AKONADIPREFIX "/bin");
237 if (!executableDirs.contains(prefixExecutableDir)) {
238 executableDirs << prefixExecutableDir;
239 }
240
241 if (QCoreApplication::instance() != 0) {
242 const QString appExecutableDir = QCoreApplication::instance()->applicationDirPath();
243 if (!executableDirs.contains(appExecutableDir)) {
244 executableDirs << appExecutableDir;
245 }
246 }
247
248 executableDirs += searchPath;
249
250#if defined(Q_OS_MAC) //krazy:exclude=cpp
251 executableDirs += QLatin1String(AKONADIBUNDLEPATH);
252#endif
253 qWarning() << "search paths: " << executableDirs;
254
255 instance()->mExecutableDirs = executableDirs;
256 }
257
258#ifdef Q_OS_WIN
259 QStringList executableDirs = instance()->mExecutableDirs;
260 // on Windows installation might be scattered across several directories
261 // so check if any installer providing agents has registered its base path
262 QSettings agentProviders(QSettings::SystemScope, QLatin1String("Akonadi"), QLatin1String("Akonadi"));
263 agentProviders.beginGroup(QLatin1String("AgentProviders"));
264 Q_FOREACH (const QString &agentProvider, agentProviders.childKeys()) {
265 const QString basePath = agentProviders.value(agentProvider).toString();
266 if (!basePath.isEmpty()) {
267 const QString path = basePath + QDir::separator() + QLatin1String("bin");
268 if (!executableDirs.contains(path)) {
269 executableDirs << path;
270 }
271 }
272 }
273
274 QStringList::const_iterator pathIt = executableDirs.constBegin();
275 const QStringList::const_iterator pathEndIt = executableDirs.constEnd();
276#else
277 QStringList::const_iterator pathIt = instance()->mExecutableDirs.constBegin();
278 const QStringList::const_iterator pathEndIt = instance()->mExecutableDirs.constEnd();
279#endif
280 for (; pathIt != pathEndIt; ++pathIt) {
281 const QStringList fullPathList = alternateExecPaths(*pathIt + QLatin1Char('/') + relPath);
282
283 QStringList::const_iterator it = fullPathList.constBegin();
284 const QStringList::const_iterator endIt = fullPathList.constEnd();
285 for (; it != endIt; ++it) {
286 const QFileInfo fileInfo(*it);
287
288 // resolve symlinks, happens eg. with Maemo optify
289 if (fileInfo.canonicalFilePath().isEmpty()) {
290 continue;
291 }
292
293 const QFileInfo canonicalFileInfo(fileInfo.canonicalFilePath());
294
295 if (canonicalFileInfo.exists() && canonicalFileInfo.isFile() && canonicalFileInfo.isExecutable()) {
296 return *it;
297 }
298 }
299 }
300
301 return QString();
302}
303
304QStringList XdgBaseDirs::findPluginDirs()
305{
306 if (instance()->mPluginDirs.isEmpty()) {
307#if QT_VERSION < 0x050000
308 QStringList pluginDirs = instance()->systemPathList("QT_PLUGIN_PATH", AKONADILIB ":" AKONADILIB "/qt4/plugins/:" AKONADILIB "/kde4/:" AKONADILIB "/kde4/plugins/:/usr/lib/qt4/plugins/");
309#else
310 QStringList pluginDirs = instance()->systemPathList("QT_PLUGIN_PATH", AKONADILIB ":" AKONADILIB "/qt5/plugins/:" AKONADILIB "/kf5/:" AKONADILIB "/kf5/plugins/:/usr/lib/qt5/plugins/");
311#endif
312 if (QCoreApplication::instance() != 0) {
313 Q_FOREACH (const QString &libraryPath, QCoreApplication::instance()->libraryPaths()) {
314 if (!pluginDirs.contains(libraryPath)) {
315 pluginDirs << libraryPath;
316 }
317 }
318 }
319#if QT_VERSION < 0x050000
320 // fallback for users with KDE in a different prefix and not correctly set up XDG_DATA_DIRS, hi David ;-)
321 QProcess proc;
322 // ### should probably rather be --path xdg-something
323 const QStringList args = QStringList() << QLatin1String("--path") << QLatin1String("module");
324 proc.start(QLatin1String("kde4-config"), args);
325 if (proc.waitForStarted() && proc.waitForFinished() && proc.exitCode() == 0) {
326 proc.setReadChannel(QProcess::StandardOutput);
327 Q_FOREACH (const QString &path, splitPathList(QString::fromLocal8Bit(proc.readLine().trimmed()))) {
328 if (!pluginDirs.contains(path)) {
329 pluginDirs.append(path);
330 }
331 }
332 }
333#endif
334 qWarning() << "search paths: " << pluginDirs;
335 instance()->mPluginDirs = pluginDirs;
336 }
337
338 return instance()->mPluginDirs;
339}
340
341QString XdgBaseDirs::findPluginFile(const QString &relPath, const QStringList &searchPath)
342{
343 const QStringList searchDirs = findPluginDirs() + searchPath;
344
345#if defined(Q_OS_WIN) //krazy:exclude=cpp
346 const QString pluginName = relPath + QLatin1String(".dll");
347#else
348 const QString pluginName = relPath + QLatin1String(".so");
349#endif
350
351 Q_FOREACH (const QString &path, searchDirs) {
352 const QFileInfo fileInfo(path + QDir::separator() + pluginName);
353
354 // resolve symlinks, happens eg. with Maemo optify
355 if (fileInfo.canonicalFilePath().isEmpty()) {
356 continue;
357 }
358
359 const QFileInfo canonicalFileInfo(fileInfo.canonicalFilePath());
360 if (canonicalFileInfo.exists() && canonicalFileInfo.isFile()) {
361 return canonicalFileInfo.absoluteFilePath();
362 }
363 }
364
365 return QString();
366}
367
368QString XdgBaseDirs::findResourceDir(const char *resource, const QString &relPath)
369{
370 QString fullPath = homePath(resource) + QLatin1Char('/') + relPath;
371
372 QFileInfo fileInfo(fullPath);
373 if (fileInfo.exists() && fileInfo.isDir() && fileInfo.isReadable()) {
374 return fullPath;
375 }
376
377 Q_FOREACH (const QString &path, systemPathList(resource)) {
378 fileInfo = QFileInfo(path + QLatin1Char('/') + relPath);
379 if (fileInfo.exists() && fileInfo.isDir() && fileInfo.isReadable()) {
380 return fileInfo.absoluteFilePath();
381 }
382 }
383
384 return QString();
385}
386
387QStringList XdgBaseDirs::findAllResourceDirs(const char *resource, const QString &relPath)
388{
389 QStringList resultList;
390
391 const QString fullPath = homePath(resource) + QLatin1Char('/') + relPath;
392
393 QFileInfo fileInfo(fullPath);
394 if (fileInfo.exists() && fileInfo.isDir() && fileInfo.isReadable()) {
395 resultList << fileInfo.absoluteFilePath();
396 }
397
398 Q_FOREACH (const QString &path, systemPathList(resource)) {
399 fileInfo = QFileInfo(path + QLatin1Char('/') + relPath);
400 if (fileInfo.exists() && fileInfo.isDir() && fileInfo.isReadable()) {
401 const QString absPath = fileInfo.absoluteFilePath();
402 if (!resultList.contains(absPath)) {
403 resultList << absPath;
404 }
405 }
406 }
407
408 return resultList;
409}
410
411QString XdgBaseDirs::saveDir(const char *resource, const QString &relPath)
412{
413 const QString fullPath = homePath(resource) + QLatin1Char('/') + relPath;
414
415 QFileInfo fileInfo(fullPath);
416 if (fileInfo.exists()) {
417 if (fileInfo.isDir()) {
418 return fullPath;
419 } else {
420 qWarning() << "XdgBaseDirs::saveDir: '" << fileInfo.absoluteFilePath()
421 << "' exists but is not a directory";
422 }
423 } else {
424 if (!QDir::home().mkpath(fileInfo.absoluteFilePath())) {
425 qWarning() << "XdgBaseDirs::saveDir: failed to create directory '"
426 << fileInfo.absoluteFilePath() << "'";
427 } else {
428 return fullPath;
429 }
430 }
431
432 return QString();
433}
434
435QString XdgBaseDirs::akonadiServerConfigFile(FileAccessMode openMode)
436{
437 return akonadiConfigFile(QLatin1String("akonadiserverrc"), openMode);
438}
439
440QString XdgBaseDirs::akonadiConnectionConfigFile(FileAccessMode openMode)
441{
442 return akonadiConfigFile(QLatin1String("akonadiconnectionrc"), openMode);
443}
444
445QString XdgBaseDirs::akonadiConfigFile(const QString &file, FileAccessMode openMode)
446{
447 const QString akonadiDir = QLatin1String("akonadi");
448
449 const QString savePath = saveDir("config", akonadiDir) + QLatin1Char('/') + file;
450
451 if (openMode == WriteOnly) {
452 return savePath;
453 }
454
455 const QString path = findResourceFile("config", akonadiDir + QLatin1Char('/') + file);
456
457 if (path.isEmpty()) {
458 return savePath;
459 } else if (openMode == ReadOnly || path == savePath) {
460 return path;
461 }
462
463 // file found in system paths and mode is ReadWrite, thus
464 // we copy to the home path location and return this path
465 QFile systemFile(path);
466
467 systemFile.copy(savePath);
468
469 return savePath;
470}
471
472QString XdgBaseDirsSingleton::homePath(const char *variable, const char *defaultSubDir)
473{
474 const QByteArray env = qgetenv(variable);
475
476 QString xdgPath;
477 if (env.isEmpty()) {
478 xdgPath = QDir::homePath() + QLatin1Char('/') + QLatin1String(defaultSubDir);
479#if defined(Q_OS_WIN) //krazy:exclude=cpp
480 } else if (QDir::isAbsolutePath(QString::fromLocal8Bit(env))) {
481#else
482 } else if (env.startsWith('/')) {
483#endif
484 xdgPath = QString::fromLocal8Bit(env);
485 } else {
486 xdgPath = QDir::homePath() + QLatin1Char('/') + QString::fromLocal8Bit(env);
487 }
488
489 return xdgPath;
490}
491
492QStringList XdgBaseDirsSingleton::systemPathList(const char *variable, const char *defaultDirList)
493{
494 const QByteArray env = qgetenv(variable);
495
496 QString xdgDirList;
497 if (env.isEmpty()) {
498 xdgDirList = QLatin1String(defaultDirList);
499 } else {
500 xdgDirList = QString::fromLocal8Bit(env);
501 }
502
503 return splitPathList(xdgDirList);
504}
505