20) { $expected_key = crc32($expected_key); } $key = preg_replace("[^0-9-.a-zA-Z_=]", "_", (string) $expected_key); return substr((string) $key, 0, min([strlen((string) $key), 20])); } /** * 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 * @param bool $toForm - Convert to form */ public function getConvertedUri( string $document_uri, string $from_extension, string $to_extension, string $document_revision_id, string $region = "", bool $toForm = false ): string { $response = $this->sendRequestToConvertService($document_uri, $from_extension, $to_extension, $document_revision_id, false, $region, $toForm); $error = $response["error"] ?? null; if ($error !== null) { $this->processConvServResponceError((int)$error); } return $response["fileUrl"] ?? ""; } /** * 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 * @param bool $toForm - Convert to form * @param array $thumbnail - Settings for the thumbnail * * @return array */ public function sendRequestToConvertService( string $document_uri, string $from_extension, string $to_extension, string $document_revision_id, bool $is_async, string $region = "", bool $toForm = false, array $thumbnail = [], ): array { $documentServerUrl = $this->appConfig->getDocumentServerInternalUrl(); if (empty($documentServerUrl)) { throw new \Exception($this->trans->t("ONLYOFFICE app is not configured. Please contact admin")); } $urlToConverter = $documentServerUrl . "converter"; if (empty($document_revision_id)) { $document_revision_id = $document_uri; } $document_revision_id = self::generateRevisionId($document_revision_id); $urlToConverter = $urlToConverter . "?shardkey=" . $document_revision_id; $from_extension = empty($from_extension) ? pathinfo($document_uri)["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 ($region !== "") { $data["region"] = $region; } if ($this->appConfig->useDemo()) { $data["tenant"] = $this->appConfig->getSystemValue("instanceid", true); } if ($toForm) { $data["pdf"] = [ "form" => true ]; } if (!empty($thumbnail)) { $data['thumbnail'] = $thumbnail; } $opts = [ "timeout" => "120", "headers" => [ "Content-type" => "application/json" ], "body" => json_encode($data) ]; if (!empty($this->appConfig->getDocumentServerSecret())) { $now = time(); $iat = $now; $exp = $now + $this->appConfig->getJwtExpiration() * 60; $params = [ "payload" => $data, "iat" => $iat, "exp" => $exp ]; $token = \Firebase\JWT\JWT::encode($params, $this->appConfig->getDocumentServerSecret(), "HS256"); $opts["headers"][$this->appConfig->jwtHeader()] = "Bearer " . $token; $data["iat"] = $iat; $data["exp"] = $exp; $token = \Firebase\JWT\JWT::encode($data, $this->appConfig->getDocumentServerSecret(), "HS256"); $data["token"] = $token; $opts["body"] = json_encode($data); } $responseJsonData = $this->request($urlToConverter, "post", $opts); $responseData = json_decode($responseJsonData, true); if (json_last_error() !== 0) { $exc = $this->trans->t("Bad Response. JSON error: " . json_last_error_msg()); throw new \Exception($exc); } return $responseData; } /** * Generate an error code table of convertion */ public function processConvServResponceError(int $errorCode): void { $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 */ public function healthcheckRequest(): bool { $documentServerUrl = $this->appConfig->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 */ public function commandRequest(string $method): array { $documentServerUrl = $this->appConfig->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->appConfig->getDocumentServerSecret())) { $now = time(); $iat = $now; $exp = $now + $this->appConfig->getJwtExpiration() * 60; $params = [ "payload" => $data, "iat" => $iat, "exp" => $exp ]; $token = \Firebase\JWT\JWT::encode($params, $this->appConfig->getDocumentServerSecret(), "HS256"); $opts["headers"][$this->appConfig->jwtHeader()] = "Bearer " . $token; $data["iat"] = $iat; $data["exp"] = $exp; $token = \Firebase\JWT\JWT::encode($data, $this->appConfig->getDocumentServerSecret(), "HS256"); $data["token"] = $token; $opts["body"] = json_encode($data); } $response = $this->request($urlCommand, "post", $opts); $data = json_decode($response, true); $this->processCommandServResponceError((int)$data["error"]); return $data; } /** * Generate an error code table of command * * @param int $errorCode - Error code */ public function processCommandServResponceError(int $errorCode): void { $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 correct"; 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 string $method - request method * @param array $opts - request options * * @return string */ public function request(string $url, string $method = "get", array $opts = []) { $httpClientService = \OCP\Server::get(IClientService::class); $client = $httpClientService->newClient(); if (str_starts_with($url, "https") && $this->appConfig->getVerifyPeerOff()) { $opts["verify"] = false; } if (!array_key_exists("timeout", $opts)) { $opts["timeout"] = 60; } $opts['nextcloud'] = [ 'allow_local_address' => true, ]; $response = $method === "post" ? $client->post($url, $opts) : $client->get($url, $opts); return $response->getBody(); } /** * Checking document service location */ public function checkDocServiceUrl(): array { $version = null; try { if (preg_match("/^https:\/\//i", (string) $this->urlGenerator->getAbsoluteURL("/")) && preg_match("/^http:\/\//i", $this->appConfig->getDocumentServerUrl())) { throw new \Exception($this->trans->t("Mixed Active Content is not allowed. HTTPS address for ONLYOFFICE Docs is required.")); } } catch (\Exception $e) { $this->logger->error("Protocol on check error", ['exception' => $e]); return [$e->getMessage(), $version]; } try { $healthcheckResponse = $this->healthcheckRequest(); if (!$healthcheckResponse) { throw new \Exception($this->trans->t("Bad healthcheck status")); } } catch (\Exception $e) { $this->logger->error("healthcheckRequest on check error", ['exception' => $e]); return [$e->getMessage(), $version]; } try { $commandResponse = $this->commandRequest("version"); $this->logger->debug("commandRequest on check: " . json_encode($commandResponse), ["app" => self::$appName]); if (empty($commandResponse) || !array_key_exists("version", $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) { $this->logger->error("commandRequest on check error", ['exception' => $e]); return [$e->getMessage(), $version]; } $convertedFileUri = null; try { $hashUrl = $this->crypt->getHash(["action" => "empty"]); $fileUrl = $this->urlGenerator->linkToRouteAbsolute(self::$appName . ".callback.emptyfile", ["doc" => $hashUrl]); if (!$this->appConfig->useDemo() && !empty($this->appConfig->getStorageUrl())) { $fileUrl = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->appConfig->getStorageUrl(), $fileUrl); } $convertedFileUri = $this->getConvertedUri($fileUrl, "docx", "docx", "check_" . random_int(0, mt_getrandmax())); if (strcmp($convertedFileUri, (string) $fileUrl) === 0) { $this->logger->debug("getConvertedUri skipped", ["app" => self::$appName]); } } catch (\Exception $e) { $this->logger->error("getConvertedUri on check error", ['exception' => $e]); return [$e->getMessage(), $version]; } try { $this->request($convertedFileUri); } catch (\Exception $e) { $this->logger->error("Request converted file on check error", ['exception' => $e]); return [$e->getMessage(), $version]; } return ["", $version]; } }