diff options
Diffstat (limited to 'src')
53 files changed, 821 insertions, 387 deletions
diff --git a/src/common/ownsql.h b/src/common/ownsql.h index 250fa109b..a5e53e8d0 100644 --- a/src/common/ownsql.h +++ b/src/common/ownsql.h @@ -21,6 +21,7 @@ #include <QObject> #include <QVariant> +#include <QSet> #include "ocsynclib.h" diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index 1e59a7947..8a41a4ac6 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -1683,6 +1683,8 @@ void SyncJournalDb::setSelectiveSyncList(SyncJournalDb::SelectiveSyncListType ty return; } + startTransaction(); + //first, delete all entries of this type SqlQuery delQuery("DELETE FROM selectivesync WHERE type == ?1", _db); delQuery.bindValue(1, int(type)); @@ -1699,6 +1701,8 @@ void SyncJournalDb::setSelectiveSyncList(SyncJournalDb::SelectiveSyncListType ty qCWarning(lcDb) << "SQL error when inserting into selective sync" << type << path << delQuery.error(); } } + + commitInternal("setSelectiveSyncList"); } void SyncJournalDb::avoidRenamesOnNextSync(const QByteArray &path) diff --git a/src/common/syncjournaldb.h b/src/common/syncjournaldb.h index 2ba00dc45..4db859bbe 100644 --- a/src/common/syncjournaldb.h +++ b/src/common/syncjournaldb.h @@ -113,6 +113,12 @@ public: int _errorCount; bool _valid; QByteArray _contentChecksum; + /** + * Returns true if this entry refers to a chunked upload that can be continued. + * (As opposed to a small file transfer which is stored in the db so we can detect the case + * when the upload succeeded, but the connection was dropped before we got the answer) + */ + bool isChunked() const { return _transferid != 0; } }; struct PollInfo @@ -222,13 +228,13 @@ public: /// Store a new or updated record in the database void setConflictRecord(const ConflictRecord &record); - /// Retrieve a conflict record by path of the _conflict- file + /// Retrieve a conflict record by path of the file with the conflict tag ConflictRecord conflictRecord(const QByteArray &path); - /// Delete a conflict record by path of the _conflict- file + /// Delete a conflict record by path of the file with the conflict tag void deleteConflictRecord(const QByteArray &path); - /// Return all paths of _conflict- files with records in the db + /// Return all paths of files with a conflict tag in the name and records in the db QByteArrayList conflictRecordPaths(); diff --git a/src/common/syncjournalfilerecord.h b/src/common/syncjournalfilerecord.h index b09006365..43259c1e6 100644 --- a/src/common/syncjournalfilerecord.h +++ b/src/common/syncjournalfilerecord.h @@ -114,18 +114,15 @@ public: /** Represents a conflict in the conflicts table. * - * In the following the "conflict file" is the file with the "_conflict-" - * tag and the base file is the file that its a conflict for. So if - * a/foo.txt is the base file, its conflict file could be - * a/foo_conflict-1234.txt. + * In the following the "conflict file" is the file that has the conflict + * tag in the filename, and the base file is the file that it's a conflict for. + * So if "a/foo.txt" is the base file, its conflict file could be + * "a/foo (conflicted copy 1234).txt". */ class OCSYNC_EXPORT ConflictRecord { public: - /** Path to the _conflict- file - * - * So if a/foo.txt has a conflict, this path would point to - * a/foo_conflict-1234.txt. + /** Path to the file with the conflict tag in the name * * The path is sync-folder relative. */ diff --git a/src/common/utility.cpp b/src/common/utility.cpp index 70e7a5ae7..d2a46bb7c 100644 --- a/src/common/utility.cpp +++ b/src/common/utility.cpp @@ -548,19 +548,23 @@ QString Utility::makeConflictFileName( const QString &fn, const QDateTime &dt, const QString &user) { QString conflictFileName(fn); - // Add _conflict-XXXX before the extension. + // Add conflict tag before the extension. int dotLocation = conflictFileName.lastIndexOf('.'); // If no extension, add it at the end (take care of cases like foo/.hidden or foo.bar/file) if (dotLocation <= conflictFileName.lastIndexOf('/') + 1) { dotLocation = conflictFileName.size(); } - QString conflictMarker = QStringLiteral("_conflict-"); + QString conflictMarker = QStringLiteral(" (conflicted copy "); if (!user.isEmpty()) { - conflictMarker.append(sanitizeForFileName(user)); - conflictMarker.append('-'); + // Don't allow parens in the user name, to ensure + // we can find the beginning and end of the conflict tag. + const auto userName = sanitizeForFileName(user).replace('(', '_').replace(')', '_'); + conflictMarker.append(userName); + conflictMarker.append(' '); } - conflictMarker.append(dt.toString("yyyyMMdd-hhmmss")); + conflictMarker.append(dt.toString("yyyy-MM-dd hhmmss")); + conflictMarker.append(')'); conflictFileName.insert(dotLocation, conflictMarker); return conflictFileName; @@ -575,13 +579,28 @@ bool Utility::isConflictFile(const char *name) bname = name; } - return std::strstr(bname, "_conflict-"); + // Old pattern + if (std::strstr(bname, "_conflict-")) + return true; + + // New pattern + if (std::strstr(bname, "(conflicted copy")) + return true; + + return false; } bool Utility::isConflictFile(const QString &name) { auto bname = name.midRef(name.lastIndexOf('/') + 1); - return bname.contains("_conflict-", Utility::fsCasePreserving() ? Qt::CaseInsensitive : Qt::CaseSensitive); + + if (bname.contains(QStringLiteral("_conflict-"))) + return true; + + if (bname.contains(QStringLiteral("(conflicted copy"))) + return true; + + return false; } QByteArray Utility::conflictFileBaseName(const QByteArray &conflictName) @@ -589,19 +608,29 @@ QByteArray Utility::conflictFileBaseName(const QByteArray &conflictName) // This function must be able to deal with conflict files for conflict files. // To do this, we scan backwards, for the outermost conflict marker and // strip only that to generate the conflict file base name. - int from = conflictName.size(); - while (from != -1) { - auto start = conflictName.lastIndexOf("_conflict-", from); - if (start == -1) - return ""; - from = start - 1; - - auto end = conflictName.indexOf('.', start); - if (end == -1) - end = conflictName.size(); - return conflictName.left(start) + conflictName.mid(end); + auto startOld = conflictName.lastIndexOf("_conflict-"); + + // A single space before "(conflicted copy" is considered part of the tag + auto startNew = conflictName.lastIndexOf("(conflicted copy"); + if (startNew > 0 && conflictName[startNew - 1] == ' ') + startNew -= 1; + + // The rightmost tag is relevant + auto tagStart = qMax(startOld, startNew); + if (tagStart == -1) + return ""; + + // Find the end of the tag + auto tagEnd = conflictName.size(); + auto dot = conflictName.lastIndexOf('.'); // dot could be part of user name for new tag! + if (dot > tagStart) + tagEnd = dot; + if (tagStart == startNew) { + auto paren = conflictName.indexOf(')', tagStart); + if (paren != -1) + tagEnd = paren + 1; } - return ""; + return conflictName.left(tagStart) + conflictName.mid(tagEnd); } QString Utility::sanitizeForFileName(const QString &name) diff --git a/src/crashreporter/CMakeLists.txt b/src/crashreporter/CMakeLists.txt index 693fd9efd..40b37e95a 100644 --- a/src/crashreporter/CMakeLists.txt +++ b/src/crashreporter/CMakeLists.txt @@ -1,9 +1,6 @@ PROJECT( CrashReporter ) cmake_policy(SET CMP0017 NEW) -list(APPEND crashreporter_SOURCES main.cpp) -list(APPEND crashreporter_RC resources.qrc) - # TODO: differentiate release channel # if(BUILD_RELEASE) # set(CRASHREPORTER_RELEASE_CHANNEL "release") @@ -11,9 +8,27 @@ list(APPEND crashreporter_RC resources.qrc) set(CRASHREPORTER_RELEASE_CHANNEL "nightly") # endif() +# Theme +if(DEFINED OEM_THEME_DIR AND EXISTS "${OEM_THEME_DIR}/theme/colored") + set(CRASHREPORTER_ICON_DIR "${OEM_THEME_DIR}/theme/colored") +else() + set(CRASHREPORTER_ICON_DIR "${CMAKE_SOURCE_DIR}/theme/colored") +endif() + +set(CRASHREPORTER_ICON_FILENAME "${APPLICATION_ICON_NAME}-icon.png") +set(CRASHREPORTER_ICON ":/${CRASHREPORTER_ICON_FILENAME}") +set(CRASHREPORTER_ICON_SIZE "128") +set(CRASHREPORTER_ICON_PATH "${CRASHREPORTER_ICON_DIR}/${CRASHREPORTER_ICON_SIZE}-${CRASHREPORTER_ICON_FILENAME}") + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/resources.qrc.in + ${CMAKE_CURRENT_BINARY_DIR}/resources.qrc) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/CrashReporterConfig.h.in ${CMAKE_CURRENT_BINARY_DIR}/CrashReporterConfig.h) +# Sources +list(APPEND crashreporter_SOURCES main.cpp) +list(APPEND crashreporter_RC "${CMAKE_CURRENT_BINARY_DIR}/resources.qrc") if(NOT BUILD_LIBRARIES_ONLY) @@ -21,13 +36,14 @@ if(NOT BUILD_LIBRARIES_ONLY) ${crashreporter_SOURCES} ${crashreporter_HEADERS_MOC} ${crashreporter_UI_HEADERS} - ${crashreporter_RC_RCC} + ${crashreporter_RC} ) find_package(Qt5 REQUIRED COMPONENTS Widgets) target_include_directories(${CRASHREPORTER_EXECUTABLE} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) set_target_properties(${CRASHREPORTER_EXECUTABLE} PROPERTIES AUTOMOC ON) + set_target_properties(${CRASHREPORTER_EXECUTABLE} PROPERTIES AUTORCC ON) set_target_properties(${CRASHREPORTER_EXECUTABLE} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${BIN_OUTPUT_DIRECTORY} ) set_target_properties(${CRASHREPORTER_EXECUTABLE} PROPERTIES INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${LIB_INSTALL_DIR}/${APPLICATION_EXECUTABLE}" ) target_link_libraries(${CRASHREPORTER_EXECUTABLE} diff --git a/src/crashreporter/resources.qrc b/src/crashreporter/resources.qrc deleted file mode 100644 index 830281e77..000000000 --- a/src/crashreporter/resources.qrc +++ /dev/null @@ -1,5 +0,0 @@ -<RCC> - <qresource prefix="/"> - <file alias="owncloud-icon.png">../../theme/colored/owncloud-icon-128.png</file> - </qresource> -</RCC> diff --git a/src/crashreporter/resources.qrc.in b/src/crashreporter/resources.qrc.in new file mode 100644 index 000000000..8afaf4327 --- /dev/null +++ b/src/crashreporter/resources.qrc.in @@ -0,0 +1,5 @@ +<RCC> + <qresource prefix="/"> + <file alias="@CRASHREPORTER_ICON_FILENAME@">@CRASHREPORTER_ICON_PATH@</file> + </qresource> +</RCC> diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index 5a0933c31..c64589fff 100644 --- a/src/csync/csync_exclude.cpp +++ b/src/csync/csync_exclude.cpp @@ -220,8 +220,8 @@ static CSYNC_EXCLUDE_TYPE _csync_excluded_common(const char *path, bool excludeC } #endif - /* We create a desktop.ini on Windows for the sidebar icon, make sure we don't sync them. */ - if (blen == 11) { + /* We create a Desktop.ini on Windows for the sidebar icon, make sure we don't sync it. */ + if (blen == 11 && path == bname) { rc = csync_fnmatch("Desktop.ini", bname, 0); if (rc == 0) { match = CSYNC_FILE_SILENTLY_EXCLUDED; diff --git a/src/csync/csync_update.cpp b/src/csync/csync_update.cpp index 4de7dcb90..2f180dda4 100644 --- a/src/csync/csync_update.cpp +++ b/src/csync/csync_update.cpp @@ -207,13 +207,17 @@ static int _csync_detect_update(CSYNC *ctx, std::unique_ptr<csync_file_stat_t> f /* we have an update! */ qCInfo(lcUpdate, "Database entry found, compare: %" PRId64 " <-> %" PRId64 ", etag: %s <-> %s, inode: %" PRId64 " <-> %" PRId64 - ", size: %" PRId64 " <-> %" PRId64 ", perms: %x <-> %x" - ", type: %d <-> %d, ignore: %d", + ", size: %" PRId64 " <-> %" PRId64 + ", perms: %x <-> %x" + ", type: %d <-> %d" + ", checksum: %s <-> %s" + ", ignore: %d", ((int64_t) fs->modtime), ((int64_t) base._modtime), fs->etag.constData(), base._etag.constData(), (uint64_t) fs->inode, (uint64_t) base._inode, (uint64_t) fs->size, (uint64_t) base._fileSize, *reinterpret_cast<short*>(&fs->remotePerm), *reinterpret_cast<short*>(&base._remotePerm), fs->type, base._type, + fs->checksumHeader.constData(), base._checksumHeader.constData(), base._serverHasIgnoredFiles ); // If the db suggests a placeholder should be downloaded, diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index d3edef9aa..1745e1b31 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -216,12 +216,6 @@ endif() # list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(ECMAddAppIcon) -# For historical reasons we can not use the application_shortname -# for ownCloud but must rather set it manually. -if (NOT DEFINED APPLICATION_ICON_NAME) - set(APPLICATION_ICON_NAME ${APPLICATION_SHORTNAME}) -endif() - file(GLOB_RECURSE OWNCLOUD_ICONS "${theme_dir}/colored/*-${APPLICATION_ICON_NAME}-icon*") if(APPLE) file(GLOB_RECURSE OWNCLOUD_SIDEBAR_ICONS "${theme_dir}/colored/*-${APPLICATION_ICON_NAME}-sidebar*") diff --git a/src/gui/activitydata.h b/src/gui/activitydata.h index 40cd35e05..7d14600c1 100644 --- a/src/gui/activitydata.h +++ b/src/gui/activitydata.h @@ -58,9 +58,10 @@ public: QString _file; QUrl _link; QDateTime _dateTime; - QString _accName; + QString _accName; /* display name of the account involved */ - QVector<ActivityLink> _links; + QVector<ActivityLink> _links; /* These links are transformed into buttons that + * call links as reactions on the activity */ /** * @brief Sort operator to sort the list youngest first. * @param val diff --git a/src/gui/activitywidget.h b/src/gui/activitywidget.h index 89fd6c08c..1195e7ffb 100644 --- a/src/gui/activitywidget.h +++ b/src/gui/activitywidget.h @@ -92,7 +92,7 @@ private slots: void slotNotifyNetworkError(QNetworkReply *); void slotNotifyServerFinished(const QString &reply, int replyCode); void endNotificationRequest(NotificationWidget *widget, int replyCode); - void scheduleWidgetToRemove(NotificationWidget *widget, int milliseconds = 4500); + void scheduleWidgetToRemove(NotificationWidget *widget, int milliseconds = 100); void slotCheckToCleanWidgets(); private: diff --git a/src/gui/application.cpp b/src/gui/application.cpp index 34f811375..703f1f843 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -377,13 +377,17 @@ void Application::slotownCloudWizardDone(int res) void Application::setupLogging() { // might be called from second instance - Logger::instance()->setLogFile(_logFile); - Logger::instance()->setLogDir(_logDir); - Logger::instance()->setLogExpire(_logExpire); - Logger::instance()->setLogFlush(_logFlush); - Logger::instance()->setLogDebug(_logDebug); + auto logger = Logger::instance(); + logger->setLogFile(_logFile); + logger->setLogDir(_logDir); + logger->setLogExpire(_logExpire); + logger->setLogFlush(_logFlush); + logger->setLogDebug(_logDebug); + if (!logger->isLoggingToFile() && ConfigFile().automaticLogDir()) { + logger->setupTemporaryFolderLogDir(); + } - Logger::instance()->enterNextLogFile(); + logger->enterNextLogFile(); qCInfo(lcApplication) << QString::fromLatin1("################## %1 locale:[%2] ui_lang:[%3] version:[%4] os:[%5]").arg(_theme->appName()).arg(QLocale::system().name()).arg(property("ui_lang").toString()).arg(_theme->version()).arg(Utility::platformName()); } diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index b9ed12928..32f562a09 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -122,6 +122,10 @@ void Folder::checkLocalPath() { const QFileInfo fi(_definition.localPath); _canonicalLocalPath = fi.canonicalFilePath(); +#ifdef Q_OS_MAC + // Workaround QTBUG-55896 (Should be fixed in Qt 5.8) + _canonicalLocalPath = _canonicalLocalPath.normalized(QString::NormalizationForm_C); +#endif if (_canonicalLocalPath.isEmpty()) { qCWarning(lcFolder) << "Broken symlink:" << _definition.localPath; _canonicalLocalPath = _definition.localPath; @@ -488,6 +492,8 @@ void Folder::slotWatchedPathChanged(const QString &path) return; // probably a spurious notification } + warnOnNewExcludedItem(record, relativePath); + emit watchedFileChangedExternally(path); // Also schedule this folder for a sync, but only after some delay: @@ -1001,6 +1007,38 @@ void Folder::slotFolderConflicts(const QString &folder, const QStringList &confl r.setNumOldConflictItems(conflictPaths.size() - r.numNewConflictItems()); } +void Folder::warnOnNewExcludedItem(const SyncJournalFileRecord &record, const QStringRef &path) +{ + // Never warn for items in the database + if (record.isValid()) + return; + + // Don't warn for items that no longer exist. + // Note: This assumes we're getting file watcher notifications + // for folders only on creation and deletion - if we got a notification + // on content change that would create spurious warnings. + QFileInfo fi(_canonicalLocalPath + path); + if (!fi.exists()) + return; + + bool ok = false; + auto blacklist = _journal.getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok); + if (!ok) + return; + if (!blacklist.contains(path + "/")) + return; + + const auto message = fi.isDir() + ? tr("The folder %1 was created but was excluded from synchronization previously. " + "Data inside it will not be synchronized.") + .arg(fi.filePath()) + : tr("The file %1 was created but was excluded from synchronization previously. " + "It will not be synchronized.") + .arg(fi.filePath()); + + Logger::instance()->postOptionalGuiLog(Theme::instance()->appNameGUI(), message); +} + void Folder::scheduleThisFolderSoon() { if (!_scheduleSelfTimer.isActive()) { diff --git a/src/gui/folder.h b/src/gui/folder.h index 2bd8a2882..276cb2d9d 100644 --- a/src/gui/folder.h +++ b/src/gui/folder.h @@ -328,6 +328,9 @@ private slots: */ void slotFolderConflicts(const QString &folder, const QStringList &conflictPaths); + /** Warn users if they create a file or folder that is selective-sync excluded */ + void warnOnNewExcludedItem(const SyncJournalFileRecord &record, const QStringRef &path); + private: bool reloadExcludes(); diff --git a/src/gui/folderstatusmodel.cpp b/src/gui/folderstatusmodel.cpp index ec2104c7e..2e784101b 100644 --- a/src/gui/folderstatusmodel.cpp +++ b/src/gui/folderstatusmodel.cpp @@ -245,9 +245,15 @@ QVariant FolderStatusModel::data(const QModelIndex &index, int role) const } else if (status == SyncResult::Undefined) { return theme->syncStateIcon(SyncResult::SyncRunning); } else { - // keep the previous icon for the prepare phase. - if (status == SyncResult::Problem) { - return theme->syncStateIcon(SyncResult::Success); + // The "Problem" *result* just means some files weren't + // synced, so we show "Success" in these cases. But we + // do use the "Problem" *icon* for unresolved conflicts. + if (status == SyncResult::Success || status == SyncResult::Problem) { + if (f->syncResult().hasUnresolvedConflicts()) { + return theme->syncStateIcon(SyncResult::Problem); + } else { + return theme->syncStateIcon(SyncResult::Success); + } } else { return theme->syncStateIcon(status); } @@ -526,7 +532,7 @@ bool FolderStatusModel::canFetchMore(const QModelIndex &parent) const return false; } auto info = infoForIndex(parent); - if (!info || info->_fetched || info->_fetching) + if (!info || info->_fetched || info->_fetchingJob) return false; if (info->_hasError) { // Keep showing the error to the user, it will be hidden when the account reconnects @@ -540,10 +546,9 @@ void FolderStatusModel::fetchMore(const QModelIndex &parent) { auto info = infoForIndex(parent); - if (!info || info->_fetched || info->_fetching) + if (!info || info->_fetched || info->_fetchingJob) return; info->resetSubs(this, parent); - info->_fetching = true; QString path = info->_folder->remotePath(); if (info->_path != QLatin1String("/")) { if (!path.endsWith(QLatin1Char('/'))) { @@ -552,6 +557,7 @@ void FolderStatusModel::fetchMore(const QModelIndex &parent) path += info->_path; } LsColJob *job = new LsColJob(_accountState->account(), path, this); + info->_fetchingJob = job; job->setProperties(QList<QByteArray>() << "resourcetype" << "http://owncloud.org/ns:size" << "http://owncloud.org/ns:permissions"); @@ -596,18 +602,18 @@ void FolderStatusModel::slotUpdateDirectories(const QStringList &list) if (!parentInfo) { return; } - ASSERT(parentInfo->_fetching); // we should only get a result if we were doing a fetch + ASSERT(parentInfo->_fetchingJob == job); ASSERT(parentInfo->_subs.isEmpty()); if (parentInfo->hasLabel()) { beginRemoveRows(idx, 0, 0); - parentInfo->_lastErrorString.clear(); parentInfo->_hasError = false; parentInfo->_fetchingLabel = false; endRemoveRows(); } - parentInfo->_fetching = false; + parentInfo->_lastErrorString.clear(); + parentInfo->_fetchingJob = nullptr; parentInfo->_fetched = true; QUrl url = parentInfo->_folder->remoteUrl(); @@ -1197,7 +1203,7 @@ void FolderStatusModel::slotShowFetchProgress() if (it.value().elapsed() > 800) { auto idx = it.key(); auto *info = infoForIndex(idx); - if (info && info->_fetching) { + if (info && info->_fetchingJob) { bool add = !info->hasLabel(); if (add) { beginInsertRows(idx, 0, 0); @@ -1220,7 +1226,7 @@ bool FolderStatusModel::SubFolderInfo::hasLabel() const void FolderStatusModel::SubFolderInfo::resetSubs(FolderStatusModel *model, QModelIndex index) { _fetched = false; - _fetching = false; + delete _fetchingJob; if (hasLabel()) { model->beginRemoveRows(index, 0, 0); _fetchingLabel = false; diff --git a/src/gui/folderstatusmodel.h b/src/gui/folderstatusmodel.h index 7cdf49c7f..dc451f1e5 100644 --- a/src/gui/folderstatusmodel.h +++ b/src/gui/folderstatusmodel.h @@ -20,6 +20,7 @@ #include <QLoggingCategory> #include <QVector> #include <QElapsedTimer> +#include <QPointer> class QNetworkReply; namespace OCC { @@ -28,6 +29,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcFolderStatus) class Folder; class ProgressInfo; +class LsColJob; /** * @brief The FolderStatusModel class @@ -59,7 +61,6 @@ public: , _size(0) , _isExternal(false) , _fetched(false) - , _fetching(false) , _hasError(false) , _fetchingLabel(false) , _isUndecided(false) @@ -75,7 +76,7 @@ public: bool _isExternal; bool _fetched; // If we did the LSCOL for this folder already - bool _fetching; // Whether a LSCOL job is currently running + QPointer<LsColJob> _fetchingJob; // Currently running LsColJob bool _hasError; // If the last fetching job ended in an error QString _lastErrorString; bool _fetchingLabel; // Whether a 'fetching in progress' label is shown. diff --git a/src/gui/generalsettings.cpp b/src/gui/generalsettings.cpp index 69baaa84b..e224fca47 100644 --- a/src/gui/generalsettings.cpp +++ b/src/gui/generalsettings.cpp @@ -55,7 +55,7 @@ GeneralSettings::GeneralSettings(QWidget *parent) _ui->aboutGroupBox->hide(); } else { _ui->aboutLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - _ui->aboutLabel->setText(about); + _ui->aboutLabel->setText("<qt><style> p{padding-bottom:0; margin-bottom:0;padding-top:2; margin-top: 2; font-size:smaller;};</style>"+about+"</qt>"); _ui->aboutLabel->setWordWrap(true); _ui->aboutLabel->setOpenExternalLinks(true); } diff --git a/src/gui/logbrowser.cpp b/src/gui/logbrowser.cpp index a7303b0f0..7abfeb22d 100644 --- a/src/gui/logbrowser.cpp +++ b/src/gui/logbrowser.cpp @@ -96,6 +96,20 @@ LogBrowser::LogBrowser(QWidget *parent) mainLayout->addWidget(btnbox); + // button to permanently save logs + _permanentLogging = new QCheckBox; + _permanentLogging->setText(tr("Permanently save logs")); + _permanentLogging->setToolTip( + tr("When this option is enabled and no other logging is configured, " + "logs will be written to a temporary folder and expire after a few hours. " + "This setting persists across client restarts.\n" + "\n" + "Logs will be written to %1") + .arg(Logger::instance()->temporaryFolderLogDirPath())); + _permanentLogging->setChecked(ConfigFile().automaticLogDir()); + btnbox->addButton(_permanentLogging, QDialogButtonBox::ActionRole); + connect(_permanentLogging, &QCheckBox::toggled, this, &LogBrowser::togglePermanentLogging); + // clear button _clearBtn = new QPushButton; _clearBtn->setText(tr("Clear")); @@ -216,4 +230,18 @@ void LogBrowser::slotClearLog() _logWidget->clear(); } +void LogBrowser::togglePermanentLogging(bool enabled) +{ + ConfigFile().setAutomaticLogDir(enabled); + + auto logger = Logger::instance(); + if (enabled) { + if (!logger->isLoggingToFile()) { + logger->setupTemporaryFolderLogDir(); + } + } else { + logger->disableTemporaryFolderLogDir(); + } +} + } // namespace diff --git a/src/gui/logbrowser.h b/src/gui/logbrowser.h index a1c5c6da0..b7e2d58ac 100644 --- a/src/gui/logbrowser.h +++ b/src/gui/logbrowser.h @@ -66,11 +66,13 @@ protected slots: void search(const QString &); void slotSave(); void slotClearLog(); + void togglePermanentLogging(bool enabled); private: LogWidget *_logWidget; QLineEdit *_findTermEdit; QCheckBox *_logDebugCheckBox; + QCheckBox *_permanentLogging; QPushButton *_saveBtn; QPushButton *_clearBtn; QLabel *_statusLabel; diff --git a/src/gui/notificationwidget.cpp b/src/gui/notificationwidget.cpp index cd879451e..04573f74a 100644 --- a/src/gui/notificationwidget.cpp +++ b/src/gui/notificationwidget.cpp @@ -44,7 +44,16 @@ void NotificationWidget::setActivity(const Activity &activity) _ui._subjectLabel->setVisible(!activity._subject.isEmpty()); _ui._messageLabel->setVisible(!activity._message.isEmpty()); - _ui._subjectLabel->setText(activity._subject); + if (activity._link.isEmpty()) { + _ui._subjectLabel->setText(activity._subject); + } else { + _ui._subjectLabel->setText( QString("<a href=\"%1\">%2</a>") + .arg(activity._link.toString(QUrl::FullyEncoded), + activity._subject.toHtmlEscaped() )); + _ui._subjectLabel->setTextFormat(Qt::RichText); + _ui._subjectLabel->setOpenExternalLinks(true); + } + _ui._messageLabel->setText(activity._message); _ui._notifIcon->setPixmap(QPixmap(":/client/resources/bell.png")); diff --git a/src/gui/ocsshareejob.cpp b/src/gui/ocsshareejob.cpp index 40bb70d91..0d1b6d44b 100644 --- a/src/gui/ocsshareejob.cpp +++ b/src/gui/ocsshareejob.cpp @@ -14,6 +14,8 @@ #include "ocsshareejob.h" +#include <QJsonDocument> + namespace OCC { OcsShareeJob::OcsShareeJob(AccountPtr account) diff --git a/src/gui/ocssharejob.h b/src/gui/ocssharejob.h index 09c69410d..5760085fc 100644 --- a/src/gui/ocssharejob.h +++ b/src/gui/ocssharejob.h @@ -21,6 +21,8 @@ #include <QList> #include <QPair> +class QJsonDocument; + namespace OCC { /** diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 16c82f19a..e6e520401 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -165,7 +165,10 @@ void ownCloudGui::slotSyncStateChange(Folder *folder) qCInfo(lcApplication) << "Sync state changed for folder " << folder->remoteUrl().toString() << ": " << result.statusString(); - if (result.status() == SyncResult::Success || result.status() == SyncResult::Error) { + if (result.status() == SyncResult::Success + || result.status() == SyncResult::Problem + || result.status() == SyncResult::SyncAbortRequested + || result.status() == SyncResult::Error) { Logger::instance()->enterNextLogFile(); } @@ -1029,7 +1032,7 @@ void ownCloudGui::raiseDialog(QWidget *raiseWidget) } -void ownCloudGui::slotShowShareDialog(const QString &sharePath, const QString &localPath) +void ownCloudGui::slotShowShareDialog(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage) { QString file; const auto folder = FolderMan::instance()->folderForPath(localPath, &file); @@ -1073,7 +1076,7 @@ void ownCloudGui::slotShowShareDialog(const QString &sharePath, const QString &l w = _shareDialogs[localPath]; } else { qCInfo(lcApplication) << "Opening share dialog" << sharePath << localPath << maxSharingPermissions; - w = new ShareDialog(accountState, sharePath, localPath, maxSharingPermissions, fileRecord.numericFileId()); + w = new ShareDialog(accountState, sharePath, localPath, maxSharingPermissions, fileRecord.numericFileId(), startPage); w->setAttribute(Qt::WA_DeleteOnClose, true); _shareDialogs[localPath] = w; diff --git a/src/gui/owncloudgui.h b/src/gui/owncloudgui.h index d1b60ae0e..9bc136472 100644 --- a/src/gui/owncloudgui.h +++ b/src/gui/owncloudgui.h @@ -37,6 +37,11 @@ class Application; class LogBrowser; class AccountState; +enum class ShareDialogStartPage { + UsersAndGroups, + PublicLinks, +}; + /** * @brief The ownCloudGui class * @ingroup gui @@ -93,7 +98,7 @@ public slots: * localPath is the absolute local path to it (so not relative * to the folder). */ - void slotShowShareDialog(const QString &sharePath, const QString &localPath); + void slotShowShareDialog(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage); void slotRemoveDestroyedShareDialogs(); diff --git a/src/gui/sharedialog.cpp b/src/gui/sharedialog.cpp index 5c070a125..7b4610500 100644 --- a/src/gui/sharedialog.cpp +++ b/src/gui/sharedialog.cpp @@ -39,6 +39,7 @@ ShareDialog::ShareDialog(QPointer<AccountState> accountState, const QString &localPath, SharePermissions maxSharingPermissions, const QByteArray &numericFileId, + ShareDialogStartPage startPage, QWidget *parent) : QDialog(parent) , _ui(new Ui::ShareDialog) @@ -47,6 +48,7 @@ ShareDialog::ShareDialog(QPointer<AccountState> accountState, , _localPath(localPath) , _maxSharingPermissions(maxSharingPermissions) , _privateLinkUrl(accountState->account()->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded)) + , _startPage(startPage) , _linkWidget(NULL) , _userGroupWidget(NULL) , _progressIndicator(NULL) @@ -217,6 +219,9 @@ void ShareDialog::showSharingUi() _linkWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); _ui->shareWidgets->addTab(_linkWidget, tr("Public Links")); _linkWidget->getShares(); + + if (_startPage == ShareDialogStartPage::PublicLinks) + _ui->shareWidgets->setCurrentWidget(_linkWidget); } } diff --git a/src/gui/sharedialog.h b/src/gui/sharedialog.h index 8af3f3fff..7aa93cfde 100644 --- a/src/gui/sharedialog.h +++ b/src/gui/sharedialog.h @@ -17,6 +17,7 @@ #include "accountstate.h" #include "sharepermissions.h" +#include "owncloudgui.h" #include <QPointer> #include <QString> @@ -44,6 +45,7 @@ public: const QString &localPath, SharePermissions maxSharingPermissions, const QByteArray &numericFileId, + ShareDialogStartPage startPage, QWidget *parent = 0); ~ShareDialog(); @@ -64,6 +66,7 @@ private: SharePermissions _maxSharingPermissions; QByteArray _numericFileId; QString _privateLinkUrl; + ShareDialogStartPage _startPage; ShareLinkWidget *_linkWidget; ShareUserGroupWidget *_userGroupWidget; diff --git a/src/gui/sharelinkwidget.ui b/src/gui/sharelinkwidget.ui index ef321bfaf..5c4b2e648 100644 --- a/src/gui/sharelinkwidget.ui +++ b/src/gui/sharelinkwidget.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>394</width> - <height>534</height> + <width>441</width> + <height>568</height> </rect> </property> <property name="windowTitle"> @@ -94,7 +94,7 @@ <property name="bottomMargin"> <number>0</number> </property> - <item row="7" column="0"> + <item row="6" column="0"> <layout class="QHBoxLayout" name="horizontalLayout_expire"> <property name="leftMargin"> <number>0</number> @@ -115,7 +115,7 @@ </item> </layout> </item> - <item row="5" column="0"> + <item row="4" column="0"> <layout class="QHBoxLayout" name="horizontalLayout"> <property name="leftMargin"> <number>20</number> @@ -142,7 +142,7 @@ </item> </layout> </item> - <item row="4" column="0"> + <item row="3" column="0"> <layout class="QHBoxLayout" name="horizontalLayout_password"> <item> <widget class="QCheckBox" name="checkBox_password"> @@ -173,13 +173,6 @@ </layout> </item> <item row="2" column="0"> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Link properties:</string> - </property> - </widget> - </item> - <item row="3" column="0"> <widget class="QWidget" name="widget_editing" native="true"> <layout class="QGridLayout" name="layout_editing"> <property name="leftMargin"> @@ -203,7 +196,7 @@ </sizepolicy> </property> <property name="text"> - <string>Users can view and download contents.</string> + <string>Recipients can view or download contents.</string> </property> <property name="wordWrap"> <bool>true</bool> @@ -252,14 +245,14 @@ <item row="0" column="0"> <widget class="QRadioButton" name="radio_readOnly"> <property name="text"> - <string>Read only</string> + <string>Download / View</string> </property> </widget> </item> <item row="3" column="0"> <widget class="QRadioButton" name="radio_readWrite"> <property name="text"> - <string>Read && Write</string> + <string>Download / View / Upload</string> </property> </widget> </item> @@ -272,7 +265,7 @@ </sizepolicy> </property> <property name="text"> - <string>Users can view, download, edit and upload contents.</string> + <string>Recipients can view, download, edit, delete and upload contents.</string> </property> <property name="wordWrap"> <bool>true</bool> diff --git a/src/gui/socketapi.cpp b/src/gui/socketapi.cpp index 1a663ad05..585e4e833 100644 --- a/src/gui/socketapi.cpp +++ b/src/gui/socketapi.cpp @@ -32,6 +32,9 @@ #include "capabilities.h" #include "common/asserts.h" #include "guiutility.h" +#ifndef OWNCLOUD_TEST +#include "sharemanager.h" +#endif #include <array> #include <QBitArray> @@ -45,6 +48,7 @@ #include <QApplication> #include <QLocalSocket> #include <QStringBuilder> +#include <QMessageBox> #include <QClipboard> @@ -82,6 +86,7 @@ static QString buildMessage(const QString &verb, const QString &path, const QStr namespace OCC { Q_LOGGING_CATEGORY(lcSocketApi, "gui.socketapi", QtInfoMsg) +Q_LOGGING_CATEGORY(lcPublicLink, "gui.socketapi.publiclink", QtInfoMsg) class BloomFilter { @@ -181,6 +186,10 @@ SocketApi::SocketApi(QObject *parent) // Example for developer builds (with ad-hoc signing identity): "" "com.owncloud.desktopclient" ".socketApi" // Example for official signed packages: "9B5WD74GWJ." "com.owncloud.desktopclient" ".socketApi" socketPath = SOCKETAPI_TEAM_IDENTIFIER_PREFIX APPLICATION_REV_DOMAIN ".socketApi"; +#ifdef Q_OS_MAC + // Tell Finder to use the Extension (checking it from System Preferences -> Extensions) + system("pluginkit -e use -i " APPLICATION_REV_DOMAIN ".FinderSyncExt &"); +#endif } else if (Utility::isLinux() || Utility::isBSD()) { QString runtimeDir; runtimeDir = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); @@ -218,10 +227,17 @@ SocketApi::~SocketApi() // All remaining sockets will be destroyed with _localServer, their parent ASSERT(_listeners.isEmpty() || _listeners.first().socket->parent() == &_localServer); _listeners.clear(); + +#ifdef Q_OS_MAC + // Unload the extension (uncheck from System Preferences -> Extensions) + system("pluginkit -e ignore -i " APPLICATION_REV_DOMAIN ".FinderSyncExt &"); +#endif } void SocketApi::slotNewConnection() { + // Note that on macOS this is not actually a line-based QIODevice, it's a SocketApiSocket which is our + // custom message based macOS IPC. QIODevice *socket = _localServer.nextPendingConnection(); if (!socket) { @@ -341,6 +357,49 @@ void SocketApi::broadcastMessage(const QString &msg, bool doWait) } } +void SocketApi::processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage) +{ + auto theme = Theme::instance(); + + auto fileData = FileData::get(localFile); + auto shareFolder = fileData.folder; + if (!shareFolder) { + const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile); + // files that are not within a sync folder are not synced. + listener->sendMessage(message); + } else if (!shareFolder->accountState()->isConnected()) { + const QString message = QLatin1String("SHARE:NOTCONNECTED:") + QDir::toNativeSeparators(localFile); + // if the folder isn't connected, don't open the share dialog + listener->sendMessage(message); + } else if (!theme->linkSharing() && (!theme->userGroupSharing() || shareFolder->accountState()->account()->serverVersionInt() < Account::makeServerVersion(8, 2, 0))) { + const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile); + listener->sendMessage(message); + } else { + SyncFileStatus fileStatus = fileData.syncFileStatus(); + + // Verify the file is on the server (to our knowledge of course) + if (fileStatus.tag() != SyncFileStatus::StatusUpToDate) { + const QString message = QLatin1String("SHARE:NOTSYNCED:") + QDir::toNativeSeparators(localFile); + listener->sendMessage(message); + return; + } + + auto &remotePath = fileData.accountRelativePath; + + // Can't share root folder + if (remotePath == "/") { + const QString message = QLatin1String("SHARE:CANNOTSHAREROOT:") + QDir::toNativeSeparators(localFile); + listener->sendMessage(message); + return; + } + + const QString message = QLatin1String("SHARE:OK:") + QDir::toNativeSeparators(localFile); + listener->sendMessage(message); + + emit shareCommandReceived(remotePath, fileData.localPath, startPage); + } +} + void SocketApi::broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus) { QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString()); @@ -361,23 +420,17 @@ void SocketApi::command_RETRIEVE_FILE_STATUS(const QString &argument, SocketList { QString statusString; - QString relativePath; - Folder *syncFolder = FolderMan::instance()->folderForPath(argument, &relativePath); - if (!syncFolder) { + auto fileData = FileData::get(argument); + if (!fileData.folder) { // this can happen in offline mode e.g.: nothing to worry about statusString = QLatin1String("NOP"); } else { - QString systemPath = QDir::cleanPath(argument); - if (systemPath.endsWith(QLatin1Char('/'))) { - systemPath.truncate(systemPath.length() - 1); - qCWarning(lcSocketApi) << "Removed trailing slash for directory: " << systemPath << "Status pushes won't have one."; - } // The user probably visited this directory in the file shell. // Let the listener know that it should now send status pushes for sibblings of this file. - QString directory = systemPath.left(systemPath.lastIndexOf('/')); + QString directory = fileData.localPath.left(fileData.localPath.lastIndexOf('/')); listener->registerMonitoredDirectory(qHash(directory)); - SyncFileStatus fileStatus = syncFolder->syncEngine().syncFileStatusTracker().fileStatus(relativePath); + SyncFileStatus fileStatus = fileData.syncFileStatus(); statusString = fileStatus.toSocketAPIString(); } @@ -387,142 +440,220 @@ void SocketApi::command_RETRIEVE_FILE_STATUS(const QString &argument, SocketList void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener) { - auto theme = Theme::instance(); + processShareRequest(localFile, listener, ShareDialogStartPage::UsersAndGroups); +} - QString file; - Folder *shareFolder = FolderMan::instance()->folderForPath(localFile, &file); - if (!shareFolder) { - const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile); - // files that are not within a sync folder are not synced. +void SocketApi::command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener) +{ + processShareRequest(localFile, listener, ShareDialogStartPage::PublicLinks); +} + +void SocketApi::command_VERSION(const QString &, SocketListener *listener) +{ + listener->sendMessage(QLatin1String("VERSION:" MIRALL_VERSION_STRING ":" MIRALL_SOCKET_API_VERSION)); +} + +void SocketApi::command_SHARE_STATUS(const QString &localFile, SocketListener *listener) +{ + auto fileData = FileData::get(localFile); + if (!fileData.folder) { + const QString message = QLatin1String("SHARE_STATUS:NOP:") + QDir::toNativeSeparators(localFile); listener->sendMessage(message); - } else if (!shareFolder->accountState()->isConnected()) { - const QString message = QLatin1String("SHARE:NOTCONNECTED:") + QDir::toNativeSeparators(localFile); - // if the folder isn't connected, don't open the share dialog + return; + } + + SyncFileStatus fileStatus = fileData.syncFileStatus(); + + // Verify the file is on the server (to our knowledge of course) + if (fileStatus.tag() != SyncFileStatus::StatusUpToDate) { + const QString message = QLatin1String("SHARE_STATUS:NOTSYNCED:") + QDir::toNativeSeparators(localFile); listener->sendMessage(message); - } else if (!theme->linkSharing() && (!theme->userGroupSharing() || shareFolder->accountState()->account()->serverVersionInt() < Account::makeServerVersion(8, 2, 0))) { - const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile); + return; + } + + const Capabilities capabilities = fileData.folder->accountState()->account()->capabilities(); + + if (!capabilities.shareAPI()) { + const QString message = QLatin1String("SHARE_STATUS:DISABLED:") + QDir::toNativeSeparators(localFile); listener->sendMessage(message); } else { - const QString localFileClean = QDir::cleanPath(localFile); - SyncFileStatus fileStatus = shareFolder->syncEngine().syncFileStatusTracker().fileStatus(file); + auto theme = Theme::instance(); + QString available; - // Verify the file is on the server (to our knowledge of course) - if (fileStatus.tag() != SyncFileStatus::StatusUpToDate) { - const QString message = QLatin1String("SHARE:NOTSYNCED:") + QDir::toNativeSeparators(localFile); - listener->sendMessage(message); - return; + if (theme->userGroupSharing()) { + available = "USER,GROUP"; } - const QString remotePath = QDir(shareFolder->remotePath()).filePath(file); + if (theme->linkSharing() && capabilities.sharePublicLink()) { + if (available.isEmpty()) { + available = "LINK"; + } else { + available += ",LINK"; + } + } - // Can't share root folder - if (remotePath == "/") { - const QString message = QLatin1String("SHARE:CANNOTSHAREROOT:") + QDir::toNativeSeparators(localFile); + if (available.isEmpty()) { + const QString message = QLatin1String("SHARE_STATUS:DISABLED") + ":" + QDir::toNativeSeparators(localFile); + listener->sendMessage(message); + } else { + const QString message = QLatin1String("SHARE_STATUS:") + available + ":" + QDir::toNativeSeparators(localFile); listener->sendMessage(message); - return; } - - const QString message = QLatin1String("SHARE:OK:") + QDir::toNativeSeparators(localFile); - listener->sendMessage(message); - - emit shareCommandReceived(remotePath, localFileClean); } } -void SocketApi::command_VERSION(const QString &, SocketListener *listener) +void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketListener *listener) { - listener->sendMessage(QLatin1String("VERSION:" MIRALL_VERSION_STRING ":" MIRALL_SOCKET_API_VERSION)); + listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + tr("Share with %1", "parameter is ownCloud").arg(Theme::instance()->appNameGUI())); } -void SocketApi::command_SHARE_STATUS(const QString &localFile, SocketListener *listener) +// don't pull the share manager into socketapi unittests +#ifndef OWNCLOUD_TEST + +class GetOrCreatePublicLinkShare : public QObject { - QString file; - Folder *shareFolder = FolderMan::instance()->folderForPath(localFile, &file); + Q_OBJECT +public: + GetOrCreatePublicLinkShare(const AccountPtr &account, const QString &localFile, + std::function<void(const QString &link)> targetFun, QObject *parent) + : QObject(parent) + , _shareManager(account) + , _localFile(localFile) + , _targetFun(targetFun) + { + connect(&_shareManager, &ShareManager::sharesFetched, + this, &GetOrCreatePublicLinkShare::sharesFetched); + connect(&_shareManager, &ShareManager::linkShareCreated, + this, &GetOrCreatePublicLinkShare::linkShareCreated); + connect(&_shareManager, &ShareManager::serverError, + this, &GetOrCreatePublicLinkShare::serverError); + } - if (!shareFolder) { - const QString message = QLatin1String("SHARE_STATUS:NOP:") + QDir::toNativeSeparators(localFile); - listener->sendMessage(message); - } else { - SyncFileStatus fileStatus = shareFolder->syncEngine().syncFileStatusTracker().fileStatus(file); + void run() + { + qCDebug(lcPublicLink) << "Fetching shares"; + _shareManager.fetchShares(_localFile); + } - // Verify the file is on the server (to our knowledge of course) - if (fileStatus.tag() != SyncFileStatus::StatusUpToDate) { - const QString message = QLatin1String("SHARE_STATUS:NOTSYNCED:") + QDir::toNativeSeparators(localFile); - listener->sendMessage(message); - return; +private slots: + void sharesFetched(const QList<QSharedPointer<Share>> &shares) + { + auto shareName = SocketApi::tr("Context menu share"); + // If there already is a context menu share, reuse it + for (const auto &share : shares) { + const auto linkShare = qSharedPointerDynamicCast<LinkShare>(share); + if (!linkShare) + continue; + + if (linkShare->getName() == shareName) { + qCDebug(lcPublicLink) << "Found existing share, reusing"; + return success(linkShare->getLink().toString()); + } } - const Capabilities capabilities = shareFolder->accountState()->account()->capabilities(); + // otherwise create a new one + qCDebug(lcPublicLink) << "Creating new share"; + _shareManager.createLinkShare(_localFile, shareName, QString()); + } - if (!capabilities.shareAPI()) { - const QString message = QLatin1String("SHARE_STATUS:DISABLED:") + QDir::toNativeSeparators(localFile); - listener->sendMessage(message); - } else { - auto theme = Theme::instance(); - QString available; + void linkShareCreated(const QSharedPointer<LinkShare> &share) + { + qCDebug(lcPublicLink) << "New share created"; + success(share->getLink().toString()); + } - if (theme->userGroupSharing()) { - available = "USER,GROUP"; - } + void serverError(int code, const QString &message) + { + qCWarning(lcPublicLink) << "Share fetch/create error" << code << message; + QMessageBox::warning( + 0, + tr("Sharing error"), + tr("Could not retrieve or create the public link share. Error:\n\n%1").arg(message), + QMessageBox::Ok, + QMessageBox::NoButton); + deleteLater(); + } - if (theme->linkSharing() && capabilities.sharePublicLink()) { - if (available.isEmpty()) { - available = "LINK"; - } else { - available += ",LINK"; - } - } +private: + void success(const QString &link) + { + _targetFun(link); + deleteLater(); + } - if (available.isEmpty()) { - const QString message = QLatin1String("SHARE_STATUS:DISABLED") + ":" + QDir::toNativeSeparators(localFile); - listener->sendMessage(message); - } else { - const QString message = QLatin1String("SHARE_STATUS:") + available + ":" + QDir::toNativeSeparators(localFile); - listener->sendMessage(message); - } - } + ShareManager _shareManager; + QString _localFile; + std::function<void(const QString &url)> _targetFun; +}; + +#else + +class GetOrCreatePublicLinkShare : public QObject +{ + Q_OBJECT +public: + GetOrCreatePublicLinkShare(const AccountPtr &, const QString &, + std::function<void(const QString &link)>, QObject *) + { } -} -void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketListener *listener) + void run() + { + } +}; + +#endif + +void SocketApi::command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *) { - listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + tr("Share with %1", "parameter is ownCloud").arg(Theme::instance()->appNameGUI())); + auto fileData = FileData::get(localFile); + if (!fileData.folder) + return; + + AccountPtr account = fileData.folder->accountState()->account(); + auto job = new GetOrCreatePublicLinkShare(account, fileData.accountRelativePath, [](const QString &url) { copyUrlToClipboard(url); }, this); + job->run(); } // Fetches the private link url asynchronously and then calls the target slot -static void fetchPrivateLinkUrlHelper(const QString &localFile, SocketApi *target, void (SocketApi::*targetFun)(const QString &url) const) +void SocketApi::fetchPrivateLinkUrlHelper(const QString &localFile, const std::function<void(const QString &url)> &targetFun) { - QString file; - Folder *shareFolder = FolderMan::instance()->folderForPath(localFile, &file); - if (!shareFolder) { + auto fileData = FileData::get(localFile); + if (!fileData.folder) { qCWarning(lcSocketApi) << "Unknown path" << localFile; return; } - AccountPtr account = shareFolder->accountState()->account(); - - SyncJournalFileRecord rec; - if (!shareFolder->journalDb()->getFileRecord(file, &rec) || !rec.isValid()) + auto record = fileData.journalRecord(); + if (!record.isValid()) return; - fetchPrivateLinkUrl(account, file, rec.numericFileId(), target, [=](const QString &url) { - (target->*targetFun)(url); - }); + fetchPrivateLinkUrl( + fileData.folder->accountState()->account(), + fileData.accountRelativePath, + record.numericFileId(), + this, + targetFun); } void SocketApi::command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *) { - fetchPrivateLinkUrlHelper(localFile, this, &SocketApi::copyPrivateLinkToClipboard); + fetchPrivateLinkUrlHelper(localFile, &SocketApi::copyUrlToClipboard); } void SocketApi::command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *) { - fetchPrivateLinkUrlHelper(localFile, this, &SocketApi::emailPrivateLink); + fetchPrivateLinkUrlHelper(localFile, &SocketApi::emailPrivateLink); } void SocketApi::command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *) { - fetchPrivateLinkUrlHelper(localFile, this, &SocketApi::openPrivateLink); + fetchPrivateLinkUrlHelper(localFile, &SocketApi::openPrivateLink); +} + +void SocketApi::copyUrlToClipboard(const QString &link) +{ + QApplication::clipboard()->setText(link); } void SocketApi::command_DOWNLOAD_PLACEHOLDER(const QString &filesArg, SocketListener *) @@ -541,12 +672,7 @@ void SocketApi::command_DOWNLOAD_PLACEHOLDER(const QString &filesArg, SocketList } } -void SocketApi::copyPrivateLinkToClipboard(const QString &link) const -{ - QApplication::clipboard()->setText(link); -} - -void SocketApi::emailPrivateLink(const QString &link) const +void SocketApi::emailPrivateLink(const QString &link) { Utility::openEmailComposer( tr("I shared something with you"), @@ -554,7 +680,7 @@ void SocketApi::emailPrivateLink(const QString &link) const 0); } -void OCC::SocketApi::openPrivateLink(const QString &link) const +void OCC::SocketApi::openPrivateLink(const QString &link) { Utility::openBrowser(link, nullptr); } @@ -576,6 +702,79 @@ void SocketApi::command_GET_STRINGS(const QString &argument, SocketListener *lis listener->sendMessage(QString("GET_STRINGS:END")); } +void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener) +{ + auto record = fileData.journalRecord(); + bool isOnTheServer = record.isValid(); + auto flagString = isOnTheServer ? QLatin1String("::") : QLatin1String(":d:"); + + auto capabilities = fileData.folder->accountState()->account()->capabilities(); + auto theme = Theme::instance(); + if (!capabilities.shareAPI() || !(theme->userGroupSharing() || (theme->linkSharing() && capabilities.sharePublicLink()))) + return; + + // If sharing is globally disabled, do not show any sharing entries. + // If there is no permission to share for this file, add a disabled entry saying so + if (isOnTheServer && !record._remotePerm.isNull() && !record._remotePerm.hasPermission(RemotePermissions::CanReshare)) { + listener->sendMessage(QLatin1String("MENU_ITEM:DISABLED:d:") + tr("Resharing this file is not allowed")); + } else { + listener->sendMessage(QLatin1String("MENU_ITEM:SHARE") + flagString + tr("Share...")); + + // Do we have public links? + bool publicLinksEnabled = theme->linkSharing() && capabilities.sharePublicLink(); + + // Is is possible to create a public link without user choices? + bool canCreateDefaultPublicLink = publicLinksEnabled + && !capabilities.sharePublicLinkEnforceExpireDate() + && !capabilities.sharePublicLinkEnforcePassword(); + + if (canCreateDefaultPublicLink) { + listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PUBLIC_LINK") + flagString + tr("Copy public link to clipboard")); + } else if (publicLinksEnabled) { + listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + flagString + tr("Copy public link to clipboard")); + } + } + + listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PRIVATE_LINK") + flagString + tr("Copy private link to clipboard")); + + // Disabled: only providing email option for private links would look odd, + // and the copy option is more general. + //listener->sendMessage(QLatin1String("MENU_ITEM:EMAIL_PRIVATE_LINK") + flagString + tr("Send private link by email...")); +} + +SocketApi::FileData SocketApi::FileData::get(const QString &localFile) +{ + FileData data; + + data.localPath = QDir::cleanPath(localFile); + if (data.localPath.endsWith(QLatin1Char('/'))) + data.localPath.chop(1); + + data.folder = FolderMan::instance()->folderForPath(data.localPath, &data.folderRelativePath); + if (!data.folder) + return data; + + data.accountRelativePath = QDir(data.folder->remotePath()).filePath(data.folderRelativePath); + + return data; +} + +SyncFileStatus SocketApi::FileData::syncFileStatus() const +{ + if (!folder) + return SyncFileStatus::StatusNone; + return folder->syncEngine().syncFileStatusTracker().fileStatus(folderRelativePath); +} + +SyncJournalFileRecord SocketApi::FileData::journalRecord() const +{ + SyncJournalFileRecord record; + if (!folder) + return record; + folder->journalDb()->getFileRecord(folderRelativePath, &record); + return record; +} + void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListener *listener) { listener->sendMessage(QString("GET_MENU_ITEMS:BEGIN")); @@ -583,51 +782,33 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe // Find the common sync folder. // syncFolder will be null if files are in different folders. - Folder *syncFolder = nullptr; + Folder *folder = nullptr; for (const auto &file : files) { - auto folder = FolderMan::instance()->folderForPath(file); - if (folder != syncFolder) { - if (!syncFolder) { - syncFolder = folder; + auto f = FolderMan::instance()->folderForPath(file); + if (f != folder) { + if (!folder) { + folder = f; } else { - syncFolder = nullptr; + folder = nullptr; break; } } } - // Sharing actions show for single files only - if (syncFolder && files.size() == 1 && syncFolder->accountState()->isConnected()) { - QString systemPath = QDir::cleanPath(argument); - if (systemPath.endsWith(QLatin1Char('/'))) { - systemPath.truncate(systemPath.length() - 1); - } - - SyncJournalFileRecord rec; - QString relativePath = systemPath.mid(syncFolder->cleanPath().length() + 1); - // If the file is on the DB, it is on the server - bool isOnTheServer = syncFolder->journalDb()->getFileRecord(relativePath, &rec) && rec.isValid(); + // Some options only show for single files + if (files.size() == 1) { + FileData fileData = FileData::get(files.first()); + bool isOnTheServer = fileData.journalRecord().isValid(); auto flagString = isOnTheServer ? QLatin1String("::") : QLatin1String(":d:"); - auto capabilities = syncFolder->accountState()->account()->capabilities(); - auto theme = Theme::instance(); - if (capabilities.shareAPI() && (theme->userGroupSharing() || (theme->linkSharing() && capabilities.sharePublicLink()))) { - // If sharing is globally disabled, do not show any sharing entries. - // If there is no permission to share for this file, add a disabled entry saying so - if (isOnTheServer && !rec._remotePerm.isNull() && !rec._remotePerm.hasPermission(RemotePermissions::CanReshare)) { - listener->sendMessage(QLatin1String("MENU_ITEM:DISABLED:d:") + tr("Resharing this file is not allowed")); - } else { - listener->sendMessage(QLatin1String("MENU_ITEM:SHARE") + flagString + tr("Share...")); - } - listener->sendMessage(QLatin1String("MENU_ITEM:EMAIL_PRIVATE_LINK") + flagString + tr("Send private link by email...")); - listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PRIVATE_LINK") + flagString + tr("Copy private link to clipboard")); + if (fileData.folder && fileData.folder->accountState()->isConnected()) { + sendSharingContextMenuOptions(fileData, listener); + listener->sendMessage(QLatin1String("MENU_ITEM:OPEN_PRIVATE_LINK") + flagString + tr("Open in browser")); } - - listener->sendMessage(QLatin1String("MENU_ITEM:OPEN_PRIVATE_LINK") + flagString + tr("Open in browser")); } // Placeholder download action - if (syncFolder) { + if (folder) { auto placeholderSuffix = QStringLiteral(APPLICATION_DOTPLACEHOLDER_SUFFIX); bool hasPlaceholderFile = false; for (const auto &file : files) { @@ -650,3 +831,5 @@ QString SocketApi::buildRegisterPathMessage(const QString &path) } } // namespace OCC + +#include "socketapi.moc" diff --git a/src/gui/socketapi.h b/src/gui/socketapi.h index e3f395ff3..20ab82672 100644 --- a/src/gui/socketapi.h +++ b/src/gui/socketapi.h @@ -18,7 +18,8 @@ #include "syncfileitem.h" #include "syncfilestatus.h" -// #include "ownsql.h" +#include "sharedialog.h" // for the ShareDialogStartPage +#include "common/syncjournalfilerecord.h" #if defined(Q_OS_MAC) #include "socketapisocket_mac.h" @@ -56,7 +57,7 @@ public slots: void broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus); signals: - void shareCommandReceived(const QString &sharePath, const QString &localPath); + void shareCommandReceived(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage); private slots: void slotNewConnection(); @@ -64,13 +65,31 @@ private slots: void slotSocketDestroyed(QObject *obj); void slotReadSocket(); - void copyPrivateLinkToClipboard(const QString &link) const; - void emailPrivateLink(const QString &link) const; - void openPrivateLink(const QString &link) const; + static void copyUrlToClipboard(const QString &link); + static void emailPrivateLink(const QString &link); + static void openPrivateLink(const QString &link); private: + // Helper structure for getting information on a file + // based on its local path - used for nearly all remote + // actions. + struct FileData + { + static FileData get(const QString &localFile); + SyncFileStatus syncFileStatus() const; + SyncJournalFileRecord journalRecord() const; + + Folder *folder; + QString localPath; + QString folderRelativePath; + QString accountRelativePath; + }; + void broadcastMessage(const QString &msg, bool doWait = false); + // opens share dialog, sends reply + void processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage); + Q_INVOKABLE void command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener); Q_INVOKABLE void command_RETRIEVE_FILE_STATUS(const QString &argument, SocketListener *listener); @@ -81,14 +100,22 @@ private: // The context menu actions Q_INVOKABLE void command_SHARE(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *listener); Q_INVOKABLE void command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *listener); Q_INVOKABLE void command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *listener); Q_INVOKABLE void command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *listener); Q_INVOKABLE void command_DOWNLOAD_PLACEHOLDER(const QString &filesArg, SocketListener *listener); + // Fetch the private link and call targetFun + void fetchPrivateLinkUrlHelper(const QString &localFile, const std::function<void(const QString &url)> &targetFun); + /** Sends translated/branded strings that may be useful to the integration */ Q_INVOKABLE void command_GET_STRINGS(const QString &argument, SocketListener *listener); + // Sends the context menu options relating to sharing to listener + void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener); + /** Send the list of menu item. (added in version 1.1) * argument is a list of files for which the menu should be shown, separated by '\x1e' * Reply with GET_MENU_ITEMS:BEGIN diff --git a/src/gui/sslbutton.cpp b/src/gui/sslbutton.cpp index e387f885e..b41076ba4 100644 --- a/src/gui/sslbutton.cpp +++ b/src/gui/sslbutton.cpp @@ -58,7 +58,7 @@ static bool isSelfSigned(const QSslCertificate &certificate) } QMenu *SslButton::buildCertMenu(QMenu *parent, const QSslCertificate &cert, - const QList<QSslCertificate> &userApproved, int pos) + const QList<QSslCertificate> &userApproved, int pos, const QList<QSslCertificate> &systemCaCertificates) { QString cn = QStringList(cert.subjectInfo(QSslCertificate::CommonName)).join(QChar(';')); QString ou = QStringList(cert.subjectInfo(QSslCertificate::OrganizationalUnitName)).join(QChar(';')); @@ -129,7 +129,7 @@ QMenu *SslButton::buildCertMenu(QMenu *parent, const QSslCertificate &cert, QString certId = cn.isEmpty() ? ou : cn; - if (QSslConfiguration::systemCaCertificates().contains(cert)) { + if (systemCaCertificates.contains(cert)) { txt += certId; } else { if (isSelfSigned(cert)) { @@ -189,6 +189,10 @@ void SslButton::slotUpdateMenu() AccountPtr account = _accountState->account(); + if (account->isHttp2Supported()) { + _menu->addAction("HTTP/2")->setEnabled(false); + } + if (account->url().scheme() == QLatin1String("https")) { QString sslVersion = account->_sessionCipher.protocolString() + ", " + account->_sessionCipher.authenticationMethod() @@ -232,7 +236,7 @@ void SslButton::slotUpdateMenu() it.toBack(); int i = 0; while (it.hasPrevious()) { - _menu->addMenu(buildCertMenu(_menu, it.previous(), account->approvedCerts(), i)); + _menu->addMenu(buildCertMenu(_menu, it.previous(), account->approvedCerts(), i, systemCerts)); i++; } } diff --git a/src/gui/sslbutton.h b/src/gui/sslbutton.h index c48f7c3a7..906f4b846 100644 --- a/src/gui/sslbutton.h +++ b/src/gui/sslbutton.h @@ -43,7 +43,7 @@ public slots: private: QMenu *buildCertMenu(QMenu *parent, const QSslCertificate &cert, - const QList<QSslCertificate> &userApproved, int pos); + const QList<QSslCertificate> &userApproved, int pos, const QList<QSslCertificate> &systemCaCertificates); QPointer<AccountState> _accountState; QMenu *_menu; }; diff --git a/src/gui/updater/sparkleupdater_mac.mm b/src/gui/updater/sparkleupdater_mac.mm index a1e3e4f05..88c3541ff 100644 --- a/src/gui/updater/sparkleupdater_mac.mm +++ b/src/gui/updater/sparkleupdater_mac.mm @@ -30,6 +30,7 @@ // Only possible in later versions, we're not up to date here. - (BOOL)updaterMayCheckForUpdates:(SUUpdater *)bundle { + Q_UNUSED(bundle) qCDebug(OCC::lcUpdater) << "may check: YES"; return YES; } @@ -37,17 +38,22 @@ // Sent when a valid update is found by the update driver. - (void)updater:(SUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)update { + Q_UNUSED(updater) + Q_UNUSED(update) } // Sent when a valid update is not found. // Does not seem to get called ever. - (void)updaterDidNotFindUpdate:(SUUpdater *)update { + Q_UNUSED(update) } // Sent immediately before installing the specified update. - (void)updater:(SUUpdater *)updater willInstallUpdate:(SUAppcastItem *)update { + Q_UNUSED(updater) + Q_UNUSED(update) } // Tried implementing those methods, but they never ever seem to get called diff --git a/src/libsync/account.h b/src/libsync/account.h index 025dca907..a7879f4d0 100644 --- a/src/libsync/account.h +++ b/src/libsync/account.h @@ -218,7 +218,7 @@ public: /** Detects a specific bug in older server versions */ bool rootEtagChangesNotOnlySubFolderEtags(); - /** True when the server supports HTTP2 */ + /** True when the server connection is using HTTP2 */ bool isHttp2Supported() { return _http2Supported; } void setHttp2Supported(bool value) { _http2Supported = value; } diff --git a/src/libsync/configfile.cpp b/src/libsync/configfile.cpp index 82c2bc5f3..d0dadbf0f 100644 --- a/src/libsync/configfile.cpp +++ b/src/libsync/configfile.cpp @@ -64,6 +64,7 @@ static const char chunkSizeC[] = "chunkSize"; static const char minChunkSizeC[] = "minChunkSize"; static const char maxChunkSizeC[] = "maxChunkSize"; static const char targetChunkUploadDurationC[] = "targetChunkUploadDuration"; +static const char automaticLogDirC[] = "logToTemporaryLogDir"; static const char showExperimentalOptionsC[] = "showExperimentalOptions"; static const char proxyHostC[] = "Proxy/host"; @@ -737,6 +738,18 @@ void ConfigFile::setCrashReporter(bool enabled) settings.setValue(QLatin1String(crashReporterC), enabled); } +bool ConfigFile::automaticLogDir() const +{ + QSettings settings(configFile(), QSettings::IniFormat); + return settings.value(QLatin1String(automaticLogDirC), false).toBool(); +} + +void ConfigFile::setAutomaticLogDir(bool enabled) +{ + QSettings settings(configFile(), QSettings::IniFormat); + settings.setValue(QLatin1String(automaticLogDirC), enabled); +} + bool ConfigFile::showExperimentalOptions() const { QSettings settings(configFile(), QSettings::IniFormat); diff --git a/src/libsync/configfile.h b/src/libsync/configfile.h index 8877bb6dc..99aa6d5f1 100644 --- a/src/libsync/configfile.h +++ b/src/libsync/configfile.h @@ -89,6 +89,9 @@ public: bool crashReporter() const; void setCrashReporter(bool enabled); + bool automaticLogDir() const; + void setAutomaticLogDir(bool enabled); + // Whether experimental UI options should be shown bool showExperimentalOptions() const; diff --git a/src/libsync/logger.cpp b/src/libsync/logger.cpp index cfba000ed..7033bd851 100644 --- a/src/libsync/logger.cpp +++ b/src/libsync/logger.cpp @@ -14,11 +14,15 @@ #include "logger.h" +#include "config.h" + #include <QDir> #include <QStringList> #include <QThread> #include <qmetaobject.h> +#include <zlib.h> + namespace OCC { static void mirallLogCatcher(QtMsgType type, const QMessageLogContext &ctx, const QString &message) @@ -95,10 +99,15 @@ void Logger::log(Log log) */ bool Logger::isNoop() const { - QMutexLocker lock(const_cast<QMutex *>(&_mutex)); + QMutexLocker lock(&_mutex); return !_logstream && !_logWindowActivated; } +bool Logger::isLoggingToFile() const +{ + QMutexLocker lock(&_mutex); + return _logstream; +} void Logger::doLog(const QString &msg) { @@ -181,34 +190,99 @@ void Logger::setLogDebug(bool debug) _logDebug = debug; } +QString Logger::temporaryFolderLogDirPath() const +{ + QString dirName = APPLICATION_SHORTNAME + QString("-logdir"); + return QDir::temp().filePath(dirName); +} + +void Logger::setupTemporaryFolderLogDir() +{ + auto dir = temporaryFolderLogDirPath(); + if (!QDir().mkpath(dir)) + return; + setLogDebug(true); + setLogExpire(4 /*hours*/); + setLogDir(dir); + _temporaryFolderLogDir = true; +} + +void Logger::disableTemporaryFolderLogDir() +{ + if (!_temporaryFolderLogDir) + return; + + enterNextLogFile(); + setLogDir(QString()); + setLogDebug(false); + setLogFile(QString()); + _temporaryFolderLogDir = false; +} + +static bool compressLog(const QString &originalName, const QString &targetName) +{ + QFile original(originalName); + if (!original.open(QIODevice::ReadOnly)) + return false; + auto compressed = gzopen(targetName.toUtf8(), "wb"); + if (!compressed) { + return false; + } + + while (!original.atEnd()) { + auto data = original.read(1024 * 1024); + auto written = gzwrite(compressed, data.data(), data.size()); + if (written != data.size()) { + gzclose(compressed); + return false; + } + } + gzclose(compressed); + return true; +} + void Logger::enterNextLogFile() { if (!_logDirectory.isEmpty()) { + QDir dir(_logDirectory); if (!dir.exists()) { dir.mkpath("."); } - // Find out what is the file with the highest number if any - QStringList files = dir.entryList(QStringList("owncloud.log.*"), + // Tentative new log name, will be adjusted if one like this already exists + QDateTime now = QDateTime::currentDateTime(); + QString newLogName = now.toString("yyyyMMdd_HHmm") + "_owncloud.log"; + + // Expire old log files and deal with conflicts + QStringList files = dir.entryList(QStringList("*owncloud.log.*"), QDir::Files); - QRegExp rx("owncloud.log.(\\d+)"); - uint maxNumber = 0; - QDateTime now = QDateTime::currentDateTimeUtc(); + QRegExp rx(R"(.*owncloud\.log\.(\d+).*)"); + int maxNumber = -1; foreach (const QString &s, files) { - if (rx.exactMatch(s)) { - maxNumber = qMax(maxNumber, rx.cap(1).toUInt()); - if (_logExpire > 0) { - QFileInfo fileInfo = dir.absoluteFilePath(s); - if (fileInfo.lastModified().addSecs(60 * 60 * _logExpire) < now) { - dir.remove(s); - } + if (_logExpire > 0) { + QFileInfo fileInfo(dir.absoluteFilePath(s)); + if (fileInfo.lastModified().addSecs(60 * 60 * _logExpire) < now) { + dir.remove(s); } } + if (s.startsWith(newLogName) && rx.exactMatch(s)) { + maxNumber = qMax(maxNumber, rx.cap(1).toInt()); + } } + newLogName.append("." + QString::number(maxNumber + 1)); - QString filename = _logDirectory + "/owncloud.log." + QString::number(maxNumber + 1); - setLogFile(filename); + auto previousLog = _logFile.fileName(); + setLogFile(dir.filePath(newLogName)); + + if (!previousLog.isEmpty()) { + QString compressedName = previousLog + ".gz"; + if (compressLog(previousLog, compressedName)) { + QFile::remove(previousLog); + } else { + QFile::remove(compressedName); + } + } } } diff --git a/src/libsync/logger.h b/src/libsync/logger.h index 9d6ef6b26..7ae278e49 100644 --- a/src/libsync/logger.h +++ b/src/libsync/logger.h @@ -43,6 +43,8 @@ class OWNCLOUDSYNC_EXPORT Logger : public QObject Q_OBJECT public: bool isNoop() const; + bool isLoggingToFile() const; + void log(Log log); void doLog(const QString &log); @@ -65,6 +67,22 @@ public: bool logDebug() const { return _logDebug; } void setLogDebug(bool debug); + /** Returns where the automatic logdir would be */ + QString temporaryFolderLogDirPath() const; + + /** Sets up default dir log setup. + * + * logdir: a temporary folder + * logexpire: 4 hours + * logdebug: true + * + * Used in conjunction with ConfigFile::automaticLogDir + */ + void setupTemporaryFolderLogDir(); + + /** For switching off via logwindow */ + void disableTemporaryFolderLogDir(); + signals: void logWindowLog(const QString &); @@ -86,8 +104,9 @@ private: int _logExpire; bool _logDebug; QScopedPointer<QTextStream> _logstream; - QMutex _mutex; + mutable QMutex _mutex; QString _logDirectory; + bool _temporaryFolderLogDir = false; }; } // namespace OCC diff --git a/src/libsync/networkjobs.cpp b/src/libsync/networkjobs.cpp index a3881cd48..44fdc4553 100644 --- a/src/libsync/networkjobs.cpp +++ b/src/libsync/networkjobs.cpp @@ -879,6 +879,8 @@ void DetermineAuthTypeJob::start() get->setFollowRedirects(false); } #else + Q_UNUSED(this) + Q_UNUSED(get) Q_UNUSED(target) #endif }); diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index dafaec99c..3cd8ab0be 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -517,8 +517,7 @@ bool OwncloudPropagator::localFileNameClash(const QString &relFile) re = false; qCWarning(lcPropagator) << "No valid fileinfo"; } else { - // Need to normalize to composited form because of - // https://bugreports.qt-project.org/browse/QTBUG-39622 + // Need to normalize to composited form because of QTBUG-39622/QTBUG-55896 const QString cName = fileInfo.canonicalFilePath().normalized(QString::NormalizationForm_C); bool equal = (file == cName); re = (!equal && !cName.endsWith(relFile, Qt::CaseSensitive)); @@ -840,10 +839,13 @@ void PropagatorCompositeJob::slotSubJobFinished(SyncFileItem::Status status) ASSERT(i >= 0); _runningJobs.remove(i); + // Any sub job error will cause the whole composite to fail. This is important + // for knowing whether to update the etag in PropagateDirectory, for example. if (status == SyncFileItem::FatalError || status == SyncFileItem::NormalError || status == SyncFileItem::SoftError - || status == SyncFileItem::DetailError) { + || status == SyncFileItem::DetailError + || status == SyncFileItem::BlacklistedError) { _hasError = status; } diff --git a/src/libsync/owncloudpropagator.h b/src/libsync/owncloudpropagator.h index c272a06d2..b4bc470d2 100644 --- a/src/libsync/owncloudpropagator.h +++ b/src/libsync/owncloudpropagator.h @@ -156,7 +156,7 @@ class PropagateItemJob : public PropagatorJob { Q_OBJECT protected: - void done(SyncFileItem::Status status, const QString &errorString = QString()); + virtual void done(SyncFileItem::Status status, const QString &errorString = QString()); /* * set a custom restore job message that is used if the restore job succeeded. diff --git a/src/libsync/owncloudtheme.cpp b/src/libsync/owncloudtheme.cpp index 873257c19..1e142b962 100644 --- a/src/libsync/owncloudtheme.cpp +++ b/src/libsync/owncloudtheme.cpp @@ -19,8 +19,6 @@ #ifndef TOKEN_AUTH_ONLY #include <QPixmap> #include <QIcon> -#include <QStyle> -#include <QApplication> #endif #include <QCoreApplication> @@ -35,63 +33,13 @@ ownCloudTheme::ownCloudTheme() { } -QString ownCloudTheme::configFileName() const -{ - return QLatin1String("owncloud.cfg"); -} - -QString ownCloudTheme::about() const -{ - QString devString; - devString = tr("<p>Version %2. For more information visit <a href=\"%3\">https://%4</a></p>" - "<p>For known issues and help, please visit: <a href=\"https://central.owncloud.org/c/desktop-client\">https://central.owncloud.org</a></p>" - "<p><small>By Klaas Freitag, Daniel Molkentin, Olivier Goffart, Markus Götz, " - " Jan-Christoph Borchardt, and others.</small></p>" - "<p>Copyright ownCloud GmbH</p>" - "<p>Licensed under the GNU General Public License (GPL) Version 2.0<br/>" - "ownCloud and the ownCloud Logo are registered trademarks of ownCloud GmbH " - "in the United States, other countries, or both.</p>") - .arg(Utility::escape(MIRALL_VERSION_STRING), - Utility::escape("https://" MIRALL_STRINGIFY(APPLICATION_DOMAIN)), - Utility::escape(MIRALL_STRINGIFY(APPLICATION_DOMAIN))); - - devString += gitSHA1(); - return devString; -} - #ifndef TOKEN_AUTH_ONLY -QIcon ownCloudTheme::trayFolderIcon(const QString &) const -{ - QPixmap fallback = qApp->style()->standardPixmap(QStyle::SP_FileDialogNewFolder); - return QIcon::fromTheme("folder", fallback); -} - -QIcon ownCloudTheme::applicationIcon() const -{ - return themeIcon(QLatin1String("owncloud-icon")); -} - - -QVariant ownCloudTheme::customMedia(Theme::CustomMediaType type) +QVariant ownCloudTheme::customMedia(CustomMediaType) { - if (type == Theme::oCSetupTop) { - // return QCoreApplication::translate("ownCloudTheme", - // "If you don't have an ownCloud server yet, " - // "see <a href=\"https://owncloud.com\">owncloud.com</a> for more info.", - // "Top text in setup wizard. Keep short!"); - return QVariant(); - } else { - return QVariant(); - } + return QVariant(); } - #endif -QString ownCloudTheme::helpUrl() const -{ - return QString::fromLatin1("https://doc.owncloud.org/desktop/%1.%2/").arg(MIRALL_VERSION_MAJOR).arg(MIRALL_VERSION_MINOR); -} - #ifndef TOKEN_AUTH_ONLY QColor ownCloudTheme::wizardHeaderBackgroundColor() const { @@ -109,14 +57,4 @@ QPixmap ownCloudTheme::wizardHeaderLogo() const } #endif - -QString ownCloudTheme::appName() const -{ - return QLatin1String("ownCloud"); -} - -QString ownCloudTheme::appNameGUI() const -{ - return QLatin1String("ownCloud"); -} } diff --git a/src/libsync/owncloudtheme.h b/src/libsync/owncloudtheme.h index 327c96e5f..99143fe93 100644 --- a/src/libsync/owncloudtheme.h +++ b/src/libsync/owncloudtheme.h @@ -28,18 +28,6 @@ class ownCloudTheme : public Theme Q_OBJECT public: ownCloudTheme(); - - QString configFileName() const Q_DECL_OVERRIDE; - QString about() const Q_DECL_OVERRIDE; - -#ifndef TOKEN_AUTH_ONLY - QIcon trayFolderIcon(const QString &) const Q_DECL_OVERRIDE; - QIcon applicationIcon() const Q_DECL_OVERRIDE; -#endif - QString appName() const Q_DECL_OVERRIDE; - QString appNameGUI() const Q_DECL_OVERRIDE; - - QString helpUrl() const Q_DECL_OVERRIDE; #ifndef TOKEN_AUTH_ONLY QVariant customMedia(CustomMediaType type) Q_DECL_OVERRIDE; diff --git a/src/libsync/propagateupload.cpp b/src/libsync/propagateupload.cpp index 4579139ce..155eed8cc 100644 --- a/src/libsync/propagateupload.cpp +++ b/src/libsync/propagateupload.cpp @@ -471,7 +471,6 @@ void PropagateUploadFileCommon::slotPollFinished() propagator()->_activeJobList.removeOne(this); if (job->_item->_status != SyncFileItem::Success) { - _finished = true; done(job->_item->_status, job->_item->_errorString); return; } @@ -479,6 +478,12 @@ void PropagateUploadFileCommon::slotPollFinished() finalize(); } +void PropagateUploadFileCommon::done(SyncFileItem::Status status, const QString &errorString) +{ + _finished = true; + PropagateItemJob::done(status, errorString); +} + void PropagateUploadFileCommon::checkResettingErrors() { if (_item->_httpErrorCode == 412 @@ -561,7 +566,6 @@ void PropagateUploadFileCommon::abort(PropagatorJob::AbortType abortType) // This function is used whenever there is an error occuring and jobs might be in progress void PropagateUploadFileCommon::abortWithError(SyncFileItem::Status status, const QString &error) { - _finished = true; abort(AbortType::Synchronous); done(status, error); } @@ -610,8 +614,6 @@ QMap<QByteArray, QByteArray> PropagateUploadFileCommon::headers() void PropagateUploadFileCommon::finalize() { - _finished = true; - // Update the quota, if known auto quotaIt = propagator()->_folderQuota.find(QFileInfo(_item->_file).path()); if (quotaIt != propagator()->_folderQuota.end()) diff --git a/src/libsync/propagateupload.h b/src/libsync/propagateupload.h index fbbbc14f1..c7e8b4854 100644 --- a/src/libsync/propagateupload.h +++ b/src/libsync/propagateupload.h @@ -257,6 +257,8 @@ private slots: void slotPollFinished(); protected: + void done(SyncFileItem::Status status, const QString &errorString = QString()) override; + /** * Prepares the abort e.g. connects proper signals and slots * to the subjobs to abort asynchronously diff --git a/src/libsync/propagateuploadng.cpp b/src/libsync/propagateuploadng.cpp index a679887a5..13dfe513b 100644 --- a/src/libsync/propagateuploadng.cpp +++ b/src/libsync/propagateuploadng.cpp @@ -83,7 +83,7 @@ void PropagateUploadFileNG::doStartUpload() propagator()->_activeJobList.append(this); const SyncJournalDb::UploadInfo progressInfo = propagator()->_journal->getUploadInfo(_item->_file); - if (progressInfo._valid && progressInfo._modtime == _item->_modtime) { + if (progressInfo._valid && progressInfo.isChunked() && progressInfo._modtime == _item->_modtime) { _transferId = progressInfo._transferid; auto url = chunkUrl(); auto job = new LsColJob(propagator()->account(), url, this); @@ -98,7 +98,7 @@ void PropagateUploadFileNG::doStartUpload() this, &PropagateUploadFileNG::slotPropfindIterate); job->start(); return; - } else if (progressInfo._valid) { + } else if (progressInfo._valid && progressInfo.isChunked()) { // The upload info is stale. remove the stale chunks on the server _transferId = progressInfo._transferid; // Fire and forget. Any error will be ignored. @@ -143,6 +143,12 @@ void PropagateUploadFileNG::slotPropfindFinished() qCCritical(lcPropagateUpload) << "Inconsistency while resuming " << _item->_file << ": the size on the server (" << _sent << ") is bigger than the size of the file (" << _item->_size << ")"; + + // Wipe the old chunking data. + // Fire and forget. Any error will be ignored. + (new DeleteJob(propagator()->account(), chunkUrl(), this))->start(); + + propagator()->_activeJobList.append(this); startNewUpload(); return; } @@ -274,6 +280,7 @@ void PropagateUploadFileNG::startNextChunk() if (_currentChunkSize == 0) { Q_ASSERT(_jobs.isEmpty()); // There should be no running job anymore _finished = true; + // Finish with a MOVE QString destination = QDir::cleanPath(propagator()->account()->url().path() + QLatin1Char('/') + propagator()->account()->davPath() + propagator()->_remoteFolder + _item->_file); @@ -389,12 +396,12 @@ void PropagateUploadFileNG::slotPutFinished() << propagator()->_chunkSize << "bytes"; } - bool finished = _sent == _item->_size; + _finished = _sent == _item->_size; // Check if the file still exists const QString fullFilePath(propagator()->getFilePath(_item->_file)); if (!FileSystem::fileExists(fullFilePath)) { - if (!finished) { + if (!_finished) { abortWithError(SyncFileItem::SoftError, tr("The local file was removed during sync.")); return; } else { @@ -405,13 +412,13 @@ void PropagateUploadFileNG::slotPutFinished() // Check whether the file changed since discovery. if (!FileSystem::verifyFileUnchanged(fullFilePath, _item->_size, _item->_modtime)) { propagator()->_anotherSyncNeeded = true; - if (!finished) { + if (!_finished) { abortWithError(SyncFileItem::SoftError, tr("Local file changed during sync.")); return; } } - if (!finished) { + if (!_finished) { // Deletes an existing blacklist entry on successful chunk upload if (_item->_hasBlacklistEntry) { propagator()->_journal->wipeErrorBlacklistEntry(_item->_file); diff --git a/src/libsync/propagateuploadv1.cpp b/src/libsync/propagateuploadv1.cpp index 062b17634..8bd9489f5 100644 --- a/src/libsync/propagateuploadv1.cpp +++ b/src/libsync/propagateuploadv1.cpp @@ -43,7 +43,7 @@ void PropagateUploadFileV1::doStartUpload() const SyncJournalDb::UploadInfo progressInfo = propagator()->_journal->getUploadInfo(_item->_file); - if (progressInfo._valid && progressInfo._modtime == _item->_modtime + if (progressInfo._valid && progressInfo.isChunked() && progressInfo._modtime == _item->_modtime && (progressInfo._contentChecksum == _item->_checksumHeader || progressInfo._contentChecksum.isEmpty() || _item->_checksumHeader.isEmpty())) { _startChunk = progressInfo._chunk; _transferId = progressInfo._transferid; @@ -55,7 +55,7 @@ void PropagateUploadFileV1::doStartUpload() SyncJournalDb::UploadInfo pi; pi._valid = true; pi._chunk = 0; - pi._transferid = _transferId; + pi._transferid = 0; // We set a null transfer id because it is not chunked. pi._modtime = _item->_modtime; pi._errorCount = 0; pi._contentChecksum = _item->_checksumHeader; @@ -206,12 +206,12 @@ void PropagateUploadFileV1::slotPutFinished() // The server needs some time to process the request and provide us with a poll URL if (_item->_httpErrorCode == 202) { - _finished = true; QString path = QString::fromUtf8(job->reply()->rawHeader("OC-Finish-Poll")); if (path.isEmpty()) { done(SyncFileItem::NormalError, tr("Poll URL missing")); return; } + _finished = true; startPollJob(path); return; } @@ -226,12 +226,12 @@ void PropagateUploadFileV1::slotPutFinished() // yet, the upload can be stopped and an error can be displayed, because // the server hasn't registered the new file yet. QByteArray etag = getEtagFromReply(job->reply()); - bool finished = etag.length() > 0; + _finished = etag.length() > 0; // Check if the file still exists const QString fullFilePath(propagator()->getFilePath(_item->_file)); if (!FileSystem::fileExists(fullFilePath)) { - if (!finished) { + if (!_finished) { abortWithError(SyncFileItem::SoftError, tr("The local file was removed during sync.")); return; } else { @@ -242,7 +242,7 @@ void PropagateUploadFileV1::slotPutFinished() // Check whether the file changed since discovery. if (!FileSystem::verifyFileUnchanged(fullFilePath, _item->_size, _item->_modtime)) { propagator()->_anotherSyncNeeded = true; - if (!finished) { + if (!_finished) { abortWithError(SyncFileItem::SoftError, tr("Local file changed during sync.")); // FIXME: the legacy code was retrying for a few seconds. // and also checking that after the last chunk, and removed the file in case of INSTRUCTION_NEW @@ -250,14 +250,13 @@ void PropagateUploadFileV1::slotPutFinished() } } - if (!finished) { + if (!_finished) { // Proceed to next chunk. if (_currentChunk >= _chunkCount) { if (!_jobs.empty()) { // just wait for the other job to finish. return; } - _finished = true; done(SyncFileItem::NormalError, tr("The server did not acknowledge the last chunk. (No e-tag was present)")); return; } @@ -287,9 +286,8 @@ void PropagateUploadFileV1::slotPutFinished() startNextChunk(); return; } - // the following code only happens after all chunks were uploaded. - _finished = true; + // the file id should only be empty for new files up- or downloaded QByteArray fid = job->reply()->rawHeader("OC-FileID"); if (!fid.isEmpty()) { @@ -309,6 +307,7 @@ void PropagateUploadFileV1::slotPutFinished() qCWarning(lcPropagateUpload) << "Server does not support X-OC-MTime" << job->reply()->rawHeader("X-OC-MTime"); // Well, the mtime was not set done(SyncFileItem::SoftError, "Server does not support X-OC-MTime"); + return; } finalize(); diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 0f9907ca4..64481ca60 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -303,6 +303,8 @@ void SyncEngine::deleteStaleUploadInfos(const SyncFileItemVector &syncItems) // Delete the stales chunk on the server. if (account()->capabilities().chunkingNg()) { foreach (uint transferId, ids) { + if (!transferId) + continue; // Was not a chunked upload QUrl url = Utility::concatUrlPath(account()->url(), QLatin1String("remote.php/dav/uploads/") + account()->davUser() + QLatin1Char('/') + QString::number(transferId)); (new DeleteJob(account(), url, this))->start(); } @@ -1630,10 +1632,10 @@ AccountPtr SyncEngine::account() const return _account; } -void SyncEngine::setLocalDiscoveryOptions(LocalDiscoveryStyle style, std::set<QByteArray> dirs) +void SyncEngine::setLocalDiscoveryOptions(LocalDiscoveryStyle style, std::set<QByteArray> paths) { _localDiscoveryStyle = style; - _localDiscoveryPaths = std::move(dirs); + _localDiscoveryPaths = std::move(paths); } bool SyncEngine::shouldDiscoverLocally(const QByteArray &path) const diff --git a/src/libsync/syncengine.h b/src/libsync/syncengine.h index f43e2f26b..e00034f4d 100644 --- a/src/libsync/syncengine.h +++ b/src/libsync/syncengine.h @@ -104,7 +104,7 @@ public: /** * Control whether local discovery should read from filesystem or db. * - * If style is DatabaseAndFilesystem, dirs a set of file paths relative to + * If style is DatabaseAndFilesystem, paths a set of file paths relative to * the synced folder. All the parent directories of these paths will not * be read from the db and scanned on the filesystem. * @@ -112,7 +112,7 @@ public: * revert afterwards. Use _lastLocalDiscoveryStyle to discover the last * sync's style. */ - void setLocalDiscoveryOptions(LocalDiscoveryStyle style, std::set<QByteArray> dirs = {}); + void setLocalDiscoveryOptions(LocalDiscoveryStyle style, std::set<QByteArray> paths = {}); /** * Returns whether the given folder-relative path should be locally discovered diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index 4272bf9e5..88421c39d 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -75,10 +75,9 @@ public: /** For files whose errors were blacklisted * * If an file is blacklisted due to an error it isn't even reattempted. These - * errors should appear in the issues tab, but not on the account settings and - * should not cause the sync run to fail. + * errors should appear in the issues tab but should be silent otherwise. * - * A DetailError that doesn't cause sync failure. + * A SoftError caused by blacklisting. */ BlacklistedError }; diff --git a/src/libsync/theme.cpp b/src/libsync/theme.cpp index e9e389fd6..da6e28344 100644 --- a/src/libsync/theme.cpp +++ b/src/libsync/theme.cpp @@ -20,6 +20,8 @@ #include <QtCore> #ifndef TOKEN_AUTH_ONLY #include <QtGui> +#include <QStyle> +#include <QApplication> #endif #include <QSslSocket> @@ -105,12 +107,16 @@ QString Theme::version() const return MIRALL_VERSION_STRING; } +QString Theme::configFileName() const +{ + return QStringLiteral(APPLICATION_EXECUTABLE ".cfg"); +} + #ifndef TOKEN_AUTH_ONLY -QIcon Theme::trayFolderIcon(const QString &backend) const +QIcon Theme::applicationIcon() const { - Q_UNUSED(backend) - return applicationIcon(); + return themeIcon(QStringLiteral(APPLICATION_ICON_NAME "-icon")); } /* @@ -217,6 +223,11 @@ QString Theme::defaultServerFolder() const return QLatin1String("/"); } +QString Theme::helpUrl() const +{ + return QString::fromLatin1("https://doc.owncloud.org/desktop/%1.%2/").arg(MIRALL_VERSION_MAJOR).arg(MIRALL_VERSION_MINOR); +} + QString Theme::overrideServerUrl() const { return QString(); @@ -311,21 +322,21 @@ QString Theme::gitSHA1() const QString Theme::about() const { - QString re; - re = tr("<p>Version %1. For more information please visit <a href='%2'>%3</a>.</p>") - .arg(MIRALL_VERSION_STRING) - .arg("http://" MIRALL_STRINGIFY(APPLICATION_DOMAIN)) - .arg(MIRALL_STRINGIFY(APPLICATION_DOMAIN)); - - re += tr("<p>Copyright ownCloud GmbH</p>"); - re += tr("<p>Distributed by %1 and licensed under the GNU General Public License (GPL) Version 2.0.<br/>" - "%2 and the %2 logo are registered trademarks of %1 in the " - "United States, other countries, or both.</p>") - .arg(APPLICATION_VENDOR) - .arg(APPLICATION_NAME); - - re += gitSHA1(); - return re; + QString devString; + devString = tr("<p>Version %2. For more information visit <a href=\"%3\">https://%4</a></p>" + "<p>For known issues and help, please visit: <a href=\"https://central.owncloud.org/c/desktop-client\">https://central.owncloud.org</a></p>" + "<p><small>By Klaas Freitag, Daniel Molkentin, Olivier Goffart, Markus Götz, " + " Jan-Christoph Borchardt, and others.</small></p>" + "<p>Copyright ownCloud GmbH</p>" + "<p>Licensed under the GNU General Public License (GPL) Version 2.0<br/>" + "ownCloud and the ownCloud Logo are registered trademarks of ownCloud GmbH " + "in the United States, other countries, or both.</p>") + .arg(Utility::escape(MIRALL_VERSION_STRING), + Utility::escape("https://" MIRALL_STRINGIFY(APPLICATION_DOMAIN)), + Utility::escape(MIRALL_STRINGIFY(APPLICATION_DOMAIN))); + + devString += gitSHA1(); + return devString; } #ifndef TOKEN_AUTH_ONLY diff --git a/src/libsync/theme.h b/src/libsync/theme.h index c2cd4922f..90ddd7ef1 100644 --- a/src/libsync/theme.h +++ b/src/libsync/theme.h @@ -84,24 +84,19 @@ public: * @brief configFileName * @return the name of the config file. */ - virtual QString configFileName() const = 0; + virtual QString configFileName() const; #ifndef TOKEN_AUTH_ONLY static QString hidpiFileName(const QString &fileName, QPaintDevice *dev = 0); /** - * the icon that is shown in the tray context menu left of the folder name - */ - virtual QIcon trayFolderIcon(const QString &) const; - - /** * get an sync state icon */ virtual QIcon syncStateIcon(SyncResult::Status, bool sysTray = false, bool sysTrayMenuVisible = false) const; virtual QIcon folderDisabledIcon() const; virtual QIcon folderOfflineIcon(bool sysTray = false, bool sysTrayMenuVisible = false) const; - virtual QIcon applicationIcon() const = 0; + virtual QIcon applicationIcon() const; #endif virtual QString statusHeaderText(SyncResult::Status) const; @@ -118,9 +113,11 @@ public: virtual bool multiAccount() const; /** - * URL to help file + * URL to documentation. + * This is opened in the browser when the "Help" action is selected from the tray menu. + * (If it is an empty stringn the action is removed from the menu. Defaults to ownCloud's help) */ - virtual QString helpUrl() const { return QString(); } + virtual QString helpUrl() const; /** * Setting a value here will pre-define the server url. |