File indexing completed on 2024-05-05 03:56:08

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2004 David Faure <faure@kde.org>
0004     SPDX-FileCopyrightText: 2009 Christian Ehrlicher <ch.ehrlicher@gmx.de>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "kio_trash_win.h"
0010 #include "kio/job.h"
0011 #include "kioglobal_p.h"
0012 #include "kiotrashdebug.h"
0013 
0014 #include <QCoreApplication>
0015 #include <QDataStream>
0016 #include <QDateTime>
0017 
0018 #include <KConfigGroup>
0019 #include <KLocalizedString>
0020 
0021 #include <objbase.h>
0022 
0023 // Pseudo plugin class to embed meta data
0024 class KIOPluginForMetaData : public QObject
0025 {
0026     Q_OBJECT
0027     Q_PLUGIN_METADATA(IID "org.kde.kio.worker.trash" FILE "trash.json")
0028 };
0029 
0030 extern "C" {
0031 int Q_DECL_EXPORT kdemain(int argc, char **argv)
0032 {
0033     bool bNeedsUninit = (CoInitializeEx(NULL, COINIT_MULTITHREADED) == S_OK);
0034     // necessary to use other KIO workers
0035     QCoreApplication app(argc, argv);
0036 
0037     // start the worker
0038     TrashProtocol worker(argv[1], argv[2], argv[3]);
0039     worker.dispatchLoop();
0040 
0041     if (bNeedsUninit) {
0042         CoUninitialize();
0043     }
0044     return 0;
0045 }
0046 }
0047 
0048 static const qint64 KDE_SECONDS_SINCE_1601 = 11644473600LL;
0049 static const qint64 KDE_USEC_IN_SEC = 1000000LL;
0050 static const int WM_SHELLNOTIFY = (WM_USER + 42);
0051 #ifndef SHCNRF_InterruptLevel
0052 static const int SHCNRF_InterruptLevel = 0x0001;
0053 static const int SHCNRF_ShellLevel = 0x0002;
0054 static const int SHCNRF_RecursiveInterrupt = 0x1000;
0055 #endif
0056 
0057 static inline time_t filetimeToTime_t(const FILETIME *time)
0058 {
0059     ULARGE_INTEGER i64;
0060     i64.LowPart = time->dwLowDateTime;
0061     i64.HighPart = time->dwHighDateTime;
0062     i64.QuadPart /= KDE_USEC_IN_SEC * 10;
0063     i64.QuadPart -= KDE_SECONDS_SINCE_1601;
0064     return i64.QuadPart;
0065 }
0066 
0067 LRESULT CALLBACK trash_internal_proc(HWND hwnd, UINT message, WPARAM wp, LPARAM lp)
0068 {
0069     if (message == WM_SHELLNOTIFY) {
0070         TrashProtocol *that = (TrashProtocol *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
0071         that->updateRecycleBin();
0072     }
0073     return DefWindowProc(hwnd, message, wp, lp);
0074 }
0075 
0076 TrashProtocol::TrashProtocol(const QByteArray &protocol, const QByteArray &pool, const QByteArray &app)
0077     : WorkerBase(protocol, pool, app)
0078     , m_config(QString::fromLatin1("trashrc"), KConfig::SimpleConfig)
0079 {
0080     // create a hidden window to receive notifications through window messages
0081     const QString className = QLatin1String("TrashProtocol_Widget") + QString::number(quintptr(trash_internal_proc));
0082     HINSTANCE hi = GetModuleHandle(nullptr);
0083     WNDCLASS wc;
0084     memset(&wc, 0, sizeof(WNDCLASS));
0085     wc.lpfnWndProc = trash_internal_proc;
0086     wc.hInstance = hi;
0087     wc.lpszClassName = (LPCWSTR)className.utf16();
0088     RegisterClass(&wc);
0089     m_notificationWindow = CreateWindow(wc.lpszClassName, // classname
0090                                         wc.lpszClassName, // window name
0091                                         0, // style
0092                                         0,
0093                                         0,
0094                                         0,
0095                                         0, // geometry
0096                                         0, // parent
0097                                         0, // menu handle
0098                                         hi, // application
0099                                         0); // windows creation data.
0100     SetWindowLongPtr(m_notificationWindow, GWLP_USERDATA, (LONG_PTR)this);
0101 
0102     // get trash IShellFolder object
0103     LPITEMIDLIST iilTrash;
0104     IShellFolder *isfDesktop;
0105     // we assume that this will always work - if not we've a bigger problem than a kio_trash crash...
0106     SHGetFolderLocation(NULL, CSIDL_BITBUCKET, 0, 0, &iilTrash);
0107     SHGetDesktopFolder(&isfDesktop);
0108     isfDesktop->BindToObject(iilTrash, NULL, IID_IShellFolder2, (void **)&m_isfTrashFolder);
0109     isfDesktop->Release();
0110     SHGetMalloc(&m_pMalloc);
0111 
0112     // register for recycle bin notifications, have to do it for *every* single recycle bin
0113 #if 0
0114     // TODO: this does not work for devices attached after this loop here...
0115     DWORD dwSize = GetLogicalDriveStrings(0, NULL);
0116     LPWSTR pszDrives = (LPWSTR)malloc((dwSize + 2) * sizeof(WCHAR));
0117 #endif
0118 
0119     SHChangeNotifyEntry stPIDL;
0120     stPIDL.pidl = iilTrash;
0121     stPIDL.fRecursive = TRUE;
0122     m_hNotifyRBin = SHChangeNotifyRegister(m_notificationWindow,
0123                                            SHCNRF_InterruptLevel | SHCNRF_ShellLevel | SHCNRF_RecursiveInterrupt,
0124                                            SHCNE_ALLEVENTS,
0125                                            WM_SHELLNOTIFY,
0126                                            1,
0127                                            &stPIDL);
0128 
0129     ILFree(iilTrash);
0130 
0131     updateRecycleBin();
0132 }
0133 
0134 TrashProtocol::~TrashProtocol()
0135 {
0136     SHChangeNotifyDeregister(m_hNotifyRBin);
0137     const QString className = QLatin1String("TrashProtocol_Widget") + QString::number(quintptr(trash_internal_proc));
0138     UnregisterClass((LPCWSTR)className.utf16(), GetModuleHandle(nullptr));
0139     DestroyWindow(m_notificationWindow);
0140 
0141     if (m_pMalloc) {
0142         m_pMalloc->Release();
0143     }
0144     if (m_isfTrashFolder) {
0145         m_isfTrashFolder->Release();
0146     }
0147 }
0148 
0149 KIO::WorkerResult TrashProtocol::restore(const QUrl &trashURL, const QUrl &destURL)
0150 {
0151     LPITEMIDLIST pidl = NULL;
0152     LPCONTEXTMENU pCtxMenu = NULL;
0153 
0154     const QString path = trashURL.path().mid(1).replace(QLatin1Char('/'), QLatin1Char('\\'));
0155     LPWSTR lpFile = (LPWSTR)path.utf16();
0156     HRESULT res = m_isfTrashFolder->ParseDisplayName(0, 0, lpFile, 0, &pidl, 0);
0157     if (auto result = translateError(res); !result.success()) {
0158         return result;
0159     }
0160 
0161     res = m_isfTrashFolder->GetUIObjectOf(0, 1, (LPCITEMIDLIST *)&pidl, IID_IContextMenu, NULL, (LPVOID *)&pCtxMenu);
0162     if (auto result = translateError(res); !result.success()) {
0163         return result;
0164     }
0165 
0166     // this looks hacky but it's the only solution I found so far...
0167     HMENU hmenuCtx = CreatePopupMenu();
0168     res = pCtxMenu->QueryContextMenu(hmenuCtx, 0, 1, 0x00007FFF, CMF_NORMAL);
0169     if (auto result = translateError(res); !result.success()) {
0170         return result;
0171     }
0172 
0173     UINT uiCommand = ~0U;
0174     char verb[MAX_PATH];
0175     const int iMenuMax = GetMenuItemCount(hmenuCtx);
0176     for (int i = 0; i < iMenuMax; i++) {
0177         UINT uiID = GetMenuItemID(hmenuCtx, i) - 1;
0178         if ((uiID == -1) || (uiID == 0)) {
0179             continue;
0180         }
0181         res = pCtxMenu->GetCommandString(uiID, GCS_VERBA, NULL, verb, sizeof(verb));
0182         if (FAILED(res)) {
0183             continue;
0184         }
0185         if (stricmp(verb, "undelete") == 0) {
0186             uiCommand = uiID;
0187             break;
0188         }
0189     }
0190 
0191     KIO::WorkerResult result = KIO::WorkerResult::pass();
0192 
0193     if (uiCommand != ~0U) {
0194         CMINVOKECOMMANDINFO cmi;
0195 
0196         memset(&cmi, 0, sizeof(CMINVOKECOMMANDINFO));
0197         cmi.cbSize = sizeof(CMINVOKECOMMANDINFO);
0198         cmi.lpVerb = MAKEINTRESOURCEA(uiCommand);
0199         cmi.fMask = CMIC_MASK_FLAG_NO_UI;
0200         res = pCtxMenu->InvokeCommand((CMINVOKECOMMANDINFO *)&cmi);
0201 
0202         result = translateError(res);
0203     }
0204     DestroyMenu(hmenuCtx);
0205     pCtxMenu->Release();
0206     ILFree(pidl);
0207 
0208     return result;
0209 }
0210 
0211 KIO::WorkerResult TrashProtocol::clearTrash()
0212 {
0213     return translateError(SHEmptyRecycleBin(0, 0, 0));
0214 }
0215 
0216 KIO::WorkerResult TrashProtocol::rename(const QUrl &oldURL, const QUrl &newURL, KIO::JobFlags flags)
0217 {
0218     qCDebug(KIO_TRASH) << "TrashProtocol::rename(): old=" << oldURL << " new=" << newURL << " overwrite=" << (flags & KIO::Overwrite);
0219 
0220     if (oldURL.scheme() == QLatin1String("trash") && newURL.scheme() == QLatin1String("trash")) {
0221         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_RENAME, oldURL.toDisplayString());
0222     }
0223 
0224     return copyOrMove(oldURL, newURL, (flags & KIO::Overwrite), Move);
0225 }
0226 
0227 KIO::WorkerResult TrashProtocol::copy(const QUrl &src, const QUrl &dest, int /*permissions*/, KIO::JobFlags flags)
0228 {
0229     qCDebug(KIO_TRASH) << "TrashProtocol::copy(): " << src << " " << dest;
0230 
0231     if (src.scheme() == QLatin1String("trash") && dest.scheme() == QLatin1String("trash")) {
0232         return KIO::WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION, i18n("This file is already in the trash bin."));
0233     }
0234 
0235     return copyOrMove(src, dest, (flags & KIO::Overwrite), Copy);
0236 }
0237 
0238 KIO::WorkerResult TrashProtocol::copyOrMove(const QUrl &src, const QUrl &dest, bool overwrite, CopyOrMove action)
0239 {
0240     if (src.scheme() == QLatin1String("trash") && dest.isLocalFile()) {
0241         if (action == Move) {
0242             return restore(src, dest);
0243         } else {
0244             return KIO::WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION, i18n("not supported"));
0245         }
0246     } else if (src.isLocalFile() && dest.scheme() == QLatin1String("trash")) {
0247         UINT op = (action == Move) ? FO_DELETE : FO_COPY;
0248         if (auto result = doFileOp(src, FO_DELETE, FOF_ALLOWUNDO); !result.success()) {
0249             return result;
0250         }
0251         return KIO::WorkerResult::pass();
0252     } else {
0253         return KIO::WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION, i18n("Internal error in copyOrMove, should never happen"));
0254     }
0255 
0256     return KIO::WorkerResult::pass();
0257 }
0258 
0259 KIO::WorkerResult TrashProtocol::stat(const QUrl &url)
0260 {
0261     KIO::UDSEntry entry;
0262     if (url.path() == QLatin1String("/")) {
0263         STRRET strret;
0264         IShellFolder *isfDesktop;
0265         LPITEMIDLIST iilTrash;
0266 
0267         SHGetFolderLocation(NULL, CSIDL_BITBUCKET, 0, 0, &iilTrash);
0268         SHGetDesktopFolder(&isfDesktop);
0269         isfDesktop->BindToObject(iilTrash, NULL, IID_IShellFolder2, (void **)&m_isfTrashFolder);
0270         isfDesktop->GetDisplayNameOf(iilTrash, SHGDN_NORMAL, &strret);
0271         isfDesktop->Release();
0272         ILFree(iilTrash);
0273 
0274         entry.fastInsert(KIO::UDSEntry::UDS_NAME, QString::fromUtf16(reinterpret_cast<const char16_t *>(strret.pOleStr)));
0275         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0276         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700);
0277         entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("inode/directory"));
0278         m_pMalloc->Free(strret.pOleStr);
0279     } else {
0280         // TODO: when does this happen?
0281     }
0282     statEntry(entry);
0283     return KIO::WorkerResult::pass();
0284 }
0285 
0286 KIO::WorkerResult TrashProtocol::del(const QUrl &url, bool /*isfile*/)
0287 {
0288     if (auto result = doFileOp(url, FO_DELETE, 0); !result.success()) {
0289         return result;
0290     }
0291     return KIO::WorkerResult::pass();
0292 }
0293 
0294 KIO::WorkerResult TrashProtocol::listDir(const QUrl &url)
0295 {
0296     qCDebug(KIO_TRASH) << "TrashProtocol::listDir(): " << url;
0297     // There are no subfolders in Windows Trash
0298     return listRoot();
0299 }
0300 
0301 KIO::WorkerResult TrashProtocol::listRoot()
0302 {
0303     IEnumIDList *l;
0304     HRESULT res = m_isfTrashFolder->EnumObjects(0, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN, &l);
0305     if (res != S_OK) {
0306         return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, QStringLiteral("fixme!"));
0307     }
0308 
0309     STRRET strret;
0310     SFGAOF attribs;
0311     KIO::UDSEntry entry;
0312     LPITEMIDLIST i;
0313     WIN32_FIND_DATAW findData;
0314     while (l->Next(1, &i, NULL) == S_OK) {
0315         m_isfTrashFolder->GetDisplayNameOf(i, SHGDN_NORMAL, &strret);
0316         entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, QString::fromUtf16(reinterpret_cast<const char16_t *>(strret.pOleStr)));
0317         m_pMalloc->Free(strret.pOleStr);
0318         m_isfTrashFolder->GetDisplayNameOf(i, SHGDN_FORPARSING | SHGDN_INFOLDER, &strret);
0319         entry.fastInsert(KIO::UDSEntry::UDS_NAME, QString::fromUtf16(reinterpret_cast<const char16_t *>(strret.pOleStr)));
0320         m_pMalloc->Free(strret.pOleStr);
0321         m_isfTrashFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&i, &attribs);
0322         SHGetDataFromIDList(m_isfTrashFolder, i, SHGDFIL_FINDDATA, &findData, sizeof(findData));
0323         entry.fastInsert(KIO::UDSEntry::UDS_SIZE, ((quint64)findData.nFileSizeLow) + (((quint64)findData.nFileSizeHigh) << 32));
0324         entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, filetimeToTime_t(&findData.ftLastWriteTime));
0325         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, filetimeToTime_t(&findData.ftLastAccessTime));
0326         entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, filetimeToTime_t(&findData.ftCreationTime));
0327         entry.fastInsert(KIO::UDSEntry::UDS_EXTRA, QString::fromUtf16(reinterpret_cast<const char16_t *>(strret.pOleStr)));
0328         entry.fastInsert(KIO::UDSEntry::UDS_EXTRA + 1, QDateTime().toString(Qt::ISODate));
0329         mode_t type = QT_STAT_REG;
0330         if ((attribs & SFGAO_FOLDER) == SFGAO_FOLDER) {
0331             type = QT_STAT_DIR;
0332         }
0333         if ((attribs & SFGAO_LINK) == SFGAO_LINK) {
0334             type = QT_STAT_LNK;
0335         }
0336         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, type);
0337         mode_t access = 0700;
0338         if ((findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY) == FILE_ATTRIBUTE_READONLY) {
0339             type = 0300;
0340         }
0341         listEntry(entry);
0342 
0343         ILFree(i);
0344     }
0345     l->Release();
0346     return KIO::WorkerResult::pass();
0347 }
0348 
0349 KIO::WorkerResult TrashProtocol::special(const QByteArray &data)
0350 {
0351     QDataStream stream(data);
0352     int cmd;
0353     stream >> cmd;
0354 
0355     switch (cmd) {
0356     case 1:
0357         // empty trash folder
0358         return clearTrash();
0359     case 2:
0360         // convert old trash folder (non-windows only)
0361         return KIO::WorkerResult::pass();
0362     case 3: {
0363         QUrl url;
0364         stream >> url;
0365         return restore(url, QUrl());
0366     }
0367     default:
0368         qCWarning(KIO_TRASH) << "Unknown command in special(): " << cmd;
0369         return KIO::WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION, QString::number(cmd));
0370         break;
0371     }
0372 
0373     return KIO::WorkerResult::pass();
0374 }
0375 
0376 void TrashProtocol::updateRecycleBin()
0377 {
0378     IEnumIDList *l;
0379     HRESULT res = m_isfTrashFolder->EnumObjects(0, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN, &l);
0380     if (res != S_OK) {
0381         return;
0382     }
0383 
0384     bool bEmpty = true;
0385     LPITEMIDLIST i;
0386     if (l->Next(1, &i, NULL) == S_OK) {
0387         bEmpty = false;
0388         ILFree(i);
0389     }
0390     KConfigGroup group = m_config.group(QStringLiteral("Status"));
0391     group.writeEntry("Empty", bEmpty);
0392     m_config.sync();
0393     l->Release();
0394 }
0395 
0396 KIO::WorkerResult TrashProtocol::put(const QUrl &url, int /*permissions*/, KIO::JobFlags)
0397 {
0398     qCDebug(KIO_TRASH) << "put: " << url;
0399     // create deleted file. We need to get the mtime and original location from metadata...
0400     // Maybe we can find the info file for url.fileName(), in case ::rename() was called first, and failed...
0401     return KIO::WorkerResult::fail(KIO::ERR_ACCESS_DENIED, url.toDisplayString());
0402 }
0403 
0404 KIO::WorkerResult TrashProtocol::get(const QUrl &url)
0405 {
0406     // TODO
0407     return KIO::WorkerResult::pass();
0408 }
0409 
0410 KIO::WorkerResult TrashProtocol::doFileOp(const QUrl &url, UINT wFunc, FILEOP_FLAGS fFlags)
0411 {
0412     const QString path = url.path().replace(QLatin1Char('/'), QLatin1Char('\\'));
0413     // must be double-null terminated.
0414     QByteArray delBuf((path.length() + 2) * 2, 0);
0415     memcpy(delBuf.data(), path.utf16(), path.length() * 2);
0416 
0417     SHFILEOPSTRUCTW op;
0418     memset(&op, 0, sizeof(SHFILEOPSTRUCTW));
0419     op.wFunc = wFunc;
0420     op.pFrom = (LPCWSTR)delBuf.constData();
0421     op.fFlags = fFlags | FOF_NOCONFIRMATION | FOF_NOERRORUI;
0422     return translateError(SHFileOperationW(&op));
0423 }
0424 
0425 KIO::WorkerResult TrashProtocol::translateError(HRESULT hRes)
0426 {
0427     // TODO!
0428     if (FAILED(hRes)) {
0429         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, QLatin1String("fixme!"));
0430     }
0431     return KIO::WorkerResult::pass();
0432 }
0433 
0434 #include "kio_trash_win.moc"
0435 
0436 #include "moc_kio_trash_win.cpp"