1 //===========================================
2 //  Lumina-DE source code
3 //  Copyright (c) 2012-2017, Ken Moore
4 //  Available under the 3-clause BSD license
5 //  See the LICENSE file for full details
6 //===========================================
7 
8 #include <QApplication>
9 #include <QX11Info>
10 #include <QProcess>
11 #include <QProcessEnvironment>
12 #include <QFile>
13 #include <QFileInfo>
14 #include <QString>
15 #include <QUrl>
16 #include <QDebug>
17 #include <QTranslator>
18 #include <QMessageBox>
19 #include <QLabel>
20 #include <QDateTime>
21 #include <QPixmap>
22 #include <QColor>
23 #include <QDesktopWidget>
24 
25 #include "LFileDialog.h"
26 
27 #include <LuminaXDG.h>
28 #include <LUtils.h>
29 #include <LuminaOS.h>
30 #include <LuminaThemes.h>
31 
32 #define DEBUG 0
33 
printUsageInfo()34 void printUsageInfo(){
35   qDebug() << "lumina-open: Application launcher for the Lumina Desktop Environment";
36   qDebug() << "Description: Given a file (with absolute path) or URL, this utility will try to find the appropriate application with which to open the file. If the file is a *.desktop application shortcut, it will just start the application appropriately. It can also perform a few specific system operations if given special flags.";
37   qDebug() << "Usage: lumina-open [-select] [-action <ActionID>] <absolute file path or URL>";
38   qDebug() << "           lumina-open [-volumeup, -volumedown, -brightnessup, -brightnessdown]";
39   qDebug() << "  [-select] (optional) flag to bypass any default application settings and show the application selector window";
40   qDebug() << "  [-action <ActionID>] (optional) Flag to run one of the alternate Actions listed in a .desktop registration file rather than the main command.";
41   qDebug() << "Special Flags:";
42   qDebug() << " \"-volume[up/down]\" Flag to increase/decrease audio volume by 5%";
43   qDebug() << " \"-brightness[up/down]\" Flag to increase/decrease screen brightness by 5%";
44   qDebug() << " \"-autostart-apps\" Flag to launch all the various apps which are registered with XDG autostart specification";
45   qDebug() << "\"-terminal\" Flag to open the terminal currently set as the user's default";
46   exit(1);
47 }
48 
ShowErrorDialog(int argc,char ** argv,QString message)49 void ShowErrorDialog(int argc, char **argv, QString message){
50     //Setup the application
51     QApplication App(argc, argv);
52         App.setAttribute(Qt::AA_UseHighDpiPixmaps);
53 	LUtils::LoadTranslation(&App,"lumina-open");
54     QMessageBox dlg(QMessageBox::Critical, QObject::tr("File Error"), message );
55     dlg.exec();
56     exit(1);
57 }
58 
showOSD(int argc,char ** argv,QString message)59 void showOSD(int argc, char **argv, QString message){
60   //Setup the application
61   QApplication App(argc, argv);
62     LUtils::LoadTranslation(&App,"lumina-open");
63     App.setAttribute(Qt::AA_UseHighDpiPixmaps);
64   //Display the OSD
65   QPixmap pix(":/icons/OSD.png");
66   QLabel splash(0, Qt::Window | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint);
67      splash.setWindowTitle("");
68      splash.setStyleSheet("QLabel{background: black; color: white; font-weight: bold; font-size: 13pt; margin: 1ex;}");
69      splash.setAlignment(Qt::AlignCenter);
70 
71 
72   if(DEBUG) qDebug() << "Display OSD";
73   splash.setText(message);
74   //Make sure it is centered on the current screen
75   QPoint center = App.screenAt(QCursor::pos())->availableGeometry().center();
76   splash.move(center.x()-(splash.sizeHint().width()/2), center.y()-(splash.sizeHint().height()/2));
77   splash.show();
78   //qDebug() << " - show message";
79   //qDebug() << " - loop";
80   QDateTime end = QDateTime::currentDateTime().addMSecs(800);
81   while(QDateTime::currentDateTime() < end){ App.processEvents(); }
82   splash.hide();
83 }
84 
LaunchAutoStart()85 void LaunchAutoStart(){
86   QList<XDGDesktop*> xdgapps = LXDG::findAutoStartFiles();
87   for(int i=0; i<xdgapps.length(); i++){
88     //Generate command and clean up any stray "Exec" field codes (should not be any here)
89     QString cmd = xdgapps[i]->getDesktopExec();
90     if(cmd.contains("%")){cmd = cmd.remove("%U").remove("%u").remove("%F").remove("%f").remove("%i").remove("%c").remove("%k").simplified(); }
91     //Now run the command
92     if(!cmd.isEmpty()){
93       if(DEBUG) qDebug() << " - Auto-Starting File:" << xdgapps[i]->filePath;
94       QProcess::startDetached(cmd);
95     }
96   }
97   //make sure we clean up all the xdgapps structures
98   for(int i=0;  i<xdgapps.length(); i++){ xdgapps[i]->deleteLater(); }
99 }
100 
cmdFromUser(int argc,char ** argv,QString inFile,QString extension,QString & path,bool showDLG=false)101 QString cmdFromUser(int argc, char **argv, QString inFile, QString extension, QString& path, bool showDLG=false){
102   //First check to see if there is a default for this extension
103   QString defApp;
104   if(extension=="mimetype"){
105 	//qDebug() << "inFile:" << inFile;
106 	QStringList matches = LXDG::findAppMimeForFile(inFile, true).split("::::"); //allow multiple matches
107 	if(DEBUG) qDebug() << "Mimetype Matches:" << matches;
108 	for(int i=0; i<matches.length(); i++){
109 	  defApp = LXDG::findDefaultAppForMime(matches[i]);
110     //qDebug() << "MimeType:" << matches[i] << defApp;
111 	  if(!defApp.isEmpty()){ extension = matches[i]; break; }
112 	  else if(i+1==matches.length()){ extension = matches[0]; }
113 	}
114   }else{ defApp = LFileDialog::getDefaultApp(extension); }
115   if(DEBUG) qDebug() << "Mimetype:" << extension << "defApp:" << defApp;
116   if( !defApp.isEmpty() && !showDLG ){
117     if(defApp.endsWith(".desktop")){
118       XDGDesktop DF(defApp);
119       if(DF.isValid()){
120         QString exec = DF.getDesktopExec();
121         if(!exec.isEmpty()){
122           if(DEBUG) qDebug() << "[lumina-open] Using default application:" << DF.name << "File:" << inFile;
123           if(!DF.path.isEmpty()){ path = DF.path; }
124           return exec;
125         }
126       }
127     }else{
128     //Only binary given
129       if(LUtils::isValidBinary(defApp)){
130       if(DEBUG) qDebug() << "[lumina-open] Using default application:" << defApp << "File:" << inFile;
131       return defApp; //just use the binary
132     }
133   }
134     //invalid default - reset it and continue on
135     LFileDialog::setDefaultApp(extension, "");
136   }
137   //Final catch: directory given - no valid default found - use lumina-fm
138   if(extension=="inode/directory" && !showDLG){ return "lumina-fm"; }
139   //No default set -- Start up the application selection dialog
140   LTHEME::LoadCustomEnvSettings();
141   QApplication App(argc, argv);
142   App.setAttribute(Qt::AA_UseHighDpiPixmaps);
143   LUtils::LoadTranslation(&App,"lumina-open");
144 
145   LFileDialog w;
146   if(extension=="email" || extension.startsWith("x-scheme-handler/")){
147     //URL
148     w.setFileInfo(inFile, extension, false);
149   }else{
150     //File
151     if(inFile.endsWith("/")){ inFile.chop(1); }
152     w.setFileInfo(inFile.section("/",-1), extension, true);
153   }
154 
155   w.show();
156   App.exec();
157   if(!w.appSelected){ return ""; }
158   //Return the run path if appropriate
159   if(!w.appPath.isEmpty()){ path = w.appPath; }
160   //Just do the default application registration here for now
161   //  might move it to the runtime phase later after seeing that the app has successfully started
162   if(w.setDefault){
163     if(!w.appFile.isEmpty()){ LFileDialog::setDefaultApp(extension, w.appFile); }
164     else{ LFileDialog::setDefaultApp(extension, w.appExec); }
165   }
166   //Now return the resulting application command
167   return w.appExec;
168 }
169 
getCMD(int argc,char ** argv,QString & binary,QString & args,QString & path,bool & watch)170 void getCMD(int argc, char ** argv, QString& binary, QString& args, QString& path, bool& watch){
171 //Get the input file
172   //Make sure to load the proper system encoding first
173   LUtils::LoadTranslation(0,""); //bypass application modification
174 QString inFile, ActionID;
175 bool showDLG = false; //flag to bypass any default application setting
176 if(argc > 1){
177   for(int i=1; i<argc; i++){
178     if(QString(argv[i]).simplified() == "-select"){
179       showDLG = true;
180     }else if(QString(argv[i]).simplified() == "-testcrash"){
181 //Test the crash handler
182 binary = "internalcrashtest"; watch=true;
183 return;
184     }else if(QString(argv[i]).simplified() == "-autostart-apps"){
185 LaunchAutoStart();
186 return;
187     }else if(QString(argv[i]).simplified() == "-volumeup"){
188         bool isInt = false;
189         int volupVal = 5;
190         if(argc > i){
191             int parse = QString(argv[i+1]).toInt(&isInt,10);
192             if(isInt && 0 < parse && parse <= 20){
193                 volupVal = parse;
194             }
195         }
196         int vol = LOS::audioVolume()+volupVal;
197         if(vol>100){ vol=100; }
198         LOS::setAudioVolume(vol);
199         showOSD(argc,argv, QString(QObject::tr("Audio Volume %1%")).arg(QString::number(vol)) );
200         return;
201     }else if(QString(argv[i]).simplified() == "-volumedown"){
202         bool isInt = false;
203         int voldownVal = 5;
204         if(argc > i){
205             int parse = QString(argv[i+1]).toInt(&isInt,10);
206             if(isInt && 0 < parse && parse <= 20){
207                 voldownVal = parse;
208             }
209         }
210         int vol = LOS::audioVolume()-voldownVal; //decrease 5%
211         if(vol<0){ vol=0; }
212         LOS::setAudioVolume(vol);
213         showOSD(argc,argv, QString(QObject::tr("Audio Volume %1%")).arg(QString::number(vol)) );
214         return;
215     }else if(QString(argv[i]).simplified() == "-brightnessup"){
216 int bright = LOS::ScreenBrightness();
217 if(bright > 0){ //brightness control available
218   bright = bright+5; //increase 5%
219   if(bright>100){ bright = 100; }
220   LOS::setScreenBrightness(bright);
221   showOSD(argc,argv, QString(QObject::tr("Screen Brightness %1%")).arg(QString::number(bright)) );
222 }
223 return;
224     }else if(QString(argv[i]).simplified() == "-brightnessdown"){
225 int bright = LOS::ScreenBrightness();
226 if(bright > 0){ //brightness control available
227   bright = bright-5; //decrease 5%
228   if(bright<0){ bright = 0; }
229   LOS::setScreenBrightness(bright);
230   showOSD(argc,argv, QString(QObject::tr("Screen Brightness %1%")).arg(QString::number(bright)) );
231 }
232 return;
233     }else if( (QString(argv[i]).simplified() =="-action") && (argc>(i+1)) ){
234       ActionID = QString(argv[i+1]);
235 i++; //skip the next input
236     }else if(QString(argv[i]).simplified()=="-terminal"){
237       inFile = LXDG::findDefaultAppForMime("application/terminal");
238       break;
239     }else{
240       inFile = QString::fromLocal8Bit(argv[i]);
241         break;
242       }
243     }
244   }else{
245     printUsageInfo();
246   }
247 
248 
249   //Make sure that it is a valid file/URL
250   bool isFile=false; bool isUrl=false;
251   QString extension;
252   //Quick check/replacement for the URL syntax of a file
253   if(inFile.startsWith("file://")){ inFile = QUrl(inFile).toLocalFile(); } //change from URL to file format for a local file
254   //First make sure this is not a binary name first
255   QString bin = inFile.section(" ",0,0).simplified();
256   if(LUtils::isValidBinary(bin) && !bin.endsWith(".desktop") && !QFileInfo(inFile).isDir() ){isFile=true; }
257   //Now check what type of file this is
258   else if(QFile::exists(inFile)){ isFile=true; }
259   else if(QFile::exists(QDir::currentPath()+"/"+inFile)){isFile=true; inFile = QDir::currentPath()+"/"+inFile;} //account for relative paths
260   else if(inFile.startsWith("mailto:")){ isUrl= true; }
261   else if(QUrl(inFile).isValid() && !inFile.startsWith("/") ){ isUrl=true; }
262   if( !isFile && !isUrl ){ ShowErrorDialog( argc, argv, QString(QObject::tr("Invalid file or URL: %1")).arg(inFile) ); }
263   //Determing the type of file (extension)
264   //qDebug() << "File Type:" << isFile << isUrl;
265   if(isFile && extension.isEmpty()){
266     QFileInfo info(inFile);
267     extension=info.suffix();
268     //qDebug() << " - Extension:" << extension;
269     if(info.isDir()){ extension="inode/directory"; }
270     else if(info.isExecutable() && (extension.isEmpty() || extension=="sh") ){ extension="binary"; }
271     else if(extension!="desktop"){ extension="mimetype"; } //flag to check for mimetype default based on file
272   }
273   else if(isUrl && inFile.startsWith("mailto:")){ extension = "application/email"; }
274   else if(isUrl && inFile.contains("://") ){ extension = "x-scheme-handler/"+inFile.section("://",0,0); }
275   else if(isUrl && inFile.startsWith("www.")){ extension = "x-scheme-handler/http"; inFile.prepend("http://"); } //this catches partial (but still valid) URL's ("www.<something>" for instance)
276   //qDebug() << "Input:" << inFile << isFile << isUrl << extension;
277   //if not an application  - find the right application to open the file
278   QString cmd;
279   bool useInputFile = false;
280   if(extension=="desktop" && !showDLG){
281     XDGDesktop DF(inFile);
282     if(!DF.isValid()){
283       ShowErrorDialog( argc, argv, QString(QObject::tr("Application entry is invalid: %1")).arg(inFile) );
284     }
285     switch(DF.type){
286       case XDGDesktop::APP:
287         if(DEBUG) qDebug() << "Found .desktop application:" << ActionID;
288         if(!DF.exec.isEmpty()){
289           cmd = DF.getDesktopExec(ActionID);
290           if(DEBUG) qDebug() << "Got command:" << cmd;
291           if(!DF.path.isEmpty()){ path = DF.path; }
292           watch = DF.startupNotify || !DF.filePath.contains("/xdg/autostart/");
293         }else{
294           ShowErrorDialog( argc, argv, QString(QObject::tr("Application shortcut is missing the launching information (malformed shortcut): %1")).arg(inFile) );
295         }
296         break;
297       case XDGDesktop::LINK:
298         if(!DF.url.isEmpty()){
299           //This is a URL - so adjust the input variables appropriately
300           inFile = DF.url;
301           cmd.clear();
302           extension = inFile.section(":",0,0);
303         if(extension=="file"){ extension = "http"; } //local file URL - Make sure we use the default browser for a LINK type
304           extension.prepend("x-scheme-handler/");
305           watch = DF.startupNotify || !DF.filePath.contains("/xdg/autostart/");
306         }else{
307           ShowErrorDialog( argc, argv, QString(QObject::tr("URL shortcut is missing the URL: %1")).arg(inFile) );
308         }
309         break;
310       case XDGDesktop::DIR:
311         if(!DF.path.isEmpty()){
312           //This is a directory link - adjust inputs
313           inFile = DF.path;
314           cmd.clear();
315           extension = "inode/directory";
316           watch = DF.startupNotify || !DF.filePath.contains("/xdg/autostart/");
317         }else{
318           ShowErrorDialog( argc, argv, QString(QObject::tr("Directory shortcut is missing the path to the directory: %1")).arg(inFile) );
319         }
320         break;
321       default:
322         if(DEBUG) qDebug() << DF.type << DF.name << DF.icon << DF.exec;
323         ShowErrorDialog( argc, argv, QString(QObject::tr("Unknown type of shortcut : %1")).arg(inFile) );
324     }
325   }
326   if(cmd.isEmpty()){
327     if(extension=="binary" && !showDLG){ cmd = inFile; }
328     else{
329     //Find out the proper application to use this file/directory
330     useInputFile=true;
331     cmd = cmdFromUser(argc, argv, inFile, extension, path, showDLG);
332     if(cmd.isEmpty()){ return; }
333     }
334   }
335   //Now assemble the exec string (replace file/url field codes as necessary)
336   if(useInputFile){
337     args = inFile; //just to keep them distinct internally
338     // NOTE: lumina-open is only designed for a single input file,
339     //    so no need to distinguish between the list codes (uppercase)
340     //    and the single-file codes (lowercase)
341     //Special "inFile" format replacements for input codes
342     if( (cmd.contains("%f") || cmd.contains("%F") ) ){
343       //Apply any special field replacements for the desired format
344       inFile.replace("%20"," ");
345       if(inFile.startsWith("file://")){ inFile.remove(0,7); } //chop that URL prefix off the front (should have happened earlier - just make sure)
346       //Now replace the field codes
347       cmd.replace("%f","\""+inFile+"\"");
348       cmd.replace("%F","\""+inFile+"\"");
349     }else if( (cmd.contains("%U") || cmd.contains("%u")) ){
350       //Apply any special field replacements for the desired format
351       if(!inFile.contains("://") && !isUrl){ inFile.prepend("file://"); } //local file - add the extra flag
352       inFile.replace(" ", "%20");
353       //Now replace the field codes
354       cmd.replace("%u","\""+inFile+"\"");
355       cmd.replace("%U","\""+inFile+"\"");
356     }else{
357       //No field codes (or improper field codes given in the file - which is quite common)
358       // - Just tack the input file on the end and let the app handle it as necessary
359       inFile.replace("%20"," "); //assume a local-file format rather than URL format
360       cmd.append(" \""+inFile+"\"");
361     }
362   }
363   if(DEBUG) qDebug() << "Found Command:" << cmd << "Extension:" << extension;
364   //Clean up any leftover "Exec" field codes (should have already been replaced earlier)
365   if(cmd.contains("%")){cmd = cmd.remove("%U").remove("%u").remove("%F").remove("%f").remove("%i").remove("%c").remove("%k").simplified(); }
366   binary = cmd; //pass this string to the calling function
367 
368 }
369 
main(int argc,char ** argv)370 int main(int argc, char **argv){
371   //Run all the actual code in a separate function to have as little memory usage
372   //  as possible aside from the main application when running
373 
374   //Make sure the XDG environment variables exist first
375   LXDG::setEnvironmentVars();
376   //now get the command
377   QString cmd, args, path;
378   bool watch = true; //enable the crash handler by default (only disabled for some *.desktop inputs)
379   getCMD(argc, argv, cmd, args, path, watch);
380   //qDebug() << "Run CMD:" << cmd << args;
381   //Now run the command (move to execvp() later?)
382   if(cmd.isEmpty()){ return 0; } //no command to run (handled internally)
383   QString bin = cmd.section(" ",0,0);
384   if( !LUtils::isValidBinary(bin) ){
385     //invalid binary for some reason - open a dialog to warn the user instead
386     ShowErrorDialog(argc,argv, QString(QObject::tr("Could not find \"%1\". Please ensure it is installed first.")).arg(bin)+"\n\n"+cmd);
387     return 1;
388   }
389   if(DEBUG) qDebug() << "[lumina-open] Running Cmd:" << cmd;
390   int retcode = 0;
391   //Provide an override file for never watching running processes.
392   if(watch){ watch = !QFile::exists( QString(getenv("XDG_CONFIG_HOME"))+"/lumina-desktop/nowatch" ); }
393   //Do the slimmer run routine if no watching needed
394   if(!watch && path.isEmpty()){
395       //Nothing special about this one - just start it detached (less overhead)
396       QProcess::startDetached(cmd);
397   }else{
398     //Keep an eye on this process for errors and notify the user if it crashes
399     QString log;
400     if(cmd.contains("\\\\")){
401       //Special case (generally for Wine applications)
402       cmd = cmd.replace("\\\\","\\");
403       retcode = system(cmd.toLocal8Bit()); //need to run it through the "system" instead of QProcess
404     }else if(cmd=="internalcrashtest"){
405       log = "This is a sample crash log";
406       retcode = -1;
407     }else{
408       QProcess *p = new QProcess();
409       p->setProcessEnvironment(QProcessEnvironment::systemEnvironment());
410       if(!path.isEmpty() && QFile::exists(path)){
411         //qDebug() << " - Setting working path:" << path;
412         p->setWorkingDirectory(path);
413       }
414       p->start(cmd);
415 
416       //Now check up on it once every minute until it is finished
417       while(!p->waitForFinished(60000)){
418         //qDebug() << "[lumina-open] process check:" << p->state();
419         if(p->state() != QProcess::Running){ break; } //somehow missed the finished signal
420       }
421       retcode = p->exitCode();
422       if( (p->exitStatus()==QProcess::CrashExit) && retcode ==0){ retcode=-1; } //so we catch it later
423       log = QString(p->readAllStandardError());
424       if(log.isEmpty()){ log = QString(p->readAllStandardOutput()); }
425     }
426     //qDebug() << "[lumina-open] Finished Cmd:" << cmd << retcode << p->exitStatus();
427     if( QFile::exists("/tmp/.luminastopping") ){ watch = false; } //closing down session - ignore "crashes" (app could have been killed during cleanup)
428     if( (retcode < 0) && watch){ //-1 is used internally for crashed processes - most apps return >=0
429       qDebug() << "[lumina-open] Application Error:" << retcode;
430       //Setup the application
431       QApplication App(argc, argv);
432       App.setAttribute(Qt::AA_UseHighDpiPixmaps);
433       LUtils::LoadTranslation(&App,"lumina-open");
434 	//App.setApplicationName("LuminaOpen");
435       QMessageBox dlg(QMessageBox::Critical, QObject::tr("Application Error"), QObject::tr("The following application experienced an error and needed to close:")+"\n\n"+cmd );
436       dlg.setWindowFlags(Qt::Window);
437       if(!log.isEmpty()){ dlg.setDetailedText(log); }
438       dlg.exec();
439     }
440   }
441   return retcode;
442 }
443