File indexing completed on 2024-05-05 16:13:54

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.slave.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 slaves
0035     QCoreApplication app(argc, argv);
0036 
0037     // start the slave
0038     TrashProtocol slave(argv[1], argv[2], argv[3]);
0039     slave.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     : SlaveBase(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 void 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     bool bOk = translateError(res);
0158     if (!bOk) {
0159         return;
0160     }
0161 
0162     res = m_isfTrashFolder->GetUIObjectOf(0, 1, (LPCITEMIDLIST *)&pidl, IID_IContextMenu, NULL, (LPVOID *)&pCtxMenu);
0163     bOk = translateError(res);
0164     if (!bOk) {
0165         return;
0166     }
0167 
0168     // this looks hacky but it's the only solution I found so far...
0169     HMENU hmenuCtx = CreatePopupMenu();
0170     res = pCtxMenu->QueryContextMenu(hmenuCtx, 0, 1, 0x00007FFF, CMF_NORMAL);
0171     bOk = translateError(res);
0172     if (!bOk) {
0173         return;
0174     }
0175 
0176     UINT uiCommand = ~0U;
0177     char verb[MAX_PATH];
0178     const int iMenuMax = GetMenuItemCount(hmenuCtx);
0179     for (int i = 0; i < iMenuMax; i++) {
0180         UINT uiID = GetMenuItemID(hmenuCtx, i) - 1;
0181         if ((uiID == -1) || (uiID == 0)) {
0182             continue;
0183         }
0184         res = pCtxMenu->GetCommandString(uiID, GCS_VERBA, NULL, verb, sizeof(verb));
0185         if (FAILED(res)) {
0186             continue;
0187         }
0188         if (stricmp(verb, "undelete") == 0) {
0189             uiCommand = uiID;
0190             break;
0191         }
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         bOk = translateError(res);
0203         if (bOk) {
0204             finished();
0205         }
0206     }
0207     DestroyMenu(hmenuCtx);
0208     pCtxMenu->Release();
0209     ILFree(pidl);
0210 }
0211 
0212 void TrashProtocol::clearTrash()
0213 {
0214     translateError(SHEmptyRecycleBin(0, 0, 0));
0215     finished();
0216 }
0217 
0218 void TrashProtocol::rename(const QUrl &oldURL, const QUrl &newURL, KIO::JobFlags flags)
0219 {
0220     qCDebug(KIO_TRASH) << "TrashProtocol::rename(): old=" << oldURL << " new=" << newURL << " overwrite=" << (flags & KIO::Overwrite);
0221 
0222     if (oldURL.scheme() == QLatin1String("trash") && newURL.scheme() == QLatin1String("trash")) {
0223         error(KIO::ERR_CANNOT_RENAME, oldURL.toDisplayString());
0224         return;
0225     }
0226 
0227     copyOrMove(oldURL, newURL, (flags & KIO::Overwrite), Move);
0228 }
0229 
0230 void TrashProtocol::copy(const QUrl &src, const QUrl &dest, int /*permissions*/, KIO::JobFlags flags)
0231 {
0232     qCDebug(KIO_TRASH) << "TrashProtocol::copy(): " << src << " " << dest;
0233 
0234     if (src.scheme() == QLatin1String("trash") && dest.scheme() == QLatin1String("trash")) {
0235         error(KIO::ERR_UNSUPPORTED_ACTION, i18n("This file is already in the trash bin."));
0236         return;
0237     }
0238 
0239     copyOrMove(src, dest, (flags & KIO::Overwrite), Copy);
0240 }
0241 
0242 void TrashProtocol::copyOrMove(const QUrl &src, const QUrl &dest, bool overwrite, CopyOrMove action)
0243 {
0244     if (src.scheme() == QLatin1String("trash") && dest.isLocalFile()) {
0245         if (action == Move) {
0246             restore(src, dest);
0247         } else {
0248             error(KIO::ERR_UNSUPPORTED_ACTION, i18n("not supported"));
0249         }
0250         // Extracting (e.g. via dnd). Ignore original location stored in info file.
0251         return;
0252     } else if (src.isLocalFile() && dest.scheme() == QLatin1String("trash")) {
0253         UINT op = (action == Move) ? FO_DELETE : FO_COPY;
0254         if (!doFileOp(src, FO_DELETE, FOF_ALLOWUNDO)) {
0255             return;
0256         }
0257         finished();
0258         return;
0259     } else {
0260         error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Internal error in copyOrMove, should never happen"));
0261     }
0262 }
0263 
0264 void TrashProtocol::stat(const QUrl &url)
0265 {
0266     KIO::UDSEntry entry;
0267     if (url.path() == QLatin1String("/")) {
0268         STRRET strret;
0269         IShellFolder *isfDesktop;
0270         LPITEMIDLIST iilTrash;
0271 
0272         SHGetFolderLocation(NULL, CSIDL_BITBUCKET, 0, 0, &iilTrash);
0273         SHGetDesktopFolder(&isfDesktop);
0274         isfDesktop->BindToObject(iilTrash, NULL, IID_IShellFolder2, (void **)&m_isfTrashFolder);
0275         isfDesktop->GetDisplayNameOf(iilTrash, SHGDN_NORMAL, &strret);
0276         isfDesktop->Release();
0277         ILFree(iilTrash);
0278 
0279         entry.fastInsert(KIO::UDSEntry::UDS_NAME, QString::fromUtf16((const unsigned short *)strret.pOleStr));
0280         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0281         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700);
0282         entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("inode/directory"));
0283         m_pMalloc->Free(strret.pOleStr);
0284     } else {
0285         // TODO: when does this happen?
0286     }
0287     statEntry(entry);
0288     finished();
0289 }
0290 
0291 void TrashProtocol::del(const QUrl &url, bool /*isfile*/)
0292 {
0293     if (!doFileOp(url, FO_DELETE, 0)) {
0294         return;
0295     }
0296     finished();
0297 }
0298 
0299 void TrashProtocol::listDir(const QUrl &url)
0300 {
0301     qCDebug(KIO_TRASH) << "TrashProtocol::listDir(): " << url;
0302     // There are no subfolders in Windows Trash
0303     listRoot();
0304 }
0305 
0306 void TrashProtocol::listRoot()
0307 {
0308     IEnumIDList *l;
0309     HRESULT res = m_isfTrashFolder->EnumObjects(0, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN, &l);
0310     if (res != S_OK) {
0311         return;
0312     }
0313 
0314     STRRET strret;
0315     SFGAOF attribs;
0316     KIO::UDSEntry entry;
0317     LPITEMIDLIST i;
0318     WIN32_FIND_DATAW findData;
0319     while (l->Next(1, &i, NULL) == S_OK) {
0320         m_isfTrashFolder->GetDisplayNameOf(i, SHGDN_NORMAL, &strret);
0321         entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, QString::fromUtf16((const unsigned short *)strret.pOleStr));
0322         m_pMalloc->Free(strret.pOleStr);
0323         m_isfTrashFolder->GetDisplayNameOf(i, SHGDN_FORPARSING | SHGDN_INFOLDER, &strret);
0324         entry.fastInsert(KIO::UDSEntry::UDS_NAME, QString::fromUtf16((const unsigned short *)strret.pOleStr));
0325         m_pMalloc->Free(strret.pOleStr);
0326         m_isfTrashFolder->GetAttributesOf(1, (LPCITEMIDLIST *)&i, &attribs);
0327         SHGetDataFromIDList(m_isfTrashFolder, i, SHGDFIL_FINDDATA, &findData, sizeof(findData));
0328         entry.fastInsert(KIO::UDSEntry::UDS_SIZE, ((quint64)findData.nFileSizeLow) + (((quint64)findData.nFileSizeHigh) << 32));
0329         entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, filetimeToTime_t(&findData.ftLastWriteTime));
0330         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, filetimeToTime_t(&findData.ftLastAccessTime));
0331         entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, filetimeToTime_t(&findData.ftCreationTime));
0332         entry.fastInsert(KIO::UDSEntry::UDS_EXTRA, QString::fromUtf16((const unsigned short *)strret.pOleStr));
0333         entry.fastInsert(KIO::UDSEntry::UDS_EXTRA + 1, QDateTime().toString(Qt::ISODate));
0334         mode_t type = QT_STAT_REG;
0335         if ((attribs & SFGAO_FOLDER) == SFGAO_FOLDER) {
0336             type = QT_STAT_DIR;
0337         }
0338         if ((attribs & SFGAO_LINK) == SFGAO_LINK) {
0339             type = QT_STAT_LNK;
0340         }
0341         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, type);
0342         mode_t access = 0700;
0343         if ((findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY) == FILE_ATTRIBUTE_READONLY) {
0344             type = 0300;
0345         }
0346         listEntry(entry);
0347 
0348         ILFree(i);
0349     }
0350     l->Release();
0351     finished();
0352 }
0353 
0354 void TrashProtocol::special(const QByteArray &data)
0355 {
0356     QDataStream stream(data);
0357     int cmd;
0358     stream >> cmd;
0359 
0360     switch (cmd) {
0361     case 1:
0362         // empty trash folder
0363         clearTrash();
0364         break;
0365     case 2:
0366         // convert old trash folder (non-windows only)
0367         finished();
0368         break;
0369     case 3: {
0370         QUrl url;
0371         stream >> url;
0372         restore(url, QUrl());
0373         break;
0374     }
0375     default:
0376         qCWarning(KIO_TRASH) << "Unknown command in special(): " << cmd;
0377         error(KIO::ERR_UNSUPPORTED_ACTION, QString::number(cmd));
0378         break;
0379     }
0380 }
0381 
0382 void TrashProtocol::updateRecycleBin()
0383 {
0384     IEnumIDList *l;
0385     HRESULT res = m_isfTrashFolder->EnumObjects(0, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN, &l);
0386     if (res != S_OK) {
0387         return;
0388     }
0389 
0390     bool bEmpty = true;
0391     LPITEMIDLIST i;
0392     if (l->Next(1, &i, NULL) == S_OK) {
0393         bEmpty = false;
0394         ILFree(i);
0395     }
0396     KConfigGroup group = m_config.group("Status");
0397     group.writeEntry("Empty", bEmpty);
0398     m_config.sync();
0399     l->Release();
0400 }
0401 
0402 void TrashProtocol::put(const QUrl &url, int /*permissions*/, KIO::JobFlags)
0403 {
0404     qCDebug(KIO_TRASH) << "put: " << url;
0405     // create deleted file. We need to get the mtime and original location from metadata...
0406     // Maybe we can find the info file for url.fileName(), in case ::rename() was called first, and failed...
0407     error(KIO::ERR_ACCESS_DENIED, url.toDisplayString());
0408 }
0409 
0410 void TrashProtocol::get(const QUrl &url)
0411 {
0412     // TODO
0413 }
0414 
0415 bool TrashProtocol::doFileOp(const QUrl &url, UINT wFunc, FILEOP_FLAGS fFlags)
0416 {
0417     const QString path = url.path().replace(QLatin1Char('/'), QLatin1Char('\\'));
0418     // must be double-null terminated.
0419     QByteArray delBuf((path.length() + 2) * 2, 0);
0420     memcpy(delBuf.data(), path.utf16(), path.length() * 2);
0421 
0422     SHFILEOPSTRUCTW op;
0423     memset(&op, 0, sizeof(SHFILEOPSTRUCTW));
0424     op.wFunc = wFunc;
0425     op.pFrom = (LPCWSTR)delBuf.constData();
0426     op.fFlags = fFlags | FOF_NOCONFIRMATION | FOF_NOERRORUI;
0427     return translateError(SHFileOperationW(&op));
0428 }
0429 
0430 bool TrashProtocol::translateError(HRESULT hRes)
0431 {
0432     // TODO!
0433     if (FAILED(hRes)) {
0434         error(KIO::ERR_DOES_NOT_EXIST, QLatin1String("fixme!"));
0435         return false;
0436     }
0437     return true;
0438 }
0439 
0440 #include "kio_trash_win.moc"
0441 
0442 #include "moc_kio_trash_win.cpp"