1 /*
2     This file is part of the KDE project
3     SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.0-only
6 */
7 
8 #include "kfileplacesitem_p.h"
9 
10 #include <QDateTime>
11 #include <QIcon>
12 
13 #include <KBookmarkManager>
14 #include <KConfig>
15 #include <KConfigGroup>
16 #include <KIconUtils>
17 #include <KLocalizedString>
18 #include <KMountPoint>
19 #include <kprotocolinfo.h>
20 #include <solid/block.h>
21 #include <solid/networkshare.h>
22 #include <solid/opticaldisc.h>
23 #include <solid/opticaldrive.h>
24 #include <solid/portablemediaplayer.h>
25 #include <solid/storageaccess.h>
26 #include <solid/storagedrive.h>
27 #include <solid/storagevolume.h>
28 
isTrash(const KBookmark & bk)29 static bool isTrash(const KBookmark &bk)
30 {
31     return bk.url().toString() == QLatin1String("trash:/");
32 }
33 
KFilePlacesItem(KBookmarkManager * manager,const QString & address,const QString & udi,KFilePlacesModel * parent)34 KFilePlacesItem::KFilePlacesItem(KBookmarkManager *manager, const QString &address, const QString &udi, KFilePlacesModel *parent)
35     : QObject(static_cast<QObject *>(parent))
36     , m_manager(manager)
37     , m_folderIsEmpty(true)
38     , m_isCdrom(false)
39     , m_isAccessible(false)
40 {
41     updateDeviceInfo(udi);
42     setBookmark(m_manager->findByAddress(address));
43 
44     if (udi.isEmpty() && m_bookmark.metaDataItem(QStringLiteral("ID")).isEmpty()) {
45         m_bookmark.setMetaDataItem(QStringLiteral("ID"), generateNewId());
46     } else if (udi.isEmpty()) {
47         if (isTrash(m_bookmark)) {
48             KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig);
49             const KConfigGroup group = cfg.group("Status");
50             m_folderIsEmpty = group.readEntry("Empty", true);
51         }
52     }
53 
54     // Hide SSHFS network device mounted by kdeconnect, since we already have the kdeconnect:// place.
55     if (isDevice() && m_access && device().vendor() == QLatin1String("fuse.sshfs")) {
56         // Not using findByPath() as it resolves symlinks, potentially blocking,
57         // but here we know we query for an existing actual mount point.
58         const auto mountPoints = KMountPoint::currentMountPoints();
59         for (const auto &mountPoint : mountPoints) {
60             if (mountPoint->mountPoint() == m_access->filePath()) {
61                 if (mountPoint->mountedFrom().startsWith(QLatin1String("kdeconnect@"))) {
62                     // Hide only if the user never set the "Hide" checkbox on the device.
63                     if (m_bookmark.metaDataItem(QStringLiteral("IsHidden")).isEmpty()) {
64                         setHidden(true);
65                     }
66                 }
67                 break;
68             }
69         }
70     }
71 }
72 
~KFilePlacesItem()73 KFilePlacesItem::~KFilePlacesItem()
74 {
75 }
76 
id() const77 QString KFilePlacesItem::id() const
78 {
79     if (isDevice()) {
80         return bookmark().metaDataItem(QStringLiteral("UDI"));
81     } else {
82         return bookmark().metaDataItem(QStringLiteral("ID"));
83     }
84 }
85 
hasSupportedScheme(const QStringList & schemes) const86 bool KFilePlacesItem::hasSupportedScheme(const QStringList &schemes) const
87 {
88     if (schemes.isEmpty()) {
89         return true;
90     }
91 
92     // StorageAccess is always local, doesn't need to be accessible to know this
93     if (m_access && schemes.contains(QLatin1String("file"))) {
94         return true;
95     }
96 
97     if (m_networkShare && schemes.contains(m_networkShare->url().scheme())) {
98         return true;
99     }
100 
101     if (m_player) {
102         const QStringList protocols = m_player->supportedProtocols();
103         for (const QString &protocol : protocols) {
104             if (schemes.contains(protocol)) {
105                 return true;
106             }
107         }
108     }
109 
110     return false;
111 }
112 
isDevice() const113 bool KFilePlacesItem::isDevice() const
114 {
115     return !bookmark().metaDataItem(QStringLiteral("UDI")).isEmpty();
116 }
117 
bookmark() const118 KBookmark KFilePlacesItem::bookmark() const
119 {
120     return m_bookmark;
121 }
122 
setBookmark(const KBookmark & bookmark)123 void KFilePlacesItem::setBookmark(const KBookmark &bookmark)
124 {
125     m_bookmark = bookmark;
126 
127     updateDeviceInfo(m_bookmark.metaDataItem(QStringLiteral("UDI")));
128 
129     if (bookmark.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true")) {
130         // This context must stay as it is - the translated system bookmark names
131         // are created with 'KFile System Bookmarks' as their context, so this
132         // ensures the right string is picked from the catalog.
133         // (coles, 13th May 2009)
134 
135         m_text = i18nc("KFile System Bookmarks", bookmark.text().toUtf8().data());
136     } else {
137         m_text = bookmark.text();
138     }
139 
140     const KFilePlacesModel::GroupType type = groupType();
141     switch (type) {
142     case KFilePlacesModel::PlacesType:
143         m_groupName = i18nc("@item", "Places");
144         break;
145     case KFilePlacesModel::RemoteType:
146         m_groupName = i18nc("@item", "Remote");
147         break;
148     case KFilePlacesModel::RecentlySavedType:
149         m_groupName = i18nc("@item The place group section name for recent dynamic lists", "Recent");
150         break;
151     case KFilePlacesModel::SearchForType:
152         m_groupName = i18nc("@item", "Search For");
153         break;
154     case KFilePlacesModel::DevicesType:
155         m_groupName = i18nc("@item", "Devices");
156         break;
157     case KFilePlacesModel::RemovableDevicesType:
158         m_groupName = i18nc("@item", "Removable Devices");
159         break;
160     case KFilePlacesModel::TagsType:
161         m_groupName = i18nc("@item", "Tags");
162         break;
163     default:
164         Q_UNREACHABLE();
165         break;
166     }
167 }
168 
device() const169 Solid::Device KFilePlacesItem::device() const
170 {
171     return m_device;
172 }
173 
data(int role) const174 QVariant KFilePlacesItem::data(int role) const
175 {
176     if (role == KFilePlacesModel::GroupRole) {
177         return QVariant(m_groupName);
178     } else if (role != KFilePlacesModel::HiddenRole && role != Qt::BackgroundRole && isDevice()) {
179         return deviceData(role);
180     } else {
181         return bookmarkData(role);
182     }
183 }
184 
groupType() const185 KFilePlacesModel::GroupType KFilePlacesItem::groupType() const
186 {
187     if (!isDevice()) {
188         const QString protocol = bookmark().url().scheme();
189         if (protocol == QLatin1String("timeline") || protocol == QLatin1String("recentlyused")) {
190             return KFilePlacesModel::RecentlySavedType;
191         }
192 
193         if (protocol.contains(QLatin1String("search"))) {
194             return KFilePlacesModel::SearchForType;
195         }
196 
197         if (protocol == QLatin1String("bluetooth") || protocol == QLatin1String("obexftp") || protocol == QLatin1String("kdeconnect")) {
198             return KFilePlacesModel::DevicesType;
199         }
200 
201         if (protocol == QLatin1String("tags")) {
202             return KFilePlacesModel::TagsType;
203         }
204 
205         if (protocol == QLatin1String("remote") || KProtocolInfo::protocolClass(protocol) != QLatin1String(":local")) {
206             return KFilePlacesModel::RemoteType;
207         } else {
208             return KFilePlacesModel::PlacesType;
209         }
210     }
211 
212     if (m_drive && (m_drive->isHotpluggable() || m_drive->isRemovable())) {
213         return KFilePlacesModel::RemovableDevicesType;
214     } else if (m_networkShare) {
215         return KFilePlacesModel::RemoteType;
216     } else {
217         return KFilePlacesModel::DevicesType;
218     }
219 }
220 
isHidden() const221 bool KFilePlacesItem::isHidden() const
222 {
223     return m_bookmark.metaDataItem(QStringLiteral("IsHidden")) == QLatin1String("true");
224 }
225 
setHidden(bool hide)226 void KFilePlacesItem::setHidden(bool hide)
227 {
228     if (m_bookmark.isNull() || isHidden() == hide) {
229         return;
230     }
231     m_bookmark.setMetaDataItem(QStringLiteral("IsHidden"), hide ? QStringLiteral("true") : QStringLiteral("false"));
232 }
233 
bookmarkData(int role) const234 QVariant KFilePlacesItem::bookmarkData(int role) const
235 {
236     KBookmark b = bookmark();
237 
238     if (b.isNull()) {
239         return QVariant();
240     }
241 
242     switch (role) {
243     case Qt::DisplayRole:
244         return m_text;
245     case Qt::DecorationRole:
246         return QIcon::fromTheme(iconNameForBookmark(b));
247     case Qt::BackgroundRole:
248         if (isHidden()) {
249             return QColor(Qt::lightGray);
250         } else {
251             return QVariant();
252         }
253     case KFilePlacesModel::UrlRole:
254         return b.url();
255     case KFilePlacesModel::SetupNeededRole:
256         return false;
257     case KFilePlacesModel::HiddenRole:
258         return isHidden();
259     case KFilePlacesModel::IconNameRole:
260         return iconNameForBookmark(b);
261     default:
262         return QVariant();
263     }
264 }
265 
deviceData(int role) const266 QVariant KFilePlacesItem::deviceData(int role) const
267 {
268     Solid::Device d = device();
269 
270     if (d.isValid()) {
271         switch (role) {
272         case Qt::DisplayRole:
273             return d.displayName();
274         case Qt::DecorationRole:
275             // qDebug() << "adding emblems" << m_emblems << "to device icon" << m_deviceIconName;
276             return KIconUtils::addOverlays(m_deviceIconName, m_emblems);
277         case KFilePlacesModel::UrlRole:
278             if (m_access) {
279                 const QString path = m_access->filePath();
280                 return path.isEmpty() ? QUrl() : QUrl::fromLocalFile(path);
281             } else if (m_disc && (m_disc->availableContent() & Solid::OpticalDisc::Audio) != 0) {
282                 Solid::Block *block = d.as<Solid::Block>();
283                 if (block) {
284                     QString device = block->device();
285                     return QUrl(QStringLiteral("audiocd:/?device=%1").arg(device));
286                 }
287                 // We failed to get the block device. Assume audiocd:/ can
288                 // figure it out, but cannot handle multiple disc drives.
289                 // See https://bugs.kde.org/show_bug.cgi?id=314544#c40
290                 return QUrl(QStringLiteral("audiocd:/"));
291             } else if (m_player) {
292                 const QStringList protocols = m_player->supportedProtocols();
293                 if (!protocols.isEmpty()) {
294                     return QUrl(QStringLiteral("%1:udi=%2").arg(protocols.first(), d.udi()));
295                 }
296                 return QVariant();
297             } else {
298                 return QVariant();
299             }
300         case KFilePlacesModel::SetupNeededRole:
301             if (m_access) {
302                 return !m_isAccessible;
303             } else {
304                 return QVariant();
305             }
306 
307         case KFilePlacesModel::FixedDeviceRole: {
308             if (m_drive != nullptr) {
309                 return !m_drive->isHotpluggable() && !m_drive->isRemovable();
310             }
311             return true;
312         }
313 
314         case KFilePlacesModel::CapacityBarRecommendedRole:
315             return m_isAccessible && !m_isCdrom;
316 
317         case KFilePlacesModel::IconNameRole:
318             return m_deviceIconName;
319 
320         default:
321             return QVariant();
322         }
323     } else {
324         return QVariant();
325     }
326 }
327 
createBookmark(KBookmarkManager * manager,const QString & label,const QUrl & url,const QString & iconName,KFilePlacesItem * after)328 KBookmark KFilePlacesItem::createBookmark(KBookmarkManager *manager, const QString &label, const QUrl &url, const QString &iconName, KFilePlacesItem *after)
329 {
330     KBookmarkGroup root = manager->root();
331     if (root.isNull()) {
332         return KBookmark();
333     }
334     QString empty_icon = iconName;
335     if (url.toString() == QLatin1String("trash:/")) {
336         if (empty_icon.endsWith(QLatin1String("-full"))) {
337             empty_icon.chop(5);
338         } else if (empty_icon.isEmpty()) {
339             empty_icon = QStringLiteral("user-trash");
340         }
341     }
342     KBookmark bookmark = root.addBookmark(label, url, empty_icon);
343     bookmark.setMetaDataItem(QStringLiteral("ID"), generateNewId());
344 
345     if (after) {
346         root.moveBookmark(bookmark, after->bookmark());
347     }
348 
349     return bookmark;
350 }
351 
createSystemBookmark(KBookmarkManager * manager,const char * translationContext,const QByteArray & untranslatedLabel,const QUrl & url,const QString & iconName,const KBookmark & after)352 KBookmark KFilePlacesItem::createSystemBookmark(KBookmarkManager *manager,
353                                                 const char *translationContext,
354                                                 const QByteArray &untranslatedLabel,
355                                                 const QUrl &url,
356                                                 const QString &iconName,
357                                                 const KBookmark &after)
358 {
359     Q_UNUSED(translationContext); // parameter is only necessary to force the caller
360     // to provide a marked-for-translation string for the label, with context
361 
362     KBookmark bookmark = createBookmark(manager, QString::fromUtf8(untranslatedLabel), url, iconName);
363     if (!bookmark.isNull()) {
364         bookmark.setMetaDataItem(QStringLiteral("isSystemItem"), QStringLiteral("true"));
365     }
366     if (!after.isNull()) {
367         manager->root().moveBookmark(bookmark, after);
368     }
369     return bookmark;
370 }
371 
createDeviceBookmark(KBookmarkManager * manager,const QString & udi)372 KBookmark KFilePlacesItem::createDeviceBookmark(KBookmarkManager *manager, const QString &udi)
373 {
374     KBookmarkGroup root = manager->root();
375     if (root.isNull()) {
376         return KBookmark();
377     }
378     KBookmark bookmark = root.createNewSeparator();
379     bookmark.setMetaDataItem(QStringLiteral("UDI"), udi);
380     bookmark.setMetaDataItem(QStringLiteral("isSystemItem"), QStringLiteral("true"));
381     return bookmark;
382 }
383 
createTagBookmark(KBookmarkManager * manager,const QString & tag)384 KBookmark KFilePlacesItem::createTagBookmark(KBookmarkManager *manager, const QString &tag)
385 {
386     KBookmark bookmark = createSystemBookmark(manager, tag.toUtf8().data(), tag.toUtf8(), QUrl(QLatin1String("tags:/") + tag), QLatin1String("tag"));
387     bookmark.setMetaDataItem(QStringLiteral("tag"), tag);
388     bookmark.setMetaDataItem(QStringLiteral("isSystemItem"), QStringLiteral("true"));
389 
390     return bookmark;
391 }
392 
generateNewId()393 QString KFilePlacesItem::generateNewId()
394 {
395     static int count = 0;
396 
397     //    return QString::number(count++);
398 
399     return QString::number(QDateTime::currentSecsSinceEpoch()) + QLatin1Char('/') + QString::number(count++);
400 
401     //    return QString::number(QDateTime::currentSecsSinceEpoch())
402     //         + '/' + QString::number(qrand());
403 }
404 
updateDeviceInfo(const QString & udi)405 bool KFilePlacesItem::updateDeviceInfo(const QString &udi)
406 {
407     if (m_device.udi() == udi) {
408         return false;
409     }
410 
411     if (m_access) {
412         m_access->disconnect(this);
413     }
414 
415     m_device = Solid::Device(udi);
416     if (m_device.isValid()) {
417         m_access = m_device.as<Solid::StorageAccess>();
418         m_volume = m_device.as<Solid::StorageVolume>();
419         m_disc = m_device.as<Solid::OpticalDisc>();
420         m_player = m_device.as<Solid::PortableMediaPlayer>();
421         m_networkShare = m_device.as<Solid::NetworkShare>();
422         m_deviceIconName = m_device.icon();
423         m_emblems = m_device.emblems();
424 
425         m_drive = nullptr;
426         Solid::Device parentDevice = m_device;
427         while (parentDevice.isValid() && !m_drive) {
428             m_drive = parentDevice.as<Solid::StorageDrive>();
429             parentDevice = parentDevice.parent();
430         }
431 
432         if (m_access) {
433             connect(m_access.data(), &Solid::StorageAccess::accessibilityChanged, this, &KFilePlacesItem::onAccessibilityChanged);
434             onAccessibilityChanged(m_access->isAccessible());
435         }
436     } else {
437         m_access = nullptr;
438         m_volume = nullptr;
439         m_disc = nullptr;
440         m_player = nullptr;
441         m_drive = nullptr;
442         m_networkShare = nullptr;
443         m_deviceIconName.clear();
444         m_emblems.clear();
445     }
446 
447     return true;
448 }
449 
onAccessibilityChanged(bool isAccessible)450 void KFilePlacesItem::onAccessibilityChanged(bool isAccessible)
451 {
452     m_isAccessible = isAccessible;
453     m_isCdrom =
454         m_device.is<Solid::OpticalDrive>() || m_device.parent().is<Solid::OpticalDrive>() || (m_volume && m_volume->fsType() == QLatin1String("iso9660"));
455     m_emblems = m_device.emblems();
456 
457     Q_EMIT itemChanged(id());
458 }
459 
iconNameForBookmark(const KBookmark & bookmark) const460 QString KFilePlacesItem::iconNameForBookmark(const KBookmark &bookmark) const
461 {
462     if (!m_folderIsEmpty && isTrash(bookmark)) {
463         return bookmark.icon() + QLatin1String("-full");
464     } else {
465         return bookmark.icon();
466     }
467 }
468 
469 #include "moc_kfileplacesitem_p.cpp"
470