2022-07-04 15:36:06 +03:00
/*
* Copyright ( C ) by Oleksandr Zolotov < alex @ nextcloud . 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 "shellextensionsserver.h"
# include "account.h"
# include "accountstate.h"
# include "common/shellextensionutils.h"
2022-09-02 12:48:52 +03:00
# include <libsync/vfs/cfapi/shellext/configvfscfapishellext.h>
2022-07-04 15:36:06 +03:00
# include "folder.h"
# include "folderman.h"
# include <QDir>
2022-09-02 12:48:52 +03:00
# include <QJsonArray>
2022-07-04 15:36:06 +03:00
# include <QJsonDocument>
2022-09-02 12:48:52 +03:00
# include <QJsonObject>
2022-07-04 15:36:06 +03:00
# include <QLocalSocket>
2022-09-02 12:48:52 +03:00
namespace {
constexpr auto isSharedInvalidationInterval = 2 * 60 * 1000 ; // 2 minutes, so we don't make fetch sharees requests too often
}
2022-07-04 15:36:06 +03:00
namespace OCC {
2022-09-02 12:48:52 +03:00
Q_LOGGING_CATEGORY ( lcShellExtServer , " nextcloud.gui.shellextensions.server " , QtInfoMsg )
2022-07-04 15:36:06 +03:00
ShellExtensionsServer : : ShellExtensionsServer ( QObject * parent )
: QObject ( parent )
{
2022-09-02 12:48:52 +03:00
_isSharedInvalidationInterval = isSharedInvalidationInterval ;
2022-07-04 15:36:06 +03:00
_localServer . listen ( VfsShellExtensions : : serverNameForApplicationNameDefault ( ) ) ;
connect ( & _localServer , & QLocalServer : : newConnection , this , & ShellExtensionsServer : : slotNewConnection ) ;
}
ShellExtensionsServer : : ~ ShellExtensionsServer ( )
{
2022-09-02 12:48:52 +03:00
for ( const auto & connection : _customStateSocketConnections ) {
if ( connection ) {
QObject : : disconnect ( connection ) ;
}
}
_customStateSocketConnections . clear ( ) ;
2022-07-04 15:36:06 +03:00
if ( ! _localServer . isListening ( ) ) {
return ;
}
_localServer . close ( ) ;
}
2022-09-02 12:48:52 +03:00
QString ShellExtensionsServer : : getFetchThumbnailPath ( )
{
return QStringLiteral ( " /index.php/core/preview " ) ;
}
void ShellExtensionsServer : : setIsSharedInvalidationInterval ( qint64 interval )
{
_isSharedInvalidationInterval = interval ;
}
2022-07-04 15:36:06 +03:00
void ShellExtensionsServer : : sendJsonMessageWithVersion ( QLocalSocket * socket , const QVariantMap & message )
{
socket - > write ( VfsShellExtensions : : Protocol : : createJsonMessage ( message ) ) ;
socket - > waitForBytesWritten ( ) ;
}
void ShellExtensionsServer : : sendEmptyDataAndCloseSession ( QLocalSocket * socket )
{
sendJsonMessageWithVersion ( socket , QVariantMap { } ) ;
closeSession ( socket ) ;
}
void ShellExtensionsServer : : closeSession ( QLocalSocket * socket )
{
connect ( socket , & QLocalSocket : : disconnected , this , [ socket ] {
socket - > close ( ) ;
socket - > deleteLater ( ) ;
} ) ;
socket - > disconnectFromServer ( ) ;
}
2022-09-02 12:48:52 +03:00
void ShellExtensionsServer : : processCustomStateRequest ( QLocalSocket * socket , const CustomStateRequestInfo & customStateRequestInfo )
{
if ( ! customStateRequestInfo . isValid ( ) ) {
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
const auto folder = FolderMan : : instance ( ) - > folder ( customStateRequestInfo . folderAlias ) ;
if ( ! folder ) {
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
2022-10-21 17:11:29 +03:00
2022-09-02 12:48:52 +03:00
const auto filePathRelative = QString ( customStateRequestInfo . path ) . remove ( folder - > path ( ) ) ;
SyncJournalFileRecord record ;
if ( ! folder - > journalDb ( ) - > getFileRecord ( filePathRelative , & record ) | | ! record . isValid ( ) | | record . path ( ) . isEmpty ( ) ) {
qCWarning ( lcShellExtServer ) < < " Record not found in SyncJournal for: " < < filePathRelative ;
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
const auto composeMessageReplyFromRecord = [ ] ( const SyncJournalFileRecord & record ) {
QVariantList states ;
if ( record . _lockstate . _locked ) {
states . push_back ( QString ( CUSTOM_STATE_ICON_LOCKED_INDEX ) . toInt ( ) - QString ( CUSTOM_STATE_ICON_INDEX_OFFSET ) . toInt ( ) ) ;
}
if ( record . _isShared ) {
states . push_back ( QString ( CUSTOM_STATE_ICON_SHARED_INDEX ) . toInt ( ) - QString ( CUSTOM_STATE_ICON_INDEX_OFFSET ) . toInt ( ) ) ;
}
return QVariantMap { { VfsShellExtensions : : Protocol : : CustomStateDataKey ,
QVariantMap { { VfsShellExtensions : : Protocol : : CustomStateStatesKey , states } } } } ;
} ;
2022-10-21 17:11:29 +03:00
if ( QDateTime : : currentMSecsSinceEpoch ( ) - record . _lastShareStateFetchedTimestamp < _isSharedInvalidationInterval ) {
qCInfo ( lcShellExtServer ) < < record . path ( ) < < " record._lastShareStateFetchedTimestamp has less than " < < _isSharedInvalidationInterval < < " ms difference with QDateTime::currentMSecsSinceEpoch(). Returning data from SyncJournal. " ;
2022-09-02 12:48:52 +03:00
sendJsonMessageWithVersion ( socket , composeMessageReplyFromRecord ( record ) ) ;
closeSession ( socket ) ;
return ;
}
2022-10-21 17:11:29 +03:00
const auto lsColJobPath = [ folder , & filePathRelative ] ( ) {
2022-09-02 12:48:52 +03:00
const auto filePathRelativeRemote = QDir ( folder - > remotePath ( ) ) . filePath ( filePathRelative ) ;
// either get parent's path, or, return '/' if we are in the root folder
auto recordPathSplit = filePathRelativeRemote . split ( QLatin1Char ( ' / ' ) , Qt : : SkipEmptyParts ) ;
if ( recordPathSplit . size ( ) > 1 ) {
recordPathSplit . removeLast ( ) ;
return recordPathSplit . join ( QLatin1Char ( ' / ' ) ) ;
}
return QStringLiteral ( " / " ) ;
} ( ) ;
2022-10-21 17:11:29 +03:00
if ( _runningLsColJobsForPaths . contains ( lsColJobPath ) ) {
qCInfo ( lcShellExtServer ) < < " LsColJob is already running for path: " < < lsColJobPath ;
sendJsonMessageWithVersion ( socket , composeMessageReplyFromRecord ( record ) ) ;
closeSession ( socket ) ;
return ;
2022-09-02 12:48:52 +03:00
}
2022-10-21 17:11:29 +03:00
_customStateSocketConnections . insert ( socket - > socketDescriptor ( ) , QObject : : connect ( this , & ShellExtensionsServer : : directoryListingIterationFinished , [ this , socket , filePathRelative , composeMessageReplyFromRecord ] ( const QString & folderAlias ) {
{
const auto connection = _customStateSocketConnections [ socket - > socketDescriptor ( ) ] ;
if ( connection ) {
QObject : : disconnect ( connection ) ;
}
_customStateSocketConnections . remove ( socket - > socketDescriptor ( ) ) ;
}
const auto folder = FolderMan : : instance ( ) - > folder ( folderAlias ) ;
SyncJournalFileRecord record ;
if ( ! folder | | ! folder - > journalDb ( ) - > getFileRecord ( filePathRelative , & record ) | | ! record . isValid ( ) ) {
qCWarning ( lcShellExtServer ) < < " Record not found in SyncJournal for: " < < filePathRelative ;
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
qCInfo ( lcShellExtServer ) < < " Sending reply from LsColJob for socket: " < < socket - > socketDescriptor ( ) < < " and record: " < < record . path ( ) ;
sendJsonMessageWithVersion ( socket , composeMessageReplyFromRecord ( record ) ) ;
closeSession ( socket ) ;
} ) ) ;
auto * const lsColJob = new LsColJob ( folder - > accountState ( ) - > account ( ) , QDir : : cleanPath ( folder - > remotePath ( ) + lsColJobPath ) , this ) ;
lsColJob - > setProperties ( { QByteArrayLiteral ( " http://owncloud.org/ns:share-types " ) , QByteArrayLiteral ( " http://owncloud.org/ns:permissions " ) } ) ;
const auto folderAlias = customStateRequestInfo . folderAlias ;
QObject : : connect ( lsColJob , & LsColJob : : directoryListingIterated , this , [ this , folderAlias , lsColJobPath ] ( const QString & name , const QMap < QString , QString > & properties ) {
const auto folder = FolderMan : : instance ( ) - > folder ( folderAlias ) ;
if ( ! folder ) {
qCWarning ( lcShellExtServer ) < < " No folder found for folderAlias: " < < folderAlias ;
return ;
}
SyncJournalFileRecord record ;
const auto filePathWithoutDavPath = QString ( name ) . remove ( folder - > accountState ( ) - > account ( ) - > davPathRoot ( ) ) ;
const auto filePathAdjusted = ( filePathWithoutDavPath . size ( ) > 1 & & filePathWithoutDavPath . startsWith ( QLatin1Char ( ' / ' ) ) ) ? filePathWithoutDavPath . mid ( 1 ) : filePathWithoutDavPath ;
if ( filePathAdjusted . isEmpty ( ) | | filePathAdjusted = = lsColJobPath ) {
// we are skipping the first item as it is the current path, but we are interested in nested items
return ;
}
if ( ! folder | | ! folder - > journalDb ( ) - > getFileRecord ( filePathAdjusted , & record ) | | ! record . isValid ( ) ) {
return ;
}
const auto isIncomingShare = properties . contains ( QStringLiteral ( " permissions " ) ) & & RemotePermissions : : fromServerString ( properties . value ( QStringLiteral ( " permissions " ) ) ) . hasPermission ( OCC : : RemotePermissions : : IsShared ) ;
const auto sharedByMe = ! properties . value ( QStringLiteral ( " share-types " ) ) . isEmpty ( ) ;
record . _sharedByMe = sharedByMe ;
record . _isShared = isIncomingShare | | sharedByMe ;
record . _lastShareStateFetchedTimestamp = QDateTime : : currentMSecsSinceEpoch ( ) ;
if ( ! folder - > journalDb ( ) - > setFileRecord ( record ) ) {
qCWarning ( lcShellExtServer ) < < " Could not set file record for path: " < < record . _path ;
emit directoryListingIterationFinished ( folderAlias ) ;
return ;
}
} ) ;
QObject : : connect ( lsColJob , & LsColJob : : finishedWithError , this , [ this , folderAlias , lsColJobPath ] ( QNetworkReply * reply ) {
_runningLsColJobsForPaths . removeOne ( lsColJobPath ) ;
const auto httpCode = reply - > attribute ( QNetworkRequest : : HttpStatusCodeAttribute ) . toInt ( ) ;
qCWarning ( lcShellExtServer ) < < " LSCOL job error " < < reply - > errorString ( ) < < httpCode < < reply - > error ( ) ;
emit directoryListingIterationFinished ( folderAlias ) ;
} ) ;
QObject : : connect ( lsColJob , & LsColJob : : finishedWithoutError , this , [ this , folderAlias , lsColJobPath ] ( ) {
_runningLsColJobsForPaths . removeOne ( lsColJobPath ) ;
emit directoryListingIterationFinished ( folderAlias ) ;
} ) ;
_runningLsColJobsForPaths . push_back ( lsColJobPath ) ;
lsColJob - > start ( ) ;
2022-09-02 12:48:52 +03:00
}
2022-07-04 15:36:06 +03:00
void ShellExtensionsServer : : processThumbnailRequest ( QLocalSocket * socket , const ThumbnailRequestInfo & thumbnailRequestInfo )
{
if ( ! thumbnailRequestInfo . isValid ( ) ) {
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
const auto folder = FolderMan : : instance ( ) - > folder ( thumbnailRequestInfo . folderAlias ) ;
if ( ! folder ) {
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
const auto fileInfo = QFileInfo ( thumbnailRequestInfo . path ) ;
const auto filePathRelative = QFileInfo ( thumbnailRequestInfo . path ) . canonicalFilePath ( ) . remove ( folder - > path ( ) ) ;
SyncJournalFileRecord record ;
if ( ! folder - > journalDb ( ) - > getFileRecord ( filePathRelative , & record ) | | ! record . isValid ( ) ) {
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
QUrlQuery queryItems ;
queryItems . addQueryItem ( QStringLiteral ( " fileId " ) , record . _fileId ) ;
queryItems . addQueryItem ( QStringLiteral ( " x " ) , QString : : number ( thumbnailRequestInfo . size . width ( ) ) ) ;
queryItems . addQueryItem ( QStringLiteral ( " y " ) , QString : : number ( thumbnailRequestInfo . size . height ( ) ) ) ;
2022-09-02 12:48:52 +03:00
const QUrl jobUrl = Utility : : concatUrlPath ( folder - > accountState ( ) - > account ( ) - > url ( ) , getFetchThumbnailPath ( ) , queryItems ) ;
2022-07-04 15:36:06 +03:00
const auto job = new SimpleNetworkJob ( folder - > accountState ( ) - > account ( ) ) ;
job - > startRequest ( QByteArrayLiteral ( " GET " ) , jobUrl ) ;
connect ( job , & SimpleNetworkJob : : finishedSignal , this , [ socket , this ] ( QNetworkReply * reply ) {
const auto contentType = reply - > header ( QNetworkRequest : : ContentTypeHeader ) . toByteArray ( ) ;
if ( ! contentType . startsWith ( QByteArrayLiteral ( " image/ " ) ) ) {
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
auto messageReplyWithThumbnail = QVariantMap {
{ VfsShellExtensions : : Protocol : : ThumnailProviderDataKey , reply - > readAll ( ) . toBase64 ( ) }
} ;
sendJsonMessageWithVersion ( socket , messageReplyWithThumbnail ) ;
closeSession ( socket ) ;
} ) ;
}
void ShellExtensionsServer : : slotNewConnection ( )
{
const auto socket = _localServer . nextPendingConnection ( ) ;
if ( ! socket ) {
return ;
}
socket - > waitForReadyRead ( ) ;
const auto message = QJsonDocument : : fromJson ( socket - > readAll ( ) ) . toVariant ( ) . toMap ( ) ;
if ( ! VfsShellExtensions : : Protocol : : validateProtocolVersion ( message ) ) {
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
2022-09-02 12:48:52 +03:00
if ( message . contains ( VfsShellExtensions : : Protocol : : ThumbnailProviderRequestKey ) ) {
parseThumbnailRequest ( socket , message ) ;
return ;
} else if ( message . contains ( VfsShellExtensions : : Protocol : : CustomStateProviderRequestKey ) ) {
parseCustomStateRequest ( socket , message ) ;
return ;
}
qCWarning ( lcShellExtServer ) < < " Invalid message received from shell extension: " < < message ;
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
void ShellExtensionsServer : : parseCustomStateRequest ( QLocalSocket * socket , const QVariantMap & message )
{
const auto customStateRequestMessage = message . value ( VfsShellExtensions : : Protocol : : CustomStateProviderRequestKey ) . toMap ( ) ;
const auto itemFilePath = QDir : : fromNativeSeparators ( customStateRequestMessage . value ( VfsShellExtensions : : Protocol : : FilePathKey ) . toString ( ) ) ;
if ( itemFilePath . isEmpty ( ) ) {
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
QString foundFolderAlias ;
for ( const auto folder : FolderMan : : instance ( ) - > map ( ) ) {
if ( itemFilePath . startsWith ( folder - > path ( ) ) ) {
foundFolderAlias = folder - > alias ( ) ;
break ;
}
}
if ( foundFolderAlias . isEmpty ( ) ) {
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
const auto customStateRequestInfo = CustomStateRequestInfo {
itemFilePath ,
foundFolderAlias
} ;
processCustomStateRequest ( socket , customStateRequestInfo ) ;
}
void ShellExtensionsServer : : parseThumbnailRequest ( QLocalSocket * socket , const QVariantMap & message )
{
2022-07-04 15:36:06 +03:00
const auto thumbnailRequestMessage = message . value ( VfsShellExtensions : : Protocol : : ThumbnailProviderRequestKey ) . toMap ( ) ;
2022-09-02 12:48:52 +03:00
const auto thumbnailFilePath = QDir : : fromNativeSeparators ( thumbnailRequestMessage . value ( VfsShellExtensions : : Protocol : : FilePathKey ) . toString ( ) ) ;
2022-07-04 15:36:06 +03:00
const auto thumbnailFileSize = thumbnailRequestMessage . value ( VfsShellExtensions : : Protocol : : ThumbnailProviderRequestFileSizeKey ) . toMap ( ) ;
if ( thumbnailFilePath . isEmpty ( ) | | thumbnailFileSize . isEmpty ( ) ) {
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
QString foundFolderAlias ;
for ( const auto folder : FolderMan : : instance ( ) - > map ( ) ) {
if ( thumbnailFilePath . startsWith ( folder - > path ( ) ) ) {
foundFolderAlias = folder - > alias ( ) ;
break ;
}
}
if ( foundFolderAlias . isEmpty ( ) ) {
sendEmptyDataAndCloseSession ( socket ) ;
return ;
}
const auto thumbnailRequestInfo = ThumbnailRequestInfo {
thumbnailFilePath ,
QSize ( thumbnailFileSize . value ( " width " ) . toInt ( ) , thumbnailFileSize . value ( " height " ) . toInt ( ) ) ,
foundFolderAlias
} ;
processThumbnailRequest ( socket , thumbnailRequestInfo ) ;
}
} // namespace OCC