From 58eabc0191d5befca503dbb49a8787bd5883ea54 Mon Sep 17 00:00:00 2001 From: Yannick Warnier Date: Fri, 24 May 2024 02:56:48 +0200 Subject: [PATCH] Plugin: OnlyOffice: Add missing files --- plugin/onlyoffice/lib/documentService.php | 511 ++++++++++++++++++ .../lib/onlyofficeSettingsFormBuilder.php | 145 +++++ 2 files changed, 656 insertions(+) create mode 100644 plugin/onlyoffice/lib/documentService.php create mode 100644 plugin/onlyoffice/lib/onlyofficeSettingsFormBuilder.php diff --git a/plugin/onlyoffice/lib/documentService.php b/plugin/onlyoffice/lib/documentService.php new file mode 100644 index 0000000000..57324ec458 --- /dev/null +++ b/plugin/onlyoffice/lib/documentService.php @@ -0,0 +1,511 @@ +plugin = $plugin; + $this->newSettings = $newSettings; + } + + + /** + * 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 = []) { + if (substr($url, 0, strlen('https')) === 'https') { + $opts['verify'] = false; + } + if (!array_key_exists('timeout', $opts)) { + $opts['timeout'] = 60; + } + + $curl_info = [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => '', + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => $opts['timeout'], + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_POSTFIELDS => $opts['body'], + CURLOPT_HTTPHEADER => $opts['headers'], + ]; + + if ($opts == []) { + unset($curl_info[CURLOPT_POSTFIELDS]); + } + + $curl = curl_init(); + curl_setopt_array($curl, $curl_info); + $response = curl_exec($curl); + curl_close($curl); + + return $response; + } + + /** + * Generate an error code table of convertion + * + * @param int $errorCode - Error code + * + * @throws Exception + */ + private function processConvServResponceError($errorCode) { + $errorMessageTemplate = $this->plugin->get_lang('docServiceError'); + $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); + } + + /** + * Generate an error code table of command + * + * @param string $errorCode - Error code + * + * @throws Exception + */ + private function processCommandServResponceError($errorCode) { + $errorMessageTemplate = $this->plugin->get_lang('docServiceError'); + $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); + } + + /** + * Create temporary file for convert service testing + * + * @return array + */ + private function createTempFile() { + $fileUrl = null; + $fileName = 'convert.docx'; + $fileExt = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + $baseName = strtolower(pathinfo($fileName, PATHINFO_FILENAME)); + $templatePath = TemplateManager::getEmptyTemplate($fileExt); + $folderPath = api_get_path(SYS_PLUGIN_PATH).$this->plugin->getPluginName(); + $filePath = $folderPath . '/' . $fileName; + + if ($fp = @fopen($filePath, 'w')) { + $content = file_get_contents($templatePath); + fputs($fp, $content); + fclose($fp); + chmod($filePath, api_get_permissions_for_new_files()); + $fileUrl = api_get_path(WEB_PLUGIN_PATH).$this->plugin->getPluginName().'/'.$fileName; + } + + return [ + 'fileUrl' => $fileUrl, + 'filePath' => $filePath + ]; + } + + + /** + * 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 + * @param string $region - Region + * + * @throws Exception + * + * @return array + */ + public function sendRequestToConvertService($document_uri, $from_extension, $to_extension, $document_revision_id, $is_async, $region = null) { + if (!empty($this->getValue('document_server_internal'))) { + $documentServerUrl = $this->getValue('document_server_internal'); + } else { + $documentServerUrl = $this->getValue('document_server_url'); + } + + if (empty($documentServerUrl)) { + throw new \Exception($this->plugin->get_lang('pluginIsNotConfigured')); + } + + $urlToConverter = $documentServerUrl . 'ConvertService.ashx'; + + if (empty($document_revision_id)) { + $document_revision_id = $document_uri; + } + + $document_revision_id = FileUtility::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 + ]; + + if (!is_null($region)) { + $data['region'] = $region; + } + + $opts = [ + 'timeout' => '120', + 'headers' => [ + 'Content-type' => 'application/json' + ], + 'body' => json_encode($data) + ]; + + if (!empty($this->getValue('jwt_secret'))) { + $params = [ + 'payload' => $data + ]; + $token = JWT::encode($params, $this->getValue('jwt_secret'), 'HS256'); + $opts['headers'][$this->getValue('jwt_header')] = 'Bearer ' . $token; + $token = JWT::encode($data, $this->getValue('jwt_secret'), '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->plugin->get_lang('cantReadXml')); + } + + $response_data = simplexml_load_string($response_xml_data); + + if (!$response_data) { + $exc = $this->plugin->get_lang('badResponseErrors'); + foreach(libxml_get_errors() as $error) { + $exc = $exc . '\t' . $error->message; + } + throw new \Exception ($exc); + } + + return $response_data; + } + + /** + * Request health status + * + * @throws Exception + * + * @return bool + */ + public function healthcheckRequest() { + if (!empty($this->getValue('document_server_internal'))) { + $documentServerUrl = $this->getValue('document_server_internal'); + } else { + $documentServerUrl = $this->getValue('document_server_url'); + } + + if (empty($documentServerUrl)) { + throw new \Exception($this->plugin->get_lang('appIsNotConfigured')); + } + + $urlHealthcheck = $documentServerUrl . 'healthcheck'; + $response = $this->request($urlHealthcheck); + return $response === 'true'; + } + + /** + * 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 + * @param string $region - Region + * + * @return string + */ + public function getConvertedUri($document_uri, $from_extension, $to_extension, $document_revision_id, $region = null) { + $responceFromConvertService = $this->sendRequestToConvertService($document_uri, $from_extension, $to_extension, $document_revision_id, false, $region); + $errorElement = $responceFromConvertService->Error; + if ($errorElement->count() > 0) { + $this->processConvServResponceError($errorElement . ''); + } + + $isEndConvert = $responceFromConvertService->EndConvert; + + if ($isEndConvert !== null && strtolower($isEndConvert) === 'true') { + return $responceFromConvertService->FileUrl; + } + + return ''; + } + + /** + * Send command + * + * @param string $method - type of command + * + * @return array + */ + public function commandRequest($method) { + //$documentServerUrl = $this->plugin->getDocumentServerInternalUrl(); + if (!empty($this->getValue('document_server_internal'))) { + $documentServerUrl = $this->getValue('document_server_internal'); + } else { + $documentServerUrl = $this->getValue('document_server_url'); + } + + + if (empty($documentServerUrl)) { + throw new \Exception($this->plugin->get_lang('cantReadXml')); + } + + $urlCommand = $documentServerUrl . 'coauthoring/CommandService.ashx'; + $data = [ + 'c' => $method + ]; + $opts = [ + 'headers' => [ + 'Content-type' => 'application/json' + ], + 'body' => json_encode($data) + ]; + + if (!empty($this->getValue('jwt_secret'))) { + $params = [ + 'payload' => $data + ]; + $token = JWT::encode($params, $this->getValue('jwt_secret'), 'HS256'); + $opts['headers'][$this->getValue('jwt_header')] = 'Bearer ' . $token; + + $token = JWT::encode($data, $this->getValue('jwt_secret'), '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; + } + + /** + * Checking document service location + * + * @return array + */ + public function checkDocServiceUrl() { + $version = null; + try { + if (preg_match('/^https:\/\//i', api_get_path(WEB_PATH)) + && preg_match('/^http:\/\//i', $this->getValue('document_server_url'))) { + throw new \Exception($this->plugin->get_lang('mixedContent')); + } + } catch (\Exception $e) { + return [$e->getMessage(), $version]; + } + + try { + $healthcheckResponse = $this->healthcheckRequest(); + + if (!$healthcheckResponse) { + throw new \Exception($this->plugin->get_lang('badHealthcheckStatus')); + } + } catch (\Exception $e) { + return [$e->getMessage(), $version]; + } + + try { + $commandResponse = $this->commandRequest('version'); + + if (empty($commandResponse)) { + throw new \Exception($this->plugin->get_lang('errorOccuredDocService')); + } + + $version = $commandResponse->version; + $versionF = floatval($version); + + if ($versionF > 0.0 && $versionF <= 6.0) { + throw new \Exception($this->plugin->get_lang('notSupportedVersion')); + } + } catch (\Exception $e) { + return [$e->getMessage(), $version]; + } + + $convertedFileUri = null; + + try { + $emptyFile = $this->createTempFile(); + + if ($emptyFile['fileUrl'] !== null) { + if (!empty($this->getValue('storage_url'))) { + $emptyFile['fileUrl'] = str_replace(api_get_path(WEB_PATH), $this->getValue('storage_url'), $emptyFile['fileUrl']); + } + $convertedFileUri = $this->getConvertedUri($emptyFile['fileUrl'], 'docx', 'docx', 'check_' . rand()); + } + + unlink($emptyFile['filePath']); + } catch (\Exception $e) { + if (isset($emptyFile['filePath'])) { + unlink($emptyFile['filePath']); + } + return [$e->getMessage(), $version]; + } + + try { + $this->request($convertedFileUri); + } catch (\Exception $e) { + return [$e->getMessage(), $version]; + } + + return ['', $version]; + } + + /** + * Get setting value (from data base or submited form) + * + * @return string + */ + private function getValue($value) { + $result = null; + + if (!isset($this->newSettings)) { + switch ($value) { + case 'document_server_url': + $result = $this->plugin->getDocumentServerUrl(); + break; + case 'jwt_secret': + $result = $this->plugin->getDocumentServerSecret(); + break; + case 'jwt_header': + $result = $this->plugin->getJwtHeader(); + break; + case 'document_server_internal': + $result = $this->plugin->getDocumentServerInternalUrl(); + break; + case 'storage_url': + $result = $this->plugin->getStorageUrl(); + break; + default: + } + } else { + $result = isset($this->newSettings[$value]) ? (string)$this->newSettings[$value] : null; + if ($value !== 'jwt_secret' && $value !== 'jwt_header') { + if ($result !== null && $result !== "/") { + $result = rtrim($result, "/"); + if (strlen($result) > 0) { + $result = $result . "/"; + } + } + } else if ($value === 'jwt_header' && empty($this->newSettings[$value])) { + $result = 'Authorization'; + } + } + return $result; + } + +} \ No newline at end of file diff --git a/plugin/onlyoffice/lib/onlyofficeSettingsFormBuilder.php b/plugin/onlyoffice/lib/onlyofficeSettingsFormBuilder.php new file mode 100644 index 0000000000..12b10cd866 --- /dev/null +++ b/plugin/onlyoffice/lib/onlyofficeSettingsFormBuilder.php @@ -0,0 +1,145 @@ + $param) { + $tpl->assign($key, $param); + } + } + $parsedTemplate = $tpl->fetch(self::ONLYOFFICE_LAYOUT_DIR.$templateName.'.tpl'); + return $parsedTemplate; + } + + /** + * Display error messahe + * + * @param string $errorMessage - error message + * @param string $location - header location + * + * @return void + */ + private function displayError($errorMessage, $location = null) { + Display::addFlash( + Display::return_message( + $errorMessage, + 'error' + ) + ); + if ($location !== null) { + header('Location: '.$location); + exit; + } + } + + /** + * Build OnlyofficePlugin settings form + * + * @param OnlyofficePlugin $plugin - OnlyofficePlugin + * + * @return FormValidator + */ + public function buildSettingsForm($plugin) { + $demoData = $plugin->getDemoData(); + $plugin_info = $plugin->get_info(); + $message = ''; + $connectDemoCheckbox = $plugin_info['settings_form']->createElement( + 'checkbox', + 'connect_demo', + '', + $plugin->get_lang('connect_demo') + ); + if (!$demoData['available'] === true) { + $message = $plugin->get_lang('demoPeriodIsOver'); + $connectDemoCheckbox->setAttribute('disabled'); + } else { + if ($plugin->useDemo()) { + $message = $plugin->get_lang('demoUsingMessage'); + $connectDemoCheckbox->setChecked(true); + } else { + $message = $plugin->get_lang('demoPrevMessage'); + } + } + $demoServerMessageHtml = Display::return_message( + $message, + 'info' + ); + $bannerTemplate = self::buildTemplate('get_docs_cloud_banner', [ + 'docs_cloud_link' => AppConfig::GetLinkToDocs(), + 'banner_title' => $plugin->get_lang('DocsCloudBannerTitle'), + 'banner_main_text' => $plugin->get_lang('DocsCloudBannerMain'), + 'banner_button_text' => $plugin->get_lang('DocsCloudBannerButton'), + ]); + $plugin_info['settings_form']->insertElementBefore($connectDemoCheckbox, 'submit_button'); + $demoServerMessage = $plugin_info['settings_form']->createElement('html', $demoServerMessageHtml); + $plugin_info['settings_form']->insertElementBefore($demoServerMessage, 'submit_button'); + $banner = $plugin_info['settings_form']->createElement('html', $bannerTemplate); + $plugin_info['settings_form']->insertElementBefore($banner, 'submit_button'); + return $plugin_info['settings_form']; + } + + /** + * Validate OnlyofficePlugin settings form + * + * @param OnlyofficePlugin $plugin - OnlyofficePlugin + * + * @return OnlyofficePlugin + */ + public function validateSettingsForm($plugin) { + $errorMsg = null; + $plugin_info = $plugin->get_info(); + $result = $plugin_info['settings_form']->getSubmitValues(); + if (!$plugin->selectDemo((bool)$result['connect_demo'] === true)) { + $errorMsg = $plugin->get_lang('demoPeriodIsOver'); + self::displayError($errorMsg, $plugin->getConfigLink()); + } + $documentserver = $plugin->getDocumentServerUrl(); + if (!empty($documentserver)) { + if ((bool)$result['connect_demo'] === false) { + $documentService = new DocumentService($plugin, $result); + list ($error, $version) = $documentService->checkDocServiceUrl(); + + if (!empty($error)) { + $errorMsg = $plugin->get_lang('connectionError').'('.$error.')'.(!empty($version) ? '(Version '.$version.')' : ''); + self::displayError($errorMsg); + } + } + } + return $plugin; + } +} \ No newline at end of file