socketapi.cpp 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247
  1. /*
  2. * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
  3. * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
  4. * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
  5. *
  6. * This program is free software; you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation; either version 2 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful, but
  12. * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  13. * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  14. * for more details.
  15. */
  16. #include "socketapi.h"
  17. #include "socketapi_p.h"
  18. #include "conflictdialog.h"
  19. #include "conflictsolver.h"
  20. #include "config.h"
  21. #include "configfile.h"
  22. #include "folderman.h"
  23. #include "folder.h"
  24. #include "theme.h"
  25. #include "common/syncjournalfilerecord.h"
  26. #include "syncengine.h"
  27. #include "syncfileitem.h"
  28. #include "filesystem.h"
  29. #include "version.h"
  30. #include "account.h"
  31. #include "accountstate.h"
  32. #include "account.h"
  33. #include "capabilities.h"
  34. #include "common/asserts.h"
  35. #include "guiutility.h"
  36. #ifndef OWNCLOUD_TEST
  37. #include "sharemanager.h"
  38. #endif
  39. #include <array>
  40. #include <QBitArray>
  41. #include <QUrl>
  42. #include <QMetaMethod>
  43. #include <QMetaObject>
  44. #include <QStringList>
  45. #include <QScopedPointer>
  46. #include <QFile>
  47. #include <QDir>
  48. #include <QApplication>
  49. #include <QLocalSocket>
  50. #include <QStringBuilder>
  51. #include <QMessageBox>
  52. #include <QInputDialog>
  53. #include <QFileDialog>
  54. #include <QAction>
  55. #include <QJsonDocument>
  56. #include <QJsonObject>
  57. #include <QWidget>
  58. #include <QClipboard>
  59. #include <QDesktopServices>
  60. #include <QStandardPaths>
  61. #ifdef Q_OS_MAC
  62. #include <CoreFoundation/CoreFoundation.h>
  63. #endif
  64. // This is the version that is returned when the client asks for the VERSION.
  65. // The first number should be changed if there is an incompatible change that breaks old clients.
  66. // The second number should be changed when there are new features.
  67. #define MIRALL_SOCKET_API_VERSION "1.1"
  68. #define DEBUG qDebug() << "SocketApi: "
  69. namespace {
  70. #if GUI_TESTING
  71. QWidget *findWidget(const QString &objectName)
  72. {
  73. auto widgets = QApplication::allWidgets();
  74. auto foundWidget = std::find_if(widgets.constBegin(), widgets.constEnd(), [&](QWidget *widget) {
  75. return widget->objectName() == objectName;
  76. });
  77. if (foundWidget == widgets.constEnd()) {
  78. return nullptr;
  79. }
  80. return *foundWidget;
  81. }
  82. #endif
  83. static inline QString removeTrailingSlash(QString path)
  84. {
  85. Q_ASSERT(path.endsWith(QLatin1Char('/')));
  86. path.truncate(path.length() - 1);
  87. return path;
  88. }
  89. static QString buildMessage(const QString &verb, const QString &path, const QString &status = QString())
  90. {
  91. QString msg(verb);
  92. if (!status.isEmpty()) {
  93. msg.append(QLatin1Char(':'));
  94. msg.append(status);
  95. }
  96. if (!path.isEmpty()) {
  97. msg.append(QLatin1Char(':'));
  98. QFileInfo fi(path);
  99. msg.append(QDir::toNativeSeparators(fi.absoluteFilePath()));
  100. }
  101. return msg;
  102. }
  103. }
  104. namespace OCC {
  105. Q_LOGGING_CATEGORY(lcSocketApi, "nextcloud.gui.socketapi", QtInfoMsg)
  106. Q_LOGGING_CATEGORY(lcPublicLink, "nextcloud.gui.socketapi.publiclink", QtInfoMsg)
  107. void SocketListener::sendMessage(const QString &message, bool doWait) const
  108. {
  109. if (!socket) {
  110. qCInfo(lcSocketApi) << "Not sending message to dead socket:" << message;
  111. return;
  112. }
  113. qCInfo(lcSocketApi) << "Sending SocketAPI message -->" << message << "to" << socket;
  114. QString localMessage = message;
  115. if (!localMessage.endsWith(QLatin1Char('\n'))) {
  116. localMessage.append(QLatin1Char('\n'));
  117. }
  118. QByteArray bytesToSend = localMessage.toUtf8();
  119. qint64 sent = socket->write(bytesToSend);
  120. if (doWait) {
  121. socket->waitForBytesWritten(1000);
  122. }
  123. if (sent != bytesToSend.length()) {
  124. qCWarning(lcSocketApi) << "Could not send all data on socket for " << localMessage;
  125. }
  126. }
  127. struct ListenerHasSocketPred
  128. {
  129. QIODevice *socket;
  130. ListenerHasSocketPred(QIODevice *socket)
  131. : socket(socket)
  132. {
  133. }
  134. bool operator()(const SocketListener &listener) const { return listener.socket == socket; }
  135. };
  136. SocketApi::SocketApi(QObject *parent)
  137. : QObject(parent)
  138. {
  139. QString socketPath;
  140. qRegisterMetaType<SocketListener *>("SocketListener*");
  141. qRegisterMetaType<QSharedPointer<SocketApiJob>>("QSharedPointer<SocketApiJob>");
  142. if (Utility::isWindows()) {
  143. socketPath = QLatin1String(R"(\\.\pipe\)")
  144. + QLatin1String(APPLICATION_EXECUTABLE)
  145. + QLatin1String("-")
  146. + QString::fromLocal8Bit(qgetenv("USERNAME"));
  147. // TODO: once the windows extension supports multiple
  148. // client connections, switch back to the theme name
  149. // See issue #2388
  150. // + Theme::instance()->appName();
  151. } else if (Utility::isMac()) {
  152. // This must match the code signing Team setting of the extension
  153. // Example for developer builds (with ad-hoc signing identity): "" "com.owncloud.desktopclient" ".socketApi"
  154. // Example for official signed packages: "9B5WD74GWJ." "com.owncloud.desktopclient" ".socketApi"
  155. socketPath = SOCKETAPI_TEAM_IDENTIFIER_PREFIX APPLICATION_REV_DOMAIN ".socketApi";
  156. #ifdef Q_OS_MAC
  157. int ret = 0;
  158. CFURLRef url = (CFURLRef)CFAutorelease((CFURLRef)CFBundleCopyBundleURL(CFBundleGetMainBundle()));
  159. QString bundlePath = QUrl::fromCFURL(url).path();
  160. QString cmd;
  161. // Tell Finder to use the Extension (checking it from System Preferences -> Extensions)
  162. cmd = QString("pluginkit -v -e use -i " APPLICATION_REV_DOMAIN ".FinderSyncExt");
  163. ret = system(cmd.toLocal8Bit());
  164. // Add it again. This was needed for Mojave to trigger a load.
  165. cmd = QString("pluginkit -v -a ") + bundlePath + "Contents/PlugIns/FinderSyncExt.appex/";
  166. ret = system(cmd.toLocal8Bit());
  167. #endif
  168. } else if (Utility::isLinux() || Utility::isBSD()) {
  169. QString runtimeDir;
  170. runtimeDir = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation);
  171. socketPath = runtimeDir + "/" + Theme::instance()->appName() + "/socket";
  172. } else {
  173. qCWarning(lcSocketApi) << "An unexpected system detected, this probably won't work.";
  174. }
  175. SocketApiServer::removeServer(socketPath);
  176. QFileInfo info(socketPath);
  177. if (!info.dir().exists()) {
  178. bool result = info.dir().mkpath(".");
  179. qCDebug(lcSocketApi) << "creating" << info.dir().path() << result;
  180. if (result) {
  181. QFile::setPermissions(socketPath,
  182. QFile::Permissions(QFile::ReadOwner + QFile::WriteOwner + QFile::ExeOwner));
  183. }
  184. }
  185. if (!_localServer.listen(socketPath)) {
  186. qCWarning(lcSocketApi) << "can't start server" << socketPath;
  187. } else {
  188. qCInfo(lcSocketApi) << "server started, listening at " << socketPath;
  189. }
  190. connect(&_localServer, &SocketApiServer::newConnection, this, &SocketApi::slotNewConnection);
  191. // folder watcher
  192. connect(FolderMan::instance(), &FolderMan::folderSyncStateChange, this, &SocketApi::slotUpdateFolderView);
  193. }
  194. SocketApi::~SocketApi()
  195. {
  196. qCDebug(lcSocketApi) << "dtor";
  197. _localServer.close();
  198. // All remaining sockets will be destroyed with _localServer, their parent
  199. ASSERT(_listeners.isEmpty() || _listeners.first().socket->parent() == &_localServer);
  200. _listeners.clear();
  201. }
  202. void SocketApi::slotNewConnection()
  203. {
  204. // Note that on macOS this is not actually a line-based QIODevice, it's a SocketApiSocket which is our
  205. // custom message based macOS IPC.
  206. QIODevice *socket = _localServer.nextPendingConnection();
  207. if (!socket) {
  208. return;
  209. }
  210. qCInfo(lcSocketApi) << "New connection" << socket;
  211. connect(socket, &QIODevice::readyRead, this, &SocketApi::slotReadSocket);
  212. connect(socket, SIGNAL(disconnected()), this, SLOT(onLostConnection()));
  213. connect(socket, &QObject::destroyed, this, &SocketApi::slotSocketDestroyed);
  214. ASSERT(socket->readAll().isEmpty());
  215. _listeners.append(SocketListener(socket));
  216. SocketListener &listener = _listeners.last();
  217. foreach (Folder *f, FolderMan::instance()->map()) {
  218. if (f->canSync()) {
  219. QString message = buildRegisterPathMessage(removeTrailingSlash(f->path()));
  220. listener.sendMessage(message);
  221. }
  222. }
  223. }
  224. void SocketApi::onLostConnection()
  225. {
  226. qCInfo(lcSocketApi) << "Lost connection " << sender();
  227. sender()->deleteLater();
  228. auto socket = qobject_cast<QIODevice *>(sender());
  229. ASSERT(socket);
  230. _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end());
  231. }
  232. void SocketApi::slotSocketDestroyed(QObject *obj)
  233. {
  234. auto *socket = static_cast<QIODevice *>(obj);
  235. _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end());
  236. }
  237. void SocketApi::slotReadSocket()
  238. {
  239. auto *socket = qobject_cast<QIODevice *>(sender());
  240. ASSERT(socket);
  241. // Find the SocketListener
  242. //
  243. // It's possible for the disconnected() signal to be triggered before
  244. // the readyRead() signals are received - in that case there won't be a
  245. // valid listener. We execute the handler anyway, but it will work with
  246. // a SocketListener that doesn't send any messages.
  247. static auto noListener = SocketListener(nullptr);
  248. SocketListener *listener = &noListener;
  249. auto listenerIt = std::find_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket));
  250. if (listenerIt != _listeners.end()) {
  251. listener = &*listenerIt;
  252. }
  253. while (socket->canReadLine()) {
  254. // Make sure to normalize the input from the socket to
  255. // make sure that the path will match, especially on OS X.
  256. QString line = QString::fromUtf8(socket->readLine()).normalized(QString::NormalizationForm_C);
  257. line.chop(1); // remove the '\n'
  258. qCInfo(lcSocketApi) << "Received SocketAPI message <--" << line << "from" << socket;
  259. QByteArray command = line.split(":").value(0).toLatin1();
  260. QByteArray functionWithArguments = "command_" + command;
  261. if (command.startsWith("ASYNC_")) {
  262. functionWithArguments += "(QSharedPointer<SocketApiJob>)";
  263. } else {
  264. functionWithArguments += "(QString,SocketListener*)";
  265. }
  266. int indexOfMethod = staticMetaObject.indexOfMethod(functionWithArguments);
  267. QString argument = line.remove(0, command.length() + 1);
  268. if (command.startsWith("ASYNC_")) {
  269. auto arguments = argument.split('|');
  270. if (arguments.size() != 2) {
  271. listener->sendMessage(QLatin1String("argument count is wrong"));
  272. return;
  273. }
  274. auto json = QJsonDocument::fromJson(arguments[1].toUtf8()).object();
  275. auto jobId = arguments[0];
  276. auto socketApiJob = QSharedPointer<SocketApiJob>(
  277. new SocketApiJob(jobId, listener, json), &QObject::deleteLater);
  278. if (indexOfMethod != -1) {
  279. staticMetaObject.method(indexOfMethod)
  280. .invoke(this, Qt::QueuedConnection,
  281. Q_ARG(QSharedPointer<SocketApiJob>, socketApiJob));
  282. } else {
  283. qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command
  284. << "with argument:" << argument;
  285. socketApiJob->reject("command not found");
  286. }
  287. } else {
  288. if (indexOfMethod != -1) {
  289. staticMetaObject.method(indexOfMethod)
  290. .invoke(this, Qt::QueuedConnection, Q_ARG(QString, argument),
  291. Q_ARG(SocketListener *, listener));
  292. } else {
  293. qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command << "with argument:" << argument;
  294. }
  295. }
  296. }
  297. }
  298. void SocketApi::slotRegisterPath(const QString &alias)
  299. {
  300. // Make sure not to register twice to each connected client
  301. if (_registeredAliases.contains(alias))
  302. return;
  303. Folder *f = FolderMan::instance()->folder(alias);
  304. if (f) {
  305. QString message = buildRegisterPathMessage(removeTrailingSlash(f->path()));
  306. foreach (auto &listener, _listeners) {
  307. listener.sendMessage(message);
  308. }
  309. }
  310. _registeredAliases.insert(alias);
  311. }
  312. void SocketApi::slotUnregisterPath(const QString &alias)
  313. {
  314. if (!_registeredAliases.contains(alias))
  315. return;
  316. Folder *f = FolderMan::instance()->folder(alias);
  317. if (f)
  318. broadcastMessage(buildMessage(QLatin1String("UNREGISTER_PATH"), removeTrailingSlash(f->path()), QString()), true);
  319. _registeredAliases.remove(alias);
  320. }
  321. void SocketApi::slotUpdateFolderView(Folder *f)
  322. {
  323. if (_listeners.isEmpty()) {
  324. return;
  325. }
  326. if (f) {
  327. // do only send UPDATE_VIEW for a couple of status
  328. if (f->syncResult().status() == SyncResult::SyncPrepare
  329. || f->syncResult().status() == SyncResult::Success
  330. || f->syncResult().status() == SyncResult::Paused
  331. || f->syncResult().status() == SyncResult::Problem
  332. || f->syncResult().status() == SyncResult::Error
  333. || f->syncResult().status() == SyncResult::SetupError) {
  334. QString rootPath = removeTrailingSlash(f->path());
  335. broadcastStatusPushMessage(rootPath, f->syncEngine().syncFileStatusTracker().fileStatus(""));
  336. broadcastMessage(buildMessage(QLatin1String("UPDATE_VIEW"), rootPath));
  337. } else {
  338. qCDebug(lcSocketApi) << "Not sending UPDATE_VIEW for" << f->alias() << "because status() is" << f->syncResult().status();
  339. }
  340. }
  341. }
  342. void SocketApi::broadcastMessage(const QString &msg, bool doWait)
  343. {
  344. foreach (auto &listener, _listeners) {
  345. listener.sendMessage(msg, doWait);
  346. }
  347. }
  348. void SocketApi::processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage)
  349. {
  350. auto theme = Theme::instance();
  351. auto fileData = FileData::get(localFile);
  352. auto shareFolder = fileData.folder;
  353. if (!shareFolder) {
  354. const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile);
  355. // files that are not within a sync folder are not synced.
  356. listener->sendMessage(message);
  357. } else if (!shareFolder->accountState()->isConnected()) {
  358. const QString message = QLatin1String("SHARE:NOTCONNECTED:") + QDir::toNativeSeparators(localFile);
  359. // if the folder isn't connected, don't open the share dialog
  360. listener->sendMessage(message);
  361. } else if (!theme->linkSharing() && (!theme->userGroupSharing() || shareFolder->accountState()->account()->serverVersionInt() < Account::makeServerVersion(8, 2, 0))) {
  362. const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile);
  363. listener->sendMessage(message);
  364. } else {
  365. // If the file doesn't have a journal record, it might not be uploaded yet
  366. if (!fileData.journalRecord().isValid()) {
  367. const QString message = QLatin1String("SHARE:NOTSYNCED:") + QDir::toNativeSeparators(localFile);
  368. listener->sendMessage(message);
  369. return;
  370. }
  371. auto &remotePath = fileData.serverRelativePath;
  372. // Can't share root folder
  373. if (remotePath == "/") {
  374. const QString message = QLatin1String("SHARE:CANNOTSHAREROOT:") + QDir::toNativeSeparators(localFile);
  375. listener->sendMessage(message);
  376. return;
  377. }
  378. const QString message = QLatin1String("SHARE:OK:") + QDir::toNativeSeparators(localFile);
  379. listener->sendMessage(message);
  380. emit shareCommandReceived(remotePath, fileData.localPath, startPage);
  381. }
  382. }
  383. void SocketApi::broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus)
  384. {
  385. QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString());
  386. Q_ASSERT(!systemPath.endsWith('/'));
  387. uint directoryHash = qHash(systemPath.left(systemPath.lastIndexOf('/')));
  388. foreach (auto &listener, _listeners) {
  389. listener.sendMessageIfDirectoryMonitored(msg, directoryHash);
  390. }
  391. }
  392. void SocketApi::command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener)
  393. {
  394. // This command is the same as RETRIEVE_FILE_STATUS
  395. command_RETRIEVE_FILE_STATUS(argument, listener);
  396. }
  397. void SocketApi::command_RETRIEVE_FILE_STATUS(const QString &argument, SocketListener *listener)
  398. {
  399. QString statusString;
  400. auto fileData = FileData::get(argument);
  401. if (!fileData.folder) {
  402. // this can happen in offline mode e.g.: nothing to worry about
  403. statusString = QLatin1String("NOP");
  404. } else {
  405. // The user probably visited this directory in the file shell.
  406. // Let the listener know that it should now send status pushes for sibblings of this file.
  407. QString directory = fileData.localPath.left(fileData.localPath.lastIndexOf('/'));
  408. listener->registerMonitoredDirectory(qHash(directory));
  409. SyncFileStatus fileStatus = fileData.syncFileStatus();
  410. statusString = fileStatus.toSocketAPIString();
  411. }
  412. const QString message = QLatin1String("STATUS:") % statusString % QLatin1Char(':') % QDir::toNativeSeparators(argument);
  413. listener->sendMessage(message);
  414. }
  415. void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener)
  416. {
  417. processShareRequest(localFile, listener, ShareDialogStartPage::UsersAndGroups);
  418. }
  419. void SocketApi::command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener)
  420. {
  421. processShareRequest(localFile, listener, ShareDialogStartPage::PublicLinks);
  422. }
  423. void SocketApi::command_VERSION(const QString &, SocketListener *listener)
  424. {
  425. listener->sendMessage(QLatin1String("VERSION:" MIRALL_VERSION_STRING ":" MIRALL_SOCKET_API_VERSION));
  426. }
  427. void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketListener *listener)
  428. {
  429. //listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + tr("Share with %1", "parameter is Nextcloud").arg(Theme::instance()->appNameGUI()));
  430. listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + Theme::instance()->appNameGUI());
  431. }
  432. void SocketApi::command_EDIT(const QString &localFile, SocketListener *listener)
  433. {
  434. Q_UNUSED(listener)
  435. auto fileData = FileData::get(localFile);
  436. if (!fileData.folder) {
  437. qCWarning(lcSocketApi) << "Unknown path" << localFile;
  438. return;
  439. }
  440. auto record = fileData.journalRecord();
  441. if (!record.isValid())
  442. return;
  443. DirectEditor* editor = getDirectEditorForLocalFile(fileData.localPath);
  444. if (!editor)
  445. return;
  446. auto *job = new JsonApiJob(fileData.folder->accountState()->account(), QLatin1String("ocs/v2.php/apps/files/api/v1/directEditing/open"), this);
  447. QUrlQuery params;
  448. params.addQueryItem("path", fileData.serverRelativePath);
  449. params.addQueryItem("editorId", editor->id());
  450. job->addQueryParams(params);
  451. job->usePOST();
  452. QObject::connect(job, &JsonApiJob::jsonReceived, [](const QJsonDocument &json){
  453. auto data = json.object().value("ocs").toObject().value("data").toObject();
  454. auto url = QUrl(data.value("url").toString());
  455. if(!url.isEmpty())
  456. Utility::openBrowser(url, nullptr);
  457. });
  458. job->start();
  459. }
  460. // don't pull the share manager into socketapi unittests
  461. #ifndef OWNCLOUD_TEST
  462. class GetOrCreatePublicLinkShare : public QObject
  463. {
  464. Q_OBJECT
  465. public:
  466. GetOrCreatePublicLinkShare(const AccountPtr &account, const QString &localFile,
  467. std::function<void(const QString &link)> targetFun, QObject *parent)
  468. : QObject(parent)
  469. , _shareManager(account)
  470. , _localFile(localFile)
  471. , _targetFun(targetFun)
  472. {
  473. connect(&_shareManager, &ShareManager::sharesFetched,
  474. this, &GetOrCreatePublicLinkShare::sharesFetched);
  475. connect(&_shareManager, &ShareManager::linkShareCreated,
  476. this, &GetOrCreatePublicLinkShare::linkShareCreated);
  477. connect(&_shareManager, &ShareManager::serverError,
  478. this, &GetOrCreatePublicLinkShare::serverError);
  479. connect(&_shareManager, &ShareManager::linkShareRequiresPassword,
  480. this, &GetOrCreatePublicLinkShare::passwordRequired);
  481. }
  482. void run()
  483. {
  484. qCDebug(lcPublicLink) << "Fetching shares";
  485. _shareManager.fetchShares(_localFile);
  486. }
  487. private slots:
  488. void sharesFetched(const QList<QSharedPointer<Share>> &shares)
  489. {
  490. auto shareName = SocketApi::tr("Context menu share");
  491. // If there already is a context menu share, reuse it
  492. for (const auto &share : shares) {
  493. const auto linkShare = qSharedPointerDynamicCast<LinkShare>(share);
  494. if (!linkShare)
  495. continue;
  496. if (linkShare->getName() == shareName) {
  497. qCDebug(lcPublicLink) << "Found existing share, reusing";
  498. return success(linkShare->getLink().toString());
  499. }
  500. }
  501. // otherwise create a new one
  502. qCDebug(lcPublicLink) << "Creating new share";
  503. _shareManager.createLinkShare(_localFile, shareName, QString());
  504. }
  505. void linkShareCreated(const QSharedPointer<LinkShare> &share)
  506. {
  507. qCDebug(lcPublicLink) << "New share created";
  508. success(share->getLink().toString());
  509. }
  510. void passwordRequired() {
  511. bool ok = false;
  512. QString password = QInputDialog::getText(nullptr,
  513. tr("Password for share required"),
  514. tr("Please enter a password for your link share:"),
  515. QLineEdit::Normal,
  516. QString(),
  517. &ok);
  518. if (!ok) {
  519. // The dialog was canceled so no need to do anything
  520. return;
  521. }
  522. // Try to create the link share again with the newly entered password
  523. _shareManager.createLinkShare(_localFile, QString(), password);
  524. }
  525. void serverError(int code, const QString &message)
  526. {
  527. qCWarning(lcPublicLink) << "Share fetch/create error" << code << message;
  528. QMessageBox::warning(
  529. nullptr,
  530. tr("Sharing error"),
  531. tr("Could not retrieve or create the public link share. Error:\n\n%1").arg(message),
  532. QMessageBox::Ok,
  533. QMessageBox::NoButton);
  534. deleteLater();
  535. }
  536. private:
  537. void success(const QString &link)
  538. {
  539. _targetFun(link);
  540. deleteLater();
  541. }
  542. ShareManager _shareManager;
  543. QString _localFile;
  544. std::function<void(const QString &url)> _targetFun;
  545. };
  546. #else
  547. class GetOrCreatePublicLinkShare : public QObject
  548. {
  549. Q_OBJECT
  550. public:
  551. GetOrCreatePublicLinkShare(const AccountPtr &, const QString &,
  552. std::function<void(const QString &link)>, QObject *)
  553. {
  554. }
  555. void run()
  556. {
  557. }
  558. };
  559. #endif
  560. void SocketApi::command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *)
  561. {
  562. auto fileData = FileData::get(localFile);
  563. if (!fileData.folder)
  564. return;
  565. AccountPtr account = fileData.folder->accountState()->account();
  566. auto job = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, [](const QString &url) { copyUrlToClipboard(url); }, this);
  567. job->run();
  568. }
  569. // Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599
  570. #ifdef Q_OS_WIN
  571. void SocketApi::command_COPYASPATH(const QString &localFile, SocketListener *)
  572. {
  573. QApplication::clipboard()->setText(localFile);
  574. }
  575. void SocketApi::command_OPENNEWWINDOW(const QString &localFile, SocketListener *)
  576. {
  577. QDesktopServices::openUrl(QUrl::fromLocalFile(localFile));
  578. }
  579. void SocketApi::command_OPEN(const QString &localFile, SocketListener *socketListener)
  580. {
  581. command_OPENNEWWINDOW(localFile, socketListener);
  582. }
  583. #endif
  584. // Fetches the private link url asynchronously and then calls the target slot
  585. void SocketApi::fetchPrivateLinkUrlHelper(const QString &localFile, const std::function<void(const QString &url)> &targetFun)
  586. {
  587. auto fileData = FileData::get(localFile);
  588. if (!fileData.folder) {
  589. qCWarning(lcSocketApi) << "Unknown path" << localFile;
  590. return;
  591. }
  592. auto record = fileData.journalRecord();
  593. if (!record.isValid())
  594. return;
  595. fetchPrivateLinkUrl(
  596. fileData.folder->accountState()->account(),
  597. fileData.serverRelativePath,
  598. record.numericFileId(),
  599. this,
  600. targetFun);
  601. }
  602. void SocketApi::command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *)
  603. {
  604. fetchPrivateLinkUrlHelper(localFile, &SocketApi::copyUrlToClipboard);
  605. }
  606. void SocketApi::command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *)
  607. {
  608. fetchPrivateLinkUrlHelper(localFile, &SocketApi::emailPrivateLink);
  609. }
  610. void SocketApi::command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *)
  611. {
  612. fetchPrivateLinkUrlHelper(localFile, &SocketApi::openPrivateLink);
  613. }
  614. void SocketApi::command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketListener *)
  615. {
  616. QStringList files = filesArg.split(QLatin1Char('\x1e')); // Record Separator
  617. for (const auto &file : files) {
  618. auto data = FileData::get(file);
  619. if (!data.folder)
  620. continue;
  621. // Update the pin state on all items
  622. data.folder->vfs().setPinState(data.folderRelativePath, PinState::AlwaysLocal);
  623. // Trigger sync
  624. data.folder->schedulePathForLocalDiscovery(data.folderRelativePath);
  625. data.folder->scheduleThisFolderSoon();
  626. }
  627. }
  628. /* Go over all the files and replace them by a virtual file */
  629. void SocketApi::command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener *)
  630. {
  631. QStringList files = filesArg.split(QLatin1Char('\x1e')); // Record Separator
  632. for (const auto &file : files) {
  633. auto data = FileData::get(file);
  634. if (!data.folder)
  635. continue;
  636. // Update the pin state on all items
  637. data.folder->vfs().setPinState(data.folderRelativePath, PinState::OnlineOnly);
  638. // Trigger sync
  639. data.folder->schedulePathForLocalDiscovery(data.folderRelativePath);
  640. data.folder->scheduleThisFolderSoon();
  641. }
  642. }
  643. void SocketApi::copyUrlToClipboard(const QString &link)
  644. {
  645. QApplication::clipboard()->setText(link);
  646. }
  647. void SocketApi::command_RESOLVE_CONFLICT(const QString &localFile, SocketListener *)
  648. {
  649. const auto fileData = FileData::get(localFile);
  650. if (!fileData.folder || !Utility::isConflictFile(fileData.folderRelativePath))
  651. return; // should not have shown menu item
  652. const auto conflictedRelativePath = fileData.folderRelativePath;
  653. const auto baseRelativePath = fileData.folder->journalDb()->conflictFileBaseName(fileData.folderRelativePath.toUtf8());
  654. const auto dir = QDir(fileData.folder->path());
  655. const auto conflictedPath = dir.filePath(conflictedRelativePath);
  656. const auto basePath = dir.filePath(baseRelativePath);
  657. const auto baseName = QFileInfo(basePath).fileName();
  658. #ifndef OWNCLOUD_TEST
  659. ConflictDialog dialog;
  660. dialog.setBaseFilename(baseName);
  661. dialog.setLocalVersionFilename(conflictedPath);
  662. dialog.setRemoteVersionFilename(basePath);
  663. if (dialog.exec() == ConflictDialog::Accepted) {
  664. fileData.folder->scheduleThisFolderSoon();
  665. }
  666. #endif
  667. }
  668. void SocketApi::command_DELETE_ITEM(const QString &localFile, SocketListener *)
  669. {
  670. ConflictSolver solver;
  671. solver.setLocalVersionFilename(localFile);
  672. solver.exec(ConflictSolver::KeepRemoteVersion);
  673. }
  674. void SocketApi::command_MOVE_ITEM(const QString &localFile, SocketListener *)
  675. {
  676. const auto fileData = FileData::get(localFile);
  677. const auto parentDir = fileData.parentFolder();
  678. if (!fileData.folder)
  679. return; // should not have shown menu item
  680. QString defaultDirAndName = fileData.folderRelativePath;
  681. // If it's a conflict, we want to save it under the base name by default
  682. if (Utility::isConflictFile(defaultDirAndName)) {
  683. defaultDirAndName = fileData.folder->journalDb()->conflictFileBaseName(fileData.folderRelativePath.toUtf8());
  684. }
  685. // If the parent doesn't accept new files, go to the root of the sync folder
  686. QFileInfo fileInfo(localFile);
  687. const auto parentRecord = parentDir.journalRecord();
  688. if ((fileInfo.isFile() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddFile))
  689. || (fileInfo.isDir() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories))) {
  690. defaultDirAndName = QFileInfo(defaultDirAndName).fileName();
  691. }
  692. // Add back the folder path
  693. defaultDirAndName = QDir(fileData.folder->path()).filePath(defaultDirAndName);
  694. const auto target = QFileDialog::getSaveFileName(
  695. nullptr,
  696. tr("Select new location …"),
  697. defaultDirAndName,
  698. QString(), nullptr, QFileDialog::HideNameFilterDetails);
  699. if (target.isEmpty())
  700. return;
  701. ConflictSolver solver;
  702. solver.setLocalVersionFilename(localFile);
  703. solver.setRemoteVersionFilename(target);
  704. }
  705. void SocketApi::emailPrivateLink(const QString &link)
  706. {
  707. Utility::openEmailComposer(
  708. tr("I shared something with you"),
  709. link,
  710. nullptr);
  711. }
  712. void OCC::SocketApi::openPrivateLink(const QString &link)
  713. {
  714. Utility::openBrowser(link, nullptr);
  715. }
  716. void SocketApi::command_GET_STRINGS(const QString &argument, SocketListener *listener)
  717. {
  718. static std::array<std::pair<const char *, QString>, 5> strings { {
  719. { "SHARE_MENU_TITLE", tr("Share options") },
  720. { "CONTEXT_MENU_TITLE", Theme::instance()->appNameGUI() },
  721. { "COPY_PRIVATE_LINK_MENU_TITLE", tr("Copy private link to clipboard") },
  722. { "EMAIL_PRIVATE_LINK_MENU_TITLE", tr("Send private link by email …") },
  723. { "CONTEXT_MENU_ICON", APPLICATION_ICON_NAME},
  724. } };
  725. listener->sendMessage(QString("GET_STRINGS:BEGIN"));
  726. for (const auto& key_value : strings) {
  727. if (argument.isEmpty() || argument == QLatin1String(key_value.first)) {
  728. listener->sendMessage(QString("STRING:%1:%2").arg(key_value.first, key_value.second));
  729. }
  730. }
  731. listener->sendMessage(QString("GET_STRINGS:END"));
  732. }
  733. void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled)
  734. {
  735. auto record = fileData.journalRecord();
  736. bool isOnTheServer = record.isValid();
  737. auto flagString = isOnTheServer && enabled ? QLatin1String("::") : QLatin1String(":d:");
  738. auto capabilities = fileData.folder->accountState()->account()->capabilities();
  739. auto theme = Theme::instance();
  740. if (!capabilities.shareAPI() || !(theme->userGroupSharing() || (theme->linkSharing() && capabilities.sharePublicLink())))
  741. return;
  742. // If sharing is globally disabled, do not show any sharing entries.
  743. // If there is no permission to share for this file, add a disabled entry saying so
  744. if (isOnTheServer && !record._remotePerm.isNull() && !record._remotePerm.hasPermission(RemotePermissions::CanReshare)) {
  745. listener->sendMessage(QLatin1String("MENU_ITEM:DISABLED:d:") + (!record.isDirectory()
  746. ? tr("Resharing this file is not allowed") : tr("Resharing this folder is not allowed")));
  747. } else {
  748. listener->sendMessage(QLatin1String("MENU_ITEM:SHARE") + flagString + tr("Share options"));
  749. // Do we have public links?
  750. bool publicLinksEnabled = theme->linkSharing() && capabilities.sharePublicLink();
  751. // Is is possible to create a public link without user choices?
  752. bool canCreateDefaultPublicLink = publicLinksEnabled
  753. && !capabilities.sharePublicLinkEnforceExpireDate()
  754. && !capabilities.sharePublicLinkAskOptionalPassword()
  755. && !capabilities.sharePublicLinkEnforcePassword();
  756. if (canCreateDefaultPublicLink) {
  757. listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PUBLIC_LINK") + flagString + tr("Copy public link"));
  758. } else if (publicLinksEnabled) {
  759. listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + flagString + tr("Copy public link"));
  760. }
  761. }
  762. listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PRIVATE_LINK") + flagString + tr("Copy internal link"));
  763. // Disabled: only providing email option for private links would look odd,
  764. // and the copy option is more general.
  765. //listener->sendMessage(QLatin1String("MENU_ITEM:EMAIL_PRIVATE_LINK") + flagString + tr("Send private link by email …"));
  766. }
  767. SocketApi::FileData SocketApi::FileData::get(const QString &localFile)
  768. {
  769. FileData data;
  770. data.localPath = QDir::cleanPath(localFile);
  771. if (data.localPath.endsWith(QLatin1Char('/')))
  772. data.localPath.chop(1);
  773. data.folder = FolderMan::instance()->folderForPath(data.localPath);
  774. if (!data.folder)
  775. return data;
  776. data.folderRelativePath = data.localPath.mid(data.folder->cleanPath().length() + 1);
  777. data.serverRelativePath = QDir(data.folder->remotePath()).filePath(data.folderRelativePath);
  778. QString virtualFileExt = QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX);
  779. if (data.serverRelativePath.endsWith(virtualFileExt)) {
  780. data.serverRelativePath.chop(virtualFileExt.size());
  781. }
  782. return data;
  783. }
  784. QString SocketApi::FileData::folderRelativePathNoVfsSuffix() const
  785. {
  786. auto result = folderRelativePath;
  787. QString virtualFileExt = QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX);
  788. if (result.endsWith(virtualFileExt)) {
  789. result.chop(virtualFileExt.size());
  790. }
  791. return result;
  792. }
  793. SyncFileStatus SocketApi::FileData::syncFileStatus() const
  794. {
  795. if (!folder)
  796. return SyncFileStatus::StatusNone;
  797. return folder->syncEngine().syncFileStatusTracker().fileStatus(folderRelativePath);
  798. }
  799. SyncJournalFileRecord SocketApi::FileData::journalRecord() const
  800. {
  801. SyncJournalFileRecord record;
  802. if (!folder)
  803. return record;
  804. folder->journalDb()->getFileRecord(folderRelativePath, &record);
  805. return record;
  806. }
  807. SocketApi::FileData SocketApi::FileData::parentFolder() const
  808. {
  809. return FileData::get(QFileInfo(localPath).dir().path().toUtf8());
  810. }
  811. void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListener *listener)
  812. {
  813. listener->sendMessage(QString("GET_MENU_ITEMS:BEGIN"));
  814. QStringList files = argument.split(QLatin1Char('\x1e')); // Record Separator
  815. // Find the common sync folder.
  816. // syncFolder will be null if files are in different folders.
  817. Folder *syncFolder = nullptr;
  818. for (const auto &file : files) {
  819. auto folder = FolderMan::instance()->folderForPath(file);
  820. if (folder != syncFolder) {
  821. if (!syncFolder) {
  822. syncFolder = folder;
  823. } else {
  824. syncFolder = nullptr;
  825. break;
  826. }
  827. }
  828. }
  829. // Sharing actions show for single files only
  830. if (syncFolder && files.size() == 1 && syncFolder->accountState()->isConnected()) {
  831. QString systemPath = QDir::cleanPath(argument);
  832. if (systemPath.endsWith(QLatin1Char('/'))) {
  833. systemPath.truncate(systemPath.length() - 1);
  834. }
  835. FileData fileData = FileData::get(argument);
  836. const auto record = fileData.journalRecord();
  837. const bool isOnTheServer = record.isValid();
  838. const auto isE2eEncryptedPath = fileData.journalRecord()._isE2eEncrypted || !fileData.journalRecord()._e2eMangledName.isEmpty();
  839. auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:");
  840. DirectEditor* editor = getDirectEditorForLocalFile(fileData.localPath);
  841. if (editor) {
  842. //listener->sendMessage(QLatin1String("MENU_ITEM:EDIT") + flagString + tr("Edit via ") + editor->name());
  843. listener->sendMessage(QLatin1String("MENU_ITEM:EDIT") + flagString + tr("Edit"));
  844. } else {
  845. listener->sendMessage(QLatin1String("MENU_ITEM:OPEN_PRIVATE_LINK") + flagString + tr("Open in browser"));
  846. }
  847. sendSharingContextMenuOptions(fileData, listener, !isE2eEncryptedPath);
  848. // Conflict files get conflict resolution actions
  849. bool isConflict = Utility::isConflictFile(fileData.folderRelativePath);
  850. if (isConflict || !isOnTheServer) {
  851. // Check whether this new file is in a read-only directory
  852. QFileInfo fileInfo(fileData.localPath);
  853. const auto parentDir = fileData.parentFolder();
  854. const auto parentRecord = parentDir.journalRecord();
  855. const bool canAddToDir =
  856. !parentRecord.isValid() // We're likely at the root of the sync folder, got to assume we can add there
  857. || (fileInfo.isFile() && parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddFile))
  858. || (fileInfo.isDir() && parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories));
  859. const bool canChangeFile =
  860. !isOnTheServer
  861. || (record._remotePerm.hasPermission(RemotePermissions::CanDelete)
  862. && record._remotePerm.hasPermission(RemotePermissions::CanMove)
  863. && record._remotePerm.hasPermission(RemotePermissions::CanRename));
  864. if (isConflict && canChangeFile) {
  865. if (canAddToDir) {
  866. listener->sendMessage(QLatin1String("MENU_ITEM:RESOLVE_CONFLICT::") + tr("Resolve conflict …"));
  867. } else {
  868. if (isOnTheServer) {
  869. // Uploaded conflict file in read-only directory
  870. listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move and rename …"));
  871. } else {
  872. // Local-only conflict file in a read-only dir
  873. listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move, rename and upload …"));
  874. }
  875. listener->sendMessage(QLatin1String("MENU_ITEM:DELETE_ITEM::") + tr("Delete local changes"));
  876. }
  877. }
  878. // File in a read-only directory?
  879. if (!isConflict && !isOnTheServer && !canAddToDir) {
  880. listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move and upload …"));
  881. listener->sendMessage(QLatin1String("MENU_ITEM:DELETE_ITEM::") + tr("Delete"));
  882. }
  883. }
  884. }
  885. // File availability actions
  886. if (syncFolder
  887. && syncFolder->supportsVirtualFiles()
  888. && syncFolder->vfs().socketApiPinStateActionsShown()) {
  889. ENFORCE(!files.isEmpty());
  890. // Determine the combined availability status of the files
  891. auto combined = Optional<VfsItemAvailability>();
  892. auto merge = [](VfsItemAvailability lhs, VfsItemAvailability rhs) {
  893. if (lhs == rhs)
  894. return lhs;
  895. if (int(lhs) > int(rhs))
  896. std::swap(lhs, rhs); // reduce cases ensuring lhs < rhs
  897. if (lhs == VfsItemAvailability::AlwaysLocal && rhs == VfsItemAvailability::AllHydrated)
  898. return VfsItemAvailability::AllHydrated;
  899. if (lhs == VfsItemAvailability::AllDehydrated && rhs == VfsItemAvailability::OnlineOnly)
  900. return VfsItemAvailability::AllDehydrated;
  901. return VfsItemAvailability::Mixed;
  902. };
  903. for (const auto &file : files) {
  904. auto fileData = FileData::get(file);
  905. auto availability = syncFolder->vfs().availability(fileData.folderRelativePath);
  906. if (!availability) {
  907. if (availability.error() == Vfs::AvailabilityError::DbError)
  908. availability = VfsItemAvailability::Mixed;
  909. if (availability.error() == Vfs::AvailabilityError::NoSuchItem)
  910. continue;
  911. }
  912. if (!combined) {
  913. combined = *availability;
  914. } else {
  915. combined = merge(*combined, *availability);
  916. }
  917. }
  918. // TODO: Should be a submenu, should use icons
  919. auto makePinContextMenu = [&](bool makeAvailableLocally, bool freeSpace) {
  920. listener->sendMessage(QLatin1String("MENU_ITEM:CURRENT_PIN:d:")
  921. + Utility::vfsCurrentAvailabilityText(*combined));
  922. listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY:")
  923. + (makeAvailableLocally ? QLatin1String(":") : QLatin1String("d:"))
  924. + Utility::vfsPinActionText());
  925. listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_ONLINE_ONLY:")
  926. + (freeSpace ? QLatin1String(":") : QLatin1String("d:"))
  927. + Utility::vfsFreeSpaceActionText());
  928. };
  929. if (combined) {
  930. switch (*combined) {
  931. case VfsItemAvailability::AlwaysLocal:
  932. makePinContextMenu(false, true);
  933. break;
  934. case VfsItemAvailability::AllHydrated:
  935. case VfsItemAvailability::Mixed:
  936. makePinContextMenu(true, true);
  937. break;
  938. case VfsItemAvailability::AllDehydrated:
  939. case VfsItemAvailability::OnlineOnly:
  940. makePinContextMenu(true, false);
  941. break;
  942. }
  943. }
  944. }
  945. listener->sendMessage(QString("GET_MENU_ITEMS:END"));
  946. }
  947. DirectEditor* SocketApi::getDirectEditorForLocalFile(const QString &localFile)
  948. {
  949. FileData fileData = FileData::get(localFile);
  950. auto capabilities = fileData.folder->accountState()->account()->capabilities();
  951. if (fileData.folder && fileData.folder->accountState()->isConnected()) {
  952. QMimeDatabase db;
  953. QMimeType type = db.mimeTypeForFile(localFile);
  954. DirectEditor* editor = capabilities.getDirectEditorForMimetype(type);
  955. if (!editor) {
  956. editor = capabilities.getDirectEditorForOptionalMimetype(type);
  957. }
  958. return editor;
  959. }
  960. return nullptr;
  961. }
  962. #if GUI_TESTING
  963. void SocketApi::command_ASYNC_LIST_WIDGETS(const QSharedPointer<SocketApiJob> &job)
  964. {
  965. QString response;
  966. for (auto &widget : QApplication::allWidgets()) {
  967. auto objectName = widget->objectName();
  968. if (!objectName.isEmpty()) {
  969. response += objectName + ":" + widget->property("text").toString() + ", ";
  970. }
  971. }
  972. job->resolve(response);
  973. }
  974. void SocketApi::command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer<SocketApiJob> &job)
  975. {
  976. auto &arguments = job->arguments();
  977. auto widget = findWidget(arguments["objectName"].toString());
  978. if (!widget) {
  979. job->reject(QLatin1String("widget not found"));
  980. return;
  981. }
  982. QMetaObject::invokeMethod(widget, arguments["method"].toString().toLocal8Bit().constData());
  983. job->resolve();
  984. }
  985. void SocketApi::command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job)
  986. {
  987. auto widget = findWidget(job->arguments()[QLatin1String("objectName")].toString());
  988. if (!widget) {
  989. job->reject(QLatin1String("widget not found"));
  990. return;
  991. }
  992. auto propertyName = job->arguments()[QLatin1String("property")].toString();
  993. job->resolve(widget->property(propertyName.toLocal8Bit().constData())
  994. .toString()
  995. .toLocal8Bit()
  996. .constData());
  997. }
  998. void SocketApi::command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job)
  999. {
  1000. auto &arguments = job->arguments();
  1001. auto widget = findWidget(arguments["objectName"].toString());
  1002. if (!widget) {
  1003. job->reject(QLatin1String("widget not found"));
  1004. return;
  1005. }
  1006. widget->setProperty(arguments["property"].toString().toLocal8Bit().constData(),
  1007. arguments["value"].toString().toLocal8Bit().constData());
  1008. job->resolve();
  1009. }
  1010. void SocketApi::command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer<SocketApiJob> &job)
  1011. {
  1012. auto &arguments = job->arguments();
  1013. auto widget = findWidget(arguments["objectName"].toString());
  1014. if (!widget) {
  1015. job->reject(QLatin1String("widget not found"));
  1016. return;
  1017. }
  1018. ListenerClosure *closure = new ListenerClosure([job]() { job->resolve("signal emitted"); });
  1019. auto signalSignature = arguments["signalSignature"].toString();
  1020. signalSignature.prepend("2");
  1021. auto local8bit = signalSignature.toLocal8Bit();
  1022. auto signalSignatureFinal = local8bit.constData();
  1023. connect(widget, signalSignatureFinal, closure, SLOT(closureSlot()), Qt::QueuedConnection);
  1024. }
  1025. void SocketApi::command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer<SocketApiJob> &job)
  1026. {
  1027. auto &arguments = job->arguments();
  1028. auto objectName = arguments["objectName"].toString();
  1029. auto widget = findWidget(objectName);
  1030. if (!widget) {
  1031. job->reject(QLatin1String("widget not found: ") + objectName);
  1032. return;
  1033. }
  1034. auto children = widget->findChildren<QWidget *>();
  1035. for (auto childWidget : children) {
  1036. // foo is the popupwidget!
  1037. auto actions = childWidget->actions();
  1038. for (auto action : actions) {
  1039. if (action->objectName() == arguments["actionName"].toString()) {
  1040. action->trigger();
  1041. job->resolve("action found");
  1042. return;
  1043. }
  1044. }
  1045. }
  1046. job->reject("Action not found");
  1047. }
  1048. #endif
  1049. QString SocketApi::buildRegisterPathMessage(const QString &path)
  1050. {
  1051. QFileInfo fi(path);
  1052. QString message = QLatin1String("REGISTER_PATH:");
  1053. message.append(QDir::toNativeSeparators(fi.absoluteFilePath()));
  1054. return message;
  1055. }
  1056. } // namespace OCC
  1057. #include "socketapi.moc"