1 /* BEGIN_COMMON_COPYRIGHT_HEADER
2 * (c)LGPL2+
3 *
4 * LXQt - a lightweight, Qt based, desktop toolset
5 * https://lxqt.org
6 *
7 * Copyright: 2010-2011 Razor team
8 * Authors:
9 * Alexander Sokoloff <sokoloff.a@gmail.com>
10 *
11 * This program or library is free software; you can redistribute it
12 * and/or modify it under the terms of the GNU Lesser General Public
13 * License as published by the Free Software Foundation; either
14 * version 2.1 of the License, or (at your option) any later version.
15 *
16 * This library is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 * Lesser General Public License for more details.
20
21 * You should have received a copy of the GNU Lesser General
22 * Public License along with this library; if not, write to the
23 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24 * Boston, MA 02110-1301 USA
25 *
26 * END_COMMON_COPYRIGHT_HEADER */
27
28 //#include "desktopenvironment_p.cpp"
29 #include "xdgdesktopfile.h"
30 #include <stdlib.h>
31 #include <unistd.h>
32 #include <QDebug>
33 #include <QDesktopServices>
34 #include <QDir>
35 #include <QSharedData>
36 #include <QFile>
37 #include <QFileInfo>
38 #include <QHash>
39 #include <QList>
40 #include <QMimeDatabase>
41 #include <QMimeType>
42 #include <QProcess>
43 #include <QSettings>
44 #include <QStandardPaths>
45 #include <QTextStream>
46 #include <QUrl>
47 #include <QtAlgorithms>
48 #define DEFAULT_ICON ":/resources/icon.png"
49
50 /**
51 * See: http://standards.freedesktop.org/desktop-entry-spec
52 */
53
54 // A list of executables that can't be run with QProcess::startDetached(). They
55 // will be run with QProcess::start()
56 static const QStringList nonDetachExecs = QStringList()
57 << QLatin1String("pkexec");
58
59
60 static const QLatin1String nameKey("Name");
61 static const QLatin1String typeKey("Type");
62 static const QLatin1String ApplicationStr("Application");
63 static const QLatin1String LinkStr("Link");
64 static const QLatin1String DirectoryStr("Directory");
65 static const QLatin1String execKey("Exec");
66 static const QLatin1String iconKey("Icon");
67 static const QString userDirectoryString[8] =
68 {
69 QLatin1String("Desktop"),
70 QLatin1String("Download"),
71 QLatin1String("Templates"),
72 QLatin1String("Publicshare"),
73 QLatin1String("Documents"),
74 QLatin1String("Music"),
75 QLatin1String("Pictures"),
76 QLatin1String("Videos")
77 };
78
79 // Helper functions prototypes
80 bool checkTryExec(const QString& progName);
81 QString &doEscape(QString& str, const QHash<QChar,QChar> &repl);
82 QString &doUnEscape(QString& str, const QHash<QChar,QChar> &repl);
83 QString &escape(QString& str);
84 QString &escapeExec(QString& str);
85 QString expandDynamicUrl(QString url);
86 QString expandEnvVariables(const QString str);
87 QStringList expandEnvVariables(const QStringList strs);
88 QString findDesktopFile(const QString& dirName, const QString& desktopName);
89 QString findDesktopFile(const QString& desktopName);
90 static QStringList parseCombinedArgString(const QString &program);
91 bool read(const QString &prefix);
92 void replaceVar(QString &str, const QString &varName, const QString &after);
93 QString &unEscape(QString& str);
94 QString &unEscapeExec(QString& str);
95 void loadMimeCacheDir(const QString& dirName, QHash<QString, QList<XdgDesktopFile*> > & cache);
96 QStringList dataDirs(const QString &postfix);
97
98
doUnEscape(QString & str,const QHash<QChar,QChar> & repl)99 QString &doUnEscape(QString& str, const QHash<QChar,QChar> &repl)
100 {
101 int n = 0;
102 while (1)
103 {
104 n=str.indexOf(QLatin1String("\\"), n);
105 if (n < 0 || n > str.length() - 2)
106 break;
107
108 if (repl.contains(str.at(n+1)))
109 {
110 str.replace(n, 2, repl.value(str.at(n+1)));
111 }
112
113 n++;
114 }
115
116 return str;
117 }
118
119
120 /************************************************
121 The escape sequences \s, \n, \t, \r, and \\ are supported for values
122 of type string and localestring, meaning ASCII space, newline, tab,
123 carriage return, and backslash, respectively.
124 ************************************************/
unEscape(QString & str)125 QString &unEscape(QString& str)
126 {
127 QHash<QChar,QChar> repl;
128 repl.insert(QLatin1Char('\\'), QLatin1Char('\\'));
129 repl.insert(QLatin1Char('s'), QLatin1Char(' '));
130 repl.insert(QLatin1Char('n'), QLatin1Char('\n'));
131 repl.insert(QLatin1Char('t'), QLatin1Char('\t'));
132 repl.insert(QLatin1Char('r'), QLatin1Char('\r'));
133
134 return doUnEscape(str, repl);
135 }
136
137
138 /************************************************
139 Quoting must be done by enclosing the argument between double quotes and
140 escaping the
141 double quote character,
142 backtick character ("`"),
143 dollar sign ("$") and
144 backslash character ("\")
145 by preceding it with an additional backslash character.
146 Implementations must undo quoting before expanding field codes and before
147 passing the argument to the executable program.
148
149 Reserved characters are
150 space (" "),
151 tab,
152 newline,
153 double quote,
154 single quote ("'"),
155 backslash character ("\"),
156 greater-than sign (">"),
157 less-than sign ("<"),
158 tilde ("~"),
159 vertical bar ("|"),
160 ampersand ("&"),
161 semicolon (";"),
162 dollar sign ("$"),
163 asterisk ("*"),
164 question mark ("?"),
165 hash mark ("#"),
166 parenthesis ("(") and (")")
167 backtick character ("`").
168
169 Note that the general escape rule for values of type string states that the
170 backslash character can be escaped as ("\\") as well and that this escape
171 rule is applied before the quoting rule. As such, to unambiguously represent a
172 literal backslash character in a quoted argument in a desktop entry file
173 requires the use of four successive backslash characters ("\\\\").
174 Likewise, a literal dollar sign in a quoted argument in a desktop entry file
175 is unambiguously represented with ("\\$").
176 ************************************************/
unEscapeExec(QString & str)177 QString &unEscapeExec(QString& str)
178 {
179 unEscape(str);
180 QHash<QChar,QChar> repl;
181 // The parseCombinedArgString() splits the string by the space symbols,
182 // we temporarily replace them on the special characters.
183 // Replacement will reverse after the splitting.
184 repl.insert(QLatin1Char(' '), 01); // space
185 repl.insert(QLatin1Char('\t'), 02); // tab
186 repl.insert(QLatin1Char('\n'), 03); // newline,
187
188 repl.insert(QLatin1Char('"'), QLatin1Char('"')); // double quote,
189 repl.insert(QLatin1Char('\''), QLatin1Char('\'')); // single quote ("'"),
190 repl.insert(QLatin1Char('\\'), QLatin1Char('\\')); // backslash character ("\"),
191 repl.insert(QLatin1Char('>'), QLatin1Char('>')); // greater-than sign (">"),
192 repl.insert(QLatin1Char('<'), QLatin1Char('<')); // less-than sign ("<"),
193 repl.insert(QLatin1Char('~'), QLatin1Char('~')); // tilde ("~"),
194 repl.insert(QLatin1Char('|'), QLatin1Char('|')); // vertical bar ("|"),
195 repl.insert(QLatin1Char('&'), QLatin1Char('&')); // ampersand ("&"),
196 repl.insert(QLatin1Char(';'), QLatin1Char(';')); // semicolon (";"),
197 repl.insert(QLatin1Char('$'), QLatin1Char('$')); // dollar sign ("$"),
198 repl.insert(QLatin1Char('*'), QLatin1Char('*')); // asterisk ("*"),
199 repl.insert(QLatin1Char('?'), QLatin1Char('?')); // question mark ("?"),
200 repl.insert(QLatin1Char('#'), QLatin1Char('#')); // hash mark ("#"),
201 repl.insert(QLatin1Char('('), QLatin1Char('(')); // parenthesis ("(")
202 repl.insert(QLatin1Char(')'), QLatin1Char(')')); // parenthesis (")")
203 repl.insert(QLatin1Char('`'), QLatin1Char('`')); // backtick character ("`").
204
205 return doUnEscape(str, repl);
206 }
207
fixBashShortcuts(QString & s)208 void fixBashShortcuts(QString &s)
209 {
210 if (s.startsWith(QLatin1Char('~')))
211 s = QFile::decodeName(qgetenv("HOME")) + (s).mid(1);
212 }
213
removeEndingSlash(QString & s)214 void removeEndingSlash(QString &s)
215 {
216 // We don't check for empty strings. Caller must check it.
217
218 // Remove the ending slash, except for root dirs.
219 if (s.length() > 1 && s.endsWith(QLatin1Char('/')))
220 s.chop(1);
221 }
222
cleanAndAddPostfix(QStringList & dirs,const QString & postfix)223 void cleanAndAddPostfix(QStringList &dirs, const QString& postfix)
224 {
225 const int N = dirs.count();
226 for(int i = 0; i < N; ++i)
227 {
228 fixBashShortcuts(dirs[i]);
229 removeEndingSlash(dirs[i]);
230 dirs[i].append(postfix);
231 }
232 }
233
234 namespace
235 {
236 /*!
237 * Helper class for getting the keys for "Additional applications actions"
238 * ([Desktop Action %s] sections)
239 */
240 class XdgDesktopAction : public XdgDesktopFile
241 {
242 public:
XdgDesktopAction(const XdgDesktopFile & parent,const QString & action)243 XdgDesktopAction(const XdgDesktopFile & parent, const QString & action)
244 : XdgDesktopFile(parent)
245 , m_prefix(QString{QLatin1String("Desktop Action %1")}.arg(action))
246 {}
247
248 protected:
prefix() const249 virtual QString prefix() const { return m_prefix; }
250
251 private:
252 const QString m_prefix;
253 };
254 }
255
256 class XdgDesktopFileData: public QSharedData {
257 public:
258 XdgDesktopFileData();
259
clear()260 inline void clear() {
261 mFileName.clear();
262 mIsValid = false;
263 mValidIsChecked = false;
264 mIsShow.clear();
265 mItems.clear();
266 mType = XdgDesktopFile::UnknownType;
267 }
268 bool read(const QString &prefix);
269 XdgDesktopFile::Type detectType(XdgDesktopFile *q) const;
270 bool startApplicationDetached(const XdgDesktopFile *q, const QString & action, const QStringList& urls) const;
271
272 QString mFileName;
273 bool mIsValid;
274 mutable bool mValidIsChecked;
275 mutable QHash<QString, bool> mIsShow;
276 QMap<QString, QVariant> mItems;
277
278 XdgDesktopFile::Type mType;
279 };
280
281
XdgDesktopFileData()282 XdgDesktopFileData::XdgDesktopFileData():
283 mFileName(),
284 mIsValid(false),
285 mValidIsChecked(false),
286 mIsShow(),
287 mItems(),
288 mType(XdgDesktopFile::UnknownType)
289 {
290 }
291
292
read(const QString & prefix)293 bool XdgDesktopFileData::read(const QString &prefix)
294 {
295 QFile file(mFileName);
296
297 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
298 return false;
299
300 QString section;
301 QTextStream stream(&file);
302 bool prefixExists = false;
303 while (!stream.atEnd()) {
304 QString line = stream.readLine().trimmed();
305
306 // Skip comments ......................
307 if (line.startsWith(QLatin1Char('#')))
308 continue;
309
310
311 // Section ..............................
312 if (line.startsWith(QLatin1Char('[')) && line.endsWith(QLatin1Char(']')))
313 {
314 section = line.mid(1, line.length()-2);
315 if (section == prefix)
316 prefixExists = true;
317
318 continue;
319 }
320
321 QString key = line.section(QLatin1Char('='), 0, 0).trimmed();
322 QString value = line.section(QLatin1Char('='), 1).trimmed();
323
324 if (key.isEmpty())
325 continue;
326
327 mItems[section + QLatin1Char('/') + key] = QVariant(value);
328 }
329
330
331 // Not check for empty prefix
332 mIsValid = (prefix.isEmpty()) || prefixExists;
333 return mIsValid;
334 }
335
detectType(XdgDesktopFile * q) const336 XdgDesktopFile::Type XdgDesktopFileData::detectType(XdgDesktopFile *q) const
337 {
338 QString typeStr = q->value(typeKey).toString();
339 if (typeStr == ApplicationStr)
340 return XdgDesktopFile::ApplicationType;
341
342 if (typeStr == LinkStr)
343 return XdgDesktopFile::LinkType;
344
345 if (typeStr == DirectoryStr)
346 return XdgDesktopFile::DirectoryType;
347
348 if (!q->value(execKey).toString().isEmpty())
349 return XdgDesktopFile::ApplicationType;
350
351 return XdgDesktopFile::UnknownType;
352 }
353
startApplicationDetached(const XdgDesktopFile * q,const QString & action,const QStringList & urls) const354 bool XdgDesktopFileData::startApplicationDetached(const XdgDesktopFile *q, const QString & action, const QStringList& urls) const
355 {
356 //DBusActivatable handling
357 if (q->value(QLatin1String("DBusActivatable"), false).toBool()) {
358 /* WARNING: We fallback to use Exec when the DBusActivatable fails.
359 *
360 * This is a violation of the standard and we know it!
361 *
362 * From the Standard:
363 * DBusActivatable A boolean value specifying if D-Bus activation is
364 * supported for this application. If this key is missing, the default
365 * value is false. If the value is true then implementations should
366 * ignore the Exec key and send a D-Bus message to launch the
367 * application. See D-Bus Activation for more information on how this
368 * works. Applications should still include Exec= lines in their desktop
369 * files for compatibility with implementations that do not understand
370 * the DBusActivatable key.
371 *
372 * So, why are we doing it ? In the benefit of user experience.
373 * We first ignore the Exec line and in use the D-Bus to lauch the
374 * application. But if it fails, we try the Exec method.
375 *
376 * We consider that this violation is more acceptable than an failure
377 * in launching an application.
378 */
379
380 return false;
381 }
382 QStringList args = action.isEmpty()
383 ? q->expandExecString(urls)
384 : XdgDesktopAction{*q, action}.expandExecString(urls);
385
386 if (args.isEmpty())
387 return false;
388
389 if (q->value(QLatin1String("Terminal")).toBool())
390 {
391 QString term = QString::fromLocal8Bit(qgetenv("TERM"));
392 if (term.isEmpty())
393 term = QLatin1String("xterm");
394
395 args.prepend(QLatin1String("-e"));
396 args.prepend(term);
397 }
398
399 bool nonDetach = false;
400 for (const QString &s : nonDetachExecs)
401 {
402 for (const QString &a : const_cast<const QStringList&>(args))
403 {
404 if (a.contains(s))
405 {
406 nonDetach = true;
407 }
408 }
409 }
410
411 QString cmd = args.takeFirst();
412 QString workingDir = q->value(QLatin1String("Path")).toString();
413 if (!workingDir.isEmpty() && !QDir(workingDir).exists())
414 workingDir = QString();
415
416 if (nonDetach)
417 {
418 QScopedPointer<QProcess> p(new QProcess);
419 p->setStandardInputFile(QProcess::nullDevice());
420 p->setProcessChannelMode(QProcess::ForwardedChannels);
421 if (!workingDir.isEmpty())
422 p->setWorkingDirectory(workingDir);
423 p->start(cmd, args);
424 bool started = p->waitForStarted();
425 if (started)
426 {
427 QProcess* proc = p.take(); //release the pointer(will be selfdestroyed upon finish)
428 QObject::connect(proc, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
429 proc, &QProcess::deleteLater);
430 }
431 return started;
432 }
433 else
434 {
435 return QProcess::startDetached(cmd, args, workingDir);
436 }
437 }
438
XdgDesktopFile()439 XdgDesktopFile::XdgDesktopFile():
440 d(new XdgDesktopFileData)
441 {
442 }
443
444
XdgDesktopFile(const XdgDesktopFile & other)445 XdgDesktopFile::XdgDesktopFile(const XdgDesktopFile& other):
446 d(other.d)
447 {
448 }
449
~XdgDesktopFile()450 XdgDesktopFile::~XdgDesktopFile()
451 {
452 }
453
load(const QString & fileName)454 bool XdgDesktopFile::load(const QString& fileName)
455 {
456 d->clear();
457 if (fileName.startsWith(QDir::separator())) { // absolute path
458 QFileInfo f(fileName);
459 if (f.exists())
460 d->mFileName = f.canonicalFilePath();
461 else
462 return false;
463 } else { // relative path
464 const QString r = findDesktopFile(fileName);
465 if (r.isEmpty())
466 return false;
467 else
468 d->mFileName = r;
469 }
470 d->read(prefix());
471 d->mIsValid = d->mIsValid && check();
472 d->mType = d->detectType(this);
473 return isValid();
474 }
475
value(const QString & key,const QVariant & defaultValue) const476 QVariant XdgDesktopFile::value(const QString& key, const QVariant& defaultValue) const
477 {
478 QString path = (!prefix().isEmpty()) ? prefix() + QLatin1Char('/') + key : key;
479 QVariant res = d->mItems.value(path, defaultValue);
480 if (res.type() == QVariant::String)
481 {
482 QString s = res.toString();
483 return unEscape(s);
484 }
485
486 return res;
487 }
488
489
490 /************************************************
491 LC_MESSAGES value Possible keys in order of matching
492 lang_COUNTRY@MODIFIER lang_COUNTRY@MODIFIER, lang_COUNTRY, lang@MODIFIER, lang,
493 default value
494 lang_COUNTRY lang_COUNTRY, lang, default value
495 lang@MODIFIER lang@MODIFIER, lang, default value
496 lang lang, default value
497 ************************************************/
localizedKey(const QString & key) const498 QString XdgDesktopFile::localizedKey(const QString& key) const
499 {
500 QString lang = QString::fromLocal8Bit(qgetenv("LC_MESSAGES"));
501
502 if (lang.isEmpty())
503 lang = QString::fromLocal8Bit(qgetenv("LC_ALL"));
504
505 if (lang.isEmpty())
506 lang = QString::fromLocal8Bit(qgetenv("LANG"));
507
508 QString modifier = lang.section(QLatin1Char('@'), 1);
509 if (!modifier.isEmpty())
510 lang.truncate(lang.length() - modifier.length() - 1);
511
512 QString encoding = lang.section(QLatin1Char('.'), 1);
513 if (!encoding.isEmpty())
514 lang.truncate(lang.length() - encoding.length() - 1);
515
516
517 QString country = lang.section(QLatin1Char('_'), 1);
518 if (!country.isEmpty())
519 lang.truncate(lang.length() - country.length() - 1);
520
521 //qDebug() << "LC_MESSAGES: " << qgetenv("LC_MESSAGES");
522 //qDebug() << "Lang:" << lang;
523 //qDebug() << "Country:" << country;
524 //qDebug() << "Encoding:" << encoding;
525 //qDebug() << "Modifier:" << modifier;
526
527
528 if (!modifier.isEmpty() && !country.isEmpty())
529 {
530 QString k = QString::fromLatin1("%1[%2_%3@%4]").arg(key, lang, country, modifier);
531 //qDebug() << "\t try " << k << contains(k);
532 if (contains(k))
533 return k;
534 }
535
536 if (!country.isEmpty())
537 {
538 QString k = QString::fromLatin1("%1[%2_%3]").arg(key, lang, country);
539 //qDebug() << "\t try " << k << contains(k);
540 if (contains(k))
541 return k;
542 }
543
544 if (!modifier.isEmpty())
545 {
546 QString k = QString::fromLatin1("%1[%2@%3]").arg(key, lang, modifier);
547 //qDebug() << "\t try " << k << contains(k);
548 if (contains(k))
549 return k;
550 }
551
552 QString k = QString::fromLatin1("%1[%2]").arg(key, lang);
553 //qDebug() << "\t try " << k << contains(k);
554 if (contains(k))
555 return k;
556
557
558 //qDebug() << "\t try " << key << contains(key);
559 return key;
560 }
561
562
localizedValue(const QString & key,const QVariant & defaultValue) const563 QVariant XdgDesktopFile::localizedValue(const QString& key, const QVariant& defaultValue) const
564 {
565 return value(localizedKey(key), defaultValue);
566 }
567
contains(const QString & key) const568 bool XdgDesktopFile::contains(const QString& key) const
569 {
570 QString path = (!prefix().isEmpty()) ? prefix() + QLatin1Char('/') + key : key;
571 return d->mItems.contains(path);
572 }
573
574
isValid() const575 bool XdgDesktopFile::isValid() const
576 {
577 return d->mIsValid;
578 }
579
580 /************************************************
581 Returns the QIcon corresponding to name in the current icon theme. If no such icon
582 is found in the current theme fallback is return instead.
583 ************************************************/
fromTheme(const QString & iconName,int size,const QIcon & fallback)584 QIcon XdgDesktopFile::fromTheme(const QString& iconName, int size, const QIcon& fallback)
585 {
586 return QIcon::fromTheme(iconName, fallback);
587 }
588
icon(int size,const QIcon & fallback) const589 QIcon const XdgDesktopFile::icon(int size, const QIcon& fallback) const
590 {
591 QIcon result = fromTheme(value(iconKey).toString(), size, fallback);
592
593 if (result.isNull() && type() == ApplicationType) {
594 result = fromTheme(QLatin1String("application-x-executable.png"), size);
595 // TODO Maybe defaults for other desktopfile types as well..
596 }
597
598 return result;
599 }
600
601
type() const602 XdgDesktopFile::Type XdgDesktopFile::type() const
603 {
604 return d->mType;
605 }
606
607
608 /************************************************
609 Starts the program defined in this desktop file in a new process, and detaches
610 from it. Returns true on success; otherwise returns false. If the calling process
611 exits, the detached process will continue to live.
612
613 Urls - the list of URLs or files to open, can be empty (app launched without
614 argument)
615 If the function is successful then *pid is set to the process identifier of the
616 started process.
617 ************************************************/
startDetached(const QStringList & urls) const618 bool XdgDesktopFile::startDetached(const QStringList& urls) const
619 {
620 switch(d->mType)
621 {
622 case ApplicationType:
623 return d->startApplicationDetached(this, QString{}, urls);
624 break;
625
626 case LinkType:
627 return false;
628 break;
629
630 default:
631 return false;
632 }
633 }
634
635 /************************************************
636 This is an overloaded function.
637 ************************************************/
startDetached(const QString & url) const638 bool XdgDesktopFile::startDetached(const QString& url) const
639 {
640 if (url.isEmpty())
641 return startDetached(QStringList());
642 else
643 return startDetached(QStringList(url));
644 }
645
646
parseCombinedArgString(const QString & program)647 static QStringList parseCombinedArgString(const QString &program)
648 {
649 QStringList args;
650 QString tmp;
651 int quoteCount = 0;
652 bool inQuote = false;
653
654 // handle quoting. tokens can be surrounded by double quotes
655 // "hello world". three consecutive double quotes represent
656 // the quote character itself.
657 for (int i = 0; i < program.size(); ++i) {
658 if (program.at(i) == QLatin1Char('"')) {
659 ++quoteCount;
660 if (quoteCount == 3) {
661 // third consecutive quote
662 quoteCount = 0;
663 tmp += program.at(i);
664 }
665 continue;
666 }
667 if (quoteCount) {
668 if (quoteCount == 1)
669 inQuote = !inQuote;
670 quoteCount = 0;
671 }
672 if (!inQuote && program.at(i).isSpace()) {
673 if (!tmp.isEmpty()) {
674 args += tmp;
675 tmp.clear();
676 }
677 } else {
678 tmp += program.at(i);
679 }
680 }
681 if (!tmp.isEmpty())
682 args += tmp;
683
684 return args;
685 }
686
687
replaceVar(QString & str,const QString & varName,const QString & after)688 void replaceVar(QString &str, const QString &varName, const QString &after)
689 {
690 str.replace(QRegExp(QString::fromLatin1("\\$%1(?!\\w)").arg(varName)), after);
691 str.replace(QRegExp(QString::fromLatin1("\\$\\{%1\\}").arg(varName)), after);
692 }
693
userDir(UserDirectory dir)694 QString userDir(UserDirectory dir)
695 {
696 // possible values for UserDirectory
697 Q_ASSERT(!(dir < Desktop || dir > Videos));
698 if (dir < Desktop || dir > Videos)
699 return QString();
700
701 QString folderName = userDirectoryString[dir];
702 QString fallback;
703 const QString home = QFile::decodeName(qgetenv("HOME"));
704
705 if (home.isEmpty())
706 return QString::fromLatin1("/tmp");
707 else if (dir == Desktop)
708 fallback = QString::fromLatin1("%1/%2").arg(home, QLatin1String("Desktop"));
709 else
710 fallback = home;
711
712 return fallback;
713
714 QString s = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
715 fixBashShortcuts(s);
716 QDir d(s);
717 if (!d.exists())
718 {
719 if (!d.mkpath(QLatin1String(".")))
720 {
721 qWarning() << QString::fromLatin1("Can't create %1 directory.").arg(d.absolutePath());
722 }
723 }
724 QString r = d.absolutePath();
725 removeEndingSlash(r);
726 QString configDir(s);
727 QFile configFile(configDir + QLatin1String("/user-dirs.dirs"));
728 if (!configFile.exists())
729 return fallback;
730
731 if (!configFile.open(QIODevice::ReadOnly | QIODevice::Text))
732 return fallback;
733
734 QString userDirVar(QLatin1String("XDG_") + folderName.toUpper() + QLatin1String("_DIR"));
735 QTextStream in(&configFile);
736 QString line;
737 while (!in.atEnd())
738 {
739 line = in.readLine();
740 if (line.contains(userDirVar))
741 {
742 configFile.close();
743
744 // get path between quotes
745 line = line.section(QLatin1Char('"'), 1, 1);
746 if (line.isEmpty())
747 return fallback;
748 line.replace(QLatin1String("$HOME"), QLatin1String("~"));
749 fixBashShortcuts(line);
750 return line;
751 }
752 }
753
754 configFile.close();
755 return fallback;
756 }
757
expandEnvVariables(const QString str)758 QString expandEnvVariables(const QString str)
759 {
760 QString scheme = QUrl(str).scheme();
761
762 if (scheme == QLatin1String("http") || scheme == QLatin1String("https") || scheme == QLatin1String("shttp") ||
763 scheme == QLatin1String("ftp") || scheme == QLatin1String("ftps") ||
764 scheme == QLatin1String("pop") || scheme == QLatin1String("pops") ||
765 scheme == QLatin1String("imap") || scheme == QLatin1String("imaps") ||
766 scheme == QLatin1String("mailto") ||
767 scheme == QLatin1String("nntp") ||
768 scheme == QLatin1String("irc") ||
769 scheme == QLatin1String("telnet") ||
770 scheme == QLatin1String("xmpp") ||
771 scheme == QLatin1String("nfs")
772 )
773 return str;
774
775 const QString homeDir = QFile::decodeName(qgetenv("HOME"));
776
777 QString res = str;
778 res.replace(QRegExp(QString::fromLatin1("~(?=$|/)")), homeDir);
779
780 replaceVar(res, QLatin1String("HOME"), homeDir);
781 replaceVar(res, QLatin1String("USER"), QString::fromLocal8Bit(qgetenv("USER")));
782
783 replaceVar(res, QLatin1String("XDG_DESKTOP_DIR"), userDir(Desktop));
784 replaceVar(res, QLatin1String("XDG_TEMPLATES_DIR"), userDir(Templates));
785 replaceVar(res, QLatin1String("XDG_DOCUMENTS_DIR"), userDir(Documents));
786 replaceVar(res, QLatin1String("XDG_MUSIC_DIR"), userDir(Music));
787 replaceVar(res, QLatin1String("XDG_PICTURES_DIR"), userDir(Pictures));
788 replaceVar(res, QLatin1String("XDG_VIDEOS_DIR"), userDir(Videos));
789 replaceVar(res, QLatin1String("XDG_PHOTOS_DIR"), userDir(Pictures));
790
791 return res;
792 }
793
794
expandExecString(const QStringList & urls) const795 QStringList XdgDesktopFile::expandExecString(const QStringList& urls) const
796 {
797 if (d->mType != ApplicationType)
798 return QStringList();
799
800 QStringList result;
801
802 QString execStr = value(execKey).toString();
803 unEscapeExec(execStr);
804 const QStringList tokens = parseCombinedArgString(execStr);
805
806 for (QString token : tokens)
807 {
808 // The parseCombinedArgString() splits the string by the space symbols,
809 // we temporarily replaced them on the special characters.
810 // Now we reverse it.
811 token.replace(01, QLatin1Char(' '));
812 token.replace(02, QLatin1Char('\t'));
813 token.replace(03, QLatin1Char('\n'));
814
815 // ----------------------------------------------------------
816 // A single file name, even if multiple files are selected.
817 if (token == QLatin1String("%f"))
818 {
819 continue;
820 }
821
822 // ----------------------------------------------------------
823 // A list of files. Use for apps that can open several local files at once.
824 // Each file is passed as a separate argument to the executable program.
825 if (token == QLatin1String("%F"))
826 {
827 continue;
828 }
829
830 // ----------------------------------------------------------
831 // A single URL. Local files may either be passed as file: URLs or as file path.
832 if (token == QLatin1String("%u"))
833 {
834 if (!urls.isEmpty())
835 {
836 QUrl url;
837 url.setUrl(expandEnvVariables(urls.at(0)));
838 result << ((!url.toLocalFile().isEmpty()) ? url.toLocalFile() : QString::fromUtf8(url.toEncoded()));
839 }
840 continue;
841 }
842
843 // ----------------------------------------------------------
844 // A list of URLs. Each URL is passed as a separate argument to the executable
845 // program. Local files may either be passed as file: URLs or as file path.
846 if (token == QLatin1String("%U"))
847 {
848 for (const QString &s : urls)
849 {
850 QUrl url(expandEnvVariables(s));
851 result << ((!url.toLocalFile().isEmpty()) ? url.toLocalFile() : QString::fromUtf8(url.toEncoded()));
852 }
853 continue;
854 }
855
856 // ----------------------------------------------------------
857 // The Icon key of the desktop entry expanded as two arguments, first --icon
858 // and then the value of the Icon key. Should not expand to any arguments if
859 // the Icon key is empty or missing.
860 if (token == QLatin1String("%i"))
861 {
862 QString icon = value(iconKey).toString();
863 if (!icon.isEmpty())
864 result << QLatin1String("-icon") << icon.replace(QLatin1Char('%'), QLatin1String("%%"));
865 continue;
866 }
867
868
869 // ----------------------------------------------------------
870 // The translated name of the application as listed in the appropriate Name key
871 // in the desktop entry.
872 if (token == QLatin1String("%c"))
873 {
874 result << localizedValue(nameKey).toString().replace(QLatin1Char('%'), QLatin1String("%%"));
875 continue;
876 }
877
878 // ----------------------------------------------------------
879 // The location of the desktop file as either a URI (if for example gotten from
880 // the vfolder system) or a local filename or empty if no location is known.
881 if (token == QLatin1String("%k"))
882 {
883 break;
884 }
885
886 // ----------------------------------------------------------
887 // Deprecated.
888 // Deprecated field codes should be removed from the command line and ignored.
889 if (token == QLatin1String("%d") || token == QLatin1String("%D") ||
890 token == QLatin1String("%n") || token == QLatin1String("%N") ||
891 token == QLatin1String("%v") || token == QLatin1String("%m")
892 )
893 {
894 continue;
895 }
896
897 // ----------------------------------------------------------
898 result << expandEnvVariables(token);
899 }
900
901 return result;
902 }
903
904
findDesktopFile(const QString & dirName,const QString & desktopName)905 QString findDesktopFile(const QString& dirName, const QString& desktopName)
906 {
907 QDir dir(dirName);
908 QFileInfo fi(dir, desktopName);
909
910 if (fi.exists())
911 return fi.canonicalFilePath();
912
913 // Working recursively ............
914 const QFileInfoList dirs = dir.entryInfoList(QStringList(), QDir::Dirs | QDir::NoDotAndDotDot);
915 for (const QFileInfo &d : dirs)
916 {
917 QString cn = d.canonicalFilePath();
918 if (dirName != cn)
919 {
920 QString f = findDesktopFile(cn, desktopName);
921 if (!f.isEmpty())
922 return f;
923 }
924 }
925
926 return QString();
927 }
928
findDesktopFile(const QString & desktopName)929 QString findDesktopFile(const QString& desktopName)
930 {
931 QString d = QFile::decodeName(qgetenv("XDG_DATA_DIRS"));
932 QStringList dirs = d.split(QLatin1Char(':'), QString::SkipEmptyParts);
933
934 if (dirs.isEmpty()) {
935 dirs.append(QString::fromLatin1("/usr/local/share"));
936 dirs.append(QString::fromLatin1("/usr/share"));
937 } else {
938 QMutableListIterator<QString> it(dirs);
939 while (it.hasNext()) {
940 const QString dir = it.next();
941 if (!dir.startsWith(QLatin1Char('/')))
942 it.remove();
943 }
944 }
945
946 dirs.removeDuplicates();
947 cleanAndAddPostfix(dirs, QString());
948
949 QString s = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
950 fixBashShortcuts(s);
951 removeEndingSlash(s);
952 dirs.prepend(s);
953 for (const QString &dirName : const_cast<const QStringList&>(dirs))
954 {
955 QString f = findDesktopFile(dirName + QLatin1String("/applications"), desktopName);
956 if (!f.isEmpty())
957 return f;
958 }
959
960 return QString();
961 }
962
963
964 // First, we look in following places for a default in specified order:
965 // ~/.config/mimeapps.list
966 // /etc/xdg/mimeapps.list
967 // ~/.local/share/applications/mimeapps.list
968 // /usr/local/share/applications/mimeapps.list
969 // /usr/share/applications/mimeapps.list
970
971
972