xref: /OK3568_Linux_fs/app/QLauncher/xdgdesktopfile.cpp (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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