diff --git a/.github/workflows/artifact.yml b/.github/workflows/artifact.yml index 2592ac23..ad9c6a3a 100644 --- a/.github/workflows/artifact.yml +++ b/.github/workflows/artifact.yml @@ -27,7 +27,8 @@ jobs: rm -rf ./.github/ rm -rf ./.git/ rm ./.gitmodules - rm -rf ./assets/.git + rm -rf ./assets/document-templates/.git + rm -rf ./assets/document-formats/.git cd ./appinfo sed -i 's|apl2|agpl|' info.xml cd $cwd diff --git a/.github/workflows/lint-phpcs.yml b/.github/workflows/lint-phpcs.yml index 2b7aa090..dd4eff38 100644 --- a/.github/workflows/lint-phpcs.yml +++ b/.github/workflows/lint-phpcs.yml @@ -1,10 +1,11 @@ name: Lint on: + workflow_dispatch: push: branches: [master, develop] pull_request: - branches: [master] + branches: [master, develop] permissions: contents: read @@ -24,4 +25,4 @@ jobs: tools: composer, cs2pr, phpcs - name: Run phpcs run: | - phpcs --standard=PSR2 --extensions=php,module,inc,install --ignore=node_modules,bower_components,vendor,3rdparty --warning-severity=0 ./ \ No newline at end of file + phpcs --standard=./ruleset.xml --extensions=php,module,inc,install --ignore=node_modules,bower_components,vendor,3rdparty --warning-severity=0 ./ diff --git a/.gitmodules b/.gitmodules index f7a93934..e7c7242c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,7 @@ -[submodule "assets"] - path = assets +[submodule "assets/document-templates"] + path = assets/document-templates url = https://github.com/ONLYOFFICE/document-templates branch = main/new +[submodule "assets/document-formats"] + path = assets/document-formats + url = https://github.com/ONLYOFFICE/document-formats diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fcace2e..46857e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Change Log +## 9.1.1 +## Added +- support of user avatar in editor +- list of users to protect ranges of cells +- setting for disable editors cron check +- selecting a document to combine from the storage +- reference data from coediting +- opening a reference data source +- changing a reference data source +- Arabic and Serbian empty file templates + +## Changed +- fixed guest redirect when limiting the app to groups +- fixed mobile editor size +- offline viewer for share link +- updatable list of supported formats +- filling pdf instead oform +- compatible with ownCloud Web 7.0 + ## 8.2.3 ## Added - Ukrainian translation diff --git a/README.md b/README.md index 22321fd4..10f8988d 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,8 @@ The app allows to: Supported formats: -* For viewing and editing: DOCX, XLSX, PPTX, CSV, TXT, DOCXF, OFORM. -* For viewing only: PDF. -* For converting to Office Open XML formats: DOC, DOCM, DOT, DOTX, EPUB, HTM, HTML, ODP, ODT, POT, POTM, POTX, PPS, PPSM, PPSX, PPT, PPTM, RTF, XLS, XLSM, XLT, XLTM, XLTX. +* For editing: DOCM, DOCX, DOCXF, DOTM, DOTX, EPUB, FB2, HTML, ODT, OTT, RTF, TXT, CSV, ODS, OTS, XLSM, XLSX, XLTM, XLTX, ODP, OTP, POTM, POTX, PPSM, PPSX, PPTM, PPTX. +* For viewing only: DJVU, DOC, DOT, FODT, HTM, MHT, MHTML, OFORM, PDF, STW, SXW, WPS, WPT, XML, XPS, ET, ETT, FODS, SXC, XLS, XLSB, XLT, DPS, DPT, FODP, POT, PPS, PPT, SXI. ODT, ODS, and ODP is also available for instant conversion. After you enable the corresponding option in the admin settings, ODF-formatted documents are immediately converted in the editor and opened after you click on it. @@ -208,6 +207,8 @@ The instruction on enabling _master key_ based encryption is available in the of ``` To disable this check running, enter 0 value. +* When accessing a document without download permission, file printing and using the system clipboard are not available. Copying and pasting within the editor is available via buttons in the editor toolbar and in the context menu. + ## ONLYOFFICE Docs editions ONLYOFFICE offers different versions of its online document editors that can be deployed on your own servers. @@ -232,12 +233,12 @@ The table below will help you to make the right choice. | Conversion Service | + | + | | Document Builder Service | + | + | | **Interface** | **Community Edition** | **Enterprise Edition** | -| Tabbed interface | + | + | -| Dark theme | + | + | -| 125%, 150%, 175%, 200% scaling | + | + | -| White Label | - | - | -| Integrated test example (node.js) | + | + | -| Mobile web editors | - | +* | +| Tabbed interface | + | + | +| Dark theme | + | + | +| 125%, 150%, 175%, 200% scaling | + | + | +| White Label | - | - | +| Integrated test example (node.js) | + | + | +| Mobile web editors | - | +* | | **Plugins & Macros** | **Community Edition** | **Enterprise Edition** | | Plugins | + | + | | Macros | + | + | @@ -272,12 +273,18 @@ The table below will help you to make the right choice. | Font and paragraph formatting | + | + | | Object insertion | + | + | | Transitions | + | + | +| Animations | + | + | | Presenter mode | + | + | | Notes | + | + | | **Form creator features** | **Community Edition** | **Enterprise Edition** | | Adding form fields | + | + | | Form preview | + | + | | Saving as PDF | + | + | +| **Working with PDF** | **Community Edition** | **Enterprise Edition** | +| Text annotations (highlight, underline, cross out) | + | + | +| Comments | + | + | +| Freehand drawings | + | + | +| Form filling | + | + | | | [Get it now](https://www.onlyoffice.com/download-docs.aspx?utm_source=github&utm_medium=cpc&utm_campaign=GitHubOwncloud#docs-community) | [Start Free Trial](https://www.onlyoffice.com/download-docs.aspx?utm_source=github&utm_medium=cpc&utm_campaign=GitHubOwncloud#docs-enterprise) | \* If supported by DMS. diff --git a/appinfo/app.php b/appinfo/app.php index bb6cf568..e0209e25 100644 --- a/appinfo/app.php +++ b/appinfo/app.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/appinfo/application.php b/appinfo/application.php index 3e0b9eaf..d71e89aa 100644 --- a/appinfo/application.php +++ b/appinfo/application.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,216 +42,268 @@ use OCA\Onlyoffice\Notifier; use OCA\Onlyoffice\Preview; +/** + * Class Application + * + * @package OCA\Onlyoffice\AppInfo + */ class Application extends App { - - /** - * Application configuration - * - * @var AppConfig - */ - public $appConfig; - - /** - * Hash generator - * - * @var Crypt - */ - public $crypt; - - public function __construct(array $urlParams = []) { - $appName = "onlyoffice"; - - parent::__construct($appName, $urlParams); - - $this->appConfig = new AppConfig($appName); - $this->crypt = new Crypt($this->appConfig); - - // Default script and style if configured - $eventDispatcher = \OC::$server->getEventDispatcher(); - $eventDispatcher->addListener("OCA\Files::loadAdditionalScripts", - function () { - if (!empty($this->appConfig->GetDocumentServerUrl()) - && $this->appConfig->SettingsAreSuccessful() - && $this->appConfig->isUserAllowedToUse()) { - Util::addScript("onlyoffice", "desktop"); - Util::addScript("onlyoffice", "main"); - Util::addScript("onlyoffice", "share"); - Util::addScript("onlyoffice", "template"); - - if ($this->appConfig->GetSameTab()) { - Util::addScript("onlyoffice", "listener"); - } - - Util::addStyle("onlyoffice", "template"); - Util::addStyle("onlyoffice", "main"); - } - }); - - Util::connectHook("OCP\Share", "share_link_access", Hookhandler::class, "PublicPage"); - - require_once __DIR__ . "/../3rdparty/jwt/BeforeValidException.php"; - require_once __DIR__ . "/../3rdparty/jwt/ExpiredException.php"; - require_once __DIR__ . "/../3rdparty/jwt/SignatureInvalidException.php"; - require_once __DIR__ . "/../3rdparty/jwt/CachedKeySet.php"; - require_once __DIR__ . "/../3rdparty/jwt/JWT.php"; - require_once __DIR__ . "/../3rdparty/jwt/JWK.php"; - require_once __DIR__ . "/../3rdparty/jwt/Key.php"; - - // Set the leeway for the JWT library in case the system clock is a second off - \Firebase\JWT\JWT::$leeway = $this->appConfig->GetJwtLeeway(); - - $container = $this->getContainer(); - - $detector = $container->query(IMimeTypeDetector::class); - $detector->getAllMappings(); - $detector->registerType("ott", "application/vnd.oasis.opendocument.text-template"); - $detector->registerType("ots", "application/vnd.oasis.opendocument.spreadsheet-template"); - $detector->registerType("otp", "application/vnd.oasis.opendocument.presentation-template"); - $detector->registerType("docxf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf"); - $detector->registerType("oform", "application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform"); - - $previewManager = $container->query(IPreview::class); - if ($this->appConfig->GetPreview()) { - $previewManager->registerProvider(Preview::getMimeTypeRegex(), function() use ($container) { - return $container->query(Preview::class); - }); - } - - $notificationManager = \OC::$server->getNotificationManager(); - $notificationManager->registerNotifier(function () use ($appName) { - return new Notifier( - $appName, - \OC::$server->getL10NFactory(), - \OC::$server->getURLGenerator(), - \OC::$server->getLogger(), - \OC::$server->getUserManager() - ); - }, function () use ($appName) { - return [ - "id" => $appName, - "name" => $appName, - ]; - }); - - $container->registerService("L10N", function ($c) { - return $c->query("ServerContainer")->getL10N($c->query("AppName")); - }); - - $container->registerService("RootStorage", function ($c) { - return $c->query("ServerContainer")->getRootFolder(); - }); - - $container->registerService("UserSession", function ($c) { - return $c->query("ServerContainer")->getUserSession(); - }); - - $container->registerService("Logger", function ($c) { - return $c->query("ServerContainer")->getLogger(); - }); - - $container->registerService("URLGenerator", function ($c) { - return $c->query("ServerContainer")->getURLGenerator(); - }); - - - // Controllers - $container->registerService("SettingsController", function ($c) { - return new SettingsController( - $c->query("AppName"), - $c->query("Request"), - $c->query("URLGenerator"), - $c->query("L10N"), - $c->query("Logger"), - $this->appConfig, - $this->crypt - ); - }); - - $container->registerService("SettingsApiController", function ($c) { - return new SettingsApiController( - $c->query("AppName"), - $c->query("Request"), - $c->query("URLGenerator"), - $this->appConfig - ); - }); - - $container->registerService("EditorController", function ($c) { - return new EditorController( - $c->query("AppName"), - $c->query("Request"), - $c->query("RootStorage"), - $c->query("UserSession"), - $c->query("ServerContainer")->getUserManager(), - $c->query("URLGenerator"), - $c->query("L10N"), - $c->query("Logger"), - $this->appConfig, - $this->crypt, - $c->query("IManager"), - $c->query("Session"), - $c->query("ServerContainer")->getGroupManager() - ); - }); - - $container->registerService("EditorApiController", function ($c) { - return new EditorApiController( - $c->query("AppName"), - $c->query("Request"), - $c->query("RootStorage"), - $c->query("UserSession"), - $c->query("URLGenerator"), - $c->query("L10N"), - $c->query("Logger"), - $this->appConfig, - $this->crypt, - $c->query("IManager"), - $c->query("Session"), - $c->get(ITagManager::class) - ); - }); - - $container->registerService("CallbackController", function ($c) { - return new CallbackController( - $c->query("AppName"), - $c->query("Request"), - $c->query("RootStorage"), - $c->query("UserSession"), - $c->query("ServerContainer")->getUserManager(), - $c->query("L10N"), - $c->query("Logger"), - $this->appConfig, - $this->crypt, - $c->query("IManager") - ); - }); - - $container->registerService("TemplateController", function ($c) { - return new TemplateController( - $c->query("AppName"), - $c->query("Request"), - $c->query("L10N"), - $c->query("Logger") - ); - }); - - $container->registerService("WebAssetController", function ($c) { - return new WebAssetController( - $c->query("AppName"), - $c->query("Request"), - $c->query("Logger") - ); - }); - - $checkBackgroundJobs = new JobListController( - $container->query("AppName"), - $container->query("Request"), - $container->query("Logger"), - $this->appConfig, - $container->query(IJobList::class) - ); - $checkBackgroundJobs->checkAllJobs(); - - Hooks::connectHooks(); - } + /** + * Application configuration + * + * @var AppConfig + */ + public $appConfig; + + /** + * Hash generator + * + * @var Crypt + */ + public $crypt; + + /** + * Application constructor. + * + * @param array $urlParams + */ + public function __construct(array $urlParams = []) { + $appName = "onlyoffice"; + + parent::__construct($appName, $urlParams); + + $this->appConfig = new AppConfig($appName); + $this->crypt = new Crypt($this->appConfig); + + // Default script and style if configured + $eventDispatcher = \OC::$server->getEventDispatcher(); + $eventDispatcher->addListener( + "OCA\Files::loadAdditionalScripts", + function () { + if (!empty($this->appConfig->getDocumentServerUrl()) + && $this->appConfig->settingsAreSuccessful() + && $this->appConfig->isUserAllowedToUse() + ) { + Util::addScript("onlyoffice", "desktop"); + Util::addScript("onlyoffice", "main"); + Util::addScript("onlyoffice", "share"); + Util::addScript("onlyoffice", "template"); + + if ($this->appConfig->getSameTab()) { + Util::addScript("onlyoffice", "listener"); + } + + Util::addStyle("onlyoffice", "template"); + Util::addStyle("onlyoffice", "main"); + } + } + ); + + Util::connectHook("OCP\Share", "share_link_access", Hookhandler::class, "PublicPage"); + + include_once __DIR__ . "/../3rdparty/jwt/BeforeValidException.php"; + include_once __DIR__ . "/../3rdparty/jwt/ExpiredException.php"; + include_once __DIR__ . "/../3rdparty/jwt/SignatureInvalidException.php"; + include_once __DIR__ . "/../3rdparty/jwt/CachedKeySet.php"; + include_once __DIR__ . "/../3rdparty/jwt/JWT.php"; + include_once __DIR__ . "/../3rdparty/jwt/JWK.php"; + include_once __DIR__ . "/../3rdparty/jwt/Key.php"; + + // Set the leeway for the JWT library in case the system clock is a second off + \Firebase\JWT\JWT::$leeway = $this->appConfig->getJwtLeeway(); + + $container = $this->getContainer(); + + $detector = $container->query(IMimeTypeDetector::class); + $detector->getAllMappings(); + $detector->registerType("ott", "application/vnd.oasis.opendocument.text-template"); + $detector->registerType("ots", "application/vnd.oasis.opendocument.spreadsheet-template"); + $detector->registerType("otp", "application/vnd.oasis.opendocument.presentation-template"); + $detector->registerType("docxf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf"); + + $previewManager = $container->query(IPreview::class); + if ($this->appConfig->getPreview()) { + $previewManager->registerProvider( + Preview::getMimeTypeRegex(), + function () use ($container) { + return $container->query(Preview::class); + } + ); + } + + $notificationManager = \OC::$server->getNotificationManager(); + $notificationManager->registerNotifier( + function () use ($appName) { + return new Notifier( + $appName, + \OC::$server->getL10NFactory(), + \OC::$server->getURLGenerator(), + \OC::$server->getLogger(), + \OC::$server->getUserManager() + ); + }, + function () use ($appName) { + return [ + "id" => $appName, + "name" => $appName, + ]; + } + ); + + $container->registerService( + "L10N", + function ($c) { + return $c->query("ServerContainer")->getL10N($c->query("AppName")); + } + ); + + $container->registerService( + "RootStorage", + function ($c) { + return $c->query("ServerContainer")->getRootFolder(); + } + ); + + $container->registerService( + "UserSession", + function ($c) { + return $c->query("ServerContainer")->getUserSession(); + } + ); + + $container->registerService( + "Logger", + function ($c) { + return $c->query("ServerContainer")->getLogger(); + } + ); + + $container->registerService( + "URLGenerator", + function ($c) { + return $c->query("ServerContainer")->getURLGenerator(); + } + ); + + // Controllers + $container->registerService( + "SettingsController", + function ($c) { + return new SettingsController( + $c->query("AppName"), + $c->query("Request"), + $c->query("URLGenerator"), + $c->query("L10N"), + $c->query("Logger"), + $this->appConfig, + $this->crypt + ); + } + ); + + $container->registerService( + "SettingsApiController", + function ($c) { + return new SettingsApiController( + $c->query("AppName"), + $c->query("Request"), + $c->query("URLGenerator"), + $this->appConfig + ); + } + ); + + $container->registerService( + "EditorController", + function ($c) { + return new EditorController( + $c->query("AppName"), + $c->query("Request"), + $c->query("RootStorage"), + $c->query("UserSession"), + $c->query("ServerContainer")->getUserManager(), + $c->query("URLGenerator"), + $c->query("L10N"), + $c->query("Logger"), + $this->appConfig, + $this->crypt, + $c->query("IManager"), + $c->query("Session"), + $c->query("ServerContainer")->getGroupManager() + ); + } + ); + + $container->registerService( + "EditorApiController", + function ($c) { + return new EditorApiController( + $c->query("AppName"), + $c->query("Request"), + $c->query("RootStorage"), + $c->query("UserSession"), + $c->query("URLGenerator"), + $c->query("L10N"), + $c->query("Logger"), + $this->appConfig, + $this->crypt, + $c->query("IManager"), + $c->query("Session"), + $c->get(ITagManager::class) + ); + } + ); + + $container->registerService( + "CallbackController", + function ($c) { + return new CallbackController( + $c->query("AppName"), + $c->query("Request"), + $c->query("RootStorage"), + $c->query("UserSession"), + $c->query("ServerContainer")->getUserManager(), + $c->query("L10N"), + $c->query("Logger"), + $this->appConfig, + $this->crypt, + $c->query("IManager") + ); + } + ); + + $container->registerService( + "TemplateController", + function ($c) { + return new TemplateController( + $c->query("AppName"), + $c->query("Request"), + $c->query("L10N"), + $c->query("Logger") + ); + } + ); + + $container->registerService( + "WebAssetController", + function ($c) { + return new WebAssetController( + $c->query("AppName"), + $c->query("Request"), + $c->query("Logger") + ); + } + ); + + $checkBackgroundJobs = new JobListController( + $container->query("AppName"), + $container->query("Request"), + $container->query("Logger"), + $this->appConfig, + $container->query(IJobList::class) + ); + $checkBackgroundJobs->checkAllJobs(); + + Hooks::connectHooks(); + } } diff --git a/appinfo/info.xml b/appinfo/info.xml index 550ad6af..4daac1be 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -6,7 +6,7 @@ ONLYOFFICE connector allows you to view, edit and collaborate on text documents, spreadsheets and presentations within ownCloud using ONLYOFFICE Docs. This will create a new Edit in ONLYOFFICE action within the document library for Office documents. This allows multiple users to co-author documents in real time from the familiar web interface and save the changes back to your file storage. apl2 Ascensio System SIA - 8.2.3 + 9.1.1 Onlyoffice diff --git a/appinfo/routes.php b/appinfo/routes.php index 1f9b0199..cac20538 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,40 +19,41 @@ */ return [ - "routes" => [ - ["name" => "callback#download", "url" => "/download", "verb" => "GET"], - ["name" => "callback#emptyfile", "url" => "/empty", "verb" => "GET"], - ["name" => "callback#track", "url" => "/track", "verb" => "POST"], - ["name" => "editor#create_new", "url" => "/new", "verb" => "GET"], - ["name" => "editor#download", "url" => "/downloadas", "verb" => "GET"], - ["name" => "editor#index", "url" => "/{fileId}", "verb" => "GET"], - ["name" => "editor#public_page", "url" => "/s/{shareToken}", "verb" => "GET"], - ["name" => "editor#users", "url" => "/ajax/users", "verb" => "GET"], - ["name" => "editor#mention", "url" => "/ajax/mention", "verb" => "POST"], - ["name" => "editor#reference", "url" => "/ajax/reference", "verb" => "POST"], - ["name" => "editor#create", "url" => "/ajax/new", "verb" => "POST"], - ["name" => "editor#convert", "url" => "/ajax/convert", "verb" => "POST"], - ["name" => "editor#save", "url" => "/ajax/save", "verb" => "POST"], - ["name" => "editor#url", "url" => "/ajax/url", "verb" => "GET"], - ["name" => "editor#history", "url" => "/ajax/history", "verb" => "GET"], - ["name" => "editor#version", "url" => "/ajax/version", "verb" => "GET"], - ["name" => "editor#restore", "url" => "/ajax/restore", "verb" => "PUT"], - ["name" => "settings#save_address", "url" => "/ajax/settings/address", "verb" => "PUT"], - ["name" => "settings#save_common", "url" => "/ajax/settings/common", "verb" => "PUT"], - ["name" => "settings#save_security", "url" => "/ajax/settings/security", "verb" => "PUT"], - ["name" => "settings#get_settings", "url" => "/ajax/settings", "verb" => "GET"], - ["name" => "settings#clear_history", "url" => "/ajax/settings/history", "verb" => "DELETE"], - ["name" => "template#add_template", "url" => "/ajax/template", "verb" => "POST"], - ["name" => "template#get_templates", "url" => "/ajax/template", "verb" => "GET"], - ["name" => "template#delete_template", "url" => "/ajax/template", "verb" => "DELETE"], - ["name" => "webasset#get", "url" => "/js/onlyoffice.js", "verb" => "GET"], - ], - "ocs" => [ - ["name" => "federation#key", "url" => "/api/v1/key", "verb" => "POST"], - ["name" => "federation#keylock", "url" => "/api/v1/keylock", "verb" => "POST"], - ["name" => "federation#healthcheck", "url" => "/api/v1/healthcheck", "verb" => "GET"], - ["name" => "editorapi#config", "url" => "/api/v1/config/{fileId}", "verb" => "GET"], - ["name" => "editorapi#fillempty", "url" => "/api/v1/empty/{fileId}", "verb" => "GET"], - ["name" => "settingsapi#get_doc_server_url", "url" => "/api/v1/settings/docserver", "verb" => "GET"], - ] + "routes" => [ + ["name" => "callback#download", "url" => "/download", "verb" => "GET"], + ["name" => "callback#emptyfile", "url" => "/empty", "verb" => "GET"], + ["name" => "callback#track", "url" => "/track", "verb" => "POST"], + ["name" => "editor#create_new", "url" => "/new", "verb" => "GET"], + ["name" => "editor#download", "url" => "/downloadas", "verb" => "GET"], + ["name" => "editor#index", "url" => "/{fileId}", "verb" => "GET"], + ["name" => "editor#public_page", "url" => "/s/{shareToken}", "verb" => "GET"], + ["name" => "editor#user_info", "url" => "/ajax/userInfo", "verb" => "GET"], + ["name" => "editor#users", "url" => "/ajax/users", "verb" => "GET"], + ["name" => "editor#mention", "url" => "/ajax/mention", "verb" => "POST"], + ["name" => "editor#reference", "url" => "/ajax/reference", "verb" => "POST"], + ["name" => "editor#create", "url" => "/ajax/new", "verb" => "POST"], + ["name" => "editor#convert", "url" => "/ajax/convert", "verb" => "POST"], + ["name" => "editor#save", "url" => "/ajax/save", "verb" => "POST"], + ["name" => "editor#url", "url" => "/ajax/url", "verb" => "GET"], + ["name" => "editor#history", "url" => "/ajax/history", "verb" => "GET"], + ["name" => "editor#version", "url" => "/ajax/version", "verb" => "GET"], + ["name" => "editor#restore", "url" => "/ajax/restore", "verb" => "PUT"], + ["name" => "settings#save_address", "url" => "/ajax/settings/address", "verb" => "PUT"], + ["name" => "settings#save_common", "url" => "/ajax/settings/common", "verb" => "PUT"], + ["name" => "settings#save_security", "url" => "/ajax/settings/security", "verb" => "PUT"], + ["name" => "settings#get_settings", "url" => "/ajax/settings", "verb" => "GET"], + ["name" => "settings#clear_history", "url" => "/ajax/settings/history", "verb" => "DELETE"], + ["name" => "template#add_template", "url" => "/ajax/template", "verb" => "POST"], + ["name" => "template#get_templates", "url" => "/ajax/template", "verb" => "GET"], + ["name" => "template#delete_template", "url" => "/ajax/template", "verb" => "DELETE"], + ["name" => "webasset#get", "url" => "/js/onlyoffice.js", "verb" => "GET"], + ], + "ocs" => [ + ["name" => "federation#key", "url" => "/api/v1/key", "verb" => "POST"], + ["name" => "federation#keylock", "url" => "/api/v1/keylock", "verb" => "POST"], + ["name" => "federation#healthcheck", "url" => "/api/v1/healthcheck", "verb" => "GET"], + ["name" => "editorapi#config", "url" => "/api/v1/config/{fileId}", "verb" => "GET"], + ["name" => "editorapi#fillempty", "url" => "/api/v1/empty/{fileId}", "verb" => "GET"], + ["name" => "settingsapi#get_doc_server_url", "url" => "/api/v1/settings/docserver", "verb" => "GET"], + ] ]; diff --git a/assets b/assets deleted file mode 160000 index f00ab3a3..00000000 --- a/assets +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f00ab3a3efe6e2f8542ba026d1fc1d72df7dfd5f diff --git a/assets/document-formats b/assets/document-formats new file mode 160000 index 00000000..e16bf990 --- /dev/null +++ b/assets/document-formats @@ -0,0 +1 @@ +Subproject commit e16bf99040e73fbe803c6790d9e6145e33be037f diff --git a/assets/document-templates b/assets/document-templates new file mode 160000 index 00000000..e0771eb5 --- /dev/null +++ b/assets/document-templates @@ -0,0 +1 @@ +Subproject commit e0771eb517facd02b2c9f2f4ee10b278da694d5d diff --git a/controller/callbackcontroller.php b/controller/callbackcontroller.php index 05704c9f..7e9b5f1b 100644 --- a/controller/callbackcontroller.php +++ b/controller/callbackcontroller.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +42,7 @@ use OCA\Onlyoffice\Crypt; use OCA\Onlyoffice\DocumentService; use OCA\Onlyoffice\FileVersions; +use OCA\Onlyoffice\FileUtility; use OCA\Onlyoffice\VersionManager; use OCA\Onlyoffice\KeyManager; use OCA\Onlyoffice\RemoteInstance; @@ -52,739 +54,755 @@ * Save the file without authentication. */ class CallbackController extends Controller { - - /** - * Root folder - * - * @var IRootFolder - */ - private $root; - - /** - * User session - * - * @var IUserSession - */ - private $userSession; - - /** - * User manager - * - * @var IUserManager - */ - private $userManager; - - /** - * l10n service - * - * @var IL10N - */ - private $trans; - - /** - * Logger - * - * @var OCP\ILogger - */ - private $logger; - - /** - * Application configuration - * - * @var AppConfig - */ - private $config; - - /** - * Hash generator - * - * @var Crypt - */ - private $crypt; - - /** - * Share manager - * - * @var IManager - */ - private $shareManager; - - /** - * File version manager - * - * @var VersionManager - */ - private $versionManager; - - /** - * Status of the document - */ - private const TrackerStatus_Editing = 1; - private const TrackerStatus_MustSave = 2; - private const TrackerStatus_Corrupted = 3; - private const TrackerStatus_Closed = 4; - private const TrackerStatus_ForceSave = 6; - private const TrackerStatus_CorruptedForceSave = 7; - - /** - * @param string $AppName - application name - * @param IRequest $request - request object - * @param IRootFolder $root - root folder - * @param IUserSession $userSession - user session - * @param IUserManager $userManager - user manager - * @param IL10N $trans - l10n service - * @param ILogger $logger - logger - * @param AppConfig $config - application configuration - * @param Crypt $crypt - hash generator - * @param IManager $shareManager - Share manager - */ - public function __construct($AppName, - IRequest $request, - IRootFolder $root, - IUserSession $userSession, - IUserManager $userManager, - IL10N $trans, - ILogger $logger, - AppConfig $config, - Crypt $crypt, - IManager $shareManager - ) { - parent::__construct($AppName, $request); - - $this->root = $root; - $this->userSession = $userSession; - $this->userManager = $userManager; - $this->trans = $trans; - $this->logger = $logger; - $this->config = $config; - $this->crypt = $crypt; - $this->shareManager = $shareManager; - - $this->versionManager = new VersionManager($AppName, $root); - } - - - /** - * Downloading file by the document service - * - * @param string $doc - verification token with the file identifier - * - * @return DataDownloadResponse|JSONResponse - * - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - * @CORS - */ - public function download($doc) { - - list ($hashData, $error) = $this->crypt->ReadHash($doc); - if ($hashData === null) { - $this->logger->error("Download with empty or not correct hash: $error", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); - } - if ($hashData->action !== "download") { - $this->logger->error("Download with other action", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST); - } - - $fileId = $hashData->fileId; - $version = isset($hashData->version) ? $hashData->version : null; - $changes = isset($hashData->changes) ? $hashData->changes : false; - $template = isset($hashData->template) ? $hashData->template : false; - $this->logger->debug("Download: $fileId ($version)" . ($changes ? " changes" : ""), ["app" => $this->appName]); - - if (!$this->userSession->isLoggedIn() - && !$changes) { - if (!empty($this->config->GetDocumentServerSecret())) { - $header = \OC::$server->getRequest()->getHeader($this->config->JwtHeader()); - if (empty($header)) { - $this->logger->error("Download without jwt", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); - } - - $header = substr($header, strlen("Bearer ")); - - try { - $decodedHeader = \Firebase\JWT\JWT::decode($header, new \Firebase\JWT\Key($this->config->GetDocumentServerSecret(), "HS256")); - } catch (\UnexpectedValueException $e) { - $this->logger->logException($e, ["message" => "Download with invalid jwt", "app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); - } - } - } - - $userId = null; - - $user = null; - if ($this->userSession->isLoggedIn()) { - $user = $this->userSession->getUser(); - $userId = $user->getUID(); - } else { - \OC_Util::tearDownFS(); - - if (isset($hashData->userId)) { - $userId = $hashData->userId; - - $user = $this->userManager->get($userId); - if (!empty($user)) { - \OC_User::setUserId($userId); - } - - if ($this->config->checkEncryptionModule() === "master") { - \OC_User::setIncognitoMode(true); - } else { - if (!empty($user)) { - \OC_Util::setupFS($userId); - } - } - } - } - - $shareToken = isset($hashData->shareToken) ? $hashData->shareToken : null; - list ($file, $error) = empty($shareToken) ? $this->getFile($userId, $fileId, null, $changes ? null : $version, $template) : $this->getFileByToken($fileId, $shareToken, $changes ? null : $version); - - if (isset($error)) { - return $error; - } - - if ($this->userSession->isLoggedIn() && !$file->isReadable()) { - $this->logger->error("Download without access right", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); - } - - if (empty($user) - && $this->config->checkEncryptionModule() !== "master") { - $owner = $file->getFileInfo()->getOwner(); - if ($owner !== null) { - \OC_Util::setupFS($owner->getUID()); - } - } - - if ($changes) { - if ($this->versionManager->available !== true) { - $this->logger->error("Download changes: versionManager is null", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST); - } - - $owner = $file->getFileInfo()->getOwner(); - if ($owner === null) { - $this->logger->error("Download: changes owner of $fileId was not found", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Files not found")], Http::STATUS_NOT_FOUND); - } - - $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); - - $versionId = null; - if ($version > count($versions)) { - $versionId = $file->getFileInfo()->getMtime(); - } else { - $fileVersion = array_values($versions)[$version - 1]; - - $versionId = $fileVersion->getRevisionId(); - } - - $changesFile = FileVersions::getChangesFile($owner->getUID(), $fileId, $versionId); - if ($changesFile === null) { - $this->logger->error("Download: changes $fileId ($version) was not found", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Files not found")], Http::STATUS_NOT_FOUND); - } - - $file = $changesFile; - } - - try { - $response = new DataDownloadResponse($file->getContent(), $file->getName(), $file->getMimeType()); - - if ($changes) { - $response = \OC_Response::setOptionsRequestHeaders($response); - } - - return $response; - } catch (NotPermittedException $e) { - $this->logger->logException($e, ["message" => "Download Not permitted: $fileId ($version)", "app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Not permitted")], Http::STATUS_FORBIDDEN); - } - return new JSONResponse(["message" => $this->trans->t("Download failed")], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - /** - * Downloading empty file by the document service - * - * @param string $doc - verification token with the file identifier - * - * @return DataDownloadResponse|JSONResponse - * - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - * @CORS - */ - public function emptyfile($doc) { - $this->logger->debug("Download empty", ["app" => $this->appName]); - - list ($hashData, $error) = $this->crypt->ReadHash($doc); - if ($hashData === null) { - $this->logger->error("Download empty with empty or not correct hash: $error", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); - } - if ($hashData->action !== "empty") { - $this->logger->error("Download empty with other action", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST); - } - - if (!empty($this->config->GetDocumentServerSecret())) { - $header = \OC::$server->getRequest()->getHeader($this->config->JwtHeader()); - if (empty($header)) { - $this->logger->error("Download empty without jwt", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); - } - - $header = substr($header, strlen("Bearer ")); - - try { - $decodedHeader = \Firebase\JWT\JWT::decode($header, new \Firebase\JWT\Key($this->config->GetDocumentServerSecret(), "HS256")); - } catch (\UnexpectedValueException $e) { - $this->logger->logException($e, ["message" => "Download empty with invalid jwt", "app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); - } - } - - $templatePath = TemplateManager::GetEmptyTemplatePath("en", ".docx"); - - $template = file_get_contents($templatePath); - if (!$template) { - $this->logger->info("Template for download empty not found: $templatePath", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND); - } - - try { - return new DataDownloadResponse($template, "new.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); - } catch (NotPermittedException $e) { - $this->logger->logException($e, ["message" => "Download Not permitted", "app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Not permitted")], Http::STATUS_FORBIDDEN); - } - return new JSONResponse(["message" => $this->trans->t("Download failed")], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - /** - * Handle request from the document server with the document status information - * - * @param string $doc - verification token with the file identifier - * @param array $users - the list of the identifiers of the users - * @param string $key - the edited document identifier - * @param integer $status - the edited status - * @param string $url - the link to the edited document to be saved - * @param string $token - request signature - * @param array $history - file history - * @param string $changesurl - link to file changes - * @param integer $forcesavetype - the type of force save action - * @param array $actions - the array of action - * @param string $filetype - extension of the document that is downloaded from the link specified with the url parameter - * - * @return array - * - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - * @CORS - */ - public function track($doc, $users, $key, $status, $url, $token, $history, $changesurl, $forcesavetype, $actions, $filetype) { - - list ($hashData, $error) = $this->crypt->ReadHash($doc); - if ($hashData === null) { - $this->logger->error("Track with empty or not correct hash: $error", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); - } - if ($hashData->action !== "track") { - $this->logger->error("Track with other action", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST); - } - - $fileId = $hashData->fileId; - $this->logger->debug("Track: $fileId status $status", ["app" => $this->appName]); - - if (!empty($this->config->GetDocumentServerSecret())) { - if (!empty($token)) { - try { - $payload = \Firebase\JWT\JWT::decode($token, new \Firebase\JWT\Key($this->config->GetDocumentServerSecret(), "HS256")); - } catch (\UnexpectedValueException $e) { - $this->logger->logException($e, ["message" => "Track with invalid jwt in body", "app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); - } - } else { - $header = \OC::$server->getRequest()->getHeader($this->config->JwtHeader()); - if (empty($header)) { - $this->logger->error("Track without jwt", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); - } - - $header = substr($header, strlen("Bearer ")); - - try { - $decodedHeader = \Firebase\JWT\JWT::decode($header, new \Firebase\JWT\Key($this->config->GetDocumentServerSecret(), "HS256")); - - $payload = $decodedHeader->payload; - } catch (\UnexpectedValueException $e) { - $this->logger->logException($e, ["message" => "Track with invalid jwt", "app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); - } - } - - $users = isset($payload->users) ? $payload->users : null; - $key = $payload->key; - $status = $payload->status; - $url = isset($payload->url) ? $payload->url : null; - } - - $result = 1; - switch ($status) { - case self::TrackerStatus_MustSave: - case self::TrackerStatus_Corrupted: - case self::TrackerStatus_ForceSave: - case self::TrackerStatus_CorruptedForceSave: - if (empty($url)) { - $this->logger->error("Track without url: $fileId status $status", ["app" => $this->appName]); - return new JSONResponse(["message" => "Url not found"], Http::STATUS_BAD_REQUEST); - } - - try { - $shareToken = isset($hashData->shareToken) ? $hashData->shareToken : null; - $filePath = null; - - \OC_Util::tearDownFS(); - - $isForcesave = $status === self::TrackerStatus_ForceSave || $status === self::TrackerStatus_CorruptedForceSave; - - // author of the latest changes - $userId = $this->parseUserId($users[0]); - - if ($isForcesave - && $forcesavetype === 1 - && !empty($actions)) { - // the user who clicked Save - $userId = $this->parseUserId($actions[0]["userid"]); - } - - $user = $this->userManager->get($userId); - if (!empty($user)) { - \OC_User::setUserId($userId); - } else { - if (empty($shareToken)) { - $this->logger->error("Track without token: $fileId status $status", ["app" => $this->appName]); - return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); - } - - $this->logger->debug("Track $fileId by token for $userId", ["app" => $this->appName]); - } - - // owner of file from the callback link - $ownerId = $hashData->ownerId; - $owner = $this->userManager->get($ownerId); - - if (!empty($owner)) { - $userId = $ownerId; - } else { - $callbackUserId = $hashData->userId; - $callbackUser = $this->userManager->get($callbackUserId); - - if (!empty($callbackUser)) { - // author of the callback link - $userId = $callbackUserId; - - // path for author of the callback link - $filePath = $hashData->filePath; - } - } - - if ($this->config->checkEncryptionModule() === "master") { - \OC_User::setIncognitoMode(true); - } else if (!empty($userId)) { - \OC_Util::setupFS($userId); - } - - list ($file, $error) = empty($shareToken) ? $this->getFile($userId, $fileId, $filePath) : $this->getFileByToken($fileId, $shareToken); - - if (isset($error)) { - $this->logger->error("track error: $fileId " . json_encode($error->getData()), ["app" => $this->appName]); - return $error; - } - - $url = $this->config->ReplaceDocumentServerUrlToInternal($url); - - $prevVersion = $file->getFileInfo()->getMtime(); - $fileName = $file->getName(); - $curExt = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); - $downloadExt = $filetype; - - $documentService = new DocumentService($this->trans, $this->config); - if ($downloadExt !== $curExt) { - $key = DocumentService::GenerateRevisionId($fileId . $url); - - try { - $this->logger->debug("Converted from $downloadExt to $curExt", ["app" => $this->appName]); - $url = $documentService->GetConvertedUri($url, $downloadExt, $curExt, $key); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "Converted on save error", "app" => $this->appName]); - return new JSONResponse(["message" => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR); - } - } - - $newData = $documentService->Request($url); - - $prevIsForcesave = KeyManager::wasForcesave($fileId); - - if (RemoteInstance::isRemoteFile($file)) { - $isLock = RemoteInstance::lockRemoteKey($file, $isForcesave, null); - if ($isForcesave && !$isLock) { - break; - } - } else { - KeyManager::lock($fileId, $isForcesave); - } - - $this->logger->debug("Track put content " . $file->getPath(), ["app" => $this->appName]); - $this->retryOperation(function () use ($file, $newData) { - return $file->putContent($newData); - }); - - if (RemoteInstance::isRemoteFile($file)) { - if ($isForcesave) { - RemoteInstance::lockRemoteKey($file, false, $isForcesave); - } - } else { - KeyManager::lock($fileId, false); - KeyManager::setForcesave($fileId, $isForcesave); - } - - if (!$isForcesave - && !$prevIsForcesave - && $this->versionManager->available - && $this->config->GetVersionHistory()) { - $changes = null; - if (!empty($changesurl)) { - $changesurl = $this->config->ReplaceDocumentServerUrlToInternal($changesurl); - $changes = $documentService->Request($changesurl); - } - FileVersions::saveHistory($file->getFileInfo(), $history, $changes, $prevVersion); - } - - if (!empty($user) && $this->config->GetVersionHistory()) { - FileVersions::saveAuthor($file->getFileInfo(), $user); - } - - if ($this->config->checkEncryptionModule() === "master" - && !$isForcesave) { - KeyManager::delete($fileId); - } - - $result = 0; - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "Track: $fileId status $status error", "app" => $this->appName]); - } - break; - - case self::TrackerStatus_Editing: - case self::TrackerStatus_Closed: - $result = 0; - break; - } - - $this->logger->debug("Track: $fileId status $status result $result", ["app" => $this->appName]); - - return new JSONResponse(["error" => $result], Http::STATUS_OK); - } - - - /** - * Getting file by identifier - * - * @param string $userId - user identifier - * @param integer $fileId - file identifier - * @param string $filePath - file path - * @param integer $version - file version - * @param bool $template - file is template - * - * @return array - */ - private function getFile($userId, $fileId, $filePath = null, $version = 0, $template = false) { - if (empty($fileId)) { - return [null, new JSONResponse(["message" => $this->trans->t("FileId is empty")], Http::STATUS_BAD_REQUEST)]; - } - - try { - $folder = !$template ? $this->root->getUserFolder($userId) : TemplateManager::GetGlobalTemplateDir(); - $files = $folder->getById($fileId); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "getFile: $fileId", "app" => $this->appName]); - return [null, new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST)]; - } - - if (empty($files)) { - $this->logger->error("Files not found: $fileId", ["app" => $this->appName]); - return [null, new JSONResponse(["message" => $this->trans->t("Files not found")], Http::STATUS_NOT_FOUND)]; - } - - $file = $files[0]; - - if (count($files) > 1 && !empty($filePath)) { - $filePath = "/" . $userId . "/files" . $filePath; - foreach ($files as $curFile) { - if ($curFile->getPath() === $filePath) { - $file = $curFile; - break; - } - } - } - - if (!($file instanceof File)) { - $this->logger->error("File not found: $fileId", ["app" => $this->appName]); - return [null, new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND)]; - } - - if ($version > 0 && $this->versionManager->available) { - $owner = $file->getFileInfo()->getOwner(); - - if ($owner !== null) { - if ($owner->getUID() !== $userId) { - list ($file, $error) = $this->getFile($owner->getUID(), $file->getId()); - - if (isset($error)) { - return [null, $error]; - } - } - - $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); - - if ($version <= count($versions)) { - $fileVersion = array_values($versions)[$version - 1]; - $file = $this->versionManager->getVersionFile($owner, $file->getFileInfo(), $fileVersion->getRevisionId()); - } - } - } - - return [$file, null]; - } - - /** - * Getting file by token - * - * @param integer $fileId - file identifier - * @param string $shareToken - access token - * @param integer $version - file version - * - * @return array - */ - private function getFileByToken($fileId, $shareToken, $version = 0) { - list ($share, $error) = $this->getShare($shareToken); - - if (isset($error)) { - return [null, $error]; - } - - try { - $node = $share->getNode(); - } catch (NotFoundException $e) { - $this->logger->logException($e, ["message" => "getFileByToken error", "app" => $this->appName]); - return [null, new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND)]; - } - - if ($node instanceof Folder) { - try { - $files = $node->getById($fileId); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "getFileByToken: $fileId", "app" => $this->appName]); - return [null, new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_NOT_FOUND)]; - } - - if (empty($files)) { - return [null, new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND)]; - } - $file = $files[0]; - } else { - $file = $node; - } - - if ($version > 0 && $this->versionManager->available) { - $owner = $file->getFileInfo()->getOwner(); - - if ($owner !== null) { - $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); - - if ($version <= count($versions)) { - $fileVersion = array_values($versions)[$version - 1]; - $file = $this->versionManager->getVersionFile($owner, $file->getFileInfo(), $fileVersion->getRevisionId()); - } - } - } - - return [$file, null]; - } - - /** - * Getting share by token - * - * @param string $shareToken - access token - * - * @return array - */ - private function getShare($shareToken) { - if (empty($shareToken)) { - return [null, new JSONResponse(["message" => $this->trans->t("FileId is empty")], Http::STATUS_BAD_REQUEST)]; - } - - $share = null; - try { - $share = $this->shareManager->getShareByToken($shareToken); - } catch (ShareNotFound $e) { - $this->logger->logException($e, ["message" => "getShare error", "app" => $this->appName]); - $share = null; - } - - if ($share === null || $share === false) { - return [null, new JSONResponse(["message" => $this->trans->t("You do not have enough permissions to view the file")], Http::STATUS_FORBIDDEN)]; - } - - return [$share, null]; - } - - /** - * Parse user identifier for current instance - * - * @param string $userId - unique user identifier - * - * @return string - */ - private function parseUserId($userId) { - $instanceId = $this->config->GetSystemValue("instanceid", true); - $instanceId = $instanceId . "_"; - - if (substr($userId, 0, strlen($instanceId)) === $instanceId) { - return substr($userId, strlen($instanceId)); - } - - return $userId; - } - - /** - * Retry operation if a LockedException occurred - * Other exceptions will still be thrown - * - * @param callable $operation - * - * @throws LockedException - */ - private function retryOperation(callable $operation) { - $i = 0; - while (true) { - try { - return $operation(); - } catch (LockedException $e) { - if (++$i === 4) { - throw $e; - } - } - usleep(500000); - } - } + /** + * Root folder + * + * @var IRootFolder + */ + private $root; + + /** + * User session + * + * @var IUserSession + */ + private $userSession; + + /** + * User manager + * + * @var IUserManager + */ + private $userManager; + + /** + * l10n service + * + * @var IL10N + */ + private $trans; + + /** + * Logger + * + * @var OCP\ILogger + */ + private $logger; + + /** + * Application configuration + * + * @var AppConfig + */ + private $config; + + /** + * Hash generator + * + * @var Crypt + */ + private $crypt; + + /** + * Share manager + * + * @var IManager + */ + private $shareManager; + + /** + * File version manager + * + * @var VersionManager + */ + private $versionManager; + + /** + * Status of the document + */ + private const TRACKERSTATUS_EDITING = 1; + private const TRACKERSTATUS_MUSTSAVE = 2; + private const TRACKERSTATUS_CORRUPTED = 3; + private const TRACKERSTATUS_CLOSED = 4; + private const TRACKERSTATUS_FORCESAVE = 6; + private const TRACKERSTATUS_CORRUPTEDFORCESAVE = 7; + + /** + * @param string $AppName - application name + * @param IRequest $request - request object + * @param IRootFolder $root - root folder + * @param IUserSession $userSession - user session + * @param IUserManager $userManager - user manager + * @param IL10N $trans - l10n service + * @param ILogger $logger - logger + * @param AppConfig $config - application configuration + * @param Crypt $crypt - hash generator + * @param IManager $shareManager - Share manager + */ + public function __construct( + $AppName, + IRequest $request, + IRootFolder $root, + IUserSession $userSession, + IUserManager $userManager, + IL10N $trans, + ILogger $logger, + AppConfig $config, + Crypt $crypt, + IManager $shareManager + ) { + parent::__construct($AppName, $request); + + $this->root = $root; + $this->userSession = $userSession; + $this->userManager = $userManager; + $this->trans = $trans; + $this->logger = $logger; + $this->config = $config; + $this->crypt = $crypt; + $this->shareManager = $shareManager; + + $this->versionManager = new VersionManager($AppName, $root); + } + + /** + * Downloading file by the document service + * + * @param string $doc - verification token with the file identifier + * + * @return DataDownloadResponse|JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * @CORS + */ + public function download($doc) { + list($hashData, $error) = $this->crypt->readHash($doc); + if ($hashData === null) { + $this->logger->error("Download with empty or not correct hash: $error", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); + } + if ($hashData->action !== "download") { + $this->logger->error("Download with other action", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST); + } + + $fileId = $hashData->fileId; + $version = isset($hashData->version) ? $hashData->version : null; + $changes = isset($hashData->changes) ? $hashData->changes : false; + $template = isset($hashData->template) ? $hashData->template : false; + $this->logger->debug("Download: $fileId ($version)" . ($changes ? " changes" : ""), ["app" => $this->appName]); + + if (!$this->userSession->isLoggedIn() + && !$changes + ) { + if (!empty($this->config->getDocumentServerSecret())) { + $header = \OC::$server->getRequest()->getHeader($this->config->jwtHeader()); + if (empty($header)) { + $this->logger->error("Download without jwt", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); + } + + $header = substr($header, \strlen("Bearer ")); + + try { + $decodedHeader = \Firebase\JWT\JWT::decode($header, new \Firebase\JWT\Key($this->config->getDocumentServerSecret(), "HS256")); + } catch (\UnexpectedValueException $e) { + $this->logger->logException($e, ["message" => "Download with invalid jwt", "app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); + } + } + } + + $userId = null; + + $user = null; + if ($this->userSession->isLoggedIn()) { + $user = $this->userSession->getUser(); + $userId = $user->getUID(); + } else { + \OC_Util::tearDownFS(); + + if (isset($hashData->userId)) { + $userId = $hashData->userId; + + $user = $this->userManager->get($userId); + if (!empty($user)) { + \OC_User::setUserId($userId); + } + + if ($this->config->checkEncryptionModule() === "master") { + \OC_User::setIncognitoMode(true); + } else { + if (!empty($user)) { + \OC_Util::setupFS($userId); + } + } + } + } + + $shareToken = isset($hashData->shareToken) ? $hashData->shareToken : null; + list($file, $error, $share) = empty($shareToken) ? $this->getFile($userId, $fileId, null, $changes ? null : $version, $template) : $this->getFileByToken($fileId, $shareToken, $changes ? null : $version); + + if (isset($error)) { + return $error; + } + + $canDownload = true; + + $fileStorage = $file->getStorage(); + if ($fileStorage->instanceOfStorage("\OCA\Files_Sharing\SharedStorage") || !empty($shareToken)) { + $share = empty($share) ? $fileStorage->getShare() : $share; + $canDownload = FileUtility::canShareDownload($share); + if (!$canDownload && !empty($this->config->getDocumentServerSecret())) { + $canDownload = true; + } + } + + if ($this->userSession->isLoggedIn() && !$file->isReadable() || !$canDownload) { + $this->logger->error("Download without access right", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); + } + + if (empty($user) + && $this->config->checkEncryptionModule() !== "master" + ) { + $owner = $file->getFileInfo()->getOwner(); + if ($owner !== null) { + \OC_Util::setupFS($owner->getUID()); + } + } + + if ($changes) { + if ($this->versionManager->available !== true) { + $this->logger->error("Download changes: versionManager is null", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST); + } + + $owner = $file->getFileInfo()->getOwner(); + if ($owner === null) { + $this->logger->error("Download: changes owner of $fileId was not found", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Files not found")], Http::STATUS_NOT_FOUND); + } + + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + + $versionId = null; + if ($version > \count($versions)) { + $versionId = $file->getFileInfo()->getMtime(); + } else { + $fileVersion = array_values($versions)[$version - 1]; + + $versionId = $fileVersion->getRevisionId(); + } + + $changesFile = FileVersions::getChangesFile($owner->getUID(), $fileId, $versionId); + if ($changesFile === null) { + $this->logger->error("Download: changes $fileId ($version) was not found", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Files not found")], Http::STATUS_NOT_FOUND); + } + + $file = $changesFile; + } + + try { + $response = new DataDownloadResponse($file->getContent(), $file->getName(), $file->getMimeType()); + + if ($changes) { + $response = \OC_Response::setOptionsRequestHeaders($response); + } + + return $response; + } catch (NotPermittedException $e) { + $this->logger->logException($e, ["message" => "Download Not permitted: $fileId ($version)", "app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Not permitted")], Http::STATUS_FORBIDDEN); + } + return new JSONResponse(["message" => $this->trans->t("Download failed")], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + /** + * Downloading empty file by the document service + * + * @param string $doc - verification token with the file identifier + * + * @return DataDownloadResponse|JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * @CORS + */ + public function emptyfile($doc) { + $this->logger->debug("Download empty", ["app" => $this->appName]); + + list($hashData, $error) = $this->crypt->readHash($doc); + if ($hashData === null) { + $this->logger->error("Download empty with empty or not correct hash: $error", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); + } + if ($hashData->action !== "empty") { + $this->logger->error("Download empty with other action", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST); + } + + if (!empty($this->config->getDocumentServerSecret())) { + $header = \OC::$server->getRequest()->getHeader($this->config->jwtHeader()); + if (empty($header)) { + $this->logger->error("Download empty without jwt", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); + } + + $header = substr($header, \strlen("Bearer ")); + + try { + $decodedHeader = \Firebase\JWT\JWT::decode($header, new \Firebase\JWT\Key($this->config->getDocumentServerSecret(), "HS256")); + } catch (\UnexpectedValueException $e) { + $this->logger->logException($e, ["message" => "Download empty with invalid jwt", "app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); + } + } + + $templatePath = TemplateManager::getEmptyTemplatePath("en", ".docx"); + + $template = file_get_contents($templatePath); + if (!$template) { + $this->logger->info("Template for download empty not found: $templatePath", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND); + } + + try { + return new DataDownloadResponse($template, "new.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + } catch (NotPermittedException $e) { + $this->logger->logException($e, ["message" => "Download Not permitted", "app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Not permitted")], Http::STATUS_FORBIDDEN); + } + return new JSONResponse(["message" => $this->trans->t("Download failed")], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + /** + * Handle request from the document server with the document status information + * + * @param string $doc - verification token with the file identifier + * @param array $users - the list of the identifiers of the users + * @param string $key - the edited document identifier + * @param integer $status - the edited status + * @param string $url - the link to the edited document to be saved + * @param string $token - request signature + * @param array $history - file history + * @param string $changesurl - link to file changes + * @param integer $forcesavetype - the type of force save action + * @param array $actions - the array of action + * @param string $filetype - extension of the document that is downloaded from the link specified with the url parameter + * + * @return array + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * @CORS + */ + public function track($doc, $users, $key, $status, $url, $token, $history, $changesurl, $forcesavetype, $actions, $filetype) { + list($hashData, $error) = $this->crypt->readHash($doc); + if ($hashData === null) { + $this->logger->error("Track with empty or not correct hash: $error", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); + } + if ($hashData->action !== "track") { + $this->logger->error("Track with other action", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST); + } + + $fileId = $hashData->fileId; + $this->logger->debug("Track: $fileId status $status", ["app" => $this->appName]); + + if (!empty($this->config->getDocumentServerSecret())) { + if (!empty($token)) { + try { + $payload = \Firebase\JWT\JWT::decode($token, new \Firebase\JWT\Key($this->config->getDocumentServerSecret(), "HS256")); + } catch (\UnexpectedValueException $e) { + $this->logger->logException($e, ["message" => "Track with invalid jwt in body", "app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); + } + } else { + $header = \OC::$server->getRequest()->getHeader($this->config->jwtHeader()); + if (empty($header)) { + $this->logger->error("Track without jwt", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); + } + + $header = substr($header, \strlen("Bearer ")); + + try { + $decodedHeader = \Firebase\JWT\JWT::decode($header, new \Firebase\JWT\Key($this->config->getDocumentServerSecret(), "HS256")); + + $payload = $decodedHeader->payload; + } catch (\UnexpectedValueException $e) { + $this->logger->logException($e, ["message" => "Track with invalid jwt", "app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); + } + } + + $users = isset($payload->users) ? $payload->users : null; + $key = $payload->key; + $status = $payload->status; + $url = isset($payload->url) ? $payload->url : null; + } + + $result = 1; + switch ($status) { + case self::TRACKERSTATUS_MUSTSAVE: + case self::TRACKERSTATUS_CORRUPTED: + case self::TRACKERSTATUS_FORCESAVE: + case self::TRACKERSTATUS_CORRUPTEDFORCESAVE: + if (empty($url)) { + $this->logger->error("Track without url: $fileId status $status", ["app" => $this->appName]); + return new JSONResponse(["message" => "Url not found"], Http::STATUS_BAD_REQUEST); + } + + try { + $shareToken = isset($hashData->shareToken) ? $hashData->shareToken : null; + $filePath = null; + + \OC_Util::tearDownFS(); + + $isForcesave = $status === self::TRACKERSTATUS_FORCESAVE || $status === self::TRACKERSTATUS_CORRUPTEDFORCESAVE; + + // author of the latest changes + $userId = $this->parseUserId($users[0]); + + if ($isForcesave + && $forcesavetype === 1 + && !empty($actions) + ) { + // the user who clicked Save + $userId = $this->parseUserId($actions[0]["userid"]); + } + + $user = $this->userManager->get($userId); + if (!empty($user)) { + \OC_User::setUserId($userId); + } else { + if (empty($shareToken)) { + $this->logger->error("Track without token: $fileId status $status", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); + } + + $this->logger->debug("Track $fileId by token for $userId", ["app" => $this->appName]); + } + + // owner of file from the callback link + $ownerId = $hashData->ownerId; + $owner = $this->userManager->get($ownerId); + + if (!empty($owner)) { + $userId = $ownerId; + } else { + $callbackUserId = $hashData->userId; + $callbackUser = $this->userManager->get($callbackUserId); + + if (!empty($callbackUser)) { + // author of the callback link + $userId = $callbackUserId; + + // path for author of the callback link + $filePath = $hashData->filePath; + } + } + + if ($this->config->checkEncryptionModule() === "master") { + \OC_User::setIncognitoMode(true); + } elseif (!empty($userId)) { + \OC_Util::setupFS($userId); + } + + list($file, $error, $share) = empty($shareToken) ? $this->getFile($userId, $fileId, $filePath) : $this->getFileByToken($fileId, $shareToken); + + if (isset($error)) { + $this->logger->error("track error: $fileId " . json_encode($error->getData()), ["app" => $this->appName]); + return $error; + } + + $url = $this->config->replaceDocumentServerUrlToInternal($url); + + $prevVersion = $file->getFileInfo()->getMtime(); + $fileName = $file->getName(); + $curExt = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $downloadExt = $filetype; + + $documentService = new DocumentService($this->trans, $this->config); + if ($downloadExt !== $curExt) { + $key = DocumentService::generateRevisionId($fileId . $url); + + try { + $this->logger->debug("Converted from $downloadExt to $curExt", ["app" => $this->appName]); + $url = $documentService->getConvertedUri($url, $downloadExt, $curExt, $key); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "Converted on save error", "app" => $this->appName]); + return new JSONResponse(["message" => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + $newData = $documentService->request($url); + + $prevIsForcesave = KeyManager::wasForcesave($fileId); + + if (RemoteInstance::isRemoteFile($file)) { + $isLock = RemoteInstance::lockRemoteKey($file, $isForcesave, null); + if ($isForcesave && !$isLock) { + break; + } + } else { + KeyManager::lock($fileId, $isForcesave); + } + + $this->logger->debug("Track put content " . $file->getPath(), ["app" => $this->appName]); + $this->retryOperation( + function () use ($file, $newData) { + return $file->putContent($newData); + } + ); + + if (RemoteInstance::isRemoteFile($file)) { + if ($isForcesave) { + RemoteInstance::lockRemoteKey($file, false, $isForcesave); + } + } else { + KeyManager::lock($fileId, false); + KeyManager::setForcesave($fileId, $isForcesave); + } + + if (!$isForcesave + && !$prevIsForcesave + && $this->versionManager->available + && $this->config->getVersionHistory() + ) { + $changes = null; + if (!empty($changesurl)) { + $changesurl = $this->config->replaceDocumentServerUrlToInternal($changesurl); + $changes = $documentService->request($changesurl); + } + FileVersions::saveHistory($file->getFileInfo(), $history, $changes, $prevVersion); + } + + if (!empty($user) && $this->config->getVersionHistory()) { + FileVersions::saveAuthor($file->getFileInfo(), $user); + } + + if ($this->config->checkEncryptionModule() === "master" + && !$isForcesave + ) { + KeyManager::delete($fileId); + } + + $result = 0; + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "Track: $fileId status $status error", "app" => $this->appName]); + } + break; + + case self::TRACKERSTATUS_EDITING: + case self::TRACKERSTATUS_CLOSED: + $result = 0; + break; + } + + $this->logger->debug("Track: $fileId status $status result $result", ["app" => $this->appName]); + + return new JSONResponse(["error" => $result], Http::STATUS_OK); + } + + /** + * Getting file by identifier + * + * @param string $userId - user identifier + * @param integer $fileId - file identifier + * @param string $filePath - file path + * @param integer $version - file version + * @param bool $template - file is template + * + * @return array + */ + private function getFile($userId, $fileId, $filePath = null, $version = 0, $template = false) { + if (empty($fileId)) { + return [null, new JSONResponse(["message" => $this->trans->t("FileId is empty")], Http::STATUS_BAD_REQUEST), null]; + } + + try { + $folder = !$template ? $this->root->getUserFolder($userId) : TemplateManager::getGlobalTemplateDir(); + $files = $folder->getById($fileId); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "getFile: $fileId", "app" => $this->appName]); + return [null, new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST), null]; + } + + if (empty($files)) { + $this->logger->error("Files not found: $fileId", ["app" => $this->appName]); + return [null, new JSONResponse(["message" => $this->trans->t("Files not found")], Http::STATUS_NOT_FOUND), null]; + } + + $file = $files[0]; + + if (\count($files) > 1 && !empty($filePath)) { + $filePath = "/" . $userId . "/files" . $filePath; + foreach ($files as $curFile) { + if ($curFile->getPath() === $filePath) { + $file = $curFile; + break; + } + } + } + + if (!($file instanceof File)) { + $this->logger->error("File not found: $fileId", ["app" => $this->appName]); + return [null, new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND)]; + } + + if ($version > 0 && $this->versionManager->available) { + $owner = $file->getFileInfo()->getOwner(); + + if ($owner !== null) { + if ($owner->getUID() !== $userId) { + list($file, $error, $share) = $this->getFile($owner->getUID(), $file->getId()); + + if (isset($error)) { + return [null, $error, null]; + } + } + + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + + if ($version <= \count($versions)) { + $fileVersion = array_values($versions)[$version - 1]; + $file = $this->versionManager->getVersionFile($owner, $file->getFileInfo(), $fileVersion->getRevisionId()); + } + } + } + + return [$file, null, null]; + } + + /** + * Getting file by token + * + * @param integer $fileId - file identifier + * @param string $shareToken - access token + * @param integer $version - file version + * + * @return array + */ + private function getFileByToken($fileId, $shareToken, $version = 0) { + list($share, $error) = $this->getShare($shareToken); + + if (isset($error)) { + return [null, $error, null]; + } + + try { + $node = $share->getNode(); + } catch (NotFoundException $e) { + $this->logger->logException($e, ["message" => "getFileByToken error", "app" => $this->appName]); + return [null, new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND), null]; + } + + if ($node instanceof Folder) { + try { + $files = $node->getById($fileId); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "getFileByToken: $fileId", "app" => $this->appName]); + return [null, new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_NOT_FOUND), null]; + } + + if (empty($files)) { + return [null, new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND), null]; + } + $file = $files[0]; + } else { + $file = $node; + } + + if ($version > 0 && $this->versionManager->available) { + $owner = $file->getFileInfo()->getOwner(); + + if ($owner !== null) { + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + + if ($version <= \count($versions)) { + $fileVersion = array_values($versions)[$version - 1]; + $file = $this->versionManager->getVersionFile($owner, $file->getFileInfo(), $fileVersion->getRevisionId()); + } + } + } + + return [$file, null, $share]; + } + + /** + * Getting share by token + * + * @param string $shareToken - access token + * + * @return array + */ + private function getShare($shareToken) { + if (empty($shareToken)) { + return [null, new JSONResponse(["message" => $this->trans->t("FileId is empty")], Http::STATUS_BAD_REQUEST)]; + } + + $share = null; + try { + $share = $this->shareManager->getShareByToken($shareToken); + } catch (ShareNotFound $e) { + $this->logger->logException($e, ["message" => "getShare error", "app" => $this->appName]); + $share = null; + } + + if ($share === null || $share === false) { + return [null, new JSONResponse(["message" => $this->trans->t("You do not have enough permissions to view the file")], Http::STATUS_FORBIDDEN)]; + } + + return [$share, null]; + } + + /** + * Parse user identifier for current instance + * + * @param string $userId - unique user identifier + * + * @return string + */ + private function parseUserId($userId) { + $instanceId = $this->config->getSystemValue("instanceid", true); + $instanceId = $instanceId . "_"; + + if (substr($userId, 0, \strlen($instanceId)) === $instanceId) { + return substr($userId, \strlen($instanceId)); + } + + return $userId; + } + + /** + * Retry operation if a LockedException occurred + * Other exceptions will still be thrown + * + * @param callable $operation + * + * @throws LockedException + * + * @return void + */ + private function retryOperation(callable $operation) { + $i = 0; + while (true) { + try { + return $operation(); + } catch (LockedException $e) { + if (++$i === 4) { + throw $e; + } + } + usleep(500000); + } + } } diff --git a/controller/editorapicontroller.php b/controller/editorapicontroller.php index c836127c..1676cc5f 100644 --- a/controller/editorapicontroller.php +++ b/controller/editorapicontroller.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,728 +50,735 @@ * Controller with the main functions */ class EditorApiController extends OCSController { - - /** - * Current user session - * - * @var IUserSession - */ - private $userSession; - - /** - * Root folder - * - * @var IRootFolder - */ - private $root; - - /** - * Url generator service - * - * @var IURLGenerator - */ - private $urlGenerator; - - /** - * l10n service - * - * @var IL10N - */ - private $trans; - - /** - * Logger - * - * @var ILogger - */ - private $logger; - - /** - * Application configuration - * - * @var AppConfig - */ - private $config; - - /** - * Hash generator - * - * @var Crypt - */ - private $crypt; - - /** - * File utility - * - * @var FileUtility - */ - private $fileUtility; - - /** - * File version manager - * - * @var VersionManager - */ - private $versionManager; - - /** - * Tag manager - * - * @var ITagManager - */ - private $tagManager; - - /** - * Mobile regex from https://github.com/ONLYOFFICE/CommunityServer/blob/v9.1.1/web/studio/ASC.Web.Studio/web.appsettings.config#L35 - */ - const USER_AGENT_MOBILE = "/android|avantgo|playbook|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|symbian|treo|up\\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i"; - - /** - * @param string $AppName - application name - * @param IRequest $request - request object - * @param IRootFolder $root - root folder - * @param IUserSession $userSession - current user session - * @param IURLGenerator $urlGenerator - url generator service - * @param IL10N $trans - l10n service - * @param ILogger $logger - logger - * @param AppConfig $config - application configuration - * @param Crypt $crypt - hash generator - * @param IManager $shareManager - Share manager - * @param ISession $ISession - Session - * @param ITagManager $tagManager - Tag manager - */ - public function __construct($AppName, - IRequest $request, - IRootFolder $root, - IUserSession $userSession, - IURLGenerator $urlGenerator, - IL10N $trans, - ILogger $logger, - AppConfig $config, - Crypt $crypt, - IManager $shareManager, - ISession $session, - ITagManager $tagManager - ) { - parent::__construct($AppName, $request); - - $this->userSession = $userSession; - $this->root = $root; - $this->urlGenerator = $urlGenerator; - $this->trans = $trans; - $this->logger = $logger; - $this->config = $config; - $this->crypt = $crypt; - $this->tagManager = $tagManager; - - $this->versionManager = new VersionManager($AppName, $root); - - $this->fileUtility = new FileUtility($AppName, $trans, $logger, $config, $shareManager, $session); - } - - /** - * Filling empty file an template - * - * @param int $fileId - file identificator - * - * @return JSONResponse - * - * @NoAdminRequired - * @PublicPage - */ - public function fillempty($fileId) { - $this->logger->debug("Fill empty: $fileId", ["app" => $this->appName]); - - if (empty($fileId)) { - $this->logger->error("File for filling was not found: $fileId", ["app" => $this->appName]); - return new JSONResponse(["error" => $this->trans->t("FileId is empty")]); - } - - $userId = $this->userSession->getUser()->getUID(); - - list ($file, $error, $share) = $this->getFile($userId, $fileId); - if (isset($error)) { - $this->logger->error("Fill empty: $fileId $error", ["app" => $this->appName]); - return new JSONResponse(["error" => $error]); - } - - if ($file->getSize() > 0) { - $this->logger->error("File is't empty: $fileId", ["app" => $this->appName]); - return new JSONResponse(["error" => $this->trans->t("Not permitted")]); - } - - if (!$file->isUpdateable()) { - $this->logger->error("File without permission: $fileId", ["app" => $this->appName]); - return new JSONResponse(["error" => $this->trans->t("Not permitted")]); - } - - $name = $file->getName(); - $template = TemplateManager::GetEmptyTemplate($name); - - if (!$template) { - $this->logger->error("Template for file filling not found: $name ($fileId)", ["app" => $this->appName]); - return new JSONResponse(["error" => $this->trans->t("Template not found")]); - } - - try { - $file->putContent($template); - } catch (NotPermittedException $e) { - $this->logger->logException($e, ["message" => "Can't put file: $name", "app" => $this->appName]); - return new JSONResponse(["error" => $this->trans->t("Can't create file")]); - } - - return new JSONResponse([ - ]); - } - - /** - * Collecting the file parameters for the document service - * - * @param integer $fileId - file identifier - * @param string $filePath - file path - * @param string $shareToken - access token - * @param integer $version - file version - * @param bool $inframe - open in frame - * @param bool $desktop - desktop label - * @param bool $template - file is template - * - * @return JSONResponse - * - * @NoAdminRequired - * @PublicPage - * @CORS - */ - public function config($fileId, $filePath = null, $shareToken = null, $version = 0, $inframe = false, $desktop = false, $template = false, $anchor = null) { - - $user = $this->userSession->getUser(); - $userId = null; - $accountId = null; - if (!empty($user)) { - $userId = $user->getUID(); - $accountId = $user->getAccountId(); - } - - list ($file, $error, $share) = empty($shareToken) ? $this->getFile($userId, $fileId, $filePath, $template) : $this->fileUtility->getFileByToken($fileId, $shareToken); - - if (isset($error)) { - $this->logger->error("Config: $fileId $error", ["app" => $this->appName]); - return new JSONResponse(["error" => $error]); - } - - $checkUserAllowGroups = $userId; - if (!empty($share)) { - $checkUserAllowGroups = $share->getSharedBy(); - } - if (!$this->config->isUserAllowedToUse($checkUserAllowGroups)) { - return new JSONResponse(["error" => $this->trans->t("Not permitted")]); - } - - $fileName = $file->getName(); - $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); - $format = !empty($ext) && array_key_exists($ext, $this->config->FormatsSetting()) ? $this->config->FormatsSetting()[$ext] : null; - if (!isset($format)) { - $this->logger->info("Format is not supported for editing: $fileName", ["app" => $this->appName]); - return new JSONResponse(["error" => $this->trans->t("Format is not supported")]); - } - - $fileUrl = $this->getUrl($file, $user, $shareToken, $version, null, $template); - - $key = null; - if ($version > 0 - && $this->versionManager->available) { - $owner = $file->getFileInfo()->getOwner(); - if ($owner !== null) { - $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); - - if ($version <= count($versions)) { - $fileVersion = array_values($versions)[$version - 1]; - - $key = $this->fileUtility->getVersionKey($fileVersion); - } - } - } - if ($key === null) { - $key = $this->fileUtility->getKey($file, true); - } - $key = DocumentService::GenerateRevisionId($key); - - $params = [ - "document" => [ - "fileType" => $ext, - "key" => $key, - "permissions" => [], - "title" => $fileName, - "url" => $fileUrl, - "referenceData" => [ - "fileKey" => $file->getId(), - "instanceId" => $this->config->GetSystemValue("instanceid", true), - ], - ], - "documentType" => $format["type"], - "editorConfig" => [ - "lang" => str_replace("_", "-", \OC::$server->getL10NFactory("")->get("")->getLanguageCode()) - ] - ]; - - $restrictedEditing = false; - $fileStorage = $file->getStorage(); - if (empty($shareToken) && $fileStorage->instanceOfStorage("\OCA\Files_Sharing\SharedStorage")) { - $storageShare = $fileStorage->getShare(); - if (method_exists($storageShare, "getAttributes")) - { - $attributes = $storageShare->getAttributes(); - - $permissionsDownload = $attributes->getAttribute("permissions", "download"); - if ($permissionsDownload !== null) { - $params["document"]["permissions"]["download"] = $params["document"]["permissions"]["print"] = $params["document"]["permissions"]["copy"] = $permissionsDownload === true; - } - - if (isset($format["review"]) && $format["review"]) { - $permissionsReviewOnly = $attributes->getAttribute($this->appName, "review"); - if ($permissionsReviewOnly !== null && $permissionsReviewOnly === true) { - $restrictedEditing = true; - $params["document"]["permissions"]["review"] = true; - } - } - - if (isset($format["fillForms"]) && $format["fillForms"]) { - $permissionsFillFormsOnly = $attributes->getAttribute($this->appName, "fillForms"); - if ($permissionsFillFormsOnly !== null && $permissionsFillFormsOnly === true) { - $restrictedEditing = true; - $params["document"]["permissions"]["fillForms"] = true; - } - } - - if (isset($format["comment"]) && $format["comment"]) { - $permissionsCommentOnly = $attributes->getAttribute($this->appName, "comment"); - if ($permissionsCommentOnly !== null && $permissionsCommentOnly === true) { - $restrictedEditing = true; - $params["document"]["permissions"]["comment"] = true; - } - } - - if (isset($format["modifyFilter"]) && $format["modifyFilter"]) { - $permissionsModifyFilter = $attributes->getAttribute($this->appName, "modifyFilter"); - if ($permissionsModifyFilter !== null) { - $params["document"]["permissions"]["modifyFilter"] = $permissionsModifyFilter === true; - } - } - } - } - - $isPersistentLock = false; - if ($version < 1 - && (\OC::$server->getConfig()->getAppValue("files", "enable_lock_file_action", "no") === "yes") - && $fileStorage->instanceOfStorage(IPersistentLockingStorage::class)) { - - $locks = $fileStorage->getLocks($file->getFileInfo()->getInternalPath(), false); - if (count($locks) > 0) { - $activeLock = $locks[0]; - - if ($accountId !== $activeLock->getOwnerAccountId()) { - $isPersistentLock = true; - $lockOwner = $activeLock->getOwner(); - $this->logger->debug("File $fileId is locked by $lockOwner", ["app" => $this->appName]); - } - } - } - - $canEdit = isset($format["edit"]) && $format["edit"]; - $canFillForms = isset($format["fillForms"]) && $format["fillForms"]; - $editable = $version < 1 - && !$template - && $file->isUpdateable() - && !$isPersistentLock - && (empty($shareToken) || ($share->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE); - $params["document"]["permissions"]["edit"] = $editable; - if (($editable || $restrictedEditing) && $canEdit || $canFillForms) { - $ownerId = null; - $owner = $file->getOwner(); - if (!empty($owner)) { - $ownerId = $owner->getUID(); - } - - $canProtect = true; - if ($this->config->GetProtection() === "owner") { - $canProtect = $ownerId === $userId; - } - $params["document"]["permissions"]["protect"] = $canProtect; - - if (isset($shareToken)) { - $params["document"]["permissions"]["chat"] = false; - $params["document"]["permissions"]["protect"] = false; - } - - $hashCallback = $this->crypt->GetHash(["userId" => $userId, "ownerId" => $ownerId, "fileId" => $file->getId(), "filePath" => $filePath, "shareToken" => $shareToken, "action" => "track"]); - $callback = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".callback.track", ["doc" => $hashCallback]); - - if (!$this->config->UseDemo() && !empty($this->config->GetStorageUrl())) { - $callback = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->config->GetStorageUrl(), $callback); - } - - $params["editorConfig"]["callbackUrl"] = $callback; - } else { - $params["editorConfig"]["mode"] = "view"; - } - - if (\OC::$server->getRequest()->isUserAgent([$this::USER_AGENT_MOBILE])) { - $params["type"] = "mobile"; - } - - if (!$template - && $file->isUpdateable() - && !$isPersistentLock - && (empty($shareToken) || ($share->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE)) { - - $params["document"]["permissions"]["changeHistory"] = true; - } - - if (!empty($userId)) { - $params["editorConfig"]["user"] = [ - "id" => $this->buildUserId($userId), - "name" => $user->getDisplayName() - ]; - } - - $folderLink = null; - - if (!empty($shareToken)) { - $node = $share->getNode(); - if ($node instanceof Folder) { - $sharedFolder = $node; - $folderPath = $sharedFolder->getRelativePath($file->getParent()->getPath()); - if (!empty($folderPath)) { - $linkAttr = [ - "path" => $folderPath, - "scrollto" => $file->getName(), - "token" => $shareToken - ]; - $folderLink = $this->urlGenerator->linkToRouteAbsolute("files_sharing.sharecontroller.showShare", $linkAttr); - } - } - } else if (!empty($userId)) { - $userFolder = $this->root->getUserFolder($userId); - $folderPath = $userFolder->getRelativePath($file->getParent()->getPath()); - if (!empty($folderPath)) { - $linkAttr = [ - "dir" => $folderPath, - "scrollto" => $file->getName() - ]; - $folderLink = $this->urlGenerator->linkToRouteAbsolute("files.view.index", $linkAttr); - } - - switch ($params["documentType"]) { - case "word": - $createName = $this->trans->t("Document") . ".docx"; - break; - case "cell": - $createName = $this->trans->t("Spreadsheet") . ".xlsx"; - break; - case "slide": - $createName = $this->trans->t("Presentation") . ".pptx"; - break; - } - - $createParam = [ - "dir" => "/", - "name" => $createName - ]; - - if (!empty($folderPath)) { - $folder = $userFolder->get($folderPath); - if (!empty($folder) && $folder->isCreatable()) { - $createParam["dir"] = $folderPath; - } - } - - $createUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".editor.create_new", $createParam); - - $params["editorConfig"]["createUrl"] = urldecode($createUrl); - - $templatesList = TemplateManager::GetGlobalTemplates($file->getMimeType()); - if (!empty($templatesList)) { - $templates = []; - foreach($templatesList as $templateItem) { - $createParam["templateId"] = $templateItem->getId(); - $createParam["name"] = $templateItem->getName(); - - array_push($templates, [ - "image" => "", - "title" => $templateItem->getName(), - "url" => urldecode($this->urlGenerator->linkToRouteAbsolute($this->appName . ".editor.create_new", $createParam)) - ]); - } - - $params["editorConfig"]["templates"] = $templates; - } - - if (!$template) { - $params["document"]["info"]["favorite"] = $this->isFavorite($fileId); - } - $params["_file_path"] = $userFolder->getRelativePath($file->getPath()); - } - - if ($folderLink !== null - && $this->config->GetSystemValue($this->config->_customization_goback) !== false) { - $params["editorConfig"]["customization"]["goback"] = [ - "url" => $folderLink - ]; - - if (!$desktop) { - if ($this->config->GetSameTab()) { - $params["editorConfig"]["customization"]["goback"]["blank"] = false; - if ($inframe === true) { - $params["editorConfig"]["customization"]["goback"]["requestClose"] = true; - } - } - } - } - - if ($inframe === true) { - $params["_files_sharing"] = \OC::$server->getAppManager()->isEnabledForUser("files_sharing"); - } - - $params = $this->setCustomization($params); - - if ($this->config->UseDemo()) { - $params["editorConfig"]["tenant"] = $this->config->GetSystemValue("instanceid", true); - } - - if ($anchor !== null) { - try { - $actionLink = json_decode($anchor, true); - - $params["editorConfig"]["actionLink"] = $actionLink; - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "Config: $fileId decode $anchor", "app" => $this->appName]); - } - } - - if (!empty($this->config->GetDocumentServerSecret())) { - $token = \Firebase\JWT\JWT::encode($params, $this->config->GetDocumentServerSecret(), "HS256"); - $params["token"] = $token; - } - - $this->logger->debug("Config is generated for: $fileId ($version) with key $key", ["app" => $this->appName]); - - return new JSONResponse($params); - } - - /** - * Getting file by identifier - * - * @param string $userId - user identifier - * @param integer $fileId - file identifier - * @param string $filePath - file path - * @param bool $template - file is template - * - * @return array - */ - private function getFile($userId, $fileId, $filePath = null, $template = false) { - if (empty($fileId)) { - return [null, $this->trans->t("FileId is empty"), null]; - } - - try { - $folder = !$template ? $this->root->getUserFolder($userId) : TemplateManager::GetGlobalTemplateDir(); - $files = $folder->getById($fileId); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "getFile: $fileId", "app" => $this->appName]); - return [null, $this->trans->t("Invalid request"), null]; - } - - if (empty($files)) { - $this->logger->info("Files not found: $fileId", ["app" => $this->appName]); - return [null, $this->trans->t("File not found"), null]; - } - - $file = $files[0]; - - if (count($files) > 1 && !empty($filePath)) { - $filePath = "/" . $userId . "/files" . $filePath; - foreach ($files as $curFile) { - if ($curFile->getPath() === $filePath) { - $file = $curFile; - break; - } - } - } - - if (!$file->isReadable()) { - return [null, $this->trans->t("You do not have enough permissions to view the file"), null]; - } - - return [$file, null, null]; - } - - /** - * Generate secure link to download document - * - * @param File $file - file - * @param IUser $user - user with access - * @param string $shareToken - access token - * @param integer $version - file version - * @param bool $changes - is required url to file changes - * @param bool $template - file is template - * - * @return string - */ - private function getUrl($file, $user = null, $shareToken = null, $version = 0, $changes = false, $template = false) { - - $data = [ - "action" => "download", - "fileId" => $file->getId() - ]; - - $userId = null; - if (!empty($user)) { - $userId = $user->getUID(); - $data["userId"] = $userId; - } - if (!empty($shareToken)) { - $data["shareToken"] = $shareToken; - } - if ($version > 0) { - $data["version"] = $version; - } - if ($changes) { - $data["changes"] = true; - } - if ($template) { - $data["template"] = true; - } - - $hashUrl = $this->crypt->GetHash($data); - - $fileUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".callback.download", ["doc" => $hashUrl]); - - if (!$this->config->UseDemo() && !empty($this->config->GetStorageUrl())) { - $fileUrl = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->config->GetStorageUrl(), $fileUrl); - } - - return $fileUrl; - } - - /** - * Generate unique user identifier - * - * @param string $userId - current user identifier - * - * @return string - */ - private function buildUserId($userId) { - $instanceId = $this->config->GetSystemValue("instanceid", true); - $userId = $instanceId . "_" . $userId; - return $userId; - } - - /** - * Set customization parameters - * - * @param array params - file parameters - * - * @return array - */ - private function setCustomization($params) { - //default is true - if ($this->config->GetCustomizationChat() === false) { - $params["editorConfig"]["customization"]["chat"] = false; - } - - //default is false - if ($this->config->GetCustomizationCompactHeader() === true) { - $params["editorConfig"]["customization"]["compactHeader"] = true; - } - - //default is false - if ($this->config->GetCustomizationFeedback() === true) { - $params["editorConfig"]["customization"]["feedback"] = true; - } - - //default is false - if ($this->config->GetCustomizationForcesave() === true) { - $params["editorConfig"]["customization"]["forcesave"] = true; - } - - //default is true - if ($this->config->GetCustomizationHelp() === false) { - $params["editorConfig"]["customization"]["help"] = false; - } - - //default is original - $reviewDisplay = $this->config->GetCustomizationReviewDisplay(); - if ($reviewDisplay !== "original") { - $params["editorConfig"]["customization"]["reviewDisplay"] = $reviewDisplay; - } - - $theme = $this->config->GetCustomizationTheme(); - if (isset($theme)) { - $params["editorConfig"]["customization"]["uiTheme"] = $theme; - } - - //default is false - if ($this->config->GetCustomizationToolbarNoTabs() === true) { - $params["editorConfig"]["customization"]["toolbarNoTabs"] = true; - } - - //default is true - if($this->config->GetCustomizationMacros() === false) { - $params["editorConfig"]["customization"]["macros"] = false; - } - - //default is true - if($this->config->GetCustomizationPlugins() === false) { - $params["editorConfig"]["customization"]["plugins"] = false; - } - - /* from system config */ - - $autosave = $this->config->GetSystemValue($this->config->_customization_autosave); - if (isset($autosave)) { - $params["editorConfig"]["customization"]["autosave"] = $autosave; - } - - $customer = $this->config->GetSystemValue($this->config->_customization_customer); - if (isset($customer)) { - $params["editorConfig"]["customization"]["customer"] = $customer; - } - - $loaderLogo = $this->config->GetSystemValue($this->config->_customization_loaderLogo); - if (isset($loaderLogo)) { - $params["editorConfig"]["customization"]["loaderLogo"] = $loaderLogo; - } - - $loaderName = $this->config->GetSystemValue($this->config->_customization_loaderName); - if (isset($loaderName)) { - $params["editorConfig"]["customization"]["loaderName"] = $loaderName; - } - - $logo = $this->config->GetSystemValue($this->config->_customization_logo); - if (isset($logo)) { - $params["editorConfig"]["customization"]["logo"] = $logo; - } - - $zoom = $this->config->GetSystemValue($this->config->_customization_zoom); - if (isset($zoom)) { - $params["editorConfig"]["customization"]["zoom"] = $zoom; - } - - return $params; - } - - /** - * Check file favorite - * - * @param integer $fileId - file identifier - * - * @return bool - */ - private function isFavorite($fileId) { - $currentTags = $this->tagManager->load("files")->getTagsForObjects([$fileId]); - if ($currentTags) { - return in_array(Tags::TAG_FAVORITE, $currentTags[$fileId]); - } - - return false; - } + /** + * Current user session + * + * @var IUserSession + */ + private $userSession; + + /** + * Root folder + * + * @var IRootFolder + */ + private $root; + + /** + * Url generator service + * + * @var IURLGenerator + */ + private $urlGenerator; + + /** + * l10n service + * + * @var IL10N + */ + private $trans; + + /** + * Logger + * + * @var ILogger + */ + private $logger; + + /** + * Application configuration + * + * @var AppConfig + */ + private $config; + + /** + * Hash generator + * + * @var Crypt + */ + private $crypt; + + /** + * File utility + * + * @var FileUtility + */ + private $fileUtility; + + /** + * File version manager + * + * @var VersionManager + */ + private $versionManager; + + /** + * Tag manager + * + * @var ITagManager + */ + private $tagManager; + + /** + * Mobile regex from https://github.com/ONLYOFFICE/CommunityServer/blob/v9.1.1/web/studio/ASC.Web.Studio/web.appsettings.config#L35 + */ + public const USER_AGENT_MOBILE = "/android|avantgo|playbook|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|symbian|treo|up\\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i"; + + /** + * @param string $AppName - application name + * @param IRequest $request - request object + * @param IRootFolder $root - root folder + * @param IUserSession $userSession - current user session + * @param IURLGenerator $urlGenerator - url generator service + * @param IL10N $trans - l10n service + * @param ILogger $logger - logger + * @param AppConfig $config - application configuration + * @param Crypt $crypt - hash generator + * @param IManager $shareManager - Share manager + * @param ISession $session - Session + * @param ITagManager $tagManager - Tag manager + */ + public function __construct( + $AppName, + IRequest $request, + IRootFolder $root, + IUserSession $userSession, + IURLGenerator $urlGenerator, + IL10N $trans, + ILogger $logger, + AppConfig $config, + Crypt $crypt, + IManager $shareManager, + ISession $session, + ITagManager $tagManager + ) { + parent::__construct($AppName, $request); + + $this->userSession = $userSession; + $this->root = $root; + $this->urlGenerator = $urlGenerator; + $this->trans = $trans; + $this->logger = $logger; + $this->config = $config; + $this->crypt = $crypt; + $this->tagManager = $tagManager; + + $this->versionManager = new VersionManager($AppName, $root); + + $this->fileUtility = new FileUtility($AppName, $trans, $logger, $config, $shareManager, $session); + } + + /** + * Filling empty file an template + * + * @param int $fileId - file identificator + * + * @return JSONResponse + * + * @NoAdminRequired + * @PublicPage + */ + public function fillempty($fileId) { + $this->logger->debug("Fill empty: $fileId", ["app" => $this->appName]); + + if (empty($fileId)) { + $this->logger->error("File for filling was not found: $fileId", ["app" => $this->appName]); + return new JSONResponse(["error" => $this->trans->t("FileId is empty")]); + } + + $userId = $this->userSession->getUser()->getUID(); + + list($file, $error, $share) = $this->getFile($userId, $fileId); + if (isset($error)) { + $this->logger->error("Fill empty: $fileId $error", ["app" => $this->appName]); + return new JSONResponse(["error" => $error]); + } + + if ($file->getSize() > 0) { + $this->logger->error("File is't empty: $fileId", ["app" => $this->appName]); + return new JSONResponse(["error" => $this->trans->t("Not permitted")]); + } + + if (!$file->isUpdateable()) { + $this->logger->error("File without permission: $fileId", ["app" => $this->appName]); + return new JSONResponse(["error" => $this->trans->t("Not permitted")]); + } + + $name = $file->getName(); + $template = TemplateManager::getEmptyTemplate($name); + + if (!$template) { + $this->logger->error("Template for file filling not found: $name ($fileId)", ["app" => $this->appName]); + return new JSONResponse(["error" => $this->trans->t("Template not found")]); + } + + try { + $file->putContent($template); + } catch (NotPermittedException $e) { + $this->logger->logException($e, ["message" => "Can't put file: $name", "app" => $this->appName]); + return new JSONResponse(["error" => $this->trans->t("Can't create file")]); + } + + return new JSONResponse([]); + } + + /** + * Collecting the file parameters for the document service + * + * @param integer $fileId - file identifier + * @param string $filePath - file path + * @param string $shareToken - access token + * @param integer $version - file version + * @param bool $inframe - open in frame + * @param bool $desktop - desktop label + * @param bool $template - file is template + * @param string $anchor - anchor link + * + * @return JSONResponse + * + * @NoAdminRequired + * @PublicPage + * @CORS + */ + public function config($fileId, $filePath = null, $shareToken = null, $version = 0, $inframe = false, $desktop = false, $template = false, $anchor = null) { + $user = $this->userSession->getUser(); + $userId = null; + $accountId = null; + if (!empty($user)) { + $userId = $user->getUID(); + $accountId = $user->getAccountId(); + } + + list($file, $error, $share) = empty($shareToken) ? $this->getFile($userId, $fileId, $filePath, $template) : $this->fileUtility->getFileByToken($fileId, $shareToken); + + if (isset($error)) { + $this->logger->error("Config: $fileId $error", ["app" => $this->appName]); + return new JSONResponse(["error" => $error]); + } + + $checkUserAllowGroups = $userId; + if (!empty($share)) { + $checkUserAllowGroups = $share->getSharedBy(); + } + if (!$this->config->isUserAllowedToUse($checkUserAllowGroups)) { + return new JSONResponse(["error" => $this->trans->t("Not permitted")]); + } + + $fileName = $file->getName(); + $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $format = !empty($ext) && \array_key_exists($ext, $this->config->formatsSetting()) ? $this->config->formatsSetting()[$ext] : null; + if (!isset($format)) { + $this->logger->info("Format is not supported for editing: $fileName", ["app" => $this->appName]); + return new JSONResponse(["error" => $this->trans->t("Format is not supported")]); + } + + $fileUrl = $this->getUrl($file, $user, $shareToken, $version, null, $template); + + $key = null; + if ($version > 0 + && $this->versionManager->available + ) { + $owner = $file->getFileInfo()->getOwner(); + if ($owner !== null) { + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + + if ($version <= \count($versions)) { + $fileVersion = array_values($versions)[$version - 1]; + + $key = $this->fileUtility->getVersionKey($fileVersion); + } + } + } + if ($key === null) { + $key = $this->fileUtility->getKey($file, true); + } + $key = DocumentService::generateRevisionId($key); + + $params = [ + "document" => [ + "fileType" => $ext, + "key" => $key, + "permissions" => [], + "title" => $fileName, + "url" => $fileUrl, + "referenceData" => [ + "fileKey" => $file->getId(), + "instanceId" => $this->config->getSystemValue("instanceid", true), + ], + ], + "documentType" => $format["type"], + "editorConfig" => [ + "lang" => str_replace("_", "-", \OC::$server->getL10NFactory("")->get("")->getLanguageCode()) + ] + ]; + + $restrictedEditing = false; + $fileStorage = $file->getStorage(); + if (empty($shareToken) && $fileStorage->instanceOfStorage("\OCA\Files_Sharing\SharedStorage")) { + + $storageShare = $fileStorage->getShare(); + if (method_exists($storageShare, "getAttributes")) { + $attributes = $storageShare->getAttributes(); + $canDownload = FileUtility::canShareDownload($storageShare); + $params["document"]["permissions"]["download"] = $params["document"]["permissions"]["print"] = $params["document"]["permissions"]["copy"] = $canDownload === true; + + if (isset($format["review"]) && $format["review"]) { + $permissionsReviewOnly = $attributes->getAttribute($this->appName, "review"); + if ($permissionsReviewOnly !== null && $permissionsReviewOnly === true) { + $restrictedEditing = true; + $params["document"]["permissions"]["review"] = true; + } + } + + if (isset($format["fillForms"]) && $format["fillForms"]) { + $permissionsFillFormsOnly = $attributes->getAttribute($this->appName, "fillForms"); + if ($permissionsFillFormsOnly !== null && $permissionsFillFormsOnly === true) { + $restrictedEditing = true; + $params["document"]["permissions"]["fillForms"] = true; + } + } + + if (isset($format["comment"]) && $format["comment"]) { + $permissionsCommentOnly = $attributes->getAttribute($this->appName, "comment"); + if ($permissionsCommentOnly !== null && $permissionsCommentOnly === true) { + $restrictedEditing = true; + $params["document"]["permissions"]["comment"] = true; + } + } + + if (isset($format["modifyFilter"]) && $format["modifyFilter"]) { + $permissionsModifyFilter = $attributes->getAttribute($this->appName, "modifyFilter"); + if ($permissionsModifyFilter !== null) { + $params["document"]["permissions"]["modifyFilter"] = $permissionsModifyFilter === true; + } + } + } + } + + $isPersistentLock = false; + if ($version < 1 + && (\OC::$server->getConfig()->getAppValue("files", "enable_lock_file_action", "no") === "yes") + && $fileStorage->instanceOfStorage(IPersistentLockingStorage::class) + ) { + $locks = $fileStorage->getLocks($file->getFileInfo()->getInternalPath(), false); + if (\count($locks) > 0) { + $activeLock = $locks[0]; + + if ($accountId !== $activeLock->getOwnerAccountId()) { + $isPersistentLock = true; + $lockOwner = $activeLock->getOwner(); + $this->logger->debug("File $fileId is locked by $lockOwner", ["app" => $this->appName]); + } + } + } + + $canEdit = isset($format["edit"]) && $format["edit"]; + $canFillForms = isset($format["fillForms"]) && $format["fillForms"]; + $editable = $version < 1 + && !$template + && $file->isUpdateable() + && !$isPersistentLock + && (empty($shareToken) || ($share->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE); + $params["document"]["permissions"]["edit"] = $editable; + if (($editable || $restrictedEditing) && $canEdit || $canFillForms) { + $ownerId = null; + $owner = $file->getOwner(); + if (!empty($owner)) { + $ownerId = $owner->getUID(); + } + + $canProtect = true; + if ($this->config->getProtection() === "owner") { + $canProtect = $ownerId === $userId; + } + $params["document"]["permissions"]["protect"] = $canProtect; + + if (isset($shareToken)) { + $params["document"]["permissions"]["chat"] = false; + $params["document"]["permissions"]["protect"] = false; + } + + $hashCallback = $this->crypt->getHash(["userId" => $userId, "ownerId" => $ownerId, "fileId" => $file->getId(), "filePath" => $filePath, "shareToken" => $shareToken, "action" => "track"]); + $callback = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".callback.track", ["doc" => $hashCallback]); + + if (!$this->config->useDemo() && !empty($this->config->getStorageUrl())) { + $callback = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->config->getStorageUrl(), $callback); + } + + $params["editorConfig"]["callbackUrl"] = $callback; + } else { + $params["editorConfig"]["mode"] = "view"; + + if (isset($shareToken) && empty($userId)) { + $params["editorConfig"]["coEditing"] = [ + "mode" => "strict", + "change" => false + ]; + } + } + + if (\OC::$server->getRequest()->isUserAgent([$this::USER_AGENT_MOBILE])) { + $params["type"] = "mobile"; + } + + if (!$template + && $file->isUpdateable() + && !$isPersistentLock + && (empty($shareToken) || ($share->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE) + ) { + $params["document"]["permissions"]["changeHistory"] = true; + } + + if (!empty($userId)) { + $params["editorConfig"]["user"] = [ + "id" => $this->buildUserId($userId), + "name" => $user->getDisplayName() + ]; + } + + $folderLink = null; + + if (!empty($shareToken)) { + $node = $share->getNode(); + if ($node instanceof Folder) { + $sharedFolder = $node; + $folderPath = $sharedFolder->getRelativePath($file->getParent()->getPath()); + if (!empty($folderPath)) { + $linkAttr = [ + "path" => $folderPath, + "scrollto" => $file->getName(), + "token" => $shareToken + ]; + $folderLink = $this->urlGenerator->linkToRouteAbsolute("files_sharing.sharecontroller.showShare", $linkAttr); + } + } + } elseif (!empty($userId)) { + $userFolder = $this->root->getUserFolder($userId); + $folderPath = $userFolder->getRelativePath($file->getParent()->getPath()); + if (!empty($folderPath)) { + $linkAttr = [ + "dir" => $folderPath, + "scrollto" => $file->getName() + ]; + $folderLink = $this->urlGenerator->linkToRouteAbsolute("files.view.index", $linkAttr); + } + + switch ($params["documentType"]) { + case "word": + $createName = $this->trans->t("Document") . ".docx"; + break; + case "cell": + $createName = $this->trans->t("Spreadsheet") . ".xlsx"; + break; + case "slide": + $createName = $this->trans->t("Presentation") . ".pptx"; + break; + } + + $createParam = [ + "dir" => "/", + "name" => $createName + ]; + + if (!empty($folderPath)) { + $folder = $userFolder->get($folderPath); + if (!empty($folder) && $folder->isCreatable()) { + $createParam["dir"] = $folderPath; + } + } + + $createUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".editor.create_new", $createParam); + + $params["editorConfig"]["createUrl"] = urldecode($createUrl); + + $templatesList = TemplateManager::getGlobalTemplates($file->getMimeType()); + if (!empty($templatesList)) { + $templates = []; + foreach ($templatesList as $templateItem) { + $createParam["templateId"] = $templateItem->getId(); + $createParam["name"] = $templateItem->getName(); + + array_push( + $templates, + [ + "image" => "", + "title" => $templateItem->getName(), + "url" => urldecode($this->urlGenerator->linkToRouteAbsolute($this->appName . ".editor.create_new", $createParam)) + ] + ); + } + + $params["editorConfig"]["templates"] = $templates; + } + + if (!$template) { + $params["document"]["info"]["favorite"] = $this->isFavorite($fileId); + } + $params["_file_path"] = $userFolder->getRelativePath($file->getPath()); + } + + if ($folderLink !== null + && $this->config->getSystemValue($this->config->customization_goback) !== false + ) { + $params["editorConfig"]["customization"]["goback"] = [ + "url" => $folderLink + ]; + + if (!$desktop) { + if ($this->config->getSameTab()) { + $params["editorConfig"]["customization"]["goback"]["blank"] = false; + if ($inframe === true) { + $params["editorConfig"]["customization"]["goback"]["requestClose"] = true; + } + } + } + } + + if ($inframe === true) { + $params["_files_sharing"] = \OC::$server->getAppManager()->isEnabledForUser("files_sharing"); + } + + $params = $this->setCustomization($params); + + if ($this->config->useDemo()) { + $params["editorConfig"]["tenant"] = $this->config->getSystemValue("instanceid", true); + } + + if ($anchor !== null) { + try { + $actionLink = json_decode($anchor, true); + + $params["editorConfig"]["actionLink"] = $actionLink; + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "Config: $fileId decode $anchor", "app" => $this->appName]); + } + } + + if (!empty($this->config->getDocumentServerSecret())) { + $token = \Firebase\JWT\JWT::encode($params, $this->config->getDocumentServerSecret(), "HS256"); + $params["token"] = $token; + } + + $this->logger->debug("Config is generated for: $fileId ($version) with key $key", ["app" => $this->appName]); + + return new JSONResponse($params); + } + + /** + * Getting file by identifier + * + * @param string $userId - user identifier + * @param integer $fileId - file identifier + * @param string $filePath - file path + * @param bool $template - file is template + * + * @return array + */ + private function getFile($userId, $fileId, $filePath = null, $template = false) { + if (empty($fileId)) { + return [null, $this->trans->t("FileId is empty"), null]; + } + + try { + $folder = !$template ? $this->root->getUserFolder($userId) : TemplateManager::getGlobalTemplateDir(); + $files = $folder->getById($fileId); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "getFile: $fileId", "app" => $this->appName]); + return [null, $this->trans->t("Invalid request"), null]; + } + + if (empty($files)) { + $this->logger->info("Files not found: $fileId", ["app" => $this->appName]); + return [null, $this->trans->t("File not found"), null]; + } + + $file = $files[0]; + + if (\count($files) > 1 && !empty($filePath)) { + $filePath = "/" . $userId . "/files" . $filePath; + foreach ($files as $curFile) { + if ($curFile->getPath() === $filePath) { + $file = $curFile; + break; + } + } + } + + if (!$file->isReadable()) { + return [null, $this->trans->t("You do not have enough permissions to view the file"), null]; + } + + return [$file, null, null]; + } + + /** + * Generate secure link to download document + * + * @param File $file - file + * @param IUser $user - user with access + * @param string $shareToken - access token + * @param integer $version - file version + * @param bool $changes - is required url to file changes + * @param bool $template - file is template + * + * @return string + */ + private function getUrl($file, $user = null, $shareToken = null, $version = 0, $changes = false, $template = false) { + $data = [ + "action" => "download", + "fileId" => $file->getId() + ]; + + $userId = null; + if (!empty($user)) { + $userId = $user->getUID(); + $data["userId"] = $userId; + } + if (!empty($shareToken)) { + $data["shareToken"] = $shareToken; + } + if ($version > 0) { + $data["version"] = $version; + } + if ($changes) { + $data["changes"] = true; + } + if ($template) { + $data["template"] = true; + } + + $hashUrl = $this->crypt->getHash($data); + + $fileUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".callback.download", ["doc" => $hashUrl]); + + if (!$this->config->useDemo() && !empty($this->config->getStorageUrl())) { + $fileUrl = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->config->getStorageUrl(), $fileUrl); + } + + return $fileUrl; + } + + /** + * Generate unique user identifier + * + * @param string $userId - current user identifier + * + * @return string + */ + private function buildUserId($userId) { + $instanceId = $this->config->getSystemValue("instanceid", true); + $userId = $instanceId . "_" . $userId; + return $userId; + } + + /** + * Set customization parameters + * + * @param array $params - file parameters + * + * @return array + */ + private function setCustomization($params) { + //default is true + if ($this->config->getCustomizationChat() === false) { + $params["editorConfig"]["customization"]["chat"] = false; + } + + //default is false + if ($this->config->getCustomizationCompactHeader() === true) { + $params["editorConfig"]["customization"]["compactHeader"] = true; + } + + //default is false + if ($this->config->getCustomizationFeedback() === true) { + $params["editorConfig"]["customization"]["feedback"] = true; + } + + //default is false + if ($this->config->getCustomizationForcesave() === true) { + $params["editorConfig"]["customization"]["forcesave"] = true; + } + + //default is true + if ($this->config->getCustomizationHelp() === false) { + $params["editorConfig"]["customization"]["help"] = false; + } + + //default is original + $reviewDisplay = $this->config->getCustomizationReviewDisplay(); + if ($reviewDisplay !== "original") { + $params["editorConfig"]["customization"]["reviewDisplay"] = $reviewDisplay; + } + + $theme = $this->config->getCustomizationTheme(); + if (isset($theme)) { + $params["editorConfig"]["customization"]["uiTheme"] = $theme; + } + + //default is false + if ($this->config->getCustomizationToolbarNoTabs() === true) { + $params["editorConfig"]["customization"]["toolbarNoTabs"] = true; + } + + //default is true + if ($this->config->getCustomizationMacros() === false) { + $params["editorConfig"]["customization"]["macros"] = false; + } + + //default is true + if ($this->config->getCustomizationPlugins() === false) { + $params["editorConfig"]["customization"]["plugins"] = false; + } + + /* from system config */ + + $autosave = $this->config->getSystemValue($this->config->customization_autosave); + if (isset($autosave)) { + $params["editorConfig"]["customization"]["autosave"] = $autosave; + } + + $customer = $this->config->getSystemValue($this->config->customization_customer); + if (isset($customer)) { + $params["editorConfig"]["customization"]["customer"] = $customer; + } + + $loaderLogo = $this->config->getSystemValue($this->config->customization_loaderLogo); + if (isset($loaderLogo)) { + $params["editorConfig"]["customization"]["loaderLogo"] = $loaderLogo; + } + + $loaderName = $this->config->getSystemValue($this->config->customization_loaderName); + if (isset($loaderName)) { + $params["editorConfig"]["customization"]["loaderName"] = $loaderName; + } + + $logo = $this->config->getSystemValue($this->config->customization_logo); + if (isset($logo)) { + $params["editorConfig"]["customization"]["logo"] = $logo; + } + + $zoom = $this->config->getSystemValue($this->config->customization_zoom); + if (isset($zoom)) { + $params["editorConfig"]["customization"]["zoom"] = $zoom; + } + + return $params; + } + + /** + * Check file favorite + * + * @param integer $fileId - file identifier + * + * @return bool + */ + private function isFavorite($fileId) { + $currentTags = $this->tagManager->load("files")->getTagsForObjects([$fileId]); + if ($currentTags) { + return \in_array(Tags::TAG_FAVORITE, $currentTags[$fileId]); + } + + return false; + } } diff --git a/controller/editorcontroller.php b/controller/editorcontroller.php index b21e6c92..7aedb397 100644 --- a/controller/editorcontroller.php +++ b/controller/editorcontroller.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,1409 +56,1517 @@ * Controller with the main functions */ class EditorController extends Controller { - - /** - * Current user session - * - * @var IUserSession - */ - private $userSession; - - /** - * Current user manager - * - * @var IUserManager - */ - private $userManager; - - /** - * Root folder - * - * @var IRootFolder - */ - private $root; - - /** - * Url generator service - * - * @var IURLGenerator - */ - private $urlGenerator; - - /** - * l10n service - * - * @var IL10N - */ - private $trans; - - /** - * Logger - * - * @var ILogger - */ - private $logger; - - /** - * Application configuration - * - * @var AppConfig - */ - private $config; - - /** - * Hash generator - * - * @var Crypt - */ - private $crypt; - - /** - * File utility - * - * @var FileUtility - */ - private $fileUtility; - - /** - * File version manager - * - * @var VersionManager - */ - private $versionManager; - - /** - * Share manager - * - * @var IManager - */ - private $shareManager; - - /** - * Group manager - * - * @var IGroupManager - */ - private $groupManager; - - /** - * @param string $AppName - application name - * @param IRequest $request - request object - * @param IRootFolder $root - root folder - * @param IUserSession $userSession - current user session - * @param IUserManager $userManager - current user manager - * @param IURLGenerator $urlGenerator - url generator service - * @param IL10N $trans - l10n service - * @param ILogger $logger - logger - * @param AppConfig $config - application configuration - * @param Crypt $crypt - hash generator - * @param IManager $shareManager - Share manager - * @param ISession $session - Session - * @param IGroupManager $groupManager - Group manager - */ - public function __construct($AppName, - IRequest $request, - IRootFolder $root, - IUserSession $userSession, - IUserManager $userManager, - IURLGenerator $urlGenerator, - IL10N $trans, - ILogger $logger, - AppConfig $config, - Crypt $crypt, - IManager $shareManager, - ISession $session, - IGroupManager $groupManager - ) { - parent::__construct($AppName, $request); - - $this->userSession = $userSession; - $this->userManager = $userManager; - $this->root = $root; - $this->urlGenerator = $urlGenerator; - $this->trans = $trans; - $this->logger = $logger; - $this->config = $config; - $this->crypt = $crypt; - $this->shareManager = $shareManager; - $this->groupManager = $groupManager; - - $this->versionManager = new VersionManager($AppName, $root); - - $this->fileUtility = new FileUtility($AppName, $trans, $logger, $config, $shareManager, $session); - } - - /** - * Create new file in folder - * - * @param string $name - file name - * @param string $dir - folder path - * @param string $templateId - file identifier - * @param string $targetPath - file path for using as template for create - * @param string $shareToken - access token - * - * @return array - * - * @NoAdminRequired - * @PublicPage - */ - public function create($name, $dir, $templateId = null, $targetPath = null, $shareToken = null) { - $this->logger->debug("Create: $name", ["app" => $this->appName]); - - if (empty($shareToken) && !$this->config->isUserAllowedToUse()) { - return ["error" => $this->trans->t("Not permitted")]; - } - - if (empty($name)) { - $this->logger->error("File name for creation was not found: $name", ["app" => $this->appName]); - return ["error" => $this->trans->t("Template not found")]; - } - - $user = null; - if (empty($shareToken)) { - $user = $this->userSession->getUser(); - $userId = $user->getUID(); - $userFolder = $this->root->getUserFolder($userId); - } else { - list ($userFolder, $error, $share) = $this->fileUtility->getNodeByToken($shareToken); - - if (isset($error)) { - $this->logger->error("Create: $error", ["app" => $this->appName]); - return ["error" => $error]; - } - - if ($userFolder instanceof File) { - return ["error" => $this->trans->t("You don't have enough permission to create")]; - } - - if (!empty($shareToken) && ($share->getPermissions() & Constants::PERMISSION_CREATE) === 0) { - $this->logger->error("Create in public folder without access", ["app" => $this->appName]); - return ["error" => $this->trans->t("You do not have enough permissions to view the file")]; - } - } - - $folder = $userFolder->get($dir); - - if ($folder === null) { - $this->logger->error("Folder for file creation was not found: $dir", ["app" => $this->appName]); - return ["error" => $this->trans->t("The required folder was not found")]; - } - if (!($folder->isCreatable() && $folder->isUpdateable())) { - $this->logger->error("Folder for file creation without permission: $dir", ["app" => $this->appName]); - return ["error" => $this->trans->t("You don't have enough permission to create")]; - } - - if (!empty($templateId)) { - $templateFile = TemplateManager::GetTemplate($templateId); - if ($templateFile) { - $template = $templateFile->getContent(); - } - } elseif (!empty($targetPath)) { - $targetFile = $userFolder->get($targetPath); - - $canDownload = $this->fileUtility->hasPermissionAttribute($targetFile); - if (!$canDownload) { - return ["error" => $this->trans->t("Not permitted")]; - } - - $targetId = $targetFile->getId(); - $targetName = $targetFile->getName(); - $targetExt = strtolower(pathinfo($targetName, PATHINFO_EXTENSION)); - $targetKey = $this->fileUtility->getKey($targetFile); - - $fileUrl = $this->getUrl($targetFile, $user, $shareToken); - - $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); - $documentService = new DocumentService($this->trans, $this->config); - try { - $newFileUri = $documentService->GetConvertedUri($fileUrl, $targetExt, $ext, $targetKey); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "GetConvertedUri: " . $targetFile->getId(), "app" => $this->appName]); - return ["error" => $e->getMessage()]; - } - $template = $documentService->Request($newFileUri); - } else { - $template = TemplateManager::GetEmptyTemplate($name); - } - - if (!$template) { - $this->logger->error("Template for file creation not found: $name ($templateId)", ["app" => $this->appName]); - return ["error" => $this->trans->t("Template not found")]; - } - - $name = $folder->getNonExistingName($name); - - try { - $file = $folder->newFile($name); - - $file->putContent($template); - } catch (NotPermittedException $e) { - $this->logger->logException($e, ["message" => "Can't create file: $name", "app" => $this->appName]); - return ["error" => $this->trans->t("Can't create file")]; - } catch (ForbiddenException $e) { - $this->logger->logException($e, ["message" => "Can't put file: $name", "app" => $this->appName]); - return ["error" => $this->trans->t("Can't create file")]; - } - - $fileInfo = $file->getFileInfo(); - - $result = Helper::formatFileInfo($fileInfo); - return $result; - } - - /** - * Create new file in folder from editor - * - * @param string $name - file name - * @param string $dir - folder path - * @param string $templateId - file identifier - * - * @return TemplateResponse|RedirectResponse - * - * @NoAdminRequired - * @NoCSRFRequired - */ - public function createNew($name, $dir, $templateId = null) { - $this->logger->debug("Create from editor: $name in $dir", ["app" => $this->appName]); - - $result = $this->create($name, $dir, $templateId); - if (isset($result["error"])) { - return $this->renderError($result["error"]); - } - - $openEditor = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".editor.index", ["fileId" => $result["id"]]); - return new RedirectResponse($openEditor); - } - - /** - * Get users - * - * @param $fileId - file identifier - * - * @return array - * - * @NoAdminRequired - * @NoCSRFRequired - */ - public function users($fileId) { - $this->logger->debug("Search users", ["app" => $this->appName]); - $result = []; - - if (!$this->config->isUserAllowedToUse()) { - return $result; - } - - if (!$this->allowEnumeration()) { - return $result; - } - - $autocompleteMemberGroup = false; - if ($this->limitEnumerationToGroups()) { - $autocompleteMemberGroup = true; - } - - $currentUser = $this->userSession->getUser(); - $currentUserId = $currentUser->getUID(); - - list ($file, $error, $share) = $this->getFile($currentUserId, $fileId); - if (isset($error)) { - $this->logger->error("Users: $fileId $error", ["app" => $this->appName]); - return $result; - } - - $canShare = (($file->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE); - - $shareMemberGroups = $this->shareManager->shareWithGroupMembersOnly(); - - $all = false; - $users = []; - if ($canShare) { - if ($shareMemberGroups || $autocompleteMemberGroup) { - $currentUserGroups = $this->groupManager->getUserGroupIds($currentUser); - foreach ($currentUserGroups as $currentUserGroup) { - $group = $this->groupManager->get($currentUserGroup); - foreach ($group->getUsers() as $user) { - if (!in_array($user, $users)) { - array_push($users, $user); - } - } - } - } else { - $users = $this->userManager->search(""); - $all = true; - } - } - - if (!$all) { - $accessList = $this->getAccessList($file); - foreach ($accessList as $accessUser) { - if (!in_array($accessUser, $users)) { - array_push($users, $accessUser); - } - } - } - - foreach ($users as $user) { - $email = $user->getEMailAddress(); - if ($user->getUID() != $currentUserId && !empty($email)) { - array_push($result, [ - "email" => $email, - "name" => $user->getDisplayName() - ]); - } - } - - return $result; - } - - /** - * Send notify about mention - * - * @param int $fileId - file identifier - * @param string $anchor - the anchor on target content - * @param string $comment - comment - * @param array $emails - emails array to whom to send notify - * - * @return array - * - * @NoAdminRequired - * @NoCSRFRequired - */ - public function mention($fileId, $anchor, $comment, $emails) { - $this->logger->debug("mention: from $fileId to " . json_encode($emails), ["app" => $this->appName]); - - if (!$this->config->isUserAllowedToUse()) { - return ["error" => $this->trans->t("Not permitted")]; - } - - if (empty($emails)) { - return ["error" => $this->trans->t("Failed to send notification")]; - } - - $recipientIds = []; - foreach ($emails as $email) { - $recipients = $this->userManager->getByEmail($email); - foreach ($recipients as $recipient) { - $recipientId = $recipient->getUID(); - if (!in_array($recipientId, $recipientIds)) { - array_push($recipientIds, $recipientId); - } - } - } - - $user = $this->userSession->getUser(); - $userId = null; - if (!empty($user)) { - $userId = $user->getUID(); - } - - list ($file, $error, $share) = $this->getFile($userId, $fileId); - if (isset($error)) { - $this->logger->error("Mention: $fileId $error", ["app" => $this->appName]); - return ["error" => $this->trans->t("Failed to send notification")]; - } - - foreach ($emails as $email) { - $substrToDelete = "+" . $email . " "; - $comment = str_replace($substrToDelete, "", $comment); - } - - //Length from ownCloud: - //https://github.com/owncloud/core/blob/master/lib/private/Notification/Notification.php#L181 - $maxLen = 64; - if (strlen($comment) > $maxLen) { - $ending = "..."; - $comment = substr($comment, 0, ($maxLen - strlen($ending))) . $ending; - } - - $notificationManager = \OC::$server->getNotificationManager(); - $notification = $notificationManager->createNotification(); - $notification->setApp($this->appName) - ->setDateTime(new \DateTime()) - ->setObject("mention", $comment) - ->setSubject("mention_info", [ - "notifierId" => $userId, - "fileId" => $file->getId(), - "fileName" => $file->getName(), - "anchor" => $anchor - ]); - - $shareMemberGroups = $this->shareManager->shareWithGroupMembersOnly(); - $canShare = ($file->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE; - - $currentUserGroups = []; - if ($shareMemberGroups) { - $currentUserGroups = $this->groupManager->getUserGroupIds($user); - } - - $accessList = $this->getAccessList($file); - - foreach ($recipientIds as $recipientId) { - $recipient = $this->userManager->get($recipientId); - $isAvailable = in_array($recipient, $accessList); - - if (!$isAvailable - && $file->getFileInfo()->getMountPoint() instanceof \OCA\Files_External\Config\ExternalMountPoint) { - - $recipientFolder = $this->root->getUserFolder($recipientId); - $recipientFile = $recipientFolder->getById($file->getId()); - - $isAvailable = !empty($recipientFile); - } - - if (!$isAvailable) { - if (!$canShare) { - continue; - } - if ($shareMemberGroups) { - $recipientGroups = $this->groupManager->getUserGroupIds($recipient); - if (empty(array_intersect($currentUserGroups, $recipientGroups))) { - continue; - } - } - - $share = $this->shareManager->newShare(); - $share->setNode($file) - ->setShareType(Share::SHARE_TYPE_USER) - ->setSharedBy($userId) - ->setSharedWith($recipientId) - ->setShareOwner($userId) - ->setPermissions(Constants::PERMISSION_READ); - - $this->shareManager->createShare($share); - - $this->logger->debug("mention: share $fileId to $recipientId", ["app" => $this->appName]); - } - - $notification->setUser($recipientId); - - $notificationManager->notify($notification); - } - - return ["message" => $this->trans->t("Notification sent successfully")]; - } - - /** - * Reference data - * - * @param array $referenceData - reference data - * @param string $path - file path - * - * @return array - * - * @NoAdminRequired - * @PublicPage - */ - public function reference($referenceData, $path = null) { - $this->logger->debug("reference: " . json_encode($referenceData) . " $path", ["app" => $this->appName]); - - if (!$this->config->isUserAllowedToUse()) { - return ["error" => $this->trans->t("Not permitted")]; - } - - $user = $this->userSession->getUser(); - if (empty($user)) { - return ["error" => $this->trans->t("Not permitted")]; - } - - $userId = $user->getUID(); - - $file = null; - $fileId = (integer)($referenceData["fileKey"] ?? 0); - if (!empty($fileId) - && $referenceData["instanceId"] === $this->config->GetSystemValue("instanceid", true)) { - list ($file, $error, $share) = $this->getFile($userId, $fileId); - } - - $userFolder = $this->root->getUserFolder($userId); - if ($file === null - && $path !== null - && $userFolder->nodeExists($path)) { - $node = $userFolder->get($path); - if ($node instanceof File - && $node->isReadable()) { - $file = $node; - } - } - - if ($file === null) { - $this->logger->error("Reference not found: $fileId $path", ["app" => $this->appName]); - return ["error" => $this->trans->t("File not found")]; - } - - $fileName = $file->getName(); - $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); - - $response = [ - "fileType" => $ext, - "path" => $userFolder->getRelativePath($file->getPath()), - "referenceData" => [ - "fileKey" => $file->getId(), - "instanceId" => $this->config->GetSystemValue("instanceid", true), - ], - "url" => $this->getUrl($file, $user), - ]; - - if (!empty($this->config->GetDocumentServerSecret())) { - $token = \Firebase\JWT\JWT::encode($response, $this->config->GetDocumentServerSecret(), "HS256"); - $response["token"] = $token; - } - - return $response; - } - - /** - * Conversion file to Office Open XML format - * - * @param integer $fileId - file identifier - * @param string $shareToken - access token - * - * @return array - * - * @NoAdminRequired - * @PublicPage - */ - public function convert($fileId, $shareToken = null) { - $this->logger->debug("Convert: $fileId", ["app" => $this->appName]); - - if (empty($shareToken) && !$this->config->isUserAllowedToUse()) { - return ["error" => $this->trans->t("Not permitted")]; - } - - $user = $this->userSession->getUser(); - $userId = null; - if (!empty($user)) { - $userId = $user->getUID(); - } - - list ($file, $error, $share) = empty($shareToken) ? $this->getFile($userId, $fileId) : $this->fileUtility->getFileByToken($fileId, $shareToken); - - if (isset($error)) { - $this->logger->error("Convertion: $fileId $error", ["app" => $this->appName]); - return ["error" => $error]; - } - - if (!empty($shareToken) && ($share->getPermissions() & Constants::PERMISSION_CREATE) === 0) { - $this->logger->error("Convertion in public folder without access: $fileId", ["app" => $this->appName]); - return ["error" => $this->trans->t("You do not have enough permissions to view the file")]; - } - - $canDownload = $this->fileUtility->hasPermissionAttribute($file); - if (!$canDownload) { - return ["error" => $this->trans->t("Not permitted")]; - } - - $fileName = $file->getName(); - $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); - $format = $this->config->FormatsSetting()[$ext]; - if (!isset($format)) { - $this->logger->info("Format for convertion not supported: $fileName", ["app" => $this->appName]); - return ["error" => $this->trans->t("Format is not supported")]; - } - - if (!isset($format["conv"]) || $format["conv"] !== true) { - $this->logger->info("Conversion is not required: $fileName", ["app" => $this->appName]); - return ["error" => $this->trans->t("Conversion is not required")]; - } - - $internalExtension = "docx"; - switch ($format["type"]) { - case "cell": - $internalExtension = "xlsx"; - break; - case "slide": - $internalExtension = "pptx"; - break; - } - - $newFileUri = null; - $documentService = new DocumentService($this->trans, $this->config); - $key = $this->fileUtility->getKey($file); - $fileUrl = $this->getUrl($file, $user, $shareToken); - try { - $newFileUri = $documentService->GetConvertedUri($fileUrl, $ext, $internalExtension, $key); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "GetConvertedUri: " . $file->getId(), "app" => $this->appName]); - return ["error" => $e->getMessage()]; - } - - $folder = $file->getParent(); - if (!($folder->isCreatable() && $folder->isUpdateable())) { - $folder = $this->root->getUserFolder($userId); - } - - try { - $newData = $documentService->Request($newFileUri); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "Failed to download converted file", "app" => $this->appName]); - return ["error" => $this->trans->t("Failed to download converted file")]; - } - - $fileNameWithoutExt = substr($fileName, 0, strlen($fileName) - strlen($ext) - 1); - $newFileName = $folder->getNonExistingName($fileNameWithoutExt . "." . $internalExtension); - - try { - $file = $folder->newFile($newFileName); - - $file->putContent($newData); - } catch (NotPermittedException $e) { - $this->logger->logException($e, ["message" => "Can't create file: $newFileName", "app" => $this->appName]); - return ["error" => $this->trans->t("Can't create file")]; - } catch (ForbiddenException $e) { - $this->logger->logException($e, ["message" => "Can't put file: $newFileName", "app" => $this->appName]); - return ["error" => $this->trans->t("Can't create file")]; - } - - $fileInfo = $file->getFileInfo(); - - $result = Helper::formatFileInfo($fileInfo); - return $result; - } - - /** - * Save file to folder - * - * @param string $name - file name - * @param string $dir - folder path - * @param string $url - file url - * - * @return array - * - * @NoAdminRequired - * @PublicPage - */ - public function save($name, $dir, $url) { - $this->logger->debug("Save: $name", ["app" => $this->appName]); - - if (!$this->config->isUserAllowedToUse()) { - return ["error" => $this->trans->t("Not permitted")]; - } - - $userId = $this->userSession->getUser()->getUID(); - $userFolder = $this->root->getUserFolder($userId); - - $folder = $userFolder->get($dir); - - if ($folder === null) { - $this->logger->error("Folder for saving file was not found: $dir", ["app" => $this->appName]); - return ["error" => $this->trans->t("The required folder was not found")]; - } - if (!($folder->isCreatable() && $folder->isUpdateable())) { - $this->logger->error("Folder for saving file without permission: $dir", ["app" => $this->appName]); - return ["error" => $this->trans->t("You don't have enough permission to create")]; - } - - $url = $this->config->ReplaceDocumentServerUrlToInternal($url); - - try { - $documentService = new DocumentService($this->trans, $this->config); - $newData = $documentService->Request($url); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "Failed to download file for saving", "app" => $this->appName]); - return ["error" => $this->trans->t("Download failed")]; - } - - $name = $folder->getNonExistingName($name); - - try { - $file = $folder->newFile($name); - - $file->putContent($newData); - } catch (NotPermittedException $e) { - $this->logger->logException($e, ["message" => "Can't save file: $name", "app" => $this->appName]); - return ["error" => $this->trans->t("Can't create file")]; - } catch (ForbiddenException $e) { - $this->logger->logException($e, ["message" => "Can't put file: $name", "app" => $this->appName]); - return ["error" => $this->trans->t("Can't create file")]; - } - - $fileInfo = $file->getFileInfo(); - - $result = Helper::formatFileInfo($fileInfo); - return $result; - } - - /** - * Get versions history for file - * - * @param integer $fileId - file identifier - * - * @return array - * - * @NoAdminRequired - */ - public function history($fileId) { - $this->logger->debug("Request history for: $fileId", ["app" => $this->appName]); - - if (!$this->config->isUserAllowedToUse()) { - return ["error" => $this->trans->t("Not permitted")]; - } - - $history = []; - - $user = $this->userSession->getUser(); - $userId = null; - if (!empty($user)) { - $userId = $user->getUID(); - } - - list ($file, $error, $share) = $this->getFile($userId, $fileId); - - if (isset($error)) { - $this->logger->error("History: $fileId $error", ["app" => $this->appName]); - return ["error" => $error]; - } - - if ($fileId === 0) { - $fileId = $file->getId(); - } - - $ownerId = null; - $owner = $file->getFileInfo()->getOwner(); - if ($owner !== null) { - $ownerId = $owner->getUID(); - } - - $versions = array(); - if ($this->versionManager->available - && $owner !== null) { - $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); - } - - $prevVersion = ""; - $versionNum = 0; - foreach ($versions as $version) { - $versionNum = $versionNum + 1; - - $key = $this->fileUtility->getVersionKey($version); - $key = DocumentService::GenerateRevisionId($key); - - $historyItem = [ - "created" => $version->getTimestamp(), - "key" => $key, - "version" => $versionNum - ]; - - $versionId = $version->getRevisionId(); - - $author = FileVersions::getAuthor($ownerId, $fileId, $versionId); - $authorId = $author !== null ? $author["id"] : $ownerId; - $authorName = $author !== null ? $author["name"] : $owner->getDisplayName(); - - $historyItem["user"] = [ - "id" => $this->buildUserId($authorId), - "name" => $authorName - ]; - - $historyData = FileVersions::getHistoryData($ownerId, $fileId, $versionId, $prevVersion); - if ($historyData !== null) { - $historyItem["changes"] = $historyData["changes"]; - $historyItem["serverVersion"] = $historyData["serverVersion"]; - } - - $prevVersion = $versionId; - - array_push($history, $historyItem); - } - - $key = $this->fileUtility->getKey($file, true); - $key = DocumentService::GenerateRevisionId($key); - - $historyItem = [ - "created" => $file->getMTime(), - "key" => $key, - "version" => $versionNum + 1 - ]; - - $versionId = $file->getFileInfo()->getMtime(); - - $author = FileVersions::getAuthor($ownerId, $fileId, $versionId); - if ($author !== null) { - $historyItem["user"] = [ - "id" => $this->buildUserId($author["id"]), - "name" => $author["name"] - ]; - } else if ($owner !== null) { - $historyItem["user"] = [ - "id" => $this->buildUserId($ownerId), - "name" => $owner->getDisplayName() - ]; - } - - $historyData = FileVersions::getHistoryData($ownerId, $fileId, $versionId, $prevVersion); - if ($historyData !== null) { - $historyItem["changes"] = $historyData["changes"]; - $historyItem["serverVersion"] = $historyData["serverVersion"]; - } - - array_push($history, $historyItem); - - return $history; - } - - /** - * Get file attributes of specific version - * - * @param integer $fileId - file identifier - * @param integer $version - file version - * - * @return array - * - * @NoAdminRequired - */ - public function version($fileId, $version) { - $this->logger->debug("Request version for: $fileId ($version)", ["app" => $this->appName]); - - if (!$this->config->isUserAllowedToUse()) { - return ["error" => $this->trans->t("Not permitted")]; - } - - $version = empty($version) ? null : $version; - - $user = $this->userSession->getUser(); - $userId = null; - if (!empty($user)) { - $userId = $user->getUID(); - } - - list ($file, $error, $share) = $this->getFile($userId, $fileId); - - if (isset($error)) { - $this->logger->error("History: $fileId $error", ["app" => $this->appName]); - return ["error" => $error]; - } - - if ($fileId === 0) { - $fileId = $file->getId(); - } - - $owner = null; - $ownerId = null; - $versions = array(); - if ($this->versionManager->available) { - $owner = $file->getFileInfo()->getOwner(); - if ($owner !== null) { - $ownerId = $owner->getUID(); - $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); - } - } - - $key = null; - $fileUrl = null; - $versionId = null; - if ($version > count($versions)) { - $key = $this->fileUtility->getKey($file, true); - $versionId = $file->getFileInfo()->getMtime(); - - $fileUrl = $this->getUrl($file, $user); - } else { - $fileVersion = array_values($versions)[$version - 1]; - - $key = $this->fileUtility->getVersionKey($fileVersion); - $versionId = $fileVersion->getRevisionId(); - - $fileUrl = $this->getUrl($file, $user, null, $version); - } - $key = DocumentService::GenerateRevisionId($key); - $fileName = $file->getName(); - $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); - - $result = [ - "fileType" => $ext, - "url" => $fileUrl, - "version" => $version, - "key" => $key - ]; - - if ($version > 1 - && count($versions) >= $version - 1 - && FileVersions::hasChanges($ownerId, $fileId, $versionId)) { - - $changesUrl = $this->getUrl($file, $user, null, $version, true); - $result["changesUrl"] = $changesUrl; - - $prevVersion = array_values($versions)[$version - 2]; - $prevVersionKey = $this->fileUtility->getVersionKey($prevVersion); - $prevVersionKey = DocumentService::GenerateRevisionId($prevVersionKey); - - $prevVersionUrl = $this->getUrl($file, $user, null, $version - 1); - - $result["previous"] = [ - "fileType" => $ext, - "key" => $prevVersionKey, - "url" => $prevVersionUrl - ]; - } - - if (!empty($this->config->GetDocumentServerSecret())) { - $token = \Firebase\JWT\JWT::encode($result, $this->config->GetDocumentServerSecret(), "HS256"); - $result["token"] = $token; - } - - return $result; - } - - /** - * Restore file version - * - * @param integer $fileId - file identifier - * @param integer $version - file version - * - * @return array - * - * @NoAdminRequired - * @PublicPage - */ - public function restore($fileId, $version) { - $this->logger->debug("Request restore version for: $fileId ($version)", ["app" => $this->appName]); - - if (!$this->config->isUserAllowedToUse()) { - return ["error" => $this->trans->t("Not permitted")]; - } - - $version = empty($version) ? null : $version; - - $user = $this->userSession->getUser(); - $userId = null; - if (!empty($user)) { - $userId = $user->getUID(); - } - - list ($file, $error, $share) = $this->getFile($userId, $fileId); - - if (isset($error)) { - $this->logger->error("Restore: $fileId $error", ["app" => $this->appName]); - return ["error" => $error]; - } - - if ($fileId === 0) { - $fileId = $file->getId(); - } - - $owner = null; - $versions = array(); - if ($this->versionManager->available) { - $owner = $file->getFileInfo()->getOwner(); - if ($owner !== null) { - $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); - } - - if (count($versions) >= $version) { - $fileVersion = array_values($versions)[$version - 1]; - $this->versionManager->rollback($fileVersion); - } - } - - return $this->history($fileId); - } - - /** - * Get presigned url to file - * - * @param string $filePath - file path - * - * @return array - * - * @NoAdminRequired - */ - public function url($filePath) { - $this->logger->debug("Request url for: $filePath", ["app" => $this->appName]); - - if (!$this->config->isUserAllowedToUse()) { - return ["error" => $this->trans->t("Not permitted")]; - } - - $user = $this->userSession->getUser(); - $userId = $user->getUID(); - $userFolder = $this->root->getUserFolder($userId); - - $file = $userFolder->get($filePath); - - if ($file === null) { - $this->logger->error("File for generate presigned url was not found: $dir", ["app" => $this->appName]); - return ["error" => $this->trans->t("File not found")]; - } - if (!$file->isReadable()) { - $this->logger->error("Folder for saving file without permission: $dir", ["app" => $this->appName]); - return ["error" => $this->trans->t("You do not have enough permissions to view the file")]; - } - - $fileName = $file->getName(); - $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); - $fileUrl = $this->getUrl($file, $user); - - $result = [ - "fileType" => $ext, - "url" => $fileUrl - ]; - - if (!empty($this->config->GetDocumentServerSecret())) { - $token = \Firebase\JWT\JWT::encode($result, $this->config->GetDocumentServerSecret(), "HS256"); - $result["token"] = $token; - } - - return $result; - } - - /** - * Download method - * - * @param int $fileId - file identifier - * @param string $toExtension - file extension to download - * @param bool $template - file is template - * - * @return DataDownloadResponse|TemplateResponse - * - * @NoAdminRequired - * @NoCSRFRequired - */ - public function download($fileId, $toExtension = null, $template = false) { - $this->logger->debug("Download: $fileId $toExtension", ["app" => $this->appName]); - - if (!$this->config->isUserAllowedToUse()) { - return $this->renderError($this->trans->t("Not permitted")); - } - - if ($template) { - $templateFile = TemplateManager::GetTemplate($fileId); - - if (empty($templateFile)) { - $this->logger->info("Download: template not found: $fileId", ["app" => $this->appName]); - return $this->renderError($this->trans->t("File not found")); - } - - $file = $templateFile; - } else { - $user = $this->userSession->getUser(); - $userId = null; - if (!empty($user)) { - $userId = $user->getUID(); - } - - list ($file, $error, $share) = $this->getFile($userId, $fileId); - - if (isset($error)) { - $this->logger->error("Download: $fileId $error", ["app" => $this->appName]); - return $this->renderError($error); - } - } - - $canDownload = $this->fileUtility->hasPermissionAttribute($file); - if (!$canDownload) { - return $this->renderError($this->trans->t("Not permitted")); - } - - $fileName = $file->getName(); - $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); - $toExtension = strtolower($toExtension); - - if ($toExtension === null - || $ext === $toExtension - || $template) { - return new DataDownloadResponse($file->getContent(), $fileName, $file->getMimeType()); - } - - $newFileUri = null; - $documentService = new DocumentService($this->trans, $this->config); - $key = $this->fileUtility->getKey($file); - $fileUrl = $this->getUrl($file, $user); - try { - $newFileUri = $documentService->GetConvertedUri($fileUrl, $ext, $toExtension, $key); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "GetConvertedUri: " . $file->getId(), "app" => $this->appName]); - return $this->renderError($e->getMessage()); - } - - try { - $newData = $documentService->Request($newFileUri); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "Failed to download converted file", "app" => $this->appName]); - return $this->renderError($this->trans->t("Failed to download converted file")); - } - - $fileNameWithoutExt = substr($fileName, 0, strlen($fileName) - strlen($ext) - 1); - $newFileName = $fileNameWithoutExt . "." . $toExtension; - - $formats = $this->config->FormatsSetting(); - - return new DataDownloadResponse($newData, $newFileName, $formats[$toExtension]["mime"]); - } - - /** - * Print editor section - * - * @param integer $fileId - file identifier - * @param string $filePath - file path - * @param string $shareToken - access token - * @param integer $version - file version - * @param bool $inframe - open in frame - * @param bool $template - file is template - * @param string $anchor - anchor for file content - * - * @return TemplateResponse|RedirectResponse - * - * @NoAdminRequired - * @NoCSRFRequired - */ - public function index($fileId, $filePath = null, $shareToken = null, $version = 0, $inframe = false, $template = false, $anchor = null) { - $this->logger->debug("Open: $fileId ($version) $filePath", ["app" => $this->appName]); - - if (empty($shareToken) && !$this->userSession->isLoggedIn()) { - $redirectUrl = $this->urlGenerator->linkToRoute("core.login.showLoginForm", [ - "redirect_url" => $this->request->getRequestUri() - ]); - return new RedirectResponse($redirectUrl); - } - - $shareBy = null; - if (!empty($shareToken) && !$this->userSession->isLoggedIn()) { - list ($share, $error) = $this->fileUtility->getShare($shareToken); - if (!empty($share)) { - $shareBy = $share->getSharedBy(); - } - } - - if (!$this->config->isUserAllowedToUse($shareBy)) { - return $this->renderError($this->trans->t("Not permitted")); - } - - $documentServerUrl = $this->config->GetDocumentServerUrl(); - - if (empty($documentServerUrl)) { - $this->logger->error("documentServerUrl is empty", ["app" => $this->appName]); - return $this->renderError($this->trans->t("ONLYOFFICE app is not configured. Please contact admin")); - } - - $params = [ - "documentServerUrl" => $documentServerUrl, - "fileId" => $fileId, - "filePath" => $filePath, - "shareToken" => $shareToken, - "version" => $version, - "template" => $template, - "inframe" => false, - "anchor" => $anchor - ]; - - if ($inframe === true) { - $params["inframe"] = true; - $response = new TemplateResponse($this->appName, "editor", $params, "plain"); - } else { - $response = new TemplateResponse($this->appName, "editor", $params); - } - - $csp = new ContentSecurityPolicy(); - $csp->allowInlineScript(true); - - if (preg_match("/^https?:\/\//i", $documentServerUrl)) { - $csp->addAllowedScriptDomain($documentServerUrl); - $csp->addAllowedFrameDomain($documentServerUrl); - } else { - $csp->addAllowedFrameDomain("'self'"); - } - $response->setContentSecurityPolicy($csp); - - return $response; - } - - /** - * Print public editor section - * - * @param integer $fileId - file identifier - * @param string $shareToken - access token - * @param integer $version - file version - * @param bool $inframe - open in frame - * - * @return TemplateResponse - * - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - */ - public function PublicPage($fileId, $shareToken, $version = 0, $inframe = false) { - return $this->index($fileId, null, $shareToken, $version, $inframe); - } - - /** - * Getting file by identifier - * - * @param string $userId - user identifier - * @param integer $fileId - file identifier - * @param string $filePath - file path - * @param bool $template - file is template - * - * @return array - */ - private function getFile($userId, $fileId, $filePath = null, $template = false) { - if (empty($fileId)) { - return [null, $this->trans->t("FileId is empty"), null]; - } - - try { - $folder = !$template ? $this->root->getUserFolder($userId) : TemplateManager::GetGlobalTemplateDir(); - $files = $folder->getById($fileId); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "getFile: $fileId", "app" => $this->appName]); - return [null, $this->trans->t("Invalid request"), null]; - } - - if (empty($files)) { - $this->logger->info("Files not found: $fileId", ["app" => $this->appName]); - return [null, $this->trans->t("File not found"), null]; - } - - $file = $files[0]; - - if (count($files) > 1 && !empty($filePath)) { - $filePath = "/" . $userId . "/files" . $filePath; - foreach ($files as $curFile) { - if ($curFile->getPath() === $filePath) { - $file = $curFile; - break; - } - } - } - - if (!$file->isReadable()) { - return [null, $this->trans->t("You do not have enough permissions to view the file"), null]; - } - - return [$file, null, null]; - } - - /** - * Generate secure link to download document - * - * @param File $file - file - * @param IUser $user - user with access - * @param string $shareToken - access token - * @param integer $version - file version - * @param bool $changes - is required url to file changes - * @param bool $template - file is template - * - * @return string - */ - private function getUrl($file, $user = null, $shareToken = null, $version = 0, $changes = false, $template = false) { - - $data = [ - "action" => "download", - "fileId" => $file->getId() - ]; - - $userId = null; - if (!empty($user)) { - $userId = $user->getUID(); - $data["userId"] = $userId; - } - if (!empty($shareToken)) { - $data["shareToken"] = $shareToken; - } - if ($version > 0) { - $data["version"] = $version; - } - if ($changes) { - $data["changes"] = true; - } - if ($template) { - $data["template"] = true; - } - - $hashUrl = $this->crypt->GetHash($data); - - $fileUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".callback.download", ["doc" => $hashUrl]); - - if (!$this->config->UseDemo() && !empty($this->config->GetStorageUrl())) { - $fileUrl = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->config->GetStorageUrl(), $fileUrl); - } - - return $fileUrl; - } - - /** - * Generate unique user identifier - * - * @param string $userId - current user identifier - * - * @return string - */ - private function buildUserId($userId) { - $instanceId = $this->config->GetSystemValue("instanceid", true); - $userId = $instanceId . "_" . $userId; - return $userId; - } - - /** - * Return list users who has access to file - * - * @param File $file - file - * - * @return array - */ - private function getAccessList($file) { - $result = []; - - foreach ($this->shareManager->getSharesByPath($file) as $share) { - $accessList = []; - $shareWith = $share->getSharedWith(); - if ($share->getShareType() === Share::SHARE_TYPE_GROUP) { - $group = $this->groupManager->get($shareWith); - $accessList = $group->getUsers(); - } else if ($share->getShareType() === Share::SHARE_TYPE_USER) { - array_push($accessList, $this->userManager->get($shareWith)); - } - - foreach ($accessList as $accessUser) { - if (!in_array($accessUser, $result)) { - array_push($result, $accessUser); - } - } - } - - if (!in_array($file->getOwner(), $result)) { - array_push($result, $file->getOwner()); - } - - return $result; - } - - /** - * Return allow autocomplete usernames - * - * @return bool - */ - private function allowEnumeration() { - return \OC::$server->getConfig()->getAppValue("core", "shareapi_allow_share_dialog_user_enumeration", "yes") === "yes"; - } - - /** - * Return allow autocomplete usernames group member only - * - * @return bool - */ - private function limitEnumerationToGroups() { - if ($this->allowEnumeration()) { - return \OC::$server->getConfig()->getAppValue("core", "shareapi_share_dialog_user_enumeration_group_members", "no") === "yes"; - } - - return false; - } - - /** - * Print error page - * - * @param string $error - error message - * @param string $hint - error hint - * - * @return TemplateResponse - */ - private function renderError($error, $hint = "") { - return new TemplateResponse("", "error", [ - "errors" => [ - [ - "error" => $error, - "hint" => $hint - ] - ] - ], "error"); - } + /** + * Current user session + * + * @var IUserSession + */ + private $userSession; + + /** + * Current user manager + * + * @var IUserManager + */ + private $userManager; + + /** + * Root folder + * + * @var IRootFolder + */ + private $root; + + /** + * Url generator service + * + * @var IURLGenerator + */ + private $urlGenerator; + + /** + * l10n service + * + * @var IL10N + */ + private $trans; + + /** + * Logger + * + * @var ILogger + */ + private $logger; + + /** + * Application configuration + * + * @var AppConfig + */ + private $config; + + /** + * Hash generator + * + * @var Crypt + */ + private $crypt; + + /** + * File utility + * + * @var FileUtility + */ + private $fileUtility; + + /** + * File version manager + * + * @var VersionManager + */ + private $versionManager; + + /** + * Share manager + * + * @var IManager + */ + private $shareManager; + + /** + * Group manager + * + * @var IGroupManager + */ + private $groupManager; + + /** + * Avatar manager + * + * @var IAvatarManager + */ + private $avatarManager; + + /** + * @param string $AppName - application name + * @param IRequest $request - request object + * @param IRootFolder $root - root folder + * @param IUserSession $userSession - current user session + * @param IUserManager $userManager - current user manager + * @param IURLGenerator $urlGenerator - url generator service + * @param IL10N $trans - l10n service + * @param ILogger $logger - logger + * @param AppConfig $config - application configuration + * @param Crypt $crypt - hash generator + * @param IManager $shareManager - Share manager + * @param ISession $session - Session + * @param IGroupManager $groupManager - Group manager + */ + public function __construct( + $AppName, + IRequest $request, + IRootFolder $root, + IUserSession $userSession, + IUserManager $userManager, + IURLGenerator $urlGenerator, + IL10N $trans, + ILogger $logger, + AppConfig $config, + Crypt $crypt, + IManager $shareManager, + ISession $session, + IGroupManager $groupManager + ) { + parent::__construct($AppName, $request); + + $this->userSession = $userSession; + $this->userManager = $userManager; + $this->root = $root; + $this->urlGenerator = $urlGenerator; + $this->trans = $trans; + $this->logger = $logger; + $this->config = $config; + $this->crypt = $crypt; + $this->shareManager = $shareManager; + $this->groupManager = $groupManager; + + $this->versionManager = new VersionManager($AppName, $root); + + $this->fileUtility = new FileUtility($AppName, $trans, $logger, $config, $shareManager, $session); + $this->avatarManager = \OC::$server->getAvatarManager(); + } + + /** + * Create new file in folder + * + * @param string $name - file name + * @param string $dir - folder path + * @param string $templateId - file identifier + * @param string $targetPath - file path for using as template for create + * @param string $shareToken - access token + * + * @return array + * + * @NoAdminRequired + * @PublicPage + */ + public function create($name, $dir, $templateId = null, $targetPath = null, $shareToken = null) { + $this->logger->debug("Create: $name", ["app" => $this->appName]); + + if (empty($shareToken) && !$this->config->isUserAllowedToUse()) { + return ["error" => $this->trans->t("Not permitted")]; + } + + if (empty($name)) { + $this->logger->error("File name for creation was not found: $name", ["app" => $this->appName]); + return ["error" => $this->trans->t("Template not found")]; + } + + $user = null; + if (empty($shareToken)) { + $user = $this->userSession->getUser(); + $userId = $user->getUID(); + $userFolder = $this->root->getUserFolder($userId); + } else { + list($userFolder, $error, $share) = $this->fileUtility->getNodeByToken($shareToken); + + if (isset($error)) { + $this->logger->error("Create: $error", ["app" => $this->appName]); + return ["error" => $error]; + } + + if ($userFolder instanceof File) { + return ["error" => $this->trans->t("You don't have enough permission to create")]; + } + + if (!empty($shareToken) && ($share->getPermissions() & Constants::PERMISSION_CREATE) === 0) { + $this->logger->error("Create in public folder without access", ["app" => $this->appName]); + return ["error" => $this->trans->t("You do not have enough permissions to view the file")]; + } + } + + $folder = $userFolder->get($dir); + + if ($folder === null) { + $this->logger->error("Folder for file creation was not found: $dir", ["app" => $this->appName]); + return ["error" => $this->trans->t("The required folder was not found")]; + } + if (!($folder->isCreatable() && $folder->isUpdateable())) { + $this->logger->error("Folder for file creation without permission: $dir", ["app" => $this->appName]); + return ["error" => $this->trans->t("You don't have enough permission to create")]; + } + + if (!empty($templateId)) { + $templateFile = TemplateManager::getTemplate($templateId); + if ($templateFile) { + $template = $templateFile->getContent(); + } + } elseif (!empty($targetPath)) { + $targetFile = $userFolder->get($targetPath); + + $canDownload = $this->fileUtility->hasPermissionAttribute($targetFile); + if (!$canDownload) { + return ["error" => $this->trans->t("Not permitted")]; + } + + $targetId = $targetFile->getId(); + $targetName = $targetFile->getName(); + $targetExt = strtolower(pathinfo($targetName, PATHINFO_EXTENSION)); + $targetKey = $this->fileUtility->getKey($targetFile); + + $fileUrl = $this->getUrl($targetFile, $user, $shareToken); + + $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); + $documentService = new DocumentService($this->trans, $this->config); + try { + $newFileUri = $documentService->getConvertedUri($fileUrl, $targetExt, $ext, $targetKey); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "getConvertedUri: " . $targetFile->getId(), "app" => $this->appName]); + return ["error" => $e->getMessage()]; + } + $template = $documentService->request($newFileUri); + } else { + $template = TemplateManager::getEmptyTemplate($name); + } + + if (!$template) { + $this->logger->error("Template for file creation not found: $name ($templateId)", ["app" => $this->appName]); + return ["error" => $this->trans->t("Template not found")]; + } + + $name = $folder->getNonExistingName($name); + + try { + $file = $folder->newFile($name); + + $file->putContent($template); + } catch (NotPermittedException $e) { + $this->logger->logException($e, ["message" => "Can't create file: $name", "app" => $this->appName]); + return ["error" => $this->trans->t("Can't create file")]; + } catch (ForbiddenException $e) { + $this->logger->logException($e, ["message" => "Can't put file: $name", "app" => $this->appName]); + return ["error" => $this->trans->t("Can't create file")]; + } + + $fileInfo = $file->getFileInfo(); + + $result = Helper::formatFileInfo($fileInfo); + return $result; + } + + /** + * Create new file in folder from editor + * + * @param string $name - file name + * @param string $dir - folder path + * @param string $templateId - file identifier + * + * @return TemplateResponse|RedirectResponse + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function createNew($name, $dir, $templateId = null) { + $this->logger->debug("Create from editor: $name in $dir", ["app" => $this->appName]); + + $result = $this->create($name, $dir, $templateId); + if (isset($result["error"])) { + return $this->renderError($result["error"]); + } + + $openEditor = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".editor.index", ["fileId" => $result["id"]]); + return new RedirectResponse($openEditor); + } + + /** + * Get users + * + * @param $fileId - file identifier + * @param $operationType - type of operation + * + * @return array + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function users($fileId, $operationType = null) { + $this->logger->debug("Search users", ["app" => $this->appName]); + $result = []; + + if (!$this->config->isUserAllowedToUse()) { + return $result; + } + + if (!$this->allowEnumeration()) { + return $result; + } + + $autocompleteMemberGroup = false; + if ($this->limitEnumerationToGroups()) { + $autocompleteMemberGroup = true; + } + + $currentUser = $this->userSession->getUser(); + $currentUserId = $currentUser->getUID(); + + list($file, $error, $share) = $this->getFile($currentUserId, $fileId); + if (isset($error)) { + $this->logger->error("Users: $fileId $error", ["app" => $this->appName]); + return $result; + } + + $canShare = (($file->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE); + + $shareMemberGroups = $this->shareManager->shareWithGroupMembersOnly(); + + $all = false; + $users = []; + if ($canShare && $operationType !== "protect") { + if ($shareMemberGroups || $autocompleteMemberGroup) { + $currentUserGroups = $this->groupManager->getUserGroupIds($currentUser); + foreach ($currentUserGroups as $currentUserGroup) { + $group = $this->groupManager->get($currentUserGroup); + foreach ($group->getUsers() as $user) { + if (!\in_array($user, $users)) { + array_push($users, $user); + } + } + } + } else { + $users = $this->userManager->search(""); + $all = true; + } + } + + if (!$all) { + $accessList = $this->getAccessList($file); + foreach ($accessList as $accessUser) { + if (!\in_array($accessUser, $users)) { + array_push($users, $accessUser); + } + } + } + + foreach ($users as $user) { + $email = $user->getEMailAddress(); + if ($user->getUID() != $currentUserId && (!empty($email) || $operationType === "protect")) { + $userElement = [ + "name" => $user->getDisplayName(), + "id" => $user->getUID() + ]; + if (!empty($email)) { + $userElement["email"] = $email; + } + array_push($result, $userElement); + } + } + return $result; + } + + /** + * Get user for Info + * + * @param string $userIds - users identifiers + * + * @return array + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function userInfo($userIds) { + $result = []; + $userIds = json_decode($userIds, true); + + if ($userIds !== null && is_array($userIds)) { + foreach ($userIds as $userId) { + $userData = []; + $user = $this->userManager->get($this->getUserId($userId)); + if (!empty($user)) { + $userData = [ + "name" => $user->getDisplayName(), + "id" => $userId + ]; + $avatar = $this->avatarManager->getAvatar($user->getUID()); + if ($avatar->exists()) { + $userData["image"] = "data:image/png;base64," . $avatar->get()->__toString(); + } + array_push($result, $userData); + } + } + } + return $result; + } + + /** + * Send notify about mention + * + * @param int $fileId - file identifier + * @param string $anchor - the anchor on target content + * @param string $comment - comment + * @param array $emails - emails array to whom to send notify + * + * @return array + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function mention($fileId, $anchor, $comment, $emails) { + $this->logger->debug("mention: from $fileId to " . json_encode($emails), ["app" => $this->appName]); + + if (!$this->config->isUserAllowedToUse()) { + return ["error" => $this->trans->t("Not permitted")]; + } + + if (empty($emails)) { + return ["error" => $this->trans->t("Failed to send notification")]; + } + + $recipientIds = []; + foreach ($emails as $email) { + $recipients = $this->userManager->getByEmail($email); + foreach ($recipients as $recipient) { + $recipientId = $recipient->getUID(); + if (!\in_array($recipientId, $recipientIds)) { + array_push($recipientIds, $recipientId); + } + } + } + + $user = $this->userSession->getUser(); + $userId = null; + if (!empty($user)) { + $userId = $user->getUID(); + } + + list($file, $error, $share) = $this->getFile($userId, $fileId); + if (isset($error)) { + $this->logger->error("Mention: $fileId $error", ["app" => $this->appName]); + return ["error" => $this->trans->t("Failed to send notification")]; + } + + foreach ($emails as $email) { + $substrToDelete = "+" . $email . " "; + $comment = str_replace($substrToDelete, "", $comment); + } + + //Length from ownCloud: + //https://github.com/owncloud/core/blob/master/lib/private/Notification/Notification.php#L181 + $maxLen = 64; + if (\strlen($comment) > $maxLen) { + $ending = "..."; + $comment = substr($comment, 0, ($maxLen - \strlen($ending))) . $ending; + } + + $notificationManager = \OC::$server->getNotificationManager(); + $notification = $notificationManager->createNotification(); + $notification->setApp($this->appName) + ->setDateTime(new \DateTime()) + ->setObject("mention", $comment) + ->setSubject( + "mention_info", + [ + "notifierId" => $userId, + "fileId" => $file->getId(), + "fileName" => $file->getName(), + "anchor" => $anchor + ] + ); + + $shareMemberGroups = $this->shareManager->shareWithGroupMembersOnly(); + $canShare = ($file->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE; + + $currentUserGroups = []; + if ($shareMemberGroups) { + $currentUserGroups = $this->groupManager->getUserGroupIds($user); + } + + $accessList = $this->getAccessList($file); + + foreach ($recipientIds as $recipientId) { + $recipient = $this->userManager->get($recipientId); + $isAvailable = \in_array($recipient, $accessList); + + if (!$isAvailable + && $file->getFileInfo()->getMountPoint() instanceof \OCA\Files_External\Config\ExternalMountPoint + ) { + $recipientFolder = $this->root->getUserFolder($recipientId); + $recipientFile = $recipientFolder->getById($file->getId()); + + $isAvailable = !empty($recipientFile); + } + + if (!$isAvailable) { + if (!$canShare) { + continue; + } + if ($shareMemberGroups) { + $recipientGroups = $this->groupManager->getUserGroupIds($recipient); + if (empty(array_intersect($currentUserGroups, $recipientGroups))) { + continue; + } + } + + $share = $this->shareManager->newShare(); + $share->setNode($file) + ->setShareType(Share::SHARE_TYPE_USER) + ->setSharedBy($userId) + ->setSharedWith($recipientId) + ->setShareOwner($userId) + ->setPermissions(Constants::PERMISSION_READ); + + $this->shareManager->createShare($share); + + $this->logger->debug("mention: share $fileId to $recipientId", ["app" => $this->appName]); + } + + $notification->setUser($recipientId); + + $notificationManager->notify($notification); + } + + return ["message" => $this->trans->t("Notification sent successfully")]; + } + + /** + * Reference data + * + * @param array $referenceData - reference data + * @param string $path - file path + * + * @return array + * + * @NoAdminRequired + * @PublicPage + */ + public function reference($referenceData, $path = null) { + $this->logger->debug("reference: " . json_encode($referenceData) . " $path", ["app" => $this->appName]); + + if (!$this->config->isUserAllowedToUse()) { + return ["error" => $this->trans->t("Not permitted")]; + } + + $user = $this->userSession->getUser(); + if (empty($user)) { + return ["error" => $this->trans->t("Not permitted")]; + } + + $userId = $user->getUID(); + + $file = null; + $fileId = (integer)($referenceData["fileKey"] ?? 0); + if (!empty($fileId) + && $referenceData["instanceId"] === $this->config->getSystemValue("instanceid", true) + ) { + list($file, $error, $share) = $this->getFile($userId, $fileId); + } + + $userFolder = $this->root->getUserFolder($userId); + if ($file === null + && $path !== null + && $userFolder->nodeExists($path) + ) { + $node = $userFolder->get($path); + if ($node instanceof File + && $node->isReadable() + ) { + $file = $node; + } + } + + if ($file === null) { + $this->logger->error("Reference not found: $fileId $path", ["app" => $this->appName]); + return ["error" => $this->trans->t("File not found")]; + } + + $fileName = $file->getName(); + $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $key = $this->fileUtility->getKey($file); + $key = DocumentService::generateRevisionId($key); + + $response = [ + "fileType" => $ext, + "path" => $userFolder->getRelativePath($file->getPath()), + "key" => $key, + "referenceData" => [ + "fileKey" => $file->getId(), + "instanceId" => $this->config->getSystemValue("instanceid", true), + ], + "url" => $this->getUrl($file, $user), + ]; + + if (!empty($this->config->getDocumentServerSecret())) { + $token = \Firebase\JWT\JWT::encode($response, $this->config->getDocumentServerSecret(), "HS256"); + $response["token"] = $token; + } + + return $response; + } + + /** + * Conversion file to Office Open XML format + * + * @param integer $fileId - file identifier + * @param string $shareToken - access token + * + * @return array + * + * @NoAdminRequired + * @PublicPage + */ + public function convert($fileId, $shareToken = null) { + $this->logger->debug("Convert: $fileId", ["app" => $this->appName]); + + if (empty($shareToken) && !$this->config->isUserAllowedToUse()) { + return ["error" => $this->trans->t("Not permitted")]; + } + + $user = $this->userSession->getUser(); + $userId = null; + if (!empty($user)) { + $userId = $user->getUID(); + } + + list($file, $error, $share) = empty($shareToken) ? $this->getFile($userId, $fileId) : $this->fileUtility->getFileByToken($fileId, $shareToken); + + if (isset($error)) { + $this->logger->error("Convertion: $fileId $error", ["app" => $this->appName]); + return ["error" => $error]; + } + + if (!empty($shareToken) && ($share->getPermissions() & Constants::PERMISSION_CREATE) === 0) { + $this->logger->error("Convertion in public folder without access: $fileId", ["app" => $this->appName]); + return ["error" => $this->trans->t("You do not have enough permissions to view the file")]; + } + + $canDownload = $this->fileUtility->hasPermissionAttribute($file); + if (!$canDownload) { + return ["error" => $this->trans->t("Not permitted")]; + } + + $fileName = $file->getName(); + $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $format = $this->config->formatsSetting()[$ext]; + if (!isset($format)) { + $this->logger->info("Format for convertion not supported: $fileName", ["app" => $this->appName]); + return ["error" => $this->trans->t("Format is not supported")]; + } + + if (!isset($format["conv"]) || $format["conv"] !== true) { + $this->logger->info("Conversion is not required: $fileName", ["app" => $this->appName]); + return ["error" => $this->trans->t("Conversion is not required")]; + } + + $internalExtension = "docx"; + switch ($format["type"]) { + case "cell": + $internalExtension = "xlsx"; + break; + case "slide": + $internalExtension = "pptx"; + break; + } + + $newFileUri = null; + $documentService = new DocumentService($this->trans, $this->config); + $key = $this->fileUtility->getKey($file); + $fileUrl = $this->getUrl($file, $user, $shareToken); + try { + $newFileUri = $documentService->getConvertedUri($fileUrl, $ext, $internalExtension, $key); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "getConvertedUri: " . $file->getId(), "app" => $this->appName]); + return ["error" => $e->getMessage()]; + } + + $folder = $file->getParent(); + if (!($folder->isCreatable() && $folder->isUpdateable())) { + $folder = $this->root->getUserFolder($userId); + } + + try { + $newData = $documentService->request($newFileUri); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "Failed to download converted file", "app" => $this->appName]); + return ["error" => $this->trans->t("Failed to download converted file")]; + } + + $fileNameWithoutExt = substr($fileName, 0, \strlen($fileName) - \strlen($ext) - 1); + $newFileName = $folder->getNonExistingName($fileNameWithoutExt . "." . $internalExtension); + + try { + $file = $folder->newFile($newFileName); + + $file->putContent($newData); + } catch (NotPermittedException $e) { + $this->logger->logException($e, ["message" => "Can't create file: $newFileName", "app" => $this->appName]); + return ["error" => $this->trans->t("Can't create file")]; + } catch (ForbiddenException $e) { + $this->logger->logException($e, ["message" => "Can't put file: $newFileName", "app" => $this->appName]); + return ["error" => $this->trans->t("Can't create file")]; + } + + $fileInfo = $file->getFileInfo(); + + $result = Helper::formatFileInfo($fileInfo); + return $result; + } + + /** + * Save file to folder + * + * @param string $name - file name + * @param string $dir - folder path + * @param string $url - file url + * + * @return array + * + * @NoAdminRequired + * @PublicPage + */ + public function save($name, $dir, $url) { + $this->logger->debug("Save: $name", ["app" => $this->appName]); + + if (!$this->config->isUserAllowedToUse()) { + return ["error" => $this->trans->t("Not permitted")]; + } + + $userId = $this->userSession->getUser()->getUID(); + $userFolder = $this->root->getUserFolder($userId); + + $folder = $userFolder->get($dir); + + if ($folder === null) { + $this->logger->error("Folder for saving file was not found: $dir", ["app" => $this->appName]); + return ["error" => $this->trans->t("The required folder was not found")]; + } + if (!($folder->isCreatable() && $folder->isUpdateable())) { + $this->logger->error("Folder for saving file without permission: $dir", ["app" => $this->appName]); + return ["error" => $this->trans->t("You don't have enough permission to create")]; + } + + $documentServerUrl = $this->config->getDocumentServerUrl(); + + if (empty($documentServerUrl)) { + $this->logger->error("documentServerUrl is empty", ["app" => $this->appName]); + return ["error" => $this->trans->t("ONLYOFFICE app is not configured. Please contact admin")]; + } + + if (parse_url($url, PHP_URL_HOST) !== parse_url($documentServerUrl, PHP_URL_HOST)) { + $this->logger->error("Incorrect domain in file url", ["app" => $this->appName]); + return ["error" => $this->trans->t("The domain in the file url does not match the domain of the Document server")]; + } + + $url = $this->config->replaceDocumentServerUrlToInternal($url); + + try { + $documentService = new DocumentService($this->trans, $this->config); + $newData = $documentService->request($url); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "Failed to download file for saving", "app" => $this->appName]); + return ["error" => $this->trans->t("Download failed")]; + } + + $name = $folder->getNonExistingName($name); + + try { + $file = $folder->newFile($name); + + $file->putContent($newData); + } catch (NotPermittedException $e) { + $this->logger->logException($e, ["message" => "Can't save file: $name", "app" => $this->appName]); + return ["error" => $this->trans->t("Can't create file")]; + } catch (ForbiddenException $e) { + $this->logger->logException($e, ["message" => "Can't put file: $name", "app" => $this->appName]); + return ["error" => $this->trans->t("Can't create file")]; + } + + $fileInfo = $file->getFileInfo(); + + $result = Helper::formatFileInfo($fileInfo); + return $result; + } + + /** + * Get versions history for file + * + * @param integer $fileId - file identifier + * + * @return array + * + * @NoAdminRequired + */ + public function history($fileId) { + $this->logger->debug("request history for: $fileId", ["app" => $this->appName]); + + if (!$this->config->isUserAllowedToUse()) { + return ["error" => $this->trans->t("Not permitted")]; + } + + $history = []; + + $user = $this->userSession->getUser(); + $userId = null; + if (!empty($user)) { + $userId = $user->getUID(); + } + + list($file, $error, $share) = $this->getFile($userId, $fileId); + + if (isset($error)) { + $this->logger->error("History: $fileId $error", ["app" => $this->appName]); + return ["error" => $error]; + } + + if ($fileId === 0) { + $fileId = $file->getId(); + } + + $ownerId = null; + $owner = $file->getFileInfo()->getOwner(); + if ($owner !== null) { + $ownerId = $owner->getUID(); + } + + $versions = []; + if ($this->versionManager->available + && $owner !== null + ) { + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + } + + $prevVersion = ""; + $versionNum = 0; + foreach ($versions as $version) { + $versionNum = $versionNum + 1; + + $key = $this->fileUtility->getVersionKey($version); + $key = DocumentService::generateRevisionId($key); + + $historyItem = [ + "created" => $version->getTimestamp(), + "key" => $key, + "version" => $versionNum + ]; + + $versionId = $version->getRevisionId(); + + $author = FileVersions::getAuthor($ownerId, $fileId, $versionId); + $authorId = $author !== null ? $author["id"] : $ownerId; + $authorName = $author !== null ? $author["name"] : $owner->getDisplayName(); + + $historyItem["user"] = [ + "id" => $this->buildUserId($authorId), + "name" => $authorName + ]; + + $historyData = FileVersions::getHistoryData($ownerId, $fileId, $versionId, $prevVersion); + if ($historyData !== null) { + $historyItem["changes"] = $historyData["changes"]; + $historyItem["serverVersion"] = $historyData["serverVersion"]; + } + + $prevVersion = $versionId; + + array_push($history, $historyItem); + } + + $key = $this->fileUtility->getKey($file, true); + $key = DocumentService::generateRevisionId($key); + + $historyItem = [ + "created" => $file->getMTime(), + "key" => $key, + "version" => $versionNum + 1 + ]; + + $versionId = $file->getFileInfo()->getMtime(); + + $author = FileVersions::getAuthor($ownerId, $fileId, $versionId); + if ($author !== null) { + $historyItem["user"] = [ + "id" => $this->buildUserId($author["id"]), + "name" => $author["name"] + ]; + } elseif ($owner !== null) { + $historyItem["user"] = [ + "id" => $this->buildUserId($ownerId), + "name" => $owner->getDisplayName() + ]; + } + + $historyData = FileVersions::getHistoryData($ownerId, $fileId, $versionId, $prevVersion); + if ($historyData !== null) { + $historyItem["changes"] = $historyData["changes"]; + $historyItem["serverVersion"] = $historyData["serverVersion"]; + } + + array_push($history, $historyItem); + + return $history; + } + + /** + * Get file attributes of specific version + * + * @param integer $fileId - file identifier + * @param integer $version - file version + * + * @return array + * + * @NoAdminRequired + */ + public function version($fileId, $version) { + $this->logger->debug("request version for: $fileId ($version)", ["app" => $this->appName]); + + if (!$this->config->isUserAllowedToUse()) { + return ["error" => $this->trans->t("Not permitted")]; + } + + $version = empty($version) ? null : $version; + + $user = $this->userSession->getUser(); + $userId = null; + if (!empty($user)) { + $userId = $user->getUID(); + } + + list($file, $error, $share) = $this->getFile($userId, $fileId); + + if (isset($error)) { + $this->logger->error("History: $fileId $error", ["app" => $this->appName]); + return ["error" => $error]; + } + + if ($fileId === 0) { + $fileId = $file->getId(); + } + + $owner = null; + $ownerId = null; + $versions = []; + if ($this->versionManager->available) { + $owner = $file->getFileInfo()->getOwner(); + if ($owner !== null) { + $ownerId = $owner->getUID(); + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + } + } + + $key = null; + $fileUrl = null; + $versionId = null; + if ($version > \count($versions)) { + $key = $this->fileUtility->getKey($file, true); + $versionId = $file->getFileInfo()->getMtime(); + + $fileUrl = $this->getUrl($file, $user); + } else { + $fileVersion = array_values($versions)[$version - 1]; + + $key = $this->fileUtility->getVersionKey($fileVersion); + $versionId = $fileVersion->getRevisionId(); + + $fileUrl = $this->getUrl($file, $user, null, $version); + } + $key = DocumentService::generateRevisionId($key); + $fileName = $file->getName(); + $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + + $result = [ + "fileType" => $ext, + "url" => $fileUrl, + "version" => $version, + "key" => $key + ]; + + if ($version > 1 + && \count($versions) >= $version - 1 + && FileVersions::hasChanges($ownerId, $fileId, $versionId) + ) { + $changesUrl = $this->getUrl($file, $user, null, $version, true); + $result["changesUrl"] = $changesUrl; + + $prevVersion = array_values($versions)[$version - 2]; + $prevVersionKey = $this->fileUtility->getVersionKey($prevVersion); + $prevVersionKey = DocumentService::generateRevisionId($prevVersionKey); + + $prevVersionUrl = $this->getUrl($file, $user, null, $version - 1); + + $result["previous"] = [ + "fileType" => $ext, + "key" => $prevVersionKey, + "url" => $prevVersionUrl + ]; + } + + if (!empty($this->config->getDocumentServerSecret())) { + $token = \Firebase\JWT\JWT::encode($result, $this->config->getDocumentServerSecret(), "HS256"); + $result["token"] = $token; + } + + return $result; + } + + /** + * Restore file version + * + * @param integer $fileId - file identifier + * @param integer $version - file version + * + * @return array + * + * @NoAdminRequired + * @PublicPage + */ + public function restore($fileId, $version) { + $this->logger->debug("request restore version for: $fileId ($version)", ["app" => $this->appName]); + + if (!$this->config->isUserAllowedToUse()) { + return ["error" => $this->trans->t("Not permitted")]; + } + + $version = empty($version) ? null : $version; + + $user = $this->userSession->getUser(); + $userId = null; + if (!empty($user)) { + $userId = $user->getUID(); + } + + list($file, $error, $share) = $this->getFile($userId, $fileId); + + if (isset($error)) { + $this->logger->error("Restore: $fileId $error", ["app" => $this->appName]); + return ["error" => $error]; + } + + if ($fileId === 0) { + $fileId = $file->getId(); + } + + $owner = null; + $versions = []; + if ($this->versionManager->available) { + $owner = $file->getFileInfo()->getOwner(); + if ($owner !== null) { + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + } + + if (\count($versions) >= $version) { + $fileVersion = array_values($versions)[$version - 1]; + $this->versionManager->rollback($fileVersion); + } + } + + return $this->history($fileId); + } + + /** + * Get presigned url to file + * + * @param string $filePath - file path + * + * @return array + * + * @NoAdminRequired + */ + public function url($filePath) { + $this->logger->debug("request url for: $filePath", ["app" => $this->appName]); + + if (!$this->config->isUserAllowedToUse()) { + return ["error" => $this->trans->t("Not permitted")]; + } + + $user = $this->userSession->getUser(); + $userId = $user->getUID(); + $userFolder = $this->root->getUserFolder($userId); + + $file = $userFolder->get($filePath); + + if ($file === null) { + $this->logger->error("File for generate presigned url was not found: $dir", ["app" => $this->appName]); + return ["error" => $this->trans->t("File not found")]; + } + + $canDownload = true; + + $fileStorage = $file->getStorage(); + if ($fileStorage->instanceOfStorage("\OCA\Files_Sharing\SharedStorage")) { + $share = $fileStorage->getShare(); + $canDownload = FileUtility::canShareDownload($share); + } + + if (!$file->isReadable() || !$canDownload) { + $this->logger->error("File without permission: $dir", ["app" => $this->appName]); + return ["error" => $this->trans->t("You do not have enough permissions to view the file")]; + } + + $fileName = $file->getName(); + $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $fileUrl = $this->getUrl($file, $user); + + $result = [ + "fileType" => $ext, + "url" => $fileUrl + ]; + + if (!empty($this->config->getDocumentServerSecret())) { + $token = \Firebase\JWT\JWT::encode($result, $this->config->getDocumentServerSecret(), "HS256"); + $result["token"] = $token; + } + + return $result; + } + + /** + * Download method + * + * @param int $fileId - file identifier + * @param string $toExtension - file extension to download + * @param bool $template - file is template + * + * @return DataDownloadResponse|TemplateResponse + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function download($fileId, $toExtension = null, $template = false) { + $this->logger->debug("Download: $fileId $toExtension", ["app" => $this->appName]); + + if (!$this->config->isUserAllowedToUse()) { + return $this->renderError($this->trans->t("Not permitted")); + } + + if ($template) { + $templateFile = TemplateManager::getTemplate($fileId); + + if (empty($templateFile)) { + $this->logger->info("Download: template not found: $fileId", ["app" => $this->appName]); + return $this->renderError($this->trans->t("File not found")); + } + + $file = $templateFile; + } else { + $user = $this->userSession->getUser(); + $userId = null; + if (!empty($user)) { + $userId = $user->getUID(); + } + + list($file, $error, $share) = $this->getFile($userId, $fileId); + + if (isset($error)) { + $this->logger->error("Download: $fileId $error", ["app" => $this->appName]); + return $this->renderError($error); + } + } + + $canDownload = $this->fileUtility->hasPermissionAttribute($file); + if (!$canDownload) { + return $this->renderError($this->trans->t("Not permitted")); + } + + $fileStorage = $file->getStorage(); + if ($fileStorage->instanceOfStorage("\OCA\Files_Sharing\SharedStorage")) { + $share = empty($share) ? $fileStorage->getShare() : $share; + if (!FileUtility::canShareDownload($share)) { + return $this->renderError($this->trans->t("Not permitted")); + } + } + + $fileName = $file->getName(); + $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $toExtension = strtolower($toExtension); + + if ($toExtension === null + || $ext === $toExtension + || $template + ) { + return new DataDownloadResponse($file->getContent(), $fileName, $file->getMimeType()); + } + + $newFileUri = null; + $documentService = new DocumentService($this->trans, $this->config); + $key = $this->fileUtility->getKey($file); + $fileUrl = $this->getUrl($file, $user); + try { + $newFileUri = $documentService->getConvertedUri($fileUrl, $ext, $toExtension, $key); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "getConvertedUri: " . $file->getId(), "app" => $this->appName]); + return $this->renderError($e->getMessage()); + } + + try { + $newData = $documentService->request($newFileUri); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "Failed to download converted file", "app" => $this->appName]); + return $this->renderError($this->trans->t("Failed to download converted file")); + } + + $fileNameWithoutExt = substr($fileName, 0, \strlen($fileName) - \strlen($ext) - 1); + $newFileName = $fileNameWithoutExt . "." . $toExtension; + + $mimeType = $this->config->getMimeType($toExtension); + + return new DataDownloadResponse($newData, $newFileName, $mimeType); + } + + /** + * Print editor section + * + * @param integer $fileId - file identifier + * @param string $filePath - file path + * @param string $shareToken - access token + * @param integer $version - file version + * @param bool $inframe - open in frame + * @param bool $template - file is template + * @param string $anchor - anchor for file content + * + * @return TemplateResponse|RedirectResponse + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function index($fileId, $filePath = null, $shareToken = null, $version = 0, $inframe = false, $template = false, $anchor = null) { + $this->logger->debug("Open: $fileId ($version) $filePath", ["app" => $this->appName]); + + if (empty($shareToken) && !$this->userSession->isLoggedIn()) { + $redirectUrl = $this->urlGenerator->linkToRoute( + "core.login.showLoginForm", + [ + "redirect_url" => $this->request->getRequestUri() + ] + ); + return new RedirectResponse($redirectUrl); + } + + $shareBy = null; + if (!empty($shareToken) && !$this->userSession->isLoggedIn()) { + list($share, $error) = $this->fileUtility->getShare($shareToken); + if (!empty($share)) { + $shareBy = $share->getSharedBy(); + } + } + + if (!$this->config->isUserAllowedToUse($shareBy)) { + return $this->renderError($this->trans->t("Not permitted")); + } + + $documentServerUrl = $this->config->getDocumentServerUrl(); + + if (empty($documentServerUrl)) { + $this->logger->error("documentServerUrl is empty", ["app" => $this->appName]); + return $this->renderError($this->trans->t("ONLYOFFICE app is not configured. Please contact admin")); + } + + $params = [ + "documentServerUrl" => $documentServerUrl, + "fileId" => $fileId, + "filePath" => $filePath, + "shareToken" => $shareToken, + "version" => $version, + "template" => $template, + "inframe" => false, + "anchor" => $anchor + ]; + + if ($inframe === true) { + $params["inframe"] = true; + $response = new TemplateResponse($this->appName, "editor", $params, "plain"); + } else { + $response = new TemplateResponse($this->appName, "editor", $params); + } + + $csp = new ContentSecurityPolicy(); + $csp->allowInlineScript(true); + + if (preg_match("/^https?:\/\//i", $documentServerUrl)) { + $csp->addAllowedScriptDomain($documentServerUrl); + $csp->addAllowedFrameDomain($documentServerUrl); + } else { + $csp->addAllowedFrameDomain("'self'"); + } + $response->setContentSecurityPolicy($csp); + + return $response; + } + + /** + * Print public editor section + * + * @param integer $fileId - file identifier + * @param string $shareToken - access token + * @param integer $version - file version + * @param bool $inframe - open in frame + * + * @return TemplateResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + */ + public function publicPage($fileId, $shareToken, $version = 0, $inframe = false) { + return $this->index($fileId, null, $shareToken, $version, $inframe); + } + + /** + * Getting file by identifier + * + * @param string $userId - user identifier + * @param integer $fileId - file identifier + * @param string $filePath - file path + * @param bool $template - file is template + * + * @return array + */ + private function getFile($userId, $fileId, $filePath = null, $template = false) { + if (empty($fileId)) { + return [null, $this->trans->t("FileId is empty"), null]; + } + + try { + $folder = !$template ? $this->root->getUserFolder($userId) : TemplateManager::getGlobalTemplateDir(); + $files = $folder->getById($fileId); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "getFile: $fileId", "app" => $this->appName]); + return [null, $this->trans->t("Invalid request"), null]; + } + + if (empty($files)) { + $this->logger->info("Files not found: $fileId", ["app" => $this->appName]); + return [null, $this->trans->t("File not found"), null]; + } + + $file = $files[0]; + + if (\count($files) > 1 && !empty($filePath)) { + $filePath = "/" . $userId . "/files" . $filePath; + foreach ($files as $curFile) { + if ($curFile->getPath() === $filePath) { + $file = $curFile; + break; + } + } + } + + if (!$file->isReadable()) { + return [null, $this->trans->t("You do not have enough permissions to view the file"), null]; + } + + return [$file, null, null]; + } + + /** + * Generate secure link to download document + * + * @param File $file - file + * @param IUser $user - user with access + * @param string $shareToken - access token + * @param integer $version - file version + * @param bool $changes - is required url to file changes + * @param bool $template - file is template + * + * @return string + */ + private function getUrl($file, $user = null, $shareToken = null, $version = 0, $changes = false, $template = false) { + $data = [ + "action" => "download", + "fileId" => $file->getId() + ]; + + $userId = null; + if (!empty($user)) { + $userId = $user->getUID(); + $data["userId"] = $userId; + } + if (!empty($shareToken)) { + $data["shareToken"] = $shareToken; + } + if ($version > 0) { + $data["version"] = $version; + } + if ($changes) { + $data["changes"] = true; + } + if ($template) { + $data["template"] = true; + } + + $hashUrl = $this->crypt->getHash($data); + + $fileUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".callback.download", ["doc" => $hashUrl]); + + if (!$this->config->useDemo() && !empty($this->config->getStorageUrl())) { + $fileUrl = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->config->getStorageUrl(), $fileUrl); + } + + return $fileUrl; + } + + /** + * Generate unique user identifier + * + * @param string $userId - current user identifier + * + * @return string + */ + private function buildUserId($userId) { + $instanceId = $this->config->getSystemValue("instanceid", true); + $userId = $instanceId . "_" . $userId; + return $userId; + } + + /** + * Return list users who has access to file + * + * @param File $file - file + * + * @return array + */ + private function getAccessList($file) { + $result = []; + + foreach ($this->shareManager->getSharesByPath($file) as $share) { + $accessList = []; + $shareWith = $share->getSharedWith(); + if ($share->getShareType() === Share::SHARE_TYPE_GROUP) { + $group = $this->groupManager->get($shareWith); + $accessList = $group->getUsers(); + } elseif ($share->getShareType() === Share::SHARE_TYPE_USER) { + array_push($accessList, $this->userManager->get($shareWith)); + } + + foreach ($accessList as $accessUser) { + if (!\in_array($accessUser, $result)) { + array_push($result, $accessUser); + } + } + } + + if (!\in_array($file->getOwner(), $result)) { + array_push($result, $file->getOwner()); + } + + return $result; + } + + /** + * Return allow autocomplete usernames + * + * @return bool + */ + private function allowEnumeration() { + return \OC::$server->getConfig()->getAppValue("core", "shareapi_allow_share_dialog_user_enumeration", "yes") === "yes"; + } + + /** + * Return allow autocomplete usernames group member only + * + * @return bool + */ + private function limitEnumerationToGroups() { + if ($this->allowEnumeration()) { + return \OC::$server->getConfig()->getAppValue("core", "shareapi_share_dialog_user_enumeration_group_members", "no") === "yes"; + } + + return false; + } + + /** + * Get Nextcloud userId from unique user identifier + * + * @param string $userId - current user identifier + * + * @return string + */ + private function getUserId($userId) { + if (str_contains($userId, "_")) { + $userIdExp = explode("_", $userId); + $userId = end($userIdExp); + } + return $userId; + } + + /** + * Print error page + * + * @param string $error - error message + * @param string $hint - error hint + * + * @return TemplateResponse + */ + private function renderError($error, $hint = "") { + return new TemplateResponse( + "", + "error", + [ + "errors" => [ + [ + "error" => $error, + "hint" => $hint + ] + ] + ], + "error" + ); + } } diff --git a/controller/federationcontroller.php b/controller/federationcontroller.php index f6526b68..ff6610cd 100644 --- a/controller/federationcontroller.php +++ b/controller/federationcontroller.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,132 +39,132 @@ * OCS handler */ class FederationController extends OCSController { - - /** - * Logger - * - * @var ILogger - */ - private $logger; - - /** - * Application configuration - * - * @var AppConfig - */ - public $config; - - /** - * File utility - * - * @var FileUtility - */ - private $fileUtility; - - /** - * @param string $AppName - application name - * @param IRequest $request - request object - * @param IL10N $trans - l10n service - * @param ILogger $logger - logger - * @param IManager $shareManager - Share manager - * @param IManager $ISession - Session - */ - public function __construct($AppName, - IRequest $request, - IL10N $trans, - ILogger $logger, - IManager $shareManager, - ISession $session - ) { - parent::__construct($AppName, $request); - - $this->logger = $logger; - - $this->config = new AppConfig($this->appName); - $this->fileUtility = new FileUtility($AppName, $trans, $logger, $this->config, $shareManager, $session); - } - - /** - * Returns the origin document key for editor - * - * @param string $shareToken - access token - * @param string $path - file path - * - * @return Result - * - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - */ - public function key($shareToken, $path) { - list ($file, $error, $share) = $this->fileUtility->getFileByToken(null, $shareToken, $path); - - if (isset($error)) { - $this->logger->error("Federated getFileByToken: $error", ["app" => $this->appName]); - return new Result(["error" => $error]); - } - - $key = $this->fileUtility->getKey($file, true); - - $key = DocumentService::GenerateRevisionId($key); - - $this->logger->debug("Federated request get for " . $file->getId() . " key $key", ["app" => $this->appName]); - - return new Result(["key" => $key]); - } - - /** - * Lock the origin document key for editor - * - * @param string $shareToken - access token - * @param string $path - file path - * @param bool $lock - status - * @param bool $fs - status - * - * @return Result - * - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - */ - public function keylock($shareToken, $path, $lock, $fs) { - list ($file, $error, $share) = $this->fileUtility->getFileByToken(null, $shareToken, $path); - - if (isset($error)) { - $this->logger->error("Federated getFileByToken: $error", ["app" => $this->appName]); - return new Result(["error" => $error]); - } - - $fileId = $file->getId(); - - if (RemoteInstance::isRemoteFile($file)) { - $isLock = RemoteInstance::lockRemoteKey($file, $lock, $fs); - if (!$isLock) { - return new Result(["error" => "Failed request"]); - } - } else { - KeyManager::lock($fileId, $lock); - if (!empty($fs)) { - KeyManager::setForcesave($fileId, $fs); - } - } - - $this->logger->debug("Federated request lock for " . $fileId, ["app" => $this->appName]); - return new Result(); - } - - /** - * Health check instance - * - * @return Result - * - * @NoAdminRequired - * @NoCSRFRequired - * @PublicPage - */ - public function healthcheck() { - $this->logger->debug("Federated healthcheck", ["app" => $this->appName]); - - return new Result(["alive" => true]); - } -} \ No newline at end of file + /** + * Logger + * + * @var ILogger + */ + private $logger; + + /** + * Application configuration + * + * @var AppConfig + */ + public $config; + + /** + * File utility + * + * @var FileUtility + */ + private $fileUtility; + + /** + * @param string $AppName - application name + * @param IRequest $request - request object + * @param IL10N $trans - l10n service + * @param ILogger $logger - logger + * @param IManager $shareManager - Share manager + * @param IManager $session - Session + */ + public function __construct( + $AppName, + IRequest $request, + IL10N $trans, + ILogger $logger, + IManager $shareManager, + ISession $session + ) { + parent::__construct($AppName, $request); + + $this->logger = $logger; + + $this->config = new AppConfig($this->appName); + $this->fileUtility = new FileUtility($AppName, $trans, $logger, $this->config, $shareManager, $session); + } + + /** + * Returns the origin document key for editor + * + * @param string $shareToken - access token + * @param string $path - file path + * + * @return Result + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + */ + public function key($shareToken, $path) { + list($file, $error, $share) = $this->fileUtility->getFileByToken(null, $shareToken, $path); + + if (isset($error)) { + $this->logger->error("Federated getFileByToken: $error", ["app" => $this->appName]); + return new Result(["error" => $error]); + } + + $key = $this->fileUtility->getKey($file, true); + + $key = DocumentService::generateRevisionId($key); + + $this->logger->debug("Federated request get for " . $file->getId() . " key $key", ["app" => $this->appName]); + + return new Result(["key" => $key]); + } + + /** + * Lock the origin document key for editor + * + * @param string $shareToken - access token + * @param string $path - file path + * @param bool $lock - status + * @param bool $fs - status + * + * @return Result + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + */ + public function keylock($shareToken, $path, $lock, $fs) { + list($file, $error, $share) = $this->fileUtility->getFileByToken(null, $shareToken, $path); + + if (isset($error)) { + $this->logger->error("Federated getFileByToken: $error", ["app" => $this->appName]); + return new Result(["error" => $error]); + } + + $fileId = $file->getId(); + + if (RemoteInstance::isRemoteFile($file)) { + $isLock = RemoteInstance::lockRemoteKey($file, $lock, $fs); + if (!$isLock) { + return new Result(["error" => "Failed request"]); + } + } else { + KeyManager::lock($fileId, $lock); + if (!empty($fs)) { + KeyManager::setForcesave($fileId, $fs); + } + } + + $this->logger->debug("Federated request lock for " . $fileId, ["app" => $this->appName]); + return new Result(); + } + + /** + * Health check instance + * + * @return Result + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + */ + public function healthcheck() { + $this->logger->debug("Federated healthcheck", ["app" => $this->appName]); + + return new Result(["alive" => true]); + } +} diff --git a/controller/joblistcontroller.php b/controller/joblistcontroller.php index c2722692..02c4e9fd 100644 --- a/controller/joblistcontroller.php +++ b/controller/joblistcontroller.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,85 +40,94 @@ * @package OCA\Onlyoffice\Controller */ class JobListController extends Controller { + /** + * Logger + * + * @var ILogger + */ + private $logger; - /** - * Logger - * - * @var ILogger - */ - private $logger; + /** + * Job list + * + * @var IJobList + */ + private $jobList; - /** - * Job list - * - * @var IJobList - */ - private $jobList; + /** + * Application configuration + * + * @var AppConfig + */ + private $config; - /** - * Application configuration - * - * @var AppConfig - */ - private $config; + /** + * JobListController constructor. + * + * @param string $AppName - application name + * @param IRequest $request - request object + * @param ILogger $logger + * @param AppConfig $config - application configuration + * @param IJobList $jobList - job list + */ + public function __construct($AppName, IRequest $request, ILogger $logger, AppConfig $config, IJobList $jobList) { + parent::__construct($AppName, $request); + $this->logger = $logger; + $this->config = $config; + $this->jobList = $jobList; + } - /** - * JobListController constructor. - * - * @param string $AppName - application name - * @param IRequest $request - request object - * @param ILogger $logger - * @param AppConfig $config - application configuration - * @param IJobList $jobList - job list - */ - public function __construct($AppName, IRequest $request, ILogger $logger, AppConfig $config, IJobList $jobList) { - parent::__construct($AppName, $request); - $this->logger = $logger; - $this->config = $config; - $this->jobList = $jobList; - } + /** + * Add a job to list + * + * @param IJob|string $job + * + * @return void + */ + private function addJob($job) { + if (!$this->jobList->has($job, null)) { + $this->jobList->add($job); + $this->logger->debug("Job '" . $job . "' added to JobList.", ["app" => $this->appName]); + } + } - /** - * Add a job to list - * - * @param IJob|string $job - */ - private function addJob($job) { - if (!$this->jobList->has($job, null)) { - $this->jobList->add($job); - $this->logger->debug("Job '".$job."' added to JobList.", ["app" => $this->appName]); - } - } + /** + * Remove a job from list + * + * @param IJob|string $job + * + * @return void + */ + private function removeJob($job) { + if ($this->jobList->has($job, null)) { + $this->jobList->remove($job); + $this->logger->debug("Job '" . $job . "' removed from JobList.", ["app" => $this->appName]); + } + } - /** - * Remove a job from list - * - * @param IJob|string $job - */ - private function removeJob($job) { - if ($this->jobList->has($job, null)) { - $this->jobList->remove($job); - $this->logger->debug("Job '".$job."' removed from JobList.", ["app" => $this->appName]); - } - } + /** + * Add or remove EditorsCheck job depending on the value of _editors_check_interval + * + * @return void + */ + private function checkEditorsCheckJob() { + if (!$this->config->getCronChecker()) { + $this->removeJob(EditorsCheck::class); + return; + } + if ($this->config->getEditorsCheckInterval() > 0) { + $this->addJob(EditorsCheck::class); + } else { + $this->removeJob(EditorsCheck::class); + } + } - /** - * Add or remove EditorsCheck job depending on the value of _editors_check_interval - * - */ - private function checkEditorsCheckJob() { - if ($this->config->GetEditorsCheckInterval() > 0) { - $this->addJob(EditorsCheck::class); - } else { - $this->removeJob(EditorsCheck::class); - } - } - - /** - * Method for sequentially calling checks of all jobs - * - */ - public function checkAllJobs() { - $this->checkEditorsCheckJob(); - } + /** + * Method for sequentially calling checks of all jobs + * + * @return void + */ + public function checkAllJobs() { + $this->checkEditorsCheckJob(); + } } diff --git a/controller/settingsapicontroller.php b/controller/settingsapicontroller.php index cc9521f8..74483722 100644 --- a/controller/settingsapicontroller.php +++ b/controller/settingsapicontroller.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,54 +31,54 @@ * Settings controller for the administration page */ class SettingsApiController extends OCSController { + /** + * Url generator service + * + * @var IURLGenerator + */ + private $urlGenerator; - /** - * Url generator service - * - * @var IURLGenerator - */ - private $urlGenerator; + /** + * Application configuration + * + * @var AppConfig + */ + private $config; - /** - * Application configuration - * - * @var AppConfig - */ - private $config; + /** + * @param string $AppName - application name + * @param IRequest $request - request object + * @param IURLGenerator $urlGenerator - url generator service + * @param AppConfig $config - application configuration + */ + public function __construct( + $AppName, + IRequest $request, + IURLGenerator $urlGenerator, + AppConfig $config + ) { + parent::__construct($AppName, $request); - /** - * @param string $AppName - application name - * @param IRequest $request - request object - * @param IURLGenerator $urlGenerator - url generator service - * @param AppConfig $config - application configuration - */ - public function __construct($AppName, - IRequest $request, - IURLGenerator $urlGenerator, - AppConfig $config - ) { - parent::__construct($AppName, $request); + $this->urlGenerator = $urlGenerator; + $this->config = $config; + } - $this->urlGenerator = $urlGenerator; - $this->config = $config; - } + /** + * Get document server url + * + * @return JSONResponse + * + * @NoAdminRequired + * @CORS + */ + public function getDocServerUrl() { + $url = $this->config->getDocumentServerUrl(); + if (!$this->config->settingsAreSuccessful()) { + $url = ""; + } elseif (!preg_match("/^https?:\/\//i", $url)) { + $url = $this->urlGenerator->getAbsoluteURL($url); + } - /** - * Get document server url - * - * @return JSONResponse - * - * @NoAdminRequired - * @CORS - */ - public function GetDocServerUrl() { - $url = $this->config->GetDocumentServerUrl(); - if (!$this->config->SettingsAreSuccessful()) { - $url = ""; - } else if (!preg_match("/^https?:\/\//i", $url)) { - $url = $this->urlGenerator->getAbsoluteURL($url); - } - - return new JSONResponse(["documentServerUrl" => $url]); - } -} \ No newline at end of file + return new JSONResponse(["documentServerUrl" => $url]); + } +} diff --git a/controller/settingscontroller.php b/controller/settingscontroller.php index 5e112e51..072e1186 100644 --- a/controller/settingscontroller.php +++ b/controller/settingscontroller.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,292 +37,296 @@ * Settings controller for the administration page */ class SettingsController extends Controller { + /** + * l10n service + * + * @var IL10N + */ + private $trans; - /** - * l10n service - * - * @var IL10N - */ - private $trans; + /** + * Logger + * + * @var ILogger + */ + private $logger; - /** - * Logger - * - * @var ILogger - */ - private $logger; + /** + * Application configuration + * + * @var AppConfig + */ + private $config; - /** - * Application configuration - * - * @var AppConfig - */ - private $config; + /** + * Url generator service + * + * @var IURLGenerator + */ + private $urlGenerator; - /** - * Url generator service - * - * @var IURLGenerator - */ - private $urlGenerator; + /** + * Hash generator + * + * @var Crypt + */ + private $crypt; - /** - * Hash generator - * - * @var Crypt - */ - private $crypt; + /** + * @param string $AppName - application name + * @param IRequest $request - request object + * @param IURLGenerator $urlGenerator - url generator service + * @param IL10N $trans - l10n service + * @param ILogger $logger - logger + * @param AppConfig $config - application configuration + * @param Crypt $crypt - hash generator + */ + public function __construct( + $AppName, + IRequest $request, + IURLGenerator $urlGenerator, + IL10N $trans, + ILogger $logger, + AppConfig $config, + Crypt $crypt + ) { + parent::__construct($AppName, $request); - /** - * @param string $AppName - application name - * @param IRequest $request - request object - * @param IURLGenerator $urlGenerator - url generator service - * @param IL10N $trans - l10n service - * @param ILogger $logger - logger - * @param AppConfig $config - application configuration - * @param Crypt $crypt - hash generator - */ - public function __construct($AppName, - IRequest $request, - IURLGenerator $urlGenerator, - IL10N $trans, - ILogger $logger, - AppConfig $config, - Crypt $crypt - ) { - parent::__construct($AppName, $request); + $this->urlGenerator = $urlGenerator; + $this->trans = $trans; + $this->logger = $logger; + $this->config = $config; + $this->crypt = $crypt; + } - $this->urlGenerator = $urlGenerator; - $this->trans = $trans; - $this->logger = $logger; - $this->config = $config; - $this->crypt = $crypt; - } + /** + * Print config section + * + * @return TemplateResponse + */ + public function index() { + $data = [ + "documentserver" => $this->config->getDocumentServerUrl(true), + "documentserverInternal" => $this->config->getDocumentServerInternalUrl(true), + "storageUrl" => $this->config->getStorageUrl(), + "verifyPeerOff" => $this->config->getVerifyPeerOff(), + "secret" => $this->config->getDocumentServerSecret(true), + "jwtHeader" => $this->config->jwtHeader(true), + "demo" => $this->config->getDemoData(), + "currentServer" => $this->urlGenerator->getAbsoluteURL("/"), + "formats" => $this->config->formatsSetting(), + "sameTab" => $this->config->getSameTab(), + "preview" => $this->config->getPreview(), + "cronChecker" => $this->config->getCronChecker(), + "versionHistory" => $this->config->getVersionHistory(), + "protection" => $this->config->getProtection(), + "encryption" => $this->config->checkEncryptionModule(), + "limitGroups" => $this->config->getLimitGroups(), + "chat" => $this->config->getCustomizationChat(), + "compactHeader" => $this->config->getCustomizationCompactHeader(), + "feedback" => $this->config->getCustomizationFeedback(), + "forcesave" => $this->config->getCustomizationForcesave(), + "help" => $this->config->getCustomizationHelp(), + "toolbarNoTabs" => $this->config->getCustomizationToolbarNoTabs(), + "successful" => $this->config->settingsAreSuccessful(), + "plugins" => $this->config->getCustomizationPlugins(), + "macros" => $this->config->getCustomizationMacros(), + "reviewDisplay" => $this->config->getCustomizationReviewDisplay(), + "theme" => $this->config->getCustomizationTheme(), + "templates" => $this->getGlobalTemplates(), + "linkToDocs" => $this->config->getLinkToDocs() + ]; + return new TemplateResponse($this->appName, "settings", $data, "blank"); + } - /** - * Print config section - * - * @return TemplateResponse - */ - public function index() { - $data = [ - "documentserver" => $this->config->GetDocumentServerUrl(true), - "documentserverInternal" => $this->config->GetDocumentServerInternalUrl(true), - "storageUrl" => $this->config->GetStorageUrl(), - "verifyPeerOff" => $this->config->GetVerifyPeerOff(), - "secret" => $this->config->GetDocumentServerSecret(true), - "jwtHeader" => $this->config->JwtHeader(true), - "demo" => $this->config->GetDemoData(), - "currentServer" => $this->urlGenerator->getAbsoluteURL("/"), - "formats" => $this->config->FormatsSetting(), - "sameTab" => $this->config->GetSameTab(), - "preview" => $this->config->GetPreview(), - "versionHistory" => $this->config->GetVersionHistory(), - "protection" => $this->config->GetProtection(), - "encryption" => $this->config->checkEncryptionModule(), - "limitGroups" => $this->config->GetLimitGroups(), - "chat" => $this->config->GetCustomizationChat(), - "compactHeader" => $this->config->GetCustomizationCompactHeader(), - "feedback" => $this->config->GetCustomizationFeedback(), - "forcesave" => $this->config->GetCustomizationForcesave(), - "help" => $this->config->GetCustomizationHelp(), - "toolbarNoTabs" => $this->config->GetCustomizationToolbarNoTabs(), - "successful" => $this->config->SettingsAreSuccessful(), - "plugins" => $this->config->GetCustomizationPlugins(), - "macros" => $this->config->GetCustomizationMacros(), - "reviewDisplay" => $this->config->GetCustomizationReviewDisplay(), - "theme" => $this->config->GetCustomizationTheme(), - "templates" => $this->GetGlobalTemplates(), - "linkToDocs" => $this->config->GetLinkToDocs() - ]; - return new TemplateResponse($this->appName, "settings", $data, "blank"); - } + /** + * Save address settings + * + * @param string $documentserver - document service address + * @param string $documentserverInternal - document service address available from ownCloud + * @param string $storageUrl - ownCloud address available from document server + * @param bool $verifyPeerOff - parameter verification setting + * @param string $secret - secret key for signature + * @param string $jwtHeader - jwt header + * @param bool $demo - use demo server + * + * @return array + */ + public function saveAddress( + $documentserver, + $documentserverInternal, + $storageUrl, + $verifyPeerOff, + $secret, + $jwtHeader, + $demo + ) { + $error = null; + if (!$this->config->selectDemo($demo === true)) { + $error = $this->trans->t("The 30-day test period is over, you can no longer connect to demo ONLYOFFICE Docs server."); + } + if ($demo !== true) { + $this->config->setDocumentServerUrl($documentserver); + $this->config->setVerifyPeerOff($verifyPeerOff); + $this->config->setDocumentServerInternalUrl($documentserverInternal); + $this->config->setDocumentServerSecret($secret); + $this->config->setJwtHeader($jwtHeader); + } + $this->config->setStorageUrl($storageUrl); - /** - * Save address settings - * - * @param string $jwtHeader - jwt header - * @param string $documentserver - document service address - * @param string $documentserverInternal - document service address available from ownCloud - * @param string $storageUrl - ownCloud address available from document server - * @param bool $verifyPeerOff - parameter verification setting - * @param string $secret - secret key for signature - * @param bool $demo - use demo server - * - * @return array - */ - public function SaveAddress($documentserver, - $documentserverInternal, - $storageUrl, - $verifyPeerOff, - $secret, - $jwtHeader, - $demo - ) { - $error = null; - if (!$this->config->SelectDemo($demo === true)) { - $error = $this->trans->t("The 30-day test period is over, you can no longer connect to demo ONLYOFFICE Docs server."); - } - if ($demo !== true) { - $this->config->SetDocumentServerUrl($documentserver); - $this->config->SetVerifyPeerOff($verifyPeerOff); - $this->config->SetDocumentServerInternalUrl($documentserverInternal); - $this->config->SetDocumentServerSecret($secret); - $this->config->SetJwtHeader($jwtHeader); - } - $this->config->SetStorageUrl($storageUrl); + $version = null; + if (empty($error)) { + $documentserver = $this->config->getDocumentServerUrl(); + if (!empty($documentserver)) { + $documentService = new DocumentService($this->trans, $this->config); + list($error, $version) = $documentService->checkDocServiceUrl($this->urlGenerator, $this->crypt); + $this->config->setSettingsError($error); + } - $version = null; - if (empty($error)) { - $documentserver = $this->config->GetDocumentServerUrl(); - if (!empty($documentserver)) { - $documentService = new DocumentService($this->trans, $this->config); - list ($error, $version) = $documentService->checkDocServiceUrl($this->urlGenerator, $this->crypt); - $this->config->SetSettingsError($error); - } + if ($this->config->checkEncryptionModule() === true) { + $this->logger->info("SaveSettings when encryption is enabled", ["app" => $this->appName]); + } + } - if ($this->config->checkEncryptionModule() === true) { - $this->logger->info("SaveSettings when encryption is enabled", ["app" => $this->appName]); - } - } + return [ + "documentserver" => $this->config->getDocumentServerUrl(true), + "verifyPeerOff" => $this->config->getVerifyPeerOff(), + "documentserverInternal" => $this->config->getDocumentServerInternalUrl(true), + "storageUrl" => $this->config->getStorageUrl(), + "secret" => $this->config->getDocumentServerSecret(true), + "jwtHeader" => $this->config->jwtHeader(true), + "error" => $error, + "version" => $version, + ]; + } - return [ - "documentserver" => $this->config->GetDocumentServerUrl(true), - "verifyPeerOff" => $this->config->GetVerifyPeerOff(), - "documentserverInternal" => $this->config->GetDocumentServerInternalUrl(true), - "storageUrl" => $this->config->GetStorageUrl(), - "secret" => $this->config->GetDocumentServerSecret(true), - "jwtHeader" => $this->config->JwtHeader(true), - "error" => $error, - "version" => $version, - ]; - } + /** + * Save common settings + * + * @param array $defFormats - formats array with default action + * @param array $editFormats - editable formats array + * @param bool $sameTab - open in the same tab + * @param bool $preview - generate preview files + * @param bool $cronChecker - disable cron checker + * @param bool $versionHistory - keep version history + * @param array $limitGroups - list of groups + * @param bool $chat - display chat + * @param bool $compactHeader - display compact header + * @param bool $feedback - display feedback + * @param bool $forcesave - forcesave + * @param bool $help - display help + * @param bool $toolbarNoTabs - display toolbar tab + * @param string $reviewDisplay - review viewing mode + * @param string $theme - default theme mode + * + * @return array + */ + public function saveCommon( + $defFormats, + $editFormats, + $sameTab, + $preview, + $cronChecker, + $versionHistory, + $limitGroups, + $chat, + $compactHeader, + $feedback, + $forcesave, + $help, + $toolbarNoTabs, + $reviewDisplay, + $theme + ) { + $this->config->setDefaultFormats($defFormats); + $this->config->setEditableFormats($editFormats); + $this->config->setSameTab($sameTab); + $this->config->setPreview($preview); + $this->config->setCronChecker($cronChecker); + $this->config->setVersionHistory($versionHistory); + $this->config->setLimitGroups($limitGroups); + $this->config->setCustomizationChat($chat); + $this->config->setCustomizationCompactHeader($compactHeader); + $this->config->setCustomizationFeedback($feedback); + $this->config->setCustomizationForcesave($forcesave); + $this->config->setCustomizationHelp($help); + $this->config->setCustomizationToolbarNoTabs($toolbarNoTabs); + $this->config->setCustomizationReviewDisplay($reviewDisplay); + $this->config->setCustomizationTheme($theme); - /** - * Save common settings - * - * @param array $defFormats - formats array with default action - * @param array $editFormats - editable formats array - * @param bool $sameTab - open in the same tab - * @param bool $preview - generate preview files - * @param bool $versionHistory - keep version history - * @param array $limitGroups - list of groups - * @param bool $chat - display chat - * @param bool $compactHeader - display compact header - * @param bool $feedback - display feedback - * @param bool $forcesave - forcesave - * @param bool $help - display help - * @param bool $toolbarNoTabs - display toolbar tab - * @param string $reviewDisplay - review viewing mode - * @param string $theme - default theme mode - * - * @return array - */ - public function SaveCommon($defFormats, - $editFormats, - $sameTab, - $preview, - $versionHistory, - $limitGroups, - $chat, - $compactHeader, - $feedback, - $forcesave, - $help, - $toolbarNoTabs, - $reviewDisplay, - $theme - ) { + return [ + ]; + } - $this->config->SetDefaultFormats($defFormats); - $this->config->SetEditableFormats($editFormats); - $this->config->SetSameTab($sameTab); - $this->config->SetPreview($preview); - $this->config->SetVersionHistory($versionHistory); - $this->config->SetLimitGroups($limitGroups); - $this->config->SetCustomizationChat($chat); - $this->config->SetCustomizationCompactHeader($compactHeader); - $this->config->SetCustomizationFeedback($feedback); - $this->config->SetCustomizationForcesave($forcesave); - $this->config->SetCustomizationHelp($help); - $this->config->SetCustomizationToolbarNoTabs($toolbarNoTabs); - $this->config->SetCustomizationReviewDisplay($reviewDisplay); - $this->config->SetCustomizationTheme($theme); + /** + * Save security settings + * + * @param bool $plugins - enable plugins + * @param bool $macros - run document macros + * @param string $protection - protection + * + * @return array + */ + public function saveSecurity( + $plugins, + $macros, + $protection + ) { + $this->config->setCustomizationPlugins($plugins); + $this->config->setCustomizationMacros($macros); + $this->config->setProtection($protection); - return [ - ]; - } + return [ + ]; + } - /** - * Save security settings - * - * @param bool $plugins - enable plugins - * @param bool $macros - run document macros - * @param string $protection - protection - * - * @return array - */ - public function SaveSecurity($plugins, - $macros, - $protection - ) { + /** + * Clear all version history + * + * @return array + */ + public function clearHistory() { + FileVersions::clearHistory(); - $this->config->SetCustomizationPlugins($plugins); - $this->config->SetCustomizationMacros($macros); - $this->config->SetProtection($protection); + return [ + ]; + } - return [ - ]; - } + /** + * Get app settings + * + * @return array + * + * @NoAdminRequired + * @PublicPage + */ + public function getSettings() { + $result = [ + "formats" => $this->config->formatsSetting(), + "sameTab" => $this->config->getSameTab(), + "shareAttributesVersion" => $this->config->shareAttributesVersion() + ]; + return $result; + } - /** - * Clear all version history - * - * @return array - */ - public function ClearHistory() { + /** + * Get global templates + * + * @return array + */ + private function getGlobalTemplates() { + $templates = []; + $templatesList = TemplateManager::getGlobalTemplates(); - FileVersions::clearHistory(); + foreach ($templatesList as $templateItem) { + $template = [ + "id" => $templateItem->getId(), + "name" => $templateItem->getName(), + "type" => TemplateManager::getTypeTemplate($templateItem->getMimeType()) + ]; + array_push($templates, $template); + } - return [ - ]; - } - - /** - * Get app settings - * - * @return array - * - * @NoAdminRequired - * @PublicPage - */ - public function GetSettings() { - $result = [ - "formats" => $this->config->FormatsSetting(), - "sameTab" => $this->config->GetSameTab(), - "shareAttributesVersion" => $this->config->ShareAttributesVersion() - ]; - return $result; - } - - /** - * Get global templates - * - * @return array - */ - private function GetGlobalTemplates() { - $templates = []; - $templatesList = TemplateManager::GetGlobalTemplates(); - - foreach ($templatesList as $templateItem) { - $template = [ - "id" => $templateItem->getId(), - "name" => $templateItem->getName(), - "type" => TemplateManager::GetTypeTemplate($templateItem->getMimeType()) - ]; - array_push($templates, $template); - } - - return $templates; - } + return $templates; + } } diff --git a/controller/templatecontroller.php b/controller/templatecontroller.php index d9977750..481769fd 100644 --- a/controller/templatecontroller.php +++ b/controller/templatecontroller.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,133 +31,135 @@ * Template controller for template manage */ class TemplateController extends Controller { - - /** - * l10n service - * - * @var IL10N - */ - private $trans; - - /** - * Logger - * - * @var ILogger - */ - private $logger; - - /** - * @param string $AppName - application name - * @param IRequest $request - request object - * @param IL10N $trans - l10n service - */ - public function __construct($AppName, - IRequest $request, - IL10N $trans, - ILogger $logger - ) { - parent::__construct($AppName, $request); - - $this->trans = $trans; - $this->logger = $logger; - } - - /** - * Get templates - * - * @return array - * - * @NoAdminRequired - */ - public function GetTemplates() { - $templatesList = TemplateManager::GetGlobalTemplates(); - - $templates = []; - foreach ($templatesList as $templatesItem) { - $template = [ - "id" => $templatesItem->getId(), - "name" => $templatesItem->getName(), - "type" => TemplateManager::GetTypeTemplate($templatesItem->getMimeType()) - ]; - array_push($templates, $template); - } - - return $templates; - } - - /** - * Add global template - * - * @return array - */ - public function AddTemplate() { - - $file = $this->request->getUploadedFile("file"); - - if (!is_null($file)) { - if (is_uploaded_file($file["tmp_name"]) && $file["error"] === 0) { - if (!TemplateManager::IsTemplateType($file["name"])) { - return [ - "error" => $this->trans->t("Template must be in OOXML format") - ]; - } - - $templateDir = TemplateManager::GetGlobalTemplateDir(); - if ($templateDir->nodeExists($file["name"])) { - return [ - "error" => $this->trans->t("Template already exists") - ]; - } - - $templateContent = file_get_contents($file["tmp_name"]); - $template = $templateDir->newFile($file["name"]); - $template->putContent($templateContent); - - $fileInfo = $template->getFileInfo(); - $result = [ - "id" => $fileInfo->getId(), - "name" => $fileInfo->getName(), - "type" => TemplateManager::GetTypeTemplate($fileInfo->getMimeType()) - ]; - - $this->logger->debug("Template: added " . $fileInfo->getName(), ["app" => $this->appName]); - - return $result; - } - } - - return [ - "error" => $this->trans->t("Invalid file provided") - ]; - } - - /** - * Delete template - * - * @param string $templateId - file identifier - */ - public function DeleteTemplate($templateId) { - $templateDir = TemplateManager::GetGlobalTemplateDir(); - - try { - $templates = $templateDir->getById($templateId); - } catch(\Exception $e) { - $this->logger->logException($e, ["message" => "DeleteTemplate: $templateId", "app" => $this->AppName]); - return [ - "error" => $this->trans->t("Failed to delete template") - ]; - } - - if (empty($templates)) { - $this->logger->info("Template not found: $templateId", ["app" => $this->AppName]); - return [ - "error" => $this->trans->t("Failed to delete template") - ]; - } - - $templates[0]->delete(); - - $this->logger->debug("Template: deleted " . $templates[0]->getName(), ["app" => $this->appName]); - return []; - } -} \ No newline at end of file + /** + * l10n service + * + * @var IL10N + */ + private $trans; + + /** + * Logger + * + * @var ILogger + */ + private $logger; + + /** + * @param string $AppName - application name + * @param IRequest $request - request object + * @param IL10N $trans - l10n service + * @param ILogger $logger - logger + */ + public function __construct( + $AppName, + IRequest $request, + IL10N $trans, + ILogger $logger + ) { + parent::__construct($AppName, $request); + + $this->trans = $trans; + $this->logger = $logger; + } + + /** + * Get templates + * + * @return array + * + * @NoAdminRequired + */ + public function getTemplates() { + $templatesList = TemplateManager::getGlobalTemplates(); + + $templates = []; + foreach ($templatesList as $templatesItem) { + $template = [ + "id" => $templatesItem->getId(), + "name" => $templatesItem->getName(), + "type" => TemplateManager::getTypeTemplate($templatesItem->getMimeType()) + ]; + array_push($templates, $template); + } + + return $templates; + } + + /** + * Add global template + * + * @return array + */ + public function addTemplate() { + $file = $this->request->getUploadedFile("file"); + + if ($file !== null) { + if (is_uploaded_file($file["tmp_name"]) && $file["error"] === 0) { + if (!TemplateManager::isTemplateType($file["name"])) { + return [ + "error" => $this->trans->t("Template must be in OOXML format") + ]; + } + + $templateDir = TemplateManager::getGlobalTemplateDir(); + if ($templateDir->nodeExists($file["name"])) { + return [ + "error" => $this->trans->t("Template already exists") + ]; + } + + $templateContent = file_get_contents($file["tmp_name"]); + $template = $templateDir->newFile($file["name"]); + $template->putContent($templateContent); + + $fileInfo = $template->getFileInfo(); + $result = [ + "id" => $fileInfo->getId(), + "name" => $fileInfo->getName(), + "type" => TemplateManager::getTypeTemplate($fileInfo->getMimeType()) + ]; + + $this->logger->debug("Template: added " . $fileInfo->getName(), ["app" => $this->appName]); + + return $result; + } + } + + return [ + "error" => $this->trans->t("Invalid file provided") + ]; + } + + /** + * Delete template + * + * @param string $templateId - file identifier + * + * @return array + */ + public function deleteTemplate($templateId) { + $templateDir = TemplateManager::getGlobalTemplateDir(); + + try { + $templates = $templateDir->getById($templateId); + } catch(\Exception $e) { + $this->logger->logException($e, ["message" => "deleteTemplate: $templateId", "app" => $this->AppName]); + return [ + "error" => $this->trans->t("Failed to delete template") + ]; + } + + if (empty($templates)) { + $this->logger->info("Template not found: $templateId", ["app" => $this->AppName]); + return [ + "error" => $this->trans->t("Failed to delete template") + ]; + } + + $templates[0]->delete(); + + $this->logger->debug("Template: deleted " . $templates[0]->getName(), ["app" => $this->appName]); + return []; + } +} diff --git a/controller/webassetcontroller.php b/controller/webassetcontroller.php index 200db7c4..ada20499 100644 --- a/controller/webassetcontroller.php +++ b/controller/webassetcontroller.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,47 +34,50 @@ * @package OCA\Onlyoffice\Controller */ class WebAssetController extends Controller { + /** + * @var ILogger + */ + private $logger; - /** - * @var ILogger - */ - private $logger; + /** + * WebAssetController constructor. + * + * @param string $AppName - application name + * @param IRequest $request - request object + * @param ILogger $logger + */ + public function __construct($AppName, IRequest $request, ILogger $logger) { + parent::__construct($AppName, $request); + $this->logger = $logger; + } - /** - * WebAssetController constructor. - * - * @param string $AppName - application name - * @param IRequest $request - request object - * @param ILogger $logger - */ - public function __construct($AppName, IRequest $request, ILogger $logger) { - parent::__construct($AppName, $request); - $this->logger = $logger; - } - - /** - * Loads the onlyoffice.js file for integration into ownCloud Web - * - * @PublicPage - * @NoCSRFRequired - * - * @return Response - */ - public function get(): Response { - $basePath = \dirname(__DIR__,1); - $filePath = \realpath( $basePath . '/js/web/onlyoffice.js'); - try { - return new DataDisplayResponse(\file_get_contents($filePath), Http::STATUS_OK, [ - 'Content-Type' => "text/javascript", - 'Content-Length' => \filesize($filePath), - 'Cache-Control' => 'max-age=0, no-cache, no-store, must-revalidate', - 'Pragma' => 'no-cache', - 'Expires' => 'Tue, 24 Sep 1985 22:15:00 GMT', - 'X-Frame-Options' => 'DENY' - ]); - } catch(\Exception $e) { - $this->logger->logException($e, ['app' => $this->appName]); - return new DataResponse(["message" => $e->getMessage()], Http::STATUS_NOT_FOUND); - } - } + /** + * Loads the onlyoffice.js file for integration into ownCloud Web + * + * @PublicPage + * @NoCSRFRequired + * + * @return Response + */ + public function get(): Response { + $basePath = \dirname(__DIR__, 1); + $filePath = \realpath($basePath . '/js/web/onlyoffice.js'); + try { + return new DataDisplayResponse( + \file_get_contents($filePath), + Http::STATUS_OK, + [ + 'Content-Type' => "text/javascript", + 'Content-Length' => \filesize($filePath), + 'Cache-Control' => 'max-age=0, no-cache, no-store, must-revalidate', + 'Pragma' => 'no-cache', + 'Expires' => 'Tue, 24 Sep 1985 22:15:00 GMT', + 'X-Frame-Options' => 'DENY' + ] + ); + } catch(\Exception $e) { + $this->logger->logException($e, ['app' => $this->appName]); + return new DataResponse(["message" => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + } } diff --git a/css/editor.css b/css/editor.css index 33ff19a1..d2533d7d 100644 --- a/css/editor.css +++ b/css/editor.css @@ -1,6 +1,6 @@ /** * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,9 @@ #body-public #content { height: 100%; } +#content-wrapper #content { + height: calc(100dvh - 45px); +} .AscDesktopEditor #header { display: none; diff --git a/css/main.css b/css/main.css index 061319e8..f32e89ce 100644 --- a/css/main.css +++ b/css/main.css @@ -1,6 +1,6 @@ /** * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/css/settings.css b/css/settings.css index 0899c3ba..8292bef7 100644 --- a/css/settings.css +++ b/css/settings.css @@ -1,6 +1,6 @@ /** * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/css/template.css b/css/template.css index 617239bd..3e7505d6 100644 --- a/css/template.css +++ b/css/template.css @@ -1,6 +1,6 @@ /** * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/js/desktop.js b/js/desktop.js index cbcc3b42..b7747dcb 100644 --- a/js/desktop.js +++ b/js/desktop.js @@ -1,6 +1,6 @@ /** * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/js/editor.js b/js/editor.js index a925aafb..3bcf8694 100644 --- a/js/editor.js +++ b/js/editor.js @@ -1,6 +1,6 @@ /** * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -99,11 +99,9 @@ OCA.Onlyoffice.showMessage(config.error, {type: "error"}); return; } - - if ((config.document.fileType === "docxf" || config.document.fileType === "oform") - && docsVersion[0] < 7) { - OCA.Onlyoffice.showMessage(t(OCA.Onlyoffice.AppName, "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online"), {type: "error"}); - return; + OCA.Onlyoffice.device = config.type; + if (OCA.Onlyoffice.device === "mobile") { + OCA.Onlyoffice.resizeEvents(); } var docIsChanged = null; @@ -146,9 +144,12 @@ config.events.onRequestSaveAs = OCA.Onlyoffice.onRequestSaveAs; config.events.onRequestInsertImage = OCA.Onlyoffice.onRequestInsertImage; config.events.onRequestMailMergeRecipients = OCA.Onlyoffice.onRequestMailMergeRecipients; - config.events.onRequestCompareFile = OCA.Onlyoffice.onRequestCompareFile; + config.events.onRequestSelectDocument = OCA.Onlyoffice.onRequestSelectDocument; + config.events.onRequestCompareFile = OCA.Onlyoffice.onRequestSelectDocument; //todo: remove (for editors 7.4) config.events.onRequestSendNotify = OCA.Onlyoffice.onRequestSendNotify; config.events.onRequestReferenceData = OCA.Onlyoffice.onRequestReferenceData; + config.events.onRequestOpen = OCA.Onlyoffice.onRequestOpen; + config.events.onRequestReferenceSource = OCA.Onlyoffice.onRequestReferenceSource; config.events.onMetaChange = OCA.Onlyoffice.onMetaChange; if (OC.currentUser) { @@ -182,7 +183,8 @@ OCA.Onlyoffice.docEditor = new DocsAPI.DocEditor("iframeEditor", config); - if (config.type === "mobile" && $("#app > iframe").css("position") === "fixed") { + if (config.type === "mobile" && $("#app > iframe").css("position") === "fixed" + && !OCA.Onlyoffice.inframe) { $("#app > iframe").css("height", "calc(100% - 45px)"); } @@ -269,6 +271,9 @@ if (OCA.Onlyoffice.version > 0) { OCA.Onlyoffice.onRequestHistory(OCA.Onlyoffice.version); } + + OCA.Onlyoffice.resize(); + OCA.Onlyoffice.setViewport(); }; OCA.Onlyoffice.onRequestSaveAs = function (event) { @@ -400,6 +405,25 @@ }); }; + OCA.Onlyoffice.editorReferenceSource = function (filePath) { + if (filePath === OCA.Onlyoffice.filePath) { + OCA.Onlyoffice.showMessage(t(OCA.Onlyoffice.AppName, "The data source must not be the current document"), "error"); + return; + } + + $.post(OC.generateUrl("apps/" + OCA.Onlyoffice.AppName + "/ajax/reference"), + { + path: filePath + }, + function onSuccess(response) { + if (response.error) { + OCA.Onlyoffice.showMessage(response.error, "error"); + return; + } + OCA.Onlyoffice.docEditor.setReferenceSource(response); + }); + } + OCA.Onlyoffice.onRequestClose = function () { OCA.Onlyoffice.docEditor.destroyEditor(); @@ -416,27 +440,37 @@ "*"); }; - OCA.Onlyoffice.onRequestCompareFile = function () { + OCA.Onlyoffice.onRequestSelectDocument = function (event) { var revisedMimes = [ "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ]; if (OCA.Onlyoffice.inframe) { window.parent.postMessage({ - method: "editorRequestCompareFile", - param: revisedMimes + method: "editorRequestSelectDocument", + param: revisedMimes, + documentSelectionType: event.data.c }, "*"); } else { - OC.dialogs.filepicker(t(OCA.Onlyoffice.AppName, "Select file to compare"), - OCA.Onlyoffice.editorSetRevised, + let title; + switch (event.data.c) { + case "combine": + title = t(OCA.Onlyoffice.AppName, "Select file to combine"); + break; + default: + title = t(OCA.Onlyoffice.AppName, "Select file to compare"); + } + OC.dialogs.filepicker(title, + OCA.Onlyoffice.editorSetRequested.bind({documentSelectionType: event.data.c}), false, revisedMimes, true); } }; - OCA.Onlyoffice.editorSetRevised = function (filePath) { + OCA.Onlyoffice.editorSetRequested = function (filePath) { + let documentSelectionType = this.documentSelectionType; $.get(OC.generateUrl("apps/" + OCA.Onlyoffice.AppName + "/ajax/url?filePath={filePath}", { filePath: filePath @@ -449,8 +483,9 @@ }); return; } + response.c = documentSelectionType; - OCA.Onlyoffice.docEditor.setRevisedFile(response); + OCA.Onlyoffice.docEditor.setRequestedDocument(response); }); }; @@ -483,15 +518,32 @@ }; OCA.Onlyoffice.onRequestUsers = function (event) { - $.get(OC.generateUrl("apps/" + OCA.Onlyoffice.AppName + "/ajax/users?fileId={fileId}", - { - fileId: OCA.Onlyoffice.fileId || 0 - }), - function onSuccess(response) { - OCA.Onlyoffice.docEditor.setUsers({ - "users": response - }); - }); + let operationType = typeof(event.data.c) !== "undefined" ? event.data.c : null; + switch (operationType) { + case "info": + $.get(OC.generateUrl("apps/" + OCA.Onlyoffice.AppName + "/ajax/userInfo?userIds={userIds}", + { + userIds: JSON.stringify(event.data.id) + }), + function onSuccess(response) { + OCA.Onlyoffice.docEditor.setUsers({ + "c": operationType, + "users": response + }); + }); + break; + default: + $.get(OC.generateUrl("apps/" + OCA.Onlyoffice.AppName + "/ajax/users?fileId={fileId}&operationType=" + operationType, + { + fileId: OCA.Onlyoffice.fileId || 0 + }), + function onSuccess(response) { + OCA.Onlyoffice.docEditor.setUsers({ + "c": operationType, + "users": response + }); + }); + } }; OCA.Onlyoffice.onRequestReferenceData = function (event) { @@ -542,6 +594,33 @@ }); }; + OCA.Onlyoffice.onRequestOpen = function (event) { + let filePath = event.data.path; + let fileId = event.data.referenceData.fileKey; + let windowName = event.data.windowName; + let sourceUrl = OC.generateUrl(`apps/${OCA.Onlyoffice.AppName}/${fileId}?filePath=${OC.encodePath(filePath)}`); + window.open(sourceUrl, windowName); + }; + + OCA.Onlyoffice.onRequestReferenceSource = function (event) { + let referenceSourceMimes = [ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ]; + if (OCA.Onlyoffice.inframe) { + window.parent.postMessage({ + method: "editorRequestReferenceSource", + param: referenceSourceMimes + }, + "*"); + } else { + OC.dialogs.filepicker(t(OCA.Onlyoffice.AppName, "Select data source"), + OCA.Onlyoffice.editorReferenceSource, + false, + referenceSourceMimes, + true); + } + }; + OCA.Onlyoffice.onMetaChange = function (event) { if (event.data.favorite !== undefined) { $.ajax({ @@ -605,6 +684,35 @@ OCA.Onlyoffice.docEditor.refreshHistory(data); } + OCA.Onlyoffice.resize = function () { + if (OCA.Onlyoffice.device !== "mobile") { + return; + } + + var headerHeight = $("#header").length > 0 ? $("#header").height() : 45; + var wrapEl = $("#app>iframe"); + if (wrapEl.length > 0) { + wrapEl[0].style.height = (screen.availHeight - headerHeight) + "px"; + window.scrollTo(0, -1); + wrapEl[0].style.height = (window.top.innerHeight - headerHeight) + "px"; + } + }; + + OCA.Onlyoffice.resizeEvents = function() { + if (window.addEventListener) { + if (/Android/i.test(navigator.userAgent)) { + window.addEventListener("resize", OCA.Onlyoffice.resize); + } + if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) { + window.addEventListener("orientationchange", OCA.Onlyoffice.resize); + } + } + }; + + OCA.Onlyoffice.setViewport = function() { + document.querySelector('meta[name="viewport"]').setAttribute("content","width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"); + }; + $(document).ready(OCA.Onlyoffice.InitEditor); })(jQuery, OCA); diff --git a/js/listener.js b/js/listener.js index 03069aa7..12df57f6 100644 --- a/js/listener.js +++ b/js/listener.js @@ -1,6 +1,6 @@ /** * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,26 +56,50 @@ true); }; - OCA.Onlyoffice.onRequestCompareFile = function (revisedMimes) { - OC.dialogs.filepicker(t(OCA.Onlyoffice.AppName, "Select file to compare"), - $("#onlyofficeFrame")[0].contentWindow.OCA.Onlyoffice.editorSetRevised, + OCA.Onlyoffice.onRequestSelectDocument = function (revisedMimes, documentSelectionType) { + let title; + switch (documentSelectionType) { + case "combine": + title = t(OCA.Onlyoffice.AppName, "Select file to combine"); + break; + default: + title = t(OCA.Onlyoffice.AppName, "Select file to compare"); + } + OC.dialogs.filepicker(title, + $("#onlyofficeFrame")[0].contentWindow.OCA.Onlyoffice.editorSetRequested.bind({documentSelectionType: documentSelectionType}), false, revisedMimes, true); }; + OCA.Onlyoffice.onRequestReferenceSource = function (referenceSourceMimes) { + OC.dialogs.filepicker(t(OCA.Onlyoffice.AppName, "Select data source"), + $("#onlyofficeFrame")[0].contentWindow.OCA.Onlyoffice.editorReferenceSource, + false, + referenceSourceMimes, + true); + } + OCA.Onlyoffice.onDocumentReady = function (documentType) { - if (documentType === "word") { + if (documentType === "word" + || documentType === "cell" + || documentType === "slide") { OCA.Onlyoffice.bindVersionClick(); } else { OCA.Onlyoffice.unbindVersionClick(); } + + OCA.Onlyoffice.setViewport(); }; OCA.Onlyoffice.changeFavicon = function (favicon) { $('link[rel="icon"]').attr("href", favicon); }; + OCA.Onlyoffice.setViewport = function() { + document.querySelector('meta[name="viewport"]').setAttribute("content","width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"); + }; + OCA.Onlyoffice.onShowMessage = function (messageObj) { OC.Notification.show(messageObj.message, messageObj.props); } @@ -110,8 +134,11 @@ case "editorRequestMailMergeRecipients": OCA.Onlyoffice.onRequestMailMergeRecipients(event.data.param); break; - case "editorRequestCompareFile": - OCA.Onlyoffice.onRequestCompareFile(event.data.param); + case "editorRequestSelectDocument": + OCA.Onlyoffice.onRequestSelectDocument(event.data.param, event.data.documentSelectionType); + break; + case "editorRequestReferenceSource": + OCA.Onlyoffice.onRequestReferenceSource(event.data.param); break; case "onDocumentReady": OCA.Onlyoffice.onDocumentReady(event.data.param); diff --git a/js/main.js b/js/main.js index d26ae1a2..1823cb17 100644 --- a/js/main.js +++ b/js/main.js @@ -1,6 +1,6 @@ /** * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -173,7 +173,7 @@ OCA.Onlyoffice.FileClick = function (fileName, context) { var fileInfoModel = context.fileInfoModel || context.fileList.getModelForFile(fileName); - var fileId = context.$file[0].dataset.id || fileInfoModel.id; + var fileId = context.$file && context.$file[0].dataset.id || fileInfoModel.id; var winEditor = !fileInfoModel && !OCA.Onlyoffice.setting.sameTab ? document : null; OCA.Onlyoffice.OpenEditor(fileId, context.dir, fileName, 0, winEditor); @@ -185,9 +185,10 @@ OCA.Onlyoffice.FileConvertClick = function (fileName, context) { var fileInfoModel = context.fileInfoModel || context.fileList.getModelForFile(fileName); var fileList = context.fileList; + var fileId = context.$file ? context.$file[0].dataset.id : fileInfoModel.id; var convertData = { - fileId: context.$file[0].dataset.id || fileInfoModel.id + fileId: fileId }; if ($("#isPublic").val()) { @@ -284,7 +285,7 @@ "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ]; - OC.dialogs.filepicker(t(OCA.Onlyoffice.AppName, "Create new Form template"), + OC.dialogs.filepicker(t(OCA.Onlyoffice.AppName, "Create new PDF form"), function (filePath) { OCA.Onlyoffice.CreateFile(name, fileList, 0, filePath); }, @@ -295,7 +296,7 @@ OCA.Onlyoffice.CreateFormClick = function (fileName, context) { var fileList = context.fileList; - var name = fileName.replace(/\.[^.]+$/, ".oform"); + var name = fileName.replace(/\.[^.]+$/, ".pdf"); var attr = context.fileInfoModel.attributes; var targetPath = attr.path + "/" + attr.name; @@ -330,74 +331,65 @@ return true; } - OCA.Files.fileActions.registerAction({ - name: "onlyofficeOpen", - displayName: t(OCA.Onlyoffice.AppName, "Open in ONLYOFFICE"), - mime: config.mime, - permissions: OC.PERMISSION_READ, - iconClass: "icon-onlyoffice-open", - actionHandler: OCA.Onlyoffice.FileClick - }); - - if (config.def) { - OCA.Files.fileActions.setDefault(config.mime, "onlyofficeOpen"); - } - - if (config.conv) { + let mimeTypes = config.mime; + mimeTypes.forEach((mime) => { OCA.Files.fileActions.registerAction({ - name: "onlyofficeConvert", - displayName: t(OCA.Onlyoffice.AppName, "Convert with ONLYOFFICE"), - mime: config.mime, - permissions: ($("#isPublic").val() ? OC.PERMISSION_UPDATE : OC.PERMISSION_READ), - iconClass: "icon-onlyoffice-convert", - actionHandler: OCA.Onlyoffice.FileConvertClick - }); - } - - if (config.fillForms) { - OCA.Files.fileActions.registerAction({ - name: "onlyofficeFill", - displayName: t(OCA.Onlyoffice.AppName, "Fill in form in ONLYOFFICE"), - mime: config.mime, - permissions: OC.PERMISSION_UPDATE, - iconClass: "icon-onlyoffice-fill", + name: "onlyofficeOpen", + displayName: t(OCA.Onlyoffice.AppName, "Open in ONLYOFFICE"), + mime: mime, + permissions: OC.PERMISSION_READ, + iconClass: "icon-onlyoffice-open", actionHandler: OCA.Onlyoffice.FileClick }); - } - if (config.fillForms) { - OCA.Files.fileActions.setDefault(config.mime, "onlyofficeFill"); - } + if (config.def) { + OCA.Files.fileActions.setDefault(mime, "onlyofficeOpen"); + } - if (config.createForm) { - var permission = OC.PERMISSION_READ; - if ($("#isPublic").val()) { - permission = OC.PERMISSION_UPDATE; - if (parseInt($("#sharePermission").val()) === (OC.PERMISSION_READ | OC.PERMISSION_CREATE)) { - permission = OC.PERMISSION_READ; - } + if (config.conv) { + OCA.Files.fileActions.registerAction({ + name: "onlyofficeConvert", + displayName: t(OCA.Onlyoffice.AppName, "Convert with ONLYOFFICE"), + mime: mime, + permissions: ($("#isPublic").val() ? OC.PERMISSION_UPDATE : OC.PERMISSION_READ), + iconClass: "icon-onlyoffice-convert", + actionHandler: OCA.Onlyoffice.FileConvertClick + }); } - OCA.Files.fileActions.registerAction({ - name: "onlyofficeCreateForm", - displayName: t(OCA.Onlyoffice.AppName, "Create form"), - mime: config.mime, - permissions: permission, - iconClass: "icon-onlyoffice-create", - actionHandler: OCA.Onlyoffice.CreateFormClick - }); - } + if (config.fillForms) { + OCA.Files.fileActions.registerAction({ + name: "onlyofficeFill", + displayName: t(OCA.Onlyoffice.AppName, "Fill in form in ONLYOFFICE"), + mime: mime, + permissions: OC.PERMISSION_UPDATE, + iconClass: "icon-onlyoffice-fill", + actionHandler: OCA.Onlyoffice.FileClick + }); + } - if (config.saveas && !$("#isPublic").val()) { - OCA.Files.fileActions.registerAction({ - name: "onlyofficeDownload", - displayName: t(OCA.Onlyoffice.AppName, "Download as"), - mime: config.mime, - permissions: OC.PERMISSION_READ, - iconClass: "icon-onlyoffice-download", - actionHandler: OCA.Onlyoffice.DownloadClick - }); - } + if (config.createForm) { + OCA.Files.fileActions.registerAction({ + name: "onlyofficeCreateForm", + displayName: t(OCA.Onlyoffice.AppName, "Create form"), + mime: mime, + permissions: ($("#isPublic").val() ? OC.PERMISSION_UPDATE : OC.PERMISSION_READ), + iconClass: "icon-onlyoffice-create", + actionHandler: OCA.Onlyoffice.CreateFormClick + }); + } + + if (config.saveas && !$("#isPublic").val()) { + OCA.Files.fileActions.registerAction({ + name: "onlyofficeDownload", + displayName: t(OCA.Onlyoffice.AppName, "Download as"), + mime: mime, + permissions: OC.PERMISSION_READ, + iconClass: "icon-onlyoffice-download", + actionHandler: OCA.Onlyoffice.DownloadClick + }); + } + }); }); } @@ -459,8 +451,8 @@ menu.addMenuEntry({ id: "onlyofficeDocxf", - displayName: t(OCA.Onlyoffice.AppName, "Form template"), - templateName: t(OCA.Onlyoffice.AppName, "Form template"), + displayName: t(OCA.Onlyoffice.AppName, "PDF form"), + templateName: t(OCA.Onlyoffice.AppName, "PDF form"), iconClass: "icon-onlyoffice-new-docxf", fileType: "docxf", actionHandler: function (name) { @@ -471,8 +463,8 @@ if (!$("#isPublic").val()) { menu.addMenuEntry({ id: "onlyofficeDocxfExist", - displayName: t(OCA.Onlyoffice.AppName, "Form template from existing text file"), - templateName: t(OCA.Onlyoffice.AppName, "Form template from existing text file"), + displayName: t(OCA.Onlyoffice.AppName, "PDF form from existing text file"), + templateName: t(OCA.Onlyoffice.AppName, "PDF form from existing text file"), iconClass: "icon-onlyoffice-new-docxf", fileType: "docxf", actionHandler: function (name) { diff --git a/js/settings.js b/js/settings.js index c3a53c31..3f7b575f 100644 --- a/js/settings.js +++ b/js/settings.js @@ -1,6 +1,6 @@ /** * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,8 @@ }; if ($("#onlyofficeInternalUrl").val().length - || $("#onlyofficeStorageUrl").val().length) { + || $("#onlyofficeStorageUrl").val().length + || $("#onlyofficeJwtHeader").val().length) { advToogle(); } @@ -129,6 +130,7 @@ var sameTab = $("#onlyofficeSameTab").is(":checked"); var preview = $("#onlyofficePreview").is(":checked"); + var cronChecker = $("#onlyofficeCronChecker").is(":checked"); var versionHistory = $("#onlyofficeVersionHistory").is(":checked"); var limitGroupsString = $("#onlyofficeGroups").prop("checked") ? $("#onlyofficeLimitGroups").val() : ""; @@ -151,6 +153,7 @@ editFormats: editFormats, sameTab: sameTab, preview: preview, + cronChecker: cronChecker, versionHistory: versionHistory, limitGroups: limitGroups, chat: chat, diff --git a/js/share.js b/js/share.js index 1de32b3a..7ae75725 100644 --- a/js/share.js +++ b/js/share.js @@ -1,6 +1,6 @@ /** * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -341,7 +341,7 @@ } else if (attribute.key === "comment") { label = t(OCA.Onlyoffice.AppName, "comment"); } else if (attribute.key === "modifyFilter") { - label = t(OCA.Onlyoffice.AppName, "custom filter"); + label = t(OCA.Onlyoffice.AppName, "global filter"); } else { continue; } @@ -603,7 +603,7 @@ "scope": OCA.Onlyoffice.AppName, "key": "modifyFilter", "default": true, - "label": t(OCA.Onlyoffice.AppName, "custom filter"), + "label": t(OCA.Onlyoffice.AppName, "global filter"), "requiredPermissions": [OC.PERMISSION_UPDATE], "incompatibleAttributes": [ { diff --git a/js/template.js b/js/template.js index 6a6e2c24..db6a0e3a 100644 --- a/js/template.js +++ b/js/template.js @@ -1,6 +1,6 @@ /** * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/js/web/onlyoffice.js b/js/web/onlyoffice.js index c8927c7a..aa358b01 100644 --- a/js/web/onlyoffice.js +++ b/js/web/onlyoffice.js @@ -1,298 +1,183 @@ -define((function () { 'use strict'; +define(['vue'], (function (vue) { 'use strict'; var global$1 = (typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}); - // shim for using process in browser - // based off https://github.com/defunctzombie/node-process/blob/master/browser.js - - function defaultSetTimout() { - throw new Error('setTimeout has not been defined'); - } - function defaultClearTimeout () { - throw new Error('clearTimeout has not been defined'); + function getDevtoolsGlobalHook() { + return getTarget().__VUE_DEVTOOLS_GLOBAL_HOOK__; } - var cachedSetTimeout = defaultSetTimout; - var cachedClearTimeout = defaultClearTimeout; - if (typeof global$1.setTimeout === 'function') { - cachedSetTimeout = setTimeout; + function getTarget() { + // @ts-ignore + return (typeof navigator !== 'undefined' && typeof window !== 'undefined') + ? window + : typeof global$1 !== 'undefined' + ? global$1 + : {}; } - if (typeof global$1.clearTimeout === 'function') { - cachedClearTimeout = clearTimeout; - } - - function runTimeout(fun) { - if (cachedSetTimeout === setTimeout) { - //normal enviroments in sane situations - return setTimeout(fun, 0); - } - // if setTimeout wasn't available but was latter defined - if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { - cachedSetTimeout = setTimeout; - return setTimeout(fun, 0); - } - try { - // when when somebody has screwed with setTimeout but no I.E. maddness - return cachedSetTimeout(fun, 0); - } catch(e){ - try { - // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally - return cachedSetTimeout.call(null, fun, 0); - } catch(e){ - // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error - return cachedSetTimeout.call(this, fun, 0); - } - } + const isProxyAvailable = typeof Proxy === 'function'; + const HOOK_SETUP = 'devtools-plugin:setup'; + const HOOK_PLUGIN_SETTINGS_SET = 'plugin:settings:set'; - } - function runClearTimeout(marker) { - if (cachedClearTimeout === clearTimeout) { - //normal enviroments in sane situations - return clearTimeout(marker); + let supported; + let perf; + function isPerformanceSupported() { + var _a; + if (supported !== undefined) { + return supported; } - // if clearTimeout wasn't available but was latter defined - if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { - cachedClearTimeout = clearTimeout; - return clearTimeout(marker); + if (typeof window !== 'undefined' && window.performance) { + supported = true; + perf = window.performance; } - try { - // when when somebody has screwed with setTimeout but no I.E. maddness - return cachedClearTimeout(marker); - } catch (e){ - try { - // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally - return cachedClearTimeout.call(null, marker); - } catch (e){ - // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. - // Some versions of I.E. have different rules for clearTimeout vs setTimeout - return cachedClearTimeout.call(this, marker); - } + else if (typeof global$1 !== 'undefined' && ((_a = global$1.perf_hooks) === null || _a === void 0 ? void 0 : _a.performance)) { + supported = true; + perf = global$1.perf_hooks.performance; } - - - + else { + supported = false; + } + return supported; + } + function now() { + return isPerformanceSupported() ? perf.now() : Date.now(); } - var queue = []; - var draining = false; - var currentQueue; - var queueIndex = -1; - function cleanUpNextTick() { - if (!draining || !currentQueue) { - return; - } - draining = false; - if (currentQueue.length) { - queue = currentQueue.concat(queue); - } else { - queueIndex = -1; + class ApiProxy { + constructor(plugin, hook) { + this.target = null; + this.targetQueue = []; + this.onQueue = []; + this.plugin = plugin; + this.hook = hook; + const defaultSettings = {}; + if (plugin.settings) { + for (const id in plugin.settings) { + const item = plugin.settings[id]; + defaultSettings[id] = item.defaultValue; + } + } + const localSettingsSaveId = `__vue-devtools-plugin-settings__${plugin.id}`; + let currentSettings = Object.assign({}, defaultSettings); + try { + const raw = localStorage.getItem(localSettingsSaveId); + const data = JSON.parse(raw); + Object.assign(currentSettings, data); + } + catch (e) { + // noop + } + this.fallbacks = { + getSettings() { + return currentSettings; + }, + setSettings(value) { + try { + localStorage.setItem(localSettingsSaveId, JSON.stringify(value)); + } + catch (e) { + // noop + } + currentSettings = value; + }, + now() { + return now(); + }, + }; + if (hook) { + hook.on(HOOK_PLUGIN_SETTINGS_SET, (pluginId, value) => { + if (pluginId === this.plugin.id) { + this.fallbacks.setSettings(value); + } + }); + } + this.proxiedOn = new Proxy({}, { + get: (_target, prop) => { + if (this.target) { + return this.target.on[prop]; + } + else { + return (...args) => { + this.onQueue.push({ + method: prop, + args, + }); + }; + } + }, + }); + this.proxiedTarget = new Proxy({}, { + get: (_target, prop) => { + if (this.target) { + return this.target[prop]; + } + else if (prop === 'on') { + return this.proxiedOn; + } + else if (Object.keys(this.fallbacks).includes(prop)) { + return (...args) => { + this.targetQueue.push({ + method: prop, + args, + resolve: () => { }, + }); + return this.fallbacks[prop](...args); + }; + } + else { + return (...args) => { + return new Promise(resolve => { + this.targetQueue.push({ + method: prop, + args, + resolve, + }); + }); + }; + } + }, + }); } - if (queue.length) { - drainQueue(); + async setRealTarget(target) { + this.target = target; + for (const item of this.onQueue) { + this.target.on[item.method](...item.args); + } + for (const item of this.targetQueue) { + item.resolve(await this.target[item.method](...item.args)); + } } } - function drainQueue() { - if (draining) { - return; + function setupDevtoolsPlugin(pluginDescriptor, setupFn) { + const descriptor = pluginDescriptor; + const target = getTarget(); + const hook = getDevtoolsGlobalHook(); + const enableProxy = isProxyAvailable && descriptor.enableEarlyProxy; + if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !enableProxy)) { + hook.emit(HOOK_SETUP, pluginDescriptor, setupFn); } - var timeout = runTimeout(cleanUpNextTick); - draining = true; - - var len = queue.length; - while(len) { - currentQueue = queue; - queue = []; - while (++queueIndex < len) { - if (currentQueue) { - currentQueue[queueIndex].run(); - } - } - queueIndex = -1; - len = queue.length; - } - currentQueue = null; - draining = false; - runClearTimeout(timeout); - } - function nextTick(fun) { - var args = new Array(arguments.length - 1); - if (arguments.length > 1) { - for (var i = 1; i < arguments.length; i++) { - args[i - 1] = arguments[i]; - } + else { + const proxy = enableProxy ? new ApiProxy(descriptor, hook) : null; + const list = target.__VUE_DEVTOOLS_PLUGINS__ = target.__VUE_DEVTOOLS_PLUGINS__ || []; + list.push({ + pluginDescriptor: descriptor, + setupFn, + proxy, + }); + if (proxy) + setupFn(proxy.proxiedTarget); } - queue.push(new Item(fun, args)); - if (queue.length === 1 && !draining) { - runTimeout(drainQueue); - } - } - // v8 likes predictible objects - function Item(fun, array) { - this.fun = fun; - this.array = array; - } - Item.prototype.run = function () { - this.fun.apply(null, this.array); - }; - var title = 'browser'; - var platform = 'browser'; - var browser$1 = true; - var env = {}; - var argv = []; - var version$1 = ''; // empty string to avoid regexp issues - var versions = {}; - var release = {}; - var config = {}; - - function noop() {} - - var on = noop; - var addListener = noop; - var once = noop; - var off = noop; - var removeListener = noop; - var removeAllListeners = noop; - var emit = noop; - - function binding(name) { - throw new Error('process.binding is not supported'); - } - - function cwd () { return '/' } - function chdir (dir) { - throw new Error('process.chdir is not supported'); - }function umask() { return 0; } - - // from https://github.com/kumavis/browser-process-hrtime/blob/master/index.js - var performance = global$1.performance || {}; - var performanceNow = - performance.now || - performance.mozNow || - performance.msNow || - performance.oNow || - performance.webkitNow || - function(){ return (new Date()).getTime() }; - - // generate timestamp or delta - // see http://nodejs.org/api/process.html#process_process_hrtime - function hrtime(previousTimestamp){ - var clocktime = performanceNow.call(performance)*1e-3; - var seconds = Math.floor(clocktime); - var nanoseconds = Math.floor((clocktime%1)*1e9); - if (previousTimestamp) { - seconds = seconds - previousTimestamp[0]; - nanoseconds = nanoseconds - previousTimestamp[1]; - if (nanoseconds<0) { - seconds--; - nanoseconds += 1e9; - } - } - return [seconds,nanoseconds] - } - - var startTime = new Date(); - function uptime() { - var currentTime = new Date(); - var dif = currentTime - startTime; - return dif / 1000; - } - - var process = { - nextTick: nextTick, - title: title, - browser: browser$1, - env: env, - argv: argv, - version: version$1, - versions: versions, - on: on, - addListener: addListener, - once: once, - off: off, - removeListener: removeListener, - removeAllListeners: removeAllListeners, - emit: emit, - binding: binding, - cwd: cwd, - chdir: chdir, - umask: umask, - hrtime: hrtime, - platform: platform, - release: release, - config: config, - uptime: uptime - }; + } /*! - * vuex v3.6.2 - * (c) 2021 Evan You + * vuex v4.1.0 + * (c) 2022 Evan You * @license MIT */ - function applyMixin (Vue) { - var version = Number(Vue.version.split('.')[0]); - - if (version >= 2) { - Vue.mixin({ beforeCreate: vuexInit }); - } else { - // override init and inject vuex init procedure - // for 1.x backwards compatibility. - var _init = Vue.prototype._init; - Vue.prototype._init = function (options) { - if ( options === void 0 ) options = {}; - - options.init = options.init - ? [vuexInit].concat(options.init) - : vuexInit; - _init.call(this, options); - }; - } - - /** - * Vuex init hook, injected into each instances init hooks list. - */ - function vuexInit () { - var options = this.$options; - // store injection - if (options.store) { - this.$store = typeof options.store === 'function' - ? options.store() - : options.store; - } else if (options.parent && options.parent.$store) { - this.$store = options.parent.$store; - } - } - } - - var target = typeof window !== 'undefined' - ? window - : typeof global$1 !== 'undefined' - ? global$1 - : {}; - var devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__; - - function devtoolPlugin (store) { - if (!devtoolHook) { return } - - store._devtoolHook = devtoolHook; - - devtoolHook.emit('vuex:init', store); - - devtoolHook.on('vuex:travel-to-state', function (targetState) { - store.replaceState(targetState); - }); - - store.subscribe(function (mutation, state) { - devtoolHook.emit('vuex:mutation', mutation, state); - }, { prepend: true }); - - store.subscribeAction(function (action, state) { - devtoolHook.emit('vuex:action', action, state); - }, { prepend: true }); - } + var storeKey = 'store'; /** * forEach for object @@ -319,1455 +204,5104 @@ define((function () { 'use strict'; } } - // Base data struct for store's module, package with some attribute and method - var Module = function Module (rawModule, runtime) { - this.runtime = runtime; - // Store some children item - this._children = Object.create(null); - // Store the origin module object which passed by programmer - this._rawModule = rawModule; - var rawState = rawModule.state; - - // Store the origin module's state - this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}; - }; - - var prototypeAccessors = { namespaced: { configurable: true } }; + function genericSubscribe (fn, subs, options) { + if (subs.indexOf(fn) < 0) { + options && options.prepend + ? subs.unshift(fn) + : subs.push(fn); + } + return function () { + var i = subs.indexOf(fn); + if (i > -1) { + subs.splice(i, 1); + } + } + } - prototypeAccessors.namespaced.get = function () { - return !!this._rawModule.namespaced - }; + function resetStore (store, hot) { + store._actions = Object.create(null); + store._mutations = Object.create(null); + store._wrappedGetters = Object.create(null); + store._modulesNamespaceMap = Object.create(null); + var state = store.state; + // init all modules + installModule(store, state, [], store._modules.root, true); + // reset state + resetStoreState(store, state, hot); + } - Module.prototype.addChild = function addChild (key, module) { - this._children[key] = module; - }; + function resetStoreState (store, state, hot) { + var oldState = store._state; + var oldScope = store._scope; - Module.prototype.removeChild = function removeChild (key) { - delete this._children[key]; - }; + // bind store public getters + store.getters = {}; + // reset local getters cache + store._makeLocalGettersCache = Object.create(null); + var wrappedGetters = store._wrappedGetters; + var computedObj = {}; + var computedCache = {}; + + // create a new effect scope and create computed object inside it to avoid + // getters (computed) getting destroyed on component unmount. + var scope = vue.effectScope(true); + + scope.run(function () { + forEachValue(wrappedGetters, function (fn, key) { + // use computed to leverage its lazy-caching mechanism + // direct inline function use will lead to closure preserving oldState. + // using partial to return function with only arguments preserved in closure environment. + computedObj[key] = partial(fn, store); + computedCache[key] = vue.computed(function () { return computedObj[key](); }); + Object.defineProperty(store.getters, key, { + get: function () { return computedCache[key].value; }, + enumerable: true // for local getters + }); + }); + }); - Module.prototype.getChild = function getChild (key) { - return this._children[key] - }; + store._state = vue.reactive({ + data: state + }); - Module.prototype.hasChild = function hasChild (key) { - return key in this._children - }; + // register the newly created effect scope to the store so that we can + // dispose the effects when this method runs again in the future. + store._scope = scope; - Module.prototype.update = function update (rawModule) { - this._rawModule.namespaced = rawModule.namespaced; - if (rawModule.actions) { - this._rawModule.actions = rawModule.actions; + // enable strict mode for new state + if (store.strict) { + enableStrictMode(store); } - if (rawModule.mutations) { - this._rawModule.mutations = rawModule.mutations; + + if (oldState) { + if (hot) { + // dispatch changes in all subscribed watchers + // to force getter re-evaluation for hot reloading. + store._withCommit(function () { + oldState.data = null; + }); + } } - if (rawModule.getters) { - this._rawModule.getters = rawModule.getters; + + // dispose previously registered effect scope if there is one. + if (oldScope) { + oldScope.stop(); } - }; + } - Module.prototype.forEachChild = function forEachChild (fn) { - forEachValue(this._children, fn); - }; + function installModule (store, rootState, path, module, hot) { + var isRoot = !path.length; + var namespace = store._modules.getNamespace(path); - Module.prototype.forEachGetter = function forEachGetter (fn) { - if (this._rawModule.getters) { - forEachValue(this._rawModule.getters, fn); + // register in namespace map + if (module.namespaced) { + if (store._modulesNamespaceMap[namespace] && true) { + console.error(("[vuex] duplicate namespace " + namespace + " for the namespaced module " + (path.join('/')))); + } + store._modulesNamespaceMap[namespace] = module; } - }; - Module.prototype.forEachAction = function forEachAction (fn) { - if (this._rawModule.actions) { - forEachValue(this._rawModule.actions, fn); + // set state + if (!isRoot && !hot) { + var parentState = getNestedState(rootState, path.slice(0, -1)); + var moduleName = path[path.length - 1]; + store._withCommit(function () { + { + if (moduleName in parentState) { + console.warn( + ("[vuex] state field \"" + moduleName + "\" was overridden by a module with the same name at \"" + (path.join('.')) + "\"") + ); + } + } + parentState[moduleName] = module.state; + }); } - }; - Module.prototype.forEachMutation = function forEachMutation (fn) { - if (this._rawModule.mutations) { - forEachValue(this._rawModule.mutations, fn); - } - }; + var local = module.context = makeLocalContext(store, namespace, path); - Object.defineProperties( Module.prototype, prototypeAccessors ); + module.forEachMutation(function (mutation, key) { + var namespacedType = namespace + key; + registerMutation(store, namespacedType, mutation, local); + }); - var ModuleCollection = function ModuleCollection (rawRootModule) { - // register root module (Vuex.Store options) - this.register([], rawRootModule, false); - }; + module.forEachAction(function (action, key) { + var type = action.root ? key : namespace + key; + var handler = action.handler || action; + registerAction(store, type, handler, local); + }); - ModuleCollection.prototype.get = function get (path) { - return path.reduce(function (module, key) { - return module.getChild(key) - }, this.root) - }; + module.forEachGetter(function (getter, key) { + var namespacedType = namespace + key; + registerGetter(store, namespacedType, getter, local); + }); - ModuleCollection.prototype.getNamespace = function getNamespace (path) { - var module = this.root; - return path.reduce(function (namespace, key) { - module = module.getChild(key); - return namespace + (module.namespaced ? key + '/' : '') - }, '') - }; + module.forEachChild(function (child, key) { + installModule(store, rootState, path.concat(key), child, hot); + }); + } - ModuleCollection.prototype.update = function update$1 (rawRootModule) { - update([], this.root, rawRootModule); - }; + /** + * make localized dispatch, commit, getters and state + * if there is no namespace, just use root ones + */ + function makeLocalContext (store, namespace, path) { + var noNamespace = namespace === ''; - ModuleCollection.prototype.register = function register (path, rawModule, runtime) { - var this$1$1 = this; - if ( runtime === void 0 ) runtime = true; + var local = { + dispatch: noNamespace ? store.dispatch : function (_type, _payload, _options) { + var args = unifyObjectStyle(_type, _payload, _options); + var payload = args.payload; + var options = args.options; + var type = args.type; - if ((process.env.NODE_ENV !== 'production')) { - assertRawModule(path, rawModule); - } + if (!options || !options.root) { + type = namespace + type; + if (!store._actions[type]) { + console.error(("[vuex] unknown local action type: " + (args.type) + ", global type: " + type)); + return + } + } - var newModule = new Module(rawModule, runtime); - if (path.length === 0) { - this.root = newModule; - } else { - var parent = this.get(path.slice(0, -1)); - parent.addChild(path[path.length - 1], newModule); - } + return store.dispatch(type, payload) + }, - // register nested modules - if (rawModule.modules) { - forEachValue(rawModule.modules, function (rawChildModule, key) { - this$1$1.register(path.concat(key), rawChildModule, runtime); - }); - } - }; + commit: noNamespace ? store.commit : function (_type, _payload, _options) { + var args = unifyObjectStyle(_type, _payload, _options); + var payload = args.payload; + var options = args.options; + var type = args.type; - ModuleCollection.prototype.unregister = function unregister (path) { + if (!options || !options.root) { + type = namespace + type; + if (!store._mutations[type]) { + console.error(("[vuex] unknown local mutation type: " + (args.type) + ", global type: " + type)); + return + } + } + + store.commit(type, payload, options); + } + }; + + // getters and state object must be gotten lazily + // because they will be changed by state update + Object.defineProperties(local, { + getters: { + get: noNamespace + ? function () { return store.getters; } + : function () { return makeLocalGetters(store, namespace); } + }, + state: { + get: function () { return getNestedState(store.state, path); } + } + }); + + return local + } + + function makeLocalGetters (store, namespace) { + if (!store._makeLocalGettersCache[namespace]) { + var gettersProxy = {}; + var splitPos = namespace.length; + Object.keys(store.getters).forEach(function (type) { + // skip if the target getter is not match this namespace + if (type.slice(0, splitPos) !== namespace) { return } + + // extract local getter type + var localType = type.slice(splitPos); + + // Add a port to the getters proxy. + // Define as getter property because + // we do not want to evaluate the getters in this time. + Object.defineProperty(gettersProxy, localType, { + get: function () { return store.getters[type]; }, + enumerable: true + }); + }); + store._makeLocalGettersCache[namespace] = gettersProxy; + } + + return store._makeLocalGettersCache[namespace] + } + + function registerMutation (store, type, handler, local) { + var entry = store._mutations[type] || (store._mutations[type] = []); + entry.push(function wrappedMutationHandler (payload) { + handler.call(store, local.state, payload); + }); + } + + function registerAction (store, type, handler, local) { + var entry = store._actions[type] || (store._actions[type] = []); + entry.push(function wrappedActionHandler (payload) { + var res = handler.call(store, { + dispatch: local.dispatch, + commit: local.commit, + getters: local.getters, + state: local.state, + rootGetters: store.getters, + rootState: store.state + }, payload); + if (!isPromise(res)) { + res = Promise.resolve(res); + } + if (store._devtoolHook) { + return res.catch(function (err) { + store._devtoolHook.emit('vuex:error', err); + throw err + }) + } else { + return res + } + }); + } + + function registerGetter (store, type, rawGetter, local) { + if (store._wrappedGetters[type]) { + { + console.error(("[vuex] duplicate getter key: " + type)); + } + return + } + store._wrappedGetters[type] = function wrappedGetter (store) { + return rawGetter( + local.state, // local state + local.getters, // local getters + store.state, // root state + store.getters // root getters + ) + }; + } + + function enableStrictMode (store) { + vue.watch(function () { return store._state.data; }, function () { + { + assert(store._committing, "do not mutate vuex store state outside mutation handlers."); + } + }, { deep: true, flush: 'sync' }); + } + + function getNestedState (state, path) { + return path.reduce(function (state, key) { return state[key]; }, state) + } + + function unifyObjectStyle (type, payload, options) { + if (isObject$1(type) && type.type) { + options = payload; + payload = type; + type = type.type; + } + + { + assert(typeof type === 'string', ("expects string as the type, but found " + (typeof type) + ".")); + } + + return { type: type, payload: payload, options: options } + } + + var LABEL_VUEX_BINDINGS = 'vuex bindings'; + var MUTATIONS_LAYER_ID = 'vuex:mutations'; + var ACTIONS_LAYER_ID = 'vuex:actions'; + var INSPECTOR_ID = 'vuex'; + + var actionId = 0; + + function addDevtools (app, store) { + setupDevtoolsPlugin( + { + id: 'org.vuejs.vuex', + app: app, + label: 'Vuex', + homepage: 'https://next.vuex.vuejs.org/', + logo: 'https://vuejs.org/images/icons/favicon-96x96.png', + packageName: 'vuex', + componentStateTypes: [LABEL_VUEX_BINDINGS] + }, + function (api) { + api.addTimelineLayer({ + id: MUTATIONS_LAYER_ID, + label: 'Vuex Mutations', + color: COLOR_LIME_500 + }); + + api.addTimelineLayer({ + id: ACTIONS_LAYER_ID, + label: 'Vuex Actions', + color: COLOR_LIME_500 + }); + + api.addInspector({ + id: INSPECTOR_ID, + label: 'Vuex', + icon: 'storage', + treeFilterPlaceholder: 'Filter stores...' + }); + + api.on.getInspectorTree(function (payload) { + if (payload.app === app && payload.inspectorId === INSPECTOR_ID) { + if (payload.filter) { + var nodes = []; + flattenStoreForInspectorTree(nodes, store._modules.root, payload.filter, ''); + payload.rootNodes = nodes; + } else { + payload.rootNodes = [ + formatStoreForInspectorTree(store._modules.root, '') + ]; + } + } + }); + + api.on.getInspectorState(function (payload) { + if (payload.app === app && payload.inspectorId === INSPECTOR_ID) { + var modulePath = payload.nodeId; + makeLocalGetters(store, modulePath); + payload.state = formatStoreForInspectorState( + getStoreModule(store._modules, modulePath), + modulePath === 'root' ? store.getters : store._makeLocalGettersCache, + modulePath + ); + } + }); + + api.on.editInspectorState(function (payload) { + if (payload.app === app && payload.inspectorId === INSPECTOR_ID) { + var modulePath = payload.nodeId; + var path = payload.path; + if (modulePath !== 'root') { + path = modulePath.split('/').filter(Boolean).concat( path); + } + store._withCommit(function () { + payload.set(store._state.data, path, payload.state.value); + }); + } + }); + + store.subscribe(function (mutation, state) { + var data = {}; + + if (mutation.payload) { + data.payload = mutation.payload; + } + + data.state = state; + + api.notifyComponentUpdate(); + api.sendInspectorTree(INSPECTOR_ID); + api.sendInspectorState(INSPECTOR_ID); + + api.addTimelineEvent({ + layerId: MUTATIONS_LAYER_ID, + event: { + time: Date.now(), + title: mutation.type, + data: data + } + }); + }); + + store.subscribeAction({ + before: function (action, state) { + var data = {}; + if (action.payload) { + data.payload = action.payload; + } + action._id = actionId++; + action._time = Date.now(); + data.state = state; + + api.addTimelineEvent({ + layerId: ACTIONS_LAYER_ID, + event: { + time: action._time, + title: action.type, + groupId: action._id, + subtitle: 'start', + data: data + } + }); + }, + after: function (action, state) { + var data = {}; + var duration = Date.now() - action._time; + data.duration = { + _custom: { + type: 'duration', + display: (duration + "ms"), + tooltip: 'Action duration', + value: duration + } + }; + if (action.payload) { + data.payload = action.payload; + } + data.state = state; + + api.addTimelineEvent({ + layerId: ACTIONS_LAYER_ID, + event: { + time: Date.now(), + title: action.type, + groupId: action._id, + subtitle: 'end', + data: data + } + }); + } + }); + } + ); + } + + // extracted from tailwind palette + var COLOR_LIME_500 = 0x84cc16; + var COLOR_DARK = 0x666666; + var COLOR_WHITE = 0xffffff; + + var TAG_NAMESPACED = { + label: 'namespaced', + textColor: COLOR_WHITE, + backgroundColor: COLOR_DARK + }; + + /** + * @param {string} path + */ + function extractNameFromPath (path) { + return path && path !== 'root' ? path.split('/').slice(-2, -1)[0] : 'Root' + } + + /** + * @param {*} module + * @return {import('@vue/devtools-api').CustomInspectorNode} + */ + function formatStoreForInspectorTree (module, path) { + return { + id: path || 'root', + // all modules end with a `/`, we want the last segment only + // cart/ -> cart + // nested/cart/ -> cart + label: extractNameFromPath(path), + tags: module.namespaced ? [TAG_NAMESPACED] : [], + children: Object.keys(module._children).map(function (moduleName) { return formatStoreForInspectorTree( + module._children[moduleName], + path + moduleName + '/' + ); } + ) + } + } + + /** + * @param {import('@vue/devtools-api').CustomInspectorNode[]} result + * @param {*} module + * @param {string} filter + * @param {string} path + */ + function flattenStoreForInspectorTree (result, module, filter, path) { + if (path.includes(filter)) { + result.push({ + id: path || 'root', + label: path.endsWith('/') ? path.slice(0, path.length - 1) : path || 'Root', + tags: module.namespaced ? [TAG_NAMESPACED] : [] + }); + } + Object.keys(module._children).forEach(function (moduleName) { + flattenStoreForInspectorTree(result, module._children[moduleName], filter, path + moduleName + '/'); + }); + } + + /** + * @param {*} module + * @return {import('@vue/devtools-api').CustomInspectorState} + */ + function formatStoreForInspectorState (module, getters, path) { + getters = path === 'root' ? getters : getters[path]; + var gettersKeys = Object.keys(getters); + var storeState = { + state: Object.keys(module.state).map(function (key) { return ({ + key: key, + editable: true, + value: module.state[key] + }); }) + }; + + if (gettersKeys.length) { + var tree = transformPathsToObjectTree(getters); + storeState.getters = Object.keys(tree).map(function (key) { return ({ + key: key.endsWith('/') ? extractNameFromPath(key) : key, + editable: false, + value: canThrow(function () { return tree[key]; }) + }); }); + } + + return storeState + } + + function transformPathsToObjectTree (getters) { + var result = {}; + Object.keys(getters).forEach(function (key) { + var path = key.split('/'); + if (path.length > 1) { + var target = result; + var leafKey = path.pop(); + path.forEach(function (p) { + if (!target[p]) { + target[p] = { + _custom: { + value: {}, + display: p, + tooltip: 'Module', + abstract: true + } + }; + } + target = target[p]._custom.value; + }); + target[leafKey] = canThrow(function () { return getters[key]; }); + } else { + result[key] = canThrow(function () { return getters[key]; }); + } + }); + return result + } + + function getStoreModule (moduleMap, path) { + var names = path.split('/').filter(function (n) { return n; }); + return names.reduce( + function (module, moduleName, i) { + var child = module[moduleName]; + if (!child) { + throw new Error(("Missing module \"" + moduleName + "\" for path \"" + path + "\".")) + } + return i === names.length - 1 ? child : child._children + }, + path === 'root' ? moduleMap : moduleMap.root._children + ) + } + + function canThrow (cb) { + try { + return cb() + } catch (e) { + return e + } + } + + // Base data struct for store's module, package with some attribute and method + var Module = function Module (rawModule, runtime) { + this.runtime = runtime; + // Store some children item + this._children = Object.create(null); + // Store the origin module object which passed by programmer + this._rawModule = rawModule; + var rawState = rawModule.state; + + // Store the origin module's state + this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}; + }; + + var prototypeAccessors$1 = { namespaced: { configurable: true } }; + + prototypeAccessors$1.namespaced.get = function () { + return !!this._rawModule.namespaced + }; + + Module.prototype.addChild = function addChild (key, module) { + this._children[key] = module; + }; + + Module.prototype.removeChild = function removeChild (key) { + delete this._children[key]; + }; + + Module.prototype.getChild = function getChild (key) { + return this._children[key] + }; + + Module.prototype.hasChild = function hasChild (key) { + return key in this._children + }; + + Module.prototype.update = function update (rawModule) { + this._rawModule.namespaced = rawModule.namespaced; + if (rawModule.actions) { + this._rawModule.actions = rawModule.actions; + } + if (rawModule.mutations) { + this._rawModule.mutations = rawModule.mutations; + } + if (rawModule.getters) { + this._rawModule.getters = rawModule.getters; + } + }; + + Module.prototype.forEachChild = function forEachChild (fn) { + forEachValue(this._children, fn); + }; + + Module.prototype.forEachGetter = function forEachGetter (fn) { + if (this._rawModule.getters) { + forEachValue(this._rawModule.getters, fn); + } + }; + + Module.prototype.forEachAction = function forEachAction (fn) { + if (this._rawModule.actions) { + forEachValue(this._rawModule.actions, fn); + } + }; + + Module.prototype.forEachMutation = function forEachMutation (fn) { + if (this._rawModule.mutations) { + forEachValue(this._rawModule.mutations, fn); + } + }; + + Object.defineProperties( Module.prototype, prototypeAccessors$1 ); + + var ModuleCollection = function ModuleCollection (rawRootModule) { + // register root module (Vuex.Store options) + this.register([], rawRootModule, false); + }; + + ModuleCollection.prototype.get = function get (path) { + return path.reduce(function (module, key) { + return module.getChild(key) + }, this.root) + }; + + ModuleCollection.prototype.getNamespace = function getNamespace (path) { + var module = this.root; + return path.reduce(function (namespace, key) { + module = module.getChild(key); + return namespace + (module.namespaced ? key + '/' : '') + }, '') + }; + + ModuleCollection.prototype.update = function update$1 (rawRootModule) { + update([], this.root, rawRootModule); + }; + + ModuleCollection.prototype.register = function register (path, rawModule, runtime) { + var this$1$1 = this; + if ( runtime === void 0 ) runtime = true; + + { + assertRawModule(path, rawModule); + } + + var newModule = new Module(rawModule, runtime); + if (path.length === 0) { + this.root = newModule; + } else { + var parent = this.get(path.slice(0, -1)); + parent.addChild(path[path.length - 1], newModule); + } + + // register nested modules + if (rawModule.modules) { + forEachValue(rawModule.modules, function (rawChildModule, key) { + this$1$1.register(path.concat(key), rawChildModule, runtime); + }); + } + }; + + ModuleCollection.prototype.unregister = function unregister (path) { var parent = this.get(path.slice(0, -1)); var key = path[path.length - 1]; var child = parent.getChild(key); - if (!child) { - if ((process.env.NODE_ENV !== 'production')) { - console.warn( - "[vuex] trying to unregister module '" + key + "', which is " + - "not registered" - ); - } - return - } + if (!child) { + { + console.warn( + "[vuex] trying to unregister module '" + key + "', which is " + + "not registered" + ); + } + return + } + + if (!child.runtime) { + return + } + + parent.removeChild(key); + }; + + ModuleCollection.prototype.isRegistered = function isRegistered (path) { + var parent = this.get(path.slice(0, -1)); + var key = path[path.length - 1]; + + if (parent) { + return parent.hasChild(key) + } + + return false + }; + + function update (path, targetModule, newModule) { + { + assertRawModule(path, newModule); + } + + // update target module + targetModule.update(newModule); + + // update nested modules + if (newModule.modules) { + for (var key in newModule.modules) { + if (!targetModule.getChild(key)) { + { + console.warn( + "[vuex] trying to add a new module '" + key + "' on hot reloading, " + + 'manual reload is needed' + ); + } + return + } + update( + path.concat(key), + targetModule.getChild(key), + newModule.modules[key] + ); + } + } + } + + var functionAssert = { + assert: function (value) { return typeof value === 'function'; }, + expected: 'function' + }; + + var objectAssert = { + assert: function (value) { return typeof value === 'function' || + (typeof value === 'object' && typeof value.handler === 'function'); }, + expected: 'function or object with "handler" function' + }; + + var assertTypes = { + getters: functionAssert, + mutations: functionAssert, + actions: objectAssert + }; + + function assertRawModule (path, rawModule) { + Object.keys(assertTypes).forEach(function (key) { + if (!rawModule[key]) { return } + + var assertOptions = assertTypes[key]; + + forEachValue(rawModule[key], function (value, type) { + assert( + assertOptions.assert(value), + makeAssertionMessage(path, key, type, value, assertOptions.expected) + ); + }); + }); + } + + function makeAssertionMessage (path, key, type, value, expected) { + var buf = key + " should be " + expected + " but \"" + key + "." + type + "\""; + if (path.length > 0) { + buf += " in module \"" + (path.join('.')) + "\""; + } + buf += " is " + (JSON.stringify(value)) + "."; + return buf + } + + var Store = function Store (options) { + var this$1$1 = this; + if ( options === void 0 ) options = {}; + + { + assert(typeof Promise !== 'undefined', "vuex requires a Promise polyfill in this browser."); + assert(this instanceof Store, "store must be called with the new operator."); + } + + var plugins = options.plugins; if ( plugins === void 0 ) plugins = []; + var strict = options.strict; if ( strict === void 0 ) strict = false; + var devtools = options.devtools; + + // store internal state + this._committing = false; + this._actions = Object.create(null); + this._actionSubscribers = []; + this._mutations = Object.create(null); + this._wrappedGetters = Object.create(null); + this._modules = new ModuleCollection(options); + this._modulesNamespaceMap = Object.create(null); + this._subscribers = []; + this._makeLocalGettersCache = Object.create(null); + + // EffectScope instance. when registering new getters, we wrap them inside + // EffectScope so that getters (computed) would not be destroyed on + // component unmount. + this._scope = null; + + this._devtools = devtools; + + // bind commit and dispatch to self + var store = this; + var ref = this; + var dispatch = ref.dispatch; + var commit = ref.commit; + this.dispatch = function boundDispatch (type, payload) { + return dispatch.call(store, type, payload) + }; + this.commit = function boundCommit (type, payload, options) { + return commit.call(store, type, payload, options) + }; + + // strict mode + this.strict = strict; + + var state = this._modules.root.state; + + // init root module. + // this also recursively registers all sub-modules + // and collects all module getters inside this._wrappedGetters + installModule(this, state, [], this._modules.root); + + // initialize the store state, which is responsible for the reactivity + // (also registers _wrappedGetters as computed properties) + resetStoreState(this, state); + + // apply plugins + plugins.forEach(function (plugin) { return plugin(this$1$1); }); + }; + + var prototypeAccessors = { state: { configurable: true } }; + + Store.prototype.install = function install (app, injectKey) { + app.provide(injectKey || storeKey, this); + app.config.globalProperties.$store = this; + + var useDevtools = this._devtools !== undefined + ? this._devtools + : true ; + + if (useDevtools) { + addDevtools(app, this); + } + }; + + prototypeAccessors.state.get = function () { + return this._state.data + }; + + prototypeAccessors.state.set = function (v) { + { + assert(false, "use store.replaceState() to explicit replace store state."); + } + }; + + Store.prototype.commit = function commit (_type, _payload, _options) { + var this$1$1 = this; + + // check object-style commit + var ref = unifyObjectStyle(_type, _payload, _options); + var type = ref.type; + var payload = ref.payload; + var options = ref.options; + + var mutation = { type: type, payload: payload }; + var entry = this._mutations[type]; + if (!entry) { + { + console.error(("[vuex] unknown mutation type: " + type)); + } + return + } + this._withCommit(function () { + entry.forEach(function commitIterator (handler) { + handler(payload); + }); + }); + + this._subscribers + .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe + .forEach(function (sub) { return sub(mutation, this$1$1.state); }); + + if ( + options && options.silent + ) { + console.warn( + "[vuex] mutation type: " + type + ". Silent option has been removed. " + + 'Use the filter functionality in the vue-devtools' + ); + } + }; + + Store.prototype.dispatch = function dispatch (_type, _payload) { + var this$1$1 = this; + + // check object-style dispatch + var ref = unifyObjectStyle(_type, _payload); + var type = ref.type; + var payload = ref.payload; + + var action = { type: type, payload: payload }; + var entry = this._actions[type]; + if (!entry) { + { + console.error(("[vuex] unknown action type: " + type)); + } + return + } + + try { + this._actionSubscribers + .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe + .filter(function (sub) { return sub.before; }) + .forEach(function (sub) { return sub.before(action, this$1$1.state); }); + } catch (e) { + { + console.warn("[vuex] error in before action subscribers: "); + console.error(e); + } + } + + var result = entry.length > 1 + ? Promise.all(entry.map(function (handler) { return handler(payload); })) + : entry[0](payload); + + return new Promise(function (resolve, reject) { + result.then(function (res) { + try { + this$1$1._actionSubscribers + .filter(function (sub) { return sub.after; }) + .forEach(function (sub) { return sub.after(action, this$1$1.state); }); + } catch (e) { + { + console.warn("[vuex] error in after action subscribers: "); + console.error(e); + } + } + resolve(res); + }, function (error) { + try { + this$1$1._actionSubscribers + .filter(function (sub) { return sub.error; }) + .forEach(function (sub) { return sub.error(action, this$1$1.state, error); }); + } catch (e) { + { + console.warn("[vuex] error in error action subscribers: "); + console.error(e); + } + } + reject(error); + }); + }) + }; + + Store.prototype.subscribe = function subscribe (fn, options) { + return genericSubscribe(fn, this._subscribers, options) + }; + + Store.prototype.subscribeAction = function subscribeAction (fn, options) { + var subs = typeof fn === 'function' ? { before: fn } : fn; + return genericSubscribe(subs, this._actionSubscribers, options) + }; + + Store.prototype.watch = function watch$1 (getter, cb, options) { + var this$1$1 = this; + + { + assert(typeof getter === 'function', "store.watch only accepts a function."); + } + return vue.watch(function () { return getter(this$1$1.state, this$1$1.getters); }, cb, Object.assign({}, options)) + }; + + Store.prototype.replaceState = function replaceState (state) { + var this$1$1 = this; + + this._withCommit(function () { + this$1$1._state.data = state; + }); + }; + + Store.prototype.registerModule = function registerModule (path, rawModule, options) { + if ( options === void 0 ) options = {}; + + if (typeof path === 'string') { path = [path]; } + + { + assert(Array.isArray(path), "module path must be a string or an Array."); + assert(path.length > 0, 'cannot register the root module by using registerModule.'); + } + + this._modules.register(path, rawModule); + installModule(this, this.state, path, this._modules.get(path), options.preserveState); + // reset store to update getters... + resetStoreState(this, this.state); + }; + + Store.prototype.unregisterModule = function unregisterModule (path) { + var this$1$1 = this; + + if (typeof path === 'string') { path = [path]; } + + { + assert(Array.isArray(path), "module path must be a string or an Array."); + } + + this._modules.unregister(path); + this._withCommit(function () { + var parentState = getNestedState(this$1$1.state, path.slice(0, -1)); + delete parentState[path[path.length - 1]]; + }); + resetStore(this); + }; + + Store.prototype.hasModule = function hasModule (path) { + if (typeof path === 'string') { path = [path]; } + + { + assert(Array.isArray(path), "module path must be a string or an Array."); + } + + return this._modules.isRegistered(path) + }; + + Store.prototype.hotUpdate = function hotUpdate (newOptions) { + this._modules.update(newOptions); + resetStore(this, true); + }; + + Store.prototype._withCommit = function _withCommit (fn) { + var committing = this._committing; + this._committing = true; + fn(); + this._committing = committing; + }; + + Object.defineProperties( Store.prototype, prototypeAccessors ); + + /** + * Reduce the code which written in Vue.js for getting the getters + * @param {String} [namespace] - Module's namespace + * @param {Object|Array} getters + * @return {Object} + */ + var mapGetters = normalizeNamespace(function (namespace, getters) { + var res = {}; + if (!isValidMap(getters)) { + console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object'); + } + normalizeMap(getters).forEach(function (ref) { + var key = ref.key; + var val = ref.val; + + // The namespace has been mutated by normalizeNamespace + val = namespace + val; + res[key] = function mappedGetter () { + if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) { + return + } + if (!(val in this.$store.getters)) { + console.error(("[vuex] unknown getter: " + val)); + return + } + return this.$store.getters[val] + }; + // mark vuex getter for devtools + res[key].vuex = true; + }); + return res + }); + + /** + * Reduce the code which written in Vue.js for dispatch the action + * @param {String} [namespace] - Module's namespace + * @param {Object|Array} actions # Object's item can be a function which accept `dispatch` function as the first param, it can accept anthor params. You can dispatch action and do any other things in this function. specially, You need to pass anthor params from the mapped function. + * @return {Object} + */ + var mapActions = normalizeNamespace(function (namespace, actions) { + var res = {}; + if (!isValidMap(actions)) { + console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object'); + } + normalizeMap(actions).forEach(function (ref) { + var key = ref.key; + var val = ref.val; + + res[key] = function mappedAction () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + // get dispatch function from store + var dispatch = this.$store.dispatch; + if (namespace) { + var module = getModuleByNamespace(this.$store, 'mapActions', namespace); + if (!module) { + return + } + dispatch = module.context.dispatch; + } + return typeof val === 'function' + ? val.apply(this, [dispatch].concat(args)) + : dispatch.apply(this.$store, [val].concat(args)) + }; + }); + return res + }); + + /** + * Normalize the map + * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ] + * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ] + * @param {Array|Object} map + * @return {Object} + */ + function normalizeMap (map) { + if (!isValidMap(map)) { + return [] + } + return Array.isArray(map) + ? map.map(function (key) { return ({ key: key, val: key }); }) + : Object.keys(map).map(function (key) { return ({ key: key, val: map[key] }); }) + } + + /** + * Validate whether given map is valid or not + * @param {*} map + * @return {Boolean} + */ + function isValidMap (map) { + return Array.isArray(map) || isObject$1(map) + } + + /** + * Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map. + * @param {Function} fn + * @return {Function} + */ + function normalizeNamespace (fn) { + return function (namespace, map) { + if (typeof namespace !== 'string') { + map = namespace; + namespace = ''; + } else if (namespace.charAt(namespace.length - 1) !== '/') { + namespace += '/'; + } + return fn(namespace, map) + } + } + + /** + * Search a special module from store by namespace. if module not exist, print error message. + * @param {Object} store + * @param {String} helper + * @param {String} namespace + * @return {Object} + */ + function getModuleByNamespace (store, helper, namespace) { + var module = store._modulesNamespaceMap[namespace]; + if (!module) { + console.error(("[vuex] module namespace not found in " + helper + "(): " + namespace)); + } + return module + } + + function bind(fn, thisArg) { + return function wrap() { + return fn.apply(thisArg, arguments); + }; + } + + // utils is a library of generic helper functions non-specific to axios + + const {toString: toString$1} = Object.prototype; + const {getPrototypeOf} = Object; + + const kindOf = (cache => thing => { + const str = toString$1.call(thing); + return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase()); + })(Object.create(null)); + + const kindOfTest = (type) => { + type = type.toLowerCase(); + return (thing) => kindOf(thing) === type + }; + + const typeOfTest = type => thing => typeof thing === type; + + /** + * Determine if a value is an Array + * + * @param {Object} val The value to test + * + * @returns {boolean} True if value is an Array, otherwise false + */ + const {isArray: isArray$1} = Array; + + /** + * Determine if a value is undefined + * + * @param {*} val The value to test + * + * @returns {boolean} True if the value is undefined, otherwise false + */ + const isUndefined = typeOfTest('undefined'); + + /** + * Determine if a value is a Buffer + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is a Buffer, otherwise false + */ + function isBuffer$1(val) { + return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor) + && isFunction(val.constructor.isBuffer) && val.constructor.isBuffer(val); + } + + /** + * Determine if a value is an ArrayBuffer + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is an ArrayBuffer, otherwise false + */ + const isArrayBuffer = kindOfTest('ArrayBuffer'); + + + /** + * Determine if a value is a view on an ArrayBuffer + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is a view on an ArrayBuffer, otherwise false + */ + function isArrayBufferView(val) { + let result; + if ((typeof ArrayBuffer !== 'undefined') && (ArrayBuffer.isView)) { + result = ArrayBuffer.isView(val); + } else { + result = (val) && (val.buffer) && (isArrayBuffer(val.buffer)); + } + return result; + } + + /** + * Determine if a value is a String + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is a String, otherwise false + */ + const isString = typeOfTest('string'); + + /** + * Determine if a value is a Function + * + * @param {*} val The value to test + * @returns {boolean} True if value is a Function, otherwise false + */ + const isFunction = typeOfTest('function'); + + /** + * Determine if a value is a Number + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is a Number, otherwise false + */ + const isNumber = typeOfTest('number'); + + /** + * Determine if a value is an Object + * + * @param {*} thing The value to test + * + * @returns {boolean} True if value is an Object, otherwise false + */ + const isObject = (thing) => thing !== null && typeof thing === 'object'; + + /** + * Determine if a value is a Boolean + * + * @param {*} thing The value to test + * @returns {boolean} True if value is a Boolean, otherwise false + */ + const isBoolean = thing => thing === true || thing === false; + + /** + * Determine if a value is a plain Object + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is a plain Object, otherwise false + */ + const isPlainObject = (val) => { + if (kindOf(val) !== 'object') { + return false; + } + + const prototype = getPrototypeOf(val); + return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in val) && !(Symbol.iterator in val); + }; + + /** + * Determine if a value is a Date + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is a Date, otherwise false + */ + const isDate = kindOfTest('Date'); + + /** + * Determine if a value is a File + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is a File, otherwise false + */ + const isFile = kindOfTest('File'); + + /** + * Determine if a value is a Blob + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is a Blob, otherwise false + */ + const isBlob = kindOfTest('Blob'); + + /** + * Determine if a value is a FileList + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is a File, otherwise false + */ + const isFileList = kindOfTest('FileList'); + + /** + * Determine if a value is a Stream + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is a Stream, otherwise false + */ + const isStream = (val) => isObject(val) && isFunction(val.pipe); + + /** + * Determine if a value is a FormData + * + * @param {*} thing The value to test + * + * @returns {boolean} True if value is an FormData, otherwise false + */ + const isFormData = (thing) => { + let kind; + return thing && ( + (typeof FormData === 'function' && thing instanceof FormData) || ( + isFunction(thing.append) && ( + (kind = kindOf(thing)) === 'formdata' || + // detect form-data instance + (kind === 'object' && isFunction(thing.toString) && thing.toString() === '[object FormData]') + ) + ) + ) + }; + + /** + * Determine if a value is a URLSearchParams object + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is a URLSearchParams object, otherwise false + */ + const isURLSearchParams = kindOfTest('URLSearchParams'); + + /** + * Trim excess whitespace off the beginning and end of a string + * + * @param {String} str The String to trim + * + * @returns {String} The String freed of excess whitespace + */ + const trim = (str) => str.trim ? + str.trim() : str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); + + /** + * Iterate over an Array or an Object invoking a function for each item. + * + * If `obj` is an Array callback will be called passing + * the value, index, and complete array for each item. + * + * If 'obj' is an Object callback will be called passing + * the value, key, and complete object for each property. + * + * @param {Object|Array} obj The object to iterate + * @param {Function} fn The callback to invoke for each item + * + * @param {Boolean} [allOwnKeys = false] + * @returns {any} + */ + function forEach(obj, fn, {allOwnKeys = false} = {}) { + // Don't bother if no value provided + if (obj === null || typeof obj === 'undefined') { + return; + } + + let i; + let l; + + // Force an array if not already something iterable + if (typeof obj !== 'object') { + /*eslint no-param-reassign:0*/ + obj = [obj]; + } + + if (isArray$1(obj)) { + // Iterate over array values + for (i = 0, l = obj.length; i < l; i++) { + fn.call(null, obj[i], i, obj); + } + } else { + // Iterate over object keys + const keys = allOwnKeys ? Object.getOwnPropertyNames(obj) : Object.keys(obj); + const len = keys.length; + let key; + + for (i = 0; i < len; i++) { + key = keys[i]; + fn.call(null, obj[key], key, obj); + } + } + } + + function findKey(obj, key) { + key = key.toLowerCase(); + const keys = Object.keys(obj); + let i = keys.length; + let _key; + while (i-- > 0) { + _key = keys[i]; + if (key === _key.toLowerCase()) { + return _key; + } + } + return null; + } + + const _global = (() => { + /*eslint no-undef:0*/ + if (typeof globalThis !== "undefined") return globalThis; + return typeof self !== "undefined" ? self : (typeof window !== 'undefined' ? window : global$1) + })(); + + const isContextDefined = (context) => !isUndefined(context) && context !== _global; + + /** + * Accepts varargs expecting each argument to be an object, then + * immutably merges the properties of each object and returns result. + * + * When multiple objects contain the same key the later object in + * the arguments list will take precedence. + * + * Example: + * + * ```js + * var result = merge({foo: 123}, {foo: 456}); + * console.log(result.foo); // outputs 456 + * ``` + * + * @param {Object} obj1 Object to merge + * + * @returns {Object} Result of all merge properties + */ + function merge(/* obj1, obj2, obj3, ... */) { + const {caseless} = isContextDefined(this) && this || {}; + const result = {}; + const assignValue = (val, key) => { + const targetKey = caseless && findKey(result, key) || key; + if (isPlainObject(result[targetKey]) && isPlainObject(val)) { + result[targetKey] = merge(result[targetKey], val); + } else if (isPlainObject(val)) { + result[targetKey] = merge({}, val); + } else if (isArray$1(val)) { + result[targetKey] = val.slice(); + } else { + result[targetKey] = val; + } + }; + + for (let i = 0, l = arguments.length; i < l; i++) { + arguments[i] && forEach(arguments[i], assignValue); + } + return result; + } + + /** + * Extends object a by mutably adding to it the properties of object b. + * + * @param {Object} a The object to be extended + * @param {Object} b The object to copy properties from + * @param {Object} thisArg The object to bind function to + * + * @param {Boolean} [allOwnKeys] + * @returns {Object} The resulting value of object a + */ + const extend = (a, b, thisArg, {allOwnKeys}= {}) => { + forEach(b, (val, key) => { + if (thisArg && isFunction(val)) { + a[key] = bind(val, thisArg); + } else { + a[key] = val; + } + }, {allOwnKeys}); + return a; + }; + + /** + * Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) + * + * @param {string} content with BOM + * + * @returns {string} content value without BOM + */ + const stripBOM = (content) => { + if (content.charCodeAt(0) === 0xFEFF) { + content = content.slice(1); + } + return content; + }; + + /** + * Inherit the prototype methods from one constructor into another + * @param {function} constructor + * @param {function} superConstructor + * @param {object} [props] + * @param {object} [descriptors] + * + * @returns {void} + */ + const inherits = (constructor, superConstructor, props, descriptors) => { + constructor.prototype = Object.create(superConstructor.prototype, descriptors); + constructor.prototype.constructor = constructor; + Object.defineProperty(constructor, 'super', { + value: superConstructor.prototype + }); + props && Object.assign(constructor.prototype, props); + }; + + /** + * Resolve object with deep prototype chain to a flat object + * @param {Object} sourceObj source object + * @param {Object} [destObj] + * @param {Function|Boolean} [filter] + * @param {Function} [propFilter] + * + * @returns {Object} + */ + const toFlatObject = (sourceObj, destObj, filter, propFilter) => { + let props; + let i; + let prop; + const merged = {}; + + destObj = destObj || {}; + // eslint-disable-next-line no-eq-null,eqeqeq + if (sourceObj == null) return destObj; + + do { + props = Object.getOwnPropertyNames(sourceObj); + i = props.length; + while (i-- > 0) { + prop = props[i]; + if ((!propFilter || propFilter(prop, sourceObj, destObj)) && !merged[prop]) { + destObj[prop] = sourceObj[prop]; + merged[prop] = true; + } + } + sourceObj = filter !== false && getPrototypeOf(sourceObj); + } while (sourceObj && (!filter || filter(sourceObj, destObj)) && sourceObj !== Object.prototype); + + return destObj; + }; + + /** + * Determines whether a string ends with the characters of a specified string + * + * @param {String} str + * @param {String} searchString + * @param {Number} [position= 0] + * + * @returns {boolean} + */ + const endsWith = (str, searchString, position) => { + str = String(str); + if (position === undefined || position > str.length) { + position = str.length; + } + position -= searchString.length; + const lastIndex = str.indexOf(searchString, position); + return lastIndex !== -1 && lastIndex === position; + }; + + + /** + * Returns new array from array like object or null if failed + * + * @param {*} [thing] + * + * @returns {?Array} + */ + const toArray = (thing) => { + if (!thing) return null; + if (isArray$1(thing)) return thing; + let i = thing.length; + if (!isNumber(i)) return null; + const arr = new Array(i); + while (i-- > 0) { + arr[i] = thing[i]; + } + return arr; + }; + + /** + * Checking if the Uint8Array exists and if it does, it returns a function that checks if the + * thing passed in is an instance of Uint8Array + * + * @param {TypedArray} + * + * @returns {Array} + */ + // eslint-disable-next-line func-names + const isTypedArray = (TypedArray => { + // eslint-disable-next-line func-names + return thing => { + return TypedArray && thing instanceof TypedArray; + }; + })(typeof Uint8Array !== 'undefined' && getPrototypeOf(Uint8Array)); + + /** + * For each entry in the object, call the function with the key and value. + * + * @param {Object} obj - The object to iterate over. + * @param {Function} fn - The function to call for each entry. + * + * @returns {void} + */ + const forEachEntry = (obj, fn) => { + const generator = obj && obj[Symbol.iterator]; + + const iterator = generator.call(obj); + + let result; + + while ((result = iterator.next()) && !result.done) { + const pair = result.value; + fn.call(obj, pair[0], pair[1]); + } + }; + + /** + * It takes a regular expression and a string, and returns an array of all the matches + * + * @param {string} regExp - The regular expression to match against. + * @param {string} str - The string to search. + * + * @returns {Array} + */ + const matchAll = (regExp, str) => { + let matches; + const arr = []; + + while ((matches = regExp.exec(str)) !== null) { + arr.push(matches); + } + + return arr; + }; + + /* Checking if the kindOfTest function returns true when passed an HTMLFormElement. */ + const isHTMLForm = kindOfTest('HTMLFormElement'); + + const toCamelCase = str => { + return str.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g, + function replacer(m, p1, p2) { + return p1.toUpperCase() + p2; + } + ); + }; + + /* Creating a function that will check if an object has a property. */ + const hasOwnProperty = (({hasOwnProperty}) => (obj, prop) => hasOwnProperty.call(obj, prop))(Object.prototype); + + /** + * Determine if a value is a RegExp object + * + * @param {*} val The value to test + * + * @returns {boolean} True if value is a RegExp object, otherwise false + */ + const isRegExp = kindOfTest('RegExp'); + + const reduceDescriptors = (obj, reducer) => { + const descriptors = Object.getOwnPropertyDescriptors(obj); + const reducedDescriptors = {}; + + forEach(descriptors, (descriptor, name) => { + let ret; + if ((ret = reducer(descriptor, name, obj)) !== false) { + reducedDescriptors[name] = ret || descriptor; + } + }); + + Object.defineProperties(obj, reducedDescriptors); + }; + + /** + * Makes all methods read-only + * @param {Object} obj + */ + + const freezeMethods = (obj) => { + reduceDescriptors(obj, (descriptor, name) => { + // skip restricted props in strict mode + if (isFunction(obj) && ['arguments', 'caller', 'callee'].indexOf(name) !== -1) { + return false; + } + + const value = obj[name]; + + if (!isFunction(value)) return; + + descriptor.enumerable = false; + + if ('writable' in descriptor) { + descriptor.writable = false; + return; + } + + if (!descriptor.set) { + descriptor.set = () => { + throw Error('Can not rewrite read-only method \'' + name + '\''); + }; + } + }); + }; + + const toObjectSet = (arrayOrString, delimiter) => { + const obj = {}; + + const define = (arr) => { + arr.forEach(value => { + obj[value] = true; + }); + }; + + isArray$1(arrayOrString) ? define(arrayOrString) : define(String(arrayOrString).split(delimiter)); + + return obj; + }; + + const noop = () => {}; + + const toFiniteNumber = (value, defaultValue) => { + value = +value; + return Number.isFinite(value) ? value : defaultValue; + }; + + const ALPHA = 'abcdefghijklmnopqrstuvwxyz'; + + const DIGIT = '0123456789'; + + const ALPHABET = { + DIGIT, + ALPHA, + ALPHA_DIGIT: ALPHA + ALPHA.toUpperCase() + DIGIT + }; + + const generateString = (size = 16, alphabet = ALPHABET.ALPHA_DIGIT) => { + let str = ''; + const {length} = alphabet; + while (size--) { + str += alphabet[Math.random() * length|0]; + } + + return str; + }; + + /** + * If the thing is a FormData object, return true, otherwise return false. + * + * @param {unknown} thing - The thing to check. + * + * @returns {boolean} + */ + function isSpecCompliantForm(thing) { + return !!(thing && isFunction(thing.append) && thing[Symbol.toStringTag] === 'FormData' && thing[Symbol.iterator]); + } + + const toJSONObject = (obj) => { + const stack = new Array(10); + + const visit = (source, i) => { + + if (isObject(source)) { + if (stack.indexOf(source) >= 0) { + return; + } + + if(!('toJSON' in source)) { + stack[i] = source; + const target = isArray$1(source) ? [] : {}; + + forEach(source, (value, key) => { + const reducedValue = visit(value, i + 1); + !isUndefined(reducedValue) && (target[key] = reducedValue); + }); + + stack[i] = undefined; + + return target; + } + } + + return source; + }; + + return visit(obj, 0); + }; + + const isAsyncFn = kindOfTest('AsyncFunction'); + + const isThenable = (thing) => + thing && (isObject(thing) || isFunction(thing)) && isFunction(thing.then) && isFunction(thing.catch); + + var utils$1 = { + isArray: isArray$1, + isArrayBuffer, + isBuffer: isBuffer$1, + isFormData, + isArrayBufferView, + isString, + isNumber, + isBoolean, + isObject, + isPlainObject, + isUndefined, + isDate, + isFile, + isBlob, + isRegExp, + isFunction, + isStream, + isURLSearchParams, + isTypedArray, + isFileList, + forEach, + merge, + extend, + trim, + stripBOM, + inherits, + toFlatObject, + kindOf, + kindOfTest, + endsWith, + toArray, + forEachEntry, + matchAll, + isHTMLForm, + hasOwnProperty, + hasOwnProp: hasOwnProperty, // an alias to avoid ESLint no-prototype-builtins detection + reduceDescriptors, + freezeMethods, + toObjectSet, + toCamelCase, + noop, + toFiniteNumber, + findKey, + global: _global, + isContextDefined, + ALPHABET, + generateString, + isSpecCompliantForm, + toJSONObject, + isAsyncFn, + isThenable + }; + + var lookup = []; + var revLookup = []; + var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array; + var inited = false; + function init () { + inited = true; + var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + for (var i = 0, len = code.length; i < len; ++i) { + lookup[i] = code[i]; + revLookup[code.charCodeAt(i)] = i; + } + + revLookup['-'.charCodeAt(0)] = 62; + revLookup['_'.charCodeAt(0)] = 63; + } + + function toByteArray (b64) { + if (!inited) { + init(); + } + var i, j, l, tmp, placeHolders, arr; + var len = b64.length; + + if (len % 4 > 0) { + throw new Error('Invalid string. Length must be a multiple of 4') + } + + // the number of equal signs (place holders) + // if there are two placeholders, than the two characters before it + // represent one byte + // if there is only one, then the three characters before it represent 2 bytes + // this is just a cheap hack to not do indexOf twice + placeHolders = b64[len - 2] === '=' ? 2 : b64[len - 1] === '=' ? 1 : 0; + + // base64 is 4/3 + up to two characters of the original data + arr = new Arr(len * 3 / 4 - placeHolders); + + // if there are placeholders, only get up to the last complete 4 chars + l = placeHolders > 0 ? len - 4 : len; + + var L = 0; + + for (i = 0, j = 0; i < l; i += 4, j += 3) { + tmp = (revLookup[b64.charCodeAt(i)] << 18) | (revLookup[b64.charCodeAt(i + 1)] << 12) | (revLookup[b64.charCodeAt(i + 2)] << 6) | revLookup[b64.charCodeAt(i + 3)]; + arr[L++] = (tmp >> 16) & 0xFF; + arr[L++] = (tmp >> 8) & 0xFF; + arr[L++] = tmp & 0xFF; + } + + if (placeHolders === 2) { + tmp = (revLookup[b64.charCodeAt(i)] << 2) | (revLookup[b64.charCodeAt(i + 1)] >> 4); + arr[L++] = tmp & 0xFF; + } else if (placeHolders === 1) { + tmp = (revLookup[b64.charCodeAt(i)] << 10) | (revLookup[b64.charCodeAt(i + 1)] << 4) | (revLookup[b64.charCodeAt(i + 2)] >> 2); + arr[L++] = (tmp >> 8) & 0xFF; + arr[L++] = tmp & 0xFF; + } + + return arr + } + + function tripletToBase64 (num) { + return lookup[num >> 18 & 0x3F] + lookup[num >> 12 & 0x3F] + lookup[num >> 6 & 0x3F] + lookup[num & 0x3F] + } + + function encodeChunk (uint8, start, end) { + var tmp; + var output = []; + for (var i = start; i < end; i += 3) { + tmp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2]); + output.push(tripletToBase64(tmp)); + } + return output.join('') + } + + function fromByteArray (uint8) { + if (!inited) { + init(); + } + var tmp; + var len = uint8.length; + var extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes + var output = ''; + var parts = []; + var maxChunkLength = 16383; // must be multiple of 3 + + // go through the array every three bytes, we'll deal with trailing stuff later + for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { + parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength))); + } + + // pad the end with zeros, but make sure to not forget the extra bytes + if (extraBytes === 1) { + tmp = uint8[len - 1]; + output += lookup[tmp >> 2]; + output += lookup[(tmp << 4) & 0x3F]; + output += '=='; + } else if (extraBytes === 2) { + tmp = (uint8[len - 2] << 8) + (uint8[len - 1]); + output += lookup[tmp >> 10]; + output += lookup[(tmp >> 4) & 0x3F]; + output += lookup[(tmp << 2) & 0x3F]; + output += '='; + } + + parts.push(output); + + return parts.join('') + } + + function read (buffer, offset, isLE, mLen, nBytes) { + var e, m; + var eLen = nBytes * 8 - mLen - 1; + var eMax = (1 << eLen) - 1; + var eBias = eMax >> 1; + var nBits = -7; + var i = isLE ? (nBytes - 1) : 0; + var d = isLE ? -1 : 1; + var s = buffer[offset + i]; + + i += d; + + e = s & ((1 << (-nBits)) - 1); + s >>= (-nBits); + nBits += eLen; + for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8) {} + + m = e & ((1 << (-nBits)) - 1); + e >>= (-nBits); + nBits += mLen; + for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8) {} + + if (e === 0) { + e = 1 - eBias; + } else if (e === eMax) { + return m ? NaN : ((s ? -1 : 1) * Infinity) + } else { + m = m + Math.pow(2, mLen); + e = e - eBias; + } + return (s ? -1 : 1) * m * Math.pow(2, e - mLen) + } + + function write (buffer, value, offset, isLE, mLen, nBytes) { + var e, m, c; + var eLen = nBytes * 8 - mLen - 1; + var eMax = (1 << eLen) - 1; + var eBias = eMax >> 1; + var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0); + var i = isLE ? 0 : (nBytes - 1); + var d = isLE ? 1 : -1; + var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0; + + value = Math.abs(value); + + if (isNaN(value) || value === Infinity) { + m = isNaN(value) ? 1 : 0; + e = eMax; + } else { + e = Math.floor(Math.log(value) / Math.LN2); + if (value * (c = Math.pow(2, -e)) < 1) { + e--; + c *= 2; + } + if (e + eBias >= 1) { + value += rt / c; + } else { + value += rt * Math.pow(2, 1 - eBias); + } + if (value * c >= 2) { + e++; + c /= 2; + } + + if (e + eBias >= eMax) { + m = 0; + e = eMax; + } else if (e + eBias >= 1) { + m = (value * c - 1) * Math.pow(2, mLen); + e = e + eBias; + } else { + m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen); + e = 0; + } + } + + for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {} + + e = (e << mLen) | m; + eLen += mLen; + for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {} + + buffer[offset + i - d] |= s * 128; + } + + var toString = {}.toString; + + var isArray = Array.isArray || function (arr) { + return toString.call(arr) == '[object Array]'; + }; + + var INSPECT_MAX_BYTES = 50; + + /** + * If `Buffer.TYPED_ARRAY_SUPPORT`: + * === true Use Uint8Array implementation (fastest) + * === false Use Object implementation (most compatible, even IE6) + * + * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+, + * Opera 11.6+, iOS 4.2+. + * + * Due to various browser bugs, sometimes the Object implementation will be used even + * when the browser supports typed arrays. + * + * Note: + * + * - Firefox 4-29 lacks support for adding new properties to `Uint8Array` instances, + * See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438. + * + * - Chrome 9-10 is missing the `TypedArray.prototype.subarray` function. + * + * - IE10 has a broken `TypedArray.prototype.subarray` function which returns arrays of + * incorrect length in some situations. + + * We detect these buggy browsers and set `Buffer.TYPED_ARRAY_SUPPORT` to `false` so they + * get the Object implementation, which is slower but behaves correctly. + */ + Buffer.TYPED_ARRAY_SUPPORT = global$1.TYPED_ARRAY_SUPPORT !== undefined + ? global$1.TYPED_ARRAY_SUPPORT + : true; + + /* + * Export kMaxLength after typed array support is determined. + */ + kMaxLength(); + + function kMaxLength () { + return Buffer.TYPED_ARRAY_SUPPORT + ? 0x7fffffff + : 0x3fffffff + } + + function createBuffer (that, length) { + if (kMaxLength() < length) { + throw new RangeError('Invalid typed array length') + } + if (Buffer.TYPED_ARRAY_SUPPORT) { + // Return an augmented `Uint8Array` instance, for best performance + that = new Uint8Array(length); + that.__proto__ = Buffer.prototype; + } else { + // Fallback: Return an object instance of the Buffer class + if (that === null) { + that = new Buffer(length); + } + that.length = length; + } + + return that + } + + /** + * The Buffer constructor returns instances of `Uint8Array` that have their + * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of + * `Uint8Array`, so the returned instances will have all the node `Buffer` methods + * and the `Uint8Array` methods. Square bracket notation works as expected -- it + * returns a single octet. + * + * The `Uint8Array` prototype remains unmodified. + */ + + function Buffer (arg, encodingOrOffset, length) { + if (!Buffer.TYPED_ARRAY_SUPPORT && !(this instanceof Buffer)) { + return new Buffer(arg, encodingOrOffset, length) + } + + // Common case. + if (typeof arg === 'number') { + if (typeof encodingOrOffset === 'string') { + throw new Error( + 'If encoding is specified then the first argument must be a string' + ) + } + return allocUnsafe(this, arg) + } + return from(this, arg, encodingOrOffset, length) + } + + Buffer.poolSize = 8192; // not used by this implementation + + // TODO: Legacy, not needed anymore. Remove in next major version. + Buffer._augment = function (arr) { + arr.__proto__ = Buffer.prototype; + return arr + }; + + function from (that, value, encodingOrOffset, length) { + if (typeof value === 'number') { + throw new TypeError('"value" argument must not be a number') + } + + if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) { + return fromArrayBuffer(that, value, encodingOrOffset, length) + } + + if (typeof value === 'string') { + return fromString(that, value, encodingOrOffset) + } + + return fromObject(that, value) + } + + /** + * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError + * if value is a number. + * Buffer.from(str[, encoding]) + * Buffer.from(array) + * Buffer.from(buffer) + * Buffer.from(arrayBuffer[, byteOffset[, length]]) + **/ + Buffer.from = function (value, encodingOrOffset, length) { + return from(null, value, encodingOrOffset, length) + }; + + if (Buffer.TYPED_ARRAY_SUPPORT) { + Buffer.prototype.__proto__ = Uint8Array.prototype; + Buffer.__proto__ = Uint8Array; + if (typeof Symbol !== 'undefined' && Symbol.species && + Buffer[Symbol.species] === Buffer) ; + } + + function assertSize (size) { + if (typeof size !== 'number') { + throw new TypeError('"size" argument must be a number') + } else if (size < 0) { + throw new RangeError('"size" argument must not be negative') + } + } + + function alloc (that, size, fill, encoding) { + assertSize(size); + if (size <= 0) { + return createBuffer(that, size) + } + if (fill !== undefined) { + // Only pay attention to encoding if it's a string. This + // prevents accidentally sending in a number that would + // be interpretted as a start offset. + return typeof encoding === 'string' + ? createBuffer(that, size).fill(fill, encoding) + : createBuffer(that, size).fill(fill) + } + return createBuffer(that, size) + } + + /** + * Creates a new filled Buffer instance. + * alloc(size[, fill[, encoding]]) + **/ + Buffer.alloc = function (size, fill, encoding) { + return alloc(null, size, fill, encoding) + }; + + function allocUnsafe (that, size) { + assertSize(size); + that = createBuffer(that, size < 0 ? 0 : checked(size) | 0); + if (!Buffer.TYPED_ARRAY_SUPPORT) { + for (var i = 0; i < size; ++i) { + that[i] = 0; + } + } + return that + } + + /** + * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance. + * */ + Buffer.allocUnsafe = function (size) { + return allocUnsafe(null, size) + }; + /** + * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance. + */ + Buffer.allocUnsafeSlow = function (size) { + return allocUnsafe(null, size) + }; + + function fromString (that, string, encoding) { + if (typeof encoding !== 'string' || encoding === '') { + encoding = 'utf8'; + } + + if (!Buffer.isEncoding(encoding)) { + throw new TypeError('"encoding" must be a valid string encoding') + } + + var length = byteLength(string, encoding) | 0; + that = createBuffer(that, length); + + var actual = that.write(string, encoding); + + if (actual !== length) { + // Writing a hex string, for example, that contains invalid characters will + // cause everything after the first invalid character to be ignored. (e.g. + // 'abxxcd' will be treated as 'ab') + that = that.slice(0, actual); + } + + return that + } + + function fromArrayLike (that, array) { + var length = array.length < 0 ? 0 : checked(array.length) | 0; + that = createBuffer(that, length); + for (var i = 0; i < length; i += 1) { + that[i] = array[i] & 255; + } + return that + } + + function fromArrayBuffer (that, array, byteOffset, length) { + array.byteLength; // this throws if `array` is not a valid ArrayBuffer + + if (byteOffset < 0 || array.byteLength < byteOffset) { + throw new RangeError('\'offset\' is out of bounds') + } + + if (array.byteLength < byteOffset + (length || 0)) { + throw new RangeError('\'length\' is out of bounds') + } + + if (byteOffset === undefined && length === undefined) { + array = new Uint8Array(array); + } else if (length === undefined) { + array = new Uint8Array(array, byteOffset); + } else { + array = new Uint8Array(array, byteOffset, length); + } + + if (Buffer.TYPED_ARRAY_SUPPORT) { + // Return an augmented `Uint8Array` instance, for best performance + that = array; + that.__proto__ = Buffer.prototype; + } else { + // Fallback: Return an object instance of the Buffer class + that = fromArrayLike(that, array); + } + return that + } + + function fromObject (that, obj) { + if (internalIsBuffer(obj)) { + var len = checked(obj.length) | 0; + that = createBuffer(that, len); + + if (that.length === 0) { + return that + } + + obj.copy(that, 0, 0, len); + return that + } + + if (obj) { + if ((typeof ArrayBuffer !== 'undefined' && + obj.buffer instanceof ArrayBuffer) || 'length' in obj) { + if (typeof obj.length !== 'number' || isnan(obj.length)) { + return createBuffer(that, 0) + } + return fromArrayLike(that, obj) + } + + if (obj.type === 'Buffer' && isArray(obj.data)) { + return fromArrayLike(that, obj.data) + } + } + + throw new TypeError('First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.') + } + + function checked (length) { + // Note: cannot use `length < kMaxLength()` here because that fails when + // length is NaN (which is otherwise coerced to zero.) + if (length >= kMaxLength()) { + throw new RangeError('Attempt to allocate Buffer larger than maximum ' + + 'size: 0x' + kMaxLength().toString(16) + ' bytes') + } + return length | 0 + } + Buffer.isBuffer = isBuffer; + function internalIsBuffer (b) { + return !!(b != null && b._isBuffer) + } + + Buffer.compare = function compare (a, b) { + if (!internalIsBuffer(a) || !internalIsBuffer(b)) { + throw new TypeError('Arguments must be Buffers') + } + + if (a === b) return 0 + + var x = a.length; + var y = b.length; + + for (var i = 0, len = Math.min(x, y); i < len; ++i) { + if (a[i] !== b[i]) { + x = a[i]; + y = b[i]; + break + } + } + + if (x < y) return -1 + if (y < x) return 1 + return 0 + }; + + Buffer.isEncoding = function isEncoding (encoding) { + switch (String(encoding).toLowerCase()) { + case 'hex': + case 'utf8': + case 'utf-8': + case 'ascii': + case 'latin1': + case 'binary': + case 'base64': + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return true + default: + return false + } + }; + + Buffer.concat = function concat (list, length) { + if (!isArray(list)) { + throw new TypeError('"list" argument must be an Array of Buffers') + } + + if (list.length === 0) { + return Buffer.alloc(0) + } + + var i; + if (length === undefined) { + length = 0; + for (i = 0; i < list.length; ++i) { + length += list[i].length; + } + } + + var buffer = Buffer.allocUnsafe(length); + var pos = 0; + for (i = 0; i < list.length; ++i) { + var buf = list[i]; + if (!internalIsBuffer(buf)) { + throw new TypeError('"list" argument must be an Array of Buffers') + } + buf.copy(buffer, pos); + pos += buf.length; + } + return buffer + }; + + function byteLength (string, encoding) { + if (internalIsBuffer(string)) { + return string.length + } + if (typeof ArrayBuffer !== 'undefined' && typeof ArrayBuffer.isView === 'function' && + (ArrayBuffer.isView(string) || string instanceof ArrayBuffer)) { + return string.byteLength + } + if (typeof string !== 'string') { + string = '' + string; + } + + var len = string.length; + if (len === 0) return 0 + + // Use a for loop to avoid recursion + var loweredCase = false; + for (;;) { + switch (encoding) { + case 'ascii': + case 'latin1': + case 'binary': + return len + case 'utf8': + case 'utf-8': + case undefined: + return utf8ToBytes(string).length + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return len * 2 + case 'hex': + return len >>> 1 + case 'base64': + return base64ToBytes(string).length + default: + if (loweredCase) return utf8ToBytes(string).length // assume utf8 + encoding = ('' + encoding).toLowerCase(); + loweredCase = true; + } + } + } + Buffer.byteLength = byteLength; + + function slowToString (encoding, start, end) { + var loweredCase = false; + + // No need to verify that "this.length <= MAX_UINT32" since it's a read-only + // property of a typed array. + + // This behaves neither like String nor Uint8Array in that we set start/end + // to their upper/lower bounds if the value passed is out of range. + // undefined is handled specially as per ECMA-262 6th Edition, + // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization. + if (start === undefined || start < 0) { + start = 0; + } + // Return early if start > this.length. Done here to prevent potential uint32 + // coercion fail below. + if (start > this.length) { + return '' + } + + if (end === undefined || end > this.length) { + end = this.length; + } + + if (end <= 0) { + return '' + } + + // Force coersion to uint32. This will also coerce falsey/NaN values to 0. + end >>>= 0; + start >>>= 0; + + if (end <= start) { + return '' + } + + if (!encoding) encoding = 'utf8'; + + while (true) { + switch (encoding) { + case 'hex': + return hexSlice(this, start, end) + + case 'utf8': + case 'utf-8': + return utf8Slice(this, start, end) + + case 'ascii': + return asciiSlice(this, start, end) + + case 'latin1': + case 'binary': + return latin1Slice(this, start, end) + + case 'base64': + return base64Slice(this, start, end) + + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return utf16leSlice(this, start, end) + + default: + if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) + encoding = (encoding + '').toLowerCase(); + loweredCase = true; + } + } + } + + // The property is used by `Buffer.isBuffer` and `is-buffer` (in Safari 5-7) to detect + // Buffer instances. + Buffer.prototype._isBuffer = true; + + function swap (b, n, m) { + var i = b[n]; + b[n] = b[m]; + b[m] = i; + } + + Buffer.prototype.swap16 = function swap16 () { + var len = this.length; + if (len % 2 !== 0) { + throw new RangeError('Buffer size must be a multiple of 16-bits') + } + for (var i = 0; i < len; i += 2) { + swap(this, i, i + 1); + } + return this + }; + + Buffer.prototype.swap32 = function swap32 () { + var len = this.length; + if (len % 4 !== 0) { + throw new RangeError('Buffer size must be a multiple of 32-bits') + } + for (var i = 0; i < len; i += 4) { + swap(this, i, i + 3); + swap(this, i + 1, i + 2); + } + return this + }; + + Buffer.prototype.swap64 = function swap64 () { + var len = this.length; + if (len % 8 !== 0) { + throw new RangeError('Buffer size must be a multiple of 64-bits') + } + for (var i = 0; i < len; i += 8) { + swap(this, i, i + 7); + swap(this, i + 1, i + 6); + swap(this, i + 2, i + 5); + swap(this, i + 3, i + 4); + } + return this + }; + + Buffer.prototype.toString = function toString () { + var length = this.length | 0; + if (length === 0) return '' + if (arguments.length === 0) return utf8Slice(this, 0, length) + return slowToString.apply(this, arguments) + }; + + Buffer.prototype.equals = function equals (b) { + if (!internalIsBuffer(b)) throw new TypeError('Argument must be a Buffer') + if (this === b) return true + return Buffer.compare(this, b) === 0 + }; + + Buffer.prototype.inspect = function inspect () { + var str = ''; + var max = INSPECT_MAX_BYTES; + if (this.length > 0) { + str = this.toString('hex', 0, max).match(/.{2}/g).join(' '); + if (this.length > max) str += ' ... '; + } + return '' + }; + + Buffer.prototype.compare = function compare (target, start, end, thisStart, thisEnd) { + if (!internalIsBuffer(target)) { + throw new TypeError('Argument must be a Buffer') + } + + if (start === undefined) { + start = 0; + } + if (end === undefined) { + end = target ? target.length : 0; + } + if (thisStart === undefined) { + thisStart = 0; + } + if (thisEnd === undefined) { + thisEnd = this.length; + } + + if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) { + throw new RangeError('out of range index') + } + + if (thisStart >= thisEnd && start >= end) { + return 0 + } + if (thisStart >= thisEnd) { + return -1 + } + if (start >= end) { + return 1 + } + + start >>>= 0; + end >>>= 0; + thisStart >>>= 0; + thisEnd >>>= 0; + + if (this === target) return 0 + + var x = thisEnd - thisStart; + var y = end - start; + var len = Math.min(x, y); + + var thisCopy = this.slice(thisStart, thisEnd); + var targetCopy = target.slice(start, end); + + for (var i = 0; i < len; ++i) { + if (thisCopy[i] !== targetCopy[i]) { + x = thisCopy[i]; + y = targetCopy[i]; + break + } + } + + if (x < y) return -1 + if (y < x) return 1 + return 0 + }; + + // Finds either the first index of `val` in `buffer` at offset >= `byteOffset`, + // OR the last index of `val` in `buffer` at offset <= `byteOffset`. + // + // Arguments: + // - buffer - a Buffer to search + // - val - a string, Buffer, or number + // - byteOffset - an index into `buffer`; will be clamped to an int32 + // - encoding - an optional encoding, relevant is val is a string + // - dir - true for indexOf, false for lastIndexOf + function bidirectionalIndexOf (buffer, val, byteOffset, encoding, dir) { + // Empty buffer means no match + if (buffer.length === 0) return -1 + + // Normalize byteOffset + if (typeof byteOffset === 'string') { + encoding = byteOffset; + byteOffset = 0; + } else if (byteOffset > 0x7fffffff) { + byteOffset = 0x7fffffff; + } else if (byteOffset < -0x80000000) { + byteOffset = -0x80000000; + } + byteOffset = +byteOffset; // Coerce to Number. + if (isNaN(byteOffset)) { + // byteOffset: it it's undefined, null, NaN, "foo", etc, search whole buffer + byteOffset = dir ? 0 : (buffer.length - 1); + } + + // Normalize byteOffset: negative offsets start from the end of the buffer + if (byteOffset < 0) byteOffset = buffer.length + byteOffset; + if (byteOffset >= buffer.length) { + if (dir) return -1 + else byteOffset = buffer.length - 1; + } else if (byteOffset < 0) { + if (dir) byteOffset = 0; + else return -1 + } + + // Normalize val + if (typeof val === 'string') { + val = Buffer.from(val, encoding); + } + + // Finally, search either indexOf (if dir is true) or lastIndexOf + if (internalIsBuffer(val)) { + // Special case: looking for empty string/buffer always fails + if (val.length === 0) { + return -1 + } + return arrayIndexOf(buffer, val, byteOffset, encoding, dir) + } else if (typeof val === 'number') { + val = val & 0xFF; // Search for a byte value [0-255] + if (Buffer.TYPED_ARRAY_SUPPORT && + typeof Uint8Array.prototype.indexOf === 'function') { + if (dir) { + return Uint8Array.prototype.indexOf.call(buffer, val, byteOffset) + } else { + return Uint8Array.prototype.lastIndexOf.call(buffer, val, byteOffset) + } + } + return arrayIndexOf(buffer, [ val ], byteOffset, encoding, dir) + } + + throw new TypeError('val must be string, number or Buffer') + } + + function arrayIndexOf (arr, val, byteOffset, encoding, dir) { + var indexSize = 1; + var arrLength = arr.length; + var valLength = val.length; + + if (encoding !== undefined) { + encoding = String(encoding).toLowerCase(); + if (encoding === 'ucs2' || encoding === 'ucs-2' || + encoding === 'utf16le' || encoding === 'utf-16le') { + if (arr.length < 2 || val.length < 2) { + return -1 + } + indexSize = 2; + arrLength /= 2; + valLength /= 2; + byteOffset /= 2; + } + } + + function read (buf, i) { + if (indexSize === 1) { + return buf[i] + } else { + return buf.readUInt16BE(i * indexSize) + } + } + + var i; + if (dir) { + var foundIndex = -1; + for (i = byteOffset; i < arrLength; i++) { + if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) { + if (foundIndex === -1) foundIndex = i; + if (i - foundIndex + 1 === valLength) return foundIndex * indexSize + } else { + if (foundIndex !== -1) i -= i - foundIndex; + foundIndex = -1; + } + } + } else { + if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength; + for (i = byteOffset; i >= 0; i--) { + var found = true; + for (var j = 0; j < valLength; j++) { + if (read(arr, i + j) !== read(val, j)) { + found = false; + break + } + } + if (found) return i + } + } + + return -1 + } + + Buffer.prototype.includes = function includes (val, byteOffset, encoding) { + return this.indexOf(val, byteOffset, encoding) !== -1 + }; + + Buffer.prototype.indexOf = function indexOf (val, byteOffset, encoding) { + return bidirectionalIndexOf(this, val, byteOffset, encoding, true) + }; + + Buffer.prototype.lastIndexOf = function lastIndexOf (val, byteOffset, encoding) { + return bidirectionalIndexOf(this, val, byteOffset, encoding, false) + }; + + function hexWrite (buf, string, offset, length) { + offset = Number(offset) || 0; + var remaining = buf.length - offset; + if (!length) { + length = remaining; + } else { + length = Number(length); + if (length > remaining) { + length = remaining; + } + } + + // must be an even number of digits + var strLen = string.length; + if (strLen % 2 !== 0) throw new TypeError('Invalid hex string') + + if (length > strLen / 2) { + length = strLen / 2; + } + for (var i = 0; i < length; ++i) { + var parsed = parseInt(string.substr(i * 2, 2), 16); + if (isNaN(parsed)) return i + buf[offset + i] = parsed; + } + return i + } + + function utf8Write (buf, string, offset, length) { + return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length) + } + + function asciiWrite (buf, string, offset, length) { + return blitBuffer(asciiToBytes(string), buf, offset, length) + } + + function latin1Write (buf, string, offset, length) { + return asciiWrite(buf, string, offset, length) + } + + function base64Write (buf, string, offset, length) { + return blitBuffer(base64ToBytes(string), buf, offset, length) + } + + function ucs2Write (buf, string, offset, length) { + return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length) + } + + Buffer.prototype.write = function write (string, offset, length, encoding) { + // Buffer#write(string) + if (offset === undefined) { + encoding = 'utf8'; + length = this.length; + offset = 0; + // Buffer#write(string, encoding) + } else if (length === undefined && typeof offset === 'string') { + encoding = offset; + length = this.length; + offset = 0; + // Buffer#write(string, offset[, length][, encoding]) + } else if (isFinite(offset)) { + offset = offset | 0; + if (isFinite(length)) { + length = length | 0; + if (encoding === undefined) encoding = 'utf8'; + } else { + encoding = length; + length = undefined; + } + // legacy write(string, encoding, offset, length) - remove in v0.13 + } else { + throw new Error( + 'Buffer.write(string, encoding, offset[, length]) is no longer supported' + ) + } + + var remaining = this.length - offset; + if (length === undefined || length > remaining) length = remaining; + + if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) { + throw new RangeError('Attempt to write outside buffer bounds') + } + + if (!encoding) encoding = 'utf8'; + + var loweredCase = false; + for (;;) { + switch (encoding) { + case 'hex': + return hexWrite(this, string, offset, length) + + case 'utf8': + case 'utf-8': + return utf8Write(this, string, offset, length) + + case 'ascii': + return asciiWrite(this, string, offset, length) + + case 'latin1': + case 'binary': + return latin1Write(this, string, offset, length) + + case 'base64': + // Warning: maxLength not taken into account in base64Write + return base64Write(this, string, offset, length) + + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return ucs2Write(this, string, offset, length) + + default: + if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) + encoding = ('' + encoding).toLowerCase(); + loweredCase = true; + } + } + }; + + Buffer.prototype.toJSON = function toJSON () { + return { + type: 'Buffer', + data: Array.prototype.slice.call(this._arr || this, 0) + } + }; + + function base64Slice (buf, start, end) { + if (start === 0 && end === buf.length) { + return fromByteArray(buf) + } else { + return fromByteArray(buf.slice(start, end)) + } + } + + function utf8Slice (buf, start, end) { + end = Math.min(buf.length, end); + var res = []; + + var i = start; + while (i < end) { + var firstByte = buf[i]; + var codePoint = null; + var bytesPerSequence = (firstByte > 0xEF) ? 4 + : (firstByte > 0xDF) ? 3 + : (firstByte > 0xBF) ? 2 + : 1; + + if (i + bytesPerSequence <= end) { + var secondByte, thirdByte, fourthByte, tempCodePoint; + + switch (bytesPerSequence) { + case 1: + if (firstByte < 0x80) { + codePoint = firstByte; + } + break + case 2: + secondByte = buf[i + 1]; + if ((secondByte & 0xC0) === 0x80) { + tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F); + if (tempCodePoint > 0x7F) { + codePoint = tempCodePoint; + } + } + break + case 3: + secondByte = buf[i + 1]; + thirdByte = buf[i + 2]; + if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) { + tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F); + if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) { + codePoint = tempCodePoint; + } + } + break + case 4: + secondByte = buf[i + 1]; + thirdByte = buf[i + 2]; + fourthByte = buf[i + 3]; + if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) { + tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F); + if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) { + codePoint = tempCodePoint; + } + } + } + } + + if (codePoint === null) { + // we did not generate a valid codePoint so insert a + // replacement char (U+FFFD) and advance only 1 byte + codePoint = 0xFFFD; + bytesPerSequence = 1; + } else if (codePoint > 0xFFFF) { + // encode to utf16 (surrogate pair dance) + codePoint -= 0x10000; + res.push(codePoint >>> 10 & 0x3FF | 0xD800); + codePoint = 0xDC00 | codePoint & 0x3FF; + } + + res.push(codePoint); + i += bytesPerSequence; + } + + return decodeCodePointsArray(res) + } + + // Based on http://stackoverflow.com/a/22747272/680742, the browser with + // the lowest limit is Chrome, with 0x10000 args. + // We go 1 magnitude less, for safety + var MAX_ARGUMENTS_LENGTH = 0x1000; + + function decodeCodePointsArray (codePoints) { + var len = codePoints.length; + if (len <= MAX_ARGUMENTS_LENGTH) { + return String.fromCharCode.apply(String, codePoints) // avoid extra slice() + } + + // Decode in chunks to avoid "call stack size exceeded". + var res = ''; + var i = 0; + while (i < len) { + res += String.fromCharCode.apply( + String, + codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH) + ); + } + return res + } + + function asciiSlice (buf, start, end) { + var ret = ''; + end = Math.min(buf.length, end); + + for (var i = start; i < end; ++i) { + ret += String.fromCharCode(buf[i] & 0x7F); + } + return ret + } + + function latin1Slice (buf, start, end) { + var ret = ''; + end = Math.min(buf.length, end); + + for (var i = start; i < end; ++i) { + ret += String.fromCharCode(buf[i]); + } + return ret + } + + function hexSlice (buf, start, end) { + var len = buf.length; + + if (!start || start < 0) start = 0; + if (!end || end < 0 || end > len) end = len; + + var out = ''; + for (var i = start; i < end; ++i) { + out += toHex(buf[i]); + } + return out + } + + function utf16leSlice (buf, start, end) { + var bytes = buf.slice(start, end); + var res = ''; + for (var i = 0; i < bytes.length; i += 2) { + res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256); + } + return res + } + + Buffer.prototype.slice = function slice (start, end) { + var len = this.length; + start = ~~start; + end = end === undefined ? len : ~~end; + + if (start < 0) { + start += len; + if (start < 0) start = 0; + } else if (start > len) { + start = len; + } + + if (end < 0) { + end += len; + if (end < 0) end = 0; + } else if (end > len) { + end = len; + } + + if (end < start) end = start; + + var newBuf; + if (Buffer.TYPED_ARRAY_SUPPORT) { + newBuf = this.subarray(start, end); + newBuf.__proto__ = Buffer.prototype; + } else { + var sliceLen = end - start; + newBuf = new Buffer(sliceLen, undefined); + for (var i = 0; i < sliceLen; ++i) { + newBuf[i] = this[i + start]; + } + } + + return newBuf + }; + + /* + * Need to make sure that buffer isn't trying to write out of bounds. + */ + function checkOffset (offset, ext, length) { + if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint') + if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length') + } + + Buffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) { + offset = offset | 0; + byteLength = byteLength | 0; + if (!noAssert) checkOffset(offset, byteLength, this.length); + + var val = this[offset]; + var mul = 1; + var i = 0; + while (++i < byteLength && (mul *= 0x100)) { + val += this[offset + i] * mul; + } + + return val + }; + + Buffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) { + offset = offset | 0; + byteLength = byteLength | 0; + if (!noAssert) { + checkOffset(offset, byteLength, this.length); + } + + var val = this[offset + --byteLength]; + var mul = 1; + while (byteLength > 0 && (mul *= 0x100)) { + val += this[offset + --byteLength] * mul; + } + + return val + }; + + Buffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) { + if (!noAssert) checkOffset(offset, 1, this.length); + return this[offset] + }; + + Buffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) { + if (!noAssert) checkOffset(offset, 2, this.length); + return this[offset] | (this[offset + 1] << 8) + }; + + Buffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) { + if (!noAssert) checkOffset(offset, 2, this.length); + return (this[offset] << 8) | this[offset + 1] + }; + + Buffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) { + if (!noAssert) checkOffset(offset, 4, this.length); + + return ((this[offset]) | + (this[offset + 1] << 8) | + (this[offset + 2] << 16)) + + (this[offset + 3] * 0x1000000) + }; + + Buffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) { + if (!noAssert) checkOffset(offset, 4, this.length); + + return (this[offset] * 0x1000000) + + ((this[offset + 1] << 16) | + (this[offset + 2] << 8) | + this[offset + 3]) + }; + + Buffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) { + offset = offset | 0; + byteLength = byteLength | 0; + if (!noAssert) checkOffset(offset, byteLength, this.length); - if (!child.runtime) { - return + var val = this[offset]; + var mul = 1; + var i = 0; + while (++i < byteLength && (mul *= 0x100)) { + val += this[offset + i] * mul; } + mul *= 0x80; - parent.removeChild(key); + if (val >= mul) val -= Math.pow(2, 8 * byteLength); + + return val }; - ModuleCollection.prototype.isRegistered = function isRegistered (path) { - var parent = this.get(path.slice(0, -1)); - var key = path[path.length - 1]; + Buffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) { + offset = offset | 0; + byteLength = byteLength | 0; + if (!noAssert) checkOffset(offset, byteLength, this.length); - if (parent) { - return parent.hasChild(key) + var i = byteLength; + var mul = 1; + var val = this[offset + --i]; + while (i > 0 && (mul *= 0x100)) { + val += this[offset + --i] * mul; } + mul *= 0x80; - return false + if (val >= mul) val -= Math.pow(2, 8 * byteLength); + + return val }; - function update (path, targetModule, newModule) { - if ((process.env.NODE_ENV !== 'production')) { - assertRawModule(path, newModule); - } + Buffer.prototype.readInt8 = function readInt8 (offset, noAssert) { + if (!noAssert) checkOffset(offset, 1, this.length); + if (!(this[offset] & 0x80)) return (this[offset]) + return ((0xff - this[offset] + 1) * -1) + }; - // update target module - targetModule.update(newModule); + Buffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) { + if (!noAssert) checkOffset(offset, 2, this.length); + var val = this[offset] | (this[offset + 1] << 8); + return (val & 0x8000) ? val | 0xFFFF0000 : val + }; - // update nested modules - if (newModule.modules) { - for (var key in newModule.modules) { - if (!targetModule.getChild(key)) { - if ((process.env.NODE_ENV !== 'production')) { - console.warn( - "[vuex] trying to add a new module '" + key + "' on hot reloading, " + - 'manual reload is needed' - ); - } - return - } - update( - path.concat(key), - targetModule.getChild(key), - newModule.modules[key] - ); - } - } - } + Buffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) { + if (!noAssert) checkOffset(offset, 2, this.length); + var val = this[offset + 1] | (this[offset] << 8); + return (val & 0x8000) ? val | 0xFFFF0000 : val + }; - var functionAssert = { - assert: function (value) { return typeof value === 'function'; }, - expected: 'function' + Buffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) { + if (!noAssert) checkOffset(offset, 4, this.length); + + return (this[offset]) | + (this[offset + 1] << 8) | + (this[offset + 2] << 16) | + (this[offset + 3] << 24) }; - var objectAssert = { - assert: function (value) { return typeof value === 'function' || - (typeof value === 'object' && typeof value.handler === 'function'); }, - expected: 'function or object with "handler" function' + Buffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) { + if (!noAssert) checkOffset(offset, 4, this.length); + + return (this[offset] << 24) | + (this[offset + 1] << 16) | + (this[offset + 2] << 8) | + (this[offset + 3]) }; - var assertTypes = { - getters: functionAssert, - mutations: functionAssert, - actions: objectAssert + Buffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) { + if (!noAssert) checkOffset(offset, 4, this.length); + return read(this, offset, true, 23, 4) }; - function assertRawModule (path, rawModule) { - Object.keys(assertTypes).forEach(function (key) { - if (!rawModule[key]) { return } + Buffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) { + if (!noAssert) checkOffset(offset, 4, this.length); + return read(this, offset, false, 23, 4) + }; - var assertOptions = assertTypes[key]; + Buffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) { + if (!noAssert) checkOffset(offset, 8, this.length); + return read(this, offset, true, 52, 8) + }; - forEachValue(rawModule[key], function (value, type) { - assert( - assertOptions.assert(value), - makeAssertionMessage(path, key, type, value, assertOptions.expected) - ); - }); - }); + Buffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) { + if (!noAssert) checkOffset(offset, 8, this.length); + return read(this, offset, false, 52, 8) + }; + + function checkInt (buf, value, offset, ext, max, min) { + if (!internalIsBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance') + if (value > max || value < min) throw new RangeError('"value" argument is out of bounds') + if (offset + ext > buf.length) throw new RangeError('Index out of range') } - function makeAssertionMessage (path, key, type, value, expected) { - var buf = key + " should be " + expected + " but \"" + key + "." + type + "\""; - if (path.length > 0) { - buf += " in module \"" + (path.join('.')) + "\""; + Buffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) { + value = +value; + offset = offset | 0; + byteLength = byteLength | 0; + if (!noAssert) { + var maxBytes = Math.pow(2, 8 * byteLength) - 1; + checkInt(this, value, offset, byteLength, maxBytes, 0); } - buf += " is " + (JSON.stringify(value)) + "."; - return buf - } - var Vue; // bind on install + var mul = 1; + var i = 0; + this[offset] = value & 0xFF; + while (++i < byteLength && (mul *= 0x100)) { + this[offset + i] = (value / mul) & 0xFF; + } - var Store = function Store (options) { - var this$1$1 = this; - if ( options === void 0 ) options = {}; + return offset + byteLength + }; - // Auto install if it is not done yet and `window` has `Vue`. - // To allow users to avoid auto-installation in some cases, - // this code should be placed here. See #731 - if (!Vue && typeof window !== 'undefined' && window.Vue) { - install(window.Vue); + Buffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) { + value = +value; + offset = offset | 0; + byteLength = byteLength | 0; + if (!noAssert) { + var maxBytes = Math.pow(2, 8 * byteLength) - 1; + checkInt(this, value, offset, byteLength, maxBytes, 0); } - if ((process.env.NODE_ENV !== 'production')) { - assert(Vue, "must call Vue.use(Vuex) before creating a store instance."); - assert(typeof Promise !== 'undefined', "vuex requires a Promise polyfill in this browser."); - assert(this instanceof Store, "store must be called with the new operator."); + var i = byteLength - 1; + var mul = 1; + this[offset + i] = value & 0xFF; + while (--i >= 0 && (mul *= 0x100)) { + this[offset + i] = (value / mul) & 0xFF; } - var plugins = options.plugins; if ( plugins === void 0 ) plugins = []; - var strict = options.strict; if ( strict === void 0 ) strict = false; - - // store internal state - this._committing = false; - this._actions = Object.create(null); - this._actionSubscribers = []; - this._mutations = Object.create(null); - this._wrappedGetters = Object.create(null); - this._modules = new ModuleCollection(options); - this._modulesNamespaceMap = Object.create(null); - this._subscribers = []; - this._watcherVM = new Vue(); - this._makeLocalGettersCache = Object.create(null); + return offset + byteLength + }; - // bind commit and dispatch to self - var store = this; - var ref = this; - var dispatch = ref.dispatch; - var commit = ref.commit; - this.dispatch = function boundDispatch (type, payload) { - return dispatch.call(store, type, payload) - }; - this.commit = function boundCommit (type, payload, options) { - return commit.call(store, type, payload, options) - }; + Buffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) { + value = +value; + offset = offset | 0; + if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0); + if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value); + this[offset] = (value & 0xff); + return offset + 1 + }; - // strict mode - this.strict = strict; + function objectWriteUInt16 (buf, value, offset, littleEndian) { + if (value < 0) value = 0xffff + value + 1; + for (var i = 0, j = Math.min(buf.length - offset, 2); i < j; ++i) { + buf[offset + i] = (value & (0xff << (8 * (littleEndian ? i : 1 - i)))) >>> + (littleEndian ? i : 1 - i) * 8; + } + } - var state = this._modules.root.state; + Buffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) { + value = +value; + offset = offset | 0; + if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0); + if (Buffer.TYPED_ARRAY_SUPPORT) { + this[offset] = (value & 0xff); + this[offset + 1] = (value >>> 8); + } else { + objectWriteUInt16(this, value, offset, true); + } + return offset + 2 + }; - // init root module. - // this also recursively registers all sub-modules - // and collects all module getters inside this._wrappedGetters - installModule(this, state, [], this._modules.root); + Buffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) { + value = +value; + offset = offset | 0; + if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0); + if (Buffer.TYPED_ARRAY_SUPPORT) { + this[offset] = (value >>> 8); + this[offset + 1] = (value & 0xff); + } else { + objectWriteUInt16(this, value, offset, false); + } + return offset + 2 + }; - // initialize the store vm, which is responsible for the reactivity - // (also registers _wrappedGetters as computed properties) - resetStoreVM(this, state); + function objectWriteUInt32 (buf, value, offset, littleEndian) { + if (value < 0) value = 0xffffffff + value + 1; + for (var i = 0, j = Math.min(buf.length - offset, 4); i < j; ++i) { + buf[offset + i] = (value >>> (littleEndian ? i : 3 - i) * 8) & 0xff; + } + } - // apply plugins - plugins.forEach(function (plugin) { return plugin(this$1$1); }); + Buffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) { + value = +value; + offset = offset | 0; + if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0); + if (Buffer.TYPED_ARRAY_SUPPORT) { + this[offset + 3] = (value >>> 24); + this[offset + 2] = (value >>> 16); + this[offset + 1] = (value >>> 8); + this[offset] = (value & 0xff); + } else { + objectWriteUInt32(this, value, offset, true); + } + return offset + 4 + }; - var useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools; - if (useDevtools) { - devtoolPlugin(this); + Buffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) { + value = +value; + offset = offset | 0; + if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0); + if (Buffer.TYPED_ARRAY_SUPPORT) { + this[offset] = (value >>> 24); + this[offset + 1] = (value >>> 16); + this[offset + 2] = (value >>> 8); + this[offset + 3] = (value & 0xff); + } else { + objectWriteUInt32(this, value, offset, false); } + return offset + 4 }; - var prototypeAccessors$1 = { state: { configurable: true } }; + Buffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) { + value = +value; + offset = offset | 0; + if (!noAssert) { + var limit = Math.pow(2, 8 * byteLength - 1); - prototypeAccessors$1.state.get = function () { - return this._vm._data.$$state - }; + checkInt(this, value, offset, byteLength, limit - 1, -limit); + } - prototypeAccessors$1.state.set = function (v) { - if ((process.env.NODE_ENV !== 'production')) { - assert(false, "use store.replaceState() to explicit replace store state."); + var i = 0; + var mul = 1; + var sub = 0; + this[offset] = value & 0xFF; + while (++i < byteLength && (mul *= 0x100)) { + if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) { + sub = 1; + } + this[offset + i] = ((value / mul) >> 0) - sub & 0xFF; } + + return offset + byteLength }; - Store.prototype.commit = function commit (_type, _payload, _options) { - var this$1$1 = this; + Buffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) { + value = +value; + offset = offset | 0; + if (!noAssert) { + var limit = Math.pow(2, 8 * byteLength - 1); - // check object-style commit - var ref = unifyObjectStyle(_type, _payload, _options); - var type = ref.type; - var payload = ref.payload; - var options = ref.options; + checkInt(this, value, offset, byteLength, limit - 1, -limit); + } - var mutation = { type: type, payload: payload }; - var entry = this._mutations[type]; - if (!entry) { - if ((process.env.NODE_ENV !== 'production')) { - console.error(("[vuex] unknown mutation type: " + type)); + var i = byteLength - 1; + var mul = 1; + var sub = 0; + this[offset + i] = value & 0xFF; + while (--i >= 0 && (mul *= 0x100)) { + if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) { + sub = 1; } - return + this[offset + i] = ((value / mul) >> 0) - sub & 0xFF; } - this._withCommit(function () { - entry.forEach(function commitIterator (handler) { - handler(payload); - }); - }); - this._subscribers - .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe - .forEach(function (sub) { return sub(mutation, this$1$1.state); }); + return offset + byteLength + }; - if ( - (process.env.NODE_ENV !== 'production') && - options && options.silent - ) { - console.warn( - "[vuex] mutation type: " + type + ". Silent option has been removed. " + - 'Use the filter functionality in the vue-devtools' - ); + Buffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) { + value = +value; + offset = offset | 0; + if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80); + if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value); + if (value < 0) value = 0xff + value + 1; + this[offset] = (value & 0xff); + return offset + 1 + }; + + Buffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) { + value = +value; + offset = offset | 0; + if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000); + if (Buffer.TYPED_ARRAY_SUPPORT) { + this[offset] = (value & 0xff); + this[offset + 1] = (value >>> 8); + } else { + objectWriteUInt16(this, value, offset, true); } + return offset + 2 }; - Store.prototype.dispatch = function dispatch (_type, _payload) { - var this$1$1 = this; + Buffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) { + value = +value; + offset = offset | 0; + if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000); + if (Buffer.TYPED_ARRAY_SUPPORT) { + this[offset] = (value >>> 8); + this[offset + 1] = (value & 0xff); + } else { + objectWriteUInt16(this, value, offset, false); + } + return offset + 2 + }; - // check object-style dispatch - var ref = unifyObjectStyle(_type, _payload); - var type = ref.type; - var payload = ref.payload; + Buffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) { + value = +value; + offset = offset | 0; + if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000); + if (Buffer.TYPED_ARRAY_SUPPORT) { + this[offset] = (value & 0xff); + this[offset + 1] = (value >>> 8); + this[offset + 2] = (value >>> 16); + this[offset + 3] = (value >>> 24); + } else { + objectWriteUInt32(this, value, offset, true); + } + return offset + 4 + }; - var action = { type: type, payload: payload }; - var entry = this._actions[type]; - if (!entry) { - if ((process.env.NODE_ENV !== 'production')) { - console.error(("[vuex] unknown action type: " + type)); - } - return + Buffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) { + value = +value; + offset = offset | 0; + if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000); + if (value < 0) value = 0xffffffff + value + 1; + if (Buffer.TYPED_ARRAY_SUPPORT) { + this[offset] = (value >>> 24); + this[offset + 1] = (value >>> 16); + this[offset + 2] = (value >>> 8); + this[offset + 3] = (value & 0xff); + } else { + objectWriteUInt32(this, value, offset, false); } + return offset + 4 + }; - try { - this._actionSubscribers - .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe - .filter(function (sub) { return sub.before; }) - .forEach(function (sub) { return sub.before(action, this$1$1.state); }); - } catch (e) { - if ((process.env.NODE_ENV !== 'production')) { - console.warn("[vuex] error in before action subscribers: "); - console.error(e); - } + function checkIEEE754 (buf, value, offset, ext, max, min) { + if (offset + ext > buf.length) throw new RangeError('Index out of range') + if (offset < 0) throw new RangeError('Index out of range') + } + + function writeFloat (buf, value, offset, littleEndian, noAssert) { + if (!noAssert) { + checkIEEE754(buf, value, offset, 4); } + write(buf, value, offset, littleEndian, 23, 4); + return offset + 4 + } - var result = entry.length > 1 - ? Promise.all(entry.map(function (handler) { return handler(payload); })) - : entry[0](payload); + Buffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) { + return writeFloat(this, value, offset, true, noAssert) + }; - return new Promise(function (resolve, reject) { - result.then(function (res) { - try { - this$1$1._actionSubscribers - .filter(function (sub) { return sub.after; }) - .forEach(function (sub) { return sub.after(action, this$1$1.state); }); - } catch (e) { - if ((process.env.NODE_ENV !== 'production')) { - console.warn("[vuex] error in after action subscribers: "); - console.error(e); - } - } - resolve(res); - }, function (error) { - try { - this$1$1._actionSubscribers - .filter(function (sub) { return sub.error; }) - .forEach(function (sub) { return sub.error(action, this$1$1.state, error); }); - } catch (e) { - if ((process.env.NODE_ENV !== 'production')) { - console.warn("[vuex] error in error action subscribers: "); - console.error(e); - } - } - reject(error); - }); - }) + Buffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) { + return writeFloat(this, value, offset, false, noAssert) }; - Store.prototype.subscribe = function subscribe (fn, options) { - return genericSubscribe(fn, this._subscribers, options) + function writeDouble (buf, value, offset, littleEndian, noAssert) { + if (!noAssert) { + checkIEEE754(buf, value, offset, 8); + } + write(buf, value, offset, littleEndian, 52, 8); + return offset + 8 + } + + Buffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) { + return writeDouble(this, value, offset, true, noAssert) }; - Store.prototype.subscribeAction = function subscribeAction (fn, options) { - var subs = typeof fn === 'function' ? { before: fn } : fn; - return genericSubscribe(subs, this._actionSubscribers, options) + Buffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) { + return writeDouble(this, value, offset, false, noAssert) }; - Store.prototype.watch = function watch (getter, cb, options) { - var this$1$1 = this; + // copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length) + Buffer.prototype.copy = function copy (target, targetStart, start, end) { + if (!start) start = 0; + if (!end && end !== 0) end = this.length; + if (targetStart >= target.length) targetStart = target.length; + if (!targetStart) targetStart = 0; + if (end > 0 && end < start) end = start; + + // Copy 0 bytes; we're done + if (end === start) return 0 + if (target.length === 0 || this.length === 0) return 0 + + // Fatal error conditions + if (targetStart < 0) { + throw new RangeError('targetStart out of bounds') + } + if (start < 0 || start >= this.length) throw new RangeError('sourceStart out of bounds') + if (end < 0) throw new RangeError('sourceEnd out of bounds') + + // Are we oob? + if (end > this.length) end = this.length; + if (target.length - targetStart < end - start) { + end = target.length - targetStart + start; + } + + var len = end - start; + var i; + + if (this === target && start < targetStart && targetStart < end) { + // descending copy from end + for (i = len - 1; i >= 0; --i) { + target[i + targetStart] = this[i + start]; + } + } else if (len < 1000 || !Buffer.TYPED_ARRAY_SUPPORT) { + // ascending copy from start + for (i = 0; i < len; ++i) { + target[i + targetStart] = this[i + start]; + } + } else { + Uint8Array.prototype.set.call( + target, + this.subarray(start, start + len), + targetStart + ); + } + + return len + }; - if ((process.env.NODE_ENV !== 'production')) { - assert(typeof getter === 'function', "store.watch only accepts a function."); + // Usage: + // buffer.fill(number[, offset[, end]]) + // buffer.fill(buffer[, offset[, end]]) + // buffer.fill(string[, offset[, end]][, encoding]) + Buffer.prototype.fill = function fill (val, start, end, encoding) { + // Handle string cases: + if (typeof val === 'string') { + if (typeof start === 'string') { + encoding = start; + start = 0; + end = this.length; + } else if (typeof end === 'string') { + encoding = end; + end = this.length; + } + if (val.length === 1) { + var code = val.charCodeAt(0); + if (code < 256) { + val = code; + } + } + if (encoding !== undefined && typeof encoding !== 'string') { + throw new TypeError('encoding must be a string') + } + if (typeof encoding === 'string' && !Buffer.isEncoding(encoding)) { + throw new TypeError('Unknown encoding: ' + encoding) + } + } else if (typeof val === 'number') { + val = val & 255; } - return this._watcherVM.$watch(function () { return getter(this$1$1.state, this$1$1.getters); }, cb, options) - }; - Store.prototype.replaceState = function replaceState (state) { - var this$1$1 = this; + // Invalid ranges are not set to a default, so can range check early. + if (start < 0 || this.length < start || this.length < end) { + throw new RangeError('Out of range index') + } - this._withCommit(function () { - this$1$1._vm._data.$$state = state; - }); - }; + if (end <= start) { + return this + } - Store.prototype.registerModule = function registerModule (path, rawModule, options) { - if ( options === void 0 ) options = {}; + start = start >>> 0; + end = end === undefined ? this.length : end >>> 0; - if (typeof path === 'string') { path = [path]; } + if (!val) val = 0; - if ((process.env.NODE_ENV !== 'production')) { - assert(Array.isArray(path), "module path must be a string or an Array."); - assert(path.length > 0, 'cannot register the root module by using registerModule.'); + var i; + if (typeof val === 'number') { + for (i = start; i < end; ++i) { + this[i] = val; + } + } else { + var bytes = internalIsBuffer(val) + ? val + : utf8ToBytes(new Buffer(val, encoding).toString()); + var len = bytes.length; + for (i = 0; i < end - start; ++i) { + this[i + start] = bytes[i % len]; + } } - this._modules.register(path, rawModule); - installModule(this, this.state, path, this._modules.get(path), options.preserveState); - // reset store to update getters... - resetStoreVM(this, this.state); + return this }; - Store.prototype.unregisterModule = function unregisterModule (path) { - var this$1$1 = this; + // HELPER FUNCTIONS + // ================ - if (typeof path === 'string') { path = [path]; } + var INVALID_BASE64_RE = /[^+\/0-9A-Za-z-_]/g; - if ((process.env.NODE_ENV !== 'production')) { - assert(Array.isArray(path), "module path must be a string or an Array."); + function base64clean (str) { + // Node strips out invalid characters like \n and \t from the string, base64-js does not + str = stringtrim(str).replace(INVALID_BASE64_RE, ''); + // Node converts strings with length < 2 to '' + if (str.length < 2) return '' + // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not + while (str.length % 4 !== 0) { + str = str + '='; } + return str + } - this._modules.unregister(path); - this._withCommit(function () { - var parentState = getNestedState(this$1$1.state, path.slice(0, -1)); - Vue.delete(parentState, path[path.length - 1]); - }); - resetStore(this); - }; + function stringtrim (str) { + if (str.trim) return str.trim() + return str.replace(/^\s+|\s+$/g, '') + } - Store.prototype.hasModule = function hasModule (path) { - if (typeof path === 'string') { path = [path]; } + function toHex (n) { + if (n < 16) return '0' + n.toString(16) + return n.toString(16) + } - if ((process.env.NODE_ENV !== 'production')) { - assert(Array.isArray(path), "module path must be a string or an Array."); - } + function utf8ToBytes (string, units) { + units = units || Infinity; + var codePoint; + var length = string.length; + var leadSurrogate = null; + var bytes = []; + + for (var i = 0; i < length; ++i) { + codePoint = string.charCodeAt(i); + + // is surrogate component + if (codePoint > 0xD7FF && codePoint < 0xE000) { + // last char was a lead + if (!leadSurrogate) { + // no lead yet + if (codePoint > 0xDBFF) { + // unexpected trail + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD); + continue + } else if (i + 1 === length) { + // unpaired lead + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD); + continue + } - return this._modules.isRegistered(path) - }; + // valid lead + leadSurrogate = codePoint; - Store.prototype.hotUpdate = function hotUpdate (newOptions) { - this._modules.update(newOptions); - resetStore(this, true); - }; + continue + } - Store.prototype._withCommit = function _withCommit (fn) { - var committing = this._committing; - this._committing = true; - fn(); - this._committing = committing; - }; + // 2 leads in a row + if (codePoint < 0xDC00) { + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD); + leadSurrogate = codePoint; + continue + } - Object.defineProperties( Store.prototype, prototypeAccessors$1 ); + // valid surrogate pair + codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000; + } else if (leadSurrogate) { + // valid bmp char, but last char was a lead + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD); + } - function genericSubscribe (fn, subs, options) { - if (subs.indexOf(fn) < 0) { - options && options.prepend - ? subs.unshift(fn) - : subs.push(fn); - } - return function () { - var i = subs.indexOf(fn); - if (i > -1) { - subs.splice(i, 1); + leadSurrogate = null; + + // encode utf8 + if (codePoint < 0x80) { + if ((units -= 1) < 0) break + bytes.push(codePoint); + } else if (codePoint < 0x800) { + if ((units -= 2) < 0) break + bytes.push( + codePoint >> 0x6 | 0xC0, + codePoint & 0x3F | 0x80 + ); + } else if (codePoint < 0x10000) { + if ((units -= 3) < 0) break + bytes.push( + codePoint >> 0xC | 0xE0, + codePoint >> 0x6 & 0x3F | 0x80, + codePoint & 0x3F | 0x80 + ); + } else if (codePoint < 0x110000) { + if ((units -= 4) < 0) break + bytes.push( + codePoint >> 0x12 | 0xF0, + codePoint >> 0xC & 0x3F | 0x80, + codePoint >> 0x6 & 0x3F | 0x80, + codePoint & 0x3F | 0x80 + ); + } else { + throw new Error('Invalid code point') } } + + return bytes } - function resetStore (store, hot) { - store._actions = Object.create(null); - store._mutations = Object.create(null); - store._wrappedGetters = Object.create(null); - store._modulesNamespaceMap = Object.create(null); - var state = store.state; - // init all modules - installModule(store, state, [], store._modules.root, true); - // reset vm - resetStoreVM(store, state, hot); + function asciiToBytes (str) { + var byteArray = []; + for (var i = 0; i < str.length; ++i) { + // Node's code seems to be doing this and not & 0x7F.. + byteArray.push(str.charCodeAt(i) & 0xFF); + } + return byteArray } - function resetStoreVM (store, state, hot) { - var oldVm = store._vm; + function utf16leToBytes (str, units) { + var c, hi, lo; + var byteArray = []; + for (var i = 0; i < str.length; ++i) { + if ((units -= 2) < 0) break + + c = str.charCodeAt(i); + hi = c >> 8; + lo = c % 256; + byteArray.push(lo); + byteArray.push(hi); + } - // bind store public getters - store.getters = {}; - // reset local getters cache - store._makeLocalGettersCache = Object.create(null); - var wrappedGetters = store._wrappedGetters; - var computed = {}; - forEachValue(wrappedGetters, function (fn, key) { - // use computed to leverage its lazy-caching mechanism - // direct inline function use will lead to closure preserving oldVm. - // using partial to return function with only arguments preserved in closure environment. - computed[key] = partial(fn, store); - Object.defineProperty(store.getters, key, { - get: function () { return store._vm[key]; }, - enumerable: true // for local getters - }); - }); + return byteArray + } - // use a Vue instance to store the state tree - // suppress warnings just in case the user has added - // some funky global mixins - var silent = Vue.config.silent; - Vue.config.silent = true; - store._vm = new Vue({ - data: { - $$state: state - }, - computed: computed - }); - Vue.config.silent = silent; - // enable strict mode for new vm - if (store.strict) { - enableStrictMode(store); - } + function base64ToBytes (str) { + return toByteArray(base64clean(str)) + } - if (oldVm) { - if (hot) { - // dispatch changes in all subscribed watchers - // to force getter re-evaluation for hot reloading. - store._withCommit(function () { - oldVm._data.$$state = null; - }); - } - Vue.nextTick(function () { return oldVm.$destroy(); }); + function blitBuffer (src, dst, offset, length) { + for (var i = 0; i < length; ++i) { + if ((i + offset >= dst.length) || (i >= src.length)) break + dst[i + offset] = src[i]; } + return i } - function installModule (store, rootState, path, module, hot) { - var isRoot = !path.length; - var namespace = store._modules.getNamespace(path); + function isnan (val) { + return val !== val // eslint-disable-line no-self-compare + } - // register in namespace map - if (module.namespaced) { - if (store._modulesNamespaceMap[namespace] && (process.env.NODE_ENV !== 'production')) { - console.error(("[vuex] duplicate namespace " + namespace + " for the namespaced module " + (path.join('/')))); - } - store._modulesNamespaceMap[namespace] = module; + + // the following is from is-buffer, also by Feross Aboukhadijeh and with same lisence + // The _isBuffer check is for Safari 5-7 support, because it's missing + // Object.prototype.constructor. Remove this eventually + function isBuffer(obj) { + return obj != null && (!!obj._isBuffer || isFastBuffer(obj) || isSlowBuffer(obj)) + } + + function isFastBuffer (obj) { + return !!obj.constructor && typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj) + } + + // For Node v0.10 support. Remove this eventually. + function isSlowBuffer (obj) { + return typeof obj.readFloatLE === 'function' && typeof obj.slice === 'function' && isFastBuffer(obj.slice(0, 0)) + } + + /** + * Create an Error with the specified message, config, error code, request and response. + * + * @param {string} message The error message. + * @param {string} [code] The error code (for example, 'ECONNABORTED'). + * @param {Object} [config] The config. + * @param {Object} [request] The request. + * @param {Object} [response] The response. + * + * @returns {Error} The created error. + */ + function AxiosError(message, code, config, request, response) { + Error.call(this); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } else { + this.stack = (new Error()).stack; } - // set state - if (!isRoot && !hot) { - var parentState = getNestedState(rootState, path.slice(0, -1)); - var moduleName = path[path.length - 1]; - store._withCommit(function () { - if ((process.env.NODE_ENV !== 'production')) { - if (moduleName in parentState) { - console.warn( - ("[vuex] state field \"" + moduleName + "\" was overridden by a module with the same name at \"" + (path.join('.')) + "\"") - ); - } - } - Vue.set(parentState, moduleName, module.state); - }); + this.message = message; + this.name = 'AxiosError'; + code && (this.code = code); + config && (this.config = config); + request && (this.request = request); + response && (this.response = response); + } + + utils$1.inherits(AxiosError, Error, { + toJSON: function toJSON() { + return { + // Standard + message: this.message, + name: this.name, + // Microsoft + description: this.description, + number: this.number, + // Mozilla + fileName: this.fileName, + lineNumber: this.lineNumber, + columnNumber: this.columnNumber, + stack: this.stack, + // Axios + config: utils$1.toJSONObject(this.config), + code: this.code, + status: this.response && this.response.status ? this.response.status : null + }; } + }); - var local = module.context = makeLocalContext(store, namespace, path); + const prototype$1 = AxiosError.prototype; + const descriptors = {}; + + [ + 'ERR_BAD_OPTION_VALUE', + 'ERR_BAD_OPTION', + 'ECONNABORTED', + 'ETIMEDOUT', + 'ERR_NETWORK', + 'ERR_FR_TOO_MANY_REDIRECTS', + 'ERR_DEPRECATED', + 'ERR_BAD_RESPONSE', + 'ERR_BAD_REQUEST', + 'ERR_CANCELED', + 'ERR_NOT_SUPPORT', + 'ERR_INVALID_URL' + // eslint-disable-next-line func-names + ].forEach(code => { + descriptors[code] = {value: code}; + }); - module.forEachMutation(function (mutation, key) { - var namespacedType = namespace + key; - registerMutation(store, namespacedType, mutation, local); - }); + Object.defineProperties(AxiosError, descriptors); + Object.defineProperty(prototype$1, 'isAxiosError', {value: true}); - module.forEachAction(function (action, key) { - var type = action.root ? key : namespace + key; - var handler = action.handler || action; - registerAction(store, type, handler, local); - }); + // eslint-disable-next-line func-names + AxiosError.from = (error, code, config, request, response, customProps) => { + const axiosError = Object.create(prototype$1); - module.forEachGetter(function (getter, key) { - var namespacedType = namespace + key; - registerGetter(store, namespacedType, getter, local); + utils$1.toFlatObject(error, axiosError, function filter(obj) { + return obj !== Error.prototype; + }, prop => { + return prop !== 'isAxiosError'; }); - module.forEachChild(function (child, key) { - installModule(store, rootState, path.concat(key), child, hot); - }); + AxiosError.call(axiosError, error.message, code, config, request, response); + + axiosError.cause = error; + + axiosError.name = error.name; + + customProps && Object.assign(axiosError, customProps); + + return axiosError; + }; + + // eslint-disable-next-line strict + var httpAdapter = null; + + /** + * Determines if the given thing is a array or js object. + * + * @param {string} thing - The object or array to be visited. + * + * @returns {boolean} + */ + function isVisitable(thing) { + return utils$1.isPlainObject(thing) || utils$1.isArray(thing); } /** - * make localized dispatch, commit, getters and state - * if there is no namespace, just use root ones + * It removes the brackets from the end of a string + * + * @param {string} key - The key of the parameter. + * + * @returns {string} the key without the brackets. */ - function makeLocalContext (store, namespace, path) { - var noNamespace = namespace === ''; + function removeBrackets(key) { + return utils$1.endsWith(key, '[]') ? key.slice(0, -2) : key; + } - var local = { - dispatch: noNamespace ? store.dispatch : function (_type, _payload, _options) { - var args = unifyObjectStyle(_type, _payload, _options); - var payload = args.payload; - var options = args.options; - var type = args.type; + /** + * It takes a path, a key, and a boolean, and returns a string + * + * @param {string} path - The path to the current key. + * @param {string} key - The key of the current object being iterated over. + * @param {string} dots - If true, the key will be rendered with dots instead of brackets. + * + * @returns {string} The path to the current key. + */ + function renderKey(path, key, dots) { + if (!path) return key; + return path.concat(key).map(function each(token, i) { + // eslint-disable-next-line no-param-reassign + token = removeBrackets(token); + return !dots && i ? '[' + token + ']' : token; + }).join(dots ? '.' : ''); + } - if (!options || !options.root) { - type = namespace + type; - if ((process.env.NODE_ENV !== 'production') && !store._actions[type]) { - console.error(("[vuex] unknown local action type: " + (args.type) + ", global type: " + type)); - return - } - } + /** + * If the array is an array and none of its elements are visitable, then it's a flat array. + * + * @param {Array} arr - The array to check + * + * @returns {boolean} + */ + function isFlatArray(arr) { + return utils$1.isArray(arr) && !arr.some(isVisitable); + } - return store.dispatch(type, payload) - }, + const predicates = utils$1.toFlatObject(utils$1, {}, null, function filter(prop) { + return /^is[A-Z]/.test(prop); + }); - commit: noNamespace ? store.commit : function (_type, _payload, _options) { - var args = unifyObjectStyle(_type, _payload, _options); - var payload = args.payload; - var options = args.options; - var type = args.type; + /** + * Convert a data object to FormData + * + * @param {Object} obj + * @param {?Object} [formData] + * @param {?Object} [options] + * @param {Function} [options.visitor] + * @param {Boolean} [options.metaTokens = true] + * @param {Boolean} [options.dots = false] + * @param {?Boolean} [options.indexes = false] + * + * @returns {Object} + **/ - if (!options || !options.root) { - type = namespace + type; - if ((process.env.NODE_ENV !== 'production') && !store._mutations[type]) { - console.error(("[vuex] unknown local mutation type: " + (args.type) + ", global type: " + type)); - return - } - } + /** + * It converts an object into a FormData object + * + * @param {Object} obj - The object to convert to form data. + * @param {string} formData - The FormData object to append to. + * @param {Object} options + * + * @returns + */ + function toFormData(obj, formData, options) { + if (!utils$1.isObject(obj)) { + throw new TypeError('target must be an object'); + } + + // eslint-disable-next-line no-param-reassign + formData = formData || new (FormData)(); + + // eslint-disable-next-line no-param-reassign + options = utils$1.toFlatObject(options, { + metaTokens: true, + dots: false, + indexes: false + }, false, function defined(option, source) { + // eslint-disable-next-line no-eq-null,eqeqeq + return !utils$1.isUndefined(source[option]); + }); + + const metaTokens = options.metaTokens; + // eslint-disable-next-line no-use-before-define + const visitor = options.visitor || defaultVisitor; + const dots = options.dots; + const indexes = options.indexes; + const _Blob = options.Blob || typeof Blob !== 'undefined' && Blob; + const useBlob = _Blob && utils$1.isSpecCompliantForm(formData); + + if (!utils$1.isFunction(visitor)) { + throw new TypeError('visitor must be a function'); + } - store.commit(type, payload, options); + function convertValue(value) { + if (value === null) return ''; + + if (utils$1.isDate(value)) { + return value.toISOString(); } - }; - // getters and state object must be gotten lazily - // because they will be changed by vm update - Object.defineProperties(local, { - getters: { - get: noNamespace - ? function () { return store.getters; } - : function () { return makeLocalGetters(store, namespace); } - }, - state: { - get: function () { return getNestedState(store.state, path); } + if (!useBlob && utils$1.isBlob(value)) { + throw new AxiosError('Blob is not supported. Use a Buffer instead.'); } - }); - return local - } + if (utils$1.isArrayBuffer(value) || utils$1.isTypedArray(value)) { + return useBlob && typeof Blob === 'function' ? new Blob([value]) : Buffer.from(value); + } - function makeLocalGetters (store, namespace) { - if (!store._makeLocalGettersCache[namespace]) { - var gettersProxy = {}; - var splitPos = namespace.length; - Object.keys(store.getters).forEach(function (type) { - // skip if the target getter is not match this namespace - if (type.slice(0, splitPos) !== namespace) { return } + return value; + } - // extract local getter type - var localType = type.slice(splitPos); + /** + * Default visitor. + * + * @param {*} value + * @param {String|Number} key + * @param {Array} path + * @this {FormData} + * + * @returns {boolean} return true to visit the each prop of the value recursively + */ + function defaultVisitor(value, key, path) { + let arr = value; + + if (value && !path && typeof value === 'object') { + if (utils$1.endsWith(key, '{}')) { + // eslint-disable-next-line no-param-reassign + key = metaTokens ? key : key.slice(0, -2); + // eslint-disable-next-line no-param-reassign + value = JSON.stringify(value); + } else if ( + (utils$1.isArray(value) && isFlatArray(value)) || + ((utils$1.isFileList(value) || utils$1.endsWith(key, '[]')) && (arr = utils$1.toArray(value)) + )) { + // eslint-disable-next-line no-param-reassign + key = removeBrackets(key); + + arr.forEach(function each(el, index) { + !(utils$1.isUndefined(el) || el === null) && formData.append( + // eslint-disable-next-line no-nested-ternary + indexes === true ? renderKey([key], index, dots) : (indexes === null ? key : key + '[]'), + convertValue(el) + ); + }); + return false; + } + } - // Add a port to the getters proxy. - // Define as getter property because - // we do not want to evaluate the getters in this time. - Object.defineProperty(gettersProxy, localType, { - get: function () { return store.getters[type]; }, - enumerable: true - }); - }); - store._makeLocalGettersCache[namespace] = gettersProxy; + if (isVisitable(value)) { + return true; + } + + formData.append(renderKey(path, key, dots), convertValue(value)); + + return false; } - return store._makeLocalGettersCache[namespace] - } + const stack = []; - function registerMutation (store, type, handler, local) { - var entry = store._mutations[type] || (store._mutations[type] = []); - entry.push(function wrappedMutationHandler (payload) { - handler.call(store, local.state, payload); + const exposedHelpers = Object.assign(predicates, { + defaultVisitor, + convertValue, + isVisitable }); - } - function registerAction (store, type, handler, local) { - var entry = store._actions[type] || (store._actions[type] = []); - entry.push(function wrappedActionHandler (payload) { - var res = handler.call(store, { - dispatch: local.dispatch, - commit: local.commit, - getters: local.getters, - state: local.state, - rootGetters: store.getters, - rootState: store.state - }, payload); - if (!isPromise(res)) { - res = Promise.resolve(res); - } - if (store._devtoolHook) { - return res.catch(function (err) { - store._devtoolHook.emit('vuex:error', err); - throw err - }) - } else { - return res - } - }); - } + function build(value, path) { + if (utils$1.isUndefined(value)) return; - function registerGetter (store, type, rawGetter, local) { - if (store._wrappedGetters[type]) { - if ((process.env.NODE_ENV !== 'production')) { - console.error(("[vuex] duplicate getter key: " + type)); + if (stack.indexOf(value) !== -1) { + throw Error('Circular reference detected in ' + path.join('.')); } - return - } - store._wrappedGetters[type] = function wrappedGetter (store) { - return rawGetter( - local.state, // local state - local.getters, // local getters - store.state, // root state - store.getters // root getters - ) - }; - } - function enableStrictMode (store) { - store._vm.$watch(function () { return this._data.$$state }, function () { - if ((process.env.NODE_ENV !== 'production')) { - assert(store._committing, "do not mutate vuex store state outside mutation handlers."); - } - }, { deep: true, sync: true }); - } + stack.push(value); - function getNestedState (state, path) { - return path.reduce(function (state, key) { return state[key]; }, state) - } + utils$1.forEach(value, function each(el, key) { + const result = !(utils$1.isUndefined(el) || el === null) && visitor.call( + formData, el, utils$1.isString(key) ? key.trim() : key, path, exposedHelpers + ); - function unifyObjectStyle (type, payload, options) { - if (isObject$1(type) && type.type) { - options = payload; - payload = type; - type = type.type; + if (result === true) { + build(el, path ? path.concat(key) : [key]); + } + }); + + stack.pop(); } - if ((process.env.NODE_ENV !== 'production')) { - assert(typeof type === 'string', ("expects string as the type, but found " + (typeof type) + ".")); + if (!utils$1.isObject(obj)) { + throw new TypeError('data must be an object'); } - return { type: type, payload: payload, options: options } - } + build(obj); - function install (_Vue) { - if (Vue && _Vue === Vue) { - if ((process.env.NODE_ENV !== 'production')) { - console.error( - '[vuex] already installed. Vue.use(Vuex) should be called only once.' - ); - } - return - } - Vue = _Vue; - applyMixin(Vue); + return formData; } /** - * Reduce the code which written in Vue.js for getting the getters - * @param {String} [namespace] - Module's namespace - * @param {Object|Array} getters - * @return {Object} + * It encodes a string by replacing all characters that are not in the unreserved set with + * their percent-encoded equivalents + * + * @param {string} str - The string to encode. + * + * @returns {string} The encoded string. */ - var mapGetters = normalizeNamespace(function (namespace, getters) { - var res = {}; - if ((process.env.NODE_ENV !== 'production') && !isValidMap(getters)) { - console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object'); - } - normalizeMap(getters).forEach(function (ref) { - var key = ref.key; - var val = ref.val; - - // The namespace has been mutated by normalizeNamespace - val = namespace + val; - res[key] = function mappedGetter () { - if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) { - return - } - if ((process.env.NODE_ENV !== 'production') && !(val in this.$store.getters)) { - console.error(("[vuex] unknown getter: " + val)); - return - } - return this.$store.getters[val] - }; - // mark vuex getter for devtools - res[key].vuex = true; + function encode$1(str) { + const charMap = { + '!': '%21', + "'": '%27', + '(': '%28', + ')': '%29', + '~': '%7E', + '%20': '+', + '%00': '\x00' + }; + return encodeURIComponent(str).replace(/[!'()~]|%20|%00/g, function replacer(match) { + return charMap[match]; }); - return res - }); + } /** - * Reduce the code which written in Vue.js for dispatch the action - * @param {String} [namespace] - Module's namespace - * @param {Object|Array} actions # Object's item can be a function which accept `dispatch` function as the first param, it can accept anthor params. You can dispatch action and do any other things in this function. specially, You need to pass anthor params from the mapped function. - * @return {Object} + * It takes a params object and converts it to a FormData object + * + * @param {Object} params - The parameters to be converted to a FormData object. + * @param {Object} options - The options object passed to the Axios constructor. + * + * @returns {void} */ - var mapActions = normalizeNamespace(function (namespace, actions) { - var res = {}; - if ((process.env.NODE_ENV !== 'production') && !isValidMap(actions)) { - console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object'); - } - normalizeMap(actions).forEach(function (ref) { - var key = ref.key; - var val = ref.val; + function AxiosURLSearchParams(params, options) { + this._pairs = []; - res[key] = function mappedAction () { - var args = [], len = arguments.length; - while ( len-- ) args[ len ] = arguments[ len ]; + params && toFormData(params, this, options); + } - // get dispatch function from store - var dispatch = this.$store.dispatch; - if (namespace) { - var module = getModuleByNamespace(this.$store, 'mapActions', namespace); - if (!module) { - return - } - dispatch = module.context.dispatch; - } - return typeof val === 'function' - ? val.apply(this, [dispatch].concat(args)) - : dispatch.apply(this.$store, [val].concat(args)) - }; - }); - return res - }); + const prototype = AxiosURLSearchParams.prototype; + + prototype.append = function append(name, value) { + this._pairs.push([name, value]); + }; + + prototype.toString = function toString(encoder) { + const _encode = encoder ? function(value) { + return encoder.call(this, value, encode$1); + } : encode$1; + + return this._pairs.map(function each(pair) { + return _encode(pair[0]) + '=' + _encode(pair[1]); + }, '').join('&'); + }; /** - * Normalize the map - * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ] - * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ] - * @param {Array|Object} map - * @return {Object} + * It replaces all instances of the characters `:`, `$`, `,`, `+`, `[`, and `]` with their + * URI encoded counterparts + * + * @param {string} val The value to be encoded. + * + * @returns {string} The encoded value. */ - function normalizeMap (map) { - if (!isValidMap(map)) { - return [] - } - return Array.isArray(map) - ? map.map(function (key) { return ({ key: key, val: key }); }) - : Object.keys(map).map(function (key) { return ({ key: key, val: map[key] }); }) + function encode(val) { + return encodeURIComponent(val). + replace(/%3A/gi, ':'). + replace(/%24/g, '$'). + replace(/%2C/gi, ','). + replace(/%20/g, '+'). + replace(/%5B/gi, '['). + replace(/%5D/gi, ']'); } /** - * Validate whether given map is valid or not - * @param {*} map - * @return {Boolean} + * Build a URL by appending params to the end + * + * @param {string} url The base of the url (e.g., http://www.google.com) + * @param {object} [params] The params to be appended + * @param {?object} options + * + * @returns {string} The formatted url */ - function isValidMap (map) { - return Array.isArray(map) || isObject$1(map) + function buildURL(url, params, options) { + /*eslint no-param-reassign:0*/ + if (!params) { + return url; + } + + const _encode = options && options.encode || encode; + + const serializeFn = options && options.serialize; + + let serializedParams; + + if (serializeFn) { + serializedParams = serializeFn(params, options); + } else { + serializedParams = utils$1.isURLSearchParams(params) ? + params.toString() : + new AxiosURLSearchParams(params, options).toString(_encode); + } + + if (serializedParams) { + const hashmarkIndex = url.indexOf("#"); + + if (hashmarkIndex !== -1) { + url = url.slice(0, hashmarkIndex); + } + url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams; + } + + return url; } - /** - * Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map. - * @param {Function} fn - * @return {Function} - */ - function normalizeNamespace (fn) { - return function (namespace, map) { - if (typeof namespace !== 'string') { - map = namespace; - namespace = ''; - } else if (namespace.charAt(namespace.length - 1) !== '/') { - namespace += '/'; + class InterceptorManager { + constructor() { + this.handlers = []; + } + + /** + * Add a new interceptor to the stack + * + * @param {Function} fulfilled The function to handle `then` for a `Promise` + * @param {Function} rejected The function to handle `reject` for a `Promise` + * + * @return {Number} An ID used to remove interceptor later + */ + use(fulfilled, rejected, options) { + this.handlers.push({ + fulfilled, + rejected, + synchronous: options ? options.synchronous : false, + runWhen: options ? options.runWhen : null + }); + return this.handlers.length - 1; + } + + /** + * Remove an interceptor from the stack + * + * @param {Number} id The ID that was returned by `use` + * + * @returns {Boolean} `true` if the interceptor was removed, `false` otherwise + */ + eject(id) { + if (this.handlers[id]) { + this.handlers[id] = null; } - return fn(namespace, map) } - } - /** - * Search a special module from store by namespace. if module not exist, print error message. - * @param {Object} store - * @param {String} helper - * @param {String} namespace - * @return {Object} - */ - function getModuleByNamespace (store, helper, namespace) { - var module = store._modulesNamespaceMap[namespace]; - if ((process.env.NODE_ENV !== 'production') && !module) { - console.error(("[vuex] module namespace not found in " + helper + "(): " + namespace)); + /** + * Clear all interceptors from the stack + * + * @returns {void} + */ + clear() { + if (this.handlers) { + this.handlers = []; + } + } + + /** + * Iterate over all the registered interceptors + * + * This method is particularly useful for skipping over any + * interceptors that may have become `null` calling `eject`. + * + * @param {Function} fn The function to call for each interceptor + * + * @returns {void} + */ + forEach(fn) { + utils$1.forEach(this.handlers, function forEachHandler(h) { + if (h !== null) { + fn(h); + } + }); } - return module } - var bind = function bind(fn, thisArg) { - return function wrap() { - var args = new Array(arguments.length); - for (var i = 0; i < args.length; i++) { - args[i] = arguments[i]; - } - return fn.apply(thisArg, args); - }; + var InterceptorManager$1 = InterceptorManager; + + var transitionalDefaults = { + silentJSONParsing: true, + forcedJSONParsing: true, + clarifyTimeoutError: false }; - // utils is a library of generic helper functions non-specific to axios + var URLSearchParams$1 = typeof URLSearchParams !== 'undefined' ? URLSearchParams : AxiosURLSearchParams; - var toString = Object.prototype.toString; + var FormData$1 = typeof FormData !== 'undefined' ? FormData : null; - /** - * Determine if a value is an Array - * - * @param {Object} val The value to test - * @returns {boolean} True if value is an Array, otherwise false - */ - function isArray(val) { - return toString.call(val) === '[object Array]'; - } + var Blob$1 = typeof Blob !== 'undefined' ? Blob : null; - /** - * Determine if a value is undefined - * - * @param {Object} val The value to test - * @returns {boolean} True if the value is undefined, otherwise false - */ - function isUndefined(val) { - return typeof val === 'undefined'; - } + var platform$1 = { + isBrowser: true, + classes: { + URLSearchParams: URLSearchParams$1, + FormData: FormData$1, + Blob: Blob$1 + }, + protocols: ['http', 'https', 'file', 'blob', 'url', 'data'] + }; + + const hasBrowserEnv = typeof window !== 'undefined' && typeof document !== 'undefined'; /** - * Determine if a value is a Buffer + * Determine if we're running in a standard browser environment * - * @param {Object} val The value to test - * @returns {boolean} True if value is a Buffer, otherwise false + * This allows axios to run in a web worker, and react-native. + * Both environments support XMLHttpRequest, but not fully standard globals. + * + * web workers: + * typeof window -> undefined + * typeof document -> undefined + * + * react-native: + * navigator.product -> 'ReactNative' + * nativescript + * navigator.product -> 'NativeScript' or 'NS' + * + * @returns {boolean} */ - function isBuffer(val) { - return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor) - && typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val); - } + const hasStandardBrowserEnv = ( + (product) => { + return hasBrowserEnv && ['ReactNative', 'NativeScript', 'NS'].indexOf(product) < 0 + })(typeof navigator !== 'undefined' && navigator.product); /** - * Determine if a value is an ArrayBuffer + * Determine if we're running in a standard browser webWorker environment * - * @param {Object} val The value to test - * @returns {boolean} True if value is an ArrayBuffer, otherwise false + * Although the `isStandardBrowserEnv` method indicates that + * `allows axios to run in a web worker`, the WebWorker will still be + * filtered out due to its judgment standard + * `typeof window !== 'undefined' && typeof document !== 'undefined'`. + * This leads to a problem when axios post `FormData` in webWorker */ - function isArrayBuffer(val) { - return toString.call(val) === '[object ArrayBuffer]'; + const hasStandardBrowserWebWorkerEnv = (() => { + return ( + typeof WorkerGlobalScope !== 'undefined' && + // eslint-disable-next-line no-undef + self instanceof WorkerGlobalScope && + typeof self.importScripts === 'function' + ); + })(); + + var utils = /*#__PURE__*/Object.freeze({ + __proto__: null, + hasBrowserEnv: hasBrowserEnv, + hasStandardBrowserEnv: hasStandardBrowserEnv, + hasStandardBrowserWebWorkerEnv: hasStandardBrowserWebWorkerEnv + }); + + var platform = { + ...utils, + ...platform$1 + }; + + function toURLEncodedForm(data, options) { + return toFormData(data, new platform.classes.URLSearchParams(), Object.assign({ + visitor: function(value, key, path, helpers) { + if (platform.isNode && utils$1.isBuffer(value)) { + this.append(key, value.toString('base64')); + return false; + } + + return helpers.defaultVisitor.apply(this, arguments); + } + }, options)); } /** - * Determine if a value is a FormData + * It takes a string like `foo[x][y][z]` and returns an array like `['foo', 'x', 'y', 'z'] * - * @param {Object} val The value to test - * @returns {boolean} True if value is an FormData, otherwise false + * @param {string} name - The name of the property to get. + * + * @returns An array of strings. */ - function isFormData(val) { - return (typeof FormData !== 'undefined') && (val instanceof FormData); + function parsePropPath(name) { + // foo[x][y][z] + // foo.x.y.z + // foo-x-y-z + // foo x y z + return utils$1.matchAll(/\w+|\[(\w*)]/g, name).map(match => { + return match[0] === '[]' ? '' : match[1] || match[0]; + }); } /** - * Determine if a value is a view on an ArrayBuffer + * Convert an array to an object. * - * @param {Object} val The value to test - * @returns {boolean} True if value is a view on an ArrayBuffer, otherwise false + * @param {Array} arr - The array to convert to an object. + * + * @returns An object with the same keys and values as the array. */ - function isArrayBufferView(val) { - var result; - if ((typeof ArrayBuffer !== 'undefined') && (ArrayBuffer.isView)) { - result = ArrayBuffer.isView(val); - } else { - result = (val) && (val.buffer) && (val.buffer instanceof ArrayBuffer); + function arrayToObject(arr) { + const obj = {}; + const keys = Object.keys(arr); + let i; + const len = keys.length; + let key; + for (i = 0; i < len; i++) { + key = keys[i]; + obj[key] = arr[key]; } - return result; + return obj; } /** - * Determine if a value is a String + * It takes a FormData object and returns a JavaScript object * - * @param {Object} val The value to test - * @returns {boolean} True if value is a String, otherwise false - */ - function isString(val) { - return typeof val === 'string'; - } - - /** - * Determine if a value is a Number + * @param {string} formData The FormData object to convert to JSON. * - * @param {Object} val The value to test - * @returns {boolean} True if value is a Number, otherwise false + * @returns {Object | null} The converted object. */ - function isNumber(val) { - return typeof val === 'number'; - } + function formDataToJSON(formData) { + function buildPath(path, value, target, index) { + let name = path[index++]; - /** - * Determine if a value is an Object - * - * @param {Object} val The value to test - * @returns {boolean} True if value is an Object, otherwise false - */ - function isObject(val) { - return val !== null && typeof val === 'object'; - } + if (name === '__proto__') return true; - /** - * Determine if a value is a plain Object - * - * @param {Object} val The value to test - * @return {boolean} True if value is a plain Object, otherwise false - */ - function isPlainObject(val) { - if (toString.call(val) !== '[object Object]') { - return false; + const isNumericKey = Number.isFinite(+name); + const isLast = index >= path.length; + name = !name && utils$1.isArray(target) ? target.length : name; + + if (isLast) { + if (utils$1.hasOwnProp(target, name)) { + target[name] = [target[name], value]; + } else { + target[name] = value; + } + + return !isNumericKey; + } + + if (!target[name] || !utils$1.isObject(target[name])) { + target[name] = []; + } + + const result = buildPath(path, value, target[name], index); + + if (result && utils$1.isArray(target[name])) { + target[name] = arrayToObject(target[name]); + } + + return !isNumericKey; + } + + if (utils$1.isFormData(formData) && utils$1.isFunction(formData.entries)) { + const obj = {}; + + utils$1.forEachEntry(formData, (name, value) => { + buildPath(parsePropPath(name), value, obj, 0); + }); + + return obj; } - var prototype = Object.getPrototypeOf(val); - return prototype === null || prototype === Object.prototype; + return null; } /** - * Determine if a value is a Date + * It takes a string, tries to parse it, and if it fails, it returns the stringified version + * of the input * - * @param {Object} val The value to test - * @returns {boolean} True if value is a Date, otherwise false + * @param {any} rawValue - The value to be stringified. + * @param {Function} parser - A function that parses a string into a JavaScript object. + * @param {Function} encoder - A function that takes a value and returns a string. + * + * @returns {string} A stringified version of the rawValue. */ - function isDate(val) { - return toString.call(val) === '[object Date]'; + function stringifySafely(rawValue, parser, encoder) { + if (utils$1.isString(rawValue)) { + try { + (parser || JSON.parse)(rawValue); + return utils$1.trim(rawValue); + } catch (e) { + if (e.name !== 'SyntaxError') { + throw e; + } + } + } + + return (encoder || JSON.stringify)(rawValue); } + const defaults = { + + transitional: transitionalDefaults, + + adapter: ['xhr', 'http'], + + transformRequest: [function transformRequest(data, headers) { + const contentType = headers.getContentType() || ''; + const hasJSONContentType = contentType.indexOf('application/json') > -1; + const isObjectPayload = utils$1.isObject(data); + + if (isObjectPayload && utils$1.isHTMLForm(data)) { + data = new FormData(data); + } + + const isFormData = utils$1.isFormData(data); + + if (isFormData) { + return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data; + } + + if (utils$1.isArrayBuffer(data) || + utils$1.isBuffer(data) || + utils$1.isStream(data) || + utils$1.isFile(data) || + utils$1.isBlob(data) + ) { + return data; + } + if (utils$1.isArrayBufferView(data)) { + return data.buffer; + } + if (utils$1.isURLSearchParams(data)) { + headers.setContentType('application/x-www-form-urlencoded;charset=utf-8', false); + return data.toString(); + } + + let isFileList; + + if (isObjectPayload) { + if (contentType.indexOf('application/x-www-form-urlencoded') > -1) { + return toURLEncodedForm(data, this.formSerializer).toString(); + } + + if ((isFileList = utils$1.isFileList(data)) || contentType.indexOf('multipart/form-data') > -1) { + const _FormData = this.env && this.env.FormData; + + return toFormData( + isFileList ? {'files[]': data} : data, + _FormData && new _FormData(), + this.formSerializer + ); + } + } + + if (isObjectPayload || hasJSONContentType ) { + headers.setContentType('application/json', false); + return stringifySafely(data); + } + + return data; + }], + + transformResponse: [function transformResponse(data) { + const transitional = this.transitional || defaults.transitional; + const forcedJSONParsing = transitional && transitional.forcedJSONParsing; + const JSONRequested = this.responseType === 'json'; + + if (data && utils$1.isString(data) && ((forcedJSONParsing && !this.responseType) || JSONRequested)) { + const silentJSONParsing = transitional && transitional.silentJSONParsing; + const strictJSONParsing = !silentJSONParsing && JSONRequested; + + try { + return JSON.parse(data); + } catch (e) { + if (strictJSONParsing) { + if (e.name === 'SyntaxError') { + throw AxiosError.from(e, AxiosError.ERR_BAD_RESPONSE, this, null, this.response); + } + throw e; + } + } + } + + return data; + }], + + /** + * A timeout in milliseconds to abort a request. If set to 0 (default) a + * timeout is not created. + */ + timeout: 0, + + xsrfCookieName: 'XSRF-TOKEN', + xsrfHeaderName: 'X-XSRF-TOKEN', + + maxContentLength: -1, + maxBodyLength: -1, + + env: { + FormData: platform.classes.FormData, + Blob: platform.classes.Blob + }, + + validateStatus: function validateStatus(status) { + return status >= 200 && status < 300; + }, + + headers: { + common: { + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': undefined + } + } + }; + + utils$1.forEach(['delete', 'get', 'head', 'post', 'put', 'patch'], (method) => { + defaults.headers[method] = {}; + }); + + var defaults$1 = defaults; + + // RawAxiosHeaders whose duplicates are ignored by node + // c.f. https://nodejs.org/api/http.html#http_message_headers + const ignoreDuplicateOf = utils$1.toObjectSet([ + 'age', 'authorization', 'content-length', 'content-type', 'etag', + 'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since', + 'last-modified', 'location', 'max-forwards', 'proxy-authorization', + 'referer', 'retry-after', 'user-agent' + ]); + /** - * Determine if a value is a File + * Parse headers into an object * - * @param {Object} val The value to test - * @returns {boolean} True if value is a File, otherwise false + * ``` + * Date: Wed, 27 Aug 2014 08:58:49 GMT + * Content-Type: application/json + * Connection: keep-alive + * Transfer-Encoding: chunked + * ``` + * + * @param {String} rawHeaders Headers needing to be parsed + * + * @returns {Object} Headers parsed into an object */ - function isFile(val) { - return toString.call(val) === '[object File]'; - } + var parseHeaders = rawHeaders => { + const parsed = {}; + let key; + let val; + let i; + + rawHeaders && rawHeaders.split('\n').forEach(function parser(line) { + i = line.indexOf(':'); + key = line.substring(0, i).trim().toLowerCase(); + val = line.substring(i + 1).trim(); + + if (!key || (parsed[key] && ignoreDuplicateOf[key])) { + return; + } + + if (key === 'set-cookie') { + if (parsed[key]) { + parsed[key].push(val); + } else { + parsed[key] = [val]; + } + } else { + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; + } + }); + + return parsed; + }; + + const $internals = Symbol('internals'); - /** - * Determine if a value is a Blob - * - * @param {Object} val The value to test - * @returns {boolean} True if value is a Blob, otherwise false - */ - function isBlob(val) { - return toString.call(val) === '[object Blob]'; + function normalizeHeader(header) { + return header && String(header).trim().toLowerCase(); } - /** - * Determine if a value is a Function - * - * @param {Object} val The value to test - * @returns {boolean} True if value is a Function, otherwise false - */ - function isFunction(val) { - return toString.call(val) === '[object Function]'; + function normalizeValue(value) { + if (value === false || value == null) { + return value; + } + + return utils$1.isArray(value) ? value.map(normalizeValue) : String(value); } - /** - * Determine if a value is a Stream - * - * @param {Object} val The value to test - * @returns {boolean} True if value is a Stream, otherwise false - */ - function isStream(val) { - return isObject(val) && isFunction(val.pipe); + function parseTokens(str) { + const tokens = Object.create(null); + const tokensRE = /([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g; + let match; + + while ((match = tokensRE.exec(str))) { + tokens[match[1]] = match[2]; + } + + return tokens; } - /** - * Determine if a value is a URLSearchParams object - * - * @param {Object} val The value to test - * @returns {boolean} True if value is a URLSearchParams object, otherwise false - */ - function isURLSearchParams(val) { - return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams; + const isValidHeaderName = (str) => /^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(str.trim()); + + function matchHeaderValue(context, value, header, filter, isHeaderNameFilter) { + if (utils$1.isFunction(filter)) { + return filter.call(this, value, header); + } + + if (isHeaderNameFilter) { + value = header; + } + + if (!utils$1.isString(value)) return; + + if (utils$1.isString(filter)) { + return value.indexOf(filter) !== -1; + } + + if (utils$1.isRegExp(filter)) { + return filter.test(value); + } } - /** - * Trim excess whitespace off the beginning and end of a string - * - * @param {String} str The String to trim - * @returns {String} The String freed of excess whitespace - */ - function trim(str) { - return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, ''); + function formatHeader(header) { + return header.trim() + .toLowerCase().replace(/([a-z\d])(\w*)/g, (w, char, str) => { + return char.toUpperCase() + str; + }); } - /** - * Determine if we're running in a standard browser environment - * - * This allows axios to run in a web worker, and react-native. - * Both environments support XMLHttpRequest, but not fully standard globals. - * - * web workers: - * typeof window -> undefined - * typeof document -> undefined - * - * react-native: - * navigator.product -> 'ReactNative' - * nativescript - * navigator.product -> 'NativeScript' or 'NS' - */ - function isStandardBrowserEnv() { - if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' || - navigator.product === 'NativeScript' || - navigator.product === 'NS')) { - return false; - } - return ( - typeof window !== 'undefined' && - typeof document !== 'undefined' - ); + function buildAccessors(obj, header) { + const accessorName = utils$1.toCamelCase(' ' + header); + + ['get', 'set', 'has'].forEach(methodName => { + Object.defineProperty(obj, methodName + accessorName, { + value: function(arg1, arg2, arg3) { + return this[methodName].call(this, header, arg1, arg2, arg3); + }, + configurable: true + }); + }); } - /** - * Iterate over an Array or an Object invoking a function for each item. - * - * If `obj` is an Array callback will be called passing - * the value, index, and complete array for each item. - * - * If 'obj' is an Object callback will be called passing - * the value, key, and complete object for each property. - * - * @param {Object|Array} obj The object to iterate - * @param {Function} fn The callback to invoke for each item - */ - function forEach(obj, fn) { - // Don't bother if no value provided - if (obj === null || typeof obj === 'undefined') { - return; + class AxiosHeaders { + constructor(headers) { + headers && this.set(headers); } - // Force an array if not already something iterable - if (typeof obj !== 'object') { - /*eslint no-param-reassign:0*/ - obj = [obj]; - } + set(header, valueOrRewrite, rewrite) { + const self = this; - if (isArray(obj)) { - // Iterate over array values - for (var i = 0, l = obj.length; i < l; i++) { - fn.call(null, obj[i], i, obj); - } - } else { - // Iterate over object keys - for (var key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - fn.call(null, obj[key], key, obj); + function setHeader(_value, _header, _rewrite) { + const lHeader = normalizeHeader(_header); + + if (!lHeader) { + throw new Error('header name must be a non-empty string'); + } + + const key = utils$1.findKey(self, lHeader); + + if(!key || self[key] === undefined || _rewrite === true || (_rewrite === undefined && self[key] !== false)) { + self[key || _header] = normalizeValue(_value); } } - } - } - /** - * Accepts varargs expecting each argument to be an object, then - * immutably merges the properties of each object and returns result. - * - * When multiple objects contain the same key the later object in - * the arguments list will take precedence. - * - * Example: - * - * ```js - * var result = merge({foo: 123}, {foo: 456}); - * console.log(result.foo); // outputs 456 - * ``` - * - * @param {Object} obj1 Object to merge - * @returns {Object} Result of all merge properties - */ - function merge(/* obj1, obj2, obj3, ... */) { - var result = {}; - function assignValue(val, key) { - if (isPlainObject(result[key]) && isPlainObject(val)) { - result[key] = merge(result[key], val); - } else if (isPlainObject(val)) { - result[key] = merge({}, val); - } else if (isArray(val)) { - result[key] = val.slice(); + const setHeaders = (headers, _rewrite) => + utils$1.forEach(headers, (_value, _header) => setHeader(_value, _header, _rewrite)); + + if (utils$1.isPlainObject(header) || header instanceof this.constructor) { + setHeaders(header, valueOrRewrite); + } else if(utils$1.isString(header) && (header = header.trim()) && !isValidHeaderName(header)) { + setHeaders(parseHeaders(header), valueOrRewrite); } else { - result[key] = val; + header != null && setHeader(valueOrRewrite, header, rewrite); + } + + return this; + } + + get(header, parser) { + header = normalizeHeader(header); + + if (header) { + const key = utils$1.findKey(this, header); + + if (key) { + const value = this[key]; + + if (!parser) { + return value; + } + + if (parser === true) { + return parseTokens(value); + } + + if (utils$1.isFunction(parser)) { + return parser.call(this, value, key); + } + + if (utils$1.isRegExp(parser)) { + return parser.exec(value); + } + + throw new TypeError('parser must be boolean|regexp|function'); + } } } - for (var i = 0, l = arguments.length; i < l; i++) { - forEach(arguments[i], assignValue); + has(header, matcher) { + header = normalizeHeader(header); + + if (header) { + const key = utils$1.findKey(this, header); + + return !!(key && this[key] !== undefined && (!matcher || matchHeaderValue(this, this[key], key, matcher))); + } + + return false; } - return result; - } - /** - * Extends object a by mutably adding to it the properties of object b. - * - * @param {Object} a The object to be extended - * @param {Object} b The object to copy properties from - * @param {Object} thisArg The object to bind function to - * @return {Object} The resulting value of object a - */ - function extend(a, b, thisArg) { - forEach(b, function assignValue(val, key) { - if (thisArg && typeof val === 'function') { - a[key] = bind(val, thisArg); + delete(header, matcher) { + const self = this; + let deleted = false; + + function deleteHeader(_header) { + _header = normalizeHeader(_header); + + if (_header) { + const key = utils$1.findKey(self, _header); + + if (key && (!matcher || matchHeaderValue(self, self[key], key, matcher))) { + delete self[key]; + + deleted = true; + } + } + } + + if (utils$1.isArray(header)) { + header.forEach(deleteHeader); } else { - a[key] = val; + deleteHeader(header); } - }); - return a; - } - /** - * Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) - * - * @param {string} content with BOM - * @return {string} content value without BOM - */ - function stripBOM(content) { - if (content.charCodeAt(0) === 0xFEFF) { - content = content.slice(1); + return deleted; } - return content; - } - var utils = { - isArray: isArray, - isArrayBuffer: isArrayBuffer, - isBuffer: isBuffer, - isFormData: isFormData, - isArrayBufferView: isArrayBufferView, - isString: isString, - isNumber: isNumber, - isObject: isObject, - isPlainObject: isPlainObject, - isUndefined: isUndefined, - isDate: isDate, - isFile: isFile, - isBlob: isBlob, - isFunction: isFunction, - isStream: isStream, - isURLSearchParams: isURLSearchParams, - isStandardBrowserEnv: isStandardBrowserEnv, - forEach: forEach, - merge: merge, - extend: extend, - trim: trim, - stripBOM: stripBOM - }; + clear(matcher) { + const keys = Object.keys(this); + let i = keys.length; + let deleted = false; - function encode(val) { - return encodeURIComponent(val). - replace(/%3A/gi, ':'). - replace(/%24/g, '$'). - replace(/%2C/gi, ','). - replace(/%20/g, '+'). - replace(/%5B/gi, '['). - replace(/%5D/gi, ']'); - } + while (i--) { + const key = keys[i]; + if(!matcher || matchHeaderValue(this, this[key], key, matcher, true)) { + delete this[key]; + deleted = true; + } + } - /** - * Build a URL by appending params to the end - * - * @param {string} url The base of the url (e.g., http://www.google.com) - * @param {object} [params] The params to be appended - * @returns {string} The formatted url - */ - var buildURL = function buildURL(url, params, paramsSerializer) { - /*eslint no-param-reassign:0*/ - if (!params) { - return url; + return deleted; } - var serializedParams; - if (paramsSerializer) { - serializedParams = paramsSerializer(params); - } else if (utils.isURLSearchParams(params)) { - serializedParams = params.toString(); - } else { - var parts = []; + normalize(format) { + const self = this; + const headers = {}; + + utils$1.forEach(this, (value, header) => { + const key = utils$1.findKey(headers, header); - utils.forEach(params, function serialize(val, key) { - if (val === null || typeof val === 'undefined') { + if (key) { + self[key] = normalizeValue(value); + delete self[header]; return; } - if (utils.isArray(val)) { - key = key + '[]'; - } else { - val = [val]; + const normalized = format ? formatHeader(header) : String(header).trim(); + + if (normalized !== header) { + delete self[header]; } - utils.forEach(val, function parseValue(v) { - if (utils.isDate(v)) { - v = v.toISOString(); - } else if (utils.isObject(v)) { - v = JSON.stringify(v); - } - parts.push(encode(key) + '=' + encode(v)); - }); + self[normalized] = normalizeValue(value); + + headers[normalized] = true; }); - serializedParams = parts.join('&'); + return this; } - if (serializedParams) { - var hashmarkIndex = url.indexOf('#'); - if (hashmarkIndex !== -1) { - url = url.slice(0, hashmarkIndex); - } + concat(...targets) { + return this.constructor.concat(this, ...targets); + } - url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams; + toJSON(asStrings) { + const obj = Object.create(null); + + utils$1.forEach(this, (value, header) => { + value != null && value !== false && (obj[header] = asStrings && utils$1.isArray(value) ? value.join(', ') : value); + }); + + return obj; } - return url; - }; + [Symbol.iterator]() { + return Object.entries(this.toJSON())[Symbol.iterator](); + } - function InterceptorManager() { - this.handlers = []; - } + toString() { + return Object.entries(this.toJSON()).map(([header, value]) => header + ': ' + value).join('\n'); + } - /** - * Add a new interceptor to the stack - * - * @param {Function} fulfilled The function to handle `then` for a `Promise` - * @param {Function} rejected The function to handle `reject` for a `Promise` - * - * @return {Number} An ID used to remove interceptor later - */ - InterceptorManager.prototype.use = function use(fulfilled, rejected, options) { - this.handlers.push({ - fulfilled: fulfilled, - rejected: rejected, - synchronous: options ? options.synchronous : false, - runWhen: options ? options.runWhen : null - }); - return this.handlers.length - 1; - }; + get [Symbol.toStringTag]() { + return 'AxiosHeaders'; + } - /** - * Remove an interceptor from the stack - * - * @param {Number} id The ID that was returned by `use` - */ - InterceptorManager.prototype.eject = function eject(id) { - if (this.handlers[id]) { - this.handlers[id] = null; + static from(thing) { + return thing instanceof this ? thing : new this(thing); } - }; - /** - * Iterate over all the registered interceptors - * - * This method is particularly useful for skipping over any - * interceptors that may have become `null` calling `eject`. - * - * @param {Function} fn The function to call for each interceptor - */ - InterceptorManager.prototype.forEach = function forEach(fn) { - utils.forEach(this.handlers, function forEachHandler(h) { - if (h !== null) { - fn(h); + static concat(first, ...targets) { + const computed = new this(first); + + targets.forEach((target) => computed.set(target)); + + return computed; + } + + static accessor(header) { + const internals = this[$internals] = (this[$internals] = { + accessors: {} + }); + + const accessors = internals.accessors; + const prototype = this.prototype; + + function defineAccessor(_header) { + const lHeader = normalizeHeader(_header); + + if (!accessors[lHeader]) { + buildAccessors(prototype, _header); + accessors[lHeader] = true; + } } - }); - }; - var InterceptorManager_1 = InterceptorManager; + utils$1.isArray(header) ? header.forEach(defineAccessor) : defineAccessor(header); + + return this; + } + } + + AxiosHeaders.accessor(['Content-Type', 'Content-Length', 'Accept', 'Accept-Encoding', 'User-Agent', 'Authorization']); - var normalizeHeaderName = function normalizeHeaderName(headers, normalizedName) { - utils.forEach(headers, function processHeader(value, name) { - if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) { - headers[normalizedName] = value; - delete headers[name]; + // reserved names hotfix + utils$1.reduceDescriptors(AxiosHeaders.prototype, ({value}, key) => { + let mapped = key[0].toUpperCase() + key.slice(1); // map `set` => `Set` + return { + get: () => value, + set(headerValue) { + this[mapped] = headerValue; } - }); - }; + } + }); + + utils$1.freezeMethods(AxiosHeaders); + + var AxiosHeaders$1 = AxiosHeaders; /** - * Update an Error with the specified config, error code, and response. + * Transform the data for a request or a response * - * @param {Error} error The error to update. - * @param {Object} config The config. - * @param {string} [code] The error code (for example, 'ECONNABORTED'). - * @param {Object} [request] The request. - * @param {Object} [response] The response. - * @returns {Error} The error. + * @param {Array|Function} fns A single function or Array of functions + * @param {?Object} response The response object + * + * @returns {*} The resulting transformed data */ - var enhanceError = function enhanceError(error, config, code, request, response) { - error.config = config; - if (code) { - error.code = code; - } + function transformData(fns, response) { + const config = this || defaults$1; + const context = response || config; + const headers = AxiosHeaders$1.from(context.headers); + let data = context.data; + + utils$1.forEach(fns, function transform(fn) { + data = fn.call(config, data, headers.normalize(), response ? response.status : undefined); + }); - error.request = request; - error.response = response; - error.isAxiosError = true; + headers.normalize(); - error.toJSON = function toJSON() { - return { - // Standard - message: this.message, - name: this.name, - // Microsoft - description: this.description, - number: this.number, - // Mozilla - fileName: this.fileName, - lineNumber: this.lineNumber, - columnNumber: this.columnNumber, - stack: this.stack, - // Axios - config: this.config, - code: this.code - }; - }; - return error; - }; + return data; + } + + function isCancel(value) { + return !!(value && value.__CANCEL__); + } /** - * Create an Error with the specified message, config, error code, request and response. + * A `CanceledError` is an object that is thrown when an operation is canceled. * - * @param {string} message The error message. - * @param {Object} config The config. - * @param {string} [code] The error code (for example, 'ECONNABORTED'). - * @param {Object} [request] The request. - * @param {Object} [response] The response. - * @returns {Error} The created error. + * @param {string=} message The message. + * @param {Object=} config The config. + * @param {Object=} request The request. + * + * @returns {CanceledError} The created error. */ - var createError = function createError(message, config, code, request, response) { - var error = new Error(message); - return enhanceError(error, config, code, request, response); - }; + function CanceledError(message, config, request) { + // eslint-disable-next-line no-eq-null,eqeqeq + AxiosError.call(this, message == null ? 'canceled' : message, AxiosError.ERR_CANCELED, config, request); + this.name = 'CanceledError'; + } + + utils$1.inherits(CanceledError, AxiosError, { + __CANCEL__: true + }); /** * Resolve or reject a Promise based on response status. @@ -1775,97 +5309,90 @@ define((function () { 'use strict'; * @param {Function} resolve A function that resolves the promise. * @param {Function} reject A function that rejects the promise. * @param {object} response The response. + * + * @returns {object} The response. */ - var settle = function settle(resolve, reject, response) { - var validateStatus = response.config.validateStatus; + function settle(resolve, reject, response) { + const validateStatus = response.config.validateStatus; if (!response.status || !validateStatus || validateStatus(response.status)) { resolve(response); } else { - reject(createError( + reject(new AxiosError( 'Request failed with status code ' + response.status, + [AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4], response.config, - null, response.request, response )); } - }; + } - var cookies = ( - utils.isStandardBrowserEnv() ? + var cookies = platform.hasStandardBrowserEnv ? // Standard browser envs support document.cookie - (function standardBrowserEnv() { - return { - write: function write(name, value, expires, path, domain, secure) { - var cookie = []; - cookie.push(name + '=' + encodeURIComponent(value)); + { + write(name, value, expires, path, domain, secure) { + const cookie = [name + '=' + encodeURIComponent(value)]; - if (utils.isNumber(expires)) { - cookie.push('expires=' + new Date(expires).toGMTString()); - } + utils$1.isNumber(expires) && cookie.push('expires=' + new Date(expires).toGMTString()); - if (utils.isString(path)) { - cookie.push('path=' + path); - } + utils$1.isString(path) && cookie.push('path=' + path); - if (utils.isString(domain)) { - cookie.push('domain=' + domain); - } + utils$1.isString(domain) && cookie.push('domain=' + domain); - if (secure === true) { - cookie.push('secure'); - } + secure === true && cookie.push('secure'); - document.cookie = cookie.join('; '); - }, + document.cookie = cookie.join('; '); + }, - read: function read(name) { - var match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)')); - return (match ? decodeURIComponent(match[3]) : null); - }, + read(name) { + const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)')); + return (match ? decodeURIComponent(match[3]) : null); + }, - remove: function remove(name) { - this.write(name, '', Date.now() - 86400000); - } - }; - })() : + remove(name) { + this.write(name, '', Date.now() - 86400000); + } + } - // Non standard browser env (web workers, react-native) lack needed support. - (function nonStandardBrowserEnv() { - return { - write: function write() {}, - read: function read() { return null; }, - remove: function remove() {} - }; - })() - ); + : + + // Non-standard browser env (web workers, react-native) lack needed support. + { + write() {}, + read() { + return null; + }, + remove() {} + }; /** * Determines whether the specified URL is absolute * * @param {string} url The URL to test + * * @returns {boolean} True if the specified URL is absolute, otherwise false */ - var isAbsoluteURL = function isAbsoluteURL(url) { + function isAbsoluteURL(url) { // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL). // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed // by any combination of letters, digits, plus, period, or hyphen. - return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url); - }; + return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url); + } /** * Creates a new URL by combining the specified URLs * * @param {string} baseURL The base URL * @param {string} relativeURL The relative URL + * * @returns {string} The combined URL */ - var combineURLs = function combineURLs(baseURL, relativeURL) { + function combineURLs(baseURL, relativeURL) { return relativeURL - ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '') + ? baseURL.replace(/\/?\/$/, '') + '/' + relativeURL.replace(/^\/+/, '') : baseURL; - }; + } /** * Creates a new URL by combining the baseURL with the requestedURL, @@ -1874,150 +5401,206 @@ define((function () { 'use strict'; * * @param {string} baseURL The base URL * @param {string} requestedURL Absolute or relative URL to combine + * * @returns {string} The combined full path */ - var buildFullPath = function buildFullPath(baseURL, requestedURL) { + function buildFullPath(baseURL, requestedURL) { if (baseURL && !isAbsoluteURL(requestedURL)) { return combineURLs(baseURL, requestedURL); } return requestedURL; - }; - - // Headers whose duplicates are ignored by node - // c.f. https://nodejs.org/api/http.html#http_message_headers - var ignoreDuplicateOf = [ - 'age', 'authorization', 'content-length', 'content-type', 'etag', - 'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since', - 'last-modified', 'location', 'max-forwards', 'proxy-authorization', - 'referer', 'retry-after', 'user-agent' - ]; - - /** - * Parse headers into an object - * - * ``` - * Date: Wed, 27 Aug 2014 08:58:49 GMT - * Content-Type: application/json - * Connection: keep-alive - * Transfer-Encoding: chunked - * ``` - * - * @param {String} headers Headers needing to be parsed - * @returns {Object} Headers parsed into an object - */ - var parseHeaders = function parseHeaders(headers) { - var parsed = {}; - var key; - var val; - var i; - - if (!headers) { return parsed; } - - utils.forEach(headers.split('\n'), function parser(line) { - i = line.indexOf(':'); - key = utils.trim(line.substr(0, i)).toLowerCase(); - val = utils.trim(line.substr(i + 1)); - - if (key) { - if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) { - return; - } - if (key === 'set-cookie') { - parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]); - } else { - parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; - } - } - }); - - return parsed; - }; + } - var isURLSameOrigin = ( - utils.isStandardBrowserEnv() ? + var isURLSameOrigin = platform.hasStandardBrowserEnv ? - // Standard browser envs have full support of the APIs needed to test - // whether the request URL is of the same origin as current location. - (function standardBrowserEnv() { - var msie = /(msie|trident)/i.test(navigator.userAgent); - var urlParsingNode = document.createElement('a'); - var originURL; + // Standard browser envs have full support of the APIs needed to test + // whether the request URL is of the same origin as current location. + (function standardBrowserEnv() { + const msie = /(msie|trident)/i.test(navigator.userAgent); + const urlParsingNode = document.createElement('a'); + let originURL; - /** - * Parse a URL to discover it's components + /** + * Parse a URL to discover its components * * @param {String} url The URL to be parsed * @returns {Object} */ - function resolveURL(url) { - var href = url; + function resolveURL(url) { + let href = url; - if (msie) { + if (msie) { // IE needs attribute set twice to normalize properties - urlParsingNode.setAttribute('href', href); - href = urlParsingNode.href; - } - urlParsingNode.setAttribute('href', href); - - // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils - return { - href: urlParsingNode.href, - protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', - host: urlParsingNode.host, - search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', - hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', - hostname: urlParsingNode.hostname, - port: urlParsingNode.port, - pathname: (urlParsingNode.pathname.charAt(0) === '/') ? - urlParsingNode.pathname : - '/' + urlParsingNode.pathname - }; + href = urlParsingNode.href; } - originURL = resolveURL(window.location.href); + urlParsingNode.setAttribute('href', href); + + // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + return { + href: urlParsingNode.href, + protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', + host: urlParsingNode.host, + search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', + hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', + hostname: urlParsingNode.hostname, + port: urlParsingNode.port, + pathname: (urlParsingNode.pathname.charAt(0) === '/') ? + urlParsingNode.pathname : + '/' + urlParsingNode.pathname + }; + } + + originURL = resolveURL(window.location.href); - /** + /** * Determine if a URL shares the same origin as the current location * * @param {String} requestURL The URL to test * @returns {boolean} True if URL shares the same origin, otherwise false */ - return function isURLSameOrigin(requestURL) { - var parsed = (utils.isString(requestURL)) ? resolveURL(requestURL) : requestURL; - return (parsed.protocol === originURL.protocol && - parsed.host === originURL.host); - }; - })() : + return function isURLSameOrigin(requestURL) { + const parsed = (utils$1.isString(requestURL)) ? resolveURL(requestURL) : requestURL; + return (parsed.protocol === originURL.protocol && + parsed.host === originURL.host); + }; + })() : // Non standard browser envs (web workers, react-native) lack needed support. - (function nonStandardBrowserEnv() { - return function isURLSameOrigin() { - return true; - }; - })() - ); + (function nonStandardBrowserEnv() { + return function isURLSameOrigin() { + return true; + }; + })(); + + function parseProtocol(url) { + const match = /^([-+\w]{1,25})(:?\/\/|:)/.exec(url); + return match && match[1] || ''; + } + + /** + * Calculate data maxRate + * @param {Number} [samplesCount= 10] + * @param {Number} [min= 1000] + * @returns {Function} + */ + function speedometer(samplesCount, min) { + samplesCount = samplesCount || 10; + const bytes = new Array(samplesCount); + const timestamps = new Array(samplesCount); + let head = 0; + let tail = 0; + let firstSampleTS; + + min = min !== undefined ? min : 1000; + + return function push(chunkLength) { + const now = Date.now(); + + const startedAt = timestamps[tail]; + + if (!firstSampleTS) { + firstSampleTS = now; + } + + bytes[head] = chunkLength; + timestamps[head] = now; + + let i = tail; + let bytesCount = 0; - var xhr = function xhrAdapter(config) { + while (i !== head) { + bytesCount += bytes[i++]; + i = i % samplesCount; + } + + head = (head + 1) % samplesCount; + + if (head === tail) { + tail = (tail + 1) % samplesCount; + } + + if (now - firstSampleTS < min) { + return; + } + + const passed = startedAt && now - startedAt; + + return passed ? Math.round(bytesCount * 1000 / passed) : undefined; + }; + } + + function progressEventReducer(listener, isDownloadStream) { + let bytesNotified = 0; + const _speedometer = speedometer(50, 250); + + return e => { + const loaded = e.loaded; + const total = e.lengthComputable ? e.total : undefined; + const progressBytes = loaded - bytesNotified; + const rate = _speedometer(progressBytes); + const inRange = loaded <= total; + + bytesNotified = loaded; + + const data = { + loaded, + total, + progress: total ? (loaded / total) : undefined, + bytes: progressBytes, + rate: rate ? rate : undefined, + estimated: rate && total && inRange ? (total - loaded) / rate : undefined, + event: e + }; + + data[isDownloadStream ? 'download' : 'upload'] = true; + + listener(data); + }; + } + + const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined'; + + var xhrAdapter = isXHRAdapterSupported && function (config) { return new Promise(function dispatchXhrRequest(resolve, reject) { - var requestData = config.data; - var requestHeaders = config.headers; - var responseType = config.responseType; + let requestData = config.data; + const requestHeaders = AxiosHeaders$1.from(config.headers).normalize(); + let {responseType, withXSRFToken} = config; + let onCanceled; + function done() { + if (config.cancelToken) { + config.cancelToken.unsubscribe(onCanceled); + } + + if (config.signal) { + config.signal.removeEventListener('abort', onCanceled); + } + } + + let contentType; - if (utils.isFormData(requestData)) { - delete requestHeaders['Content-Type']; // Let the browser set it + if (utils$1.isFormData(requestData)) { + if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) { + requestHeaders.setContentType(false); // Let the browser set it + } else if ((contentType = requestHeaders.getContentType()) !== false) { + // fix semicolon duplication issue for ReactNative FormData implementation + const [type, ...tokens] = contentType ? contentType.split(';').map(token => token.trim()).filter(Boolean) : []; + requestHeaders.setContentType([type || 'multipart/form-data', ...tokens].join('; ')); + } } - var request = new XMLHttpRequest(); + let request = new XMLHttpRequest(); // HTTP basic authentication if (config.auth) { - var username = config.auth.username || ''; - var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : ''; - requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); + const username = config.auth.username || ''; + const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : ''; + requestHeaders.set('Authorization', 'Basic ' + btoa(username + ':' + password)); } - var fullPath = buildFullPath(config.baseURL, config.url); + const fullPath = buildFullPath(config.baseURL, config.url); + request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true); // Set the request timeout in MS @@ -2028,19 +5611,27 @@ define((function () { 'use strict'; return; } // Prepare the response - var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; - var responseData = !responseType || responseType === 'text' || responseType === 'json' ? + const responseHeaders = AxiosHeaders$1.from( + 'getAllResponseHeaders' in request && request.getAllResponseHeaders() + ); + const responseData = !responseType || responseType === 'text' || responseType === 'json' ? request.responseText : request.response; - var response = { + const response = { data: responseData, status: request.status, statusText: request.statusText, headers: responseHeaders, - config: config, - request: request + config, + request }; - settle(resolve, reject, response); + settle(function _resolve(value) { + resolve(value); + done(); + }, function _reject(err) { + reject(err); + done(); + }, response); // Clean up request request = null; @@ -2075,7 +5666,7 @@ define((function () { 'use strict'; return; } - reject(createError('Request aborted', config, 'ECONNABORTED', request)); + reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request)); // Clean up request request = null; @@ -2085,7 +5676,7 @@ define((function () { 'use strict'; request.onerror = function handleError() { // Real errors are hidden from us by the browser // onerror should only fire if it's a network error - reject(createError('Network Error', config, null, request)); + reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request)); // Clean up request request = null; @@ -2093,14 +5684,15 @@ define((function () { 'use strict'; // Handle timeout request.ontimeout = function handleTimeout() { - var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded'; + let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded'; + const transitional = config.transitional || transitionalDefaults; if (config.timeoutErrorMessage) { timeoutErrorMessage = config.timeoutErrorMessage; } - reject(createError( + reject(new AxiosError( timeoutErrorMessage, + transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED, config, - config.transitional && config.transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED', request)); // Clean up request @@ -2110,32 +5702,31 @@ define((function () { 'use strict'; // Add xsrf header // This is only done if running in a standard browser environment. // Specifically not if we're in a web worker, or react-native. - if (utils.isStandardBrowserEnv()) { - // Add xsrf header - var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ? - cookies.read(config.xsrfCookieName) : - undefined; - - if (xsrfValue) { - requestHeaders[config.xsrfHeaderName] = xsrfValue; + if(platform.hasStandardBrowserEnv) { + withXSRFToken && utils$1.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(config)); + + if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(fullPath))) { + // Add xsrf header + const xsrfValue = config.xsrfHeaderName && config.xsrfCookieName && cookies.read(config.xsrfCookieName); + + if (xsrfValue) { + requestHeaders.set(config.xsrfHeaderName, xsrfValue); + } } } + // Remove Content-Type if data is undefined + requestData === undefined && requestHeaders.setContentType(null); + // Add headers to the request if ('setRequestHeader' in request) { - utils.forEach(requestHeaders, function setRequestHeader(val, key) { - if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') { - // Remove Content-Type if data is undefined - delete requestHeaders[key]; - } else { - // Otherwise add header to the request - request.setRequestHeader(key, val); - } + utils$1.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) { + request.setRequestHeader(key, val); }); } // Add withCredentials to request if needed - if (!utils.isUndefined(config.withCredentials)) { + if (!utils$1.isUndefined(config.withCredentials)) { request.withCredentials = !!config.withCredentials; } @@ -2146,232 +5737,158 @@ define((function () { 'use strict'; // Handle progress if needed if (typeof config.onDownloadProgress === 'function') { - request.addEventListener('progress', config.onDownloadProgress); + request.addEventListener('progress', progressEventReducer(config.onDownloadProgress, true)); } // Not all browsers support upload events if (typeof config.onUploadProgress === 'function' && request.upload) { - request.upload.addEventListener('progress', config.onUploadProgress); + request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress)); } - if (config.cancelToken) { + if (config.cancelToken || config.signal) { // Handle cancellation - config.cancelToken.promise.then(function onCanceled(cancel) { + // eslint-disable-next-line func-names + onCanceled = cancel => { if (!request) { return; } - + reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel); request.abort(); - reject(cancel); - // Clean up request request = null; - }); + }; + + config.cancelToken && config.cancelToken.subscribe(onCanceled); + if (config.signal) { + config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled); + } } - if (!requestData) { - requestData = null; + const protocol = parseProtocol(fullPath); + + if (protocol && platform.protocols.indexOf(protocol) === -1) { + reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config)); + return; } + // Send the request - request.send(requestData); + request.send(requestData || null); }); }; - var DEFAULT_CONTENT_TYPE = { - 'Content-Type': 'application/x-www-form-urlencoded' + const knownAdapters = { + http: httpAdapter, + xhr: xhrAdapter }; - function setContentTypeIfUnset(headers, value) { - if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) { - headers['Content-Type'] = value; - } - } - - function getDefaultAdapter() { - var adapter; - if (typeof XMLHttpRequest !== 'undefined') { - // For browsers use XHR adapter - adapter = xhr; - } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { - // For node use HTTP adapter - adapter = xhr; - } - return adapter; - } - - function stringifySafely(rawValue, parser, encoder) { - if (utils.isString(rawValue)) { + utils$1.forEach(knownAdapters, (fn, value) => { + if (fn) { try { - (parser || JSON.parse)(rawValue); - return utils.trim(rawValue); + Object.defineProperty(fn, 'name', {value}); } catch (e) { - if (e.name !== 'SyntaxError') { - throw e; - } + // eslint-disable-next-line no-empty } + Object.defineProperty(fn, 'adapterName', {value}); } + }); - return (encoder || JSON.stringify)(rawValue); - } - - var defaults = { - - transitional: { - silentJSONParsing: true, - forcedJSONParsing: true, - clarifyTimeoutError: false - }, - - adapter: getDefaultAdapter(), + const renderReason = (reason) => `- ${reason}`; - transformRequest: [function transformRequest(data, headers) { - normalizeHeaderName(headers, 'Accept'); - normalizeHeaderName(headers, 'Content-Type'); - - if (utils.isFormData(data) || - utils.isArrayBuffer(data) || - utils.isBuffer(data) || - utils.isStream(data) || - utils.isFile(data) || - utils.isBlob(data) - ) { - return data; - } - if (utils.isArrayBufferView(data)) { - return data.buffer; - } - if (utils.isURLSearchParams(data)) { - setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8'); - return data.toString(); - } - if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) { - setContentTypeIfUnset(headers, 'application/json'); - return stringifySafely(data); - } - return data; - }], + const isResolvedHandle = (adapter) => utils$1.isFunction(adapter) || adapter === null || adapter === false; - transformResponse: [function transformResponse(data) { - var transitional = this.transitional; - var silentJSONParsing = transitional && transitional.silentJSONParsing; - var forcedJSONParsing = transitional && transitional.forcedJSONParsing; - var strictJSONParsing = !silentJSONParsing && this.responseType === 'json'; + var adapters = { + getAdapter: (adapters) => { + adapters = utils$1.isArray(adapters) ? adapters : [adapters]; - if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) { - try { - return JSON.parse(data); - } catch (e) { - if (strictJSONParsing) { - if (e.name === 'SyntaxError') { - throw enhanceError(e, this, 'E_JSON_PARSE'); - } - throw e; - } - } - } + const {length} = adapters; + let nameOrAdapter; + let adapter; - return data; - }], + const rejectedReasons = {}; - /** - * A timeout in milliseconds to abort a request. If set to 0 (default) a - * timeout is not created. - */ - timeout: 0, + for (let i = 0; i < length; i++) { + nameOrAdapter = adapters[i]; + let id; - xsrfCookieName: 'XSRF-TOKEN', - xsrfHeaderName: 'X-XSRF-TOKEN', + adapter = nameOrAdapter; - maxContentLength: -1, - maxBodyLength: -1, + if (!isResolvedHandle(nameOrAdapter)) { + adapter = knownAdapters[(id = String(nameOrAdapter)).toLowerCase()]; - validateStatus: function validateStatus(status) { - return status >= 200 && status < 300; - } - }; + if (adapter === undefined) { + throw new AxiosError(`Unknown adapter '${id}'`); + } + } - defaults.headers = { - common: { - 'Accept': 'application/json, text/plain, */*' - } - }; + if (adapter) { + break; + } - utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) { - defaults.headers[method] = {}; - }); + rejectedReasons[id || '#' + i] = adapter; + } - utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { - defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE); - }); + if (!adapter) { - var defaults_1 = defaults; + const reasons = Object.entries(rejectedReasons) + .map(([id, state]) => `adapter ${id} ` + + (state === false ? 'is not supported by the environment' : 'is not available in the build') + ); - /** - * Transform the data for a request or a response - * - * @param {Object|String} data The data to be transformed - * @param {Array} headers The headers for the request or response - * @param {Array|Function} fns A single function or Array of functions - * @returns {*} The resulting transformed data - */ - var transformData = function transformData(data, headers, fns) { - var context = this || defaults_1; - /*eslint no-param-reassign:0*/ - utils.forEach(fns, function transform(fn) { - data = fn.call(context, data, headers); - }); + let s = length ? + (reasons.length > 1 ? 'since :\n' + reasons.map(renderReason).join('\n') : ' ' + renderReason(reasons[0])) : + 'as no adapter specified'; - return data; - }; + throw new AxiosError( + `There is no suitable adapter to dispatch the request ` + s, + 'ERR_NOT_SUPPORT' + ); + } - var isCancel = function isCancel(value) { - return !!(value && value.__CANCEL__); + return adapter; + }, + adapters: knownAdapters }; /** - * Throws a `Cancel` if cancellation has been requested. + * Throws a `CanceledError` if cancellation has been requested. + * + * @param {Object} config The config that is to be used for the request + * + * @returns {void} */ function throwIfCancellationRequested(config) { if (config.cancelToken) { config.cancelToken.throwIfRequested(); } + + if (config.signal && config.signal.aborted) { + throw new CanceledError(null, config); + } } /** * Dispatch a request to the server using the configured adapter. * * @param {object} config The config that is to be used for the request + * * @returns {Promise} The Promise to be fulfilled */ - var dispatchRequest = function dispatchRequest(config) { + function dispatchRequest(config) { throwIfCancellationRequested(config); - // Ensure headers exist - config.headers = config.headers || {}; + config.headers = AxiosHeaders$1.from(config.headers); // Transform request data config.data = transformData.call( config, - config.data, - config.headers, config.transformRequest ); - // Flatten headers - config.headers = utils.merge( - config.headers.common || {}, - config.headers[config.method] || {}, - config.headers - ); - - utils.forEach( - ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'], - function cleanHeaderConfig(method) { - delete config.headers[method]; - } - ); + if (['post', 'put', 'patch'].indexOf(config.method) !== -1) { + config.headers.setContentType('application/x-www-form-urlencoded', false); + } - var adapter = config.adapter || defaults_1.adapter; + const adapter = adapters.getAdapter(config.adapter || defaults$1.adapter); return adapter(config).then(function onAdapterResolution(response) { throwIfCancellationRequested(config); @@ -2379,11 +5896,12 @@ define((function () { 'use strict'; // Transform response data response.data = transformData.call( config, - response.data, - response.headers, - config.transformResponse + config.transformResponse, + response ); + response.headers = AxiosHeaders$1.from(response.headers); + return response; }, function onAdapterRejection(reason) { if (!isCancel(reason)) { @@ -2393,16 +5911,18 @@ define((function () { 'use strict'; if (reason && reason.response) { reason.response.data = transformData.call( config, - reason.response.data, - reason.response.headers, - config.transformResponse + config.transformResponse, + reason.response ); + reason.response.headers = AxiosHeaders$1.from(reason.response.headers); } } return Promise.reject(reason); }); - }; + } + + const headersToObject = (thing) => thing instanceof AxiosHeaders$1 ? { ...thing } : thing; /** * Config-specific merge-function which creates a new config-object @@ -2410,239 +5930,137 @@ define((function () { 'use strict'; * * @param {Object} config1 * @param {Object} config2 + * * @returns {Object} New object resulting from merging config2 to config1 */ - var mergeConfig = function mergeConfig(config1, config2) { + function mergeConfig(config1, config2) { // eslint-disable-next-line no-param-reassign config2 = config2 || {}; - var config = {}; - - var valueFromConfig2Keys = ['url', 'method', 'data']; - var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params']; - var defaultToConfig2Keys = [ - 'baseURL', 'transformRequest', 'transformResponse', 'paramsSerializer', - 'timeout', 'timeoutMessage', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName', - 'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'decompress', - 'maxContentLength', 'maxBodyLength', 'maxRedirects', 'transport', 'httpAgent', - 'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding' - ]; - var directMergeKeys = ['validateStatus']; - - function getMergedValue(target, source) { - if (utils.isPlainObject(target) && utils.isPlainObject(source)) { - return utils.merge(target, source); - } else if (utils.isPlainObject(source)) { - return utils.merge({}, source); - } else if (utils.isArray(source)) { + const config = {}; + + function getMergedValue(target, source, caseless) { + if (utils$1.isPlainObject(target) && utils$1.isPlainObject(source)) { + return utils$1.merge.call({caseless}, target, source); + } else if (utils$1.isPlainObject(source)) { + return utils$1.merge({}, source); + } else if (utils$1.isArray(source)) { return source.slice(); } return source; } - function mergeDeepProperties(prop) { - if (!utils.isUndefined(config2[prop])) { - config[prop] = getMergedValue(config1[prop], config2[prop]); - } else if (!utils.isUndefined(config1[prop])) { - config[prop] = getMergedValue(undefined, config1[prop]); + // eslint-disable-next-line consistent-return + function mergeDeepProperties(a, b, caseless) { + if (!utils$1.isUndefined(b)) { + return getMergedValue(a, b, caseless); + } else if (!utils$1.isUndefined(a)) { + return getMergedValue(undefined, a, caseless); } } - utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) { - if (!utils.isUndefined(config2[prop])) { - config[prop] = getMergedValue(undefined, config2[prop]); + // eslint-disable-next-line consistent-return + function valueFromConfig2(a, b) { + if (!utils$1.isUndefined(b)) { + return getMergedValue(undefined, b); } - }); - - utils.forEach(mergeDeepPropertiesKeys, mergeDeepProperties); + } - utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) { - if (!utils.isUndefined(config2[prop])) { - config[prop] = getMergedValue(undefined, config2[prop]); - } else if (!utils.isUndefined(config1[prop])) { - config[prop] = getMergedValue(undefined, config1[prop]); + // eslint-disable-next-line consistent-return + function defaultToConfig2(a, b) { + if (!utils$1.isUndefined(b)) { + return getMergedValue(undefined, b); + } else if (!utils$1.isUndefined(a)) { + return getMergedValue(undefined, a); } - }); + } - utils.forEach(directMergeKeys, function merge(prop) { + // eslint-disable-next-line consistent-return + function mergeDirectKeys(a, b, prop) { if (prop in config2) { - config[prop] = getMergedValue(config1[prop], config2[prop]); + return getMergedValue(a, b); } else if (prop in config1) { - config[prop] = getMergedValue(undefined, config1[prop]); + return getMergedValue(undefined, a); } - }); - - var axiosKeys = valueFromConfig2Keys - .concat(mergeDeepPropertiesKeys) - .concat(defaultToConfig2Keys) - .concat(directMergeKeys); + } - var otherKeys = Object - .keys(config1) - .concat(Object.keys(config2)) - .filter(function filterAxiosKeys(key) { - return axiosKeys.indexOf(key) === -1; - }); + const mergeMap = { + url: valueFromConfig2, + method: valueFromConfig2, + data: valueFromConfig2, + baseURL: defaultToConfig2, + transformRequest: defaultToConfig2, + transformResponse: defaultToConfig2, + paramsSerializer: defaultToConfig2, + timeout: defaultToConfig2, + timeoutMessage: defaultToConfig2, + withCredentials: defaultToConfig2, + withXSRFToken: defaultToConfig2, + adapter: defaultToConfig2, + responseType: defaultToConfig2, + xsrfCookieName: defaultToConfig2, + xsrfHeaderName: defaultToConfig2, + onUploadProgress: defaultToConfig2, + onDownloadProgress: defaultToConfig2, + decompress: defaultToConfig2, + maxContentLength: defaultToConfig2, + maxBodyLength: defaultToConfig2, + beforeRedirect: defaultToConfig2, + transport: defaultToConfig2, + httpAgent: defaultToConfig2, + httpsAgent: defaultToConfig2, + cancelToken: defaultToConfig2, + socketPath: defaultToConfig2, + responseEncoding: defaultToConfig2, + validateStatus: mergeDirectKeys, + headers: (a, b) => mergeDeepProperties(headersToObject(a), headersToObject(b), true) + }; - utils.forEach(otherKeys, mergeDeepProperties); + utils$1.forEach(Object.keys(Object.assign({}, config1, config2)), function computeConfigValue(prop) { + const merge = mergeMap[prop] || mergeDeepProperties; + const configValue = merge(config1[prop], config2[prop], prop); + (utils$1.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue); + }); return config; - }; + } - var name = "axios"; - var version = "0.21.4"; - var description = "Promise based HTTP client for the browser and node.js"; - var main = "index.js"; - var scripts = { - test: "grunt test", - start: "node ./sandbox/server.js", - build: "NODE_ENV=production grunt build", - preversion: "npm test", - version: "npm run build && grunt version && git add -A dist && git add CHANGELOG.md bower.json package.json", - postversion: "git push && git push --tags", - examples: "node ./examples/server.js", - coveralls: "cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", - fix: "eslint --fix lib/**/*.js" - }; - var repository = { - type: "git", - url: "https://github.com/axios/axios.git" - }; - var keywords = [ - "xhr", - "http", - "ajax", - "promise", - "node" - ]; - var author = "Matt Zabriskie"; - var license = "MIT"; - var bugs = { - url: "https://github.com/axios/axios/issues" - }; - var homepage = "https://axios-http.com"; - var devDependencies = { - coveralls: "^3.0.0", - "es6-promise": "^4.2.4", - grunt: "^1.3.0", - "grunt-banner": "^0.6.0", - "grunt-cli": "^1.2.0", - "grunt-contrib-clean": "^1.1.0", - "grunt-contrib-watch": "^1.0.0", - "grunt-eslint": "^23.0.0", - "grunt-karma": "^4.0.0", - "grunt-mocha-test": "^0.13.3", - "grunt-ts": "^6.0.0-beta.19", - "grunt-webpack": "^4.0.2", - "istanbul-instrumenter-loader": "^1.0.0", - "jasmine-core": "^2.4.1", - karma: "^6.3.2", - "karma-chrome-launcher": "^3.1.0", - "karma-firefox-launcher": "^2.1.0", - "karma-jasmine": "^1.1.1", - "karma-jasmine-ajax": "^0.1.13", - "karma-safari-launcher": "^1.0.0", - "karma-sauce-launcher": "^4.3.6", - "karma-sinon": "^1.0.5", - "karma-sourcemap-loader": "^0.3.8", - "karma-webpack": "^4.0.2", - "load-grunt-tasks": "^3.5.2", - minimist: "^1.2.0", - mocha: "^8.2.1", - sinon: "^4.5.0", - "terser-webpack-plugin": "^4.2.3", - typescript: "^4.0.5", - "url-search-params": "^0.10.0", - webpack: "^4.44.2", - "webpack-dev-server": "^3.11.0" - }; - var browser = { - "./lib/adapters/http.js": "./lib/adapters/xhr.js" - }; - var jsdelivr = "dist/axios.min.js"; - var unpkg = "dist/axios.min.js"; - var typings = "./index.d.ts"; - var dependencies = { - "follow-redirects": "^1.14.0" - }; - var bundlesize = [ - { - path: "./dist/axios.min.js", - threshold: "5kB" - } - ]; - var pkg = { - name: name, - version: version, - description: description, - main: main, - scripts: scripts, - repository: repository, - keywords: keywords, - author: author, - license: license, - bugs: bugs, - homepage: homepage, - devDependencies: devDependencies, - browser: browser, - jsdelivr: jsdelivr, - unpkg: unpkg, - typings: typings, - dependencies: dependencies, - bundlesize: bundlesize - }; - - var validators$1 = {}; + const VERSION = "1.6.8"; + + const validators$1 = {}; // eslint-disable-next-line func-names - ['object', 'boolean', 'number', 'function', 'string', 'symbol'].forEach(function(type, i) { + ['object', 'boolean', 'number', 'function', 'string', 'symbol'].forEach((type, i) => { validators$1[type] = function validator(thing) { return typeof thing === type || 'a' + (i < 1 ? 'n ' : ' ') + type; }; }); - var deprecatedWarnings = {}; - var currentVerArr = pkg.version.split('.'); - - /** - * Compare package versions - * @param {string} version - * @param {string?} thanVersion - * @returns {boolean} - */ - function isOlderVersion(version, thanVersion) { - var pkgVersionArr = thanVersion ? thanVersion.split('.') : currentVerArr; - var destVer = version.split('.'); - for (var i = 0; i < 3; i++) { - if (pkgVersionArr[i] > destVer[i]) { - return true; - } else if (pkgVersionArr[i] < destVer[i]) { - return false; - } - } - return false; - } + const deprecatedWarnings = {}; /** * Transitional option validator - * @param {function|boolean?} validator - * @param {string?} version - * @param {string} message + * + * @param {function|boolean?} validator - set to false if the transitional option has been removed + * @param {string?} version - deprecated version / removed since version + * @param {string?} message - some message with additional info + * * @returns {function} */ validators$1.transitional = function transitional(validator, version, message) { - var isDeprecated = version && isOlderVersion(version); - function formatMessage(opt, desc) { - return '[Axios v' + pkg.version + '] Transitional option \'' + opt + '\'' + desc + (message ? '. ' + message : ''); + return '[Axios v' + VERSION + '] Transitional option \'' + opt + '\'' + desc + (message ? '. ' + message : ''); } // eslint-disable-next-line func-names - return function(value, opt, opts) { + return (value, opt, opts) => { if (validator === false) { - throw new Error(formatMessage(opt, ' has been removed in ' + version)); + throw new AxiosError( + formatMessage(opt, ' has been removed' + (version ? ' in ' + version : '')), + AxiosError.ERR_DEPRECATED + ); } - if (isDeprecated && !deprecatedWarnings[opt]) { + if (version && !deprecatedWarnings[opt]) { deprecatedWarnings[opt] = true; // eslint-disable-next-line no-console console.warn( @@ -2659,251 +6077,374 @@ define((function () { 'use strict'; /** * Assert object's properties type + * * @param {object} options * @param {object} schema * @param {boolean?} allowUnknown + * + * @returns {object} */ function assertOptions(options, schema, allowUnknown) { if (typeof options !== 'object') { - throw new TypeError('options must be an object'); + throw new AxiosError('options must be an object', AxiosError.ERR_BAD_OPTION_VALUE); } - var keys = Object.keys(options); - var i = keys.length; + const keys = Object.keys(options); + let i = keys.length; while (i-- > 0) { - var opt = keys[i]; - var validator = schema[opt]; + const opt = keys[i]; + const validator = schema[opt]; if (validator) { - var value = options[opt]; - var result = value === undefined || validator(value, opt, options); + const value = options[opt]; + const result = value === undefined || validator(value, opt, options); if (result !== true) { - throw new TypeError('option ' + opt + ' must be ' + result); + throw new AxiosError('option ' + opt + ' must be ' + result, AxiosError.ERR_BAD_OPTION_VALUE); } continue; } if (allowUnknown !== true) { - throw Error('Unknown option ' + opt); + throw new AxiosError('Unknown option ' + opt, AxiosError.ERR_BAD_OPTION); } } } var validator = { - isOlderVersion: isOlderVersion, - assertOptions: assertOptions, + assertOptions, validators: validators$1 }; - var validators = validator.validators; + const validators = validator.validators; + /** * Create a new instance of Axios * * @param {Object} instanceConfig The default config for the instance - */ - function Axios(instanceConfig) { - this.defaults = instanceConfig; - this.interceptors = { - request: new InterceptorManager_1(), - response: new InterceptorManager_1() - }; - } - - /** - * Dispatch a request * - * @param {Object} config The config specific for this request (merged with this.defaults) + * @return {Axios} A new instance of Axios */ - Axios.prototype.request = function request(config) { - /*eslint no-param-reassign:0*/ - // Allow for axios('example/url'[, config]) a la fetch API - if (typeof config === 'string') { - config = arguments[1] || {}; - config.url = arguments[0]; - } else { - config = config || {}; + class Axios { + constructor(instanceConfig) { + this.defaults = instanceConfig; + this.interceptors = { + request: new InterceptorManager$1(), + response: new InterceptorManager$1() + }; } - config = mergeConfig(this.defaults, config); + /** + * Dispatch a request + * + * @param {String|Object} configOrUrl The config specific for this request (merged with this.defaults) + * @param {?Object} config + * + * @returns {Promise} The Promise to be fulfilled + */ + async request(configOrUrl, config) { + try { + return await this._request(configOrUrl, config); + } catch (err) { + if (err instanceof Error) { + let dummy; - // Set config.method - if (config.method) { - config.method = config.method.toLowerCase(); - } else if (this.defaults.method) { - config.method = this.defaults.method.toLowerCase(); - } else { - config.method = 'get'; - } + Error.captureStackTrace ? Error.captureStackTrace(dummy = {}) : (dummy = new Error()); + + // slice off the Error: ... line + const stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : ''; - var transitional = config.transitional; + if (!err.stack) { + err.stack = stack; + // match without the 2 top stack lines + } else if (stack && !String(err.stack).endsWith(stack.replace(/^.+\n.+\n/, ''))) { + err.stack += '\n' + stack; + } + } - if (transitional !== undefined) { - validator.assertOptions(transitional, { - silentJSONParsing: validators.transitional(validators.boolean, '1.0.0'), - forcedJSONParsing: validators.transitional(validators.boolean, '1.0.0'), - clarifyTimeoutError: validators.transitional(validators.boolean, '1.0.0') - }, false); + throw err; + } } - // filter out skipped interceptors - var requestInterceptorChain = []; - var synchronousRequestInterceptors = true; - this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { - if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) { - return; + _request(configOrUrl, config) { + /*eslint no-param-reassign:0*/ + // Allow for axios('example/url'[, config]) a la fetch API + if (typeof configOrUrl === 'string') { + config = config || {}; + config.url = configOrUrl; + } else { + config = configOrUrl || {}; } - synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous; + config = mergeConfig(this.defaults, config); - requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); - }); + const {transitional, paramsSerializer, headers} = config; - var responseInterceptorChain = []; - this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { - responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); - }); + if (transitional !== undefined) { + validator.assertOptions(transitional, { + silentJSONParsing: validators.transitional(validators.boolean), + forcedJSONParsing: validators.transitional(validators.boolean), + clarifyTimeoutError: validators.transitional(validators.boolean) + }, false); + } + + if (paramsSerializer != null) { + if (utils$1.isFunction(paramsSerializer)) { + config.paramsSerializer = { + serialize: paramsSerializer + }; + } else { + validator.assertOptions(paramsSerializer, { + encode: validators.function, + serialize: validators.function + }, true); + } + } + + // Set config.method + config.method = (config.method || this.defaults.method || 'get').toLowerCase(); + + // Flatten headers + let contextHeaders = headers && utils$1.merge( + headers.common, + headers[config.method] + ); + + headers && utils$1.forEach( + ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'], + (method) => { + delete headers[method]; + } + ); + + config.headers = AxiosHeaders$1.concat(contextHeaders, headers); + + // filter out skipped interceptors + const requestInterceptorChain = []; + let synchronousRequestInterceptors = true; + this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { + if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) { + return; + } + + synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous; + + requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); + }); + + const responseInterceptorChain = []; + this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { + responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); + }); + + let promise; + let i = 0; + let len; - var promise; + if (!synchronousRequestInterceptors) { + const chain = [dispatchRequest.bind(this), undefined]; + chain.unshift.apply(chain, requestInterceptorChain); + chain.push.apply(chain, responseInterceptorChain); + len = chain.length; - if (!synchronousRequestInterceptors) { - var chain = [dispatchRequest, undefined]; + promise = Promise.resolve(config); - Array.prototype.unshift.apply(chain, requestInterceptorChain); - chain = chain.concat(responseInterceptorChain); + while (i < len) { + promise = promise.then(chain[i++], chain[i++]); + } - promise = Promise.resolve(config); - while (chain.length) { - promise = promise.then(chain.shift(), chain.shift()); + return promise; } - return promise; - } + len = requestInterceptorChain.length; + + let newConfig = config; + i = 0; + + while (i < len) { + const onFulfilled = requestInterceptorChain[i++]; + const onRejected = requestInterceptorChain[i++]; + try { + newConfig = onFulfilled(newConfig); + } catch (error) { + onRejected.call(this, error); + break; + } + } - var newConfig = config; - while (requestInterceptorChain.length) { - var onFulfilled = requestInterceptorChain.shift(); - var onRejected = requestInterceptorChain.shift(); try { - newConfig = onFulfilled(newConfig); + promise = dispatchRequest.call(this, newConfig); } catch (error) { - onRejected(error); - break; + return Promise.reject(error); } - } - try { - promise = dispatchRequest(newConfig); - } catch (error) { - return Promise.reject(error); - } + i = 0; + len = responseInterceptorChain.length; - while (responseInterceptorChain.length) { - promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift()); - } + while (i < len) { + promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]); + } - return promise; - }; + return promise; + } - Axios.prototype.getUri = function getUri(config) { - config = mergeConfig(this.defaults, config); - return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, ''); - }; + getUri(config) { + config = mergeConfig(this.defaults, config); + const fullPath = buildFullPath(config.baseURL, config.url); + return buildURL(fullPath, config.params, config.paramsSerializer); + } + } // Provide aliases for supported request methods - utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) { + utils$1.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) { /*eslint func-names:0*/ Axios.prototype[method] = function(url, config) { return this.request(mergeConfig(config || {}, { - method: method, - url: url, + method, + url, data: (config || {}).data })); }; }); - utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { + utils$1.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { /*eslint func-names:0*/ - Axios.prototype[method] = function(url, data, config) { - return this.request(mergeConfig(config || {}, { - method: method, - url: url, - data: data - })); - }; - }); - - var Axios_1 = Axios; - /** - * A `Cancel` is an object that is thrown when an operation is canceled. - * - * @class - * @param {string=} message The message. - */ - function Cancel(message) { - this.message = message; - } + function generateHTTPMethod(isForm) { + return function httpMethod(url, data, config) { + return this.request(mergeConfig(config || {}, { + method, + headers: isForm ? { + 'Content-Type': 'multipart/form-data' + } : {}, + url, + data + })); + }; + } - Cancel.prototype.toString = function toString() { - return 'Cancel' + (this.message ? ': ' + this.message : ''); - }; + Axios.prototype[method] = generateHTTPMethod(); - Cancel.prototype.__CANCEL__ = true; + Axios.prototype[method + 'Form'] = generateHTTPMethod(true); + }); - var Cancel_1 = Cancel; + var Axios$1 = Axios; /** * A `CancelToken` is an object that can be used to request cancellation of an operation. * - * @class * @param {Function} executor The executor function. + * + * @returns {CancelToken} */ - function CancelToken(executor) { - if (typeof executor !== 'function') { - throw new TypeError('executor must be a function.'); + class CancelToken { + constructor(executor) { + if (typeof executor !== 'function') { + throw new TypeError('executor must be a function.'); + } + + let resolvePromise; + + this.promise = new Promise(function promiseExecutor(resolve) { + resolvePromise = resolve; + }); + + const token = this; + + // eslint-disable-next-line func-names + this.promise.then(cancel => { + if (!token._listeners) return; + + let i = token._listeners.length; + + while (i-- > 0) { + token._listeners[i](cancel); + } + token._listeners = null; + }); + + // eslint-disable-next-line func-names + this.promise.then = onfulfilled => { + let _resolve; + // eslint-disable-next-line func-names + const promise = new Promise(resolve => { + token.subscribe(resolve); + _resolve = resolve; + }).then(onfulfilled); + + promise.cancel = function reject() { + token.unsubscribe(_resolve); + }; + + return promise; + }; + + executor(function cancel(message, config, request) { + if (token.reason) { + // Cancellation has already been requested + return; + } + + token.reason = new CanceledError(message, config, request); + resolvePromise(token.reason); + }); } - var resolvePromise; - this.promise = new Promise(function promiseExecutor(resolve) { - resolvePromise = resolve; - }); + /** + * Throws a `CanceledError` if cancellation has been requested. + */ + throwIfRequested() { + if (this.reason) { + throw this.reason; + } + } + + /** + * Subscribe to the cancel signal + */ - var token = this; - executor(function cancel(message) { - if (token.reason) { - // Cancellation has already been requested + subscribe(listener) { + if (this.reason) { + listener(this.reason); return; } - token.reason = new Cancel_1(message); - resolvePromise(token.reason); - }); - } + if (this._listeners) { + this._listeners.push(listener); + } else { + this._listeners = [listener]; + } + } - /** - * Throws a `Cancel` if cancellation has been requested. - */ - CancelToken.prototype.throwIfRequested = function throwIfRequested() { - if (this.reason) { - throw this.reason; + /** + * Unsubscribe from the cancel signal + */ + + unsubscribe(listener) { + if (!this._listeners) { + return; + } + const index = this._listeners.indexOf(listener); + if (index !== -1) { + this._listeners.splice(index, 1); + } } - }; - /** - * Returns an object that contains a new `CancelToken` and a function that, when called, - * cancels the `CancelToken`. - */ - CancelToken.source = function source() { - var cancel; - var token = new CancelToken(function executor(c) { - cancel = c; - }); - return { - token: token, - cancel: cancel - }; - }; + /** + * Returns an object that contains a new `CancelToken` and a function that, when called, + * cancels the `CancelToken`. + */ + static source() { + let cancel; + const token = new CancelToken(function executor(c) { + cancel = c; + }); + return { + token, + cancel + }; + } + } - var CancelToken_1 = CancelToken; + var CancelToken$1 = CancelToken; /** * Syntactic sugar for invoking a function and expanding an array for arguments. @@ -2923,77 +6464,168 @@ define((function () { 'use strict'; * ``` * * @param {Function} callback + * * @returns {Function} */ - var spread = function spread(callback) { + function spread(callback) { return function wrap(arr) { return callback.apply(null, arr); }; - }; + } /** * Determines whether the payload is an error thrown by Axios * * @param {*} payload The value to test + * * @returns {boolean} True if the payload is an error thrown by Axios, otherwise false */ - var isAxiosError = function isAxiosError(payload) { - return (typeof payload === 'object') && (payload.isAxiosError === true); + function isAxiosError(payload) { + return utils$1.isObject(payload) && (payload.isAxiosError === true); + } + + const HttpStatusCode = { + Continue: 100, + SwitchingProtocols: 101, + Processing: 102, + EarlyHints: 103, + Ok: 200, + Created: 201, + Accepted: 202, + NonAuthoritativeInformation: 203, + NoContent: 204, + ResetContent: 205, + PartialContent: 206, + MultiStatus: 207, + AlreadyReported: 208, + ImUsed: 226, + MultipleChoices: 300, + MovedPermanently: 301, + Found: 302, + SeeOther: 303, + NotModified: 304, + UseProxy: 305, + Unused: 306, + TemporaryRedirect: 307, + PermanentRedirect: 308, + BadRequest: 400, + Unauthorized: 401, + PaymentRequired: 402, + Forbidden: 403, + NotFound: 404, + MethodNotAllowed: 405, + NotAcceptable: 406, + ProxyAuthenticationRequired: 407, + RequestTimeout: 408, + Conflict: 409, + Gone: 410, + LengthRequired: 411, + PreconditionFailed: 412, + PayloadTooLarge: 413, + UriTooLong: 414, + UnsupportedMediaType: 415, + RangeNotSatisfiable: 416, + ExpectationFailed: 417, + ImATeapot: 418, + MisdirectedRequest: 421, + UnprocessableEntity: 422, + Locked: 423, + FailedDependency: 424, + TooEarly: 425, + UpgradeRequired: 426, + PreconditionRequired: 428, + TooManyRequests: 429, + RequestHeaderFieldsTooLarge: 431, + UnavailableForLegalReasons: 451, + InternalServerError: 500, + NotImplemented: 501, + BadGateway: 502, + ServiceUnavailable: 503, + GatewayTimeout: 504, + HttpVersionNotSupported: 505, + VariantAlsoNegotiates: 506, + InsufficientStorage: 507, + LoopDetected: 508, + NotExtended: 510, + NetworkAuthenticationRequired: 511, }; + Object.entries(HttpStatusCode).forEach(([key, value]) => { + HttpStatusCode[value] = key; + }); + + var HttpStatusCode$1 = HttpStatusCode; + /** * Create an instance of Axios * * @param {Object} defaultConfig The default config for the instance - * @return {Axios} A new instance of Axios + * + * @returns {Axios} A new instance of Axios */ function createInstance(defaultConfig) { - var context = new Axios_1(defaultConfig); - var instance = bind(Axios_1.prototype.request, context); + const context = new Axios$1(defaultConfig); + const instance = bind(Axios$1.prototype.request, context); // Copy axios.prototype to instance - utils.extend(instance, Axios_1.prototype, context); + utils$1.extend(instance, Axios$1.prototype, context, {allOwnKeys: true}); // Copy context to instance - utils.extend(instance, context); + utils$1.extend(instance, context, null, {allOwnKeys: true}); + + // Factory for creating new instances + instance.create = function create(instanceConfig) { + return createInstance(mergeConfig(defaultConfig, instanceConfig)); + }; return instance; } // Create the default instance to be exported - var axios$1 = createInstance(defaults_1); + const axios = createInstance(defaults$1); // Expose Axios class to allow class inheritance - axios$1.Axios = Axios_1; - - // Factory for creating new instances - axios$1.create = function create(instanceConfig) { - return createInstance(mergeConfig(axios$1.defaults, instanceConfig)); - }; + axios.Axios = Axios$1; // Expose Cancel & CancelToken - axios$1.Cancel = Cancel_1; - axios$1.CancelToken = CancelToken_1; - axios$1.isCancel = isCancel; + axios.CanceledError = CanceledError; + axios.CancelToken = CancelToken$1; + axios.isCancel = isCancel; + axios.VERSION = VERSION; + axios.toFormData = toFormData; + + // Expose AxiosError class + axios.AxiosError = AxiosError; + + // alias for CanceledError for backward compatibility + axios.Cancel = axios.CanceledError; // Expose all/spread - axios$1.all = function all(promises) { + axios.all = function all(promises) { return Promise.all(promises); }; - axios$1.spread = spread; + + axios.spread = spread; // Expose isAxiosError - axios$1.isAxiosError = isAxiosError; + axios.isAxiosError = isAxiosError; - var axios_1 = axios$1; + // Expose mergeConfig + axios.mergeConfig = mergeConfig; - // Allow use of default import syntax in TypeScript - var _default = axios$1; - axios_1.default = _default; + axios.AxiosHeaders = AxiosHeaders$1; - var axios = axios_1; + axios.formToJSON = thing => formDataToJSON(utils$1.isHTMLForm(thing) ? new FormData(thing) : thing); + + axios.getAdapter = adapters.getAdapter; + + axios.HttpStatusCode = HttpStatusCode$1; + + axios.default = axios; + + // this module should only have a default export + var axios$1 = axios; - // var script = { data: () => ({ mode: "edit", @@ -3002,15 +6634,13 @@ define((function () { 'use strict'; config: null, docEditor: null }), - created() { this.mode = this.$route.params.mode; this.fileId = this.$route.params.fileId; this.filePath = this.$route.params.filePath; }, - - methods: { ...mapActions(["showMessage"]), - + methods: { + ...mapActions(["showMessage"]), messageDisplay(desc, status = "danger", title = "") { this.showMessage({ title: title, @@ -3021,24 +6651,20 @@ define((function () { 'use strict'; } }); }, - onRequestClose() { let params = { - item: null + driveAliasAndItem: null }; - if (this.currentFolder) { - params.item = this.currentFolder.path; + params.driveAliasAndItem = "personal/" + this.currentFolder.ownerId + this.currentFolder.path; } - this.$router.push({ - name: "files-personal", + name: "files-spaces-generic", params }); }, - getDocumentServerUrl() { - return axios({ + return axios$1({ method: "GET", url: this.configuration.server + "ocs/v2.php/apps/onlyoffice/api/v1/settings/docserver", headers: { @@ -3048,19 +6674,16 @@ define((function () { 'use strict'; if (!response.data.documentServerUrl) { throw "ONLYOFFICE app is not configured. Please contact admin"; } - return response.data.documentServerUrl; }); }, - create() { return new Promise((resolve, reject) => { if (this.mode != "create") { resolve(); return; } - - axios({ + axios$1({ method: "GET", url: this.configuration.server + "ocs/v2.php/apps/onlyoffice/api/v1/empty/" + this.fileId, headers: { @@ -3071,14 +6694,12 @@ define((function () { 'use strict'; reject(response.data.error); return; } - resolve(); }); }); }, - initConfig() { - return axios({ + return axios$1({ method: "GET", url: this.configuration.server + "ocs/v2.php/apps/onlyoffice/api/v1/config/" + this.fileId, headers: { @@ -3088,7 +6709,6 @@ define((function () { 'use strict'; if (response.data.error) { throw response.data.error; } - this.config = response.data; let events = []; events["onRequestClose"] = this.onRequestClose; @@ -3097,12 +6717,11 @@ define((function () { 'use strict'; this.docEditor = new DocsAPI.DocEditor("iframeEditor", this.config); }); } - }, - computed: { ...mapGetters(["getToken", "configuration", "apps"]), + computed: { + ...mapGetters(["getToken", "configuration", "apps"]), ...mapGetters("Files", ["currentFolder"]) }, - mounted() { this.create().then(() => { return this.getDocumentServerUrl(); @@ -3117,196 +6736,73 @@ define((function () { 'use strict'; this.onRequestClose(); }); } - }; - function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) { - if (typeof shadowMode !== 'boolean') { - createInjectorSSR = createInjector; - createInjector = shadowMode; - shadowMode = false; - } - // Vue.extend constructor export interop. - const options = typeof script === 'function' ? script.options : script; - // render functions - if (template && template.render) { - options.render = template.render; - options.staticRenderFns = template.staticRenderFns; - options._compiled = true; - // functional template - if (isFunctionalTemplate) { - options.functional = true; - } - } - // scopedId - if (scopeId) { - options._scopeId = scopeId; - } - let hook; - if (moduleIdentifier) { - // server build - hook = function (context) { - // 2.3 injection - context = - context || // cached call - (this.$vnode && this.$vnode.ssrContext) || // stateful - (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional - // 2.2 with runInNewContext: true - if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { - context = __VUE_SSR_CONTEXT__; - } - // inject component styles - if (style) { - style.call(this, createInjectorSSR(context)); - } - // register component module identifier for async chunk inference - if (context && context._registeredComponents) { - context._registeredComponents.add(moduleIdentifier); - } - }; - // used by ssr in case component is cached and beforeCreate - // never gets called - options._ssrRegister = hook; - } - else if (style) { - hook = shadowMode - ? function (context) { - style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot)); - } - : function (context) { - style.call(this, createInjector(context)); - }; - } - if (hook) { - if (options.functional) { - // register for functional component in vue file - const originalRender = options.render; - options.render = function renderWithStyleInjection(h, context) { - hook.call(context); - return originalRender(h, context); - }; - } - else { - // inject component registration as beforeCreate hook - const existing = options.beforeCreate; - options.beforeCreate = existing ? [].concat(existing, hook) : [hook]; - } - } - return script; - } - - const isOldIE = typeof navigator !== 'undefined' && - /msie [6-9]\\b/.test(navigator.userAgent.toLowerCase()); - function createInjector(context) { - return (id, style) => addStyle(id, style); - } - let HEAD; - const styles = {}; - function addStyle(id, css) { - const group = isOldIE ? css.media || 'default' : id; - const style = styles[group] || (styles[group] = { ids: new Set(), styles: [] }); - if (!style.ids.has(id)) { - style.ids.add(id); - let code = css.source; - if (css.map) { - // https://developer.chrome.com/devtools/docs/javascript-debugging - // this makes source maps inside style tags work properly in Chrome - code += '\n/*# sourceURL=' + css.map.sources[0] + ' */'; - // http://stackoverflow.com/a/26603875 - code += - '\n/*# sourceMappingURL=data:application/json;base64,' + - btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) + - ' */'; - } - if (!style.element) { - style.element = document.createElement('style'); - style.element.type = 'text/css'; - if (css.media) - style.element.setAttribute('media', css.media); - if (HEAD === undefined) { - HEAD = document.head || document.getElementsByTagName('head')[0]; - } - HEAD.appendChild(style.element); - } - if ('styleSheet' in style.element) { - style.styles.push(code); - style.element.styleSheet.cssText = style.styles - .filter(Boolean) - .join('\n'); - } - else { - const index = style.ids.size - 1; - const textNode = document.createTextNode(code); - const nodes = style.element.childNodes; - if (nodes[index]) - style.element.removeChild(nodes[index]); - if (nodes.length) - style.element.insertBefore(textNode, nodes[index]); - else - style.element.appendChild(textNode); - } - } + const _hoisted_1 = /*#__PURE__*/vue.createElementVNode("div", { + id: "app" + }, [/*#__PURE__*/vue.createElementVNode("div", { + id: "iframeEditor" + })], -1 /* HOISTED */); + const _hoisted_2 = [_hoisted_1]; + function render(_ctx, _cache, $props, $setup, $data, $options) { + return vue.openBlock(), vue.createElementBlock("main", null, _hoisted_2); } - /* script */ - const __vue_script__ = script; + function styleInject(css, ref) { + if ( ref === void 0 ) ref = {}; + var insertAt = ref.insertAt; - /* template */ - var __vue_render__ = function () { - var _vm = this; - var _h = _vm.$createElement; - _vm._self._c || _h; - return _vm._m(0) - }; - var __vue_staticRenderFns__ = [ - function () { - var _vm = this; - var _h = _vm.$createElement; - var _c = _vm._self._c || _h; - return _c("main", [ - _c("div", { attrs: { id: "app" } }, [ - _c("div", { attrs: { id: "iframeEditor" } }), - ]), - ]) - }, - ]; - __vue_render__._withStripped = true; + if (!css || typeof document === 'undefined') { return; } - /* style */ - const __vue_inject_styles__ = function (inject) { - if (!inject) return - inject("data-v-404880d3_0", { source: "\n#app {\n width: 100%;\n}\n#app > iframe {\n position: absolute;\n}\n", map: {"version":3,"sources":["D:\\onlyoffice-owncloud-web\\src\\editor.vue"],"names":[],"mappings":";AAoJA;IACA,WAAA;AACA;AACA;IACA,kBAAA;AACA","file":"editor.vue","sourcesContent":["\r\n\r\n\r\n\r\n"]}, media: undefined }); + var head = document.head || document.getElementsByTagName('head')[0]; + var style = document.createElement('style'); + style.type = 'text/css'; - }; - /* scoped */ - const __vue_scope_id__ = undefined; - /* module identifier */ - const __vue_module_identifier__ = undefined; - /* functional template */ - const __vue_is_functional_template__ = false; - /* style inject SSR */ - - /* style inject shadow dom */ - + if (insertAt === 'top') { + if (head.firstChild) { + head.insertBefore(style, head.firstChild); + } else { + head.appendChild(style); + } + } else { + head.appendChild(style); + } - - const __vue_component__ = /*#__PURE__*/normalizeComponent( - { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, - __vue_inject_styles__, - __vue_script__, - __vue_scope_id__, - __vue_is_functional_template__, - __vue_module_identifier__, - false, - createInjector, - undefined, - undefined - ); + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + } + + var css_248z = "\n#app {\r\n width: 100%;\n}\n#app > iframe {\r\n position: fixed;\r\n height: calc(100vh - 52px);\r\n right: 0;\n}\r\n"; + styleInject(css_248z); + + script.render = render; + script.__file = "src/editor.vue"; + + /** + * + * (c) Copyright Ascensio System SIA 2023 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ const routes = [{ path: "/editor/:fileId/:filePath/:mode", components: { - fullscreen: __vue_component__ + fullscreen: script }, name: "editor", meta: { @@ -3326,10 +6822,9 @@ define((function () { 'use strict'; menuTitle($gettext) { return $gettext("Document"); }, - icon: "x-office-document" }, - routes: ["files-personal", "files-favorites", "files-shared-with-others", "files-shared-with-me", "files-public-list"] + routes: ["files-spaces-generic", "files-common-favorites", "files-shares-with-others", "files-shares-with-me", "files-trash-generic", "files-public-link"] }, { extension: "xlsx", routeName: "onlyoffice-editor", @@ -3338,10 +6833,9 @@ define((function () { 'use strict'; menuTitle($gettext) { return $gettext("Spreadsheet"); }, - icon: "x-office-spreadsheet" }, - routes: ["files-personal", "files-favorites", "files-shared-with-others", "files-shared-with-me", "files-public-list"] + routes: ["files-spaces-generic", "files-common-favorites", "files-shares-with-others", "files-shares-with-me", "files-trash-generic", "files-public-link"] }, { extension: "pptx", routeName: "onlyoffice-editor", @@ -3350,10 +6844,9 @@ define((function () { 'use strict'; menuTitle($gettext) { return $gettext("Presentation"); }, - icon: "x-office-presentation" }, - routes: ["files-personal", "files-favorites", "files-shared-with-others", "files-shared-with-me", "files-public-list"] + routes: ["files-spaces-generic", "files-common-favorites", "files-shares-with-others", "files-shares-with-me", "files-trash-generic", "files-public-link"] }] }; var app = { diff --git a/l10n/bg_BG.js b/l10n/bg_BG.js index 1fc558a9..4362a217 100644 --- a/l10n/bg_BG.js +++ b/l10n/bg_BG.js @@ -45,11 +45,10 @@ OC.L10N.register( "View details" : "Виж детайли", "Save" : "Запази", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Смесеното активно съдържание е недопустимо. За ONLYOFFICE Docs е необходимо използването на HTTPS-адрес.", - "Restrict access to editors to following groups" : "Разреши достъп до редакторите само за тези групи", + "Allow the following groups to access the editors" : "Разреши достъп до редакторите само за тези групи", "review" : "преглед", "form filling" : "попълване на формуляр", "comment" : "коментар", - "custom filter" : "персонализиран филтър", "download" : "изтегли", "Server settings" : "Настройки на сървъра", "Common settings" : "Общи настройки", @@ -100,12 +99,8 @@ OC.L10N.register( "Notification sent successfully": "Успешно изпратено известие", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s, Ви спомена във %2\$s: \"%3\$s\".", "Choose a format to convert {fileName}": "Изберете формат за конвертиране {fileName}", - "Form template": "Шаблон на формуляр", - "Form template from existing text file": "Шаблон на формуляр от съществуващ текстов файл", "Create form": "Създайте формуляр", "Fill in form in ONLYOFFICE": "Попълнете формуляр в ONLYOFFICE", - "Create new Form template": "Създайте нов шаблон на формуляр", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Молим да актуализирате ONLYOFFICE Docs към версия 7.0, за да работи с онлайн формуляри за попълване", "Security": "Сигурност", "Light": "Светла", "Classic Light": "Класически светла", diff --git a/l10n/bg_BG.json b/l10n/bg_BG.json index b6666515..0ff9686e 100644 --- a/l10n/bg_BG.json +++ b/l10n/bg_BG.json @@ -43,11 +43,10 @@ "View details" : "Виж детайли", "Save" : "Запази", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Смесеното активно съдържание е недопустимо. За ONLYOFFICE Docs е необходимо използването на HTTPS-адрес.", - "Restrict access to editors to following groups" : "Разреши достъп до редакторите само за тези групи", + "Allow the following groups to access the editors" : "Разреши достъп до редакторите само за тези групи", "review" : "преглед", "form filling" : "попълване на формуляр", "comment" : "коментар", - "custom filter" : "персонализиран филтър", "download" : "изтегли", "Server settings" : "Настройки на сървъра", "Common settings" : "Общи настройки", @@ -98,12 +97,8 @@ "Notification sent successfully": "Успешно изпратено известие", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s, Ви спомена във %2$s: \"%3$s\".", "Choose a format to convert {fileName}": "Изберете формат за конвертиране {fileName}", - "Form template": "Шаблон на формуляр", - "Form template from existing text file": "Шаблон на формуляр от съществуващ текстов файл", "Create form": "Създайте формуляр", "Fill in form in ONLYOFFICE": "Попълнете формуляр в ONLYOFFICE", - "Create new Form template": "Създайте нов шаблон на формуляр", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Молим да актуализирате ONLYOFFICE Docs към версия 7.0, за да работи с онлайн формуляри за попълване", "Security": "Сигурност", "Light": "Светла", "Classic Light": "Класически светла", diff --git a/l10n/ca.js b/l10n/ca.js index c230910f..872e27bd 100644 --- a/l10n/ca.js +++ b/l10n/ca.js @@ -45,11 +45,9 @@ OC.L10N.register( "View details" : "Veure detalls", "Save" : "Guardar", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Contingut Mixt Actiu no està permès. Es requereix la direcció HTTPS per a ONLYOFFICE Docs.", - "Restrict access to editors to following groups" : "Restringir l'accés a editors a següents grups", "review" : "", "form filling" : "", "comment" : "", - "custom filter" : "", "download" : "", "Server settings" : "Ajustos de servidor", "Common settings" : "Ajustos comuns", @@ -100,12 +98,8 @@ OC.L10N.register( "Notification sent successfully": "Notificació enviada correctament", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s ha esmentat en %2\$s: \"%3\$s\".", "Choose a format to convert {fileName}": "Triï un format per a convertir {fileName}", - "Form template": "Plantilla de formulari", - "Form template from existing text file": "Plantilla de formulari d'un fitxer de text existent", "Create form": "Crear formulari", "Fill in form in ONLYOFFICE": "Omplir el formulari en ONLYOFFICE", - "Create new Form template": "Crear nova plantilla de formulari", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Si us plau, actualitzi ONLYOFFICE Docs a la versió 7.0 per a poder treballar amb formularis emplenables en línia", "Security": "Seguretat", "Light": "Llum", "Classic Light": "Llum clàssica", diff --git a/l10n/ca.json b/l10n/ca.json index f15dba9e..c468b51d 100644 --- a/l10n/ca.json +++ b/l10n/ca.json @@ -43,11 +43,9 @@ "View details" : "Veure detalls", "Save" : "Guardar", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Contingut Mixt Actiu no està permès. Es requereix la direcció HTTPS per a ONLYOFFICE Docs.", - "Restrict access to editors to following groups" : "Restringir l'accés a editors a següents grups", "review" : "", "form filling" : "", "comment" : "", - "custom filter" : "", "download" : "", "Server settings" : "Ajustos de servidor", "Common settings" : "Ajustos comuns", @@ -98,12 +96,8 @@ "Notification sent successfully": "Notificació enviada correctament", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s ha esmentat en %2$s: \"%3$s\".", "Choose a format to convert {fileName}": "Triï un format per a convertir {fileName}", - "Form template": "Plantilla de formulari", - "Form template from existing text file": "Plantilla de formulari d'un fitxer de text existent", "Create form": "Crear formulari", "Fill in form in ONLYOFFICE": "Omplir el formulari en ONLYOFFICE", - "Create new Form template": "Crear nova plantilla de formulari", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Si us plau, actualitzi ONLYOFFICE Docs a la versió 7.0 per a poder treballar amb formularis emplenables en línia", "Security": "Seguretat", "Light": "Llum", "Classic Light": "Llum clàssica", diff --git a/l10n/da.js b/l10n/da.js index 586a0082..e4ad0762 100644 --- a/l10n/da.js +++ b/l10n/da.js @@ -45,11 +45,9 @@ OC.L10N.register( "View details": "Se detaljer", "Save": "Gem", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required.": "Blandet aktivt indhold er ikke tilladt. HTTPS-adresse for ONLYOFFICE Docs er påkrævet.", - "Restrict access to editors to following groups": "Begræns adgangen til redaktører til følgende grupper", "review": "Anmeldelse", "form filling": "Formular udfyldning", "comment": "Kommentar", - "custom filter": "Brugerdefineret filter", "download": "Hent", "Server settings": "Serverindstillinger", "Common settings": "Fælles indstillinger", @@ -100,12 +98,8 @@ OC.L10N.register( "Notification sent successfully": "Meddelelse sendt", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s nævnt i %2\$s: \"%3\$s\".", "Choose a format to convert {fileName}": "Vælg et format til at konvertere {fileName}", - "Form template": "Formularskabelon", - "Form template from existing text file": "Formularskabelon fra eksisterende tekstfil", "Create form": "Opret formular", "Fill in form in ONLYOFFICE": "Udfyld formularen i ONLYOFFICE", - "Create new Form template": "Opret ny formularskabelon", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Opdater venligst ONLYOFFICE Docs til version 7.0 for at arbejde på udfyldelige formularer online", "Security": "Sikkerhed", "Run document macros": "Kør dokumentmakroer", "Default editor theme": "Standard editortema", diff --git a/l10n/da.json b/l10n/da.json index 6223424c..ac2024fc 100644 --- a/l10n/da.json +++ b/l10n/da.json @@ -43,11 +43,9 @@ "View details": "Se detaljer", "Save": "Gem", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required.": "Blandet aktivt indhold er ikke tilladt. HTTPS-adresse for ONLYOFFICE Docs er påkrævet.", - "Restrict access to editors to following groups": "Begræns adgangen til redaktører til følgende grupper", "review": "Anmeldelse", "form filling": "Formular udfyldning", "comment": "Kommentar", - "custom filter": "Brugerdefineret filter", "download": "Hent", "Server settings": "Serverindstillinger", "Common settings": "Fælles indstillinger", @@ -98,12 +96,8 @@ "Notification sent successfully": "Meddelelse sendt", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s nævnt i %2$s: \"%3$s\".", "Choose a format to convert {fileName}": "Vælg et format til at konvertere {fileName}", - "Form template": "Formularskabelon", - "Form template from existing text file": "Formularskabelon fra eksisterende tekstfil", "Create form": "Opret formular", "Fill in form in ONLYOFFICE": "Udfyld formularen i ONLYOFFICE", - "Create new Form template": "Opret ny formularskabelon", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Opdater venligst ONLYOFFICE Docs til version 7.0 for at arbejde på udfyldelige formularer online", "Security": "Sikkerhed", "Run document macros": "Kør dokumentmakroer", "Default editor theme": "Standard editortema", diff --git a/l10n/de.js b/l10n/de.js index 855ee303..e23c4eed 100644 --- a/l10n/de.js +++ b/l10n/de.js @@ -45,12 +45,12 @@ OC.L10N.register( "View details" : "Details anzeigen", "Save" : "Speichern", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Mixed Active Content ist nicht möglich. HTTPS-Adresse für ONLYOFFICE Docs ist erforderlich.", - "Restrict access to editors to following groups" : "Den Zugriff auf Editoren auf folgende Gruppen gewähren", + "Allow the following groups to access the editors": "Den folgenden Gruppen den Zugriff auf die Editoren erlauben", "review" : "review", "form filling" : "ausfüllen von formularen", "comment" : "kommentarе", - "custom filter" : "benutzerdefinierter filter", - "download" : "herunterladen", + "global filter": "Globaler Filter", + "download": "herunterladen", "Server settings" : "Servereinstellungen", "Common settings" : "Allgemeine Einstellungen", "Editor customization settings" : "Editor-Einstellungen", @@ -100,12 +100,11 @@ OC.L10N.register( "Notification sent successfully": "Benachrichtigung erfolgreich gesendet", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s hat dich in %2\$s erwähnt: \"%3\$s\".", "Choose a format to convert {fileName}": "Wählen Sie das Format für {fileName} aus", - "Form template": "Formularvorlage", - "Form template from existing text file": "Formularvorlage aus Textdatei", + "PDF form": "PDF-Formular", + "PDF form from existing text file": "PDF-Formular aus vorhandener Textdatei", "Create form": "Formular erstellen", "Fill in form in ONLYOFFICE": "Formular in ONLYOFFICE ausfüllen", - "Create new Form template": "Neue Formularvorlage erstellen", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Für Online-Arbeit mit Formularen ist Version 7.0 von ONLYOFFICE Docs erforderlich", + "Create new PDF form": "Neues PDF-Formular erstellen", "Security": "Sicherheit", "Run document macros": "Makros im Dokument ausführen", "Default editor theme": "Standardmäßiges Thema des Editors", @@ -123,6 +122,11 @@ OC.L10N.register( "View settings": "Einstellungen anzeigen", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "Öffnen Sie die Editoren in der Cloud einfach ohne Herunterladen und Installation", - "Get Now": "Jetzt erhalten" + "Get Now": "Jetzt erhalten", + "Select file to combine": "Datei zum Kombinieren auswählen", + "Select data source": "Datenquelle auswählen", + "The data source must not be the current document": "Die Datenquelle darf nicht das aktuelle Dokument sein", + "Enable background connection check to the editors": "Die Hintergrund-Verbindungsprüfung zu den Editoren erlauben", + "The domain in the file url does not match the domain of the Document server": "Die Domäne in der Datei-URL stimmt nicht mit der Domäne von Document Server überein" }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/de.json b/l10n/de.json index eb5b5f90..1517db62 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -43,11 +43,11 @@ "View details" : "Details anzeigen", "Save" : "Speichern", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Mixed Active Content ist nicht möglich. HTTPS-Adresse für ONLYOFFICE Docs ist erforderlich.", - "Restrict access to editors to following groups" : "Den Zugriff auf Editoren auf folgende Gruppen gewähren", + "Allow the following groups to access the editors" : "Den folgenden Gruppen den Zugriff auf die Editoren erlauben", "review" : "review", "form filling" : "ausfüllen von formularen", "comment" : "kommentarе", - "custom filter" : "benutzerdefinierter filter", + "global filter" : "Globaler Filter", "download" : "herunterladen", "Server settings" : "Servereinstellungen", "Common settings" : "Allgemeine Einstellungen", @@ -98,12 +98,11 @@ "Notification sent successfully": "Benachrichtigung erfolgreich gesendet", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s hat dich in %2$s erwähnt: \"%3$s\".", "Choose a format to convert {fileName}": "Wählen Sie das Format für {fileName} aus", - "Form template": "Formularvorlage", - "Form template from existing text file": "Formularvorlage aus Textdatei", + "PDF form": "PDF-Formular", + "PDF form from existing text file": "PDF-Formular aus vorhandener Textdatei", "Create form": "Formular erstellen", "Fill in form in ONLYOFFICE": "Formular in ONLYOFFICE ausfüllen", - "Create new Form template": "Neue Formularvorlage erstellen", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Für Online-Arbeit mit Formularen ist Version 7.0 von ONLYOFFICE Docs erforderlich", + "Create new PDF form": "Neues PDF-Formular erstellen", "Security": "Sicherheit", "Run document macros": "Makros im Dokument ausführen", "Default editor theme": "Standardmäßiges Thema des Editors", @@ -121,6 +120,11 @@ "View settings": "Einstellungen anzeigen", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "Öffnen Sie die Editoren in der Cloud einfach ohne Herunterladen und Installation", - "Get Now": "Jetzt erhalten" + "Get Now": "Jetzt erhalten", + "Select file to combine" : "Datei zum Kombinieren auswählen", + "Select data source": "Datenquelle auswählen", + "The data source must not be the current document": "Die Datenquelle darf nicht das aktuelle Dokument sein", + "Enable background connection check to the editors": "Die Hintergrund-Verbindungsprüfung zu den Editoren erlauben", + "The domain in the file url does not match the domain of the Document server": "Die Domäne in der Datei-URL stimmt nicht mit der Domäne von Document Server überein" },"pluralForm" :"nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/l10n/de_DE.js b/l10n/de_DE.js index fc25407c..be96a38f 100644 --- a/l10n/de_DE.js +++ b/l10n/de_DE.js @@ -45,12 +45,12 @@ OC.L10N.register( "View details" : "Details anzeigen", "Save" : "Speichern", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Mixed Active Content ist nicht möglich. HTTPS-Adresse für ONLYOFFICE Docs ist erforderlich.", - "Restrict access to editors to following groups" : "Den Zugriff auf Editoren auf folgende Gruppen gewähren", + "Allow the following groups to access the editors": "Den folgenden Gruppen den Zugriff auf die Editoren erlauben", "review" : "review", "form filling" : "ausfüllen von formularen", "comment" : "kommentarе", - "custom filter" : "benutzerdefinierter filter", - "download" : "herunterladen", + "global filter": "Globaler Filter", + "download": "herunterladen", "Server settings" : "Servereinstellungen", "Common settings" : "Allgemeine Einstellungen", "Editor customization settings" : "Editor-Einstellungen", @@ -100,12 +100,11 @@ OC.L10N.register( "Notification sent successfully": "Benachrichtigung erfolgreich gesendet", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s hat dich in %2\$s erwähnt: \"%3\$s\".", "Choose a format to convert {fileName}": "Wählen Sie das Format für {fileName} aus", - "Form template": "Formularvorlage", - "Form template from existing text file": "Formularvorlage aus Textdatei", + "PDF form": "PDF-Formular", + "PDF form from existing text file": "PDF-Formular aus vorhandener Textdatei", "Create form": "Formular erstellen", "Fill in form in ONLYOFFICE": "Formular in ONLYOFFICE ausfüllen", - "Create new Form template": "Neue Formularvorlage erstellen", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Für Online-Arbeit mit Formularen ist Version 7.0 von ONLYOFFICE Docs erforderlich", + "Create new PDF form": "Neues PDF-Formular erstellen", "Security": "Sicherheit", "Run document macros": "Makros im Dokument ausführen", "Default editor theme": "Standardmäßiges Thema des Editors", @@ -123,6 +122,11 @@ OC.L10N.register( "View settings": "Einstellungen anzeigen", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "Öffnen Sie die Editoren in der Cloud einfach ohne Herunterladen und Installation", - "Get Now": "Jetzt erhalten" + "Get Now": "Jetzt erhalten", + "Select file to combine": "Datei zum Kombinieren auswählen", + "Select data source": "Datenquelle auswählen", + "The data source must not be the current document": "Die Datenquelle darf nicht das aktuelle Dokument sein", + "Enable background connection check to the editors": "Die Hintergrund-Verbindungsprüfung zu den Editoren erlauben", + "The domain in the file url does not match the domain of the Document server": "Die Domäne in der Datei-URL stimmt nicht mit der Domäne von Document Server überein" }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/de_DE.json b/l10n/de_DE.json index 967c3eb8..8a9bf9c9 100644 --- a/l10n/de_DE.json +++ b/l10n/de_DE.json @@ -43,11 +43,11 @@ "View details" : "Details anzeigen", "Save" : "Speichern", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Mixed Active Content ist nicht möglich. HTTPS-Adresse für ONLYOFFICE Docs ist erforderlich.", - "Restrict access to editors to following groups" : "Den Zugriff auf Editoren auf folgende Gruppen gewähren", + "Allow the following groups to access the editors" : "Den folgenden Gruppen den Zugriff auf die Editoren erlauben", "review" : "review", "form filling" : "ausfüllen von formularen", "comment" : "kommentarе", - "custom filter" : "benutzerdefinierter filter", + "global filter" : "Globaler Filter", "download" : "herunterladen", "Server settings" : "Servereinstellungen", "Common settings" : "Allgemeine Einstellungen", @@ -98,12 +98,11 @@ "Notification sent successfully": "Benachrichtigung erfolgreich gesendet", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s hat dich in %2$s erwähnt: \"%3$s\".", "Choose a format to convert {fileName}": "Wählen Sie das Format für {fileName} aus", - "Form template": "Formularvorlage", - "Form template from existing text file": "Formularvorlage aus Textdatei", + "PDF form": "PDF-Formular", + "PDF form from existing text file": "PDF-Formular aus vorhandener Textdatei", "Create form": "Formular erstellen", "Fill in form in ONLYOFFICE": "Formular in ONLYOFFICE ausfüllen", - "Create new Form template": "Neue Formularvorlage erstellen", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Für Online-Arbeit mit Formularen ist Version 7.0 von ONLYOFFICE Docs erforderlich", + "Create new PDF form": "Neues PDF-Formular erstellen", "Security": "Sicherheit", "Run document macros": "Makros im Dokument ausführen", "Default editor theme": "Standardmäßiges Thema des Editors", @@ -121,6 +120,11 @@ "View settings": "Einstellungen anzeigen", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "Öffnen Sie die Editoren in der Cloud einfach ohne Herunterladen und Installation", - "Get Now": "Jetzt erhalten" + "Get Now": "Jetzt erhalten", + "Select file to combine" : "Datei zum Kombinieren auswählen", + "Select data source": "Datenquelle auswählen", + "The data source must not be the current document": "Die Datenquelle darf nicht das aktuelle Dokument sein", + "Enable background connection check to the editors": "Die Hintergrund-Verbindungsprüfung zu den Editoren erlauben", + "The domain in the file url does not match the domain of the Document server": "Die Domäne in der Datei-URL stimmt nicht mit der Domäne von Document Server überein" },"pluralForm" :"nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/l10n/es.js b/l10n/es.js index a2cb53eb..091bc6f8 100644 --- a/l10n/es.js +++ b/l10n/es.js @@ -45,12 +45,12 @@ OC.L10N.register( "View details" : "Ver detalles", "Save" : "Guardar", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Contenido Mixto Activo no está permitido. Se requiere la dirección HTTPS para ONLYOFFICE Docs.", - "Restrict access to editors to following groups" : "Restringir el acceso a editores a siguientes grupos", - "review" : "revista", + "Allow the following groups to access the editors": "Permitir a los siguientes grupos acceder a los editores", + "review": "revista", "form filling" : "relleno de formulario", "comment" : "comentarios", - "custom filter" : "filtro personalizado", - "download" : "descargar", + "global filter": "filtro global", + "download": "descargar", "Server settings" : "Ajustes de servidor", "Common settings" : "Ajustes comunes", "Editor customization settings" : "Ajustes del editor", @@ -100,12 +100,11 @@ OC.L10N.register( "Notification sent successfully": "Notificación enviada correctamente", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s le ha mencionado en %2\$s: \"%3\$s\".", "Choose a format to convert {fileName}": "Elija un formato para convertir {fileName}", - "Form template": "Plantilla de formulario", - "Form template from existing text file": "Plantilla de formulario desde un archivo de texto existente", + "PDF form": "Formulario PDF", + "PDF form from existing text file": "Formulario PDF a partir de un archivo de texto existente", "Create form": "Crear formulario", "Fill in form in ONLYOFFICE": "Rellenar el formulario en ONLYOFFICE", - "Create new Form template": "Crear nueva plantilla de formulario", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Por favor, actualice ONLYOFFICE Docs a la versión 7.0 para poder trabajar con formularios rellenables en línea", + "Create new PDF form": "Crear nuevo formulario PDF", "Security": "Seguridad", "Run document macros": "Ejecutar macros de documentos", "Default editor theme": "Tema del editor predeterminado", @@ -123,6 +122,11 @@ OC.L10N.register( "View settings": "Ver configuración", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "Inicie fácilmente los editores en la nube sin tener que descargarlos ni instalarlos", - "Get Now": "Obtener ahora" + "Get Now": "Obtener ahora", + "Select file to combine": "Seleccionar archivo para combinar", + "Select data source": "Seleccionar fuente de datos", + "The data source must not be the current document": "La fuente de datos no debe ser el documento actual", + "Enable background connection check to the editors": "Activar la comprobación de conexión en segundo plano con los editores", + "The domain in the file url does not match the domain of the Document server": "El dominio de la URL del archivo no coincide con el dominio del Servidor de documentos" }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/es.json b/l10n/es.json index 2713e30b..84e00364 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -43,11 +43,11 @@ "View details" : "Ver detalles", "Save" : "Guardar", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Contenido Mixto Activo no está permitido. Se requiere la dirección HTTPS para ONLYOFFICE Docs.", - "Restrict access to editors to following groups" : "Restringir el acceso a editores a siguientes grupos", + "Allow the following groups to access the editors" : "Permitir a los siguientes grupos acceder a los editores", "review" : "revista", "form filling" : "relleno de formulario", "comment" : "comentarios", - "custom filter" : "filtro personalizado", + "global filter" : "filtro global", "download" : "descargar", "Server settings" : "Ajustes de servidor", "Common settings" : "Ajustes comunes", @@ -98,12 +98,11 @@ "Notification sent successfully": "Notificación enviada correctamente", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s le ha mencionado en %2$s: \"%3$s\".", "Choose a format to convert {fileName}": "Elija un formato para convertir {fileName}", - "Form template": "Plantilla de formulario", - "Form template from existing text file": "Plantilla de formulario desde un archivo de texto existente", + "PDF form": "Formulario PDF", + "PDF form from existing text file": "Formulario PDF a partir de un archivo de texto existente", "Create form": "Crear formulario", "Fill in form in ONLYOFFICE": "Rellenar el formulario en ONLYOFFICE", - "Create new Form template": "Crear nueva plantilla de formulario", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Por favor, actualice ONLYOFFICE Docs a la versión 7.0 para poder trabajar con formularios rellenables en línea", + "Create new PDF form": "Crear nuevo formulario PDF", "Security": "Seguridad", "Run document macros": "Ejecutar macros de documentos", "Default editor theme": "Tema del editor predeterminado", @@ -121,6 +120,11 @@ "View settings": "Ver configuración", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "Inicie fácilmente los editores en la nube sin tener que descargarlos ni instalarlos", - "Get Now": "Obtener ahora" + "Get Now": "Obtener ahora", + "Select file to combine" : "Seleccionar archivo para combinar", + "Select data source": "Seleccionar fuente de datos", + "The data source must not be the current document": "La fuente de datos no debe ser el documento actual", + "Enable background connection check to the editors": "Activar la comprobación de conexión en segundo plano con los editores", + "The domain in the file url does not match the domain of the Document server": "El dominio de la URL del archivo no coincide con el dominio del Servidor de documentos" },"pluralForm" :"nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/l10n/fr.js b/l10n/fr.js index f1dfcaa9..7b583432 100644 --- a/l10n/fr.js +++ b/l10n/fr.js @@ -45,12 +45,12 @@ OC.L10N.register( "View details" : "Voir les détails", "Save" : "Enregistrer", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Le contenu mixte actif n'est pas autorisé. Une adresse HTTPS pour le ONLYOFFICE Docs est requise", - "Restrict access to editors to following groups" : "Restreindre l'accès aux éditeurs pour les groupes suivants", - "review" : "révision", + "Allow the following groups to access the editors": "Autoriser les groupes suivants à accéder aux éditeurs", + "review": "révision", "form filling" : "remplissage de formulaire", "comment" : "commentaire", - "custom filter" : "filtre personnalisé", - "download" : "télécharger", + "global filter": "filtre global", + "download": "télécharger", "Server settings" : "Paramètres du serveur", "Common settings" : "Paramètres communs", "Editor customization settings" : "Paramètres de personnalisation de l'éditeur", @@ -100,12 +100,11 @@ OC.L10N.register( "Notification sent successfully": "Notification a été envoyée avec succès", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s vous a mentionné dans %2\$s: \"%3\$s\".", "Choose a format to convert {fileName}": "Choisissez un format à convertir {fileName}", - "Form template": "Modèle de formulaire", - "Form template from existing text file": "Modèle de formulaire à partir d'un fichier texte existant", + "PDF form": "Formulaire PDF", + "PDF form from existing text file": "Formulaire PDF à partir d'un fichier texte existant", "Create form": "Créer un formulaire", "Fill in form in ONLYOFFICE": "Remplir le formulaire dans ONLYOFFICE", - "Create new Form template": "Créer un nouveau modèle de formulaire", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Veuillez mettre à jour ONLYOFFICE Docs vers la version 7.0 pour travailler sur les formulaires à remplir en ligne", + "Create new PDF form": "Créer un nouveau formulaire PDF", "Security": "Sécurité", "Run document macros": "Exécuter des macros de documents", "Default editor theme": "Thème d'éditeur par défaut", @@ -123,6 +122,11 @@ OC.L10N.register( "View settings": "Afficher les paramètres", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "Lancez facilement les éditeurs dans le cloud sans téléchargement ni installation", - "Get Now": "Obtenir maintenant" + "Get Now": "Obtenir maintenant", + "Select file to combine": "Sélectionner le fichier à combiner", + "Select data source": "Sélectionner la source de données", + "The data source must not be the current document": "La source de données ne doit pas être le document actuel", + "Enable background connection check to the editors": "Activer la vérification des connexions en arrière-plan avec les éditeurs", + "The domain in the file url does not match the domain of the Document server": "Le domaine dans l'URL du fichier ne correspond pas au domaine de Document Server" }, "nplurals=2; plural=(n > 1);"); diff --git a/l10n/fr.json b/l10n/fr.json index 959369dd..4f4fd174 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -43,11 +43,11 @@ "View details" : "Voir les détails", "Save" : "Enregistrer", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Le contenu mixte actif n'est pas autorisé. Une adresse HTTPS pour le ONLYOFFICE Docs est requise", - "Restrict access to editors to following groups" : "Restreindre l'accès aux éditeurs pour les groupes suivants", + "Allow the following groups to access the editors" : "Autoriser les groupes suivants à accéder aux éditeurs", "review" : "révision", "form filling" : "remplissage de formulaire", "comment" : "commentaire", - "custom filter" : "filtre personnalisé", + "global filter" : "filtre global", "download" : "télécharger", "Server settings" : "Paramètres du serveur", "Common settings" : "Paramètres communs", @@ -98,12 +98,11 @@ "Notification sent successfully": "Notification a été envoyée avec succès", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s vous a mentionné dans %2$s: \"%3$s\".", "Choose a format to convert {fileName}": "Choisissez un format à convertir {fileName}", - "Form template": "Modèle de formulaire ", - "Form template from existing text file": "Modèle de formulaire à partir d'un fichier texte existant", + "PDF form": "Formulaire PDF", + "PDF form from existing text file": "Formulaire PDF à partir d'un fichier texte existant", "Create form": "Créer un formulaire", "Fill in form in ONLYOFFICE": "Remplir le formulaire dans ONLYOFFICE", - "Create new Form template": "Créer un nouveau modèle de formulaire", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Veuillez mettre à jour ONLYOFFICE Docs vers la version 7.0 pour travailler sur les formulaires à remplir en ligne", + "Create new PDF form": "Créer un nouveau formulaire PDF", "Security": "Sécurité", "Run document macros": "Exécuter des macros de documents", "Default editor theme": "Thème d'éditeur par défaut", @@ -121,6 +120,11 @@ "View settings": "Afficher les paramètres", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "Lancez facilement les éditeurs dans le cloud sans téléchargement ni installation", - "Get Now": "Obtenir maintenant" + "Get Now": "Obtenir maintenant", + "Select file to combine" : "Sélectionner le fichier à combiner", + "Select data source": "Sélectionner la source de données", + "The data source must not be the current document": "La source de données ne doit pas être le document actuel", + "Enable background connection check to the editors": "Activer la vérification des connexions en arrière-plan avec les éditeurs", + "The domain in the file url does not match the domain of the Document server": "Le domaine dans l'URL du fichier ne correspond pas au domaine de Document Server" },"pluralForm" :"nplurals=2; plural=(n > 1);" } \ No newline at end of file diff --git a/l10n/it.js b/l10n/it.js index ae29a205..18d27a34 100644 --- a/l10n/it.js +++ b/l10n/it.js @@ -45,12 +45,12 @@ OC.L10N.register( "View details" : "Vedi dettagli", "Save" : "Salva", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Il contenuto attivo misto non è consentito. È richiesto l'indirizzo HTTPS per ONLYOFFICE Docs.", - "Restrict access to editors to following groups" : "Limita l'accesso degli editor ai seguenti gruppi", - "review" : "Revisione", + "Allow the following groups to access the editors": "Consenti ai seguenti gruppi di accedere agli editor", + "review": "Revisione", "form filling" : "Compilare un modulo", "comment" : "Commento", - "custom filter" : "Filtro personalizzato", - "download" : "Scarica", + "global filter": "filtro globale", + "download": "Scarica", "Server settings" : "Impostazioni del server", "Common settings" : "Impostazioni comuni", "Editor customization settings" : "Impostazioni di personalizzazione dell'editor", @@ -100,12 +100,11 @@ OC.L10N.register( "Notification sent successfully": "Notifica è stata inviata con successo", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s ti ha menzionato in %2\$s: \"%3\$s\".", "Choose a format to convert {fileName}": "Scegli un formato per convertire {fileName}", - "Form template": "Modello di modulo", - "Form template from existing text file": "Modello di modulo da file di testo esistente", + "PDF form": "Modulo PDF", + "PDF form from existing text file": "Modulo PDF da file di testo esistente", "Create form": "Creare modulo", "Fill in form in ONLYOFFICE": "Compilare il modulo in ONLYOFFICE", - "Create new Form template": "Creare un nuovo modello di modulo", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Si prega di aggiornare ONLYOFFICE Docs alla versione 7.0 per lavorare su moduli compilabili online", + "Create new PDF form": "Crea un nuovo modulo PDF", "Security": "Sicurezza", "Run document macros": "Esegui le macro del documento", "Default editor theme": "Tema dell'editor predefinito", @@ -123,6 +122,11 @@ OC.L10N.register( "View settings": "Visualizza impostazioni", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs nel Cloud", "Easily launch the editors in the cloud without downloading and installation": "Avvia facilmente gli editor nel cloud senza scaricarli e installarli", - "Get Now": "Ottieni ora" + "Get Now": "Ottieni ora", + "Select file to combine": "Seleziona file da unire", + "Select data source": "Seleziona fonte dati", + "The data source must not be the current document": "La fonte dei dati non deve essere il documento corrente", + "Enable background connection check to the editors": "Abilita il controllo della connessione in background per gli editor", + "The domain in the file url does not match the domain of the Document server": "Il dominio nell'URL del file non corrisponde al dominio del Document server" }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/it.json b/l10n/it.json index 0a0b4694..56bc7a07 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -43,11 +43,11 @@ "View details" : "Vedi dettagli", "Save" : "Salva", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Il contenuto attivo misto non è consentito. È richiesto l'indirizzo HTTPS per ONLYOFFICE Docs.", - "Restrict access to editors to following groups" : "Limita l'accesso degli editor ai seguenti gruppi", + "Allow the following groups to access the editors" : "Consenti ai seguenti gruppi di accedere agli editor", "review" : "Revisione", "form filling" : "Compilare un modulo", "comment" : "Commento", - "custom filter" : "Filtro personalizzato", + "global filter" : "filtro globale", "download" : "Scarica", "Server settings" : "Impostazioni del server", "Common settings" : "Impostazioni comuni", @@ -98,12 +98,11 @@ "Notification sent successfully": "Notifica è stata inviata con successo", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s ti ha menzionato in %2$s: \"%3$s\".", "Choose a format to convert {fileName}": "Scegli un formato per convertire {fileName}", - "Form template": "Modello di modulo", - "Form template from existing text file": "Modello di modulo da file di testo esistente", + "PDF form": "Modulo PDF", + "PDF form from existing text file": "Modulo PDF da file di testo esistente", "Create form": "Creare modulo", "Fill in form in ONLYOFFICE": "Compilare il modulo in ONLYOFFICE", - "Create new Form template": "Creare un nuovo modello di modulo", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Si prega di aggiornare ONLYOFFICE Docs alla versione 7.0 per lavorare su moduli compilabili online", + "Create new PDF form": "Crea un nuovo modulo PDF", "Security": "Sicurezza", "Run document macros": "Esegui le macro del documento", "Default editor theme": "Tema dell'editor predefinito", @@ -121,6 +120,11 @@ "View settings": "Visualizza impostazioni", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs nel Cloud", "Easily launch the editors in the cloud without downloading and installation": "Avvia facilmente gli editor nel cloud senza scaricarli e installarli", - "Get Now": "Ottieni ora" + "Get Now": "Ottieni ora", + "Select file to combine" : "Seleziona file da unire", + "Select data source": "Seleziona fonte dati", + "The data source must not be the current document": "La fonte dei dati non deve essere il documento corrente", + "Enable background connection check to the editors": "Abilita il controllo della connessione in background per gli editor", + "The domain in the file url does not match the domain of the Document server": "Il dominio nell'URL del file non corrisponde al dominio del Document server" },"pluralForm" :"nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/l10n/ja.js b/l10n/ja.js index daa72df4..1991e3d3 100644 --- a/l10n/ja.js +++ b/l10n/ja.js @@ -45,12 +45,12 @@ OC.L10N.register( "View details" : "詳細表示", "Save" : "保存", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "アクティブコンテンツの混在は許可されていません。ONLYOFFICE DocsにはHTTPSアドレスが必要です", - "Restrict access to editors to following groups": "エディターの利用を以下のグループに制限する", - "review" : "レビュー", + "Allow the following groups to access the editors": "以下のグループにエディタへのアクセスを許可する", + "review": "レビュー", "form filling" : "フォーム入力", "comment" : "コメント", - "custom filter" : "ユーザー設定フィルター", - "download" : "ダウンロード", + "global filter": "グローバルフィルター", + "download": "ダウンロード", "Server settings" : "サーバー設定", "Common settings" : "共通設定", "Editor customization settings" : "エディターカスタム設定", @@ -100,12 +100,11 @@ OC.L10N.register( "Notification sent successfully": "通知を送信しました", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s は あなたを %2\$s: \"%3\$s\"に記載されました。", "Choose a format to convert {fileName}": "{fileName}を変換する形式を選択してください", - "Form template": "フォーム テンプレート", - "Form template from existing text file": "既存のテキストファイルからフォームテンプレートを作成する", + "PDF form": "PDFフォーム", + "PDF form from existing text file": "既存のテキストファイルからPDFフォーム", "Create form": "フォームの作成", "Fill in form in ONLYOFFICE": "ONLYOFFICEでフォームを記入する", - "Create new Form template": "新しいフォームテンプレートの作成", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "オンラインで入力可能なフォームを作成するには、ONLYOFFICE Docs 7.0版まで更新してください", + "Create new PDF form": "新規PDFフォームの作成", "Security": "セキュリティ", "Run document macros": "ドキュメントマクロを実行する", "Default editor theme": "エディターのデフォルトテーマ", @@ -123,6 +122,11 @@ OC.L10N.register( "View settings": "設定を見る", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "ダウンロードやインストールをすることなく、クラウド上で簡単にエディタを起動することができます", - "Get Now": "今すぐ使ってみる" + "Get Now": "今すぐ使ってみる", + "Select file to combine": "結合するファイルの選択", + "Select data source": "データソースの選択", + "The data source must not be the current document": "データソースは現在の文書であってはなりません", + "Enable background connection check to the editors": "エディタへのバックグラウンド接続チェックを有効にする", + "The domain in the file url does not match the domain of the Document server": "ファイルURLのドメインがドキュメントサーバーのドメインと一致しません" }, "nplurals=1; plural=0;"); diff --git a/l10n/ja.json b/l10n/ja.json index b33590db..bc7df660 100644 --- a/l10n/ja.json +++ b/l10n/ja.json @@ -43,11 +43,11 @@ "View details" : "詳細表示", "Save" : "保存", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "アクティブコンテンツの混在は許可されていません。ONLYOFFICE DocsにはHTTPSアドレスが必要です", - "Restrict access to editors to following groups" : "エディターの利用を以下のグループに制限する", + "Allow the following groups to access the editors" : "以下のグループにエディタへのアクセスを許可する", "review" : "レビュー", "form filling" : "フォーム入力", "comment" : "コメント", - "custom filter" : "ユーザー設定フィルター", + "global filter" : "グローバルフィルター", "download" : "ダウンロード", "Server settings" : "サーバー設定", "Common settings" : "共通設定", @@ -98,12 +98,11 @@ "Notification sent successfully": "通知を送信しました", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s は あなたを %2$s: \"%3$s\"に記載されました。", "Choose a format to convert {fileName}": "{fileName}を変換する形式を選択してください", - "Form template": "フォーム テンプレート", - "Form template from existing text file": "既存のテキストファイルからフォームテンプレートを作成する", + "PDF form": "PDFフォーム", + "PDF form from existing text file": "既存のテキストファイルからPDFフォーム", "Create form": "フォームの作成", "Fill in form in ONLYOFFICE": "ONLYOFFICEでフォームを記入する", - "Create new Form template": "新しいフォームテンプレートの作成", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "オンラインで入力可能なフォームを作成するには、ONLYOFFICE Docs 7.0版まで更新してください", + "Create new PDF form": "新規PDFフォームの作成", "Security": "セキュリティ", "Run document macros": "ドキュメントマクロを実行する", "Default editor theme": "エディターのデフォルトテーマ", @@ -121,6 +120,11 @@ "View settings": "設定を見る", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "ダウンロードやインストールをすることなく、クラウド上で簡単にエディタを起動することができます", - "Get Now": "今すぐ使ってみる" + "Get Now": "今すぐ使ってみる", + "Select file to combine" : "結合するファイルの選択", + "Select data source": "データソースの選択", + "The data source must not be the current document": "データソースは現在の文書であってはなりません", + "Enable background connection check to the editors": "エディタへのバックグラウンド接続チェックを有効にする", + "The domain in the file url does not match the domain of the Document server": "ファイルURLのドメインがドキュメントサーバーのドメインと一致しません" },"pluralForm" :"nplurals=1; plural=0;" } \ No newline at end of file diff --git a/l10n/nl.js b/l10n/nl.js index b050da28..da588b57 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -45,11 +45,9 @@ OC.L10N.register( "View details": "Bekijk details", "Save": "Opslaan", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required.": "Gemende Actieve Inhoud is niet toegestaan. HTTPS-adres voor ONLYOFFICE Docs is vereist.", - "Restrict access to editors to following groups": "Beperk de toegang tot editors tot de volgende groepen", "review": "overzicht", "form filling": "formulier invullen", "comment": "opmerking", - "custom filter": "aangepast filter", "download": "downloaden", "Server settings": "Serverinstellingen", "Common settings": "Algemene instellingen", @@ -100,17 +98,25 @@ OC.L10N.register( "Notification sent successfully": "Kennisgeving succesvol verzonden", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s genoemd in de %2\$s: \"%3\$s\".", "Choose a format to convert {fileName}": "Kies een formaat om {fileName} te converteren", - "Form template": "Formulier sjabloon", - "Form template from existing text file": "Formulier sjabloon uit bestaand tekstbestand", "Create form": "Formulier maken", "Fill in form in ONLYOFFICE": "Formulier invullen in ONLYOFFICE", - "Create new Form template": "Nieuw Formulier sjabloon maken", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Update ONLYOFFICE Docs naar versie 7.0 om online invulbare formulieren te kunnen gebruiken", "Security": "Beveiliging", "Run document macros": "Document macro's uitvoeren", "Default editor theme": "Standaard editor thema", "Light": "Licht", "Classic Light": "Klassiek Licht", - "Dark": "Donker" + "Dark": "Donker", + "This feature is unavailable due to encryption settings.": "Deze functie is niet beschikbaar vanwege versleutelingsinstellingen.", + "Enable plugins": "Plug-ins inschakelen", + "Enable document protection for": "Documentbeveiliging inschakelen voor", + "All users": "Alle gebruikers", + "Owner only": "Alleen eigenaar", + "Authorization header (leave blank to use default header)": "Autorisatieheader (laat leeg om de standaard header te gebruiken)", + "ONLYOFFICE server is not available": "ONLYOFFICE server is niet beschikbaar", + "Please check the settings to resolve the problem.": "Controleer de instellingen om het probleem op te lossen.", + "View settings": "Bekijk instellingen", + "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", + "Easily launch the editors in the cloud without downloading and installation": "Start de editors gemakkelijk in de cloud zonder te hoeven downloaden en te installeren", + "Get Now": "Download Nu" }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/nl.json b/l10n/nl.json index c17c867a..fb5d79cd 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -43,11 +43,9 @@ "View details" : "Bekijk details", "Save" : "Opslaan", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Gemende Actieve Inhoud is niet toegestaan. HTTPS-adres voor ONLYOFFICE Docs is vereist.", - "Restrict access to editors to following groups" : "Beperk de toegang tot editors tot de volgende groepen", "review" : "overzicht", "form filling" : "formulier invullen", "comment" : "opmerking", - "custom filter" : "aangepast filter", "download" : "downloaden", "Server settings" : "Serverinstellingen", "Common settings" : "Algemene instellingen", @@ -98,17 +96,25 @@ "Notification sent successfully": "Kennisgeving succesvol verzonden", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s genoemd in de %2$s: \"%3$s\".", "Choose a format to convert {fileName}": "Kies een formaat om {fileName} te converteren", - "Form template": "Formulier sjabloon", - "Form template from existing text file": "Formulier sjabloon uit bestaand tekstbestand", "Create form": "Formulier maken", "Fill in form in ONLYOFFICE": "Formulier invullen in ONLYOFFICE", - "Create new Form template": "Nieuw Formulier sjabloon maken", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Update ONLYOFFICE Docs naar versie 7.0 om online invulbare formulieren te kunnen gebruiken", "Security": "Beveiliging", "Run document macros": "Document macro's uitvoeren", "Default editor theme": "Standaard editor thema", "Light": "Licht", "Classic Light": "Klassiek Licht", - "Dark": "Donker" + "Dark": "Donker", + "This feature is unavailable due to encryption settings.": "Deze functie is niet beschikbaar vanwege versleutelingsinstellingen.", + "Enable plugins": "Plug-ins inschakelen", + "Enable document protection for": "Documentbeveiliging inschakelen voor", + "All users": "Alle gebruikers", + "Owner only": "Alleen eigenaar", + "Authorization header (leave blank to use default header)": "Autorisatieheader (laat leeg om de standaard header te gebruiken)", + "ONLYOFFICE server is not available": "ONLYOFFICE server is niet beschikbaar", + "Please check the settings to resolve the problem.": "Controleer de instellingen om het probleem op te lossen.", + "View settings": "Bekijk instellingen", + "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", + "Easily launch the editors in the cloud without downloading and installation": "Start de editors gemakkelijk in de cloud zonder te hoeven downloaden en te installeren", + "Get Now": "Download Nu" },"pluralForm" :"nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/l10n/pl.js b/l10n/pl.js index 6a54a02f..32fbfc1f 100644 --- a/l10n/pl.js +++ b/l10n/pl.js @@ -45,11 +45,9 @@ OC.L10N.register( "View details" : "Szczegóły", "Save" : "Zapisz", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Aktywna Zawartość Mieszana nie jest dozwolona. Adres HTTPS jest wymagany dla ONLYOFFICE Docs.", - "Restrict access to editors to following groups" : "Ogranicz dostęp do edytorów dla tych grup", "review" : "recenzja", "form filling" : "wypełnianie formularza", "comment" : "komentarz", - "custom filter" : "niestandardowy filtr", "download" : "pobierz", "Server settings" : "Ustawienia serwera", "Common settings" : "Ustawienia ogólne", @@ -100,15 +98,25 @@ OC.L10N.register( "Notification sent successfully": "Powiadomienie zostało wysłane", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s dodał(a) Cię w %2\$s następujący komentarz: \"%3\$s\".", "Choose a format to convert {fileName}": "Wybierz format, do którego chcesz przekonwertować {fileName}", - "Form template": "Szablon formularza", - "Form template from existing text file": "Szablon formularza z istniejącego pliku tekstowego", "Create form": "Utwórz formularz", "Fill in form in ONLYOFFICE": "Wypełnić formularz w ONLYOFFICE", - "Create new Form template": "Utwórz nowy szablon formularza", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Zaktualizuj ONLYOFFICE Docs do wersji 7.0, aby działały w formularzach do wypełniania online", "Security": "Bezpieczeństwo", + "Run document macros": "Uruchom makra dokumentu", + "Default editor theme": "Domyślny motyw edytora", "Light": "Jasny", "Classic Light": "Klasyczny jasny", - "Dark": "Ciemny" + "Dark": "Ciemny", + "This feature is unavailable due to encryption settings.": "Dana funkcja jest niedostępna ze względu na ustawienia szyfrowania.", + "Enable plugins": "Włącz wtyczki", + "Enable document protection for": "Włącz ochronę dokumentów dla", + "All users": "Wszystkich użytkowników", + "Owner only": "Tylko właściciela", + "Authorization header (leave blank to use default header)": "Nagłówek autoryzacji (pozostaw puste pole, aby użyć domyślnego nagłówka)", + "ONLYOFFICE server is not available": "Serwer ONLYOFFICE jest niedostępny", + "Please check the settings to resolve the problem.": "Sprawdź ustawienia, aby rozwiązać problem.", + "View settings": "Zobacz ustawienia", + "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", + "Easily launch the editors in the cloud without downloading and installation": "Z łatwością uruchamiaj edytory w chmurze bez konieczności pobierania czy instalacji", + "Get Now": "Wypróbuj teraz" }, "nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);"); \ No newline at end of file diff --git a/l10n/pl.json b/l10n/pl.json index d16a3835..8a1e94a9 100644 --- a/l10n/pl.json +++ b/l10n/pl.json @@ -43,11 +43,9 @@ "View details" : "Szczegóły", "Save" : "Zapisz", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Aktywna Zawartość Mieszana nie jest dozwolona. Adres HTTPS jest wymagany dla ONLYOFFICE Docs.", - "Restrict access to editors to following groups" : "Ogranicz dostęp do edytorów dla tych grup", "review" : "recenzja", "form filling" : "wypełnianie formularza", "comment" : "komentarz", - "custom filter" : "niestandardowy filtr", "download" : "pobierz", "Server settings" : "Ustawienia serwera", "Common settings" : "Ustawienia ogólne", @@ -98,15 +96,25 @@ "Notification sent successfully": "Powiadomienie zostało wysłane", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s dodał(a) Cię w %2$s następujący komentarz: \"%3$s\".", "Choose a format to convert {fileName}": "Wybierz format, do którego chcesz przekonwertować {fileName}", - "Form template": "Szablon formularza", - "Form template from existing text file": "Szablon formularza z istniejącego pliku tekstowego", "Create form": "Utwórz formularz", "Fill in form in ONLYOFFICE": "Wypełnić formularz w ONLYOFFICE", - "Create new Form template": "Utwórz nowy szablon formularza", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Zaktualizuj ONLYOFFICE Docs do wersji 7.0, aby działały w formularzach do wypełniania online", "Security": "Bezpieczeństwo", + "Run document macros": "Uruchom makra dokumentu", + "Default editor theme": "Domyślny motyw edytora", "Light": "Jasny", "Classic Light": "Klasyczny jasny", - "Dark": "Ciemny" + "Dark": "Ciemny", + "This feature is unavailable due to encryption settings.": "Dana funkcja jest niedostępna ze względu na ustawienia szyfrowania.", + "Enable plugins": "Włącz wtyczki", + "Enable document protection for": "Włącz ochronę dokumentów dla", + "All users": "Wszystkich użytkowników", + "Owner only": "Tylko właściciela", + "Authorization header (leave blank to use default header)" : "Nagłówek autoryzacji (pozostaw puste pole, aby użyć domyślnego nagłówka)", + "ONLYOFFICE server is not available": "Serwer ONLYOFFICE jest niedostępny", + "Please check the settings to resolve the problem.": "Sprawdź ustawienia, aby rozwiązać problem.", + "View settings": "Zobacz ustawienia", + "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", + "Easily launch the editors in the cloud without downloading and installation": "Z łatwością uruchamiaj edytory w chmurze bez konieczności pobierania czy instalacji", + "Get Now": "Wypróbuj teraz" },"pluralForm" :"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);" } \ No newline at end of file diff --git a/l10n/pt_BR.js b/l10n/pt_BR.js index 5a8b9747..27766642 100644 --- a/l10n/pt_BR.js +++ b/l10n/pt_BR.js @@ -45,12 +45,12 @@ OC.L10N.register( "View details" : "Ver detalhes", "Save" : "Salvar", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Conteúdo Misto não é permitido. É necessário um endereço HTTPS para o ONLYOFFICE Docs.", - "Restrict access to editors to following groups" : "Acesso apenas para os seguintes grupos", + "Allow the following groups to access the editors": "Permitir que os seguintes grupos acessem os editores", "review" : "revisar", "form filling" : "preenchimento de formularios", "comment" : "comente", - "custom filter" : "filtro personalizado", - "download" : "baixar", + "global filter": "filtro global", + "download": "baixar", "Server settings" : "Configurações do servidor", "Common settings" : "Configurações comuns", "Editor customization settings" : "Configurações de personalização do editor", @@ -100,12 +100,11 @@ OC.L10N.register( "Notification sent successfully": "Notificação enviada com sucesso", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s mencionado você em %2\$s: \"%3\$s\".", "Choose a format to convert {fileName}": "Escolha um formato para converter {fileName}", - "Form template": "Modelo de formulário", - "Form template from existing text file": "Modelo de formulário a partir de arquivo de texto existente", + "PDF form": "Formulário PDF", + "PDF form from existing text file": "Formulário PDF de arquivo de texto existente", "Create form": "Criar formulário", "Fill in form in ONLYOFFICE": "Preencher formulário no ONLYOFFICE", - "Create new Form template": "Criar novo modelo de Formulário", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Atualize o ONLYOFFICE Docs para a versão 7.0 para trabalhar em formulários preenchíveis online", + "Create new PDF form": "Criar novo formulário PDF", "Security": "Segurança", "Run document macros": "Executar macros de documento", "Default editor theme": "Tema do editor padrão", @@ -123,6 +122,11 @@ OC.L10N.register( "View settings": "Configurações de exibição", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "Inicie facilmente os editores na nuvem sem download e instalação", - "Get Now": "Obter agora" + "Get Now": "Obter agora", + "Select file to combine": "Selecione o arquivo para combinar", + "Select data source": "Selecionar fonte de dados", + "The data source must not be the current document": "A fonte de dados não deve ser o documento atual", + "Enable background connection check to the editors": "Ativar verificação de conexão em segundo plano para os editores", + "The domain in the file url does not match the domain of the Document server": "O domínio no URL do arquivo não corresponde ao domínio do servidor de documentos" }, "nplurals=2; plural=(n > 1);"); diff --git a/l10n/pt_BR.json b/l10n/pt_BR.json index 02352a56..aadc2144 100644 --- a/l10n/pt_BR.json +++ b/l10n/pt_BR.json @@ -43,11 +43,11 @@ "View details" : "Ver detalhes", "Save" : "Salvar", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Conteúdo Misto não é permitido. É necessário um endereço HTTPS para o ONLYOFFICE Docs.", - "Restrict access to editors to following groups" : "Acesso apenas para os seguintes grupos", + "Allow the following groups to access the editors" : "Permitir que os seguintes grupos acessem os editores", "review" : "revisar", "form filling" : "preenchimento de formularios", "comment" : "comente", - "custom filter" : "filtro personalizado", + "global filter" : "filtro global", "download" : "baixar", "Server settings" : "Configurações do servidor", "Common settings" : "Configurações comuns", @@ -98,12 +98,11 @@ "Notification sent successfully": "Notificação enviada com sucesso", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s mencionado você em %2$s: \"%3$s\".", "Choose a format to convert {fileName}": "Escolha um formato para converter {fileName}", - "Form template": "Modelo de formulário", - "Form template from existing text file": "Modelo de formulário a partir de arquivo de texto existente", + "PDF form": "Formulário PDF", + "PDF form from existing text file": "Formulário PDF de arquivo de texto existente", "Create form": "Criar formulário", "Fill in form in ONLYOFFICE": "Preencher formulário no ONLYOFFICE", - "Create new Form template": "Criar novo modelo de Formulário", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Atualize o ONLYOFFICE Docs para a versão 7.0 para trabalhar em formulários preenchíveis online", + "Create new PDF form": "Criar novo formulário PDF", "Security": "Segurança", "Run document macros": "Executar macros de documento", "Default editor theme": "Tema do editor padrão", @@ -121,6 +120,11 @@ "View settings": "Configurações de exibição", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "Inicie facilmente os editores na nuvem sem download e instalação", - "Get Now": "Obter agora" + "Get Now": "Obter agora", + "Select file to combine" : "Selecione o arquivo para combinar", + "Select data source": "Selecionar fonte de dados", + "The data source must not be the current document": "A fonte de dados não deve ser o documento atual", + "Enable background connection check to the editors": "Ativar verificação de conexão em segundo plano para os editores", + "The domain in the file url does not match the domain of the Document server": "O domínio no URL do arquivo não corresponde ao domínio do servidor de documentos" },"pluralForm" :"nplurals=2; plural=(n > 1);" } \ No newline at end of file diff --git a/l10n/ru.js b/l10n/ru.js index 0ccf5a92..e989b28f 100644 --- a/l10n/ru.js +++ b/l10n/ru.js @@ -45,11 +45,11 @@ OC.L10N.register( "View details" : "Подробнее", "Save" : "Сохранить", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Смешанное активное содержимое запрещено. Для ONLYOFFICE Docs необходимо использовать HTTPS-адрес.", - "Restrict access to editors to following groups" : "Дать доступ к редакторам только следующим группам", + "Allow the following groups to access the editors" : "Дать доступ к редакторам только следующим группам", "review" : "рецензирование", "form filling" : "заполнение форм", "comment" : "комментирование", - "custom filter" : "пользовательский фильтр", + "global filter" : "глобальный фильтр", "download" : "скачивание", "Server settings" : "Настройки сервера", "Common settings" : "Общие настройки", @@ -100,12 +100,11 @@ OC.L10N.register( "Notification sent successfully": "Оповещение успешно отправлено", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s упомянул вас в %2\$s: \"%3\$s\".", "Choose a format to convert {fileName}": "Выберите формат для {fileName}", - "Form template": "Шаблон формы", - "Form template from existing text file": "Шаблон формы из существующего текстового файла", + "PDF form": "PDF-форма", + "PDF form from existing text file": "PDF-форма из существующего текстового файла", "Create form": "Создать форму", "Fill in form in ONLYOFFICE": "Заполнить форму в ONLYOFFICE", - "Create new Form template": "Создать новый шаблон формы", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Обновите сервер ONLYOFFICE Docs до версии 7.0 для работы с формами онлайн", + "Create new PDF form": "Создать новую PDF-форму", "Security": "Безопасность", "Run document macros": "Запускать макросы документа", "Default editor theme": "Тема редактора по умолчанию", @@ -123,6 +122,11 @@ OC.L10N.register( "View settings": "Посмотреть настройки", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "Легко запускайте редакторы в облаке без скачивания и установки", - "Get Now": "Получить сейчас" + "Get Now": "Получить сейчас", + "Select file to combine" : "Выбрать файл для объединения", + "Select data source": "Выбрать источник данных", + "The data source must not be the current document": "Источником данных не должен быть текущий документ", + "Enable background connection check to the editors": "Включить фоновую проверку подключения к редакторам", + "The domain in the file url does not match the domain of the Document server": "Домен в адресе файла не соответствует домену сервера документов" }, "nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"); diff --git a/l10n/ru.json b/l10n/ru.json index 026884ea..873946d2 100644 --- a/l10n/ru.json +++ b/l10n/ru.json @@ -43,11 +43,11 @@ "View details" : "Подробнее", "Save" : "Сохранить", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Смешанное активное содержимое запрещено. Для ONLYOFFICE Docs необходимо использовать HTTPS-адрес.", - "Restrict access to editors to following groups" : "Дать доступ к редакторам только следующим группам", + "Allow the following groups to access the editors" : "Дать доступ к редакторам только следующим группам", "review" : "рецензирование", "form filling" : "заполнение форм", "comment" : "комментирование", - "custom filter" : "пользовательский фильтр", + "global filter" : "глобальный фильтр", "download" : "скачивание", "Server settings" : "Настройки сервера", "Common settings" : "Общие настройки", @@ -98,12 +98,11 @@ "Notification sent successfully": "Оповещение успешно отправлено", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s упомянул вас в %2$s: \"%3$s\".", "Choose a format to convert {fileName}": "Выберите формат для {fileName}", - "Form template": "Шаблон формы", - "Form template from existing text file": "Шаблон формы из существующего текстового файла", + "PDF form": "PDF-форма", + "PDF form from existing text file": "PDF-форма из существующего текстового файла", "Create form": "Создать форму", "Fill in form in ONLYOFFICE": "Заполнить форму в ONLYOFFICE", - "Create new Form template": "Создать новый шаблон формы", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Обновите сервер ONLYOFFICE Docs до версии 7.0 для работы с формами онлайн", + "Create new PDF form": "Создать новую PDF-форму", "Security": "Безопасность", "Run document macros": "Запускать макросы документа", "Default editor theme": "Тема редактора по умолчанию", @@ -121,6 +120,11 @@ "View settings": "Посмотреть настройки", "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs Cloud", "Easily launch the editors in the cloud without downloading and installation": "Легко запускайте редакторы в облаке без скачивания и установки", - "Get Now": "Получить сейчас" + "Get Now": "Получить сейчас", + "Select file to combine" : "Выбрать файл для объединения", + "Select data source": "Выбрать источник данных", + "The data source must not be the current document": "Источником данных не должен быть текущий документ", + "Enable background connection check to the editors": "Включить фоновую проверку подключения к редакторам", + "The domain in the file url does not match the domain of the Document server": "Домен в адресе файла не соответствует домену сервера документов" },"pluralForm" :"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);" } \ No newline at end of file diff --git a/l10n/sv.js b/l10n/sv.js index 9f5a7164..d17263a8 100644 --- a/l10n/sv.js +++ b/l10n/sv.js @@ -45,11 +45,9 @@ OC.L10N.register( "View details" : "Visa detaljer", "Save" : "Spara", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Blandat aktivt innehåll är inte tillåtet. HTTPS-adress till ONLYOFFICE Docs krävs.", - "Restrict access to editors to following groups" : "Begränsa åtkomst till följande grupper", "review" : "granska", "form filling" : "formulärfyllning", "comment" : "kommentar", - "custom filter" : "anpassat filter", "download" : "ladda ner", "Server settings" : "Serverinställningar", "Common settings" : "Allmänna inställningar", @@ -100,15 +98,24 @@ OC.L10N.register( "Notification sent successfully": "Aviseringen har skickats", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s nämnde dig i %2\$s: \"%3\$s\".", "Choose a format to convert {fileName}": "Välj det filformat som {fileName} ska konverteras till.", - "Form template": "Formulärmall", - "Form template from existing text file": "Formulärmall från befintlig textfil", "Create form": "Skapa formulär", "Fill in form in ONLYOFFICE": "Fylla i formulär i ONLYOFFICE", - "Create new Form template": "Skapa ny formulärmall", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Uppdatera ONLYOFFICE Docs till version 7.0 för att arbeta med ifyllbart onlineformulär", "Security": "Säkerhet", + "Run document macros": "Kör dokumentmakron", "Light": "Ljus", "Classic Light": "Klassiskt ljus", - "Dark": "Mörk" + "Dark": "Mörk", + "This feature is unavailable due to encryption settings.": "Denna funktion är otillgänglig på grund av krypteringsinställningar.", + "Enable plugins": "Aktivera plugins", + "Enable document protection for": "Aktivera dokumentsskydd för", + "All users": "Alla användare", + "Owner only": "Endast ägaren", + "Authorization header (leave blank to use default header)": "Autentiseringshuvud (lämna tom för att använda standardhuvud)", + "ONLYOFFICE server is not available": "ONLYOFFICE-servern är inte tillgänglig", + "Please check the settings to resolve the problem.": "Kontrollera inställningarna för att åtgärda problemet.", + "View settings": "Visa inställningar", + "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs-moln", + "Easily launch the editors in the cloud without downloading and installation": "Starta redigerarna i molnet enkelt utan nedladdning och installation", + "Get Now": "Skaffa nu" }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/sv.json b/l10n/sv.json index 82483a92..ad37882a 100644 --- a/l10n/sv.json +++ b/l10n/sv.json @@ -43,11 +43,9 @@ "View details" : "Visa detaljer", "Save" : "Spara", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "Blandat aktivt innehåll är inte tillåtet. HTTPS-adress till ONLYOFFICE Docs krävs.", - "Restrict access to editors to following groups" : "Begränsa åtkomst till följande grupper", "review" : "granska", "form filling" : "formulärfyllning", "comment" : "kommentar", - "custom filter" : "anpassat filter", "download" : "ladda ner", "Server settings" : "Serverinställningar", "Common settings" : "Allmänna inställningar", @@ -98,15 +96,24 @@ "Notification sent successfully": "Aviseringen har skickats", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s nämnde dig i %2$s: \"%3$s\".", "Choose a format to convert {fileName}": "Välj det filformat som {fileName} ska konverteras till.", - "Form template": "Formulärmall", - "Form template from existing text file": "Formulärmall från befintlig textfil", "Create form": "Skapa formulär", "Fill in form in ONLYOFFICE": "Fylla i formulär i ONLYOFFICE", - "Create new Form template": "Skapa ny formulärmall", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Uppdatera ONLYOFFICE Docs till version 7.0 för att arbeta med ifyllbart onlineformulär", "Security": "Säkerhet", + "Run document macros": "Kör dokumentmakron", "Light": "Ljus", "Classic Light": "Klassiskt ljus", - "Dark": "Mörk" + "Dark": "Mörk", + "This feature is unavailable due to encryption settings.": "Denna funktion är otillgänglig på grund av krypteringsinställningar.", + "Enable plugins": "Aktivera plugins", + "Enable document protection for": "Aktivera dokumentsskydd för", + "All users": "Alla användare", + "Owner only": "Endast ägaren", + "Authorization header (leave blank to use default header)" : "Autentiseringshuvud (lämna tom för att använda standardhuvud)", + "ONLYOFFICE server is not available": "ONLYOFFICE-servern är inte tillgänglig", + "Please check the settings to resolve the problem.": "Kontrollera inställningarna för att åtgärda problemet.", + "View settings": "Visa inställningar", + "ONLYOFFICE Docs Cloud": "ONLYOFFICE Docs-moln", + "Easily launch the editors in the cloud without downloading and installation": "Starta redigerarna i molnet enkelt utan nedladdning och installation", + "Get Now": "Skaffa nu" },"pluralForm" :"nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/l10n/uk.js b/l10n/uk.js index bb9b820f..9d21c690 100644 --- a/l10n/uk.js +++ b/l10n/uk.js @@ -44,11 +44,10 @@ OC.L10N.register( "View details": "Докладно", "Save": "Зберегти", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required.": "Зміщаний активний вміст заборонено. Для використання ONLYOFFICE Docs потрібно використовувати безпечне HTTPS-з'єднання.", - "Restrict access to editors to following groups": "Надати доступ до редагування лише таким групам", + "Allow the following groups to access the editors": "Надати доступ до редагування лише таким групам", "review": "рецензії", "form filling": "заповнення форм", "comment": "коментарі", - "custom filter": "користувацький фільтр", "download": "звантажити", "Server settings": "Налаштування сервера", "Common settings": "Загальні налаштування", @@ -99,11 +98,8 @@ OC.L10N.register( "Notification sent successfully": "Сповіщення успішно надіслено", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s згадав у %2\$s: \"%3\$s\".", "Choose a format to convert {fileName}": "Виберіть формат для {fileName}", - "Form template": "Шаблон форми", "Create form": "Створити форму", "Fill in form in ONLYOFFICE": "Заповнити форму у ONLYOFFICE", - "Create new Form template": "Створити новий шаблон форми", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Оновіть сервер ONLYOFFICE Docs до версії 7.0 для роботи з формами в онлайні", "Security": "Безпека", "Run document macros": "Виконувати макроси документу", "Default editor theme": "Типова тема редактора", diff --git a/l10n/uk.json b/l10n/uk.json index dde01d0a..48f76a20 100644 --- a/l10n/uk.json +++ b/l10n/uk.json @@ -43,11 +43,10 @@ "View details": "Докладно", "Save": "Зберегти", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required.": "Зміщаний активний вміст заборонено. Для використання ONLYOFFICE Docs потрібно використовувати безпечне HTTPS-з'єднання.", - "Restrict access to editors to following groups": "Надати доступ до редагування лише таким групам", + "Allow the following groups to access the editors": "Надати доступ до редагування лише таким групам", "review": "рецензії", "form filling": "заповнення форм", "comment": "коментарі", - "custom filter": "користувацький фільтр", "download": "звантажити", "Server settings": "Налаштування сервера", "Common settings": "Загальні налаштування", @@ -98,11 +97,8 @@ "Notification sent successfully": "Сповіщення успішно надіслено", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s згадав у %2$s: \"%3$s\".", "Choose a format to convert {fileName}": "Виберіть формат для {fileName}", - "Form template": "Шаблон форми", "Create form": "Створити форму", "Fill in form in ONLYOFFICE": "Заповнити форму у ONLYOFFICE", - "Create new Form template": "Створити новий шаблон форми", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "Оновіть сервер ONLYOFFICE Docs до версії 7.0 для роботи з формами в онлайні", "Security": "Безпека", "Run document macros": "Виконувати макроси документу", "Default editor theme": "Типова тема редактора", diff --git a/l10n/zh_CN.js b/l10n/zh_CN.js index f732f498..a1336785 100644 --- a/l10n/zh_CN.js +++ b/l10n/zh_CN.js @@ -2,24 +2,24 @@ OC.L10N.register( "onlyoffice", { "Access denied" : "禁止访问", - "Invalid request" : "非法请求", + "Invalid request": "无效请求", "Files not found" : "文件未找到", "File not found" : "文件未找到", "Not permitted" : "没有权限", "Download failed" : "下载失败", - "The required folder was not found" : "必须的文件夹未找到", - "You don't have enough permission to create" : "没有足够权限创建文件", + "The required folder was not found": "未找到所需文件夹", + "You don't have enough permission to create": "没有足够权限创建文档", "Template not found" : "模板未找到", "Can't create file" : "无法创建文件", - "Format is not supported" : "文件格式不支持", - "Conversion is not required" : "无需文件转换", - "Failed to download converted file" : "转换后的文件下载失败", + "Format is not supported": "格式不支持", + "Conversion is not required": "无需转换", + "Failed to download converted file": "无法下载转换后的文件", "ONLYOFFICE app is not configured. Please contact admin" : "ONLYOFFICE未配置,请联系管理员。", "FileId is empty" : "文件ID为空", "You do not have enough permissions to view the file" : "您没有足够权限浏览该文件", "Error occurred in the document service" : "文档服务内部发生异常", "Not supported version" : "不支持的版本", - "ONLYOFFICE cannot be reached. Please contact admin" : "ONLYOFFICE服务器无法连接,请联系管理员。", + "ONLYOFFICE cannot be reached. Please contact admin": "ONLYOFFICE 服务器无法连接,请联系管理员。", "Loading, please wait." : "载入中,请稍后...", "File created" : "文件已创建", "Open in ONLYOFFICE" : "用 ONLYOFFICE 打开", @@ -27,7 +27,7 @@ OC.L10N.register( "Document" : "文档", "Spreadsheet" : "电子表格", "Presentation" : "演示文稿", - "Error when trying to connect" : "连接是发生异常", + "Error when trying to connect": "连接时发生异常", "Settings have been successfully updated" : "设置已保存", "Server can't read xml" : "服务器无法读取XML", "Bad Response. Errors: " : "错误的返回: ", @@ -36,21 +36,21 @@ OC.L10N.register( "Encryption App is enabled, the application cannot work. You can continue working with the application if you enable master key." : "加密应用程序已启用,应用程序无法工作。如果启用主密钥,则可以继续使用该应用程序。", "ONLYOFFICE Docs address" : "ONLYOFFICE Docs地址", "Advanced server settings" : "更多设置", - "ONLYOFFICE Docs address for internal requests from the server" : "用于服务器内部的ONLYOFFICE Docs的地址", - "Server address for internal requests from ONLYOFFICE Docs" : "用于ONLYOFFICE Docs内部请求的服务器的地址", + "ONLYOFFICE Docs address for internal requests from the server": "服务器内部请求 ONLYOFFICE Docs 的地址", + "Server address for internal requests from ONLYOFFICE Docs": "ONLYOFFICE Docs 内部请求服务器的地址", "Secret key (leave blank to disable)" : "秘钥(留空为关闭)", - "Open file in the same tab" : "在相同的切签中打开", + "Open file in the same tab": "在相同的标签页中打开", "The default application for opening the format": "默认关联的文件格式", "Open the file for editing (due to format restrictions, the data might be lost when saving to the formats from the list below)" : "默认的文件编辑器 (由于文件格式限制,保存为下列格式时,数据可能会缺失)", "View details" : "查看详情", "Save" : "保存", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "不允许混合活动内容,请使用HTTPS连接ONLYOFFICE Docs。", - "Restrict access to editors to following groups" : "仅授权的用户组可以使用该服务", + "Allow the following groups to access the editors": "让以下用户组访问编辑器", "review" : "审阅", "form filling" : "表单填报", "comment" : "评论", - "custom filter" : "自定义筛选器", - "download" : "下载", + "global filter": "全局过滤器", + "download": "下载", "Server settings" : "服务器设置", "Common settings" : "常用设置", "Editor customization settings" : "编辑器自定义设置", @@ -64,10 +64,10 @@ OC.L10N.register( "File saved" : "文件已保存", "Insert image" : "插入图片", "Select recipients" : "选择接收者", - "Connect to demo ONLYOFFICE Docs server" : "连接到 ONLYOFFICE Docs 服务器的演示", - "This is a public test server, please do not use it for private sensitive data. The server will be available during a 30-day period." : "这是公开的测试服务器,请勿用于隐私数据。服务器试用期限为30天。", - "The 30-day test period is over, you can no longer connect to demo ONLYOFFICE Docs server." : "30天试用期已结束,无法连接ONLYOFFICE Docs 服务器的演示。", - "You are using public demo ONLYOFFICE Docs server. Please do not store private sensitive data." : "您正在使用公开ONLYOFFICE Docs服务器的演示,请勿存储隐私数据。", + "Connect to demo ONLYOFFICE Docs server": "连接到 ONLYOFFICE Docs 演示服务器", + "This is a public test server, please do not use it for private sensitive data. The server will be available during a 30-day period.": "这是公开的测试服务器,请勿存储隐私数据。服务器试用期限为30天。", + "The 30-day test period is over, you can no longer connect to demo ONLYOFFICE Docs server.": "30天试用期已结束,无法连接ONLYOFFICE Docs 演示服务器。", + "You are using public demo ONLYOFFICE Docs server. Please do not store private sensitive data.": "您正在使用 ONLYOFFICE Docs 公开的演示服务器,请勿存储隐私数据。", "Select file to compare" : "选择文件比较", "Review mode for viewing": "审阅模式浏览", "Markup": "修订", @@ -95,17 +95,16 @@ OC.L10N.register( "File has been converted. Its content might look different.": "文件已被转换。其内容可能看起来有所不同。", "Download as": "下载为", "Download": "下载", - "Origin format": "原产地格式", + "Origin format": "原格式", "Failed to send notification": "发送通知失败", "Notification sent successfully": "通知发送成功", "%1\$s mentioned in the %2\$s: \"%3\$s\".": "%1\$s 在 %2\$s: \"%3\$s\"中提到了您.", - "Choose a format to convert {fileName}": "选择要转换{fileName}的格式", - "Form template": "表单模板", - "Form template from existing text file": "用现有的文本文件创建模板", + "Choose a format to convert {fileName}": "转换 {fileName} 为选中的格式", + "PDF form": "PDF 表单", + "PDF form from existing text file": "用文本文件生成 PDF 表单", "Create form": "创建表单", "Fill in form in ONLYOFFICE": "在ONLYOFFICE上填写表单", - "Create new Form template": "创建新的表单模板", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "请将ONLYOFFICE Docs更新到7.0版本,以便在线编辑可填写的表单", + "Create new PDF form": "创建新的 PDF 表单", "Security": "安全", "Run document macros": "运行文档宏", "Default editor theme": "编辑器默认的主题", @@ -123,6 +122,11 @@ OC.L10N.register( "View settings": "查看设置", "ONLYOFFICE Docs Cloud": "ONLYOFFICE 文档云", "Easily launch the editors in the cloud without downloading and installation": "无需下载和安装即可轻松启动云端编辑器", - "Get Now": "立即获取" + "Get Now": "立即获取", + "Select file to combine": "选择要合并的文件", + "Select data source": "选择数据源", + "The data source must not be the current document": "数据源不应该是当前文档", + "Enable background connection check to the editors": "启用编辑器后台连接检查", + "The domain in the file url does not match the domain of the Document server": "文件 URL 中的域与文档服务器的域不匹配" }, "nplurals=1; plural=0;"); diff --git a/l10n/zh_CN.json b/l10n/zh_CN.json index dec2a79e..9d9170ca 100644 --- a/l10n/zh_CN.json +++ b/l10n/zh_CN.json @@ -1,23 +1,23 @@ { "translations": { "Access denied" : "禁止访问", - "Invalid request" : "非法请求", + "Invalid request" : "无效请求", "Files not found" : "文件未找到", "File not found" : "文件未找到", "Not permitted" : "没有权限", "Download failed" : "下载失败", - "The required folder was not found" : "必须的文件夹未找到", - "You don't have enough permission to create" : "没有足够权限创建文件", + "The required folder was not found" : "未找到所需文件夹", + "You don't have enough permission to create" : "没有足够权限创建文档", "Template not found" : "模板未找到", "Can't create file" : "无法创建文件", - "Format is not supported" : "文件格式不支持", - "Conversion is not required" : "无需文件转换", - "Failed to download converted file" : "转换后的文件下载失败", + "Format is not supported" : "格式不支持", + "Conversion is not required" : "无需转换", + "Failed to download converted file" : "无法下载转换后的文件", "ONLYOFFICE app is not configured. Please contact admin" : "ONLYOFFICE未配置,请联系管理员。", "FileId is empty" : "文件ID为空", "You do not have enough permissions to view the file" : "您没有足够权限浏览该文件", "Error occurred in the document service" : "文档服务内部发生异常", "Not supported version" : "不支持的版本", - "ONLYOFFICE cannot be reached. Please contact admin" : "ONLYOFFICE服务器无法连接,请联系管理员。", + "ONLYOFFICE cannot be reached. Please contact admin" : "ONLYOFFICE 服务器无法连接,请联系管理员。", "Loading, please wait." : "载入中,请稍后...", "File created" : "文件已创建", "Open in ONLYOFFICE" : "用 ONLYOFFICE 打开", @@ -25,7 +25,7 @@ "Document" : "文档", "Spreadsheet" : "电子表格", "Presentation" : "演示文稿", - "Error when trying to connect" : "连接是发生异常", + "Error when trying to connect" : "连接时发生异常", "Settings have been successfully updated" : "设置已保存", "Server can't read xml" : "服务器无法读取XML", "Bad Response. Errors: " : "错误的返回: ", @@ -34,20 +34,20 @@ "Encryption App is enabled, the application cannot work. You can continue working with the application if you enable master key." : "加密应用程序已启用,应用程序无法工作。如果启用主密钥,则可以继续使用该应用程序。", "ONLYOFFICE Docs address" : "ONLYOFFICE Docs地址", "Advanced server settings" : "更多设置", - "ONLYOFFICE Docs address for internal requests from the server" : "用于服务器内部的ONLYOFFICE Docs的地址", - "Server address for internal requests from ONLYOFFICE Docs" : "用于ONLYOFFICE Docs内部请求的服务器的地址", + "ONLYOFFICE Docs address for internal requests from the server" : "服务器内部请求 ONLYOFFICE Docs 的地址", + "Server address for internal requests from ONLYOFFICE Docs" : "ONLYOFFICE Docs 内部请求服务器的地址", "Secret key (leave blank to disable)" : "秘钥(留空为关闭)", - "Open file in the same tab" : "在相同的切签中打开", + "Open file in the same tab" : "在相同的标签页中打开", "The default application for opening the format": "默认关联的文件格式", "Open the file for editing (due to format restrictions, the data might be lost when saving to the formats from the list below)" : "默认的文件编辑器 (由于文件格式限制,保存为下列格式时,数据可能会缺失)", "View details" : "查看详情", "Save" : "保存", "Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required." : "不允许混合活动内容,请使用HTTPS连接ONLYOFFICE Docs。", - "Restrict access to editors to following groups" : "仅授权的用户组可以使用该服务", + "Allow the following groups to access the editors" : "让以下用户组访问编辑器", "review" : "审阅", "form filling" : "表单填报", "comment" : "评论", - "custom filter" : "自定义筛选器", + "global filter" : "全局过滤器", "download" : "下载", "Server settings" : "服务器设置", "Common settings" : "常用设置", @@ -62,10 +62,10 @@ "File saved" : "文件已保存", "Insert image" : "插入图片", "Select recipients" : "选择接收者", - "Connect to demo ONLYOFFICE Docs server" : "连接到 ONLYOFFICE Docs 服务器的演示", - "This is a public test server, please do not use it for private sensitive data. The server will be available during a 30-day period." : "这是公开的测试服务器,请勿用于隐私数据。服务器试用期限为30天。", - "The 30-day test period is over, you can no longer connect to demo ONLYOFFICE Docs server." : "30天试用期已结束,无法连接ONLYOFFICE Docs 服务器的演示。", - "You are using public demo ONLYOFFICE Docs server. Please do not store private sensitive data." : "您正在使用公开ONLYOFFICE Docs服务器的演示,请勿存储隐私数据。", + "Connect to demo ONLYOFFICE Docs server" : "连接到 ONLYOFFICE Docs 演示服务器", + "This is a public test server, please do not use it for private sensitive data. The server will be available during a 30-day period." : "这是公开的测试服务器,请勿存储隐私数据。服务器试用期限为30天。", + "The 30-day test period is over, you can no longer connect to demo ONLYOFFICE Docs server." : "30天试用期已结束,无法连接ONLYOFFICE Docs 演示服务器。", + "You are using public demo ONLYOFFICE Docs server. Please do not store private sensitive data." : "您正在使用 ONLYOFFICE Docs 公开的演示服务器,请勿存储隐私数据。", "Select file to compare" : "选择文件比较", "Review mode for viewing": "审阅模式浏览", "Markup": "修订", @@ -93,17 +93,16 @@ "File has been converted. Its content might look different.": "文件已被转换。其内容可能看起来有所不同。", "Download as": "下载为", "Download": "下载", - "Origin format": "原产地格式", + "Origin format": "原格式", "Failed to send notification": "发送通知失败", "Notification sent successfully": "通知发送成功", "%1$s mentioned in the %2$s: \"%3$s\".": "%1$s 在 %2$s: \"%3$s\"中提到了您.", - "Choose a format to convert {fileName}": "选择要转换{fileName}的格式", - "Form template": "表单模板", - "Form template from existing text file": "用现有的文本文件创建模板", + "Choose a format to convert {fileName}": "转换 {fileName} 为选中的格式", + "PDF form": "PDF 表单", + "PDF form from existing text file": "用文本文件生成 PDF 表单", "Create form": "创建表单", "Fill in form in ONLYOFFICE": "在ONLYOFFICE上填写表单", - "Create new Form template": "创建新的表单模板", - "Please update ONLYOFFICE Docs to version 7.0 to work on fillable forms online": "请将ONLYOFFICE Docs更新到7.0版本,以便在线编辑可填写的表单", + "Create new PDF form": "创建新的 PDF 表单", "Security": "安全", "Run document macros": "运行文档宏", "Default editor theme": "编辑器默认的主题", @@ -121,6 +120,11 @@ "View settings": "查看设置", "ONLYOFFICE Docs Cloud": "ONLYOFFICE 文档云", "Easily launch the editors in the cloud without downloading and installation": "无需下载和安装即可轻松启动云端编辑器", - "Get Now": "立即获取" + "Get Now": "立即获取", + "Select file to combine" : "选择要合并的文件", + "Select data source": "选择数据源", + "The data source must not be the current document": "数据源不应该是当前文档", + "Enable background connection check to the editors": "启用编辑器后台连接检查", + "The domain in the file url does not match the domain of the Document server": "文件 URL 中的域与文档服务器的域不匹配" },"pluralForm" :"nplurals=1; plural=0;" } \ No newline at end of file diff --git a/lib/adminsettings.php b/lib/adminsettings.php index 84994fa3..30acaa54 100644 --- a/lib/adminsettings.php +++ b/lib/adminsettings.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,34 +26,36 @@ * Settings controller for the administration page */ class AdminSettings implements ISettings { + /** + * Constructor + */ + public function __construct() { + } - public function __construct() { - } + /** + * Print config section + * + * @return TemplateResponse + */ + public function getPanel() { + return $this->getForm(); + } - /** - * Print config section - * - * @return TemplateResponse - */ - public function getPanel() { - return $this->getForm(); - } + /** + * Get section ID + * + * @return string + */ + public function getSectionID() { + return "general"; + } - /** - * Get section ID - * - * @return string - */ - public function getSectionID() { - return "general"; - } - - /** - * Get priority order - * - * @return int - */ - public function getPriority() { - return 50; - } + /** + * Get priority order + * + * @return int + */ + public function getPriority() { + return 50; + } } diff --git a/lib/appconfig.php b/lib/appconfig.php index 0dd9b699..37c11e2e 100644 --- a/lib/appconfig.php +++ b/lib/appconfig.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +23,8 @@ use \DateInterval; use \DateTime; +use OCP\ICache; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\ILogger; @@ -31,1234 +34,1409 @@ * @package OCA\Onlyoffice */ class AppConfig { - - /** - * Application name - * - * @var string - */ - private $appName; - - /** - * Config service - * - * @var IConfig - */ - private $config; - - /** - * Logger - * - * @var ILogger - */ - private $logger; - - /** - * The config key for the demo server - * - * @var string - */ - private $_demo = "demo"; - - /** - * The config key for the document server address - * - * @var string - */ - private $_documentserver = "DocumentServerUrl"; - - /** - * The config key for the document server address available from ownCloud - * - * @var string - */ - private $_documentserverInternal = "DocumentServerInternalUrl"; - - /** - * The config key for the ownCloud address available from document server - * - * @var string - */ - private $_storageUrl = "StorageUrl"; - - /** - * The config key for the secret key - * - * @var string - */ - private $_cryptSecret = "secret"; - - /** - * The config key for the default formats - * - * @var string - */ - private $_defFormats = "defFormats"; - - /** - * The config key for the editable formats - * - * @var string - */ - private $_editFormats = "editFormats"; - - /** - * The config key for the setting same tab - * - * @var string - */ - private $_sameTab = "sameTab"; - - /** - * The config key for the generate preview - * - * @var string - */ - private $_preview = "preview"; - - /** - * The config key for the keep versions history - * - * @var string - */ - private $_versionHistory = "versionHistory"; - - /** - * The config key for the protection - * - * @var string - */ - private $_protection = "protection"; - - /** - * The config key for the chat display setting - * - * @var string - */ - private $_customizationChat = "customizationChat"; - - /** - * The config key for display the header more compact setting - * - * @var string - */ - private $_customizationCompactHeader = "customizationCompactHeader"; - - /** - * The config key for the feedback display setting - * - * @var string - */ - private $_customizationFeedback = "customizationFeedback"; - - /** - * The config key for the forcesave setting - * - * @var string - */ - private $_customizationForcesave = "customizationForcesave"; - - /** - * The config key for the help display setting - * - * @var string - */ - private $_customizationHelp = "customizationHelp"; - - /** - * The config key for the no tabs setting - * - * @var string - */ - private $_customizationToolbarNoTabs = "customizationToolbarNoTabs"; - - /** - * The config key for the review mode setting - * - * @var string - */ - private $_customizationReviewDisplay = "customizationReviewDisplay"; - - /** - * The config key for the theme setting - * - * @var string - */ - private $_customizationTheme = "customizationTheme"; - - /** - * The config key for the setting limit groups - * - * @var string - */ - private $_groups = "groups"; - - /** - * The config key for the verification - * - * @var string - */ - private $_verification = "verify_peer_off"; - - /** - * The config key for the secret key in jwt - * - * @var string - */ - private $_jwtSecret = "jwt_secret"; - - /** - * The config key for the jwt header - * - * @var string - */ - private $_jwtHeader = "jwt_header"; - - /** - * The config key for the allowable leeway in Jwt checks - * - * @var string - */ - private $_jwtLeeway = "jwt_leeway"; - - /** - * The config key for the settings error - * - * @var string - */ - private $_settingsError = "settings_error"; - - /** - * The config key for limit thumbnail size - * - * @var string - */ - public $_limitThumbSize = "limit_thumb_size"; - - /** - * The config key for the customer - * - * @var string - */ - public $_customization_customer = "customization_customer"; - - /** - * The config key for the loaderLogo - * - * @var string - */ - public $_customization_loaderLogo = "customization_loaderLogo"; - - /** - * The config key for the loaderName - * - * @var string - */ - public $_customization_loaderName = "customization_loaderName"; - - /** - * The config key for the logo - * - * @var string - */ - public $_customization_logo = "customization_logo"; - - /** - * The config key for the zoom - * - * @var string - */ - public $_customization_zoom = "customization_zoom"; - - /** - * The config key for the autosave - * - * @var string - */ - public $_customization_autosave = "customization_autosave"; - - /** - * The config key for the goback - * - * @var string - */ - public $_customization_goback = "customization_goback"; - - /** - * The config key for the macros - * - * @var string - */ - public $_customization_macros = "customization_macros"; - - /** - * The config key for the plugins - * - * @var string - */ - public $_customizationPlugins = "customization_plugins"; - - /** - * The config key for the interval of editors availability check by cron - * - * @var string - */ - private $_editors_check_interval = "editors_check_interval"; - - /** - * @param string $AppName - application name - */ - public function __construct($AppName) { - - $this->appName = $AppName; - - $this->config = \OC::$server->getConfig(); - $this->logger = \OC::$server->getLogger(); - } - - /** - * Get value from the system configuration - * - * @param string $key - key configuration - * @param bool $system - get from root or from app section - * - * @return string - */ - public function GetSystemValue($key, $system = false) { - if ($system) { - return $this->config->getSystemValue($key); - } - if (!empty($this->config->getSystemValue($this->appName)) - && array_key_exists($key, $this->config->getSystemValue($this->appName))) { - return $this->config->getSystemValue($this->appName)[$key]; - } - return null; - } - - /** - * Switch on demo server - * - * @param bool $value - select demo - * - * @return bool - */ - public function SelectDemo($value) { - $this->logger->info("Select demo: " . json_encode($value), ["app" => $this->appName]); - - $data = $this->GetDemoData(); - - if ($value === true && !$data["available"]) { - $this->logger->info("Trial demo is overdue: " . json_encode($data), ["app" => $this->appName]); - return false; - } - - $data["enabled"] = $value === true; - if (!isset($data["start"])) { - $data["start"] = new DateTime(); - } - - $this->config->setAppValue($this->appName, $this->_demo, json_encode($data)); - return true; - } - - /** - * Get demo data - * - * @return array - */ - public function GetDemoData() { - $data = $this->config->getAppValue($this->appName, $this->_demo, ""); - - if (empty($data)) { - return [ - "available" => true, - "enabled" => false - ]; - } - $data = json_decode($data, true); - - $overdue = new DateTime(isset($data["start"]) ? $data["start"]["date"] : null); - $overdue->add(new DateInterval("P" . $this->DEMO_PARAM["TRIAL"] . "D")); - if ($overdue > new DateTime()) { - $data["available"] = true; - $data["enabled"] = $data["enabled"] === true; - } else { - $data["available"] = false; - $data["enabled"] = false; - } - - return $data; - } - - /** - * Get status of demo server - * - * @return bool - */ - public function UseDemo() { - return $this->GetDemoData()["enabled"] === true; - } - - /** - * Save the document service address to the application configuration - * - * @param string $documentServer - document service address - */ - public function SetDocumentServerUrl($documentServer) { - $documentServer = trim($documentServer); - if (strlen($documentServer) > 0) { - $documentServer = rtrim($documentServer, "/") . "/"; - if (!preg_match("/(^https?:\/\/)|^\//i", $documentServer)) { - $documentServer = "http://" . $documentServer; - } - } - - $this->logger->info("SetDocumentServerUrl: $documentServer", ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_documentserver, $documentServer); - } - - /** - * Get the document service address from the application configuration - * - * @param bool $origin - take origin - * - * @return string - */ - public function GetDocumentServerUrl($origin = false) { - if (!$origin && $this->UseDemo()) { - return $this->DEMO_PARAM["ADDR"]; - } - - $url = $this->config->getAppValue($this->appName, $this->_documentserver, ""); - if (empty($url)) { - $url = $this->GetSystemValue($this->_documentserver); - } - if ($url !== "/") { - $url = rtrim($url, "/"); - if (strlen($url) > 0) { - $url = $url . "/"; - } - } - return $url; - } - - /** - * Save the document service address available from ownCloud to the application configuration - * - * @param string $documentServerInternal - document service address - */ - public function SetDocumentServerInternalUrl($documentServerInternal) { - $documentServerInternal = rtrim(trim($documentServerInternal), "/"); - if (strlen($documentServerInternal) > 0) { - $documentServerInternal = $documentServerInternal . "/"; - if (!preg_match("/^https?:\/\//i", $documentServerInternal)) { - $documentServerInternal = "http://" . $documentServerInternal; - } - } - - $this->logger->info("SetDocumentServerInternalUrl: $documentServerInternal", ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_documentserverInternal, $documentServerInternal); - } - - /** - * Get the document service address available from ownCloud from the application configuration - * - * @param bool $origin - take origin - * - * @return string - */ - public function GetDocumentServerInternalUrl($origin = false) { - if (!$origin && $this->UseDemo()) { - return $this->GetDocumentServerUrl(); - } - - $url = $this->config->getAppValue($this->appName, $this->_documentserverInternal, ""); - if (empty($url)) { - $url = $this->GetSystemValue($this->_documentserverInternal); - } - if (!$origin && empty($url)) { - $url = $this->GetDocumentServerUrl(); - } - return $url; - } - - /** - * Replace domain in document server url with internal address from configuration - * - * @param string $url - document server url - * - * @return string - */ - public function ReplaceDocumentServerUrlToInternal($url) { - $documentServerUrl = $this->GetDocumentServerInternalUrl(); - if (!empty($documentServerUrl)) { - $from = $this->GetDocumentServerUrl(); - - if (!preg_match("/^https?:\/\//i", $from)) { - $parsedUrl = parse_url($url); - $from = $parsedUrl["scheme"] . "://" . $parsedUrl["host"] . (array_key_exists("port", $parsedUrl) ? (":" . $parsedUrl["port"]) : "") . $from; - } - - if ($from !== $documentServerUrl) - { - $this->logger->debug("Replace url from $from to $documentServerUrl", ["app" => $this->appName]); - $url = str_replace($from, $documentServerUrl, $url); - } - } - - return $url; - } - - /** - * Save the ownCloud address available from document server to the application configuration - * - * @param string $documentServer - document service address - */ - public function SetStorageUrl($storageUrl) { - $storageUrl = rtrim(trim($storageUrl), "/"); - if (strlen($storageUrl) > 0) { - $storageUrl = $storageUrl . "/"; - if (!preg_match("/^https?:\/\//i", $storageUrl)) { - $storageUrl = "http://" . $storageUrl; - } - } - - $this->logger->info("SetStorageUrl: $storageUrl", ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_storageUrl, $storageUrl); - } - - /** - * Get the ownCloud address available from document server from the application configuration - * - * @return string - */ - public function GetStorageUrl() { - $url = $this->config->getAppValue($this->appName, $this->_storageUrl, ""); - if (empty($url)) { - $url = $this->GetSystemValue($this->_storageUrl); - } - return $url; - } - - /** - * Save the document service secret key to the application configuration - * - * @param string $secret - secret key - */ - public function SetDocumentServerSecret($secret) { - $secret = trim($secret); - if (empty($secret)) { - $this->logger->info("Clear secret key", ["app" => $this->appName]); - } else { - $this->logger->info("Set secret key", ["app" => $this->appName]); - } - - $this->config->setAppValue($this->appName, $this->_jwtSecret, $secret); - } - - /** - * Get the document service secret key from the application configuration - * - * @param bool $origin - take origin - * - * @return string - */ - public function GetDocumentServerSecret($origin = false) { - if (!$origin && $this->UseDemo()) { - return $this->DEMO_PARAM["SECRET"]; - } - - $secret = $this->config->getAppValue($this->appName, $this->_jwtSecret, ""); - if (empty($secret)) { - $secret = $this->GetSystemValue($this->_jwtSecret); - } - return $secret; - } - - /** - * Get the secret key from the application configuration - * - * @return string - */ - public function GetSKey() { - $secret = $this->GetDocumentServerSecret(); - if (empty($secret)) { - $secret = $this->GetSystemValue($this->_cryptSecret, true); - } - return $secret; - } - - /** - * Save an array of formats with default action - * - * @param array $formats - formats with status - */ - public function SetDefaultFormats($formats) { - $value = json_encode($formats); - $this->logger->info("Set default formats: $value", ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_defFormats, $value); - } - - /** - * Get an array of formats with default action - * - * @return array - */ - private function GetDefaultFormats() { - $value = $this->config->getAppValue($this->appName, $this->_defFormats, ""); - if (empty($value)) { - return array(); - } - return json_decode($value, true); - } - - /** - * Save an array of formats that is opened for editing - * - * @param array $formats - formats with status - */ - public function SetEditableFormats($formats) { - $value = json_encode($formats); - $this->logger->info("Set editing formats: $value", ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_editFormats, $value); - } - - /** - * Get an array of formats opening for editing - * - * @return array - */ - private function GetEditableFormats() { - $value = $this->config->getAppValue($this->appName, $this->_editFormats, ""); - if (empty($value)) { - return array(); - } - return json_decode($value, true); - } - - /** - * Save the opening setting in a same tab - * - * @param bool $value - same tab - */ - public function SetSameTab($value) { - $this->logger->info("Set opening in a same tab: " . json_encode($value), ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_sameTab, json_encode($value)); - } - - /** - * Get the opening setting in a same tab - * - * @return bool - */ - public function GetSameTab() { - return $this->config->getAppValue($this->appName, $this->_sameTab, "false") === "true"; - } - - /** - * Save generate preview setting - * - * @param bool $value - preview - */ - public function SetPreview($value) { - $this->logger->info("Set generate preview: " . json_encode($value), ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_preview, json_encode($value)); - } - - /** - * Get generate preview setting - * - * @return bool - */ - public function GetPreview() { - return $this->config->getAppValue($this->appName, $this->_preview, "true") === "true"; - } - - /** - * Save keep versions history - * - * @param bool $value - version history - */ - public function SetVersionHistory($value) { - $this->logger->info("Set keep versions history: " . json_encode($value), ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_versionHistory, json_encode($value)); - } - - /** - * Get keep versions history - * - * @return bool - */ - public function GetVersionHistory() { - return $this->config->getAppValue($this->appName, $this->_versionHistory, "true") === "true"; - } - - /** - * Save protection - * - * @param bool $value - version history - */ - public function SetProtection($value) { - $this->logger->info("Set protection: " . $value, ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_protection, $value); - } - - /** - * Get protection - * - * @return bool - */ - public function GetProtection() { - $value = $this->config->getAppValue($this->appName, $this->_protection, "owner"); - if ($value === "all") { - return "all"; - } - return "owner"; - } - - /** - * Save chat display setting - * - * @param bool $value - display chat - */ - public function SetCustomizationChat($value) { - $this->logger->info("Set chat display: " . json_encode($value), ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_customizationChat, json_encode($value)); - } - - /** - * Get chat display setting - * - * @return bool - */ - public function GetCustomizationChat() { - return $this->config->getAppValue($this->appName, $this->_customizationChat, "true") === "true"; - } - - /** - * Save compact header setting - * - * @param bool $value - display compact header - */ - public function SetCustomizationCompactHeader($value) { - $this->logger->info("Set compact header display: " . json_encode($value), ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_customizationCompactHeader, json_encode($value)); - } - - /** - * Get compact header setting - * - * @return bool - */ - public function GetCustomizationCompactHeader() { - return $this->config->getAppValue($this->appName, $this->_customizationCompactHeader, "true") === "true"; - } - - /** - * Save feedback display setting - * - * @param bool $value - display feedback - */ - public function SetCustomizationFeedback($value) { - $this->logger->info("Set feedback display: " . json_encode($value), ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_customizationFeedback, json_encode($value)); - } - - /** - * Get feedback display setting - * - * @return bool - */ - public function GetCustomizationFeedback() { - return $this->config->getAppValue($this->appName, $this->_customizationFeedback, "true") === "true"; - } - - /** - * Save forcesave setting - * - * @param bool $value - forcesave - */ - public function SetCustomizationForcesave($value) { - $this->logger->info("Set forcesave: " . json_encode($value), ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_customizationForcesave, json_encode($value)); - } - - /** - * Get forcesave setting - * - * @return bool - */ - public function GetCustomizationForcesave() { - $value = $this->config->getAppValue($this->appName, $this->_customizationForcesave, "false") === "true"; - - return $value && ($this->checkEncryptionModule() === false); - } - - /** - * Save help display setting - * - * @param bool $value - display help - */ - public function SetCustomizationHelp($value) { - $this->logger->info("Set help display: " . json_encode($value), ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_customizationHelp, json_encode($value)); - } - - /** - * Get help display setting - * - * @return bool - */ - public function GetCustomizationHelp() { - return $this->config->getAppValue($this->appName, $this->_customizationHelp, "true") === "true"; - } - - /** - * Save without tabs setting - * - * @param bool $value - without tabs - */ - public function SetCustomizationToolbarNoTabs($value) { - $this->logger->info("Set without tabs: " . json_encode($value), ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_customizationToolbarNoTabs, json_encode($value)); - } - - /** - * Get without tabs setting - * - * @return bool - */ - public function GetCustomizationToolbarNoTabs() { - return $this->config->getAppValue($this->appName, $this->_customizationToolbarNoTabs, "true") === "true"; - } - - /** - * Save review viewing mode setting - * - * @param string $value - review mode - */ - public function SetCustomizationReviewDisplay($value) { - $this->logger->info("Set review mode: " . $value, array("app" => $this->appName)); - - $this->config->setAppValue($this->appName, $this->_customizationReviewDisplay, $value); - } - - /** - * Get review viewing mode setting - * - * @return string - */ - public function GetCustomizationReviewDisplay() { - $value = $this->config->getAppValue($this->appName, $this->_customizationReviewDisplay, "original"); - if ($value === "markup") { - return "markup"; - } - if ($value === "final") { - return "final"; - } - return "original"; - } - - /** - * Save theme setting - * - * @param string $value - theme - */ - public function SetCustomizationTheme($value) { - $this->logger->info("Set theme: " . $value, array("app" => $this->appName)); - - $this->config->setAppValue($this->appName, $this->_customizationTheme, $value); - } - - /** - * Get theme setting - * - * @return string - */ - public function GetCustomizationTheme() { - $value = $this->config->getAppValue($this->appName, $this->_customizationTheme, "theme-classic-light"); - if ($value === "theme-light") { - return "theme-light"; - } - if ($value === "theme-dark") { - return "theme-dark"; - } - return "theme-classic-light"; - } - - /** - * Save macros setting - * - * @param bool $value - enable macros - */ - public function SetCustomizationMacros($value) { - $this->logger->info("Set macros enabled: " . json_encode($value), ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_customization_macros, json_encode($value)); - } - - /** - * Get macros setting - * - * @return bool - */ - public function GetCustomizationMacros() { - return $this->config->getAppValue($this->appName, $this->_customization_macros, "true") === "true"; - } - - /** - * Save plugins setting - * - * @param bool $value - enable macros - */ - public function SetCustomizationPlugins($value) { - $this->logger->info("Set plugins enabled: " . json_encode($value), ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_customizationPlugins, json_encode($value)); - } - - /** - * Get plugins setting - * - * @return bool - */ - public function GetCustomizationPlugins() { - return $this->config->getAppValue($this->appName, $this->_customizationPlugins, "true") === "true"; - } - - /** - * Save the list of groups - * - * @param array $groups - the list of groups - */ - public function SetLimitGroups($groups) { - if (!is_array($groups)) { - $groups = array(); - } - $value = json_encode($groups); - $this->logger->info("Set groups: $value", ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_groups, $value); - } - - /** - * Get the list of groups - * - * @return array - */ - public function GetLimitGroups() { - $value = $this->config->getAppValue($this->appName, $this->_groups, ""); - if (empty($value)) { - return array(); - } - $groups = json_decode($value, true); - if (!is_array($groups)) { - $groups = array(); - } - return $groups; - } - - /** - * Check access for group - * - * @param string $userId - user identifier - * - * @return bool - */ - public function isUserAllowedToUse($userId = null) { - // no user -> no - $userSession = \OC::$server->getUserSession(); - if (is_null($userId) && ($userSession === null || !$userSession->isLoggedIn())) { - return false; - } - - $groups = $this->GetLimitGroups(); - // no group set -> all users are allowed - if (count($groups) === 0) { - return true; - } - - if (is_null($userId)) { - $user = $userSession->getUser(); - } else { - $user = \OC::$server->getUserManager()->get($userId); - if (empty($user)) { - return false; - } - } - - foreach ($groups as $groupName) { - // group unknown -> error and allow nobody - $group = \OC::$server->getGroupManager()->get($groupName); - if ($group === null) { - \OC::$server->getLogger()->error("Group is unknown $groupName", ["app" => $this->appName]); - $this->SetLimitGroups(array_diff($groups, [$groupName])); - } else { - if ($group->inGroup($user)) { - return true; - } - } - } - - return false; - } - - /** - * Save the document service verification setting to the application configuration - * - * @param bool $verifyPeerOff - parameter verification setting - */ - public function SetVerifyPeerOff($verifyPeerOff) { - $this->logger->info("SetVerifyPeerOff " . json_encode($verifyPeerOff), ["app" => $this->appName]); - - $this->config->setAppValue($this->appName, $this->_verification, json_encode($verifyPeerOff)); - } - - /** - * Get the document service verification setting to the application configuration - * - * @return bool - */ - public function GetVerifyPeerOff() { - $turnOff = $this->config->getAppValue($this->appName, $this->_verification, ""); - - if (!empty($turnOff)) { - return $turnOff === "true"; - } - - return $this->GetSystemValue($this->_verification); - } - - /** - * Get the limit on size document when generating thumbnails - * - * @return int - */ - public function GetLimitThumbSize() { - $limitSize = (integer)$this->GetSystemValue($this->_limitThumbSize); - - if (!empty($limitSize)) { - return $limitSize; - } - - return 100*1024*1024; - } - - /** - * Get the jwt header setting - * - * @param bool $origin - take origin - * - * @return string - */ - public function JwtHeader($origin = false) { - if (!$origin && $this->UseDemo()) { - return $this->DEMO_PARAM["HEADER"]; - } - - $header = $this->config->getAppValue($this->appName, $this->_jwtHeader, ""); - if (empty($header)) { - $header = $this->GetSystemValue($this->_jwtHeader); - } - if (!$origin && empty($header)) { - $header = "Authorization"; - } - return $header; - } - - /** - * Save the jwtHeader setting - * - * @param string $value - jwtHeader - */ - public function SetJwtHeader($value) { - $value = trim($value); - if (empty($value)) { - $this->logger->info("Clear header key", ["app" => $this->appName]); - } else { - $this->logger->info("Set header key " . $value, ["app" => $this->appName]); - } - - $this->config->setAppValue($this->appName, $this->_jwtHeader, $value); - } - - /** - * Get the Jwt Leeway - * - * @return int - */ - public function GetJwtLeeway() { - $jwtLeeway = (integer)$this->GetSystemValue($this->_jwtLeeway); - - return $jwtLeeway; - } - - /** - * Save the status settings - * - * @param string $value - error - */ - public function SetSettingsError($value) { - $this->config->setAppValue($this->appName, $this->_settingsError, $value); - } - - /** - * Get the status settings - * - * @return bool - */ - public function SettingsAreSuccessful() { - return empty($this->config->getAppValue($this->appName, $this->_settingsError, "")); - } - - /** - * Checking encryption enabled - * - * @return string|bool - */ - public function checkEncryptionModule() { - if (!\OC::$server->getAppManager()->isInstalled("encryption")) { - return false; - } - if (!\OC::$server->getEncryptionManager()->isEnabled()) { - return false; - } - - $crypt = new \OCA\Encryption\Crypto\Crypt(\OC::$server->getLogger(), \OC::$server->getUserSession(), \OC::$server->getConfig(), \OC::$server->getL10N("encryption")); - $util = new \OCA\Encryption\Util(new \OC\Files\View(), $crypt, \OC::$server->getLogger(), \OC::$server->getUserSession(), \OC::$server->getConfig(), \OC::$server->getUserManager()); - if ($util->isMasterKeyEnabled()) { - return "master"; - } - - return true; - } - - /** - * Get supported formats - * - * @return array - * - * @NoAdminRequired - */ - public function FormatsSetting() { - $result = $this->formats; - - $defFormats = $this->GetDefaultFormats(); - foreach ($defFormats as $format => $setting) { - if (array_key_exists($format, $result)) { - $result[$format]["def"] = ($setting === true || $setting === "true"); - } - } - - $editFormats = $this->GetEditableFormats(); - foreach ($editFormats as $format => $setting) { - if (array_key_exists($format, $result)) { - $result[$format]["edit"] = ($setting === true || $setting === "true"); - } - } - - return $result; - } - - public function ShareAttributesVersion() { - if (\version_compare(\implode(".", \OCP\Util::getVersion()), "10.3.0", ">=")) { - return "v2"; - } else if (\version_compare(\implode(".", \OCP\Util::getVersion()), "10.2.0", ">=")) { - return "v1"; - } - return ""; - } - - /** - * Get the editors check interval - * - * @return int - */ - public function GetEditorsCheckInterval() { - $interval = $this->GetSystemValue($this->_editors_check_interval); - - if (empty($interval) && $interval !== 0) { - $interval = 60*60*24; - } - return (integer)$interval; - } - - /** - * Additional data about formats - * - * @var array - */ - private $formats = [ - "csv" => [ "mime" => "text/csv", "type" => "cell", "edit" => true, "editable" => true, "saveas" => ["ods", "pdf", "xlsx"] ], - "doc" => [ "mime" => "application/msword", "type" => "word", "conv" => true, "saveas" => ["docx", "odt", "pdf", "rtf", "txt"] ], - "docm" => [ "mime" => "application/vnd.ms-word.document.macroEnabled.12", "type" => "word", "conv" => true, "saveas" => ["docx", "odt", "pdf", "rtf", "txt"] ], - "docx" => [ "mime" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "type" => "word", "edit" => true, "def" => true, "review" => true, "comment" => true, "saveas" => ["odt", "pdf", "rtf", "txt"] ], - "docxf" => [ "mime" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf", "type" => "word", "edit" => true, "def" => true, "review" => true, "comment" => true, "saveas" => ["odt", "pdf", "rtf", "txt"], "createForm" => true ], - "oform" => [ "mime" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform", "type" => "word", "fillForms" => true, "def" => true ], - "dot" => [ "type" => "word", "conv" => true, "saveas" => ["docx", "odt", "pdf", "rtf", "txt"] ], - "dotx" => [ "mime" => "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "type" => "word", "conv" => true, "saveas" => ["docx", "odt", "pdf", "rtf", "txt"] ], - "epub" => [ "mime" => "application/epub+zip", "type" => "word", "conv" => true, "saveas" => ["docx", "odt", "pdf", "rtf", "txt"] ], - "htm" => [ "type" => "word", "conv" => true ], - "html" => [ "mime" => "text/html", "type" => "word", "conv" => true, "saveas" => ["docx", "odt", "pdf", "rtf", "txt"] ], - "odp" => [ "mime" => "application/vnd.oasis.opendocument.presentation", "type" => "slide", "conv" => true, "editable" => true, "saveas" => ["pdf", "pptx"] ], - "ods" => [ "mime" => "application/vnd.oasis.opendocument.spreadsheet", "type" => "cell", "conv" => true, "editable" => true, "saveas" => ["csv", "pdf", "xlsx"] ], - "odt" => [ "mime" => "application/vnd.oasis.opendocument.text", "type" => "word", "conv" => true, "editable" => true, "saveas" => ["docx", "pdf", "rtf", "txt"] ], - "otp" => [ "mime" => "application/vnd.oasis.opendocument.presentation-template", "type" => "slide", "conv" => true, "saveas" => ["pdf", "pptx", "odp"] ], - "ots" => [ "mime" => "application/vnd.oasis.opendocument.spreadsheet-template", "type" => "cell", "conv" => true, "saveas" => ["csv", "ods", "pdf", "xlsx"] ], - "ott" => [ "mime" => "application/vnd.oasis.opendocument.text-template", "type" => "word", "conv" => true, "saveas" => ["docx", "odt", "pdf", "rtf", "txt"] ], - "pdf" => [ "mime" => "application/pdf", "type" => "word" ], - "pot" => [ "type" => "slide", "conv" => true, "saveas" => ["pdf", "pptx", "odp"] ], - "potm" => [ "mime" => "application/vnd.ms-powerpoint.template.macroEnabled.12", "type" => "slide", "conv" => true, "saveas" => ["pdf", "pptx", "odp"] ], - "potx" => [ "mime" => "application/vnd.openxmlformats-officedocument.presentationml.template", "type" => "slide", "conv" => true, "saveas" => ["pdf", "pptx", "odp"] ], - "pps" => [ "type" => "slide", "conv" => true, "saveas" => ["pdf", "pptx", "odp"] ], - "ppsm" => [ "mime" => "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", "type" => "slide", "conv" => true, "saveas" => ["pdf", "pptx", "odp"] ], - "ppsx" => [ "mime" => "application/vnd.openxmlformats-officedocument.presentationml.slideshow", "type" => "slide", "conv" => true, "saveas" => ["pdf", "pptx", "odp"] ], - "ppt" => [ "mime" => "application/vnd.ms-powerpoint", "type" => "slide", "conv" => true, "saveas" => ["pdf", "pptx", "odp"] ], - "pptm" => [ "mime" => "application/vnd.ms-powerpoint.presentation.macroEnabled.12", "type" => "slide", "conv" => true, "saveas" => ["pdf", "pptx", "odp"] ], - "pptx" => [ "mime" => "application/vnd.openxmlformats-officedocument.presentationml.presentation", "type" => "slide", "edit" => true, "def" => true, "comment" => true, "saveas" => ["pdf", "odp"] ], - "rtf" => [ "mime" => "text/rtf", "type" => "word", "conv" => true, "editable" => true, "saveas" => ["docx", "odt", "pdf", "txt"] ], - "txt" => [ "mime" => "text/plain", "type" => "word", "edit" => true, "editable" => true, "saveas" => ["docx", "odt", "pdf", "rtf"] ], - "xls" => [ "mime" => "application/vnd.ms-excel", "type" => "cell", "conv" => true, "saveas" => ["csv", "ods", "pdf", "xlsx"] ], - "xlsm" => [ "mime" => "application/vnd.ms-excel.sheet.macroEnabled.12", "type" => "cell", "conv" => true, "saveas" => ["csv", "ods", "pdf", "xlsx"] ], - "xlsx" => [ "mime" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "type" => "cell", "edit" => true, "def" => true, "comment" => true, "modifyFilter" => true, "saveas" => ["csv", "ods", "pdf"] ], - "xlt" => [ "type" => "cell", "conv" => true, "saveas" => ["csv", "ods", "pdf", "xlsx"] ], - "xltm" => [ "mime" => "application/vnd.ms-excel.template.macroEnabled.12", "type" => "cell", "conv" => true, "saveas" => ["csv", "ods", "pdf", "xlsx"] ], - "xltx" => [ "mime" => "application/vnd.openxmlformats-officedocument.spreadsheetml.template", "type" => "cell", "conv" => true, "saveas" => ["csv", "ods", "pdf", "xlsx"] ] - ]; - - /** - * DEMO DATA - */ - private $DEMO_PARAM = [ - "ADDR" => "https://onlinedocs.onlyoffice.com/", - "HEADER" => "AuthorizationJWT", - "SECRET" => "sn2puSUF7muF5Jas", - "TRIAL" => 30 - ]; - - private $linkToDocs = "https://www.onlyoffice.com/docs-registration.aspx?referer=owncloud"; - - /** - * Get link to Docs Cloud - * - * @return string - */ - public function GetLinkToDocs() { - return $this->linkToDocs; - } + /** + * Application name + * + * @var string + */ + private $appName; + + /** + * Config service + * + * @var IConfig + */ + private $config; + + /** + * Logger + * + * @var ILogger + */ + private $logger; + + /** + * The config key for the demo server + * + * @var string + */ + private $_demo = "demo"; + + /** + * The config key for the document server address + * + * @var string + */ + private $_documentserver = "DocumentServerUrl"; + + /** + * The config key for the document server address available from ownCloud + * + * @var string + */ + private $_documentserverInternal = "DocumentServerInternalUrl"; + + /** + * The config key for the ownCloud address available from document server + * + * @var string + */ + private $_storageUrl = "StorageUrl"; + + /** + * The config key for the secret key + * + * @var string + */ + private $_cryptSecret = "secret"; + + /** + * The config key for the default formats + * + * @var string + */ + private $_defFormats = "defFormats"; + + /** + * The config key for the editable formats + * + * @var string + */ + private $_editFormats = "editFormats"; + + /** + * The config key for the setting same tab + * + * @var string + */ + private $_sameTab = "sameTab"; + + /** + * The config key for the generate preview + * + * @var string + */ + private $_preview = "preview"; + + /** + * The config key for the cronChecker + * + * @var string + */ + private $_cronChecker = "cronChecker"; + + /** + * The config key for the keep versions history + * + * @var string + */ + private $_versionHistory = "versionHistory"; + + /** + * The config key for the protection + * + * @var string + */ + private $_protection = "protection"; + + /** + * The config key for the chat display setting + * + * @var string + */ + private $_customizationChat = "customizationChat"; + + /** + * The config key for display the header more compact setting + * + * @var string + */ + private $_customizationCompactHeader = "customizationCompactHeader"; + + /** + * The config key for the feedback display setting + * + * @var string + */ + private $_customizationFeedback = "customizationFeedback"; + + /** + * The config key for the forcesave setting + * + * @var string + */ + private $_customizationForcesave = "customizationForcesave"; + + /** + * The config key for the help display setting + * + * @var string + */ + private $_customizationHelp = "customizationHelp"; + + /** + * The config key for the no tabs setting + * + * @var string + */ + private $_customizationToolbarNoTabs = "customizationToolbarNoTabs"; + + /** + * The config key for the review mode setting + * + * @var string + */ + private $_customizationReviewDisplay = "customizationReviewDisplay"; + + /** + * The config key for the theme setting + * + * @var string + */ + private $_customizationTheme = "customizationTheme"; + + /** + * The config key for the setting limit groups + * + * @var string + */ + private $_groups = "groups"; + + /** + * The config key for the verification + * + * @var string + */ + private $_verification = "verify_peer_off"; + + /** + * The config key for the secret key in jwt + * + * @var string + */ + private $_jwtSecret = "jwt_secret"; + + /** + * The config key for the jwt header + * + * @var string + */ + private $_jwtHeader = "jwt_header"; + + /** + * The config key for the allowable leeway in Jwt checks + * + * @var string + */ + private $_jwtLeeway = "jwt_leeway"; + + /** + * The config key for the settings error + * + * @var string + */ + private $_settingsError = "settings_error"; + + /** + * The config key for limit thumbnail size + * + * @var string + */ + public $limitThumbSize = "limit_thumb_size"; + + /** + * The config key for the customer + * + * @var string + */ + public $customization_customer = "customization_customer"; + + /** + * The config key for the loaderLogo + * + * @var string + */ + public $customization_loaderLogo = "customization_loaderLogo"; + + /** + * The config key for the loaderName + * + * @var string + */ + public $customization_loaderName = "customization_loaderName"; + + /** + * The config key for the logo + * + * @var string + */ + public $customization_logo = "customization_logo"; + + /** + * The config key for the zoom + * + * @var string + */ + public $customization_zoom = "customization_zoom"; + + /** + * The config key for the autosave + * + * @var string + */ + public $customization_autosave = "customization_autosave"; + + /** + * The config key for the goback + * + * @var string + */ + public $customization_goback = "customization_goback"; + + /** + * The config key for the macros + * + * @var string + */ + public $customization_macros = "customization_macros"; + + /** + * The config key for the plugins + * + * @var string + */ + public $customizationPlugins = "customization_plugins"; + + /** + * The config key for the interval of editors availability check by cron + * + * @var string + */ + private $_editors_check_interval = "editors_check_interval"; + + /** + * The config key for store cache + * + * @var ICache + */ + private $cache; + + /** + * @param string $AppName - application name + */ + public function __construct($AppName) { + $this->appName = $AppName; + + $this->config = \OC::$server->getConfig(); + $this->logger = \OC::$server->getLogger(); + $cacheFactory = \OC::$server->getMemCacheFactory(); + $this->cache = $cacheFactory->createLocal($this->appName); + } + + /** + * Get value from the system configuration + * + * @param string $key - key configuration + * @param bool $system - get from root or from app section + * + * @return string + */ + public function getSystemValue($key, $system = false) { + if ($system) { + return $this->config->getSystemValue($key); + } + if (!empty($this->config->getSystemValue($this->appName)) + && \array_key_exists($key, $this->config->getSystemValue($this->appName)) + ) { + return $this->config->getSystemValue($this->appName)[$key]; + } + return null; + } + + /** + * Switch on demo server + * + * @param bool $value - select demo + * + * @return bool + */ + public function selectDemo($value) { + $this->logger->info("Select demo: " . json_encode($value), ["app" => $this->appName]); + + $data = $this->getDemoData(); + + if ($value === true && !$data["available"]) { + $this->logger->info("Trial demo is overdue: " . json_encode($data), ["app" => $this->appName]); + return false; + } + + $data["enabled"] = $value === true; + if (!isset($data["start"])) { + $data["start"] = new DateTime(); + } + + $this->config->setAppValue($this->appName, $this->_demo, json_encode($data)); + return true; + } + + /** + * Get demo data + * + * @return array + */ + public function getDemoData() { + $data = $this->config->getAppValue($this->appName, $this->_demo, ""); + + if (empty($data)) { + return [ + "available" => true, + "enabled" => false + ]; + } + $data = json_decode($data, true); + + $overdue = new DateTime(isset($data["start"]) ? $data["start"]["date"] : null); + $overdue->add(new DateInterval("P" . $this->DEMO_PARAM["TRIAL"] . "D")); + if ($overdue > new DateTime()) { + $data["available"] = true; + $data["enabled"] = $data["enabled"] === true; + } else { + $data["available"] = false; + $data["enabled"] = false; + } + + return $data; + } + + /** + * Get status of demo server + * + * @return bool + */ + public function useDemo() { + return $this->getDemoData()["enabled"] === true; + } + + /** + * Save the document service address to the application configuration + * + * @param string $documentServer - document service address + * + * @return void + */ + public function setDocumentServerUrl($documentServer) { + $documentServer = trim($documentServer); + if (\strlen($documentServer) > 0) { + $documentServer = rtrim($documentServer, "/") . "/"; + if (!preg_match("/(^https?:\/\/)|^\//i", $documentServer)) { + $documentServer = "http://" . $documentServer; + } + } + + $this->logger->info("setDocumentServerUrl: $documentServer", ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_documentserver, $documentServer); + } + + /** + * Get the document service address from the application configuration + * + * @param bool $origin - take origin + * + * @return string + */ + public function getDocumentServerUrl($origin = false) { + if (!$origin && $this->useDemo()) { + return $this->DEMO_PARAM["ADDR"]; + } + + $url = $this->config->getAppValue($this->appName, $this->_documentserver, ""); + if (empty($url)) { + $url = $this->getSystemValue($this->_documentserver); + } + if ($url !== "/") { + $url = rtrim($url, "/"); + if (\strlen($url) > 0) { + $url = $url . "/"; + } + } + return $url; + } + + /** + * Save the document service address available from ownCloud to the application configuration + * + * @param string $documentServerInternal - document service address + * + * @return void + */ + public function setDocumentServerInternalUrl($documentServerInternal) { + $documentServerInternal = rtrim(trim($documentServerInternal), "/"); + if (\strlen($documentServerInternal) > 0) { + $documentServerInternal = $documentServerInternal . "/"; + if (!preg_match("/^https?:\/\//i", $documentServerInternal)) { + $documentServerInternal = "http://" . $documentServerInternal; + } + } + + $this->logger->info("setDocumentServerInternalUrl: $documentServerInternal", ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_documentserverInternal, $documentServerInternal); + } + + /** + * Get the document service address available from ownCloud from the application configuration + * + * @param bool $origin - take origin + * + * @return string + */ + public function getDocumentServerInternalUrl($origin = false) { + if (!$origin && $this->useDemo()) { + return $this->getDocumentServerUrl(); + } + + $url = $this->config->getAppValue($this->appName, $this->_documentserverInternal, ""); + if (empty($url)) { + $url = $this->getSystemValue($this->_documentserverInternal); + } + if (!$origin && empty($url)) { + $url = $this->getDocumentServerUrl(); + } + return $url; + } + + /** + * Replace domain in document server url with internal address from configuration + * + * @param string $url - document server url + * + * @return string + */ + public function replaceDocumentServerUrlToInternal($url) { + $documentServerUrl = $this->getDocumentServerInternalUrl(); + if (!empty($documentServerUrl)) { + $from = $this->getDocumentServerUrl(); + + if (!preg_match("/^https?:\/\//i", $from)) { + $parsedUrl = parse_url($url); + $from = $parsedUrl["scheme"] . "://" . $parsedUrl["host"] . (\array_key_exists("port", $parsedUrl) ? (":" . $parsedUrl["port"]) : "") . $from; + } + + if ($from !== $documentServerUrl) { + $this->logger->debug("Replace url from $from to $documentServerUrl", ["app" => $this->appName]); + $url = str_replace($from, $documentServerUrl, $url); + } + } + + return $url; + } + + /** + * Save the ownCloud address available from document server to the application configuration + * + * @param string $storageUrl - storage url + * + * @return void + */ + public function setStorageUrl($storageUrl) { + $storageUrl = rtrim(trim($storageUrl), "/"); + if (\strlen($storageUrl) > 0) { + $storageUrl = $storageUrl . "/"; + if (!preg_match("/^https?:\/\//i", $storageUrl)) { + $storageUrl = "http://" . $storageUrl; + } + } + + $this->logger->info("setStorageUrl: $storageUrl", ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_storageUrl, $storageUrl); + } + + /** + * Get the ownCloud address available from document server from the application configuration + * + * @return string + */ + public function getStorageUrl() { + $url = $this->config->getAppValue($this->appName, $this->_storageUrl, ""); + if (empty($url)) { + $url = $this->getSystemValue($this->_storageUrl); + } + return $url; + } + + /** + * Save the document service secret key to the application configuration + * + * @param string $secret - secret key + * + * @return void + */ + public function setDocumentServerSecret($secret) { + $secret = trim($secret); + if (empty($secret)) { + $this->logger->info("Clear secret key", ["app" => $this->appName]); + } else { + $this->logger->info("Set secret key", ["app" => $this->appName]); + } + + $this->config->setAppValue($this->appName, $this->_jwtSecret, $secret); + } + + /** + * Get the document service secret key from the application configuration + * + * @param bool $origin - take origin + * + * @return string + */ + public function getDocumentServerSecret($origin = false) { + if (!$origin && $this->useDemo()) { + return $this->DEMO_PARAM["SECRET"]; + } + + $secret = $this->config->getAppValue($this->appName, $this->_jwtSecret, ""); + if (empty($secret)) { + $secret = $this->getSystemValue($this->_jwtSecret); + } + return $secret; + } + + /** + * Get the secret key from the application configuration + * + * @return string + */ + public function getSKey() { + $secret = $this->getDocumentServerSecret(); + if (empty($secret)) { + $secret = $this->getSystemValue($this->_cryptSecret, true); + } + return $secret; + } + + /** + * Save an array of formats with default action + * + * @param array $formats - formats with status + * + * @return void + */ + public function setDefaultFormats($formats) { + $value = json_encode($formats); + $this->logger->info("Set default formats: $value", ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_defFormats, $value); + } + + /** + * Get an array of formats with default action + * + * @return array + */ + private function getDefaultFormats() { + $value = $this->config->getAppValue($this->appName, $this->_defFormats, ""); + if (empty($value)) { + return []; + } + return json_decode($value, true); + } + + /** + * Save an array of formats that is opened for editing + * + * @param array $formats - formats with status + * + * @return void + */ + public function setEditableFormats($formats) { + $value = json_encode($formats); + $this->logger->info("Set editing formats: $value", ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_editFormats, $value); + } + + /** + * Get an array of formats opening for editing + * + * @return array + */ + private function getEditableFormats() { + $value = $this->config->getAppValue($this->appName, $this->_editFormats, ""); + if (empty($value)) { + return []; + } + return json_decode($value, true); + } + + /** + * Save the opening setting in a same tab + * + * @param bool $value - same tab + * + * @return void + */ + public function setSameTab($value) { + $this->logger->info("Set opening in a same tab: " . json_encode($value), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_sameTab, json_encode($value)); + } + + /** + * Get the opening setting in a same tab + * + * @return bool + */ + public function getSameTab() { + return $this->config->getAppValue($this->appName, $this->_sameTab, "false") === "true"; + } + + /** + * Save generate preview setting + * + * @param bool $value - preview + * + * @return bool + */ + public function setPreview($value) { + $this->logger->info("Set generate preview: " . json_encode($value), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_preview, json_encode($value)); + } + + /** + * Get generate preview setting + * + * @return bool + */ + public function getPreview() { + return $this->config->getAppValue($this->appName, $this->_preview, "true") === "true"; + } + + /** + * Get cron checker setting + * + * @return bool + */ + public function getCronChecker() { + return $this->config->getAppValue($this->appName, $this->_cronChecker, "true") !== "false"; + } + + /** + * Save cron checker setting + * + * @param bool $value - cronChecker + * + * @return void + */ + public function setCronChecker($value) { + $this->logger->info("Set cron checker: " . json_encode($value), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_cronChecker, json_encode($value)); + } + + /** + * Save keep versions history + * + * @param bool $value - version history + * + * @return void + */ + public function setVersionHistory($value) { + $this->logger->info("Set keep versions history: " . json_encode($value), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_versionHistory, json_encode($value)); + } + + /** + * Get keep versions history + * + * @return bool + */ + public function getVersionHistory() { + return $this->config->getAppValue($this->appName, $this->_versionHistory, "true") === "true"; + } + + /** + * Save protection + * + * @param bool $value - version history + * + * @return void + */ + public function setProtection($value) { + $this->logger->info("Set protection: " . $value, ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_protection, $value); + } + + /** + * Get protection + * + * @return string + */ + public function getProtection() { + $value = $this->config->getAppValue($this->appName, $this->_protection, "owner"); + if ($value === "all") { + return "all"; + } + return "owner"; + } + + /** + * Save chat display setting + * + * @param bool $value - display chat + * + * @return void + */ + public function setCustomizationChat($value) { + $this->logger->info("Set chat display: " . json_encode($value), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_customizationChat, json_encode($value)); + } + + /** + * Get chat display setting + * + * @return bool + */ + public function getCustomizationChat() { + return $this->config->getAppValue($this->appName, $this->_customizationChat, "true") === "true"; + } + + /** + * Save compact header setting + * + * @param bool $value - display compact header + * + * @return void + */ + public function setCustomizationCompactHeader($value) { + $this->logger->info("Set compact header display: " . json_encode($value), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_customizationCompactHeader, json_encode($value)); + } + + /** + * Get compact header setting + * + * @return bool + */ + public function getCustomizationCompactHeader() { + return $this->config->getAppValue($this->appName, $this->_customizationCompactHeader, "true") === "true"; + } + + /** + * Save feedback display setting + * + * @param bool $value - display feedback + * + * @return void + */ + public function setCustomizationFeedback($value) { + $this->logger->info("Set feedback display: " . json_encode($value), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_customizationFeedback, json_encode($value)); + } + + /** + * Get feedback display setting + * + * @return bool + */ + public function getCustomizationFeedback() { + return $this->config->getAppValue($this->appName, $this->_customizationFeedback, "true") === "true"; + } + + /** + * Save forcesave setting + * + * @param bool $value - forcesave + * + * @return void + */ + public function setCustomizationForcesave($value) { + $this->logger->info("Set forcesave: " . json_encode($value), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_customizationForcesave, json_encode($value)); + } + + /** + * Get forcesave setting + * + * @return bool + */ + public function getCustomizationForcesave() { + $value = $this->config->getAppValue($this->appName, $this->_customizationForcesave, "false") === "true"; + + return $value && ($this->checkEncryptionModule() === false); + } + + /** + * Save help display setting + * + * @param bool $value - display help + * + * @return void + */ + public function setCustomizationHelp($value) { + $this->logger->info("Set help display: " . json_encode($value), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_customizationHelp, json_encode($value)); + } + + /** + * Get help display setting + * + * @return bool + */ + public function getCustomizationHelp() { + return $this->config->getAppValue($this->appName, $this->_customizationHelp, "true") === "true"; + } + + /** + * Save without tabs setting + * + * @param bool $value - without tabs + * + * @return void + */ + public function setCustomizationToolbarNoTabs($value) { + $this->logger->info("Set without tabs: " . json_encode($value), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_customizationToolbarNoTabs, json_encode($value)); + } + + /** + * Get without tabs setting + * + * @return bool + */ + public function getCustomizationToolbarNoTabs() { + return $this->config->getAppValue($this->appName, $this->_customizationToolbarNoTabs, "true") === "true"; + } + + /** + * Save review viewing mode setting + * + * @param string $value - review mode + * + * @return void + */ + public function setCustomizationReviewDisplay($value) { + $this->logger->info("Set review mode: " . $value, ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_customizationReviewDisplay, $value); + } + + /** + * Get review viewing mode setting + * + * @return string + */ + public function getCustomizationReviewDisplay() { + $value = $this->config->getAppValue($this->appName, $this->_customizationReviewDisplay, "original"); + if ($value === "markup") { + return "markup"; + } + if ($value === "final") { + return "final"; + } + return "original"; + } + + /** + * Save theme setting + * + * @param string $value - theme + * + * @return void + */ + public function setCustomizationTheme($value) { + $this->logger->info("Set theme: " . $value, ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_customizationTheme, $value); + } + + /** + * Get theme setting + * + * @return string + */ + public function getCustomizationTheme() { + $value = $this->config->getAppValue($this->appName, $this->_customizationTheme, "theme-classic-light"); + if ($value === "theme-light") { + return "theme-light"; + } + if ($value === "theme-dark") { + return "theme-dark"; + } + return "theme-classic-light"; + } + + /** + * Save macros setting + * + * @param bool $value - enable macros + * + * @return void + */ + public function setCustomizationMacros($value) { + $this->logger->info("Set macros enabled: " . json_encode($value), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->customization_macros, json_encode($value)); + } + + /** + * Get macros setting + * + * @return bool + */ + public function getCustomizationMacros() { + return $this->config->getAppValue($this->appName, $this->customization_macros, "true") === "true"; + } + + /** + * Save plugins setting + * + * @param bool $value - enable macros + * + * @return void + */ + public function setCustomizationPlugins($value) { + $this->logger->info("Set plugins enabled: " . json_encode($value), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->customizationPlugins, json_encode($value)); + } + + /** + * Get plugins setting + * + * @return bool + */ + public function getCustomizationPlugins() { + return $this->config->getAppValue($this->appName, $this->customizationPlugins, "true") === "true"; + } + + /** + * Save the list of groups + * + * @param array $groups - the list of groups + * + * @return void + */ + public function setLimitGroups($groups) { + if (!\is_array($groups)) { + $groups = []; + } + $value = json_encode($groups); + $this->logger->info("Set groups: $value", ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_groups, $value); + } + + /** + * Get the list of groups + * + * @return array + */ + public function getLimitGroups() { + $value = $this->config->getAppValue($this->appName, $this->_groups, ""); + if (empty($value)) { + return []; + } + $groups = json_decode($value, true); + if (!\is_array($groups)) { + $groups = []; + } + return $groups; + } + + /** + * Check access for group + * + * @param string $userId - user identifier + * + * @return bool + */ + public function isUserAllowedToUse($userId = null) { + // no user -> no + $userSession = \OC::$server->getUserSession(); + if ($userId === null && ($userSession === null || !$userSession->isLoggedIn())) { + return false; + } + + $groups = $this->getLimitGroups(); + // no group set -> all users are allowed + if (empty($groups)) { + return true; + } + + if ($userId === null) { + $user = $userSession->getUser(); + } else { + $user = \OC::$server->getUserManager()->get($userId); + if (empty($user)) { + return false; + } + } + + foreach ($groups as $groupName) { + // group unknown -> error and allow nobody + $group = \OC::$server->getGroupManager()->get($groupName); + if ($group === null) { + \OC::$server->getLogger()->error("Group is unknown $groupName", ["app" => $this->appName]); + $this->setLimitGroups(array_diff($groups, [$groupName])); + } else { + if ($group->inGroup($user)) { + return true; + } + } + } + + return false; + } + + /** + * Save the document service verification setting to the application configuration + * + * @param bool $verifyPeerOff - parameter verification setting + * + * @return void + */ + public function setVerifyPeerOff($verifyPeerOff) { + $this->logger->info("setVerifyPeerOff " . json_encode($verifyPeerOff), ["app" => $this->appName]); + + $this->config->setAppValue($this->appName, $this->_verification, json_encode($verifyPeerOff)); + } + + /** + * Get the document service verification setting to the application configuration + * + * @return bool + */ + public function getVerifyPeerOff() { + $turnOff = $this->config->getAppValue($this->appName, $this->_verification, ""); + + if (!empty($turnOff)) { + return $turnOff === "true"; + } + + return $this->getSystemValue($this->_verification); + } + + /** + * Get the limit on size document when generating thumbnails + * + * @return int + */ + public function getLimitThumbSize() { + $limitSize = (integer)$this->getSystemValue($this->limitThumbSize); + + if (!empty($limitSize)) { + return $limitSize; + } + + return 100 * 1024 * 1024; + } + + /** + * Get the jwt header setting + * + * @param bool $origin - take origin + * + * @return string + */ + public function jwtHeader($origin = false) { + if (!$origin && $this->useDemo()) { + return $this->DEMO_PARAM["HEADER"]; + } + + $header = $this->config->getAppValue($this->appName, $this->_jwtHeader, ""); + if (empty($header)) { + $header = $this->getSystemValue($this->_jwtHeader); + } + if (!$origin && empty($header)) { + $header = "Authorization"; + } + return $header; + } + + /** + * Save the jwtHeader setting + * + * @param string $value - jwtHeader + * + * @return void + */ + public function setJwtHeader($value) { + $value = trim($value); + if (empty($value)) { + $this->logger->info("Clear header key", ["app" => $this->appName]); + } else { + $this->logger->info("Set header key " . $value, ["app" => $this->appName]); + } + + $this->config->setAppValue($this->appName, $this->_jwtHeader, $value); + } + + /** + * Get the Jwt Leeway + * + * @return int + */ + public function getJwtLeeway() { + $jwtLeeway = (integer)$this->getSystemValue($this->_jwtLeeway); + + return $jwtLeeway; + } + + /** + * Save the status settings + * + * @param string $value - error + * + * @return void + */ + public function setSettingsError($value) { + $this->config->setAppValue($this->appName, $this->_settingsError, $value); + } + + /** + * Get the status settings + * + * @return bool + */ + public function settingsAreSuccessful() { + return empty($this->config->getAppValue($this->appName, $this->_settingsError, "")); + } + + /** + * Checking encryption enabled + * + * @return string|bool + */ + public function checkEncryptionModule() { + if (!\OC::$server->getAppManager()->isInstalled("encryption")) { + return false; + } + if (!\OC::$server->getEncryptionManager()->isEnabled()) { + return false; + } + + $crypt = new \OCA\Encryption\Crypto\Crypt(\OC::$server->getLogger(), \OC::$server->getUserSession(), \OC::$server->getConfig(), \OC::$server->getL10N("encryption")); + $util = new \OCA\Encryption\Util(new \OC\Files\View(), $crypt, \OC::$server->getLogger(), \OC::$server->getUserSession(), \OC::$server->getConfig(), \OC::$server->getUserManager()); + if ($util->isMasterKeyEnabled()) { + return "master"; + } + + return true; + } + + /** + * Get supported formats + * + * @return array + * + * @NoAdminRequired + */ + public function formatsSetting() { + $result = $this->buildOnlyofficeFormats(); + + $defFormats = $this->getDefaultFormats(); + foreach ($defFormats as $format => $setting) { + if (\array_key_exists($format, $result)) { + $result[$format]["def"] = ($setting === true || $setting === "true"); + } + } + + $editFormats = $this->getEditableFormats(); + foreach ($editFormats as $format => $setting) { + if (\array_key_exists($format, $result)) { + $result[$format]["edit"] = ($setting === true || $setting === "true"); + } + } + + return $result; + } + + /** + * Get version of share attributes + * + * @return string + */ + public function shareAttributesVersion() { + if (\version_compare(\implode(".", \OCP\Util::getVersion()), "10.3.0", ">=")) { + return "v2"; + } elseif (\version_compare(\implode(".", \OCP\Util::getVersion()), "10.2.0", ">=")) { + return "v1"; + } + return ""; + } + + /** + * Get the editors check interval + * + * @return int + */ + public function getEditorsCheckInterval() { + $interval = $this->getSystemValue($this->_editors_check_interval); + if ($interval !== null && !is_int($interval)) { + if (is_string($interval) && !ctype_digit($interval)) { + $interval = null; + } else { + $interval = (integer)$interval; + } + } + + if (empty($interval) && $interval !== 0) { + $interval = 60 * 60 * 24; + } + return (integer)$interval; + } + + /** + * Get ONLYOFFICE formats list + * + * @return array + */ + private function buildOnlyofficeFormats() { + try { + $onlyofficeFormats = $this->getFormats(); + $result = []; + $additionalFormats = $this->getAdditionalFormatAttributes(); + + if ($onlyofficeFormats !== false) { + foreach ($onlyofficeFormats as $onlyOfficeFormat) { + if ($onlyOfficeFormat["name"] + && $onlyOfficeFormat["mime"] + && $onlyOfficeFormat["type"] + && $onlyOfficeFormat["actions"] + && $onlyOfficeFormat["convert"] + ) { + $result[$onlyOfficeFormat["name"]] = [ + "mime" => $onlyOfficeFormat["mime"], + "type" => $onlyOfficeFormat["type"], + "edit" => in_array("edit", $onlyOfficeFormat["actions"]), + "editable" => in_array("lossy-edit", $onlyOfficeFormat["actions"]), + "conv" => in_array("auto-convert", $onlyOfficeFormat["actions"]), + "fillForms" => in_array("fill", $onlyOfficeFormat["actions"]), + "saveas" => $onlyOfficeFormat["convert"], + ]; + if (isset($additionalFormats[$onlyOfficeFormat["name"]])) { + $result[$onlyOfficeFormat["name"]] = array_merge($result[$onlyOfficeFormat["name"]], $additionalFormats[$onlyOfficeFormat["name"]]); + } + } + } + } + return $result; + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "Format matrix error", "app" => $this->appName]); + return []; + } + } + + /** + * Get the additional format attributes + * + * @return array + */ + private function getAdditionalFormatAttributes() { + $additionalFormatAttributes = [ + "docx" => [ + "def" => true, + "review" => true, + "comment" => true, + ], + "docxf" => [ + "def" => true, + "review" => true, + "comment" => true, + "createForm" => true, + ], + "oform" => [ + "def" => true, + ], + "pdf" => [ + "def" => true, + ], + "pptx" => [ + "def" => true, + "comment" => true, + ], + "xlsx" => [ + "def" => true, + "comment" => true, + "modifyFilter" => true, + ], + "txt" => [ + "edit" => true, + ], + "csv" => [ + "edit" => true, + ], + ]; + return $additionalFormatAttributes; + } + + /** + * Get the formats list from cache or file + * + * @return array + */ + public function getFormats() { + $cachedFormats = $this->cache->get("document_formats"); + if ($cachedFormats !== null) { + return json_decode($cachedFormats, true); + } + + $formats = file_get_contents(dirname(__DIR__) . DIRECTORY_SEPARATOR . "assets" . DIRECTORY_SEPARATOR . "document-formats" . DIRECTORY_SEPARATOR . "onlyoffice-docs-formats.json"); + $this->cache->set("document_formats", $formats, 6 * 3600); + $this->logger->debug("Getting formats from file", ["app" => $this->appName]); + return json_decode($formats, true); + } + + /** + * Get the mime type by format name + * + * @param string $ext - format name + * + * @return string + */ + public function getMimeType($ext) { + $onlyofficeFormats = $this->getFormats(); + $result = "text/plain"; + + foreach ($onlyofficeFormats as $onlyOfficeFormat) { + if ($onlyOfficeFormat["name"] === $ext && !empty($onlyOfficeFormat["mime"])) { + $result = $onlyOfficeFormat["mime"][0]; + break; + } + } + + return $result; + } + + /** + * DEMO DATA + */ + private $DEMO_PARAM = [ + "ADDR" => "https://onlinedocs.onlyoffice.com/", + "HEADER" => "AuthorizationJWT", + "SECRET" => "sn2puSUF7muF5Jas", + "TRIAL" => 30 + ]; + + private $linkToDocs = "https://www.onlyoffice.com/docs-registration.aspx?referer=owncloud"; + + /** + * Get link to Docs Cloud + * + * @return string + */ + public function getLinkToDocs() { + return $this->linkToDocs; + } } diff --git a/lib/command/documentserver.php b/lib/command/documentserver.php index 4e750ed4..f757beda 100644 --- a/lib/command/documentserver.php +++ b/lib/command/documentserver.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,99 +32,109 @@ use OCA\Onlyoffice\DocumentService; use OCA\Onlyoffice\Crypt; +/** + * Class Document Server + * + * @package OCA\Onlyoffice\Command + */ class DocumentServer extends Command { - - /** - * Application configuration - * - * @var AppConfig - */ - private $config; - - /** - * l10n service - * - * @var IL10N - */ - private $trans; - - /** - * Url generator service - * - * @var IURLGenerator - */ - private $urlGenerator; - - /** - * Hash generator - * - * @var Crypt - */ - private $crypt; - - /** - * @param AppConfig $config - application configuration - * @param IL10N $trans - l10n service - * @param IURLGenerator $urlGenerator - url generator service - * @param Crypt $crypt - hash generator - */ - public function __construct(AppConfig $config, - IL10N $trans, - IURLGenerator $urlGenerator, - Crypt $crypt) { - parent::__construct(); - $this->config = $config; - $this->trans = $trans; - $this->urlGenerator = $urlGenerator; - $this->crypt = $crypt; - } - - /** - * Configures the current command. - */ - protected function configure() { - $this - ->setName("onlyoffice:documentserver") - ->setDescription("Manage document server") - ->addOption("check", - null, - InputOption::VALUE_NONE, - "Check connection document server"); - } - - /** - * Executes the current command. - * - * @param InputInterface $input - input data - * @param OutputInterface $output - output data - * - * @return int 0 if everything went fine, or an exit code - */ - protected function execute(InputInterface $input, OutputInterface $output) { - $check = $input->getOption("check"); - - $documentserver = $this->config->GetDocumentServerUrl(true); - if(empty($documentserver)) { - $output->writeln("Document server is not configured"); - return 1; - } - - if($check) { - $documentService = new DocumentService($this->trans, $this->config); - - list ($error, $version) = $documentService->checkDocServiceUrl($this->urlGenerator, $this->crypt); - $this->config->SetSettingsError($error); - - if(!empty($error)) { - $output->writeln("Error connection: $error"); - return 1; - } else { - $output->writeln("Document server $documentserver version $version is successfully connected"); - return 0; - } - } - - $output->writeln("The current document server: $documentserver"); - return 0; - } -} \ No newline at end of file + /** + * Application configuration + * + * @var AppConfig + */ + private $config; + + /** + * l10n service + * + * @var IL10N + */ + private $trans; + + /** + * Url generator service + * + * @var IURLGenerator + */ + private $urlGenerator; + + /** + * Hash generator + * + * @var Crypt + */ + private $crypt; + + /** + * @param AppConfig $config - application configuration + * @param IL10N $trans - l10n service + * @param IURLGenerator $urlGenerator - url generator service + * @param Crypt $crypt - hash generator + */ + public function __construct( + AppConfig $config, + IL10N $trans, + IURLGenerator $urlGenerator, + Crypt $crypt + ) { + parent::__construct(); + $this->config = $config; + $this->trans = $trans; + $this->urlGenerator = $urlGenerator; + $this->crypt = $crypt; + } + + /** + * Configures the current command. + * + * @return void + */ + protected function configure() { + $this + ->setName("onlyoffice:documentserver") + ->setDescription("Manage document server") + ->addOption( + "check", + null, + InputOption::VALUE_NONE, + "Check connection document server" + ); + } + + /** + * Executes the current command. + * + * @param InputInterface $input - input data + * @param OutputInterface $output - output data + * + * @return int 0 if everything went fine, or an exit code + */ + protected function execute(InputInterface $input, OutputInterface $output) { + $check = $input->getOption("check"); + + $documentserver = $this->config->getDocumentServerUrl(true); + if (empty($documentserver)) { + $output->writeln("Document server is not configured"); + return 1; + } + + if ($check) { + $documentService = new DocumentService($this->trans, $this->config); + + list($error, $version) = $documentService->checkDocServiceUrl($this->urlGenerator, $this->crypt); + $this->config->setSettingsError($error); + + if (!empty($error)) { + $output->writeln("Error connection: $error"); + return 1; + } else { + $output->writeln("Document server $documentserver version $version is successfully connected"); + return 0; + } + } + + $output->writeln("The current document server: $documentserver"); + return 0; + } +} diff --git a/lib/cron/editorscheck.php b/lib/cron/editorscheck.php index c826bcfd..15df13d4 100644 --- a/lib/cron/editorscheck.php +++ b/lib/cron/editorscheck.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,156 +38,160 @@ * */ class EditorsCheck extends TimedJob { - - /** - * Application name - * - * @var string - */ - private $appName; - - /** - * Url generator service - * - * @var IURLGenerator - */ - private $urlGenerator; - - /** - * Logger - * - * @var OCP\ILogger - */ - private $logger; - - /** - * Application configuration - * - * @var AppConfig - */ - private $config; - - /** - * l10n service - * - * @var IL10N - */ - private $trans; - - /** - * Hash generator - * - * @var Crypt - */ - private $crypt; - - /** - * Group manager - * - * @var IGroupManager - */ - private $groupManager; - - /** - * @param string $AppName - application name - * @param IURLGenerator $urlGenerator - url generator service - * @param ITimeFactory $time - time - * @param AppConfig $config - application configuration - * @param IL10N $trans - l10n service - * @param Crypt $crypt - crypt service - */ - public function __construct(string $AppName, - IURLGenerator $urlGenerator, - ITimeFactory $time, - AppConfig $config, - IL10N $trans, - Crypt $crypt, - IGroupManager $groupManager) { - $this->appName = $AppName; - $this->urlGenerator = $urlGenerator; - - $this->logger = \OC::$server->getLogger(); - $this->config = $config; - $this->trans = $trans; - $this->crypt = $crypt; - $this->groupManager = $groupManager; - $this->setInterval($this->config->GetEditorsCheckInterval()); - } - - /** - * Makes the background check - * - * @param array $argument unused argument - */ - protected function run($argument) { - if (empty($this->config->GetDocumentServerUrl())) { - $this->logger->debug("Settings are empty", ["app" => $this->appName]); - return; - } - if (!$this->config->SettingsAreSuccessful()) { - $this->logger->debug("Settings are not correct", ["app" => $this->appName]); - return; - } - $fileUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".callback.emptyfile"); - if (!$this->config->UseDemo() && !empty($this->config->GetStorageUrl())) { - $fileUrl = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->config->GetStorageUrl(), $fileUrl); - } - $host = parse_url($fileUrl)["host"]; - if ($host === "localhost" || $host === "127.0.0.1") { - $this->logger->debug("Localhost is not alowed for cron editors availability check. Please provide server address for internal requests from ONLYOFFICE Docs", ["app" => $this->appName]); - return; - } - - $this->logger->debug("ONLYOFFICE check started by cron", ["app" => $this->appName]); - - $documentService = new DocumentService($this->trans, $this->config); - list ($error, $version) = $documentService->checkDocServiceUrl($this->urlGenerator, $this->crypt); - if (!empty($error)) { - $this->logger->info("ONLYOFFICE server is not available", ["app" => $this->appName]); - $this->config->SetSettingsError($error); - $this->notifyAdmins(); - } else { - $this->logger->debug("ONLYOFFICE server availability check is finished successfully", ["app" => $this->appName]); - } - } - - /** - * Get the list of users to notify - * - * @return string[] - */ - private function getUsersToNotify() { - $notifyGroups = ["admin"]; - $notifyUsers = []; - - foreach ($notifyGroups as $notifyGroup) { - $group = $this->groupManager->get($notifyGroup); - if ($group === null || !($group instanceof IGroup)) { - continue; - } - $users = $group->getUsers(); - foreach ($users as $user) { - $notifyUsers[] = $user->getUID(); - } - } - return $notifyUsers; - } - - /** - * Send notification to admins - * @return void - */ - private function notifyAdmins() { - $notificationManager = \OC::$server->getNotificationManager(); - $notification = $notificationManager->createNotification(); - $notification->setApp($this->appName) - ->setDateTime(new \DateTime()) - ->setObject("editorsCheck", $this->trans->t("ONLYOFFICE server is not available")) - ->setSubject("editorscheck_info"); - foreach ($this->getUsersToNotify() as $uid) { - $notification->setUser($uid); - $notificationManager->notify($notification); - } - } - + /** + * Application name + * + * @var string + */ + private $appName; + + /** + * Url generator service + * + * @var IURLGenerator + */ + private $urlGenerator; + + /** + * Logger + * + * @var OCP\ILogger + */ + private $logger; + + /** + * Application configuration + * + * @var AppConfig + */ + private $config; + + /** + * l10n service + * + * @var IL10N + */ + private $trans; + + /** + * Hash generator + * + * @var Crypt + */ + private $crypt; + + /** + * Group manager + * + * @var IGroupManager + */ + private $groupManager; + + /** + * @param string $AppName - application name + * @param IURLGenerator $urlGenerator - url generator service + * @param ITimeFactory $time - time + * @param AppConfig $config - application configuration + * @param IL10N $trans - l10n service + * @param Crypt $crypt - crypt service + * @param IGroupManager $groupManager - group manager + */ + public function __construct( + string $AppName, + IURLGenerator $urlGenerator, + ITimeFactory $time, + AppConfig $config, + IL10N $trans, + Crypt $crypt, + IGroupManager $groupManager + ) { + $this->appName = $AppName; + $this->urlGenerator = $urlGenerator; + + $this->logger = \OC::$server->getLogger(); + $this->config = $config; + $this->trans = $trans; + $this->crypt = $crypt; + $this->groupManager = $groupManager; + $this->setInterval($this->config->getEditorsCheckInterval()); + } + + /** + * Makes the background check + * + * @param array $argument unused argument + * + * @return void + */ + protected function run($argument) { + if (empty($this->config->getDocumentServerUrl())) { + $this->logger->debug("Settings are empty", ["app" => $this->appName]); + return; + } + if (!$this->config->settingsAreSuccessful()) { + $this->logger->debug("Settings are not correct", ["app" => $this->appName]); + return; + } + $fileUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".callback.emptyfile"); + if (!$this->config->useDemo() && !empty($this->config->getStorageUrl())) { + $fileUrl = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->config->getStorageUrl(), $fileUrl); + } + $host = parse_url($fileUrl)["host"]; + if ($host === "localhost" || $host === "127.0.0.1") { + $this->logger->debug("Localhost is not alowed for cron editors availability check. Please provide server address for internal requests from ONLYOFFICE Docs", ["app" => $this->appName]); + return; + } + + $this->logger->debug("ONLYOFFICE check started by cron", ["app" => $this->appName]); + + $documentService = new DocumentService($this->trans, $this->config); + list($error, $version) = $documentService->checkDocServiceUrl($this->urlGenerator, $this->crypt); + if (!empty($error)) { + $this->logger->info("ONLYOFFICE server is not available", ["app" => $this->appName]); + $this->config->setSettingsError($error); + $this->notifyAdmins(); + } else { + $this->logger->debug("ONLYOFFICE server availability check is finished successfully", ["app" => $this->appName]); + } + } + + /** + * Get the list of users to notify + * + * @return string[] + */ + private function getUsersToNotify() { + $notifyGroups = ["admin"]; + $notifyUsers = []; + + foreach ($notifyGroups as $notifyGroup) { + $group = $this->groupManager->get($notifyGroup); + if ($group === null || !($group instanceof IGroup)) { + continue; + } + $users = $group->getUsers(); + foreach ($users as $user) { + $notifyUsers[] = $user->getUID(); + } + } + return $notifyUsers; + } + + /** + * Send notification to admins + * + * @return void + */ + private function notifyAdmins() { + $notificationManager = \OC::$server->getNotificationManager(); + $notification = $notificationManager->createNotification(); + $notification->setApp($this->appName) + ->setDateTime(new \DateTime()) + ->setObject("editorsCheck", $this->trans->t("ONLYOFFICE server is not available")) + ->setSubject("editorscheck_info"); + foreach ($this->getUsersToNotify() as $uid) { + $notification->setUser($uid); + $notificationManager->notify($notification); + } + } } diff --git a/lib/crypt.php b/lib/crypt.php index 5b81ab40..02923f60 100644 --- a/lib/crypt.php +++ b/lib/crypt.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,50 +28,49 @@ * @package OCA\Onlyoffice */ class Crypt { + /** + * Application configuration + * + * @var AppConfig + */ + private $config; - /** - * Application configuration - * - * @var AppConfig - */ - private $config; + /** + * @param AppConfig $appConfig - application configutarion + */ + public function __construct(AppConfig $appConfig) { + $this->config = $appConfig; + } - /** - * @param AppConfig $config - application configutarion - */ - public function __construct(AppConfig $appConfig) { - $this->config = $appConfig; - } + /** + * Generate token for the object + * + * @param array $object - object to signature + * + * @return string + */ + public function getHash($object) { + return \Firebase\JWT\JWT::encode($object, $this->config->getSKey(), "HS256"); + } - /** - * Generate token for the object - * - * @param array $object - object to signature - * - * @return string - */ - public function GetHash($object) { - return \Firebase\JWT\JWT::encode($object, $this->config->GetSKey(), "HS256"); - } - - /** - * Create an object from the token - * - * @param string $token - token - * - * @return array - */ - public function ReadHash($token) { - $result = null; - $error = null; - if ($token === null) { - return [$result, "token is empty"]; - } - try { - $result = \Firebase\JWT\JWT::decode($token, new \Firebase\JWT\Key($this->config->GetSKey(), "HS256")); - } catch (\UnexpectedValueException $e) { - $error = $e->getMessage(); - } - return [$result, $error]; - } + /** + * Create an object from the token + * + * @param string $token - token + * + * @return array + */ + public function readHash($token) { + $result = null; + $error = null; + if ($token === null) { + return [$result, "token is empty"]; + } + try { + $result = \Firebase\JWT\JWT::decode($token, new \Firebase\JWT\Key($this->config->getSKey(), "HS256")); + } catch (\UnexpectedValueException $e) { + $error = $e->getMessage(); + } + return [$result, $error]; + } } diff --git a/lib/documentservice.php b/lib/documentservice.php index ead9d9d4..8894bc9b 100644 --- a/lib/documentservice.php +++ b/lib/documentservice.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,425 +30,415 @@ * @package OCA\Onlyoffice */ class DocumentService { - - /** - * Application name - * - * @var string - */ - private static $appName = "onlyoffice"; - - /** - * l10n service - * - * @var IL10N - */ - private $trans; - - /** - * Application configuration - * - * @var AppConfig - */ - private $config; - - /** - * @param IL10N $trans - l10n service - * @param AppConfig $config - application configutarion - */ - public function __construct(IL10N $trans, AppConfig $appConfig) { - $this->trans = $trans; - $this->config = $appConfig; - } - - /** - * Translation key to a supported form. - * - * @param string $expected_key - Expected key - * - * @return string - */ - public static function GenerateRevisionId($expected_key) { - if (strlen($expected_key) > 20) { - $expected_key = crc32( $expected_key); - } - $key = preg_replace("[^0-9-.a-zA-Z_=]", "_", $expected_key); - $key = substr($key, 0, min(array(strlen($key), 20))); - return $key; - } - - /** - * The method is to convert the file to the required format and return the result url - * - * @param string $document_uri - Uri for the document to convert - * @param string $from_extension - Document extension - * @param string $to_extension - Extension to which to convert - * @param string $document_revision_id - Key for caching on service - * - * @return string - */ - function GetConvertedUri($document_uri, $from_extension, $to_extension, $document_revision_id) { - $responceFromConvertService = $this->SendRequestToConvertService($document_uri, $from_extension, $to_extension, $document_revision_id, false); - - $errorElement = $responceFromConvertService->Error; - if ($errorElement->count() > 0) { - $this->ProcessConvServResponceError($errorElement . ""); - } - - $isEndConvert = $responceFromConvertService->EndConvert; - - if ($isEndConvert !== null && strtolower($isEndConvert) === "true") { - return (string)$responceFromConvertService->FileUrl; - } - - return ""; - } - - /** - * Request for conversion to a service - * - * @param string $document_uri - Uri for the document to convert - * @param string $from_extension - Document extension - * @param string $to_extension - Extension to which to convert - * @param string $document_revision_id - Key for caching on service - * @param bool - $is_async - Perform conversions asynchronously - * - * @return array - */ - function SendRequestToConvertService($document_uri, $from_extension, $to_extension, $document_revision_id, $is_async) { - $documentServerUrl = $this->config->GetDocumentServerInternalUrl(); - - if (empty($documentServerUrl)) { - throw new \Exception($this->trans->t("ONLYOFFICE app is not configured. Please contact admin")); - } - - $urlToConverter = $documentServerUrl . "ConvertService.ashx"; - - if (empty($document_revision_id)) { - $document_revision_id = $document_uri; - } - - $document_revision_id = self::GenerateRevisionId($document_revision_id); - - if (empty($from_extension)) { - $from_extension = pathinfo($document_uri)["extension"]; - } else { - $from_extension = trim($from_extension, "."); - } - - $data = [ - "async" => $is_async, - "url" => $document_uri, - "outputtype" => trim($to_extension, "."), - "filetype" => $from_extension, - "title" => $document_revision_id . "." . $from_extension, - "key" => $document_revision_id, - "region" => str_replace("_", "-", \OC::$server->getL10NFactory("")->get("")->getLanguageCode()) - ]; - - if ($this->config->UseDemo()) { - $data["tenant"] = $this->config->GetSystemValue("instanceid", true); - } - - $opts = [ - "timeout" => "120", - "headers" => [ - "Content-type" => "application/json" - ], - "body" => json_encode($data) - ]; - - if (!empty($this->config->GetDocumentServerSecret())) { - $params = [ - "payload" => $data - ]; - $token = \Firebase\JWT\JWT::encode($params, $this->config->GetDocumentServerSecret(), "HS256"); - $opts["headers"][$this->config->JwtHeader()] = "Bearer " . $token; - - $token = \Firebase\JWT\JWT::encode($data, $this->config->GetDocumentServerSecret(), "HS256"); - $data["token"] = $token; - $opts["body"] = json_encode($data); - } - - $response_xml_data = $this->Request($urlToConverter, "post", $opts); - - libxml_use_internal_errors(true); - if (!function_exists("simplexml_load_file")) { - throw new \Exception($this->trans->t("Server can't read xml")); - } - $response_data = simplexml_load_string($response_xml_data); - if (!$response_data) { - $exc = $this->trans->t("Bad Response. Errors: "); - foreach(libxml_get_errors() as $error) { - $exc = $exc . "\t" . $error->message; - } - throw new \Exception ($exc); - } - - return $response_data; - } - - /** - * Generate an error code table of convertion - * - * @param string $errorCode - Error code - * - * @return null - */ - function ProcessConvServResponceError($errorCode) { - $errorMessageTemplate = $this->trans->t("Error occurred in the document service"); - $errorMessage = ""; - - switch ($errorCode) { - case -20: - $errorMessage = $errorMessageTemplate . ": Error encrypt signature"; - break; - case -8: - $errorMessage = $errorMessageTemplate . ": Invalid token"; - break; - case -7: - $errorMessage = $errorMessageTemplate . ": Error document request"; - break; - case -6: - $errorMessage = $errorMessageTemplate . ": Error while accessing the conversion result database"; - break; - case -5: - $errorMessage = $errorMessageTemplate . ": Incorrect password"; - break; - case -4: - $errorMessage = $errorMessageTemplate . ": Error while downloading the document file to be converted."; - break; - case -3: - $errorMessage = $errorMessageTemplate . ": Conversion error"; - break; - case -2: - $errorMessage = $errorMessageTemplate . ": Timeout conversion error"; - break; - case -1: - $errorMessage = $errorMessageTemplate . ": Unknown error"; - break; - case 0: - break; - default: - $errorMessage = $errorMessageTemplate . ": ErrorCode = " . $errorCode; - break; - } - - throw new \Exception($errorMessage); - } - - /** - * Request health status - * - * @return bool - */ - function HealthcheckRequest() { - - $documentServerUrl = $this->config->GetDocumentServerInternalUrl(); - - if (empty($documentServerUrl)) { - throw new \Exception($this->trans->t("ONLYOFFICE app is not configured. Please contact admin")); - } - - $urlHealthcheck = $documentServerUrl . "healthcheck"; - - $response = $this->Request($urlHealthcheck); - - return $response === "true"; - } - - /** - * Send command - * - * @param string $method - type of command - * - * @return array - */ - function CommandRequest($method) { - - $documentServerUrl = $this->config->GetDocumentServerInternalUrl(); - - if (empty($documentServerUrl)) { - throw new \Exception($this->trans->t("ONLYOFFICE app is not configured. Please contact admin")); - } - - $urlCommand = $documentServerUrl . "coauthoring/CommandService.ashx"; - - $data = [ - "c" => $method - ]; - - $opts = [ - "headers" => [ - "Content-type" => "application/json" - ], - "body" => json_encode($data) - ]; - - if (!empty($this->config->GetDocumentServerSecret())) { - $params = [ - "payload" => $data - ]; - $token = \Firebase\JWT\JWT::encode($params, $this->config->GetDocumentServerSecret(), "HS256"); - $opts["headers"][$this->config->JwtHeader()] = "Bearer " . $token; - - $token = \Firebase\JWT\JWT::encode($data, $this->config->GetDocumentServerSecret(), "HS256"); - $data["token"] = $token; - $opts["body"] = json_encode($data); - } - - $response = $this->Request($urlCommand, "post", $opts); - - $data = json_decode($response); - - $this->ProcessCommandServResponceError($data->error); - - return $data; - } - - /** - * Generate an error code table of command - * - * @param string $errorCode - Error code - * - * @return null - */ - function ProcessCommandServResponceError($errorCode) { - $errorMessageTemplate = $this->trans->t("Error occurred in the document service"); - $errorMessage = ""; - - switch ($errorCode) { - case 6: - $errorMessage = $errorMessageTemplate . ": Invalid token"; - break; - case 5: - $errorMessage = $errorMessageTemplate . ": Command not correсt"; - break; - case 3: - $errorMessage = $errorMessageTemplate . ": Internal server error"; - break; - case 0: - return; - default: - $errorMessage = $errorMessageTemplate . ": ErrorCode = " . $errorCode; - break; - } - - throw new \Exception($errorMessage); - } - - /** - * Request to Document Server with turn off verification - * - * @param string $url - request address - * @param array $method - request method - * @param array $opts - request options - * - * @return string - */ - public function Request($url, $method = "get", $opts = null) { - $httpClientService = \OC::$server->getHTTPClientService(); - $client = $httpClientService->newClient(); - - if (null === $opts) { - $opts = array(); - } - if (substr($url, 0, strlen("https")) === "https" && $this->config->GetVerifyPeerOff()) { - $opts["verify"] = false; - } - if (!array_key_exists("timeout", $opts)) { - $opts["timeout"] = 60; - } - - if ($method === "post") { - $response = $client->post($url, $opts); - } else { - $response = $client->get($url, $opts); - } - - return $response->getBody(); - } - - /** - * Checking document service location - * - * @param OCP\IURLGenerator $urlGenerator - url generator - * @param OCA\Onlyoffice\Crypt $crypt -crypt - * - * @return array - */ - public function checkDocServiceUrl($urlGenerator, $crypt) { - $logger = \OC::$server->getLogger(); - $version = null; - - try { - - if (preg_match("/^https:\/\//i", $urlGenerator->getAbsoluteURL("/")) - && preg_match("/^http:\/\//i", $this->config->GetDocumentServerUrl())) { - throw new \Exception($this->trans->t("Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required.")); - } - - } catch (\Exception $e) { - $logger->logException($e, ["message" => "Protocol on check error", "app" => self::$appName]); - return [$e->getMessage(), $version]; - } - - try { - - $healthcheckResponse = $this->HealthcheckRequest(); - if (!$healthcheckResponse) { - throw new \Exception($this->trans->t("Bad healthcheck status")); - } - - } catch (\Exception $e) { - $logger->logException($e, ["message" => "HealthcheckRequest on check error", "app" => self::$appName]); - return [$e->getMessage(), $version]; - } - - try { - - $commandResponse = $this->CommandRequest("version"); - - $logger->debug("CommandRequest on check: " . json_encode($commandResponse), ["app" => self::$appName]); - - if (empty($commandResponse)) { - throw new \Exception($this->trans->t("Error occurred in the document service")); - } - - $version = $commandResponse->version; - $versionF = floatval($version); - if ($versionF > 0.0 && $versionF <= 6.0) { - throw new \Exception($this->trans->t("Not supported version")); - } - - } catch (\Exception $e) { - $logger->logException($e, ["message" => "CommandRequest on check error", "app" => self::$appName]); - return [$e->getMessage(), $version]; - } - - $convertedFileUri = null; - try { - - $hashUrl = $crypt->GetHash(["action" => "empty"]); - $fileUrl = $urlGenerator->linkToRouteAbsolute(self::$appName . ".callback.emptyfile", ["doc" => $hashUrl]); - if (!$this->config->UseDemo() && !empty($this->config->GetStorageUrl())) { - $fileUrl = str_replace($urlGenerator->getAbsoluteURL("/"), $this->config->GetStorageUrl(), $fileUrl); - } - - $convertedFileUri = $this->GetConvertedUri($fileUrl, "docx", "docx", "check_" . rand()); - - } catch (\Exception $e) { - $logger->logException($e, ["message" => "GetConvertedUri on check error", "app" => self::$appName]); - return [$e->getMessage(), $version]; - } - - try { - $this->Request($convertedFileUri); - } catch (\Exception $e) { - $logger->logException($e, ["message" => "Request converted file on check error", "app" => self::$appName]); - return [$e->getMessage(), $version]; - } - - return ["", $version]; - } + /** + * Application name + * + * @var string + */ + private static $appName = "onlyoffice"; + + /** + * l10n service + * + * @var IL10N + */ + private $trans; + + /** + * Application configuration + * + * @var AppConfig + */ + private $config; + + /** + * @param IL10N $trans - l10n service + * @param AppConfig $appConfig - application configutarion + */ + public function __construct(IL10N $trans, AppConfig $appConfig) { + $this->trans = $trans; + $this->config = $appConfig; + } + + /** + * Translation key to a supported form. + * + * @param string $expected_key - Expected key + * + * @return string + */ + public static function generateRevisionId($expected_key) { + if (\strlen($expected_key) > 20) { + $expected_key = crc32($expected_key); + } + $key = preg_replace("[^0-9-.a-zA-Z_=]", "_", $expected_key); + $key = substr($key, 0, min([\strlen($key), 20])); + return $key; + } + + /** + * The method is to convert the file to the required format and return the result url + * + * @param string $document_uri - Uri for the document to convert + * @param string $from_extension - Document extension + * @param string $to_extension - Extension to which to convert + * @param string $document_revision_id - Key for caching on service + * + * @return string + */ + public function getConvertedUri($document_uri, $from_extension, $to_extension, $document_revision_id) { + $responceFromConvertService = $this->sendRequestToConvertService($document_uri, $from_extension, $to_extension, $document_revision_id, false); + + $errorElement = $responceFromConvertService->Error; + if ($errorElement->count() > 0) { + $this->processConvServResponceError($errorElement . ""); + } + + $isEndConvert = $responceFromConvertService->EndConvert; + + if ($isEndConvert !== null && strtolower($isEndConvert) === "true") { + return (string)$responceFromConvertService->FileUrl; + } + + return ""; + } + + /** + * request for conversion to a service + * + * @param string $document_uri - Uri for the document to convert + * @param string $from_extension - Document extension + * @param string $to_extension - Extension to which to convert + * @param string $document_revision_id - Key for caching on service + * @param bool - $is_async - Perform conversions asynchronously + * + * @return array + */ + public function sendRequestToConvertService($document_uri, $from_extension, $to_extension, $document_revision_id, $is_async) { + $documentServerUrl = $this->config->getDocumentServerInternalUrl(); + + if (empty($documentServerUrl)) { + throw new \Exception($this->trans->t("ONLYOFFICE app is not configured. Please contact admin")); + } + + $urlToConverter = $documentServerUrl . "ConvertService.ashx"; + + if (empty($document_revision_id)) { + $document_revision_id = $document_uri; + } + + $document_revision_id = self::generateRevisionId($document_revision_id); + + if (empty($from_extension)) { + $from_extension = pathinfo($document_uri)["extension"]; + } else { + $from_extension = trim($from_extension, "."); + } + + $data = [ + "async" => $is_async, + "url" => $document_uri, + "outputtype" => trim($to_extension, "."), + "filetype" => $from_extension, + "title" => $document_revision_id . "." . $from_extension, + "key" => $document_revision_id, + "region" => str_replace("_", "-", \OC::$server->getL10NFactory("")->get("")->getLanguageCode()) + ]; + + if ($this->config->useDemo()) { + $data["tenant"] = $this->config->getSystemValue("instanceid", true); + } + + $opts = [ + "timeout" => "120", + "headers" => [ + "Content-type" => "application/json" + ], + "body" => json_encode($data) + ]; + + if (!empty($this->config->getDocumentServerSecret())) { + $params = [ + "payload" => $data + ]; + $token = \Firebase\JWT\JWT::encode($params, $this->config->getDocumentServerSecret(), "HS256"); + $opts["headers"][$this->config->jwtHeader()] = "Bearer " . $token; + + $token = \Firebase\JWT\JWT::encode($data, $this->config->getDocumentServerSecret(), "HS256"); + $data["token"] = $token; + $opts["body"] = json_encode($data); + } + + $response_xml_data = $this->request($urlToConverter, "post", $opts); + + libxml_use_internal_errors(true); + if (!\function_exists("simplexml_load_file")) { + throw new \Exception($this->trans->t("Server can't read xml")); + } + $response_data = simplexml_load_string($response_xml_data); + if (!$response_data) { + $exc = $this->trans->t("Bad Response. Errors: "); + foreach (libxml_get_errors() as $error) { + $exc = $exc . "\t" . $error->message; + } + throw new \Exception($exc); + } + + return $response_data; + } + + /** + * Generate an error code table of convertion + * + * @param string $errorCode - Error code + * + * @return null + */ + public function processConvServResponceError($errorCode) { + $errorMessageTemplate = $this->trans->t("Error occurred in the document service"); + $errorMessage = ""; + + switch ($errorCode) { + case -20: + $errorMessage = $errorMessageTemplate . ": Error encrypt signature"; + break; + case -8: + $errorMessage = $errorMessageTemplate . ": Invalid token"; + break; + case -7: + $errorMessage = $errorMessageTemplate . ": Error document request"; + break; + case -6: + $errorMessage = $errorMessageTemplate . ": Error while accessing the conversion result database"; + break; + case -5: + $errorMessage = $errorMessageTemplate . ": Incorrect password"; + break; + case -4: + $errorMessage = $errorMessageTemplate . ": Error while downloading the document file to be converted."; + break; + case -3: + $errorMessage = $errorMessageTemplate . ": Conversion error"; + break; + case -2: + $errorMessage = $errorMessageTemplate . ": Timeout conversion error"; + break; + case -1: + $errorMessage = $errorMessageTemplate . ": Unknown error"; + break; + case 0: + break; + default: + $errorMessage = $errorMessageTemplate . ": ErrorCode = " . $errorCode; + break; + } + + throw new \Exception($errorMessage); + } + + /** + * request health status + * + * @return bool + */ + public function healthcheckRequest() { + $documentServerUrl = $this->config->getDocumentServerInternalUrl(); + + if (empty($documentServerUrl)) { + throw new \Exception($this->trans->t("ONLYOFFICE app is not configured. Please contact admin")); + } + + $urlHealthcheck = $documentServerUrl . "healthcheck"; + + $response = $this->request($urlHealthcheck); + + return $response === "true"; + } + + /** + * Send command + * + * @param string $method - type of command + * + * @return array + */ + public function commandRequest($method) { + $documentServerUrl = $this->config->getDocumentServerInternalUrl(); + + if (empty($documentServerUrl)) { + throw new \Exception($this->trans->t("ONLYOFFICE app is not configured. Please contact admin")); + } + + $urlCommand = $documentServerUrl . "coauthoring/CommandService.ashx"; + + $data = [ + "c" => $method + ]; + + $opts = [ + "headers" => [ + "Content-type" => "application/json" + ], + "body" => json_encode($data) + ]; + + if (!empty($this->config->getDocumentServerSecret())) { + $params = [ + "payload" => $data + ]; + $token = \Firebase\JWT\JWT::encode($params, $this->config->getDocumentServerSecret(), "HS256"); + $opts["headers"][$this->config->jwtHeader()] = "Bearer " . $token; + + $token = \Firebase\JWT\JWT::encode($data, $this->config->getDocumentServerSecret(), "HS256"); + $data["token"] = $token; + $opts["body"] = json_encode($data); + } + + $response = $this->request($urlCommand, "post", $opts); + + $data = json_decode($response); + + $this->processCommandServResponceError($data->error); + + return $data; + } + + /** + * Generate an error code table of command + * + * @param string $errorCode - Error code + * + * @return null + */ + public function processCommandServResponceError($errorCode) { + $errorMessageTemplate = $this->trans->t("Error occurred in the document service"); + $errorMessage = ""; + + switch ($errorCode) { + case 6: + $errorMessage = $errorMessageTemplate . ": Invalid token"; + break; + case 5: + $errorMessage = $errorMessageTemplate . ": Command not correсt"; + break; + case 3: + $errorMessage = $errorMessageTemplate . ": Internal server error"; + break; + case 0: + return; + default: + $errorMessage = $errorMessageTemplate . ": ErrorCode = " . $errorCode; + break; + } + + throw new \Exception($errorMessage); + } + + /** + * request to Document Server with turn off verification + * + * @param string $url - request address + * @param array $method - request method + * @param array $opts - request options + * + * @return string + */ + public function request($url, $method = "get", $opts = null) { + $httpClientService = \OC::$server->getHTTPClientService(); + $client = $httpClientService->newClient(); + + if ($opts === null) { + $opts = []; + } + if (substr($url, 0, \strlen("https")) === "https" && $this->config->getVerifyPeerOff()) { + $opts["verify"] = false; + } + if (!\array_key_exists("timeout", $opts)) { + $opts["timeout"] = 60; + } + + if ($method === "post") { + $response = $client->post($url, $opts); + } else { + $response = $client->get($url, $opts); + } + + return $response->getBody(); + } + + /** + * Checking document service location + * + * @param OCP\IURLGenerator $urlGenerator - url generator + * @param OCA\Onlyoffice\Crypt $crypt -crypt + * + * @return array + */ + public function checkDocServiceUrl($urlGenerator, $crypt) { + $logger = \OC::$server->getLogger(); + $version = null; + + try { + if (preg_match("/^https:\/\//i", $urlGenerator->getAbsoluteURL("/")) + && preg_match("/^http:\/\//i", $this->config->getDocumentServerUrl()) + ) { + throw new \Exception($this->trans->t("Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required.")); + } + } catch (\Exception $e) { + $logger->logException($e, ["message" => "Protocol on check error", "app" => self::$appName]); + return [$e->getMessage(), $version]; + } + + try { + $healthcheckResponse = $this->healthcheckRequest(); + if (!$healthcheckResponse) { + throw new \Exception($this->trans->t("Bad healthcheck status")); + } + } catch (\Exception $e) { + $logger->logException($e, ["message" => "healthcheckRequest on check error", "app" => self::$appName]); + return [$e->getMessage(), $version]; + } + + try { + $commandResponse = $this->commandRequest("version"); + + $logger->debug("commandRequest on check: " . json_encode($commandResponse), ["app" => self::$appName]); + + if (empty($commandResponse)) { + throw new \Exception($this->trans->t("Error occurred in the document service")); + } + + $version = $commandResponse->version; + $versionF = \floatval($version); + if ($versionF > 0.0 && $versionF <= 6.0) { + throw new \Exception($this->trans->t("Not supported version")); + } + } catch (\Exception $e) { + $logger->logException($e, ["message" => "commandRequest on check error", "app" => self::$appName]); + return [$e->getMessage(), $version]; + } + + $convertedFileUri = null; + try { + $hashUrl = $crypt->getHash(["action" => "empty"]); + $fileUrl = $urlGenerator->linkToRouteAbsolute(self::$appName . ".callback.emptyfile", ["doc" => $hashUrl]); + if (!$this->config->useDemo() && !empty($this->config->getStorageUrl())) { + $fileUrl = str_replace($urlGenerator->getAbsoluteURL("/"), $this->config->getStorageUrl(), $fileUrl); + } + + $convertedFileUri = $this->getConvertedUri($fileUrl, "docx", "docx", "check_" . rand()); + } catch (\Exception $e) { + $logger->logException($e, ["message" => "getConvertedUri on check error", "app" => self::$appName]); + return [$e->getMessage(), $version]; + } + + try { + $this->request($convertedFileUri); + } catch (\Exception $e) { + $logger->logException($e, ["message" => "request converted file on check error", "app" => self::$appName]); + return [$e->getMessage(), $version]; + } + + return ["", $version]; + } } diff --git a/lib/fileutility.php b/lib/fileutility.php index 29b61406..2d0782db 100644 --- a/lib/fileutility.php +++ b/lib/fileutility.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +27,7 @@ use OCP\ILogger; use OCP\ISession; use OCP\Share\IManager; +use OCP\Share\IShare; use OCA\Onlyoffice\AppConfig; use OCA\Onlyoffice\Version; @@ -38,262 +40,299 @@ * @package OCA\Onlyoffice */ class FileUtility { - - /** - * Application name - * - * @var string - */ - private $appName; - - /** - * l10n service - * - * @var IL10N - */ - private $trans; - - /** - * Logger - * - * @var ILogger - */ - private $logger; - - /** - * Share manager - * - * @var IManager - */ - private $shareManager; - - /** - * Session - * - * @var ISession - */ - private $session; - - /** - * Application configuration - * - * @var AppConfig - */ - private $config; - - /** - * @param string $AppName - application name - * @param IL10N $trans - l10n service - * @param ILogger $logger - logger - * @param AppConfig $config - application configuration - * @param IManager $shareManager - Share manager - * @param IManager $ISession - Session - */ - public function __construct($AppName, - IL10N $trans, - ILogger $logger, - AppConfig $config, - IManager $shareManager, - ISession $session) { - $this->appName = $AppName; - $this->trans = $trans; - $this->logger = $logger; - $this->config = $config; - $this->shareManager = $shareManager; - $this->session = $session; - } - - /** - * Getting file by token - * - * @param integer $fileId - file identifier - * @param string $shareToken - access token - * @param string $path - file path - * - * @return array - */ - public function getFileByToken($fileId, $shareToken, $path = null) { - list ($node, $error, $share) = $this->getNodeByToken($shareToken); - - if (isset($error)) { - return [null, $error, null]; - } - - if ($node instanceof Folder) { - if ($fileId !== null && $fileId !== 0) { - try { - $files = $node->getById($fileId); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "getFileByToken: $fileId", "app" => $this->appName]); - return [null, $this->trans->t("Invalid request"), null]; - } - - if (empty($files)) { - $this->logger->info("Files not found: $fileId", ["app" => $this->appName]); - return [null, $this->trans->t("File not found"), null]; - } - $file = $files[0]; - } else { - try { - $file = $node->get($path); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "getFileByToken for path: $path", "app" => $this->appName]); - return [null, $this->trans->t("Invalid request"), null]; - } - } - } else { - $file = $node; - } - - return [$file, null, $share]; - } - - /** - * Getting file by token - * - * @param string $shareToken - access token - * - * @return array - */ - public function getNodeByToken($shareToken) { - list ($share, $error) = $this->getShare($shareToken); - - if (isset($error)) { - return [null, $error, null]; - } - - if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) { - return [null, $this->trans->t("You do not have enough permissions to view the file"), null]; - } - - try { - $node = $share->getNode(); - } catch (NotFoundException $e) { - $this->logger->logException($e, ["message" => "getNodeByToken error", "app" => $this->appName]); - return [null, $this->trans->t("File not found"), null]; - } - - return [$node, null, $share]; - } - - /** - * Getting share by token - * - * @param string $shareToken - access token - * - * @return array - */ - public function getShare($shareToken) { - if (empty($shareToken)) { - return [null, $this->trans->t("FileId is empty")]; - } - - $share = null; - try { - $share = $this->shareManager->getShareByToken($shareToken); - } catch (ShareNotFound $e) { - $this->logger->logException($e, ["message" => "getShare error", "app" => $this->appName]); - $share = null; - } - - if ($share === null || $share === false) { - return [null, $this->trans->t("You do not have enough permissions to view the file")]; - } - - if ($share->getPassword() - && (!$this->session->exists("public_link_authenticated") - || $this->session->get("public_link_authenticated") !== (string) $share->getId())) { - return [null, $this->trans->t("You do not have enough permissions to view the file")]; - } - - return [$share, null]; - } - - /** - * Generate unique document identifier - * - * @param File $file - file - * @param bool $origin - request from federated store - * - * @return string - */ - public function getKey($file, $origin = false) { - $fileId = $file->getId(); - - if ($origin - && RemoteInstance::isRemoteFile($file)) { - - $key = RemoteInstance::getRemoteKey($file); - if (!empty($key)) { - return $key; - } - } - - $key = KeyManager::get($fileId); - - if (empty($key) ) { - $instanceId = $this->config->GetSystemValue("instanceid", true); - - $key = $instanceId . "_" . $this->GUID(); - - KeyManager::set($fileId, $key); - } - - return $key; - } - - /** - * Detected attribute permission for shared file - * - * @param File $file - file - * @param string $attribute - request from federated store - * - * @return bool - */ - public function hasPermissionAttribute($file, $attribute = "download") { - $fileStorage = $file->getStorage(); - if ($fileStorage->instanceOfStorage("\OCA\Files_Sharing\SharedStorage")) { - $storageShare = $fileStorage->getShare(); - if (method_exists($storageShare, "getAttributes")) { - $attributes = $storageShare->getAttributes(); - - $permissionsDownload = $attributes->getAttribute("permissions", "download"); - if ($permissionsDownload !== null && $permissionsDownload !== true) { - return false; - } - } - } - - return true; - } - - /** - * Generate unique identifier - * - * @return string - */ - private function GUID() - { - if (function_exists("com_create_guid") === true) - { - return trim(com_create_guid(), "{}"); - } - - return sprintf('%04X%04X-%04X-%04X-%04X-%04X%04X%04X', mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(16384, 20479), mt_rand(32768, 49151), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535)); - } - - /** - * Generate unique file version key - * - * @param Version $version - file version - * - * @return string - */ - public function getVersionKey($version) { - $instanceId = $this->config->GetSystemValue("instanceid", true); - - $key = $instanceId . "_" . $version->getSourceFile()->getEtag() . "_" . $version->getRevisionId(); - - return $key; - } + /** + * Application name + * + * @var string + */ + private $appName; + + /** + * l10n service + * + * @var IL10N + */ + private $trans; + + /** + * Logger + * + * @var ILogger + */ + private $logger; + + /** + * Share manager + * + * @var IManager + */ + private $shareManager; + + /** + * Session + * + * @var ISession + */ + private $session; + + /** + * Application configuration + * + * @var AppConfig + */ + private $config; + + /** + * @param string $AppName - application name + * @param IL10N $trans - l10n service + * @param ILogger $logger - logger + * @param AppConfig $config - application configuration + * @param IManager $shareManager - Share manager + * @param ISession $session - Session + */ + public function __construct( + $AppName, + IL10N $trans, + ILogger $logger, + AppConfig $config, + IManager $shareManager, + ISession $session + ) { + $this->appName = $AppName; + $this->trans = $trans; + $this->logger = $logger; + $this->config = $config; + $this->shareManager = $shareManager; + $this->session = $session; + } + + /** + * Getting file by token + * + * @param integer $fileId - file identifier + * @param string $shareToken - access token + * @param string $path - file path + * + * @return array + */ + public function getFileByToken($fileId, $shareToken, $path = null) { + list($node, $error, $share) = $this->getNodeByToken($shareToken); + + if (isset($error)) { + return [null, $error, null]; + } + + if ($node instanceof Folder) { + if ($fileId !== null && $fileId !== 0) { + try { + $files = $node->getById($fileId); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "getFileByToken: $fileId", "app" => $this->appName]); + return [null, $this->trans->t("Invalid request"), null]; + } + + if (empty($files)) { + $this->logger->info("Files not found: $fileId", ["app" => $this->appName]); + return [null, $this->trans->t("File not found"), null]; + } + $file = $files[0]; + } else { + try { + $file = $node->get($path); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "getFileByToken for path: $path", "app" => $this->appName]); + return [null, $this->trans->t("Invalid request"), null]; + } + } + } else { + $file = $node; + } + + return [$file, null, $share]; + } + + /** + * Getting file by token + * + * @param string $shareToken - access token + * + * @return array + */ + public function getNodeByToken($shareToken) { + list($share, $error) = $this->getShare($shareToken); + + if (isset($error)) { + return [null, $error, null]; + } + + if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) { + return [null, $this->trans->t("You do not have enough permissions to view the file"), null]; + } + + try { + $node = $share->getNode(); + } catch (NotFoundException $e) { + $this->logger->logException($e, ["message" => "getNodeByToken error", "app" => $this->appName]); + return [null, $this->trans->t("File not found"), null]; + } + + return [$node, null, $share]; + } + + /** + * Getting share by token + * + * @param string $shareToken - access token + * + * @return array + */ + public function getShare($shareToken) { + if (empty($shareToken)) { + return [null, $this->trans->t("FileId is empty")]; + } + + $share = null; + try { + $share = $this->shareManager->getShareByToken($shareToken); + } catch (ShareNotFound $e) { + $this->logger->logException($e, ["message" => "getShare error", "app" => $this->appName]); + $share = null; + } + + if ($share === null || $share === false) { + return [null, $this->trans->t("You do not have enough permissions to view the file")]; + } + + if ($share->getPassword() + && (!$this->session->exists("public_link_authenticated") + || $this->session->get("public_link_authenticated") !== (string) $share->getId()) + ) { + return [null, $this->trans->t("You do not have enough permissions to view the file")]; + } + + return [$share, null]; + } + + /** + * Generate unique document identifier + * + * @param File $file - file + * @param bool $origin - request from federated store + * + * @return string + */ + public function getKey($file, $origin = false) { + $fileId = $file->getId(); + + if ($origin + && RemoteInstance::isRemoteFile($file) + ) { + $key = RemoteInstance::getRemoteKey($file); + if (!empty($key)) { + return $key; + } + } + + $key = KeyManager::get($fileId); + + if (empty($key)) { + $instanceId = $this->config->getSystemValue("instanceid", true); + + $key = $instanceId . "_" . $this->GUID(); + + KeyManager::set($fileId, $key); + } + + return $key; + } + + /** + * Detected attribute permission for shared file + * + * @param File $file - file + * @param string $attribute - request from federated store + * + * @return bool + */ + public function hasPermissionAttribute($file, $attribute = "download") { + $fileStorage = $file->getStorage(); + if ($fileStorage->instanceOfStorage("\OCA\Files_Sharing\SharedStorage")) { + $storageShare = $fileStorage->getShare(); + if (method_exists($storageShare, "getAttributes")) { + $attributes = $storageShare->getAttributes(); + + $permissionsDownload = $attributes->getAttribute("permissions", "download"); + if ($permissionsDownload !== null && $permissionsDownload !== true) { + return false; + } + } + } + + return true; + } + + /** + * Generate unique identifier + * + * @return string + */ + private function GUID() { + if (\function_exists("com_create_guid") === true) { + return trim(com_create_guid(), "{}"); + } + + return sprintf('%04X%04X-%04X-%04X-%04X-%04X%04X%04X', mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(16384, 20479), mt_rand(32768, 49151), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535)); + } + + /** + * Generate unique file version key + * + * @param Version $version - file version + * + * @return string + */ + public function getVersionKey($version) { + $instanceId = $this->config->getSystemValue("instanceid", true); + + $key = $instanceId . "_" . $version->getSourceFile()->getEtag() . "_" . $version->getRevisionId(); + + return $key; + } + + /** + * The method checks download permission + * + * @param IShare $share - share object + * + * @return bool + */ + public static function canShareDownload($share) { + $can = true; + + $downloadAttribute = self::getShareAttrubute($share, "download"); + if (isset($downloadAttribute)) { + $can = $downloadAttribute; + } + + return $can; + } + + /** + * The method extracts share attribute + * + * @param IShare $share - share object + * @param string $attribute - attribute name + * + * @return bool|null + */ + private static function getShareAttrubute($share, $attribute) { + $attributes = null; + if (method_exists(IShare::class, "getAttributes")) { + $attributes = $share->getAttributes(); + } + + $attribute = isset($attributes) ? $attributes->getAttribute("permissions", $attribute) : null; + + return $attribute; + } } diff --git a/lib/fileversions.php b/lib/fileversions.php index c0eb145d..9aaea3ca 100644 --- a/lib/fileversions.php +++ b/lib/fileversions.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,445 +35,456 @@ * @package OCA\Onlyoffice */ class FileVersions { - - /** - * Application name - * - * @var string - */ - private static $appName = "onlyoffice"; - - /** - * Changes file extension - * - * @var string - */ - private static $changesExt = ".zip"; - - /** - * History file extension - * - * @var string - */ - private static $historyExt = ".json"; - - /** - * File name contain author - * - * @var string - */ - private static $authorExt = "_author.json"; - - /** - * Split file path and version id - * - * @param string $pathVersion - version path - * - * @return array - */ - public static function splitPathVersion($pathVersion) { - $pos = strrpos($pathVersion, ".v"); - if ($pos === false) { - return false; - } - $filePath = substr($pathVersion, 0, $pos); - $versionId = substr($pathVersion, 2 + $pos - strlen($pathVersion)); - return [$filePath, $versionId]; - } - - /** - * Check if folder is not exist - * - * @param View $view - view - * @param string $path - folder path - * @param bool $createIfNotExist - create folder if not exist - * - * @return bool - */ - private static function checkFolderExist($view, $path, $createIfNotExist = false) { - if ($view->is_dir($path)) { - return true; - } - if (!$createIfNotExist) { - return false; - } - $view->mkdir($path); - return true; - } - - /** - * Get view and path for changes - * - * @param string $userId - user id - * @param string $fileId - file id - * @param bool $createIfNotExist - create folder if not exist - * - * @return array - */ - private static function getView($userId, $fileId, $createIfNotExist = false) { - $view = new View("/" . $userId); - - $path = self::$appName; - if (!self::checkFolderExist($view, $path, $createIfNotExist)) { - return [null, null]; - } - - if ($fileId === null) { - return [$view, $path]; - } - - $path = $path . "/" . $fileId; - if (!self::checkFolderExist($view, $path, $createIfNotExist)) { - return [null, null]; - } - - return [$view, $path]; - } - - /** - * Get changes from stored to history object - * - * @param string $ownerId - file owner id - * @param string $fileId - file id - * @param string $versionId - file version - * @param string $prevVersion - previous version for check - * - * @return array - */ - public static function getHistoryData($ownerId, $fileId, $versionId, $prevVersion) { - $logger = \OC::$server->getLogger(); - - if ($ownerId === null || $fileId === null) { - return null; - } - - list ($view, $path) = self::getView($ownerId, $fileId); - if ($view === null) { - return null; - } - - $historyPath = $path . "/" . $versionId . self::$historyExt; - if (!$view->file_exists($historyPath)) { - return null; - } - - $historyDataString = $view->file_get_contents($historyPath); - - try { - $historyData = json_decode($historyDataString, true); - - if ($historyData["prev"] !== $prevVersion) { - $logger->debug("getHistoryData: previous $prevVersion != " . $historyData["prev"], ["app" => self::$appName]); - - $view->unlink($historyPath); - $logger->debug("getHistoryData: delete $historyPath", ["app" => self::$appName]); - - $changesPath = $path . "/" . $versionId . self::$changesExt; - if ($view->file_exists($changesPath)) { - $view->unlink($changesPath); - $logger->debug("getHistoryData: delete $changesPath", ["app" => self::$appName]); - } - return null; - } - - return $historyData; - } catch (\Exception $e) { - $logger->logException($e, ["message" => "getHistoryData: $fileId $versionId", "app" => self::$appName]); - return null; - } - } - - /** - * Check if changes is stored - * - * @param string $ownerId - file owner id - * @param string $fileId - file id - * @param string $versionId - file version - * - * @return bool - */ - public static function hasChanges($ownerId, $fileId, $versionId) { - if ($ownerId === null || $fileId === null) { - return false; - } - - list ($view, $path) = self::getView($ownerId, $fileId); - if ($view === null) { - return false; - } - - $changesPath = $path . "/" . $versionId . self::$changesExt; - return $view->file_exists($changesPath); - } - - /** - * Get changes file - * - * @param string $ownerId - file owner id - * @param string $fileId - file id - * @param string $versionId - file version - * - * @return File - */ - public static function getChangesFile($ownerId, $fileId, $versionId) { - if ($ownerId === null || $fileId === null) { - return null; - } - - list ($view, $path) = self::getView($ownerId, $fileId); - if ($view === null) { - return null; - } - - $changesPath = $path . "/" . $versionId . self::$changesExt; - if (!$view->file_exists($changesPath)) { - return null; - } - - $changesInfo = $view->getFileInfo($changesPath); - $changes = new File($view->getRoot(), $view, $changesPath, $changesInfo); - - \OC::$server->getLogger()->debug("getChangesFile: $fileId for $ownerId get changes $changesPath", ["app" => self::$appName]); - - return $changes; - } - - /** - * Save history to storage - * - * @param FileInfo $fileInfo - file info - * @param array $history - file history - * @param string $changesurl - file changes - * @param string $prevVersion - previous version for check - */ - public static function saveHistory($fileInfo, $history, $changes, $prevVersion) { - $logger = \OC::$server->getLogger(); - - if ($fileInfo === null) { - return; - } - - $owner = $fileInfo->getOwner(); - if ($owner === null) { - return; - } - - if (empty($history) || empty($changes)) { - return; - } - - if ($fileInfo->getStorage()->instanceOfStorage(SharingExternalStorage::class)) { - return; - } - - $ownerId = $owner->getUID(); - $fileId = $fileInfo->getId(); - $versionId = $fileInfo->getMtime(); - - list ($view, $path) = self::getView($ownerId, $fileId, true); - - try { - $changesPath = $path . "/" . $versionId . self::$changesExt; - $view->touch($changesPath); - $view->file_put_contents($changesPath, $changes); - - $history["prev"] = $prevVersion; - $historyPath = $path . "/" . $versionId . self::$historyExt; - $view->touch($historyPath); - $view->file_put_contents($historyPath, json_encode($history)); - - $logger->debug("saveHistory: $fileId for $ownerId stored changes $changesPath history $historyPath", ["app" => self::$appName]); - } catch (\Exception $e) { - $logger->logException($e, ["message" => "saveHistory: save $fileId history error", "app" => self::$appName]); - } - } - - /** - * Delete all versions of file - * - * @param string $ownerId - file owner id - * @param string $fileId - file id - */ - public static function deleteAllVersions($ownerId, $fileId = null) { - $logger = \OC::$server->getLogger(); - - $logger->debug("deleteAllVersions $ownerId $fileId", ["app" => self::$appName]); - - if ($ownerId === null) { - return; - } - - list ($view, $path) = self::getView($ownerId, $fileId); - if ($view === null) { - return; - } - - $view->unlink($path); - } - - /** - * Delete changes and history - * - * @param string $ownerId - file owner id - * @param string $fileId - file id - * @param string $versionId - file version - */ - public static function deleteVersion($ownerId, $fileId, $versionId) { - $logger = \OC::$server->getLogger(); - - $logger->debug("deleteVersion $fileId ($versionId)", ["app" => self::$appName]); - - if ($ownerId === null) { - return; - } - if ($fileId === null || empty($versionId)) { - return; - } - - list ($view, $path) = self::getView($ownerId, $fileId); - if ($view === null) { - return null; - } - - $historyPath = $path . "/" . $versionId . self::$historyExt; - if ($view->file_exists($historyPath)) { - $view->unlink($historyPath); - $logger->debug("deleteVersion $historyPath", ["app" => self::$appName]); - } - - $changesPath = $path . "/" . $versionId . self::$changesExt; - if ($view->file_exists($changesPath)) { - $view->unlink($changesPath); - $logger->debug("deleteVersion $changesPath", ["app" => self::$appName]); - } - } - - /** - * Clear all version history - */ - public static function clearHistory() { - $logger = \OC::$server->getLogger(); - - $userDatabase = new Database(); - $userIds = $userDatabase->getUsers(); - - $view = new View("/"); - - foreach ($userIds as $userId) { - $path = $userId . "/" . self::$appName; - - if ($view->file_exists($path)) { - $view->unlink($path); - } - } - - $logger->debug("clear all history", ["app" => self::$appName]); - } - - /** - * Save file author - * - * @param FileInfo $fileInfo - file info - * @param IUser $author - version author - */ - public static function saveAuthor($fileInfo, $author) { - $logger = \OC::$server->getLogger(); - - if ($fileInfo === null || $author === null) { - return; - } - - $owner = $fileInfo->getOwner(); - if ($owner === null) { - return; - } - - if ($fileInfo->getStorage()->instanceOfStorage(SharingExternalStorage::class)) { - return; - } - - $ownerId = $owner->getUID(); - $fileId = $fileInfo->getId(); - $versionId = $fileInfo->getMtime(); - - list ($view, $path) = self::getView($ownerId, $fileId, true); - - try { - $authorPath = $path . "/" . $versionId . self::$authorExt; - $view->touch($authorPath); - - $authorData = [ - "id" => $author->getUID(), - "name" => $author->getDisplayName() - ]; - $view->file_put_contents($authorPath, json_encode($authorData)); - - $logger->debug("saveAuthor: $fileId for $ownerId stored author $authorPath", ["app" => self::$appName]); - } catch (\Exception $e) { - $logger->logException($e, ["message" => "saveAuthor: save $fileId author error", "app" => self::$appName]); - } - } - - /** - * Get version author id and name - * - * @param string $ownerId - file owner id - * @param string $fileId - file id - * @param string $versionId - file version - * - * @return array - */ - public static function getAuthor($ownerId, $fileId, $versionId) { - if ($ownerId === null || $fileId === null) { - return null; - } - - list ($view, $path) = self::getView($ownerId, $fileId); - if ($view === null) { - return null; - } - - $authorPath = $path . "/" . $versionId . self::$authorExt; - if (!$view->file_exists($authorPath)) { - return null; - } - - $authorDataString = $view->file_get_contents($authorPath); - $author = json_decode($authorDataString, true); - - \OC::$server->getLogger()->debug("getAuthor: $fileId v.$versionId for $ownerId get author $authorPath", ["app" => self::$appName]); - - return $author; - } - - /** - * Delete version author info - * - * @param string $ownerId - file owner id - * @param string $fileId - file id - * @param string $versionId - file version - */ - public static function deleteAuthor($ownerId, $fileId, $versionId) { - $logger = \OC::$server->getLogger(); - - $logger->debug("deleteAuthor $fileId ($versionId)", ["app" => self::$appName]); - - if ($ownerId === null) { - return; - } - if ($fileId === null || empty($versionId)) { - return; - } - - list ($view, $path) = self::getView($ownerId, $fileId); - if ($view === null) { - return null; - } - - $authorPath = $path . "/" . $versionId . self::$authorExt; - if ($view->file_exists($authorPath)) { - $view->unlink($authorPath); - $logger->debug("deleteAuthor $authorPath", ["app" => self::$appName]); - } - } -} \ No newline at end of file + /** + * Application name + * + * @var string + */ + private static $appName = "onlyoffice"; + + /** + * Changes file extension + * + * @var string + */ + private static $changesExt = ".zip"; + + /** + * History file extension + * + * @var string + */ + private static $historyExt = ".json"; + + /** + * File name contain author + * + * @var string + */ + private static $authorExt = "_author.json"; + + /** + * Split file path and version id + * + * @param string $pathVersion - version path + * + * @return array + */ + public static function splitPathVersion($pathVersion) { + $pos = strrpos($pathVersion, ".v"); + if ($pos === false) { + return false; + } + $filePath = substr($pathVersion, 0, $pos); + $versionId = substr($pathVersion, 2 + $pos - \strlen($pathVersion)); + return [$filePath, $versionId]; + } + + /** + * Check if folder is not exist + * + * @param View $view - view + * @param string $path - folder path + * @param bool $createIfNotExist - create folder if not exist + * + * @return bool + */ + private static function checkFolderExist($view, $path, $createIfNotExist = false) { + if ($view->is_dir($path)) { + return true; + } + if (!$createIfNotExist) { + return false; + } + $view->mkdir($path); + return true; + } + + /** + * Get view and path for changes + * + * @param string $userId - user id + * @param string $fileId - file id + * @param bool $createIfNotExist - create folder if not exist + * + * @return array + */ + private static function getView($userId, $fileId, $createIfNotExist = false) { + $view = new View("/" . $userId); + + $path = self::$appName; + if (!self::checkFolderExist($view, $path, $createIfNotExist)) { + return [null, null]; + } + + if ($fileId === null) { + return [$view, $path]; + } + + $path = $path . "/" . $fileId; + if (!self::checkFolderExist($view, $path, $createIfNotExist)) { + return [null, null]; + } + + return [$view, $path]; + } + + /** + * Get changes from stored to history object + * + * @param string $ownerId - file owner id + * @param string $fileId - file id + * @param string $versionId - file version + * @param string $prevVersion - previous version for check + * + * @return array + */ + public static function getHistoryData($ownerId, $fileId, $versionId, $prevVersion) { + $logger = \OC::$server->getLogger(); + + if ($ownerId === null || $fileId === null) { + return null; + } + + list($view, $path) = self::getView($ownerId, $fileId); + if ($view === null) { + return null; + } + + $historyPath = $path . "/" . $versionId . self::$historyExt; + if (!$view->file_exists($historyPath)) { + return null; + } + + $historyDataString = $view->file_get_contents($historyPath); + + try { + $historyData = json_decode($historyDataString, true); + + if ($historyData["prev"] !== $prevVersion) { + $logger->debug("getHistoryData: previous $prevVersion != " . $historyData["prev"], ["app" => self::$appName]); + + $view->unlink($historyPath); + $logger->debug("getHistoryData: delete $historyPath", ["app" => self::$appName]); + + $changesPath = $path . "/" . $versionId . self::$changesExt; + if ($view->file_exists($changesPath)) { + $view->unlink($changesPath); + $logger->debug("getHistoryData: delete $changesPath", ["app" => self::$appName]); + } + return null; + } + + return $historyData; + } catch (\Exception $e) { + $logger->logException($e, ["message" => "getHistoryData: $fileId $versionId", "app" => self::$appName]); + return null; + } + } + + /** + * Check if changes is stored + * + * @param string $ownerId - file owner id + * @param string $fileId - file id + * @param string $versionId - file version + * + * @return bool + */ + public static function hasChanges($ownerId, $fileId, $versionId) { + if ($ownerId === null || $fileId === null) { + return false; + } + + list($view, $path) = self::getView($ownerId, $fileId); + if ($view === null) { + return false; + } + + $changesPath = $path . "/" . $versionId . self::$changesExt; + return $view->file_exists($changesPath); + } + + /** + * Get changes file + * + * @param string $ownerId - file owner id + * @param string $fileId - file id + * @param string $versionId - file version + * + * @return File + */ + public static function getChangesFile($ownerId, $fileId, $versionId) { + if ($ownerId === null || $fileId === null) { + return null; + } + + list($view, $path) = self::getView($ownerId, $fileId); + if ($view === null) { + return null; + } + + $changesPath = $path . "/" . $versionId . self::$changesExt; + if (!$view->file_exists($changesPath)) { + return null; + } + + $changesInfo = $view->getFileInfo($changesPath); + $changes = new File($view->getRoot(), $view, $changesPath, $changesInfo); + + \OC::$server->getLogger()->debug("getChangesFile: $fileId for $ownerId get changes $changesPath", ["app" => self::$appName]); + + return $changes; + } + + /** + * Save history to storage + * + * @param FileInfo $fileInfo - file info + * @param array $history - file history + * @param string $changes - file changes + * @param string $prevVersion - previous version for check + * + * @return void + */ + public static function saveHistory($fileInfo, $history, $changes, $prevVersion) { + $logger = \OC::$server->getLogger(); + + if ($fileInfo === null) { + return; + } + + $owner = $fileInfo->getOwner(); + if ($owner === null) { + return; + } + + if (empty($history) || empty($changes)) { + return; + } + + if ($fileInfo->getStorage()->instanceOfStorage(SharingExternalStorage::class)) { + return; + } + + $ownerId = $owner->getUID(); + $fileId = $fileInfo->getId(); + $versionId = $fileInfo->getMtime(); + + list($view, $path) = self::getView($ownerId, $fileId, true); + + try { + $changesPath = $path . "/" . $versionId . self::$changesExt; + $view->touch($changesPath); + $view->file_put_contents($changesPath, $changes); + + $history["prev"] = $prevVersion; + $historyPath = $path . "/" . $versionId . self::$historyExt; + $view->touch($historyPath); + $view->file_put_contents($historyPath, json_encode($history)); + + $logger->debug("saveHistory: $fileId for $ownerId stored changes $changesPath history $historyPath", ["app" => self::$appName]); + } catch (\Exception $e) { + $logger->logException($e, ["message" => "saveHistory: save $fileId history error", "app" => self::$appName]); + } + } + + /** + * Delete all versions of file + * + * @param string $ownerId - file owner id + * @param string $fileId - file id + * + * @return void + */ + public static function deleteAllVersions($ownerId, $fileId = null) { + $logger = \OC::$server->getLogger(); + + $logger->debug("deleteAllVersions $ownerId $fileId", ["app" => self::$appName]); + + if ($ownerId === null) { + return; + } + + list($view, $path) = self::getView($ownerId, $fileId); + if ($view === null) { + return; + } + + $view->unlink($path); + } + + /** + * Delete changes and history + * + * @param string $ownerId - file owner id + * @param string $fileId - file id + * @param string $versionId - file version + * + * @return void|null + */ + public static function deleteVersion($ownerId, $fileId, $versionId) { + $logger = \OC::$server->getLogger(); + + $logger->debug("deleteVersion $fileId ($versionId)", ["app" => self::$appName]); + + if ($ownerId === null) { + return; + } + if ($fileId === null || empty($versionId)) { + return; + } + + list($view, $path) = self::getView($ownerId, $fileId); + if ($view === null) { + return null; + } + + $historyPath = $path . "/" . $versionId . self::$historyExt; + if ($view->file_exists($historyPath)) { + $view->unlink($historyPath); + $logger->debug("deleteVersion $historyPath", ["app" => self::$appName]); + } + + $changesPath = $path . "/" . $versionId . self::$changesExt; + if ($view->file_exists($changesPath)) { + $view->unlink($changesPath); + $logger->debug("deleteVersion $changesPath", ["app" => self::$appName]); + } + } + + /** + * Clear all version history + * + * @return void + */ + public static function clearHistory() { + $logger = \OC::$server->getLogger(); + + $userDatabase = new Database(); + $userIds = $userDatabase->getUsers(); + + $view = new View("/"); + + foreach ($userIds as $userId) { + $path = $userId . "/" . self::$appName; + + if ($view->file_exists($path)) { + $view->unlink($path); + } + } + + $logger->debug("clear all history", ["app" => self::$appName]); + } + + /** + * Save file author + * + * @param FileInfo $fileInfo - file info + * @param IUser $author - version author + * + * @return void + */ + public static function saveAuthor($fileInfo, $author) { + $logger = \OC::$server->getLogger(); + + if ($fileInfo === null || $author === null) { + return; + } + + $owner = $fileInfo->getOwner(); + if ($owner === null) { + return; + } + + if ($fileInfo->getStorage()->instanceOfStorage(SharingExternalStorage::class)) { + return; + } + + $ownerId = $owner->getUID(); + $fileId = $fileInfo->getId(); + $versionId = $fileInfo->getMtime(); + + list($view, $path) = self::getView($ownerId, $fileId, true); + + try { + $authorPath = $path . "/" . $versionId . self::$authorExt; + $view->touch($authorPath); + + $authorData = [ + "id" => $author->getUID(), + "name" => $author->getDisplayName() + ]; + $view->file_put_contents($authorPath, json_encode($authorData)); + + $logger->debug("saveAuthor: $fileId for $ownerId stored author $authorPath", ["app" => self::$appName]); + } catch (\Exception $e) { + $logger->logException($e, ["message" => "saveAuthor: save $fileId author error", "app" => self::$appName]); + } + } + + /** + * Get version author id and name + * + * @param string $ownerId - file owner id + * @param string $fileId - file id + * @param string $versionId - file version + * + * @return array + */ + public static function getAuthor($ownerId, $fileId, $versionId) { + if ($ownerId === null || $fileId === null) { + return null; + } + + list($view, $path) = self::getView($ownerId, $fileId); + if ($view === null) { + return null; + } + + $authorPath = $path . "/" . $versionId . self::$authorExt; + if (!$view->file_exists($authorPath)) { + return null; + } + + $authorDataString = $view->file_get_contents($authorPath); + $author = json_decode($authorDataString, true); + + \OC::$server->getLogger()->debug("getAuthor: $fileId v.$versionId for $ownerId get author $authorPath", ["app" => self::$appName]); + + return $author; + } + + /** + * Delete version author info + * + * @param string $ownerId - file owner id + * @param string $fileId - file id + * @param string $versionId - file version + * + * @return void|null + */ + public static function deleteAuthor($ownerId, $fileId, $versionId) { + $logger = \OC::$server->getLogger(); + + $logger->debug("deleteAuthor $fileId ($versionId)", ["app" => self::$appName]); + + if ($ownerId === null) { + return; + } + if ($fileId === null || empty($versionId)) { + return; + } + + list($view, $path) = self::getView($ownerId, $fileId); + if ($view === null) { + return null; + } + + $authorPath = $path . "/" . $versionId . self::$authorExt; + if ($view->file_exists($authorPath)) { + $view->unlink($authorPath); + $logger->debug("deleteAuthor $authorPath", ["app" => self::$appName]); + } + } +} diff --git a/lib/hookhandler.php b/lib/hookhandler.php index 175c9f14..36c6b465 100644 --- a/lib/hookhandler.php +++ b/lib/hookhandler.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,27 +26,29 @@ /** * Class HookHandler - * - * handles hooks * * @package OCA\Onlyoffice */ class HookHandler { - - public static function PublicPage() { - $appName = "onlyoffice"; - - $appConfig = new AppConfig($appName); - - if (!empty($appConfig->GetDocumentServerUrl()) && $appConfig->SettingsAreSuccessful()) { - Util::addScript("onlyoffice", "main"); - Util::addScript("onlyoffice", "share"); - - if ($appConfig->GetSameTab()) { - Util::addScript("onlyoffice", "listener"); - } - - Util::addStyle("onlyoffice", "main"); - } - } + /** + * Adds scripts and styles + * + * @return void + */ + public static function publicPage() { + $appName = "onlyoffice"; + + $appConfig = new AppConfig($appName); + + if (!empty($appConfig->getDocumentServerUrl()) && $appConfig->settingsAreSuccessful() && empty($appConfig->getLimitGroups())) { + Util::addScript("onlyoffice", "main"); + Util::addScript("onlyoffice", "share"); + + if ($appConfig->getSameTab()) { + Util::addScript("onlyoffice", "listener"); + } + + Util::addStyle("onlyoffice", "main"); + } + } } diff --git a/lib/hooks.php b/lib/hooks.php index 2a3eaac0..20ba2879 100644 --- a/lib/hooks.php +++ b/lib/hooks.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,155 +33,169 @@ * @package OCA\Onlyoffice */ class Hooks { - - /** - * Application name - * - * @var string - */ - private static $appName = "onlyoffice"; - - public static function connectHooks() { - // Listen user deletion - Util::connectHook("OC_User", "pre_deleteUser", Hooks::class, "userDelete"); - - // Listen file change - Util::connectHook("OC_Filesystem", "write", Hooks::class, "fileUpdate"); - - // Listen file deletion - Util::connectHook("OC_Filesystem", "delete", Hooks::class, "fileDelete"); - - // Listen file version deletion - Util::connectHook("\OCP\Versions", "preDelete", Hooks::class, "fileVersionDelete"); - - // Listen file version restore - Util::connectHook("\OCP\Versions", "rollback", Hooks::class, "fileVersionRestore"); - } - - /** - * Erase user file versions - * - * @param array $params - hook params - */ - public static function userDelete($params) { - $userId = $params["uid"]; - - FileVersions::deleteAllVersions($userId); - } - - /** - * Listen of file change - * - * @param array $params - hook params - */ - public static function fileUpdate($params) { - $filePath = $params[Filesystem::signal_param_path]; - if (empty($filePath)) { - return; - } - - $fileInfo = Filesystem::getFileInfo($filePath); - if ($fileInfo === false) { - return; - } - - $fileId = $fileInfo->getId(); - - KeyManager::delete($fileId); - - \OC::$server->getLogger()->debug("Hook fileUpdate " . json_encode($params), ["app" => self::$appName]); - } - - /** - * Erase versions of deleted file - * - * @param array $params - hook params - */ - public static function fileDelete($params) { - $filePath = $params[Filesystem::signal_param_path]; - if (empty($filePath)) { - return; - } - - try { - $ownerId = Filesystem::getOwner($filePath); - - $fileInfo = Filesystem::getFileInfo($filePath); - if ($fileInfo === false) { - return; - } - - $fileId = $fileInfo->getId(); - - KeyManager::delete($fileId, true); - - FileVersions::deleteAllVersions($ownerId, $fileId); - } catch (\Exception $e) { - \OC::$server->getLogger()->logException($e, ["message" => "Hook: fileDelete " . json_encode($params), "app" => self::$appName]); - } - } - - /** - * Erase versions of deleted version of file - * - * @param array $params - hook param - */ - public static function fileVersionDelete($params) { - $pathVersion = $params["path"]; - if (empty($pathVersion)) { - return; - } - - try { - list ($filePath, $versionId) = FileVersions::splitPathVersion($pathVersion); - if (empty($filePath)) { - return; - } - - $ownerId = Filesystem::getOwner($filePath); - - $fileInfo = Filesystem::getFileInfo($filePath); - if ($fileInfo === false) { - return; - } - - $fileId = $fileInfo->getId(); - - FileVersions::deleteVersion($ownerId, $fileId, $versionId); - FileVersions::deleteAuthor($ownerId, $fileId, $versionId); - } catch (\Exception $e) { - \OC::$server->getLogger()->logException($e, ["message" => "Hook: fileVersionDelete " . json_encode($params), "app" => self::$appName]); - } - } - - /** - * Erase versions of restored version of file - * - * @param array $params - hook param - */ - public static function fileVersionRestore($params) { - $filePath = $params["path"]; - if (empty($filePath)) { - return; - } - - $versionId = $params["revision"]; - - try { - $ownerId = Filesystem::getOwner($filePath); - - $fileInfo = Filesystem::getFileInfo($filePath); - if ($fileInfo === false) { - return; - } - - $fileId = $fileInfo->getId(); - - KeyManager::delete($fileId); - - FileVersions::deleteVersion($ownerId, $fileId, $versionId); - } catch (\Exception $e) { - \OC::$server->getLogger()->logException($e, ["message" => "Hook: fileVersionRestore " . json_encode($params), "app" => self::$appName]); - } - } -} \ No newline at end of file + /** + * Application name + * + * @var string + */ + private static $appName = "onlyoffice"; + + /** + * Connect hooks + * + * @return void + */ + public static function connectHooks() { + // Listen user deletion + Util::connectHook("OC_User", "pre_deleteUser", Hooks::class, "userDelete"); + + // Listen file change + Util::connectHook("OC_Filesystem", "write", Hooks::class, "fileUpdate"); + + // Listen file deletion + Util::connectHook("OC_Filesystem", "delete", Hooks::class, "fileDelete"); + + // Listen file version deletion + Util::connectHook("\OCP\Versions", "preDelete", Hooks::class, "fileVersionDelete"); + + // Listen file version restore + Util::connectHook("\OCP\Versions", "rollback", Hooks::class, "fileVersionRestore"); + } + + /** + * Erase user file versions + * + * @param array $params - hook params + * + * @return void + */ + public static function userDelete($params) { + $userId = $params["uid"]; + + FileVersions::deleteAllVersions($userId); + } + + /** + * Listen of file change + * + * @param array $params - hook params + * + * @return void + */ + public static function fileUpdate($params) { + $filePath = $params[Filesystem::signal_param_path]; + if (empty($filePath)) { + return; + } + + $fileInfo = Filesystem::getFileInfo($filePath); + if ($fileInfo === false) { + return; + } + + $fileId = $fileInfo->getId(); + + KeyManager::delete($fileId); + + \OC::$server->getLogger()->debug("Hook fileUpdate " . json_encode($params), ["app" => self::$appName]); + } + + /** + * Erase versions of deleted file + * + * @param array $params - hook params + * + * @return void + */ + public static function fileDelete($params) { + $filePath = $params[Filesystem::signal_param_path]; + if (empty($filePath)) { + return; + } + + try { + $ownerId = Filesystem::getOwner($filePath); + + $fileInfo = Filesystem::getFileInfo($filePath); + if ($fileInfo === false) { + return; + } + + $fileId = $fileInfo->getId(); + + KeyManager::delete($fileId, true); + + FileVersions::deleteAllVersions($ownerId, $fileId); + } catch (\Exception $e) { + \OC::$server->getLogger()->logException($e, ["message" => "Hook: fileDelete " . json_encode($params), "app" => self::$appName]); + } + } + + /** + * Erase versions of deleted version of file + * + * @param array $params - hook param + * + * @return void + */ + public static function fileVersionDelete($params) { + $pathVersion = $params["path"]; + if (empty($pathVersion)) { + return; + } + + try { + list($filePath, $versionId) = FileVersions::splitPathVersion($pathVersion); + if (empty($filePath)) { + return; + } + + $ownerId = Filesystem::getOwner($filePath); + + $fileInfo = Filesystem::getFileInfo($filePath); + if ($fileInfo === false) { + return; + } + + $fileId = $fileInfo->getId(); + + FileVersions::deleteVersion($ownerId, $fileId, $versionId); + FileVersions::deleteAuthor($ownerId, $fileId, $versionId); + } catch (\Exception $e) { + \OC::$server->getLogger()->logException($e, ["message" => "Hook: fileVersionDelete " . json_encode($params), "app" => self::$appName]); + } + } + + /** + * Erase versions of restored version of file + * + * @param array $params - hook param + * + * @return void + */ + public static function fileVersionRestore($params) { + $filePath = $params["path"]; + if (empty($filePath)) { + return; + } + + $versionId = $params["revision"]; + + try { + $ownerId = Filesystem::getOwner($filePath); + + $fileInfo = Filesystem::getFileInfo($filePath); + if ($fileInfo === false) { + return; + } + + $fileId = $fileInfo->getId(); + + KeyManager::delete($fileId); + + FileVersions::deleteVersion($ownerId, $fileId, $versionId); + } catch (\Exception $e) { + \OC::$server->getLogger()->logException($e, ["message" => "Hook: fileVersionRestore " . json_encode($params), "app" => self::$appName]); + } + } +} diff --git a/lib/keymanager.php b/lib/keymanager.php index 8c76bfee..969ff7a4 100644 --- a/lib/keymanager.php +++ b/lib/keymanager.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,125 +26,135 @@ * @package OCA\Onlyoffice */ class KeyManager { + /** + * Table name + */ + private const TABLENAME_KEY = "onlyoffice_filekey"; - /** - * Table name - */ - private const TableName_Key = "onlyoffice_filekey"; - - /** - * Get document identifier - * - * @param integer $fileId - file identifier - * - * @return string - */ - public static function get($fileId) { - $connection = \OC::$server->getDatabaseConnection(); - $select = $connection->prepare(" + /** + * Get document identifier + * + * @param integer $fileId - file identifier + * + * @return string + */ + public static function get($fileId) { + $connection = \OC::$server->getDatabaseConnection(); + $select = $connection->prepare( + " SELECT `key` - FROM `*PREFIX*" . self::TableName_Key . "` + FROM `*PREFIX*" . self::TABLENAME_KEY . "` WHERE `file_id` = ? - "); - $result = $select->execute([$fileId]); + " + ); + $result = $select->execute([$fileId]); - $keys = $result ? $select->fetch() : []; - $key = is_array($keys) && isset($keys["key"]) ? $keys["key"] : ""; + $keys = $result ? $select->fetch() : []; + $key = \is_array($keys) && isset($keys["key"]) ? $keys["key"] : ""; - return $key; - } + return $key; + } - /** - * Store document identifier - * - * @param integer $fileId - file identifier - * @param integer $key - file key - * - * @return bool - */ - public static function set($fileId, $key) { - $connection = \OC::$server->getDatabaseConnection(); - $insert = $connection->prepare(" - INSERT INTO `*PREFIX*" . self::TableName_Key . "` + /** + * Store document identifier + * + * @param integer $fileId - file identifier + * @param integer $key - file key + * + * @return bool + */ + public static function set($fileId, $key) { + $connection = \OC::$server->getDatabaseConnection(); + $insert = $connection->prepare( + " + INSERT INTO `*PREFIX*" . self::TABLENAME_KEY . "` (`file_id`, `key`) VALUES (?, ?) - "); - return (bool)$insert->execute([$fileId, $key]); - } + " + ); + return (bool)$insert->execute([$fileId, $key]); + } - /** - * Delete document identifier - * - * @param integer $fileId - file identifier - * @param bool $unlock - delete even with lock label - * - * @return bool - */ - public static function delete($fileId, $unlock = false) { - $connection = \OC::$server->getDatabaseConnection(); - $delete = $connection->prepare(" - DELETE FROM `*PREFIX*" . self::TableName_Key . "` + /** + * Delete document identifier + * + * @param integer $fileId - file identifier + * @param bool $unlock - delete even with lock label + * + * @return bool + */ + public static function delete($fileId, $unlock = false) { + $connection = \OC::$server->getDatabaseConnection(); + $delete = $connection->prepare( + " + DELETE FROM `*PREFIX*" . self::TABLENAME_KEY . "` WHERE `file_id` = ? " . ($unlock === false ? "AND `lock` != 1" : "") - ); - return (bool)$delete->execute([$fileId]); - } + ); + return (bool)$delete->execute([$fileId]); + } - /** - * Change lock status - * - * @param integer $fileId - file identifier - * @param bool $lock - status - * - * @return bool - */ - public static function lock($fileId, $lock = true) { - $connection = \OC::$server->getDatabaseConnection(); - $update = $connection->prepare(" - UPDATE `*PREFIX*" . self::TableName_Key . "` + /** + * Change lock status + * + * @param integer $fileId - file identifier + * @param bool $lock - status + * + * @return bool + */ + public static function lock($fileId, $lock = true) { + $connection = \OC::$server->getDatabaseConnection(); + $update = $connection->prepare( + " + UPDATE `*PREFIX*" . self::TABLENAME_KEY . "` SET `lock` = ? WHERE `file_id` = ? - "); - return (bool)$update->execute([$lock === true ? 1 : 0, $fileId]); - } + " + ); + return (bool)$update->execute([$lock === true ? 1 : 0, $fileId]); + } - /** - * Change forcesave status - * - * @param integer $fileId - file identifier - * @param bool $fs - status - * - * @return bool - */ - public static function setForcesave($fileId, $fs = true) { - $connection = \OC::$server->getDatabaseConnection(); - $update = $connection->prepare(" - UPDATE `*PREFIX*" . self::TableName_Key . "` + /** + * Change forcesave status + * + * @param integer $fileId - file identifier + * @param bool $fs - status + * + * @return bool + */ + public static function setForcesave($fileId, $fs = true) { + $connection = \OC::$server->getDatabaseConnection(); + $update = $connection->prepare( + " + UPDATE `*PREFIX*" . self::TABLENAME_KEY . "` SET `fs` = ? WHERE `file_id` = ? - "); - return (bool)$update->execute([$fs === true ? 1 : 0, $fileId]); - } + " + ); + return (bool)$update->execute([$fs === true ? 1 : 0, $fileId]); + } - /** - * Get forcesave status - * - * @param integer $fileId - file identifier - * - * @return bool - */ - public static function wasForcesave($fileId) { - $connection = \OC::$server->getDatabaseConnection(); - $select = $connection->prepare(" + /** + * Get forcesave status + * + * @param integer $fileId - file identifier + * + * @return bool + */ + public static function wasForcesave($fileId) { + $connection = \OC::$server->getDatabaseConnection(); + $select = $connection->prepare( + " SELECT `fs` - FROM `*PREFIX*" . self::TableName_Key . "` + FROM `*PREFIX*" . self::TABLENAME_KEY . "` WHERE `file_id` = ? - "); - $result = $select->execute([$fileId]); + " + ); + $result = $select->execute([$fileId]); - $rows = $result ? $select->fetch() : []; - $fs = is_array($rows) && isset($rows["fs"]) ? $rows["fs"] : ""; + $rows = $result ? $select->fetch() : []; + $fs = \is_array($rows) && isset($rows["fs"]) ? $rows["fs"] : ""; - return $fs === "1"; - } -} \ No newline at end of file + return $fs === "1"; + } +} diff --git a/lib/notifier.php b/lib/notifier.php index 42773a4b..85a91991 100644 --- a/lib/notifier.php +++ b/lib/notifier.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,111 +27,119 @@ use OCP\Notification\INotification; use OCP\Notification\INotifier; +/** + * Class Notifier + * + * @package OCA\Onlyoffice + */ class Notifier implements INotifier { - - /** - * Application name - * - * @var string - */ - private $appName; - - /** - * IFactory - * - * @var IFactory - */ - private $l10nFactory; - - /** - * Url generator service - * - * @var IURLGenerator - */ - private $urlGenerator; - - /** - * Logger - * - * @var ILogger - */ - private $logger; - - /** - * User manager - * - * @var IUserManager - */ - private $userManager; - - /** - * @param string $AppName - application name - * @param IFactory $l10NFactory - l10n - * @param IURLGenerator $urlGenerator - url generator service - * @param ILogger $logger - logger - * @param IUserManager $userManager - user manager - */ - public function __construct(string $appName, - IFactory $l10nFactory, - IURLGenerator $urlGenerator, - ILogger $logger, - IUserManager $userManager - ) { - $this->appName = $appName; - $this->l10nFactory = $l10nFactory; - $this->urlGenerator = $urlGenerator; - $this->logger = $logger; - $this->userManager = $userManager; - } - - /** - * @param INotification $notification - notification object - * @param string $languageCode - the code of the language that should be used to prepare the notification - * - * @return INotification - */ - public function prepare($notification, $languageCode) { - if ($notification->getApp() !== $this->appName) { - throw new \InvalidArgumentException("Notification not from " . $this->appName); - } - - $parameters = $notification->getSubjectParameters(); - $trans = $this->l10nFactory->get($this->appName, $languageCode); - - switch ($notification->getObjectType()) { - case "editorsCheck": - $message = $trans->t("Please check the settings to resolve the problem."); - $appSettingsLink = $this->urlGenerator->getAbsoluteURL("/settings/admin?sectionid=additional"); - $notification->setLink($appSettingsLink); - $notification->setParsedSubject($notification->getObjectId()) - ->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath($this->appName, 'app-dark.svg'))); - $notification->setParsedMessage($message); - break; - case "mention": - $notifierId = $parameters["notifierId"]; - $fileId = $parameters["fileId"]; - $fileName = $parameters["fileName"]; - $anchor = $parameters["anchor"]; - - $this->logger->info("Notify prepare: from $notifierId about $fileId ", ["app" => $this->appName]); - - $notifier = $this->userManager->get($notifierId); - $notifierName = $notifier->getDisplayName(); - $trans = $this->l10nFactory->get($this->appName, $languageCode); - - $notification->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath($this->appName, "app-dark.svg"))); - $notification->setParsedSubject($trans->t("%1\$s mentioned in the %2\$s: \"%3\$s\".", [$notifierName, $fileName, $notification->getObjectId()])); - - $editorLink = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".editor.index", [ - "fileId" => $fileId, - "anchor" => $anchor - ]); - - $notification->setLink($editorLink); - break; - default: - $this->logger->info("Unsupported notification object: ".$notification->getObjectType(), ["app" => $this->appName]); - } - return $notification; - } + /** + * Application name + * + * @var string + */ + private $appName; + + /** + * IFactory + * + * @var IFactory + */ + private $l10nFactory; + + /** + * Url generator service + * + * @var IURLGenerator + */ + private $urlGenerator; + + /** + * Logger + * + * @var ILogger + */ + private $logger; + + /** + * User manager + * + * @var IUserManager + */ + private $userManager; + + /** + * @param string $appName - application name + * @param IFactory $l10nFactory - l10n + * @param IURLGenerator $urlGenerator - url generator service + * @param ILogger $logger - logger + * @param IUserManager $userManager - user manager + */ + public function __construct( + string $appName, + IFactory $l10nFactory, + IURLGenerator $urlGenerator, + ILogger $logger, + IUserManager $userManager + ) { + $this->appName = $appName; + $this->l10nFactory = $l10nFactory; + $this->urlGenerator = $urlGenerator; + $this->logger = $logger; + $this->userManager = $userManager; + } + + /** + * @param INotification $notification - notification object + * @param string $languageCode - the code of the language that should be used to prepare the notification + * + * @return INotification + */ + public function prepare($notification, $languageCode) { + if ($notification->getApp() !== $this->appName) { + throw new \InvalidArgumentException("Notification not from " . $this->appName); + } + + $parameters = $notification->getSubjectParameters(); + $trans = $this->l10nFactory->get($this->appName, $languageCode); + + switch ($notification->getObjectType()) { + case "editorsCheck": + $message = $trans->t("Please check the settings to resolve the problem."); + $appSettingsLink = $this->urlGenerator->getAbsoluteURL("/settings/admin?sectionid=additional"); + $notification->setLink($appSettingsLink); + $notification->setParsedSubject($notification->getObjectId()) + ->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath($this->appName, 'app-dark.svg'))); + $notification->setParsedMessage($message); + break; + case "mention": + $notifierId = $parameters["notifierId"]; + $fileId = $parameters["fileId"]; + $fileName = $parameters["fileName"]; + $anchor = $parameters["anchor"]; + + $this->logger->info("Notify prepare: from $notifierId about $fileId ", ["app" => $this->appName]); + + $notifier = $this->userManager->get($notifierId); + $notifierName = $notifier->getDisplayName(); + $trans = $this->l10nFactory->get($this->appName, $languageCode); + + $notification->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath($this->appName, "app-dark.svg"))); + $notification->setParsedSubject($trans->t("%1\$s mentioned in the %2\$s: \"%3\$s\".", [$notifierName, $fileName, $notification->getObjectId()])); + + $editorLink = $this->urlGenerator->linkToRouteAbsolute( + $this->appName . ".editor.index", + [ + "fileId" => $fileId, + "anchor" => $anchor + ] + ); + + $notification->setLink($editorLink); + break; + default: + $this->logger->info("Unsupported notification object: " . $notification->getObjectType(), ["app" => $this->appName]); + } + return $notification; + } } diff --git a/lib/preview.php b/lib/preview.php index 026da674..98c1afe6 100644 --- a/lib/preview.php +++ b/lib/preview.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,350 +46,354 @@ * @package OCA\Onlyoffice */ class Preview implements IProvider2 { - - /** - * Application name - * - * @var string - */ - private $appName; - - /** - * Root folder - * - * @var IRootFolder - */ - private $root; - - /** - * User manager - * - * @var IUserManager - */ - private $userManager; - - /** - * Logger - * - * @var ILogger - */ - private $logger; - - /** - * l10n service - * - * @var IL10N - */ - private $trans; - - /** - * Application configuration - * - * @var AppConfig - */ - private $config; - - /** - * Url generator service - * - * @var IURLGenerator - */ - private $urlGenerator; - - /** - * Hash generator - * - * @var Crypt - */ - private $crypt; - - /** - * File version manager - * - * @var VersionManager - */ - private $versionManager; - - /** - * File utility - * - * @var FileUtility - */ - private $fileUtility; - - /** - * Capabilities mimetype - * - * @var Array - */ - public static $capabilities = [ - "text/csv", - "application/msword", - "application/vnd.ms-word.document.macroEnabled.12", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform", - "application/vnd.openxmlformats-officedocument.wordprocessingml.template", - "application/epub+zip", - "text/html", - "application/vnd.oasis.opendocument.presentation", - "application/vnd.oasis.opendocument.spreadsheet", - "application/vnd.oasis.opendocument.text", - "application/vnd.oasis.opendocument.presentation-template", - "application/vnd.oasis.opendocument.spreadsheet-template", - "application/vnd.oasis.opendocument.text-template", - "application/pdf", - "application/vnd.ms-powerpoint.template.macroEnabled.12", - "application/vnd.openxmlformats-officedocument.presentationml.template", - "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", - "application/vnd.openxmlformats-officedocument.presentationml.slideshow", - "application/vnd.ms-powerpoint", - "application/vnd.ms-powerpoint.presentation.macroEnabled.12", - "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "text/rtf", - "text/plain", - "application/vnd.ms-excel", - "application/vnd.ms-excel.sheet.macroEnabled.12", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/vnd.ms-excel.template.macroEnabled.12", - "application/vnd.openxmlformats-officedocument.spreadsheetml.template" - ]; - - /** - * Converted thumbnail format - */ - private const thumbExtension = "jpeg"; - - /** - * @param string $appName - application name - * @param IRootFolder $root - root folder - * @param ILogger $logger - logger - * @param IL10N $trans - l10n service - * @param AppConfig $config - application configuration - * @param IURLGenerator $urlGenerator - url generator service - * @param Crypt $crypt - hash generator - * @param IManager $shareManager - share manager - * @param ISession $session - session - * @param IUserManager $userManager - user manager - */ - public function __construct(string $appName, - IRootFolder $root, - ILogger $logger, - IL10N $trans, - AppConfig $config, - IURLGenerator $urlGenerator, - Crypt $crypt, - IManager $shareManager, - ISession $session, - IUserManager $userManager - ) { - $this->appName = $appName; - $this->root = $root; - $this->logger = $logger; - $this->trans = $trans; - $this->config = $config; - $this->urlGenerator = $urlGenerator; - $this->crypt = $crypt; - $this->userManager = $userManager; - - $this->versionManager = new VersionManager($appName, $root); - - $this->fileUtility = new FileUtility($appName, $trans, $logger, $config, $shareManager, $session); - } - - /** - * Return mime type - */ - public static function getMimeTypeRegex() { - $mimeTypeRegex = ""; - foreach (self::$capabilities as $format) { - if (!empty($mimeTypeRegex)) { - $mimeTypeRegex = $mimeTypeRegex . "|"; - } - $mimeTypeRegex = $mimeTypeRegex . str_replace("/", "\/", $format); - } - $mimeTypeRegex = "/" . $mimeTypeRegex . "/"; - - return $mimeTypeRegex; - } - - /** - * Return mime type - */ - public function getMimeType() { - $m = self::getMimeTypeRegex(); - return $m; - } - - /** - * The method checks if the file can be converted - * - * @param FileInfo $fileInfo - File - * - * @return bool - */ - public function isAvailable(FileInfo $fileInfo) { - if ($this->config->GetPreview() !== true) { - return false; - } - if (!$fileInfo - || $fileInfo->getSize() === 0 - || $fileInfo->getSize() > $this->config->GetLimitThumbSize()) { - return false; - } - if (!in_array($fileInfo->getMimetype(), self::$capabilities, true)) { - return false; - } - return true; - } - - /** - * The method is generated thumbnail for file and returned image object - * - * @param File $file - file - * @param int $maxX - The maximum X size of the thumbnail - * @param int $maxY - The maximum Y size of the thumbnail - * @param bool $scalingup - Disable/Enable upscaling of previews - * - * @return Image|bool false if no preview was generated - */ - public function getThumbnail($file, $maxX, $maxY, $scalingup) { - if (empty($file)) { - $this->logger->error("getThumbnail is impossible. File is null", ["app" => $this->appName]); - return false; - } - - $this->logger->debug("getThumbnail " . $file->getPath() . " $maxX $maxY", ["app" => $this->appName]); - - list ($fileUrl, $extension, $key) = $this->getFileParam($file); - if ($fileUrl === null || $extension === null || $key === null) { - return false; - } - - $imageUrl = null; - $documentService = new DocumentService($this->trans, $this->config); - try { - $imageUrl = $documentService->GetConvertedUri($fileUrl, $extension, self::thumbExtension, $key); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "GetConvertedUri: from $extension to " . self::thumbExtension, "app" => $this->appName]); - return false; - } - - try { - $thumbnail = $documentService->Request($imageUrl); - } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "Failed to download thumbnail", "app" => $this->appName]); - return false; - } - - $image = new Image(); - $image->loadFromData($thumbnail); - - if ($image->valid()) { - $image->scaleDownToFit($maxX, $maxY); - return $image; - } - - return false; - } - - /** - * Generate secure link to download document - * - * @param File $file - file - * @param IUser $user - user with access - * @param int $version - file version - * - * @return string - */ - private function getUrl($file, $user = null, $version = 0) { - - $data = [ - "action" => "download", - "fileId" => $file->getId() - ]; - - $userId = null; - if (!empty($user)) { - $userId = $user->getUID(); - $data["userId"] = $userId; - } - if ($version > 0) { - $data["version"] = $version; - } - - $hashUrl = $this->crypt->GetHash($data); - - $fileUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".callback.download", ["doc" => $hashUrl]); - - if (!$this->config->UseDemo() && !empty($this->config->GetStorageUrl())) { - $fileUrl = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->config->GetStorageUrl(), $fileUrl); - } - - return $fileUrl; - } - - /** - * Generate array with file parameters - * - * @param File $file - file - * - * @return array - */ - private function getFileParam($file) { - if ($file->getSize() === 0) { - return [null, null, null]; - } - - $key = null; - $versionNum = 0; - if ($file instanceof MetaFileVersionNode) { - if ($this->versionManager->available !== true) { - return [null, null, null]; - } - - $fileVersion = $file->getName(); - $sourceFileId = $file->getId(); - - $storage = $file->getStorage(); - $path = $file->getContentDispositionFileName(); - - $ownerId = $storage->getOwner($path); - $owner = $this->userManager->get($ownerId); - if ($owner === null) { - return [null, null, null]; - } - - $files = $this->root->getUserFolder($ownerId)->getById($sourceFileId); - if (empty($files)) { - return [null, null, null]; - } - $file = $files[0]; - - $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); - - foreach ($versions as $version) { - $versionNum = $versionNum + 1; - - $versionId = $version->getRevisionId(); - if (strcmp($versionId, $fileVersion) === 0) { - $key = $this->fileUtility->getVersionKey($version); - $key = DocumentService::GenerateRevisionId($key); - - break; - } - } - } else { - $owner = $file->getOwner(); - - $key = $this->fileUtility->getKey($file); - $key = DocumentService::GenerateRevisionId($key); - } - - $fileUrl = $this->getUrl($file, $owner, $versionNum); - - $fileExtension = strtolower(pathinfo($file->getName(), PATHINFO_EXTENSION)); - - return [$fileUrl, $fileExtension, $key]; - } -} \ No newline at end of file + /** + * Application name + * + * @var string + */ + private $appName; + + /** + * Root folder + * + * @var IRootFolder + */ + private $root; + + /** + * User manager + * + * @var IUserManager + */ + private $userManager; + + /** + * Logger + * + * @var ILogger + */ + private $logger; + + /** + * l10n service + * + * @var IL10N + */ + private $trans; + + /** + * Application configuration + * + * @var AppConfig + */ + private $config; + + /** + * Url generator service + * + * @var IURLGenerator + */ + private $urlGenerator; + + /** + * Hash generator + * + * @var Crypt + */ + private $crypt; + + /** + * File version manager + * + * @var VersionManager + */ + private $versionManager; + + /** + * File utility + * + * @var FileUtility + */ + private $fileUtility; + + /** + * Capabilities mimetype + * + * @var Array + */ + public static $capabilities = [ + "text/csv", + "application/msword", + "application/vnd.ms-word.document.macroEnabled.12", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform", + "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "application/epub+zip", + "text/html", + "application/vnd.oasis.opendocument.presentation", + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.oasis.opendocument.text", + "application/vnd.oasis.opendocument.presentation-template", + "application/vnd.oasis.opendocument.spreadsheet-template", + "application/vnd.oasis.opendocument.text-template", + "application/pdf", + "application/vnd.ms-powerpoint.template.macroEnabled.12", + "application/vnd.openxmlformats-officedocument.presentationml.template", + "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", + "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "application/vnd.ms-powerpoint", + "application/vnd.ms-powerpoint.presentation.macroEnabled.12", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "text/rtf", + "text/plain", + "application/vnd.ms-excel", + "application/vnd.ms-excel.sheet.macroEnabled.12", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-excel.template.macroEnabled.12", + "application/vnd.openxmlformats-officedocument.spreadsheetml.template" + ]; + + /** + * Converted thumbnail format + */ + private const THUMBEXTENSION = "jpeg"; + + /** + * @param string $appName - application name + * @param IRootFolder $root - root folder + * @param ILogger $logger - logger + * @param IL10N $trans - l10n service + * @param AppConfig $config - application configuration + * @param IURLGenerator $urlGenerator - url generator service + * @param Crypt $crypt - hash generator + * @param IManager $shareManager - share manager + * @param ISession $session - session + * @param IUserManager $userManager - user manager + */ + public function __construct( + string $appName, + IRootFolder $root, + ILogger $logger, + IL10N $trans, + AppConfig $config, + IURLGenerator $urlGenerator, + Crypt $crypt, + IManager $shareManager, + ISession $session, + IUserManager $userManager + ) { + $this->appName = $appName; + $this->root = $root; + $this->logger = $logger; + $this->trans = $trans; + $this->config = $config; + $this->urlGenerator = $urlGenerator; + $this->crypt = $crypt; + $this->userManager = $userManager; + + $this->versionManager = new VersionManager($appName, $root); + + $this->fileUtility = new FileUtility($appName, $trans, $logger, $config, $shareManager, $session); + } + + /** + * Return mime type + * + * @return string + */ + public static function getMimeTypeRegex() { + $mimeTypeRegex = ""; + foreach (self::$capabilities as $format) { + if (!empty($mimeTypeRegex)) { + $mimeTypeRegex = $mimeTypeRegex . "|"; + } + $mimeTypeRegex = $mimeTypeRegex . str_replace("/", "\/", $format); + } + $mimeTypeRegex = "/" . $mimeTypeRegex . "/"; + + return $mimeTypeRegex; + } + + /** + * Return mime type + * + * @return string + */ + public function getMimeType() { + $m = self::getMimeTypeRegex(); + return $m; + } + + /** + * The method checks if the file can be converted + * + * @param FileInfo $fileInfo - File + * + * @return bool + */ + public function isAvailable(FileInfo $fileInfo) { + if ($this->config->getPreview() !== true) { + return false; + } + if (!$fileInfo + || $fileInfo->getSize() === 0 + || $fileInfo->getSize() > $this->config->getLimitThumbSize() + ) { + return false; + } + if (!\in_array($fileInfo->getMimetype(), self::$capabilities, true)) { + return false; + } + return true; + } + + /** + * The method is generated thumbnail for file and returned image object + * + * @param File $file - file + * @param int $maxX - The maximum X size of the thumbnail + * @param int $maxY - The maximum Y size of the thumbnail + * @param bool $scalingup - Disable/Enable upscaling of previews + * + * @return Image|bool false if no preview was generated + */ + public function getThumbnail($file, $maxX, $maxY, $scalingup) { + if (empty($file)) { + $this->logger->error("getThumbnail is impossible. File is null", ["app" => $this->appName]); + return false; + } + + $this->logger->debug("getThumbnail " . $file->getPath() . " $maxX $maxY", ["app" => $this->appName]); + + list($fileUrl, $extension, $key) = $this->getFileParam($file); + if ($fileUrl === null || $extension === null || $key === null) { + return false; + } + + $imageUrl = null; + $documentService = new DocumentService($this->trans, $this->config); + try { + $imageUrl = $documentService->getConvertedUri($fileUrl, $extension, self::THUMBEXTENSION, $key); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "getConvertedUri: from $extension to " . self::THUMBEXTENSION, "app" => $this->appName]); + return false; + } + + try { + $thumbnail = $documentService->request($imageUrl); + } catch (\Exception $e) { + $this->logger->logException($e, ["message" => "Failed to download thumbnail", "app" => $this->appName]); + return false; + } + + $image = new Image(); + $image->loadFromData($thumbnail); + + if ($image->valid()) { + $image->scaleDownToFit($maxX, $maxY); + return $image; + } + + return false; + } + + /** + * Generate secure link to download document + * + * @param File $file - file + * @param IUser $user - user with access + * @param int $version - file version + * + * @return string + */ + private function getUrl($file, $user = null, $version = 0) { + $data = [ + "action" => "download", + "fileId" => $file->getId() + ]; + + $userId = null; + if (!empty($user)) { + $userId = $user->getUID(); + $data["userId"] = $userId; + } + if ($version > 0) { + $data["version"] = $version; + } + + $hashUrl = $this->crypt->getHash($data); + + $fileUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".callback.download", ["doc" => $hashUrl]); + + if (!$this->config->useDemo() && !empty($this->config->getStorageUrl())) { + $fileUrl = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->config->getStorageUrl(), $fileUrl); + } + + return $fileUrl; + } + + /** + * Generate array with file parameters + * + * @param File $file - file + * + * @return array + */ + private function getFileParam($file) { + if ($file->getSize() === 0) { + return [null, null, null]; + } + + $key = null; + $versionNum = 0; + if ($file instanceof MetaFileVersionNode) { + if ($this->versionManager->available !== true) { + return [null, null, null]; + } + + $fileVersion = $file->getName(); + $sourceFileId = $file->getId(); + + $storage = $file->getStorage(); + $path = $file->getContentDispositionFileName(); + + $ownerId = $storage->getOwner($path); + $owner = $this->userManager->get($ownerId); + if ($owner === null) { + return [null, null, null]; + } + + $files = $this->root->getUserFolder($ownerId)->getById($sourceFileId); + if (empty($files)) { + return [null, null, null]; + } + $file = $files[0]; + + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + + foreach ($versions as $version) { + $versionNum = $versionNum + 1; + + $versionId = $version->getRevisionId(); + if (strcmp($versionId, $fileVersion) === 0) { + $key = $this->fileUtility->getVersionKey($version); + $key = DocumentService::generateRevisionId($key); + + break; + } + } + } else { + $owner = $file->getOwner(); + + $key = $this->fileUtility->getKey($file); + $key = DocumentService::generateRevisionId($key); + } + + $fileUrl = $this->getUrl($file, $owner, $versionNum); + + $fileExtension = strtolower(pathinfo($file->getName(), PATHINFO_EXTENSION)); + + return [$fileUrl, $fileExtension, $key]; + } +} diff --git a/lib/remoteinstance.php b/lib/remoteinstance.php index 9ac739cd..f11f74b4 100644 --- a/lib/remoteinstance.php +++ b/lib/remoteinstance.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,255 +30,263 @@ * @package OCA\Onlyoffice */ class RemoteInstance { - - /** - * App name - */ - private const App_Name = "onlyoffice"; - - /** - * Table name - */ - private const TableName_Key = "onlyoffice_instance"; - - /** - * Time to live of remote instance (12 hours) - */ - private static $ttl = 60 * 60 * 12; - - /** - * Health remote list - */ - private static $healthRemote = []; - - /** - * Get remote instance - * - * @param string $remote - remote instance - * - * @return array - */ - private static function get($remote) { - $connection = \OC::$server->getDatabaseConnection(); - $select = $connection->prepare(" + /** + * App name + */ + private const APP_NAME = "onlyoffice"; + + /** + * Table name + */ + private const TABLENAME_KEY = "onlyoffice_instance"; + + /** + * Time to live of remote instance (12 hours) + */ + private static $ttl = 60 * 60 * 12; + + /** + * Health remote list + */ + private static $healthRemote = []; + + /** + * Get remote instance + * + * @param string $remote - remote instance + * + * @return array + */ + private static function get($remote) { + $connection = \OC::$server->getDatabaseConnection(); + $select = $connection->prepare( + " SELECT remote, expire, status - FROM `*PREFIX*" . self::TableName_Key . "` + FROM `*PREFIX*" . self::TABLENAME_KEY . "` WHERE `remote` = ? - "); - $result = $select->execute([$remote]); - - $dbremote = $result ? $select->fetch() : []; - - return $dbremote; - } - - /** - * Store remote instance - * - * @param string $remote - remote instance - * @param bool $status - remote status - * - * @return bool - */ - private static function set($remote, $status) { - $connection = \OC::$server->getDatabaseConnection(); - $insert = $connection->prepare(" - INSERT INTO `*PREFIX*" . self::TableName_Key . "` + " + ); + $result = $select->execute([$remote]); + + $dbremote = $result ? $select->fetch() : []; + + return $dbremote; + } + + /** + * Store remote instance + * + * @param string $remote - remote instance + * @param bool $status - remote status + * + * @return bool + */ + private static function set($remote, $status) { + $connection = \OC::$server->getDatabaseConnection(); + $insert = $connection->prepare( + " + INSERT INTO `*PREFIX*" . self::TABLENAME_KEY . "` (`remote`, `status`, `expire`) VALUES (?, ?, ?) - "); - return (bool)$insert->execute([$remote, $status === true ? 1 : 0, time()]); - } - - /** - * Update remote instance - * - * @param string $remote - remote instance - * @param bool $status - remote status - * - * @return bool - */ - private static function update($remote, $status) { - $connection = \OC::$server->getDatabaseConnection(); - $update = $connection->prepare(" - UPDATE `*PREFIX*" . self::TableName_Key . "` + " + ); + return (bool)$insert->execute([$remote, $status === true ? 1 : 0, time()]); + } + + /** + * Update remote instance + * + * @param string $remote - remote instance + * @param bool $status - remote status + * + * @return bool + */ + private static function update($remote, $status) { + $connection = \OC::$server->getDatabaseConnection(); + $update = $connection->prepare( + " + UPDATE `*PREFIX*" . self::TABLENAME_KEY . "` SET status = ?, expire = ? WHERE remote = ? - "); - return (bool)$update->execute([$status === true ? 1 : 0, time(), $remote]); - } - - /** - * Health check remote instance - * - * @param string $remote - remote instance - * - * @return bool - */ - public static function healthCheck($remote) { - $logger = \OC::$server->getLogger(); - $remote = rtrim($remote, "/") . "/"; - - if (in_array($remote, self::$healthRemote)) { - $logger->debug("Remote instance " . $remote . " from local cache status " . $dbremote["status"], ["app" => self::App_Name]); - return true; - } - - $dbremote = self::get($remote); - if (!empty($dbremote) && $dbremote["expire"] + self::$ttl > time()) { - $logger->debug("Remote instance " . $remote . " from database status " . $dbremote["status"], ["app" => self::App_Name]); - self::$healthRemote[$remote] = $dbremote["status"]; - return self::$healthRemote[$remote]; - } - - $httpClientService = \OC::$server->getHTTPClientService(); - $client = $httpClientService->newClient(); - - $status = false; - try { - $response = $client->get($remote . "ocs/v2.php/apps/" . self::App_Name . "/api/v1/healthcheck?format=json"); - $body = json_decode($response->getBody(), true); - - $data = $body["ocs"]["data"]; - if (isset($data["alive"])) { - $status = $data["alive"] === true; - } - } catch (\Exception $e) { - $logger->logException($e, ["message" => "Failed to request federated health check for" . $remote, "app" => self::App_Name]); - } - - if (empty($dbremote)) { - self::set($remote, $status); - } else { - self::update($remote, $status); - } - - $logger->debug("Remote instance " . $remote . " was stored to database status " . $dbremote["status"], ["app" => self::App_Name]); - - self::$healthRemote[$remote] = $status; - - return self::$healthRemote[$remote]; - } - - /** - * Generate unique document identifier in federated share - * - * @param File $file - file - * - * @return string - */ - public function getRemoteKey($file) { - $logger = \OC::$server->getLogger(); - - $remote = $file->getStorage()->getRemote(); - $shareToken = $file->getStorage()->getToken(); - $internalPath = $file->getInternalPath(); - - $httpClientService = \OC::$server->getHTTPClientService(); - $client = $httpClientService->newClient(); - - try { - $response = $client->post($remote . "/ocs/v2.php/apps/" . self::App_Name . "/api/v1/key?format=json", [ - "timeout" => 5, - "json" => [ - "shareToken" => $shareToken, - "path" => $internalPath - ] - ]); - - $body = \json_decode($response->getBody(), true); - - $data = $body["ocs"]["data"]; - if (!empty($data["error"])) { - $logger->error("Error federated key " . $data["error"], ["app" => self::App_Name]); - return null; - } - - $key = $data["key"]; - $logger->debug("Federated key: $key", ["app" => self::App_Name]); - - return $key; - } catch (\Exception $e) { - $logger->logException($e, ["message" => "Failed to request federated key " . $file->getId(), "app" => self::App_Name]); - - if ($e->getResponse()->getStatusCode() === 404) { - self::update($remote, false); - $logger->debug("Changed status for remote instance $remote to false", ["app" => self::App_Name]); - } - - return null; - } - } - - /** - * Change lock status in the federated share - * - * @param File $file - file - * @param bool $lock - status - * @param bool $fs - status - * - * @return bool - */ - public static function lockRemoteKey($file, $lock, $fs) { - $logger = \OC::$server->getLogger(); - $action = $lock ? "lock" : "unlock"; - - $remote = $file->getStorage()->getRemote(); - $shareToken = $file->getStorage()->getToken(); - $internalPath = $file->getInternalPath(); - - $httpClientService = \OC::$server->getHTTPClientService(); - $client = $httpClientService->newClient(); - $data = [ - "timeout" => 5, - "json" => [ - "shareToken" => $shareToken, - "path" => $internalPath, - "lock" => $lock - ] - ]; - if (!empty($fs)) { - $data["json"]["fs"] = $fs; - } - - try { - $response = $client->post($remote . "/ocs/v2.php/apps/" . self::App_Name . "/api/v1/keylock?format=json", $data); - $body = \json_decode($response->getBody(), true); - - $data = $body["ocs"]["data"]; - - if (empty($data)) { - $logger->debug("Federated request" . $action . "for " . $file->getFileInfo()->getId() . " is successful", ["app" => self::App_Name]); - return true; - } - - if (!empty($data["error"])) { - $logger->error("Error" . $action . "federated key for " . $file->getFileInfo()->getId() . ": " . $data["error"], ["app" => self::App_Name]); - return false; - } - } catch(\Exception $e) { - $logger->logException($e, ["message" => "Failed to request federated " . $action . " for " . $file->getFileInfo()->getId(), "app" => self::App_Name]); - return false; - } - } - - /** - * Check of federated capable - * - * @param File $file - file - * - * @return bool - */ - public static function isRemoteFile($file) { - $storage = $file->getStorage(); - - $alive = false; - $isFederated = $storage->instanceOfStorage(SharingExternalStorage::class); - if (!$isFederated) { - return false; - } - - $alive = RemoteInstance::healthCheck($storage->getRemote()); - return $alive; - } -} \ No newline at end of file + " + ); + return (bool)$update->execute([$status === true ? 1 : 0, time(), $remote]); + } + + /** + * Health check remote instance + * + * @param string $remote - remote instance + * + * @return bool + */ + public static function healthCheck($remote) { + $logger = \OC::$server->getLogger(); + $remote = rtrim($remote, "/") . "/"; + + if (\in_array($remote, self::$healthRemote)) { + $logger->debug("Remote instance " . $remote . " from local cache status " . $dbremote["status"], ["app" => self::APP_NAME]); + return true; + } + + $dbremote = self::get($remote); + if (!empty($dbremote) && $dbremote["expire"] + self::$ttl > time()) { + $logger->debug("Remote instance " . $remote . " from database status " . $dbremote["status"], ["app" => self::APP_NAME]); + self::$healthRemote[$remote] = $dbremote["status"]; + return self::$healthRemote[$remote]; + } + + $httpClientService = \OC::$server->getHTTPClientService(); + $client = $httpClientService->newClient(); + + $status = false; + try { + $response = $client->get($remote . "ocs/v2.php/apps/" . self::APP_NAME . "/api/v1/healthcheck?format=json"); + $body = json_decode($response->getBody(), true); + + $data = $body["ocs"]["data"]; + if (isset($data["alive"])) { + $status = $data["alive"] === true; + } + } catch (\Exception $e) { + $logger->logException($e, ["message" => "Failed to request federated health check for" . $remote, "app" => self::APP_NAME]); + } + + if (empty($dbremote)) { + self::set($remote, $status); + } else { + self::update($remote, $status); + } + + $logger->debug("Remote instance " . $remote . " was stored to database status " . $dbremote["status"], ["app" => self::APP_NAME]); + + self::$healthRemote[$remote] = $status; + + return self::$healthRemote[$remote]; + } + + /** + * Generate unique document identifier in federated share + * + * @param File $file - file + * + * @return string + */ + public function getRemoteKey($file) { + $logger = \OC::$server->getLogger(); + + $remote = $file->getStorage()->getRemote(); + $shareToken = $file->getStorage()->getToken(); + $internalPath = $file->getInternalPath(); + + $httpClientService = \OC::$server->getHTTPClientService(); + $client = $httpClientService->newClient(); + + try { + $response = $client->post( + $remote . "/ocs/v2.php/apps/" . self::APP_NAME . "/api/v1/key?format=json", + [ + "timeout" => 5, + "json" => [ + "shareToken" => $shareToken, + "path" => $internalPath + ] + ] + ); + + $body = \json_decode($response->getBody(), true); + + $data = $body["ocs"]["data"]; + if (!empty($data["error"])) { + $logger->error("Error federated key " . $data["error"], ["app" => self::APP_NAME]); + return null; + } + + $key = $data["key"]; + $logger->debug("Federated key: $key", ["app" => self::APP_NAME]); + + return $key; + } catch (\Exception $e) { + $logger->logException($e, ["message" => "Failed to request federated key " . $file->getId(), "app" => self::APP_NAME]); + + if ($e->getResponse()->getStatusCode() === 404) { + self::update($remote, false); + $logger->debug("Changed status for remote instance $remote to false", ["app" => self::APP_NAME]); + } + + return null; + } + } + + /** + * Change lock status in the federated share + * + * @param File $file - file + * @param bool $lock - status + * @param bool $fs - status + * + * @return bool + */ + public static function lockRemoteKey($file, $lock, $fs) { + $logger = \OC::$server->getLogger(); + $action = $lock ? "lock" : "unlock"; + + $remote = $file->getStorage()->getRemote(); + $shareToken = $file->getStorage()->getToken(); + $internalPath = $file->getInternalPath(); + + $httpClientService = \OC::$server->getHTTPClientService(); + $client = $httpClientService->newClient(); + $data = [ + "timeout" => 5, + "json" => [ + "shareToken" => $shareToken, + "path" => $internalPath, + "lock" => $lock + ] + ]; + if (!empty($fs)) { + $data["json"]["fs"] = $fs; + } + + try { + $response = $client->post($remote . "/ocs/v2.php/apps/" . self::APP_NAME . "/api/v1/keylock?format=json", $data); + $body = \json_decode($response->getBody(), true); + + $data = $body["ocs"]["data"]; + + if (empty($data)) { + $logger->debug("Federated request" . $action . "for " . $file->getFileInfo()->getId() . " is successful", ["app" => self::APP_NAME]); + return true; + } + + if (!empty($data["error"])) { + $logger->error("Error" . $action . "federated key for " . $file->getFileInfo()->getId() . ": " . $data["error"], ["app" => self::APP_NAME]); + return false; + } + } catch(\Exception $e) { + $logger->logException($e, ["message" => "Failed to request federated " . $action . " for " . $file->getFileInfo()->getId(), "app" => self::APP_NAME]); + return false; + } + } + + /** + * Check of federated capable + * + * @param File $file - file + * + * @return bool + */ + public static function isRemoteFile($file) { + $storage = $file->getStorage(); + + $alive = false; + $isFederated = $storage->instanceOfStorage(SharingExternalStorage::class); + if (!$isFederated) { + return false; + } + + $alive = RemoteInstance::healthCheck($storage->getRemote()); + return $alive; + } +} diff --git a/lib/templatemanager.php b/lib/templatemanager.php index eff5ab69..a2d99dcc 100644 --- a/lib/templatemanager.php +++ b/lib/templatemanager.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,191 +29,193 @@ * @package OCA\Onlyoffice */ class TemplateManager { - - /** - * Application name - * - * @var string - */ - private static $appName = "onlyoffice"; - - /** - * Template folder name - * - * @var string - */ - private static $templateFolderName = "template"; - - /** - * Get global template directory - * - * @return Folder - */ - public static function GetGlobalTemplateDir() { - $dirPath = self::$appName . "/" . self::$templateFolderName; - - $rootFolder = \OC::$server->getRootFolder(); - $templateDir = null; - try { - $templateDir = $rootFolder->get($dirPath); - } catch (NotFoundException $e) { - $templateDir = $rootFolder->newFolder($dirPath); - } - - return $templateDir; - } - - /** - * Get global templates - * - * @param string $mimetype - mimetype of the template - * - * @return array - */ - public static function GetGlobalTemplates($mimetype = null) { - $templateDir = self::GetGlobalTemplateDir(); - - $templatesList = $templateDir->getDirectoryListing(); - if (!empty($mimetype) - && is_array($templatesList) && count($templatesList) > 0) { - $templatesList = $templateDir->searchByMime($mimetype); - } - - return $templatesList; - } - - /** - * Get template file - * - * @param string $templateId - identifier file template - * - * @return File - */ - public static function GetTemplate($templateId) { - $logger = \OC::$server->getLogger(); - - $templateDir = self::GetGlobalTemplateDir(); - try { - $templates = $templateDir->getById($templateId); - } catch(\Exception $e) { - $logger->logException($e, ["message" => "GetTemplate: $templateId", "app" => self::$appName]); - return null; - } - - if (empty($templates)) { - $logger->info("Template not found: $templateId", ["app" => self::$appName]); - return null; - } - - return $templates[0]; - } - - /** - * Get type template from mimetype - * - * @param string $mime - mimetype - * - * @return string - */ - public static function GetTypeTemplate($mime) { - switch($mime) { - case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": - return "document"; - case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": - return "spreadsheet"; - case "application/vnd.openxmlformats-officedocument.presentationml.presentation": - return "presentation"; - } - - return ""; - } - - /** - * Check template type - * - * @param string $name - template name - * - * @return bool - */ - public static function IsTemplateType($name) { - $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); - switch($ext) { - case "docx": - case "xlsx": - case "pptx": - return true; - } - - return false; - } - - /** - * Get empty template content - * - * @param string $fileName - target file name - * - * @return string - */ - public static function GetEmptyTemplate($fileName) { - $ext = strtolower("." . pathinfo($fileName, PATHINFO_EXTENSION)); - $lang = \OC::$server->getL10NFactory("")->get("")->getLanguageCode(); - - $templatePath = self::GetEmptyTemplatePath($lang, $ext); - - $template = file_get_contents($templatePath); - return $template; - } - - /** - * Get template path - * - * @param string $lang - language - * @param string $ext - file extension - * - * @return string - */ - public static function GetEmptyTemplatePath($lang, $ext) { - if (!array_key_exists($lang, self::$localPath)) { - $lang = "en"; - } - - return dirname(__DIR__) . DIRECTORY_SEPARATOR . "assets" . DIRECTORY_SEPARATOR . self::$localPath[$lang] . DIRECTORY_SEPARATOR . "new" . $ext; - } - - /** - * Mapping local path to templates - * - * @var Array - */ - private static $localPath = [ - "az" => "az-Latn-AZ", - "bg_BG" => "bg-BG", - "cs" => "cs-CZ", - "de" => "de-DE", - "de_DE" => "de-DE", - "el" => "el-GR", - "en" => "en-US", - "en_GB" => "en-GB", - "es" => "es-ES", - "eu" => "eu-ES", - "fr" => "fr-FR", - "gl" => "gl-ES", - "it" => "it-IT", - "ja" => "ja-JP", - "ko" => "ko-KR", - "lv" => "lv-LV", - "nl" => "nl-NL", - "pl" => "pl-PL", - "pt_BR" => "pt-BR", - "pt_PT" => "pt-PT", - "ru" => "ru-RU", - "si_LK" => "si-LK", - "sk_SK" => "sk-SK", - "sv" => "sv-SE", - "tr" => "tr-TR", - "uk" => "uk-UA", - "vi" => "vi-VN", - "zh_CN" => "zh-CN", - "zh_TW" => "zh-TW" - ]; -} \ No newline at end of file + /** + * Application name + * + * @var string + */ + private static $appName = "onlyoffice"; + + /** + * Template folder name + * + * @var string + */ + private static $templateFolderName = "template"; + + /** + * Get global template directory + * + * @return Folder + */ + public static function getGlobalTemplateDir() { + $dirPath = self::$appName . "/" . self::$templateFolderName; + + $rootFolder = \OC::$server->getRootFolder(); + $templateDir = null; + try { + $templateDir = $rootFolder->get($dirPath); + } catch (NotFoundException $e) { + $templateDir = $rootFolder->newFolder($dirPath); + } + + return $templateDir; + } + + /** + * Get global templates + * + * @param string $mimetype - mimetype of the template + * + * @return array + */ + public static function getGlobalTemplates($mimetype = null) { + $templateDir = self::getGlobalTemplateDir(); + + $templatesList = $templateDir->getDirectoryListing(); + if (!empty($mimetype) + && \is_array($templatesList) && \count($templatesList) > 0 + ) { + $templatesList = $templateDir->searchByMime($mimetype); + } + + return $templatesList; + } + + /** + * Get template file + * + * @param string $templateId - identifier file template + * + * @return File + */ + public static function getTemplate($templateId) { + $logger = \OC::$server->getLogger(); + + $templateDir = self::getGlobalTemplateDir(); + try { + $templates = $templateDir->getById($templateId); + } catch(\Exception $e) { + $logger->logException($e, ["message" => "getTemplate: $templateId", "app" => self::$appName]); + return null; + } + + if (empty($templates)) { + $logger->info("Template not found: $templateId", ["app" => self::$appName]); + return null; + } + + return $templates[0]; + } + + /** + * Get type template from mimetype + * + * @param string $mime - mimetype + * + * @return string + */ + public static function getTypeTemplate($mime) { + switch($mime) { + case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": + return "document"; + case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": + return "spreadsheet"; + case "application/vnd.openxmlformats-officedocument.presentationml.presentation": + return "presentation"; + } + + return ""; + } + + /** + * Check template type + * + * @param string $name - template name + * + * @return bool + */ + public static function isTemplateType($name) { + $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); + switch($ext) { + case "docx": + case "xlsx": + case "pptx": + return true; + } + + return false; + } + + /** + * Get empty template content + * + * @param string $fileName - target file name + * + * @return string + */ + public static function getEmptyTemplate($fileName) { + $ext = strtolower("." . pathinfo($fileName, PATHINFO_EXTENSION)); + $lang = \OC::$server->getL10NFactory("")->get("")->getLanguageCode(); + + $templatePath = self::getEmptyTemplatePath($lang, $ext); + + $template = file_get_contents($templatePath); + return $template; + } + + /** + * Get template path + * + * @param string $lang - language + * @param string $ext - file extension + * + * @return string + */ + public static function getEmptyTemplatePath($lang, $ext) { + if (!\array_key_exists($lang, self::$localPath)) { + $lang = "en"; + } + + return \dirname(__DIR__) . DIRECTORY_SEPARATOR . "assets" . DIRECTORY_SEPARATOR . "document-templates" . DIRECTORY_SEPARATOR . self::$localPath[$lang] . DIRECTORY_SEPARATOR . "new" . $ext; + } + + /** + * Mapping local path to templates + * + * @var Array + */ + private static $localPath = [ + "ar" => "ar-SA", + "az" => "az-Latn-AZ", + "bg_BG" => "bg-BG", + "cs" => "cs-CZ", + "de" => "de-DE", + "de_DE" => "de-DE", + "el" => "el-GR", + "en" => "en-US", + "en_GB" => "en-GB", + "es" => "es-ES", + "eu" => "eu-ES", + "fr" => "fr-FR", + "gl" => "gl-ES", + "it" => "it-IT", + "ja" => "ja-JP", + "ko" => "ko-KR", + "lv" => "lv-LV", + "nl" => "nl-NL", + "pl" => "pl-PL", + "pt_BR" => "pt-BR", + "pt_PT" => "pt-PT", + "ru" => "ru-RU", + "si_LK" => "si-LK", + "sk_SK" => "sk-SK", + "sr" => "sr-Latn-RS", + "sv" => "sv-SE", + "tr" => "tr-TR", + "uk" => "uk-UA", + "vi" => "vi-VN", + "zh_CN" => "zh-CN", + "zh_TW" => "zh-TW" + ]; +} diff --git a/lib/version.php b/lib/version.php index c56a28cb..d95ff1c3 100644 --- a/lib/version.php +++ b/lib/version.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,90 +22,91 @@ use OCP\Files\FileInfo; - /** * Version file * * @package OCA\Onlyoffice */ class Version { - /** - * Time of creation - * - * @var int - * */ - private $timestamp; + /** + * Time of creation + * + * @var int + * */ + private $timestamp; - /** - * Version file - * - * @var int|string - * */ - private $revisionId; + /** + * Version file + * + * @var int|string + * */ + private $revisionId; - /** - * File path - * - * @var string - * */ - private $path; + /** + * File path + * + * @var string + * */ + private $path; - /** - * Source file properties - * - * @var FileInfo - * */ - private $sourceFileInfo; + /** + * Source file properties + * + * @var FileInfo + * */ + private $sourceFileInfo; - /** - * @param int $timestamp - file time stamp - * @param int $revisionId - revision id - * @param FileInfo $sourceFileInfo - source file info - */ - public function __construct(int $timestamp, - int $revisionId, - string $path, - FileInfo $sourceFileInfo - ) { - $this->timestamp = $timestamp; - $this->revisionId = $revisionId; - $this->path = $path; - $this->sourceFileInfo = $sourceFileInfo; - } + /** + * @param int $timestamp - file time stamp + * @param int $revisionId - revision id + * @param string $path - file path + * @param FileInfo $sourceFileInfo - source file info + */ + public function __construct( + int $timestamp, + int $revisionId, + string $path, + FileInfo $sourceFileInfo + ) { + $this->timestamp = $timestamp; + $this->revisionId = $revisionId; + $this->path = $path; + $this->sourceFileInfo = $sourceFileInfo; + } - /** - * Get source file - * - * @return FileInfo - */ - public function getSourceFile() { - return $this->sourceFileInfo; - } + /** + * Get source file + * + * @return FileInfo + */ + public function getSourceFile() { + return $this->sourceFileInfo; + } - /** - * Get version file - * - * @return int|string - */ - public function getRevisionId() { - return $this->revisionId; - } + /** + * Get version file + * + * @return int|string + */ + public function getRevisionId() { + return $this->revisionId; + } - /** - * Get timestamp file - * - * @return int - */ - public function getTimestamp() { - return $this->timestamp; - } + /** + * Get timestamp file + * + * @return int + */ + public function getTimestamp() { + return $this->timestamp; + } - /** - * Get file path - * - * @return string - */ - public function getPath() { - return $this->path; - } + /** + * Get file path + * + * @return string + */ + public function getPath() { + return $this->path; + } } diff --git a/lib/versionmanager.php b/lib/versionmanager.php index 69c266c3..93e06f08 100644 --- a/lib/versionmanager.php +++ b/lib/versionmanager.php @@ -1,7 +1,8 @@ * - * (c) Copyright Ascensio System SIA 2023 + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,154 +37,156 @@ * @package OCA\Onlyoffice */ class VersionManager { - - /** - * Application name - * - * @var string - */ - private $appName; - - /** - * Root folder - * - * @var IRootFolder - */ - private $rootFolder; - - /** - * File versions storage - * - * @var Storage - */ - private $storage; - - /** - * Version manager is available - * - * @var bool - */ - public $available; - - /** - * @param string $AppName - application name - * @param IRootFolder $rootFolder - root folder - */ - public function __construct(string $AppName, IRootFolder $rootFolder) { - $this->appName = $AppName; - $this->rootFolder = $rootFolder; - - if (\OC::$server->getAppManager()->isInstalled("files_versions")) { - try { - $this->storage = \OC::$server->query(Storage::class); - $this->available = true; - } catch (QueryException $e) { - \OC::$server->getLogger()->logException($e, ["message" => "VersionManager init error", "app" => $this->appName]); - } - } - } - - /** - * Get version folder - * - * @param IUser $user - file owner - * - * @return Folder - */ - private function getVersionFolder($user) { - $userRoot = $this->rootFolder->getUserFolder($user->getUID())->getParent(); - try { - $folder = $userRoot->get("files_versions"); - return $folder; - } catch (NotFoundException $e) { - \OC::$server->getLogger()->logException($e, ["message" => "VersionManager: not found user version folder " . $user->getUID(), "app" => $this->appName]); - return null; - } - } - - /** - * Get file version - * - * @param IUser $user - file owner - * @param FileInfo $sourceFile - file - * @param integer $version - file version - * - * @return File - */ - public function getVersionFile($user, $sourceFile, $version) { - $userFolder = $this->rootFolder->getUserFolder($user->getUID()); - $versionsFolder = $this->getVersionFolder($user); - - $file = $versionsFolder->get($userFolder->getRelativePath($sourceFile->getPath()) . ".v" . $version); - return $file; - } - - /** - * Get versions for file - * - * @param IUser $user - file owner - * @param FileInfo $file - file - * - * @return array - */ - public function getVersionsForFile($user, $file) { - $versions = array(); - - $fileId = $file->getId(); - - try{ - $userFolder = $this->rootFolder->getUserFolder($user->getUID()); - $nodes = $userFolder->getById($fileId); - $sourceFile = $nodes[0]; - } catch (\Exception $e) { - \OC::$server->getLogger()->logException($e, ["message" => "VersionManager: $fileId", "app" => $this->appName]); - return $versions; - } - - $owner = $sourceFile->getOwner(); - if ($owner === null) { - return $versions; - } - - $ownerId = $owner->getUID(); - $userFolder = $this->rootFolder->getUserFolder($ownerId); - $sourceFilePath = $userFolder->getRelativePath($sourceFile->getPath()); - $propsVersions = $this->storage->getVersions($ownerId, $sourceFilePath); - - foreach($propsVersions as $propVersion) { - $version = new Version($propVersion["timestamp"], - $propVersion["version"], - $propVersion["path"], - $file); - - array_push($versions, $version); - } - - return $versions; - } - - /** - * Restore version - * - * @param Version $version - version for restore - * - */ - public function rollback($version) { - $sourceFile = $version->getSourceFile(); - - $ownerId = null; - $owner = $sourceFile->getOwner(); - if (!empty($owner)) { - $ownerId = $owner->getUID(); - } - - $path = $version->getPath(); - $revision = $version->getTimestamp(); - - $versionFile = $this->getVersionFile($owner, $sourceFile, $revision); - $versionFileInfo = $versionFile->getFileInfo(); - $versionPath = $versionFileInfo->getInternalPath(); - - $this->storage->restoreVersion($ownerId, $path, $versionPath, $revision); - } + /** + * Application name + * + * @var string + */ + private $appName; + + /** + * Root folder + * + * @var IRootFolder + */ + private $rootFolder; + + /** + * File versions storage + * + * @var Storage + */ + private $storage; + + /** + * Version manager is available + * + * @var bool + */ + public $available; + + /** + * @param string $AppName - application name + * @param IRootFolder $rootFolder - root folder + */ + public function __construct(string $AppName, IRootFolder $rootFolder) { + $this->appName = $AppName; + $this->rootFolder = $rootFolder; + + if (\OC::$server->getAppManager()->isInstalled("files_versions")) { + try { + $this->storage = \OC::$server->query(Storage::class); + $this->available = true; + } catch (QueryException $e) { + \OC::$server->getLogger()->logException($e, ["message" => "VersionManager init error", "app" => $this->appName]); + } + } + } + + /** + * Get version folder + * + * @param IUser $user - file owner + * + * @return Folder + */ + private function getVersionFolder($user) { + $userRoot = $this->rootFolder->getUserFolder($user->getUID())->getParent(); + try { + $folder = $userRoot->get("files_versions"); + return $folder; + } catch (NotFoundException $e) { + \OC::$server->getLogger()->logException($e, ["message" => "VersionManager: not found user version folder " . $user->getUID(), "app" => $this->appName]); + return null; + } + } + + /** + * Get file version + * + * @param IUser $user - file owner + * @param FileInfo $sourceFile - file + * @param integer $version - file version + * + * @return File + */ + public function getVersionFile($user, $sourceFile, $version) { + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $versionsFolder = $this->getVersionFolder($user); + + $file = $versionsFolder->get($userFolder->getRelativePath($sourceFile->getPath()) . ".v" . $version); + return $file; + } + + /** + * Get versions for file + * + * @param IUser $user - file owner + * @param FileInfo $file - file + * + * @return array + */ + public function getVersionsForFile($user, $file) { + $versions = []; + + $fileId = $file->getId(); + + try { + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $nodes = $userFolder->getById($fileId); + $sourceFile = $nodes[0]; + } catch (\Exception $e) { + \OC::$server->getLogger()->logException($e, ["message" => "VersionManager: $fileId", "app" => $this->appName]); + return $versions; + } + + $owner = $sourceFile->getOwner(); + if ($owner === null) { + return $versions; + } + + $ownerId = $owner->getUID(); + $userFolder = $this->rootFolder->getUserFolder($ownerId); + $sourceFilePath = $userFolder->getRelativePath($sourceFile->getPath()); + $propsVersions = $this->storage->getVersions($ownerId, $sourceFilePath); + + foreach ($propsVersions as $propVersion) { + $version = new Version( + $propVersion["timestamp"], + $propVersion["version"], + $propVersion["path"], + $file + ); + + array_push($versions, $version); + } + + return $versions; + } + + /** + * Restore version + * + * @param Version $version - version for restore + * + * @return void + */ + public function rollback($version) { + $sourceFile = $version->getSourceFile(); + + $ownerId = null; + $owner = $sourceFile->getOwner(); + if (!empty($owner)) { + $ownerId = $owner->getUID(); + } + + $path = $version->getPath(); + $revision = $version->getTimestamp(); + + $versionFile = $this->getVersionFile($owner, $sourceFile, $revision); + $versionFileInfo = $versionFile->getFileInfo(); + $versionPath = $versionFileInfo->getInternalPath(); + + $this->storage->restoreVersion($ownerId, $path, $versionPath, $revision); + } } diff --git a/ruleset.xml b/ruleset.xml new file mode 100644 index 00000000..d9d7eecc --- /dev/null +++ b/ruleset.xml @@ -0,0 +1,74 @@ + + + ownCloud coding standard + + + + */templates/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/settings.php b/settings.php index 0a613f16..e559e518 100644 --- a/settings.php +++ b/settings.php @@ -1,7 +1,8 @@ + * + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/templates/editor.php b/templates/editor.php index 936fcb80..a6e4a997 100644 --- a/templates/editor.php +++ b/templates/editor.php @@ -1,7 +1,8 @@ + * + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,25 +18,24 @@ * */ - style("onlyoffice", "editor"); - script("onlyoffice", "desktop"); - script("onlyoffice", "editor"); +style("onlyoffice", "editor"); +script("onlyoffice", "desktop"); +script("onlyoffice", "editor"); ?>
+
" + data-path="" + data-sharetoken="" + data-version="" + data-template="" + data-anchor="" + data-inframe="">
-
" - data-path="" - data-sharetoken="" - data-version="" - data-template="" - data-anchor="" - data-inframe="">
- - - - + + +
diff --git a/templates/settings.php b/templates/settings.php index 94a840cc..46d46d21 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -1,7 +1,8 @@ + * + * (c) Copyright Ascensio System SIA 2024 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,327 +18,324 @@ * */ - style("onlyoffice", "settings"); - style("onlyoffice", "template"); - script("onlyoffice", "settings"); - script("onlyoffice", "template"); +style("onlyoffice", "settings"); +style("onlyoffice", "template"); +script("onlyoffice", "settings"); +script("onlyoffice", "template"); ?>
-

- ONLYOFFICE - "> -

- -

t("Server settings")) ?>

- - -

- t("Encryption App is enabled, the application cannot work. You can continue working with the application if you enable master key.")) ?> - -

- - -
-

t("ONLYOFFICE Docs Location specifies the address of the server with the document services installed. Please change the '' for the server address in the below line.")) ?>

- -

t("ONLYOFFICE Docs address")) ?>

-

" placeholder="https:///" type="text">

- -

- checked="checked" /> - -

- -

t("Secret key (leave blank to disable)")) ?>

-

- " placeholder="secret" type="password" /> - - -

- -

- - t("Advanced server settings")) ?> - - -

-
-

t("Authorization header (leave blank to use default header)")) ?>

-

" placeholder="Authorization" type="text">

- -

t("ONLYOFFICE Docs address for internal requests from the server")) ?>

-

" placeholder="https:///" type="text">

- -

t("Server address for internal requests from ONLYOFFICE Docs")) ?>

-

" placeholder="" type="text">

-
-
-
- -
- - -
- checked="checked" - disabled="disabled" /> - - -
- - t("This is a public test server, please do not use it for private sensitive data. The server will be available during a 30-day period.")) ?> - - t("The 30-day test period is over, you can no longer connect to demo ONLYOFFICE Docs server.")) ?> - -
- -
-
-
- -
-
-
-
-

ONLYOFFICE Docs Cloud

-

t("Easily launch the editors in the cloud without downloading and installation")) ?>

-
- -
-
- -
- -
onlyoffice-hide"> -
-

t("Common settings")) ?>

- -

- 0) { ?>checked="checked" /> - -
- " - style="display: block; margin-top: 6px; width: 265px;" /> -

- -

- checked="checked" /> - -

- -

- checked="checked" /> - -

- -

- checked="checked" /> - - -

- -

t("The default application for opening the format")) ?>

-
- $setting) { ?> - -
- checked="checked" /> - -
- - -
- -

- t("Open the file for editing (due to format restrictions, the data might be lost when saving to the formats from the list below)")) ?> - "> -

-
- $setting) { ?> - -
- checked="checked" /> - -
- - -
-
- -

- t("Editor customization settings")) ?> - "> -

- -

- checked="checked" - disabled="disabled"/> - - -
- t("This feature is unavailable due to encryption settings.")) ?> - -

- -

- t("The customization section allows personalizing the editor interface")) ?> -

- -

- checked="checked" /> - -

- -

- checked="checked" /> - -

- -

- checked="checked" /> - -

- -

- checked="checked" /> - -

- -

- checked="checked" /> - -

- -

- t("Review mode for viewing")) ?> -

-
-
- checked="checked" /> - -
-
- checked="checked" /> - -
-
- checked="checked" /> - -
-
- -

- t("Default editor theme")) ?> -

-
-
- checked="checked" /> - -
-
- checked="checked" /> - -
-
- checked="checked" /> - -
-
- -
-

- -

- t("Common templates")) ?> - - -

-
    - -
  • class="onlyoffice-template-item" > - .svg" /> -

    - - -
  • - -
-
- -

t("Security")) ?>

- -

- checked="checked" /> - -

- -

- checked="checked" /> - -

- -

- t("Enable document protection for")) ?> -

-
-
- checked="checked" /> - -
-
- checked="checked" /> - -
-
- -
-

-
+

+ ONLYOFFICE + "> +

+ +

t("Server settings")) ?>

+ + +

+ t("Encryption App is enabled, the application cannot work. You can continue working with the application if you enable master key.")) ?> + +

+ +
+

t("ONLYOFFICE Docs Location specifies the address of the server with the document services installed. Please change the '' for the server address in the below line.")) ?>

+

t("ONLYOFFICE Docs address")) ?>

+

" placeholder="https:///" type="text">

+ +

+ checked="checked" /> + +

+ +

t("Secret key (leave blank to disable)")) ?>

+

+ " placeholder="secret" type="password" /> + + +

+ +

+ + t("Advanced server settings")) ?> + + +

+
+

t("Authorization header (leave blank to use default header)")) ?>

+

" placeholder="Authorization" type="text">

+

t("ONLYOFFICE Docs address for internal requests from the server")) ?>

+

" placeholder="https:///" type="text">

+

t("Server address for internal requests from ONLYOFFICE Docs")) ?>

+

" placeholder="" type="text">

+
+
+
+ +
+ +
+ checked="checked" + disabled="disabled" /> + +
+ + t("This is a public test server, please do not use it for private sensitive data. The server will be available during a 30-day period.")) ?> + + t("The 30-day test period is over, you can no longer connect to demo ONLYOFFICE Docs server.")) ?> + +
+ +
+
+
+
+
+
+
+

t("ONLYOFFICE Docs Cloud")) ?>

+

t("Easily launch the editors in the cloud without downloading and installation")) ?>

+
+ +
+
+
+ +
onlyoffice-hide"> +
+

t("Common settings")) ?>

+

+ 0) { ?>checked="checked" /> + +
+ " + style="display: block; margin-top: 6px; width: 265px;" /> +

+ +

+ checked="checked" /> + +

+ +

+ checked="checked" /> + +

+ +

+ checked="checked" /> + + +

+ +

+ checked="checked" /> + +

+ +

t("The default application for opening the format")) ?>

+
+ $setting) { ?> + +
+ checked="checked" /> + +
+ + +
+ +

+ t("Open the file for editing (due to format restrictions, the data might be lost when saving to the formats from the list below)")) ?> + "> +

+
+ $setting) { ?> + +
+ checked="checked" /> + +
+ + +
+
+ +

+ t("Editor customization settings")) ?> + "> +

+ +

+ checked="checked" + disabled="disabled"/> + + +
+ t("This feature is unavailable due to encryption settings.")) ?> + +

+ +

+ t("The customization section allows personalizing the editor interface")) ?> +

+ +

+ checked="checked" /> + +

+ +

+ checked="checked" /> + +

+ +

+ checked="checked" /> + +

+ +

+ checked="checked" /> + +

+ +

+ checked="checked" /> + +

+ +

+ t("Review mode for viewing")) ?> +

+
+
+ checked="checked" /> + +
+
+ checked="checked" /> + +
+
+ checked="checked" /> + +
+
+ +

+ t("Default editor theme")) ?> +

+
+
+ checked="checked" /> + +
+
+ checked="checked" /> + +
+
+ checked="checked" /> + +
+
+ +
+

+ +

+ t("Common templates")) ?> + + +

+
    + +
  • class="onlyoffice-template-item" > + .svg" /> +

    + + +
  • + +
+
+ +

t("Security")) ?>

+ +

+ checked="checked" /> + +

+ +

+ checked="checked" /> + +

+ +

+ t("Enable document protection for")) ?> +

+
+
+ checked="checked" /> + +
+
+ checked="checked" /> + +
+
+ +
+

+