File indexing completed on 2025-02-02 03:49:26
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"