1 /*
2  * Copyright (C) 2013 - 2015  Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * This library 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 GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17  *
18  */
19 
20 #include "utilities.h"
21 #include "utilities_p.h"
22 #include <QApplication>
23 #include <QClipboard>
24 #include <QMimeData>
25 #include <QUrl>
26 #include <QList>
27 #include <QStringBuilder>
28 #include <QMessageBox>
29 #include <QStandardPaths>
30 #include "fileoperation.h"
31 #include <QEventLoop>
32 
33 #include <pwd.h>
34 #include <grp.h>
35 #include <cstdlib>
36 #include <glib.h>
37 
38 namespace Fm {
39 
pathListFromUriList(const char * uriList)40 Fm::FilePathList pathListFromUriList(const char* uriList) {
41     Fm::FilePathList pathList;
42     char** uris = g_strsplit_set(uriList, "\r\n", -1);
43     for(char** uri = uris; *uri; ++uri) {
44         if(**uri != '\0') {
45             pathList.push_back(Fm::FilePath::fromUri(*uri));
46         }
47     }
48     g_strfreev(uris);
49     return pathList;
50 }
51 
pathListToUriList(const Fm::FilePathList & paths)52 QByteArray pathListToUriList(const Fm::FilePathList& paths) {
53     QByteArray uriList;
54     for(auto& path: paths) {
55         uriList += path.uri().get();
56         uriList += "\r\n";
57     }
58     return uriList;
59 }
60 
pathListFromQUrls(QList<QUrl> urls)61 Fm::FilePathList pathListFromQUrls(QList<QUrl> urls) {
62     Fm::FilePathList pathList;
63     for(auto it = urls.cbegin(); it != urls.cend(); ++it) {
64         auto path = Fm::FilePath::fromUri(it->toString().toUtf8().constData());
65         pathList.push_back(std::move(path));
66     }
67     return pathList;
68 }
69 
pasteFilesFromClipboard(const Fm::FilePath & destPath,QWidget * parent)70 void pasteFilesFromClipboard(const Fm::FilePath& destPath, QWidget* parent) {
71     QClipboard* clipboard = QApplication::clipboard();
72     const QMimeData* data = clipboard->mimeData();
73     Fm::FilePathList paths;
74     bool isCut = false;
75 
76     if(data->hasFormat(QStringLiteral("x-special/gnome-copied-files"))) {
77         // Gnome, LXDE, and XFCE
78         QByteArray gnomeData = data->data(QStringLiteral("x-special/gnome-copied-files"));
79         char* pdata = gnomeData.data();
80         char* eol = strchr(pdata, '\n');
81 
82         if(eol) {
83             *eol = '\0';
84             isCut = (strcmp(pdata, "cut") == 0 ? true : false);
85             paths = pathListFromUriList(eol + 1);
86         }
87     }
88 
89     if(paths.empty() && data->hasUrls()) {
90         // The KDE way
91         paths = Fm::pathListFromQUrls(data->urls());
92         QByteArray cut = data->data(QStringLiteral("application/x-kde-cutselection"));
93         if(!cut.isEmpty() && QChar::fromLatin1(cut.at(0)) == QLatin1Char('1')) {
94             isCut = true;
95         }
96     }
97 
98     if(!paths.empty()) {
99         if(isCut) {
100             FileOperation::moveFiles(paths, destPath, parent);
101             clipboard->clear(QClipboard::Clipboard);
102         }
103         else {
104             FileOperation::copyFiles(paths, destPath, parent);
105         }
106     }
107 }
108 
copyFilesToClipboard(const Fm::FilePathList & files)109 void copyFilesToClipboard(const Fm::FilePathList& files) {
110     QClipboard* clipboard = QApplication::clipboard();
111     QMimeData* data = new QMimeData();
112     auto urilist = pathListToUriList(files);
113 
114     // Gnome, LXDE, and XFCE
115     // Note: the standard text/urilist format uses CRLF for line breaks, but gnome format uses LF only
116     data->setData(QStringLiteral("x-special/gnome-copied-files"), QByteArray("copy\n") + urilist.replace("\r\n", "\n"));
117     // The KDE way
118     data->setData(QStringLiteral("text/uri-list"), urilist);
119     // data->setData(QStringLiteral("application/x-kde-cutselection"), QByteArrayLiteral("0"));
120     clipboard->setMimeData(data);
121 }
122 
cutFilesToClipboard(const Fm::FilePathList & files)123 void cutFilesToClipboard(const Fm::FilePathList& files) {
124     QClipboard* clipboard = QApplication::clipboard();
125     QMimeData* data = new QMimeData();
126     auto urilist = pathListToUriList(files);
127 
128     // Gnome, LXDE, and XFCE
129     // Note: the standard text/urilist format uses CRLF for line breaks, but gnome format uses LF only
130     data->setData(QStringLiteral("x-special/gnome-copied-files"), QByteArray("cut\n") + urilist.replace("\r\n", "\n"));
131     // The KDE way
132     data->setData(QStringLiteral("text/uri-list"), urilist);
133     data->setData(QStringLiteral("application/x-kde-cutselection"), QByteArrayLiteral("1"));
134     clipboard->setMimeData(data);
135 }
136 
changeFileName(const Fm::FilePath & filePath,const QString & newName,QWidget * parent,bool showMessage)137 bool changeFileName(const Fm::FilePath& filePath, const QString& newName, QWidget* parent, bool showMessage) {
138     // NOTE: g_file_set_display_name() is used instead of g_file_move() because,
139     // otherwise, renaming will not be possible in places like google-drive:///.
140     Fm::GErrorPtr err;
141     GFilePtr gfile{g_file_set_display_name(filePath.gfile().get(),
142                                             newName.toLocal8Bit().constData(),
143                                             nullptr, /* make this cancellable later. */
144                                             &err)};
145     if(gfile == nullptr) {
146         if (showMessage){
147             QMessageBox::critical(parent ? parent->window() : nullptr, QObject::tr("Error"), err.message());
148         }
149         return false;
150     }
151 
152     // reload the containing folder if it is in use but does not have a file monitor
153     auto folder = Fm::Folder::findByPath(filePath.parent());
154     if(folder && folder->isValid() && folder->isLoaded() && !folder->hasFileMonitor()) {
155         folder->reload();
156     }
157 
158     return true;
159 }
160 
renameFile(std::shared_ptr<const Fm::FileInfo> file,QWidget * parent)161 bool renameFile(std::shared_ptr<const Fm::FileInfo> file, QWidget* parent) {
162     FilenameDialog dlg(parent ? parent->window() : nullptr);
163     dlg.setWindowTitle(QObject::tr("Rename File"));
164     dlg.setLabelText(QObject::tr("Please enter a new name:"));
165     // NOTE: "Edit name" seems the best way to handle non-UTF8 filename encoding.
166     auto old_name = QString::fromUtf8(g_file_info_get_edit_name(file->gFileInfo().get()));
167     if(old_name.isEmpty()) {
168         old_name = QString::fromStdString(file->name());
169     }
170     dlg.setTextValue(old_name);
171 
172     if(file->isDir()) { // select filename extension for directories
173         dlg.setSelectExtension(true);
174     }
175 
176     if(dlg.exec() != QDialog::Accepted) {
177         return false; // stop multiple renaming
178     }
179 
180     QString new_name = dlg.textValue();
181     if(new_name == old_name) {
182         return true; // let multiple renaming continue
183     }
184     changeFileName(file->path(), new_name, parent);
185     return true;
186 }
187 
setDefaultAppForType(const Fm::GAppInfoPtr app,std::shared_ptr<const Fm::MimeType> mimeType)188 void setDefaultAppForType(const Fm::GAppInfoPtr app, std::shared_ptr<const Fm::MimeType> mimeType) {
189     // NOTE: "g_app_info_set_as_default_for_type()" writes to "~/.config/mimeapps.list"
190     // but we want to set the default app only for the current DE (e.g., LXQt).
191     // More importantly, if the DE-specific list already exists and contains some
192     // default apps, it will have priority over "~/.config/mimeapps.list" and so,
193     // "g_app_info_set_as_default_for_type()" could not change those apps.
194 
195     if(app == nullptr || mimeType == nullptr) {
196         return;
197     }
198 
199     // first find the DE's mimeapps list file
200     QByteArray mimeappsList = "mimeapps.list";
201     QList<QByteArray> desktopsList = qgetenv("XDG_CURRENT_DESKTOP").toLower().split(':');
202     if(!desktopsList.isEmpty()) {
203         mimeappsList = desktopsList.at(0) + "-" + mimeappsList;
204     }
205     QString configDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
206     auto mimeappsListPath = CStrPtr(g_build_filename(configDir.toUtf8().constData(),
207                                                      mimeappsList.constData(),
208                                                      nullptr));
209 
210     // set the default app in the DE's mimeapps list
211     const char* desktop_id = g_app_info_get_id(app.get());
212     GKeyFile* kf = g_key_file_new();
213     g_key_file_load_from_file(kf, mimeappsListPath.get(), G_KEY_FILE_NONE, nullptr);
214     g_key_file_set_string(kf, "Default Applications", mimeType->name(), desktop_id);
215     g_key_file_save_to_file(kf, mimeappsListPath.get(), nullptr);
216     g_key_file_free(kf);
217 }
218 
219 // templateFile is a file path used as a template of the new file.
createFileOrFolder(CreateFileType type,FilePath parentDir,const TemplateItem * templ,QWidget * parent)220 void createFileOrFolder(CreateFileType type, FilePath parentDir, const TemplateItem* templ, QWidget* parent) {
221     QString defaultNewName;
222     QString prompt;
223     QString dialogTitle = type == CreateNewFolder ? QObject::tr("Create Folder")
224                           : QObject::tr("Create File");
225 
226     switch(type) {
227     case CreateNewTextFile:
228         prompt = QObject::tr("Please enter a new file name:");
229         defaultNewName = QObject::tr("New text file");
230         break;
231 
232     case CreateNewFolder:
233         prompt = QObject::tr("Please enter a new folder name:");
234         defaultNewName = QObject::tr("New folder");
235         break;
236 
237     case CreateWithTemplate: {
238         auto mime = templ->mimeType();
239         prompt = QObject::tr("Enter a name for the new %1:").arg(QString::fromUtf8(mime->desc()));
240         defaultNewName = QString::fromStdString(templ->name());
241     }
242     break;
243     }
244 
245 _retry:
246     // ask the user to input a file name
247     bool ok;
248     QString new_name = QInputDialog::getText(parent ? parent->window() : nullptr,
249                        dialogTitle,
250                        prompt,
251                        QLineEdit::Normal,
252                        defaultNewName,
253                        &ok);
254 
255     if(!ok) {
256         return;
257     }
258 
259     auto dest = parentDir.child(new_name.toLocal8Bit().data());
260     Fm::GErrorPtr err;
261     switch(type) {
262     case CreateNewTextFile: {
263         Fm::GFileOutputStreamPtr f{g_file_create(dest.gfile().get(), G_FILE_CREATE_NONE, nullptr, &err), false};
264         if(f) {
265             g_output_stream_close(G_OUTPUT_STREAM(f.get()), nullptr, nullptr);
266         }
267         break;
268     }
269     case CreateNewFolder:
270         g_file_make_directory(dest.gfile().get(), nullptr, &err);
271         break;
272     case CreateWithTemplate:
273         // copy the template file to its destination
274         FileOperation::copyFile(templ->filePath(), dest, parent);
275         break;
276     }
277     if(err) {
278         if(err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_EXISTS) {
279             err.reset();
280             goto _retry;
281         }
282 
283         QMessageBox::critical(parent ? parent->window() : nullptr, QObject::tr("Error"), err.message());
284     }
285     else { // reload the containing folder if it is in use but does not have a file monitor
286         auto folder = Fm::Folder::findByPath(parentDir);
287         if(folder && folder->isValid() && folder->isLoaded() && !folder->hasFileMonitor()) {
288             folder->reload();
289         }
290     }
291 }
292 
uidFromName(QString name)293 uid_t uidFromName(QString name) {
294     uid_t ret;
295     if(name.isEmpty()) {
296         return INVALID_UID;
297     }
298     if(name.at(0).digitValue() != -1) {
299         ret = uid_t(name.toUInt());
300     }
301     else {
302         struct passwd* pw = getpwnam(name.toLatin1().constData());
303         // FIXME: use getpwnam_r instead later to make it reentrant
304         ret = pw ? pw->pw_uid : INVALID_UID;
305     }
306 
307     return ret;
308 }
309 
uidToName(uid_t uid)310 QString uidToName(uid_t uid) {
311     QString ret;
312     struct passwd* pw = getpwuid(uid);
313 
314     if(pw) {
315         ret = QString::fromUtf8(pw->pw_name);
316     }
317     else {
318         ret = QString::number(uid);
319     }
320 
321     return ret;
322 }
323 
gidFromName(QString name)324 gid_t gidFromName(QString name) {
325     gid_t ret;
326     if(name.isEmpty()) {
327         return INVALID_GID;
328     }
329     if(name.at(0).digitValue() != -1) {
330         ret = gid_t(name.toUInt());
331     }
332     else {
333         // FIXME: use getgrnam_r instead later to make it reentrant
334         struct group* grp = getgrnam(name.toLatin1().constData());
335         ret = grp ? grp->gr_gid : INVALID_GID;
336     }
337 
338     return ret;
339 }
340 
gidToName(gid_t gid)341 QString gidToName(gid_t gid) {
342     QString ret;
343     struct group* grp = getgrgid(gid);
344 
345     if(grp) {
346         ret = QString::fromUtf8(grp->gr_name);
347     }
348     else {
349         ret = QString::number(gid);
350     }
351 
352     return ret;
353 }
354 
execModelessDialog(QDialog * dlg)355 int execModelessDialog(QDialog* dlg) {
356     // FIXME: this does much less than QDialog::exec(). Will this work flawlessly?
357     QEventLoop loop;
358     QObject::connect(dlg, &QDialog::finished, &loop, &QEventLoop::quit);
359     // DialogExec does not seem to be documented in the Qt API doc?
360     // However, in the source code of QDialog::exec(), it's used so let's use it too.
361     dlg->show();
362     (void)loop.exec(QEventLoop::DialogExec);
363     return dlg->result();
364 }
365 
366 // check if GVFS can support this uri scheme (lower case)
367 // NOTE: this does not work reliably due to some problems in gio/gvfs and causes bug lxqt/lxqt#512
368 // https://github.com/lxqt/lxqt/issues/512
369 // Use uriExists() whenever possible.
isUriSchemeSupported(const char * uriScheme)370 bool isUriSchemeSupported(const char* uriScheme) {
371     const gchar* const* schemes = g_vfs_get_supported_uri_schemes(g_vfs_get_default());
372     if(Q_UNLIKELY(schemes == nullptr)) {
373         return false;
374     }
375     for(const gchar * const* scheme = schemes; *scheme; ++scheme)
376         if(strcmp(uriScheme, *scheme) == 0) {
377             return true;
378         }
379     return false;
380 }
381 
382 // check if the URI exists.
383 // NOTE: this is a blocking call possibly involving I/O.
384 // So it's better to use it in limited cases, like checking trash:// or computer://.
385 // Avoid calling this on a slow filesystem.
386 // Checking "network:///" is very slow, for example.
uriExists(const char * uri)387 bool uriExists(const char* uri) {
388     GFile* gf = g_file_new_for_uri(uri);
389     bool ret = (bool)g_file_query_exists(gf, nullptr);
390     g_object_unref(gf);
391     return ret;
392 }
393 
formatFileSize(uint64_t size,bool useSI)394 QString formatFileSize(uint64_t size, bool useSI) {
395     Fm::CStrPtr str{g_format_size_full(size, useSI ? G_FORMAT_SIZE_DEFAULT : G_FORMAT_SIZE_IEC_UNITS)};
396     return QString(QString::fromUtf8(str.get()));
397 }
398 
399 } // namespace Fm
400