nextcloud-desktop/src/gui/socketapi.cpp
Jocelyn Turcotte 5264a8c7f6 shell_integration: Fix disappearing context menus on Windows #2898
Since each new connection to the socket API would trigger a broadcast
of REGISTER_PATH to all existing connections, opening the context menu
would trigger a SHChangeNotify call of the root directory through
the overlay icon extension, which is currently also connected to the
socket API, waiting for changes.

Fix the issue by sending the initial REGISTER_PATH automatic response
only to the connecting socket.
2015-03-13 20:53:59 +01:00

690 lines
22 KiB
C++

/*
* Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "socketapi.h"
#include "configfile.h"
#include "folderman.h"
#include "folder.h"
#include "utility.h"
#include "theme.h"
#include "syncjournalfilerecord.h"
#include "syncfileitem.h"
#include "filesystem.h"
#include "version.h"
#include <QDebug>
#include <QUrl>
#include <QMetaObject>
#include <QStringList>
#include <QScopedPointer>
#include <QFile>
#include <QDir>
#include <QApplication>
#include <QLocalSocket>
#include <sqlite3.h>
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
#include <QStandardPaths>
#endif
// This is the version that is returned when the client asks for the VERSION.
// The first number should be changed if there is an incompatible change that breaks old clients.
// The second number should be changed when there are new features.
#define MIRALL_SOCKET_API_VERSION "1.0"
extern "C" {
enum csync_exclude_type_e {
CSYNC_NOT_EXCLUDED = 0,
CSYNC_FILE_SILENTLY_EXCLUDED,
CSYNC_FILE_EXCLUDE_AND_REMOVE,
CSYNC_FILE_EXCLUDE_LIST,
CSYNC_FILE_EXCLUDE_INVALID_CHAR,
CSYNC_FILE_EXCLUDE_LONG_FILENAME
};
typedef enum csync_exclude_type_e CSYNC_EXCLUDE_TYPE;
CSYNC_EXCLUDE_TYPE csync_excluded_no_ctx(c_strlist_t *excludes, const char *path, int filetype);
int csync_exclude_load(const char *fname, c_strlist_t **list);
}
namespace OCC {
#define DEBUG qDebug() << "SocketApi: "
SocketApi::SocketApi(QObject* parent)
: QObject(parent)
, _excludes(0)
{
QString socketPath;
if (Utility::isWindows()) {
socketPath = QLatin1String("\\\\.\\pipe\\")
+ QLatin1String("ownCloud");
// TODO: once the windows extension supports multiple
// client connections, switch back to the theme name
// See issue #2388
// + Theme::instance()->appName();
} else if (Utility::isMac()) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
// Always using Qt5 on OS X
QString runtimeDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
socketPath = runtimeDir + "/SyncStateHelper/" + Theme::instance()->appName() + ".socket";
// We use the generic SyncStateHelper name on OS X since the different branded clients
// should unfortunately not mention that they are ownCloud :-)
#endif
} else if( Utility::isLinux() || Utility::isBSD() ) {
QString runtimeDir;
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
runtimeDir = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation);
#else
runtimeDir = QFile::decodeName(qgetenv("XDG_RUNTIME_DIR"));
if (runtimeDir.isEmpty()) {
runtimeDir = QDir::tempPath() + QLatin1String("/runtime-")
+ QString::fromLocal8Bit(qgetenv("USER"));
QDir().mkdir(runtimeDir);
}
#endif
socketPath = runtimeDir + "/" + Theme::instance()->appName() + "/socket";
} else {
DEBUG << "An unexpected system detected";
}
QLocalServer::removeServer(socketPath);
QFileInfo info(socketPath);
if (!info.dir().exists()) {
bool result = info.dir().mkpath(".");
DEBUG << "creating" << info.dir().path() << result;
if( result ) {
QFile::setPermissions(socketPath,
QFile::Permissions(QFile::ReadOwner+QFile::WriteOwner+QFile::ExeOwner));
}
}
if(!_localServer.listen(socketPath)) {
DEBUG << "can't start server" << socketPath;
} else {
DEBUG << "server started, listening at " << socketPath;
}
connect(&_localServer, SIGNAL(newConnection()), this, SLOT(slotNewConnection()));
// folder watcher
connect(FolderMan::instance(), SIGNAL(folderSyncStateChange(QString)), this, SLOT(slotUpdateFolderView(QString)));
connect(ProgressDispatcher::instance(), SIGNAL(jobCompleted(QString,SyncFileItem)),
SLOT(slotJobCompleted(QString,SyncFileItem)));
connect(ProgressDispatcher::instance(), SIGNAL(syncItemDiscovered(QString,SyncFileItem)),
this, SLOT(slotSyncItemDiscovered(QString,SyncFileItem)));
}
SocketApi::~SocketApi()
{
DEBUG << "dtor";
_localServer.close();
qDeleteAll(_listeners);
slotClearExcludesList();
}
void SocketApi::slotClearExcludesList()
{
c_strlist_clear(_excludes);
}
void SocketApi::slotReadExcludes()
{
ConfigFile cfgFile;
slotClearExcludesList();
QString excludeList = cfgFile.excludeFile( ConfigFile::SystemScope );
if( !excludeList.isEmpty() ) {
qDebug() << "==== added system ignore list to socketapi:" << excludeList.toUtf8();
csync_exclude_load(excludeList.toUtf8(), &_excludes);
}
excludeList = cfgFile.excludeFile( ConfigFile::UserScope );
if( !excludeList.isEmpty() ) {
qDebug() << "==== added user defined ignore list to csync:" << excludeList.toUtf8();
csync_exclude_load(excludeList.toUtf8(), &_excludes);
}
}
void SocketApi::slotNewConnection()
{
SocketType* socket = _localServer.nextPendingConnection();
if( ! socket ) {
return;
}
DEBUG << "New connection" << socket;
connect(socket, SIGNAL(readyRead()), this, SLOT(slotReadSocket()));
connect(socket, SIGNAL(disconnected()), this, SLOT(onLostConnection()));
Q_ASSERT(socket->readAll().isEmpty());
_listeners.append(socket);
#ifdef Q_OS_MAC
// We want to tell our location so it can load the icons
// e.g. "/Users/guruz/woboq/owncloud/client/buildmirall/owncloud.app/Contents/MacOS/"
QString iconPath = qApp->applicationDirPath() + "/../Resources/icons/";
if (!QDir(iconPath).exists()) {
DEBUG << "Icon path " << iconPath << " does not exist, did you forget make install?";
}
broadcastMessage(QLatin1String("ICON_PATH"), iconPath );
#endif
foreach( Folder *f, FolderMan::instance()->map() ) {
QString message = buildRegisterPathMessage(f->path());
sendMessage(socket, message);
}
}
void SocketApi::onLostConnection()
{
DEBUG << "Lost connection " << sender();
SocketType* socket = qobject_cast<SocketType*>(sender());
_listeners.removeAll(socket);
socket->deleteLater();
}
void SocketApi::slotReadSocket()
{
SocketType* socket = qobject_cast<SocketType*>(sender());
Q_ASSERT(socket);
while(socket->canReadLine()) {
QString line = QString::fromUtf8(socket->readLine()).trimmed();
QString command = line.split(":").first();
QString function = QString(QLatin1String("command_")).append(command);
QString functionWithArguments = function + QLatin1String("(QString,SocketType*)");
int indexOfMethod = this->metaObject()->indexOfMethod(functionWithArguments.toAscii());
QString argument = line.remove(0, command.length()+1).trimmed();
if(indexOfMethod != -1) {
QMetaObject::invokeMethod(this, function.toAscii(), Q_ARG(QString, argument), Q_ARG(SocketType*, socket));
} else {
DEBUG << "The command is not supported by this version of the client:" << command << "with argument:" << argument;
}
}
}
void SocketApi::slotRegisterPath( const QString& alias )
{
Folder *f = FolderMan::instance()->folder(alias);
if (f) {
QString message = buildRegisterPathMessage(f->path());
foreach(SocketType *socket, _listeners) {
sendMessage(socket, message);
}
}
}
void SocketApi::slotUnregisterPath( const QString& alias )
{
Folder *f = FolderMan::instance()->folder(alias);
if (f) {
broadcastMessage(QLatin1String("UNREGISTER_PATH"), f->path(), QString::null, true );
if( _dbQueries.contains(f)) {
SqlQuery *h = _dbQueries[f];
if( h ) {
h->finish();
}
_dbQueries.remove(f);
}
if( _openDbs.contains(f) ) {
SqlDatabase *db = _openDbs[f];
if( db ) {
db->close();
}
_openDbs.remove(f);
}
}
}
void SocketApi::slotUpdateFolderView(const QString& alias)
{
if (_listeners.isEmpty()) {
return;
}
Folder *f = FolderMan::instance()->folder(alias);
if (f) {
// do only send UPDATE_VIEW for a couple of status
if( f->syncResult().status() == SyncResult::SyncPrepare ||
f->syncResult().status() == SyncResult::Success ||
f->syncResult().status() == SyncResult::Paused ||
f->syncResult().status() == SyncResult::Problem ||
f->syncResult().status() == SyncResult::Error ||
f->syncResult().status() == SyncResult::SetupError ) {
broadcastMessage(QLatin1String("STATUS"), f->path() ,
this->fileStatus(f, "", _excludes).toSocketAPIString());
broadcastMessage(QLatin1String("UPDATE_VIEW"), f->path() );
} else {
qDebug() << "Not sending UPDATE_VIEW for" << alias << "because status() is" << f->syncResult().status();
}
}
}
void SocketApi::slotJobCompleted(const QString &folder, const SyncFileItem &item)
{
if (_listeners.isEmpty()) {
return;
}
Folder *f = FolderMan::instance()->folder(folder);
if (!f) {
return;
}
const QString path = f->path() + item.destination();
QString command = QLatin1String("OK");
if (Progress::isWarningKind(item._status)) {
command = QLatin1String("ERROR");
}
broadcastMessage(QLatin1String("STATUS"), path, command);
}
void SocketApi::slotSyncItemDiscovered(const QString &folder, const SyncFileItem &item)
{
if (_listeners.isEmpty()) {
return;
}
if (item._instruction == CSYNC_INSTRUCTION_NONE) {
return;
}
Folder *f = FolderMan::instance()->folder(folder);
if (!f) {
return;
}
QString path = f->path() + item.destination();
// the trailing slash for directories must be appended as the filenames coming in
// from the plugins have that too. Otherwise the according entry item is not found
// in the plugin.
if( item._type == SyncFileItem::Type::Directory ) {
path += QLatin1Char('/');
}
const QString command = QLatin1String("SYNC");
broadcastMessage(QLatin1String("STATUS"), path, command);
}
void SocketApi::sendMessage(SocketType *socket, const QString& message, bool doWait)
{
DEBUG << "Sending message: " << message;
QString localMessage = message;
if( ! localMessage.endsWith(QLatin1Char('\n'))) {
localMessage.append(QLatin1Char('\n'));
}
QByteArray bytesToSend = localMessage.toUtf8();
qint64 sent = socket->write(bytesToSend);
if( doWait ) {
socket->waitForBytesWritten(1000);
}
if( sent != bytesToSend.length() ) {
qDebug() << "WARN: Could not send all data on socket for " << localMessage;
}
}
void SocketApi::broadcastMessage( const QString& verb, const QString& path, const QString& status, bool doWait )
{
QString msg(verb);
if( !status.isEmpty() ) {
msg.append(QLatin1Char(':'));
msg.append(status);
}
if( !path.isEmpty() ) {
msg.append(QLatin1Char(':'));
QFileInfo fi(path);
msg.append(QDir::toNativeSeparators(fi.absoluteFilePath()));
}
// sendMessage already has a debug output
//DEBUG << "Broadcasting to" << _listeners.count() << "listeners: " << msg;
foreach(SocketType *socket, _listeners) {
sendMessage(socket, msg, doWait);
}
}
void SocketApi::command_RETRIEVE_FOLDER_STATUS(const QString& argument, SocketType* socket)
{
// This command is the same as RETRIEVE_FILE_STATUS
//qDebug() << Q_FUNC_INFO << argument;
command_RETRIEVE_FILE_STATUS(argument, socket);
}
void SocketApi::command_RETRIEVE_FILE_STATUS(const QString& argument, SocketType* socket)
{
if( !socket ) {
qDebug() << "No valid socket object.";
return;
}
qDebug() << Q_FUNC_INFO << argument;
QString statusString;
Folder* syncFolder = FolderMan::instance()->folderForPath( argument );
if (!syncFolder) {
// this can happen in offline mode e.g.: nothing to worry about
DEBUG << "folder offline or not watched:" << argument;
statusString = QLatin1String("NOP");
} else {
const QString file = QDir::cleanPath(argument).mid(QDir::cleanPath(syncFolder->path()).length()+1);
SyncFileStatus fileStatus = this->fileStatus(syncFolder, file, _excludes);
statusString = fileStatus.toSocketAPIString();
}
QString message = QLatin1String("STATUS:")+statusString+QLatin1Char(':')
+QDir::toNativeSeparators(argument);
sendMessage(socket, message);
}
void SocketApi::command_SHARE(const QString& localFile, SocketType* socket)
{
if (!socket) {
qDebug() << Q_FUNC_INFO << "No valid socket object.";
return;
}
qDebug() << Q_FUNC_INFO << localFile;
Folder *shareFolder = FolderMan::instance()->folderForPath(localFile);
if (!shareFolder) {
const QString message = QLatin1String("SHARE:NOP:")+QDir::toNativeSeparators(localFile);
// files that are not within a sync folder are not synced.
sendMessage(socket, message);
} else {
const QString folderForPath = shareFolder->path();
const QString remotePath = shareFolder->remotePath() + localFile.right(localFile.count()-folderForPath.count()+1);
SyncJournalFileRecord rec = dbFileRecord_capi(shareFolder, localFile);
bool allowReshare = true; // lets assume the good
if( rec.isValid() ) {
// check the permission: Is resharing allowed?
if( !rec._remotePerm.contains('R') ) {
allowReshare = false;
}
}
const QString message = QLatin1String("SHARE:OK:")+QDir::toNativeSeparators(localFile);
sendMessage(socket, message);
emit shareCommandReceived(remotePath, localFile, allowReshare);
}
}
void SocketApi::command_VERSION(const QString&, SocketType* socket)
{
sendMessage(socket, QLatin1String("VERSION:" MIRALL_VERSION_STRING ":" MIRALL_SOCKET_API_VERSION));
}
void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketType* socket)
{
sendMessage(socket, QLatin1String("SHARE_MENU_TITLE:") + tr("Share with %1", "parameter is ownCloud").arg(Theme::instance()->appNameGUI()));
}
QString SocketApi::buildRegisterPathMessage(const QString& path)
{
QFileInfo fi(path);
QString message = QLatin1String("REGISTER_PATH:");
message.append(QDir::toNativeSeparators(fi.absoluteFilePath()));
return message;
}
SqlQuery* SocketApi::getSqlQuery( Folder *folder )
{
if( !folder ) {
return 0;
}
if( _dbQueries.contains(folder) ) {
return _dbQueries[folder];
}
/* No valid sql query object yet for this folder */
int rc;
const QString sql("SELECT inode, mode, modtime, type, md5, fileid, remotePerm FROM "
"metadata WHERE phash=?1");
QString dbFileName = folder->journalDb()->databaseFilePath();
QFileInfo fi(dbFileName);
if( fi.exists() ) {
SqlDatabase *db = new SqlDatabase;
if( db && db->openReadOnly(dbFileName) ) {
_openDbs.insert(folder, db);
SqlQuery *query = new SqlQuery(*db);
rc = query->prepare(sql);
if( rc != SQLITE_OK ) {
delete query;
qDebug() << "Unable to prepare the query statement:" << rc;
return 0; // do not insert into hash
}
_dbQueries.insert( folder, query);
return query;
} else {
qDebug() << "Unable to open db" << dbFileName;
}
} else {
qDebug() << Q_FUNC_INFO << "Journal to query does not yet exist.";
}
return 0;
}
SyncJournalFileRecord SocketApi::dbFileRecord_capi( Folder *folder, QString fileName )
{
if( !(folder && folder->journalDb()) ) {
return SyncJournalFileRecord();
}
if( fileName.startsWith( folder->path() )) {
fileName.remove(0, folder->path().length());
}
// remove trailing slash
if( fileName.endsWith( QLatin1Char('/') ) ) {
fileName.truncate(fileName.length()-1);
}
SqlQuery *query = getSqlQuery(folder);
SyncJournalFileRecord rec;
if( query ) {
qlonglong phash = SyncJournalDb::getPHash( fileName );
query->bindValue(1, phash);
// int column_count = sqlite3_column_count(stmt);
if (query->next()) {
rec._path = fileName;
rec._inode = query->int64Value(0);
rec._mode = query->intValue(1);
rec._modtime = Utility::qDateTimeFromTime_t( query->int64Value(2));
rec._type = query->intValue(3);
rec._etag = query->baValue(4);
rec._fileId = query->baValue(5);
rec._remotePerm = query->baValue(6);
}
query->reset();
}
return rec;
}
/**
* Get status about a single file.
*/
SyncFileStatus SocketApi::fileStatus(Folder *folder, const QString& systemFileName, c_strlist_t *excludes )
{
QString file = folder->path();
QString fileName = systemFileName.normalized(QString::NormalizationForm_C);
QString fileNameSlash = fileName;
if( fileName != QLatin1String("/") && !fileName.isEmpty() ) {
file = folder->path() + fileName;
}
if( fileName.endsWith(QLatin1Char('/')) ) {
fileName.truncate(fileName.length()-1);
qDebug() << "Removed trailing slash: " << fileName;
} else {
fileNameSlash += QLatin1Char('/');
}
if( !FileSystem::fileExists(file) ) {
qDebug() << "OO File " << file << " is not existing";
return SyncFileStatus(SyncFileStatus::STATUS_STAT_ERROR);
}
// file is ignored?
// Qt considers .lnk files symlinks on Windows so we need to work
// around that here.
const QFileInfo fi(file);
if( fi.isSymLink()
#ifdef Q_OS_WIN
&& fi.suffix() != "lnk"
#endif
) {
return SyncFileStatus(SyncFileStatus::STATUS_IGNORE);
}
csync_ftw_type_e type = CSYNC_FTW_TYPE_FILE;
if( fi.isDir() ) {
type = CSYNC_FTW_TYPE_DIR;
}
// Is it excluded?
CSYNC_EXCLUDE_TYPE excl = csync_excluded_no_ctx(excludes, fileName.toUtf8(), type);
if( excl != CSYNC_NOT_EXCLUDED ) {
return SyncFileStatus(SyncFileStatus::STATUS_IGNORE);
}
// Error if it is in the selective sync blacklistr
foreach(const auto &s, folder->selectiveSyncBlackList()) {
if (fileNameSlash.startsWith(s)) {
return SyncFileStatus(SyncFileStatus::STATUS_ERROR);
}
}
SyncFileStatus status(SyncFileStatus::STATUS_NONE);
SyncJournalFileRecord rec = dbFileRecord_capi(folder, fileName );
if (folder->estimateState(fileName, type, &status)) {
qDebug() << Q_FUNC_INFO << "Folder estimated status for" << fileName << "to" << status.toSocketAPIString();
} else if (fileName == "") {
// sync folder itself
switch (folder->syncResult().status()) {
case SyncResult::Undefined:
case SyncResult::NotYetStarted:
case SyncResult::SyncPrepare:
case SyncResult::SyncRunning:
status.set(SyncFileStatus::STATUS_EVAL);
return status;
case SyncResult::Success:
case SyncResult::Problem:
status.set(SyncFileStatus::STATUS_SYNC);
return status;
case SyncResult::Error:
case SyncResult::SetupError:
case SyncResult::SyncAbortRequested:
status.set(SyncFileStatus::STATUS_ERROR);
return status;
case SyncResult::Paused:
status.set(SyncFileStatus::STATUS_IGNORE);
return status;
}
} else if (type == CSYNC_FTW_TYPE_DIR) {
if (rec.isValid()) {
status.set(SyncFileStatus::STATUS_SYNC);
} else {
qDebug() << Q_FUNC_INFO << "Could not determine state for folder" << fileName << "will set STATUS_NEW";
status.set(SyncFileStatus::STATUS_NEW);
}
} else if (type == CSYNC_FTW_TYPE_FILE) {
if (rec.isValid()) {
if( FileSystem::getModTime(fi.absoluteFilePath()) == Utility::qDateTimeToTime_t(rec._modtime) ) {
status.set(SyncFileStatus::STATUS_SYNC);
} else {
if (rec._remotePerm.isNull() || rec._remotePerm.contains("W") ) {
status.set(SyncFileStatus::STATUS_EVAL);
} else {
status.set(SyncFileStatus::STATUS_ERROR);
}
}
} else {
qDebug() << Q_FUNC_INFO << "Could not determine state for file" << fileName << "will set STATUS_NEW";
status.set(SyncFileStatus::STATUS_NEW);
}
}
if (rec.isValid()) {
if (rec._remotePerm.isNull()) {
// probably owncloud 6, that does not have permissions flag yet.
QString url = folder->remoteUrl().toString() + fileName;
if (url.contains(QLatin1String("/remote.php/webdav/Shared/"))) {
status.setSharedWithMe(true);
}
} else if (rec._remotePerm.contains("S")) {
status.setSharedWithMe(true);
}
}
if (status.tag() == SyncFileStatus::STATUS_NEW) {
// check the parent folder if it is shared and if it is allowed to create a file/dir within
QDir d( fi.path() );
auto parentPath = d.path();
auto dirRec = dbFileRecord_capi(folder, parentPath);
bool isDir = type == CSYNC_FTW_TYPE_DIR;
while( !d.isRoot() && !(d.exists() && dirRec.isValid()) ) {
d.cdUp(); // returns true if the dir exists.
parentPath = d.path();
// cut the folder path
dirRec = dbFileRecord_capi(folder, parentPath);
isDir = true;
}
if( dirRec.isValid() && !dirRec._remotePerm.isNull()) {
if( (isDir && !dirRec._remotePerm.contains("K"))
|| (!isDir && !dirRec._remotePerm.contains("C")) ) {
status.set(SyncFileStatus::STATUS_ERROR);
}
}
}
return status;
}
} // namespace OCC